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/14 10:52:44 UTC

[01/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Repository: incubator-freemarker
Updated Branches:
  refs/heads/3 d373a34d3 -> 3fd560629


http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/ObjectBuilderSettingsTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/ObjectBuilderSettingsTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/ObjectBuilderSettingsTest.java
new file mode 100644
index 0000000..49534fa
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/ObjectBuilderSettingsTest.java
@@ -0,0 +1,1499 @@
+/*
+ * 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.IOException;
+import java.io.Serializable;
+import java.io.Writer;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.charset.UnsupportedCharsetException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.TimeZone;
+
+import org.apache.freemarker.core.arithmetic.ArithmeticEngine;
+import org.apache.freemarker.core.arithmetic.impl.BigDecimalArithmeticEngine;
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.impl.DefaultObjectWrapper;
+import org.apache.freemarker.core.templateresolver.CacheStorage;
+import org.apache.freemarker.core.templateresolver.TemplateLoader;
+import org.apache.freemarker.core.templateresolver.TemplateLoaderSession;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingResult;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingSource;
+import org.apache.freemarker.core.templateresolver.impl.MruCacheStorage;
+import org.apache.freemarker.core.userpkg.PublicWithMixedConstructors;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+@SuppressWarnings("boxing")
+public class ObjectBuilderSettingsTest {
+
+    @Test
+    public void newInstanceTest() throws Exception {
+        {
+            TestBean1 res = (TestBean1) _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean1",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            assertEquals(4f, res.f, 0);
+            assertFalse(res.b);
+        }
+        
+        {
+            TestBean1 res = (TestBean1) _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean1()",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            assertEquals(4f, res.f, 0);
+            assertFalse(res.b);
+        }
+        
+        {
+            TestBean1 res = (TestBean1) _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean1(1.5, -20, 8589934592, true)",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            assertEquals(1.5f, res.f, 0);
+            assertEquals(Integer.valueOf(-20), res.i);
+            assertEquals(8589934592l, res.l);
+            assertTrue(res.b);
+        }
+        
+        {
+            TestBean1 res = (TestBean1) _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean1(1, true)",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            assertEquals(2f, res.f, 0);
+            assertEquals(Integer.valueOf(1), res.i);
+            assertEquals(2l, res.l);
+            assertTrue(res.b);
+        }
+        
+        {
+            TestBean1 res = (TestBean1) _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean1(11, 22)",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            assertEquals(3f, res.f, 0);
+            assertEquals(Integer.valueOf(11), res.i);
+            assertEquals(22l, res.l);
+            assertFalse(res.b);
+        }
+        
+        {
+            TestBean1 res = (TestBean1) _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean1(p1 = 1, p2 = 2, p3 = true, p4 = 's')",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            assertEquals(4f, res.f, 0);
+            assertFalse(res.b);
+            assertEquals(1d, res.getP1(), 0);
+            assertEquals(2, res.getP2());
+            assertTrue(res.isP3());
+            assertEquals("s", res.getP4());
+        }
+
+        {
+            TestBean1 res = (TestBean1) _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean1("
+                    + "null, 2, p1 = 1, p2 = 2, p3 = false, p4 = null)",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            assertNull(res.i);
+            assertEquals(2, res.l, 0);
+            assertEquals(3f, res.f, 0);
+            assertFalse(res.b);
+            assertEquals(1d, res.getP1(), 0);
+            assertEquals(2, res.getP2());
+            assertFalse(res.isP3());
+            assertNull(res.getP4());
+        }
+        
+        {
+            // Deliberately odd spacings
+            TestBean1 res = (TestBean1) _ObjectBuilderSettingEvaluator.eval(
+                    "\t\torg.apache.freemarker .core  . \n"
+                    + "\tObjectBuilderSettingsTest$TestBean1(\n\r\tp1=1\n,p2=2,p3=true,p4='s'  )",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            assertEquals(4f, res.f, 0);
+            assertFalse(res.b);
+            assertEquals(1d, res.getP1(), 0);
+            assertEquals(2, res.getP2());
+            assertTrue(res.isP3());
+            assertEquals("s", res.getP4());
+        }
+        
+        {
+            TestBean1 res = (TestBean1) _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean1(1, true, p2 = 2)",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            assertEquals(2f, res.f, 0);
+            assertEquals(Integer.valueOf(1), res.i);
+            assertEquals(2l, res.l);
+            assertTrue(res.b);
+            assertEquals(0d, res.getP1(), 0);
+            assertEquals(2, res.getP2());
+            assertFalse(res.isP3());
+        }
+    }
+    
+    @Test
+    public void builderTest() throws Exception {
+        {
+            // `()`-les syntax:
+            TestBean2 res = (TestBean2) _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean2",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            assertTrue(res.built);
+            assertEquals(0, res.x);
+        }
+        
+        {
+            TestBean2 res = (TestBean2) _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean2()",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            assertTrue(res.built);
+            assertEquals(0, res.x);
+        }
+        
+        {
+            TestBean2 res = (TestBean2) _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean2(x = 1)",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            assertTrue(res.built);
+            assertEquals(1, res.x);
+        }
+        
+        {
+            TestBean2 res = (TestBean2) _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean2(1)",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            assertTrue(res.built);
+            assertEquals(1, res.x);
+        }
+    }
+
+    @Test
+    public void staticInstanceTest() throws Exception {
+        // ()-les syntax:
+        {
+            TestBean5 res = (TestBean5) _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean5",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            assertEquals(0, res.i);
+            assertEquals(0, res.x);
+            assertSame(TestBean5.INSTANCE, res); //!
+        }
+        
+        {
+            TestBean5 res = (TestBean5) _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean5()",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            assertEquals(0, res.i);
+            assertEquals(0, res.x);
+            assertSame(TestBean5.INSTANCE, res); //!
+        }
+        
+        {
+            TestBean5 res = (TestBean5) _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean5(x = 1)",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            assertEquals(0, res.i);
+            assertEquals(1, res.x);
+            assertNotSame(TestBean5.INSTANCE, res);
+        }
+
+        {
+            TestBean5 res = (TestBean5) _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean5(1)",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            assertEquals(1, res.i);
+            assertEquals(0, res.x);
+            assertNotSame(TestBean5.INSTANCE, res);
+        }
+    }
+
+    @Test
+    public void stringLiteralsTest() throws Exception {
+        {
+            TestBean4 res = (TestBean4) _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean4(\"\", '', s3 = r\"\", s4 = r'')",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            assertEquals("", res.getS1());
+            assertEquals("", res.getS2());
+            assertEquals("", res.getS3());
+            assertEquals("", res.getS4());
+        }
+        
+        {
+            TestBean4 res = (TestBean4) _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean4(\"a\", 'b', s3 = r\"c\", s4 = r'd')",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            assertEquals("a", res.getS1());
+            assertEquals("b", res.getS2());
+            assertEquals("c", res.getS3());
+            assertEquals("d", res.getS4());
+        }
+        
+        {
+            TestBean4 res = (TestBean4) _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean4(\"a'A\", 'b\"B', s3 = r\"c'C\", s4 = r'd\"D')",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            assertEquals("a'A", res.getS1());
+            assertEquals("b\"B", res.getS2());
+            assertEquals("c'C", res.getS3());
+            assertEquals("d\"D", res.getS4());
+        }
+        
+        {
+            TestBean4 res = (TestBean4) _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean4("
+                    + "\"a\\nA\\\"a\\\\A\", 'a\\nA\\'a\\\\A', s3 = r\"a\\n\\A\", s4 = r'a\\n\\A')",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            assertEquals("a\nA\"a\\A", res.getS1());
+            assertEquals("a\nA'a\\A", res.getS2());
+            assertEquals("a\\n\\A", res.getS3());
+            assertEquals("a\\n\\A", res.getS4());
+        }
+    }
+
+    @Test
+    public void nestedBuilderTest() throws Exception {
+        {
+            TestBean6 res = (TestBean6) _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean6("
+                    + "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean1(11, 22, p4 = 'foo'),"
+                    + "1,"
+                    + "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean2(11),"
+                    + "y=2,"
+                    + "b3=org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean2(x = 22)"
+                    + ")",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            assertEquals(Integer.valueOf(11), res.b1.i);
+            assertEquals(22, res.b1.l);
+            assertEquals("foo", res.b1.p4);
+            assertEquals(1, res.x);
+            assertEquals(11, res.b2.x);
+            assertEquals(2, res.y);
+            assertEquals(22, res.b3.x);
+            assertNull(res.b4);
+        }
+        
+        {
+            TestBean6 res = (TestBean6) _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean6("
+                    + "null,"
+                    + "-1,"
+                    + "null,"
+                    + "b4=org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean6("
+                    + "   org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean1(11, 22, p4 = 'foo'),"
+                    + "   1,"
+                    + "   org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean2(11),"
+                    + "   y=2,"
+                    + "   b3=org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean2(x = 22)"
+                    + "),"
+                    + "y=2"
+                    + ")",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            assertNull(res.b1);
+            assertEquals(-1, res.x);
+            assertNull(res.b2);
+            assertEquals(2, res.y);
+            assertEquals(Integer.valueOf(11), res.b4.b1.i);
+            assertEquals(22, res.b4.b1.l);
+            assertEquals("foo", res.b4.b1.p4);
+            assertEquals(1, res.b4.x);
+            assertEquals(11, res.b4.b2.x);
+            assertEquals(2, res.b4.y);
+            assertEquals(22, res.b4.b3.x);
+            assertNull(res.b4.b4);
+        }
+    }
+
+    @Test
+    public void defaultObjectWrapperTest() throws Exception {
+        DefaultObjectWrapper ow = (DefaultObjectWrapper) _ObjectBuilderSettingEvaluator.eval(
+                "DefaultObjectWrapper(3.0.0)",
+                ObjectWrapper.class, false, _SettingEvaluationEnvironment.getCurrent());
+        assertEquals(Configuration.VERSION_3_0_0, ow.getIncompatibleImprovements());
+        assertFalse(ow.isExposeFields());
+    }
+
+    @Test
+    public void defaultObjectWrapperTest2() throws Exception {
+        DefaultObjectWrapper ow = (DefaultObjectWrapper) _ObjectBuilderSettingEvaluator.eval(
+                "DefaultObjectWrapper(3.0.0, exposeFields=true)",
+                ObjectWrapper.class, false, _SettingEvaluationEnvironment.getCurrent());
+        assertEquals(Configuration.VERSION_3_0_0, ow.getIncompatibleImprovements());
+        assertTrue(ow.isExposeFields());
+    }
+
+    @Test
+    public void configurationPropertiesTest() throws Exception {
+        final Configuration.Builder cfgB = new Configuration.Builder(Configuration.getVersion());
+        
+        {
+            Properties props = new Properties();
+            props.setProperty(MutableProcessingConfiguration.OBJECT_WRAPPER_KEY,
+                    "org.apache.freemarker.core.model.impl.DefaultObjectWrapper(3.0.0)");
+            props.setProperty(MutableProcessingConfiguration.ARITHMETIC_ENGINE_KEY,
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$DummyArithmeticEngine");
+            props.setProperty(MutableProcessingConfiguration.TEMPLATE_EXCEPTION_HANDLER_KEY,
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$DummyTemplateExceptionHandler");
+            props.setProperty(Configuration.ExtendableBuilder.CACHE_STORAGE_KEY,
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$DummyCacheStorage()");
+            props.setProperty(MutableProcessingConfiguration.NEW_BUILTIN_CLASS_RESOLVER_KEY,
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$DummyNewBuiltinClassResolver()");
+            props.setProperty(Configuration.ExtendableBuilder.SOURCE_ENCODING_KEY, "utf-8");
+            props.setProperty(Configuration.ExtendableBuilder.TEMPLATE_LOADER_KEY,
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$DummyTemplateLoader()");
+            cfgB.setSettings(props);
+            assertEquals(DefaultObjectWrapper.class, cfgB.getObjectWrapper().getClass());
+            assertEquals(
+                    Configuration.VERSION_3_0_0, ((DefaultObjectWrapper) cfgB.getObjectWrapper()).getIncompatibleImprovements());
+            assertEquals(DummyArithmeticEngine.class, cfgB.getArithmeticEngine().getClass());
+            assertEquals(DummyTemplateExceptionHandler.class, cfgB.getTemplateExceptionHandler().getClass());
+            assertEquals(DummyCacheStorage.class, cfgB.getCacheStorage().getClass());
+            assertEquals(DummyNewBuiltinClassResolver.class, cfgB.getNewBuiltinClassResolver().getClass());
+            assertEquals(DummyTemplateLoader.class, cfgB.getTemplateLoader().getClass());
+            assertEquals(StandardCharsets.UTF_8, cfgB.getSourceEncoding());
+        }
+        
+        {
+            Properties props = new Properties();
+            props.setProperty(MutableProcessingConfiguration.OBJECT_WRAPPER_KEY, "defAult");
+            props.setProperty(MutableProcessingConfiguration.ARITHMETIC_ENGINE_KEY,
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$DummyArithmeticEngine(x = 1)");
+            props.setProperty(MutableProcessingConfiguration.TEMPLATE_EXCEPTION_HANDLER_KEY,
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$DummyTemplateExceptionHandler(x = 1)");
+            props.setProperty(Configuration.ExtendableBuilder.CACHE_STORAGE_KEY,
+                    "soft: 500, strong: 100");
+            props.setProperty(MutableProcessingConfiguration.NEW_BUILTIN_CLASS_RESOLVER_KEY,
+                    "allows_nothing");
+            cfgB.setSettings(props);
+            assertEquals(DefaultObjectWrapper.class, cfgB.getObjectWrapper().getClass());
+            assertEquals(1, ((DummyArithmeticEngine) cfgB.getArithmeticEngine()).getX());
+            assertEquals(1, ((DummyTemplateExceptionHandler) cfgB.getTemplateExceptionHandler()).getX());
+            assertEquals(Configuration.VERSION_3_0_0,
+                    ((DefaultObjectWrapper) cfgB.getObjectWrapper()).getIncompatibleImprovements());
+            assertEquals(500, ((MruCacheStorage) cfgB.getCacheStorage()).getSoftSizeLimit());
+            assertEquals(TemplateClassResolver.ALLOWS_NOTHING_RESOLVER, cfgB.getNewBuiltinClassResolver());
+            assertEquals(StandardCharsets.UTF_8, cfgB.getSourceEncoding());
+        }
+
+        {
+            Properties props = new Properties();
+            props.setProperty(MutableProcessingConfiguration.OBJECT_WRAPPER_KEY, "Default");
+            props.setProperty(MutableProcessingConfiguration.ARITHMETIC_ENGINE_KEY, "bigdecimal");
+            props.setProperty(MutableProcessingConfiguration.TEMPLATE_EXCEPTION_HANDLER_KEY, "rethrow");
+            cfgB.setSettings(props);
+            assertEquals(DefaultObjectWrapper.class, cfgB.getObjectWrapper().getClass());
+            assertSame(BigDecimalArithmeticEngine.INSTANCE, cfgB.getArithmeticEngine());
+            assertSame(TemplateExceptionHandler.RETHROW_HANDLER, cfgB.getTemplateExceptionHandler());
+            assertEquals(Configuration.VERSION_3_0_0,
+                    ((DefaultObjectWrapper) cfgB.getObjectWrapper()).getIncompatibleImprovements());
+        }
+        
+        {
+            Properties props = new Properties();
+            props.setProperty(MutableProcessingConfiguration.OBJECT_WRAPPER_KEY, "DefaultObjectWrapper(3.0.0)");
+            cfgB.setSettings(props);
+            assertEquals(DefaultObjectWrapper.class, cfgB.getObjectWrapper().getClass());
+            assertEquals(
+                    Configuration.VERSION_3_0_0,
+                    ((DefaultObjectWrapper) cfgB.getObjectWrapper()).getIncompatibleImprovements());
+        }
+    }
+    
+    @Test
+    public void timeZoneTest() throws _ObjectBuilderSettingEvaluationException, ClassNotFoundException,
+    InstantiationException, IllegalAccessException {
+        for (String timeZoneId : new String[] { "GMT+01", "GMT", "UTC" }) {
+            TestBean8 result = (TestBean8) _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean8(timeZone=TimeZone('"
+                    + timeZoneId + "'))",
+                    TestBean8.class, false, new _SettingEvaluationEnvironment());
+            assertEquals(TimeZone.getTimeZone(timeZoneId), result.getTimeZone());
+        }
+        
+        try {
+            _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean8(timeZone=TimeZone('foobar'))",
+                    TestBean8.class, false, new _SettingEvaluationEnvironment());
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertThat(e.getCause().getMessage(),
+                    allOf(containsStringIgnoringCase("unrecognized"), containsString("foobar")));
+        }
+    }
+
+    @Test
+    public void charsetTest() throws _ObjectBuilderSettingEvaluationException, ClassNotFoundException,
+            InstantiationException, IllegalAccessException {
+        for (String timeZoneId : new String[] { "uTf-8", "GMT", "UTC" }) {
+            TestBean8 result = (TestBean8) _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean8(charset=Charset('iso-8859-1'))",
+                    TestBean8.class, false, new _SettingEvaluationEnvironment());
+            assertEquals(StandardCharsets.ISO_8859_1, result.getCharset());
+        }
+
+        try {
+            _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean8(charset=Charset('noSuchCS'))",
+                    TestBean8.class, false, new _SettingEvaluationEnvironment());
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertThat(e.getCause(), instanceOf(UnsupportedCharsetException.class));
+        }
+    }
+
+    @Test
+    public void configureBeanTest() throws Exception {
+        final TestBean7 bean = new TestBean7();
+        final String src = "a/b(s='foo', x=1, b=true), bar";
+        int nextPos = _ObjectBuilderSettingEvaluator.configureBean(src, src.indexOf('(') + 1, bean,
+                _SettingEvaluationEnvironment.getCurrent());
+        assertEquals("foo", bean.getS());
+        assertEquals(1, bean.getX());
+        assertTrue(bean.isB());
+        assertEquals(", bar", src.substring(nextPos));
+    }
+    
+    @Test
+    public void parsingErrors() throws Exception {
+        try {
+            _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean1(1,,2)",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertThat(e.getMessage(), containsString("\",\""));
+        }
+
+        try {
+            _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean1(x=1,2)",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertThat(e.getMessage(), containsStringIgnoringCase("must precede named"));
+        }
+
+
+        try {
+            _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean1(x=1;2)",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertThat(e.getMessage(), containsString("\";\""));
+        }
+        
+        try {
+            _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean1(1,2))",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertThat(e.getMessage(), containsString("\")\""));
+        }
+        
+        try {
+            _ObjectBuilderSettingEvaluator.eval(
+                    "foo.Bar('s${x}s'))",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertThat(e.getMessage(), containsString("${...}"));
+        }
+        
+        try {
+            _ObjectBuilderSettingEvaluator.eval(
+                    "foo.Bar('s#{x}s'))",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertThat(e.getMessage(), containsString("#{...}"));
+        }
+    }
+
+    @Test
+    public void semanticErrors() throws Exception {
+        try {
+            _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$XTestBean1(1,2)",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertThat(e.getMessage(), containsStringIgnoringCase("Failed to get class"));
+        }
+        
+        try {
+            _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean1(true, 2)",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertThat(e.getMessage(), containsStringIgnoringCase("constructor"));
+        }
+        
+        try {
+            _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean1(x = 1)",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertThat(e.getMessage(), containsStringIgnoringCase("no writeable JavaBeans property called \"x\""));
+        }
+        
+        try {
+            _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean1(p1 = 1, p1 = 2)",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertThat(e.getMessage(), containsString("twice"));
+        }
+        
+        try {
+            _ObjectBuilderSettingEvaluator.eval(
+                    "java.util.HashMap()",
+                    ObjectWrapper.class, false, _SettingEvaluationEnvironment.getCurrent());
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertThat(e.getMessage(), containsString("is not a(n) " + ObjectWrapper.class.getName()));
+        }
+        
+        try {
+            _ObjectBuilderSettingEvaluator.eval(
+                    "null",
+                    ObjectWrapper.class, false, _SettingEvaluationEnvironment.getCurrent());
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertThat(e.getMessage(), containsString("can't be null"));
+        }
+    }
+    
+    @Test
+    public void testLiteralAsObjectBuilder() throws Exception {
+        assertNull(_ObjectBuilderSettingEvaluator.eval(
+                "null",
+                ObjectWrapper.class, true, _SettingEvaluationEnvironment.getCurrent()));
+        assertEquals("foo", _ObjectBuilderSettingEvaluator.eval(
+                "'foo'",
+                CharSequence.class, true, _SettingEvaluationEnvironment.getCurrent()));
+        assertEquals(Boolean.TRUE, _ObjectBuilderSettingEvaluator.eval(
+                "  true  ",
+                Object.class, true, _SettingEvaluationEnvironment.getCurrent()));
+        assertEquals(Double.valueOf("1.23"), _ObjectBuilderSettingEvaluator.eval(
+                "1.23 ",
+                Number.class, true, _SettingEvaluationEnvironment.getCurrent()));
+        assertEquals(new Version(1, 2, 3), _ObjectBuilderSettingEvaluator.eval(
+                " 1.2.3",
+                Object.class, true, _SettingEvaluationEnvironment.getCurrent()));
+    }
+
+    @Test
+    public void testNumberLiteralJavaTypes() throws Exception {
+        assertEquals(Double.valueOf("1.0"), _ObjectBuilderSettingEvaluator.eval(
+                "1.0",
+                Number.class, true, _SettingEvaluationEnvironment.getCurrent()));
+
+        assertEquals(new BigInteger("-9223372036854775809"), _ObjectBuilderSettingEvaluator.eval(
+                "-9223372036854775809",
+                Number.class, true, _SettingEvaluationEnvironment.getCurrent()));
+        assertEquals(new BigInteger("9223372036854775808"), _ObjectBuilderSettingEvaluator.eval(
+                "9223372036854775808",
+                Number.class, true, _SettingEvaluationEnvironment.getCurrent()));
+        
+        assertEquals(Long.valueOf(-9223372036854775808L), _ObjectBuilderSettingEvaluator.eval(
+                "-9223372036854775808",
+                Number.class, true, _SettingEvaluationEnvironment.getCurrent()));
+        assertEquals(Long.valueOf(9223372036854775807L), _ObjectBuilderSettingEvaluator.eval(
+                "9223372036854775807",
+                Number.class, true, _SettingEvaluationEnvironment.getCurrent()));
+        
+        assertEquals(Integer.valueOf(-2147483648), _ObjectBuilderSettingEvaluator.eval(
+                "-2147483648",
+                Number.class, true, _SettingEvaluationEnvironment.getCurrent()));
+        assertEquals(Integer.valueOf(2147483647), _ObjectBuilderSettingEvaluator.eval(
+                "2147483647",
+                Number.class, true, _SettingEvaluationEnvironment.getCurrent()));
+        
+        assertEquals(Integer.valueOf(-1), _ObjectBuilderSettingEvaluator.eval(
+                "-1",
+                Number.class, true, _SettingEvaluationEnvironment.getCurrent()));
+        assertEquals(Integer.valueOf(1), _ObjectBuilderSettingEvaluator.eval(
+                "1",
+                Number.class, true, _SettingEvaluationEnvironment.getCurrent()));
+    }
+    
+    @Test
+    public void testListLiterals() throws Exception {
+        {
+            ArrayList<Object> expected = new ArrayList();
+            expected.add("s");
+            expected.add(null);
+            expected.add(true);
+            expected.add(new TestBean9(1));
+            expected.add(ImmutableList.of(11, 22, 33));
+            assertEquals(expected, _ObjectBuilderSettingEvaluator.eval(
+                    "['s', null, true, org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean9(1), [11, 22, 33]]",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent()));
+            assertEquals(expected, _ObjectBuilderSettingEvaluator.eval(
+                    "  [  's'  ,  null ,  true , org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean9(1) ,"
+                    + "  [ 11 , 22 , 33 ]  ]  ",
+                    Collection.class, false, _SettingEvaluationEnvironment.getCurrent()));
+            assertEquals(expected, _ObjectBuilderSettingEvaluator.eval(
+                    "['s',null,true,org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean9(1),[11,22,33]]",
+                    List.class, false, _SettingEvaluationEnvironment.getCurrent()));
+        }
+        
+        assertEquals(Collections.emptyList(), _ObjectBuilderSettingEvaluator.eval(
+                "[]",
+                Object.class, false, _SettingEvaluationEnvironment.getCurrent()));
+        assertEquals(Collections.emptyList(), _ObjectBuilderSettingEvaluator.eval(
+                "[  ]",
+                Object.class, false, _SettingEvaluationEnvironment.getCurrent()));
+
+        assertEquals(Collections.singletonList(123), _ObjectBuilderSettingEvaluator.eval(
+                "[123]",
+                Object.class, false, _SettingEvaluationEnvironment.getCurrent()));
+        assertEquals(Collections.singletonList(123), _ObjectBuilderSettingEvaluator.eval(
+                "[ 123 ]",
+                Object.class, false, _SettingEvaluationEnvironment.getCurrent()));
+        
+        assertEquals(new TestBean9(1, ImmutableList.of("a", "b")), _ObjectBuilderSettingEvaluator.eval(
+                "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean9(1, ['a', 'b'])",
+                Object.class, false, _SettingEvaluationEnvironment.getCurrent()));
+        
+        try {
+            _ObjectBuilderSettingEvaluator.eval(
+                    "[1,]",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertThat(e.getMessage(), containsString("found character \"]\""));
+        }
+        try {
+            _ObjectBuilderSettingEvaluator.eval(
+                    "[,1]",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertThat(e.getMessage(), containsString("found character \",\""));
+        }
+        try {
+            _ObjectBuilderSettingEvaluator.eval(
+                    "1]",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertThat(e.getMessage(), containsString("found character \"]\""));
+        }
+        try {
+            _ObjectBuilderSettingEvaluator.eval(
+                    "[1",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertThat(e.getMessage(), containsString("end of"));
+        }
+    }
+
+    @Test
+    public void testMapLiterals() throws Exception {
+        {
+            HashMap<String, Object> expected = new HashMap();
+            expected.put("k1", "s");
+            expected.put("k2", null);
+            expected.put("k3", true);
+            expected.put("k4", new TestBean9(1));
+            expected.put("k5", ImmutableList.of(11, 22, 33));
+            assertEquals(expected, _ObjectBuilderSettingEvaluator.eval(
+                    "{'k1': 's', 'k2': null, 'k3': true, "
+                    + "'k4': org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean9(1), 'k5': [11, 22, 33]}",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent()));
+            assertEquals(expected, _ObjectBuilderSettingEvaluator.eval(
+                    " {  'k1'  :  's'  ,  'k2' :  null  , 'k3' : true , "
+                    + "'k4' : org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean9 ( 1 ) , 'k5' : [ 11 , 22 , 33 ] } ",
+                    Map.class, false, _SettingEvaluationEnvironment.getCurrent()));
+            assertEquals(expected, _ObjectBuilderSettingEvaluator.eval(
+                    " {'k1':'s','k2':null,'k3':true,"
+                    + "'k4':org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean9(1),'k5':[11,22,33]}",
+                    LinkedHashMap.class, false, _SettingEvaluationEnvironment.getCurrent()));
+        }
+        
+        {
+            HashMap<Object, String> expected = new HashMap();
+            expected.put(true, "T");
+            expected.put(1, "O");
+            expected.put(new TestBean9(1), "B");
+            expected.put(ImmutableList.of(11, 22, 33), "L");
+            assertEquals(expected, _ObjectBuilderSettingEvaluator.eval(
+                    "{ true: 'T', 1: 'O', org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean9(1): 'B', "
+                    + "[11, 22, 33]: 'L' }",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent()));
+        }
+        
+        assertEquals(Collections.emptyMap(), _ObjectBuilderSettingEvaluator.eval(
+                "{}",
+                Object.class, false, _SettingEvaluationEnvironment.getCurrent()));
+        assertEquals(Collections.emptyMap(), _ObjectBuilderSettingEvaluator.eval(
+                "{  }",
+                Object.class, false, _SettingEvaluationEnvironment.getCurrent()));
+
+        assertEquals(Collections.singletonMap("k1", 123), _ObjectBuilderSettingEvaluator.eval(
+                "{'k1':123}",
+                Object.class, false, _SettingEvaluationEnvironment.getCurrent()));
+        assertEquals(Collections.singletonMap("k1", 123), _ObjectBuilderSettingEvaluator.eval(
+                "{ 'k1' : 123 }",
+                Object.class, false, _SettingEvaluationEnvironment.getCurrent()));
+        
+        assertEquals(new TestBean9(1, ImmutableMap.of(11, "a", 22, "b")), _ObjectBuilderSettingEvaluator.eval(
+                "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean9(1, { 11: 'a', 22: 'b' })",
+                Object.class, false, _SettingEvaluationEnvironment.getCurrent()));
+        
+        try {
+            _ObjectBuilderSettingEvaluator.eval(
+                    "{1:2,}",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertThat(e.getMessage(), containsString("found character \"}\""));
+        }
+        try {
+            _ObjectBuilderSettingEvaluator.eval(
+                    "{,1:2}",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertThat(e.getMessage(), containsString("found character \",\""));
+        }
+        try {
+            _ObjectBuilderSettingEvaluator.eval(
+                    "1:2}",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertThat(e.getMessage(), containsString("found character \":\""));
+        }
+        try {
+            _ObjectBuilderSettingEvaluator.eval(
+                    "1}",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertThat(e.getMessage(), containsString("found character \"}\""));
+        }
+        try {
+            _ObjectBuilderSettingEvaluator.eval(
+                    "{1",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertThat(e.getMessage(), containsString("end of"));
+        }
+        try {
+            _ObjectBuilderSettingEvaluator.eval(
+                    "{1:",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertThat(e.getMessage(), containsString("end of"));
+        }
+        try {
+            _ObjectBuilderSettingEvaluator.eval(
+                    "{null:1}",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertThat(e.getMessage(), containsString("null as key"));
+        }
+    }
+
+    @Test
+    public void testMethodParameterNumberTypes() throws Exception {
+        {
+            TestBean8 result = (TestBean8) _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean8(anyObject=1)",
+                    TestBean8.class, false, new _SettingEvaluationEnvironment());
+            assertEquals(result.getAnyObject(), 1);
+        }
+        {
+            TestBean8 result = (TestBean8) _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean8(anyObject=2147483649)",
+                    TestBean8.class, false, new _SettingEvaluationEnvironment());
+            assertEquals(result.getAnyObject(), 2147483649L);
+        }
+        {
+            TestBean8 result = (TestBean8) _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean8(anyObject=1.0)",
+                    TestBean8.class, false, new _SettingEvaluationEnvironment());
+            // Like in FTL, non-integer numbers are BigDecimal-s, that are later coerced to the actual parameter type.
+            // However, here the type is Object, so it remains BigDecimal.
+            assertEquals(new BigDecimal("1.0"), result.getAnyObject());
+        }
+    }
+    
+    @Test
+    public void testNonMethodParameterNumberTypes() throws Exception {
+        assertEqualsEvaled(Integer.valueOf(1), "1");
+        assertEqualsEvaled(Double.valueOf(1), "1.0");
+        assertEqualsEvaled(Long.valueOf(2147483649l), "2147483649");
+
+        assertEqualsEvaled(Double.valueOf(1), "1d");
+        assertEqualsEvaled(Double.valueOf(1), "1D");
+        assertEqualsEvaled(Float.valueOf(1), "1f");
+        assertEqualsEvaled(Float.valueOf(1), "1F");
+        assertEqualsEvaled(Long.valueOf(1), "1l");
+        assertEqualsEvaled(Long.valueOf(1), "1L");
+        assertEqualsEvaled(BigDecimal.valueOf(1), "1bd");
+        assertEqualsEvaled(BigDecimal.valueOf(1), "1Bd");
+        assertEqualsEvaled(BigDecimal.valueOf(1), "1BD");
+        assertEqualsEvaled(BigInteger.valueOf(1), "1bi");
+        assertEqualsEvaled(BigInteger.valueOf(1), "1bI");
+        
+        assertEqualsEvaled(Float.valueOf(1.5f), "1.5f");
+        assertEqualsEvaled(Double.valueOf(1.5), "1.5d");
+        assertEqualsEvaled(BigDecimal.valueOf(1.5), "1.5bd");
+        
+        assertEqualsEvaled(
+                ImmutableList.of(-1, -0.5, new BigDecimal("-0.1")),
+                "[ -1, -0.5, -0.1bd ]");
+        assertEqualsEvaled(
+                ImmutableMap.of(-1, -11, -0.5, -0.55, new BigDecimal("-0.1"), new BigDecimal("-0.11")),
+                "{ -1: -11, -0.5: -0.55, -0.1bd: -0.11bd }");
+    }
+    
+    @Test
+    public void testStaticFields() throws Exception {
+        {
+            TestBean1 res = (TestBean1) _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean1("
+                    + "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestStaticFields.CONST, true)",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            assertEquals(TestStaticFields.CONST, (int) res.i);
+        }
+        {
+            TestBean1 res = (TestBean1) _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean1("
+                    + "p2 = org.apache.freemarker.core.ObjectBuilderSettingsTest$TestStaticFields.CONST)",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            assertEquals(TestStaticFields.CONST, res.getP2());
+        }
+        assertEqualsEvaled(123, "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestStaticFields.CONST");
+        
+        // With shorthand class name:
+        assertEqualsEvaled(ParsingConfiguration.AUTO_DETECT_TAG_SYNTAX, "Configuration.AUTO_DETECT_TAG_SYNTAX");
+        
+        try {
+            _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean1("
+                    + "p2 = org.apache.freemarker.core.ObjectBuilderSettingsTest$TestStaticFields.CONST())",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertThat(e.getMessage(),
+                    containsString("org.apache.freemarker.core.ObjectBuilderSettingsTest$TestStaticFields.CONST"));
+        }
+        try {
+            assertEqualsEvaled(123, "org.apache.freemarker.core.ObjectBuilderSettingsTest$TestStaticFields.CONST()");
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertThat(e.getMessage(),
+                    containsString("org.apache.freemarker.core.ObjectBuilderSettingsTest$TestStaticFields.CONST"));
+        }
+        try {
+            assertEqualsEvaled(123, "java.lang.String(org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean5.INSTANCE)");
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertThat(e.getMessage(),
+                    containsString("org.apache.freemarker.core.ObjectBuilderSettingsTest$TestBean5()"));
+        }
+    }
+    
+    private void assertEqualsEvaled(Object expectedValue, String s)
+            throws _ObjectBuilderSettingEvaluationException, ClassNotFoundException, InstantiationException,
+            IllegalAccessException {
+        Object actualValue = _ObjectBuilderSettingEvaluator.eval(
+                s, Object.class, true, _SettingEvaluationEnvironment.getCurrent());
+        assertEquals(expectedValue, actualValue);
+    }
+    
+    @Test
+    public void visibilityTest() throws Exception {
+        try {
+            _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.userpkg.PackageVisibleAll()",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertEquals(IllegalAccessException.class, e.getCause().getClass());
+        }
+
+        try {
+            _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.userpkg.PackageVisibleWithPublicConstructor()",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertEquals(IllegalAccessException.class, e.getCause().getClass());
+        }
+
+        try {
+            _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.userpkg.PublicWithPackageVisibleConstructor()",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            fail();
+        } catch (_ObjectBuilderSettingEvaluationException e) {
+            assertEquals(IllegalAccessException.class, e.getCause().getClass());
+        }
+        
+        {
+            Object o = _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.userpkg.PublicAll()",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            assertEquals(org.apache.freemarker.core.userpkg.PublicAll.class, o.getClass());
+        }
+        
+        {
+            Object o = _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.userpkg.PublicWithMixedConstructors(1)",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            assertEquals("Integer", ((PublicWithMixedConstructors) o).getS());
+        }
+        
+        
+        {
+            Object o = _ObjectBuilderSettingEvaluator.eval(
+                    "org.apache.freemarker.core.userpkg.PackageVisibleAllWithBuilder()",
+                    Object.class, false, _SettingEvaluationEnvironment.getCurrent());
+            assertEquals("org.apache.freemarker.core.userpkg.PackageVisibleAllWithBuilder", o.getClass().getName());
+        }
+    }
+
+    public static class TestBean1 {
+        float f;
+        Integer i;
+        long l;
+        boolean b;
+        
+        double p1;
+        int p2;
+        boolean p3;
+        String p4;
+        
+        public TestBean1(float f, Integer i, long l, boolean b) {
+            this.f = f;
+            this.i = i;
+            this.l = l;
+            this.b = b;
+        }
+        
+        public TestBean1(Integer i, boolean b) {
+            f = 2;
+            this.i = i;
+            l = 2;
+            this.b = b;
+        }
+    
+        public TestBean1(Integer i, long l) {
+            f = 3;
+            this.i = i;
+            this.l = l;
+            b = false;
+        }
+        
+        public TestBean1() {
+            f = 4;
+        }
+    
+        public double getP1() {
+            return p1;
+        }
+    
+        public void setP1(double p1) {
+            this.p1 = p1;
+        }
+    
+        public int getP2() {
+            return p2;
+        }
+    
+        public void setP2(int p2) {
+            this.p2 = p2;
+        }
+    
+        public boolean isP3() {
+            return p3;
+        }
+    
+        public void setP3(boolean p3) {
+            this.p3 = p3;
+        }
+
+        public String getP4() {
+            return p4;
+        }
+
+        public void setP4(String p4) {
+            this.p4 = p4;
+        }
+        
+    }
+    
+    public static class TestBean2 {
+        final boolean built;
+        final int x;
+
+        public TestBean2() {
+            built = false;
+            x = 0;
+        }
+        
+        public TestBean2(int x) {
+            built = false;
+            this.x = x;
+        }
+
+        public TestBean2(TestBean2Builder builder) {
+            built = true;
+            x = builder.x;
+        }
+        
+    }
+
+    public static class TestBean2Builder {
+        int x;
+        
+        public TestBean2Builder() { }
+
+        public TestBean2Builder(int x) {
+            this.x = x;
+        }
+        
+        public int getX() {
+            return x;
+        }
+
+        public void setX(int x) {
+            this.x = x;
+        }
+        
+        public TestBean2 build() {
+            return new TestBean2(this);
+        }
+        
+    }
+
+    public static class TestBean3 {
+        
+        private int x;
+
+        public int getX() {
+            return x;
+        }
+
+        public void setX(int x) {
+            this.x = x;
+        }
+        
+    }
+    
+    public static class TestBean4 {
+        private final String s1, s2;
+        private String s3, s4;
+        
+        public TestBean4(String s1, String s2) {
+            this.s1 = s1;
+            this.s2 = s2;
+        }
+        
+        public String getS1() {
+            return s1;
+        }
+
+        public String getS2() {
+            return s2;
+        }
+
+        public String getS3() {
+            return s3;
+        }
+        
+        public void setS3(String s3) {
+            this.s3 = s3;
+        }
+        
+        public String getS4() {
+            return s4;
+        }
+        
+        public void setS4(String s4) {
+            this.s4 = s4;
+        }
+        
+    }
+    
+    public static class TestBean5 {
+        
+        public final static TestBean5 INSTANCE = new TestBean5();
+        
+        private final int i;
+        private int x;
+        
+        public TestBean5() {
+            i = 0;
+        }
+
+        public TestBean5(int i) {
+            this.i = i;
+        }
+
+        public int getI() {
+            return i;
+        }
+
+        public int getX() {
+            return x;
+        }
+
+        public void setX(int x) {
+            this.x = x;
+        }
+        
+    }
+
+    public static class TestBean6 {
+        private final TestBean1 b1;
+        private int x;
+        private final TestBean2 b2;
+        private int y;
+        private TestBean2 b3;
+        private TestBean6 b4;
+        
+        public TestBean6(TestBean1 b1, int x, TestBean2 b2) {
+            this.b1 = b1;
+            this.x = x;
+            this.b2 = b2;
+        }
+
+        public int getX() {
+            return x;
+        }
+
+        public void setX(int x) {
+            this.x = x;
+        }
+
+        public int getY() {
+            return y;
+        }
+
+        public void setY(int y) {
+            this.y = y;
+        }
+
+        public TestBean2 getB3() {
+            return b3;
+        }
+
+        public void setB3(TestBean2 b3) {
+            this.b3 = b3;
+        }
+
+        public TestBean1 getB1() {
+            return b1;
+        }
+
+        public TestBean2 getB2() {
+            return b2;
+        }
+
+        public TestBean6 getB4() {
+            return b4;
+        }
+
+        public void setB4(TestBean6 b4) {
+            this.b4 = b4;
+        }
+        
+    }
+    
+    public class TestBean7 {
+
+        private String s;
+        private int x;
+        private boolean b;
+
+        public String getS() {
+            return s;
+        }
+
+        public void setS(String s) {
+            this.s = s;
+        }
+
+        public int getX() {
+            return x;
+        }
+
+        public void setX(int x) {
+            this.x = x;
+        }
+
+        public boolean isB() {
+            return b;
+        }
+
+        public void setB(boolean b) {
+            this.b = b;
+        }
+
+        @Override
+        public String toString() {
+            return "TestBean [s=" + s + ", x=" + x + ", b=" + b + "]";
+        }
+
+    }
+    
+    public static class TestBean8 {
+        private TimeZone timeZone;
+        private Charset charset;
+        private Object anyObject;
+        private List<?> list;
+        
+        public TimeZone getTimeZone() {
+            return timeZone;
+        }
+        
+        public void setTimeZone(TimeZone timeZone) {
+            this.timeZone = timeZone;
+        }
+
+        public Charset getCharset() {
+            return charset;
+        }
+
+        public void setCharset(Charset charset) {
+            this.charset = charset;
+        }
+
+        public Object getAnyObject() {
+            return anyObject;
+        }
+        
+        public void setAnyObject(Object anyObject) {
+            this.anyObject = anyObject;
+        }
+
+        public List<?> getList() {
+            return list;
+        }
+        
+        public void setList(List<?> list) {
+            this.list = list;
+        }
+        
+    }
+    
+    public static class TestBean9 {
+        
+        private final int n;
+        private final List<?> list;
+        private final Map<?, ?> map;
+
+        public TestBean9(int n) {
+            this(n, null, null);
+        }
+
+        public TestBean9(int n, List<?> list) {
+            this(n, list, null);
+        }
+
+        public TestBean9(int n, Map<?, ?> map) {
+            this(n, null, map);
+        }
+        
+        public TestBean9(int n, List<?> list, Map<?, ?> map) {
+            this.n = n;
+            this.list = list;
+            this.map = map;
+        }
+
+        @Override
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + ((list == null) ? 0 : list.hashCode());
+            result = prime * result + ((map == null) ? 0 : map.hashCode());
+            result = prime * result + n;
+            return result;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) return true;
+            if (obj == null) return false;
+            if (getClass() != obj.getClass()) return false;
+            TestBean9 other = (TestBean9) obj;
+            if (list == null) {
+                if (other.list != null) return false;
+            } else if (!list.equals(other.list)) return false;
+            if (map == null) {
+                if (other.map != null) return false;
+            } else if (!map.equals(other.map)) return false;
+            return n == other.n;
+        }
+        
+    }
+    
+    public static class TestStaticFields {
+        public static final int CONST = 123;
+    }
+    
+    public static class DummyArithmeticEngine extends ArithmeticEngine {
+        
+        private int x;
+
+        @Override
+        public int compareNumbers(Number first, Number second) throws TemplateException {
+            return 0;
+        }
+
+        @Override
+        public Number add(Number first, Number second) throws TemplateException {
+            return null;
+        }
+
+        @Override
+        public Number subtract(Number first, Number second) throws TemplateException {
+            return null;
+        }
+
+        @Override
+        public Number multiply(Number first, Number second) throws TemplateException {
+            return null;
+        }
+
+        @Override
+        public Number divide(Number first, Number second) throws TemplateException {
+            return null;
+        }
+
+        @Override
+        public Number modulus(Number first, Number second) throws TemplateException {
+            return null;
+        }
+
+        @Override
+        public Number toNumber(String s) {
+            return null;
+        }
+
+        public int getX() {
+            return x;
+        }
+
+        public void setX(int x) {
+            this.x = x;
+        }
+        
+    }
+    
+    public static class DummyTemplateExceptionHandler implements TemplateExceptionHandler {
+        
+        private int x;
+
+        @Override
+        public void handleTemplateException(TemplateException te, Environment env, Writer out) throws TemplateException {
+        }
+
+        public int getX() {
+            return x;
+        }
+
+        public void setX(int x) {
+            this.x = x;
+        }
+        
+    }
+    
+    public static class DummyCacheStorage implements CacheStorage {
+        
+        @Override
+        public Object get(Object key) {
+            return null;
+        }
+
+        @Override
+        public void put(Object key, Object value) {
+        }
+
+        @Override
+        public void remove(Object key) {
+        }
+
+        @Override
+        public void clear() {
+        }
+        
+    }
+    
+    public static class DummyNewBuiltinClassResolver implements TemplateClassResolver {
+
+        @Override
+        public Class resolve(String className, Environment env, Template template) throws TemplateException {
+            return null;
+        }
+        
+    }
+    
+    public static class DummyTemplateLoader implements TemplateLoader {
+
+        @Override
+        public TemplateLoaderSession createSession() {
+            return null;
+        }
+
+        @Override
+        public TemplateLoadingResult load(String name, TemplateLoadingSource ifSourceDiffersFrom,
+                Serializable ifVersionDiffersFrom, TemplateLoaderSession session) throws IOException {
+            return TemplateLoadingResult.NOT_FOUND;
+        }
+
+        @Override
+        public void resetState() {
+            // Do nothing
+        }
+        
+    }
+    
+}


[26/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/ObjectWrapperAndUnwrapper.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/ObjectWrapperAndUnwrapper.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/ObjectWrapperAndUnwrapper.java
new file mode 100644
index 0000000..3494eb7
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/ObjectWrapperAndUnwrapper.java
@@ -0,0 +1,90 @@
+/*
+ * 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;
+
+/**
+ * <b>Experimental - subject to change:</b> Adds functionality to {@link ObjectWrapper} that creates a plain Java object
+ * from a {@link TemplateModel}. This is usually implemented by {@link ObjectWrapper}-s and reverses
+ * {@link ObjectWrapper#wrap(Object)}. However, an implementation of this interface should make a reasonable effort to
+ * "unwrap" {@link TemplateModel}-s that wasn't the result of object wrapping (such as those created directly in FTL),
+ * or which was created by another {@link ObjectWrapper}. The author of an {@link ObjectWrapperAndUnwrapper} should be
+ * aware of the {@link TemplateModelAdapter} and {@link WrapperTemplateModel} interfaces, which should be used for
+ * unwrapping if the {@link TemplateModel} implements them.
+ * 
+ * <p>
+ * <b>Experimental status warning:</b> This interface is subject to change on non-backward compatible ways, hence, it
+ * shouldn't be implemented outside FreeMarker yet.
+ * 
+ * @since 2.3.22
+ */
+public interface ObjectWrapperAndUnwrapper extends ObjectWrapper {
+
+    /**
+     * Indicates that while the unwrapping is <em>maybe</em> possible, the result surely can't be the instance of the
+     * desired class, nor it can be {@code null}.
+     * 
+     * @see #tryUnwrapTo(TemplateModel, Class)
+     * 
+     * @since 2.3.22
+     */
+    Object CANT_UNWRAP_TO_TARGET_CLASS = new Object();
+
+    /**
+     * Unwraps a {@link TemplateModel} to a plain Java object.
+     * 
+     * @return The plain Java object. Can be {@code null}, if {@code null} is the appropriate Java value to represent
+     *         the template model. {@code null} must not be used to indicate an unwrapping failure. It must NOT be
+     *         {@link #CANT_UNWRAP_TO_TARGET_CLASS}.
+     * 
+     * @throws TemplateModelException
+     *             If the unwrapping fails from any reason.
+     * 
+     * @see #tryUnwrapTo(TemplateModel, Class)
+     * 
+     * @since 2.3.22
+     */
+    Object unwrap(TemplateModel tm) throws TemplateModelException;
+
+    /**
+     * Attempts to unwrap a {@link TemplateModel} to a plain Java object that's the instance of the given class (or is
+     * {@code null}).
+     * 
+     * @param targetClass
+     *            The class that the return value must be an instance of (except when the return value is {@code null}).
+     *            Can't be {@code null}; if the caller doesn't care, it should either use {#unwrap(TemplateModel)}, or
+     *            {@code Object.class} as the parameter value.
+     *
+     * @return The unwrapped value that's either an instance of {@code targetClass}, or is {@code null} (if {@code null}
+     *         is the appropriate Java value to represent the template model), or is
+     *         {@link #CANT_UNWRAP_TO_TARGET_CLASS} if the unwrapping can't satisfy the {@code targetClass} (nor the
+     *         result can be {@code null}). However, {@link #CANT_UNWRAP_TO_TARGET_CLASS} must not be returned if the
+     *         {@code targetClass} parameter was {@code Object.class}.
+     * 
+     * @throws TemplateModelException
+     *             If the unwrapping fails for a reason than doesn't fit the meaning of the
+     *             {@link #CANT_UNWRAP_TO_TARGET_CLASS} return value.
+     * 
+     * @see #unwrap(TemplateModel)
+     * 
+     * @since 2.3.22
+     */
+    Object tryUnwrapTo(TemplateModel tm, Class<?> targetClass) throws TemplateModelException;
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/ObjectWrapperWithAPISupport.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/ObjectWrapperWithAPISupport.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/ObjectWrapperWithAPISupport.java
new file mode 100644
index 0000000..102a2f0
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/ObjectWrapperWithAPISupport.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.model;
+
+/**
+ * <b>Experimental - subject to change:</b> Implemented by {@link ObjectWrapper}-s to help {@link TemplateModel}-s to
+ * implement the {@code someValue?api} operation.
+ * 
+ * <p>
+ * <b>Experimental status warning:</b> This interface is subject to change on non-backward compatible ways, hence, it
+ * shouldn't be implemented outside FreeMarker yet.
+ * 
+ * @since 2.3.22
+ */
+public interface ObjectWrapperWithAPISupport extends ObjectWrapper {
+
+    /**
+     * Wraps an object to a {@link TemplateModel} that exposes the object's "native" (usually, Java) API.
+     * 
+     * @param obj
+     *            The object for which the API model has to be returned. Shouldn't be {@code null}.
+     * 
+     * @return The {@link TemplateModel} through which the API of the object can be accessed. Can't be {@code null}.
+     * 
+     * @since 2.3.22
+     */
+    TemplateHashModel wrapAsAPI(Object obj) throws TemplateModelException;
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/RichObjectWrapper.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/RichObjectWrapper.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/RichObjectWrapper.java
new file mode 100644
index 0000000..5dfa3be
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/RichObjectWrapper.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+/**
+ * <b>Experimental - subject to change:</b> Union of the interfaces that a typical feature rich {@link ObjectWrapper} is
+ * expected to implement.
+ * 
+ * <p>
+ * <b>Experimental status warning:</b> This interface is subject to change on non-backward compatible ways, hence, it
+ * shouldn't be implemented outside FreeMarker yet.
+ * 
+ * @since 2.3.22
+ */
+public interface RichObjectWrapper extends ObjectWrapperAndUnwrapper, ObjectWrapperWithAPISupport {
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/SerializableTemplateBooleanModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/SerializableTemplateBooleanModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/SerializableTemplateBooleanModel.java
new file mode 100644
index 0000000..b01e7df
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/SerializableTemplateBooleanModel.java
@@ -0,0 +1,24 @@
+/*
+ * 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;
+
+import java.io.Serializable;
+
+interface SerializableTemplateBooleanModel extends TemplateBooleanModel, Serializable {}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateBooleanModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateBooleanModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateBooleanModel.java
new file mode 100644
index 0000000..555e619
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateBooleanModel.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+import java.io.Serializable;
+
+/**
+ * "boolean" template language data type; same as in Java; either {@code true} or {@code false}.
+ * 
+ * <p>
+ * Objects of this type should be immutable, that is, calling {@link #getAsBoolean()} should always return the same
+ * value as for the first time.
+ */
+public interface TemplateBooleanModel extends TemplateModel, Serializable {
+
+    /**
+     * @return whether to interpret this object as true or false in a boolean context
+     */
+    boolean getAsBoolean() throws TemplateModelException;
+    
+    /**
+     * A singleton object to represent boolean false
+     */
+    TemplateBooleanModel FALSE = new FalseTemplateBooleanModel();
+
+    /**
+     * A singleton object to represent boolean true
+     */
+    TemplateBooleanModel TRUE = new TrueTemplateBooleanModel();
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateCollectionModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateCollectionModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateCollectionModel.java
new file mode 100644
index 0000000..e870c2f
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateCollectionModel.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+import java.util.Collection;
+
+/**
+ * "collection" template language data type: a collection of values that can be enumerated, but can't be or not meant to
+ * be accessed by index or key. As such, this is not a super-interface of {@link TemplateSequenceModel}, and
+ * implementations of that interface needn't also implement this interface just because they can. They should though, if
+ * enumeration with this interface is significantly faster than enumeration by index. The {@code #list} directive will
+ * enumerate using this interface if it's available.
+ * 
+ * <p>
+ * The enumeration should be repeatable if that's possible with reasonable effort, otherwise a second enumeration
+ * attempt is allowed to throw an {@link TemplateModelException}. Generally, the interface user Java code need not
+ * handle that kind of exception, as in practice only the template author can handle it, by not listing such collections
+ * twice.
+ * 
+ * <p>
+ * Note that to wrap Java's {@link Collection}, you should implement {@link TemplateCollectionModelEx}, not just this
+ * interface.
+ */
+public interface TemplateCollectionModel extends TemplateModel {
+
+    /**
+     * Retrieves a template model iterator that is used to iterate over the elements in this collection.
+     */
+    TemplateModelIterator iterator() throws TemplateModelException;
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateCollectionModelEx.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateCollectionModelEx.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateCollectionModelEx.java
new file mode 100644
index 0000000..92f0e3a
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateCollectionModelEx.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;
+
+import java.util.Collection;
+
+/**
+ * "extended collection" template language data type: Adds size/emptiness querybility and "contains" test to
+ * {@link TemplateCollectionModel}. The added extra operations is provided by all Java {@link Collection}-s, and
+ * this interface was added to make that accessible for templates too.
+ *
+ * @since 2.3.22
+ */
+public interface TemplateCollectionModelEx extends TemplateCollectionModel {
+
+    /**
+     * Returns the number items in this collection, or {@link Integer#MAX_VALUE}, if there are more than
+     * {@link Integer#MAX_VALUE} items.
+     */
+    int size() throws TemplateModelException;
+
+    /**
+     * Returns if the collection contains any elements. This differs from {@code size() != 0} only in that the exact
+     * number of items need not be calculated.
+     */
+    boolean isEmpty() throws TemplateModelException;
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateDateModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateDateModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateDateModel.java
new file mode 100644
index 0000000..ab85e97
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateDateModel.java
@@ -0,0 +1,73 @@
+/*
+ * 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;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * "date", "time" and "date-time" template language data types: corresponds to {@link java.util.Date}. Contrary to Java,
+ * FreeMarker distinguishes date (no time part), time and date-time values.
+ * 
+ * <p>
+ * Objects of this type should be immutable, that is, calling {@link #getAsDate()} and {@link #getDateType()} should
+ * always return the same value as for the first time.
+ */
+public interface TemplateDateModel extends TemplateModel {
+    
+    /**
+     * It is not known whether the date represents a date, a time, or a date-time value.
+     * This often leads to exceptions in templates due to ambiguities it causes, so avoid it if possible.
+     */
+    int UNKNOWN = 0;
+
+    /**
+     * The date model represents a time value (no date part).
+     */
+    int TIME = 1;
+
+    /**
+     * The date model represents a date value (no time part).
+     */
+    int DATE = 2;
+
+    /**
+     * The date model represents a date-time value (also known as timestamp).
+     */
+    int DATETIME = 3;
+    
+    List TYPE_NAMES =
+        Collections.unmodifiableList(
+            Arrays.asList(
+                    "UNKNOWN", "TIME", "DATE", "DATETIME"));
+    /**
+     * Returns the date value. The return value must not be {@code null}.
+     */
+    Date getAsDate() throws TemplateModelException;
+
+    /**
+     * Returns the type of the date. It can be any of {@link #TIME}, 
+     * {@link #DATE}, or {@link #DATETIME}.
+     */
+    int getDateType();
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateDirectiveBody.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateDirectiveBody.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateDirectiveBody.java
new file mode 100644
index 0000000..bb54eb4
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateDirectiveBody.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;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import org.apache.freemarker.core.TemplateException;
+
+/**
+ * Represents the nested content of a directive ({@link TemplateDirectiveModel}) invocation. An implementation of this 
+ * class is passed to {@link TemplateDirectiveModel#execute(org.apache.freemarker.core.Environment, 
+ * java.util.Map, TemplateModel[], TemplateDirectiveBody)}. The implementation of the method is 
+ * free to invoke it for any number of times, with any writer.
+ *
+ * @since 2.3.11
+ */
+public interface TemplateDirectiveBody {
+    /**
+     * Renders the body of the directive body to the specified writer. The 
+     * writer is not flushed after the rendering. If you pass the environment's
+     * writer, there is no need to flush it. If you supply your own writer, you
+     * are responsible to flush/close it when you're done with using it (which
+     * might be after multiple renderings).
+     * @param out the writer to write the output to.
+     */
+    void render(Writer out) throws TemplateException, IOException;
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateDirectiveModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateDirectiveModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateDirectiveModel.java
new file mode 100644
index 0000000..c4020c9
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateDirectiveModel.java
@@ -0,0 +1,69 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.util.Map;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core.util.DeepUnwrap;
+
+/**
+ * "directive" template language data type: used as user-defined directives 
+ * (much like macros) in templates. They can do arbitrary actions, write arbitrary
+ * text to the template output, and trigger rendering of their nested content for
+ * any number of times.
+ * 
+ * <p>They are used in templates like {@code <@myDirective foo=1 bar="wombat">...</...@myDirective>} (or as
+ * {@code <@myDirective foo=1 bar="wombat" />} - the nested content is optional).
+ *
+ * @since 2.3.11
+ */
+public interface TemplateDirectiveModel extends TemplateModel {
+    /**
+     * Executes this user-defined directive; called by FreeMarker when the user-defined
+     * directive is called in the template.
+     *
+     * @param env the current processing environment. Note that you can access
+     * the output {@link java.io.Writer Writer} by {@link Environment#getOut()}.
+     * @param params the parameters (if any) passed to the directive as a 
+     * map of key/value pairs where the keys are {@link String}-s and the 
+     * values are {@link TemplateModel} instances. This is never 
+     * <code>null</code>. If you need to convert the template models to POJOs,
+     * you can use the utility methods in the {@link DeepUnwrap} class.
+     * @param loopVars an array that corresponds to the "loop variables", in
+     * the order as they appear in the directive call. ("Loop variables" are out-parameters
+     * that are available to the nested body of the directive; see in the Manual.)
+     * You set the loop variables by writing this array. The length of the array gives the
+     * number of loop-variables that the caller has specified.
+     * Never <code>null</code>, but can be a zero-length array.
+     * @param body an object that can be used to render the nested content (body) of
+     * the directive call. If the directive call has no nested content (i.e., it's like
+     * &lt;@myDirective /&gt; or &lt;@myDirective&gt;&lt;/@myDirective&gt;), then this will be
+     * <code>null</code>.
+     *
+     * @throws TemplateException If any problem occurs that's not an {@link IOException} during writing the template
+     *          output.
+     * @throws IOException When writing the template output fails.
+     */
+    void execute(Environment env, Map params, TemplateModel[] loopVars,
+                 TemplateDirectiveBody body) throws TemplateException, IOException;
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateHashModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateHashModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateHashModel.java
new file mode 100644
index 0000000..647055a
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateHashModel.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.model;
+
+/**
+ * "hash" template language data type: an object that contains other objects accessible through string keys
+ * (sub-variable names).
+ * 
+ * <p>In templates they are used like {@code myHash.myKey} or {@code myHash[myDynamicKey]}. 
+ */
+public interface TemplateHashModel extends TemplateModel {
+    
+    /**
+     * Gets a <tt>TemplateModel</tt> from the hash.
+     *
+     * @param key the name by which the <tt>TemplateModel</tt>
+     * is identified in the template.
+     * @return the <tt>TemplateModel</tt> referred to by the key,
+     * or null if not found.
+     */
+    TemplateModel get(String key) throws TemplateModelException;
+
+    boolean isEmpty() throws TemplateModelException;
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateHashModelEx.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateHashModelEx.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateHashModelEx.java
new file mode 100644
index 0000000..c95a21d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateHashModelEx.java
@@ -0,0 +1,51 @@
+/*
+ * 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;
+
+import org.apache.freemarker.core.model.impl.SimpleHash;
+
+/**
+ * "extended hash" template language data type; extends {@link TemplateHashModel} by allowing
+ * iterating through its keys and values.
+ * 
+ * <p>In templates they are used like hashes, but these will also work (among others):
+ * {@code myExtHash?size}, {@code myExtHash?keys}, {@code myExtHash?values}.
+ * @see SimpleHash
+ */
+public interface TemplateHashModelEx extends TemplateHashModel {
+
+    /**
+     * @return the number of key/value mappings in the hash.
+     */
+    int size() throws TemplateModelException;
+
+    /**
+     * @return a collection containing the keys in the hash. Every element of 
+     *      the returned collection must implement the {@link TemplateScalarModel}
+     *      (as the keys of hashes are always strings).
+     */
+    TemplateCollectionModel keys() throws TemplateModelException;
+
+    /**
+     * @return a collection containing the values in the hash. The elements of the
+     * returned collection can be any kind of {@link TemplateModel}-s.
+     */
+    TemplateCollectionModel values() throws TemplateModelException;
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateHashModelEx2.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateHashModelEx2.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateHashModelEx2.java
new file mode 100644
index 0000000..86a72b1
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateHashModelEx2.java
@@ -0,0 +1,80 @@
+/*
+ * 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;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * Adds key-value pair listing capability to {@link TemplateHashModelEx}. While in many cases that can also be achieved
+ * with {@link #keys()} and then {@link #get(String)}, that has some problems. One is that {@link #get(String)} only
+ * accepts string keys, while {@link #keys()} can return non-string keys too. The other is that calling {@link #keys()}
+ * and then {@link #get(String)} for each key can be slower than listing the key-value pairs in one go.
+ * 
+ * @since 2.3.25
+ */
+public interface TemplateHashModelEx2 extends TemplateHashModelEx {
+
+    /**
+     * @return The iterator that walks through the key-value pairs in the hash. Not {@code null}. 
+     */
+    KeyValuePairIterator keyValuePairIterator() throws TemplateModelException;
+    
+    /**
+     * A key-value pair in a hash; used for {@link KeyValuePairIterator}.
+     *  
+     * @since 2.3.25
+     */
+    interface KeyValuePair {
+        
+        /**
+         * @return Any type of {@link TemplateModel}, maybe {@code null} (if the hash entry key is {@code null}).
+         */
+        TemplateModel getKey() throws TemplateModelException;
+        
+        /**
+         * @return Any type of {@link TemplateModel}, maybe {@code null} (if the hash entry value is {@code null}).
+         */
+        TemplateModel getValue() throws TemplateModelException;
+    }
+    
+    /**
+     * Iterates over the key-value pairs in a hash. This is very similar to an {@link Iterator}, but has a oms item
+     * type, can throw {@link TemplateModelException}-s, and has no {@code remove()} method. 
+     *
+     * @since 2.3.25
+     */
+    interface KeyValuePairIterator {
+        
+        /**
+         * Similar to {@link Iterator#hasNext()}.
+         */
+        boolean hasNext() throws TemplateModelException;
+        
+        /**
+         * Similar to {@link Iterator#next()}.
+         * 
+         * @return Not {@code null}
+         * 
+         * @throws NoSuchElementException
+         */
+        KeyValuePair next() throws TemplateModelException;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateMarkupOutputModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateMarkupOutputModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateMarkupOutputModel.java
new file mode 100644
index 0000000..2215926
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateMarkupOutputModel.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.model;
+
+import org.apache.freemarker.core.outputformat.MarkupOutputFormat;
+import org.apache.freemarker.core.outputformat.OutputFormat;
+
+/**
+ * "markup output" template language data-type; stores markup (some kind of "rich text" / structured format, as opposed
+ * to plain text) that meant to be printed as template output. This type is related to the {@link OutputFormat}
+ * mechanism. Values of this kind are exempt from {@link OutputFormat}-based automatic escaping.
+ * 
+ * <p>
+ * Each implementation of this type has a {@link OutputFormat} subclass pair, whose singleton instance is returned by
+ * {@link #getOutputFormat()}. See more about how markup output values work at {@link OutputFormat}.
+ * 
+ * <p>
+ * Note that {@link TemplateMarkupOutputModel}-s are by design not treated like {@link TemplateScalarModel}-s, and so
+ * the implementations of this interface usually shouldn't implement {@link TemplateScalarModel}. (Because, operations
+ * applicable on plain strings, like converting to upper case, substringing, etc., can corrupt markup.) If the template
+ * author wants to pass in the "source" of the markup as string somewhere, he should use {@code ?markup_string}.
+ * 
+ * @param <MO>
+ *            Refers to the interface's own type, which is useful in interfaces that extend
+ *            {@link TemplateMarkupOutputModel} (Java Generics trick).
+ * 
+ * @since 2.3.24
+ */
+public interface TemplateMarkupOutputModel<MO extends TemplateMarkupOutputModel<MO>> extends TemplateModel {
+
+    /**
+     * Returns the singleton {@link OutputFormat} object that implements the operations for the "markup output" value.
+     */
+    MarkupOutputFormat<MO> getOutputFormat();
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateMethodModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateMethodModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateMethodModel.java
new file mode 100644
index 0000000..5bfe7e3
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateMethodModel.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.
+ */
+
+/*
+ * 22 October 1999: This class added by Holger Arendt.
+ */
+
+package org.apache.freemarker.core.model;
+
+import java.util.List;
+
+import org.apache.freemarker.core.Environment;
+
+/**
+ * "method" template language data type: Objects that act like functions. The name comes from that their original
+ * application was calling Java methods via {@link org.apache.freemarker.core.model.impl.DefaultObjectWrapper}.
+ * 
+ * <p>In templates they are used like {@code myMethod("foo", "bar")} or {@code myJavaObject.myJavaMethod("foo", "bar")}. 
+ * 
+ * @deprecated Use {@link TemplateMethodModelEx} instead. This interface is from the old times when the only kind of
+ *    value you could pass in was string.
+ */
+@Deprecated
+public interface TemplateMethodModel extends TemplateModel {
+
+    /**
+     * Executes the method call. All arguments passed to the method call are 
+     * coerced to strings before being passed, if the FreeMarker rules allow
+     * the coercion. If some of the passed arguments can not be coerced to a
+     * string, an exception will be raised in the engine and the method will 
+     * not be called. If your method would like to act on actual data model 
+     * objects instead of on their string representations, implement the 
+     * {@link TemplateMethodModelEx} instead.
+     * 
+     * @param arguments a <tt>List</tt> of <tt>String</tt> objects
+     *     containing the values of the arguments passed to the method.
+     *  
+     * @return the return value of the method, or {@code null}. If the returned value
+     *     does not implement {@link TemplateModel}, it will be automatically 
+     *     wrapped using the {@link Environment#getObjectWrapper() environment 
+     *     object wrapper}.
+     */
+    Object exec(List arguments) throws TemplateModelException;
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateMethodModelEx.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateMethodModelEx.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateMethodModelEx.java
new file mode 100644
index 0000000..2517d22
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateMethodModelEx.java
@@ -0,0 +1,54 @@
+/*
+ * 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;
+
+import java.util.List;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.util.DeepUnwrap;
+
+/**
+ * "extended method" template language data type: Objects that act like functions. Their main application is calling
+ * Java methods via {@link org.apache.freemarker.core.model.impl.DefaultObjectWrapper}, but you can implement this interface to invoke
+ * top-level functions too. They are "extended" compared to the deprecated {@link TemplateMethodModel}, which could only
+ * accept string parameters.
+ * 
+ * <p>In templates they are used like {@code myMethod(1, "foo")} or {@code myJavaObject.myJavaMethod(1, "foo")}.
+ */
+public interface TemplateMethodModelEx extends TemplateMethodModel {
+
+    /**
+     * Executes the method call.
+     *  
+     * @param arguments a {@link List} of {@link TemplateModel}-s,
+     *     containing the arguments passed to the method. If the implementation absolutely wants 
+     *     to operate on POJOs, it can use the static utility methods in the {@link DeepUnwrap} 
+     *     class to easily obtain them. However, unwrapping is not always possible (or not perfectly), and isn't always
+     *     efficient, so it's recommended to use the original {@link TemplateModel} value as much as possible.
+     *      
+     * @return the return value of the method, or {@code null}. If the returned value
+     *     does not implement {@link TemplateModel}, it will be automatically 
+     *     wrapped using the {@link Environment#getObjectWrapper() environment's 
+     *     object wrapper}.
+     */
+    @Override
+    Object exec(List arguments) throws TemplateModelException;
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateModel.java
new file mode 100644
index 0000000..bbe3c03
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateModel.java
@@ -0,0 +1,55 @@
+/*
+ * 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;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.util.FTLUtil;
+
+/**
+ * The common super-interface of the interfaces that stand for the FreeMarker Template Language (FTL) data types.
+ * The template language only deals with {@link TemplateModel}-s, not directly with plain Java objects. (For example,
+ * it doesn't understand {@link java.lang.Number}, but {@link TemplateNumberModel}.) This is why the
+ * data-model (aka. the "template context" in other languages) is (automatically) mapped to a tree of
+ * {@link TemplateModel}-s.
+ * 
+ * <p>Mapping the plain Java objects to {@link TemplateModel}-s (or the other way around sometimes) is the
+ * responsibility of the {@link ObjectWrapper} (see the {@link Configuration#getObjectWrapper objectWrapper} setting).
+ * But not all {@link TemplateModel}-s are for wrapping a plain object. For example, a value created within a template
+ * is not made to wrap an earlier existing object; it's a value that has always existed in the template language's
+ * domain. Users can also write {@link TemplateModel} implementations and put them directly into the data-model for
+ * full control over how that object is seen from the template. Certain {@link TemplateModel} interfaces doesn't
+ * even have equivalent in Java. For example the directive type ({@link TemplateDirectiveModel}) is like that.
+ * 
+ * <p>Because {@link TemplateModel} "subclasses" are all interfaces, a value in the template language can have multiple
+ * types. However, to prevent ambiguous situations, it's not recommended to make values that implement more than one of
+ * these types: string, number, boolean, date. The intended applications are like string+hash, string+method,
+ * hash+sequence, etc.
+ * 
+ * @see FTLUtil#getTypeDescription(TemplateModel)
+ */
+public interface TemplateModel {
+    
+    /**
+     * A general-purpose object to represent nothing. It acts as
+     * an empty string, false, empty sequence, empty hash, and
+     * null-returning method model.
+     */
+    TemplateModel NOTHING = GeneralPurposeNothing.INSTANCE;
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateModelAdapter.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateModelAdapter.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateModelAdapter.java
new file mode 100644
index 0000000..a48c065
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateModelAdapter.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+/**
+ * Implemented by classes that serve as adapters for template model objects in
+ * some other object model. Actually a functional inverse of 
+ * {@link AdapterTemplateModel}. You will rarely implement this interface 
+ * directly. It is usually implemented by unwrapping adapter classes of various 
+ * object wrapper implementations.
+ */
+public interface TemplateModelAdapter {
+    /**
+     * @return the template model this object is wrapping.
+     */
+    TemplateModel getTemplateModel();
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateModelException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateModelException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateModelException.java
new file mode 100644
index 0000000..d38faa4
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateModelException.java
@@ -0,0 +1,111 @@
+/*
+ * 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;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core._ErrorDescriptionBuilder;
+
+/**
+ * {@link TemplateModel} methods throw this exception if the requested data can't be retrieved.  
+ */
+public class TemplateModelException extends TemplateException {
+
+    /**
+     * Constructs a <tt>TemplateModelException</tt> with no
+     * specified detail message.
+     */
+    public TemplateModelException() {
+        this(null, null);
+    }
+
+    /**
+     * Constructs a <tt>TemplateModelException</tt> with the
+     * specified detail message.
+     *
+     * @param description the detail message.
+     */
+    public TemplateModelException(String description) {
+        this(description, null);
+    }
+
+    /**
+     * The same as {@link #TemplateModelException(Throwable)}; it's exists only for binary
+     * backward-compatibility.
+     */
+    public TemplateModelException(Exception cause) {
+        this(null, cause);
+    }
+
+    /**
+     * Constructs a <tt>TemplateModelException</tt> with the given underlying
+     * Exception, but no detail message.
+     *
+     * @param cause the underlying {@link Exception} that caused this
+     * exception to be raised
+     */
+    public TemplateModelException(Throwable cause) {
+        this(null, cause);
+    }
+
+    
+    /**
+     * The same as {@link #TemplateModelException(String, Throwable)}; it's exists only for binary
+     * backward-compatibility.
+     */
+    public TemplateModelException(String description, Exception cause) {
+        super(description, cause, null);
+    }
+
+    /**
+     * Constructs a TemplateModelException with both a description of the error
+     * that occurred and the underlying Exception that caused this exception
+     * to be raised.
+     *
+     * @param description the description of the error that occurred
+     * @param cause the underlying {@link Exception} that caused this
+     * exception to be raised
+     */
+    public TemplateModelException(String description, Throwable cause) {
+        super(description, cause, null);
+    }
+
+    /**
+     * Don't use this; this is to be used internally by FreeMarker.
+     * @param preventAmbiguity its value is ignored; it's only to prevent constructor selection ambiguities for
+     *     backward-compatibility
+     */
+    protected TemplateModelException(Throwable cause, Environment env, String description,
+            boolean preventAmbiguity) {
+        super(description, cause, env);
+    }
+    
+    /**
+     * Don't use this; this is to be used internally by FreeMarker.
+     * @param preventAmbiguity its value is ignored; it's only to prevent constructor selection ambiguities for
+     *     backward-compatibility
+     */
+    protected TemplateModelException(
+            Throwable cause, Environment env, _ErrorDescriptionBuilder descriptionBuilder,
+            boolean preventAmbiguity) {
+        super(cause, env, null, descriptionBuilder);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateModelIterator.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateModelIterator.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateModelIterator.java
new file mode 100644
index 0000000..9d1e241
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateModelIterator.java
@@ -0,0 +1,39 @@
+/*
+ * 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;
+
+/**
+ * Used to iterate over a set of template models <em>once</em>; usually returned from
+ * {@link TemplateCollectionModel#iterator()}. Note that it's not a {@link TemplateModel}.
+ */
+public interface TemplateModelIterator {
+
+    /**
+     * Returns the next model.
+     * @throws TemplateModelException if the next model can not be retrieved
+     *   (i.e. because the iterator is exhausted).
+     */
+    TemplateModel next() throws TemplateModelException;
+
+    /**
+     * @return whether there are any more items to iterate over.
+     */
+    boolean hasNext() throws TemplateModelException;
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateModelWithAPISupport.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateModelWithAPISupport.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateModelWithAPISupport.java
new file mode 100644
index 0000000..c1a01fe
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateModelWithAPISupport.java
@@ -0,0 +1,39 @@
+/*
+ * 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;
+
+/**
+ * <b>Experimental - subject to change:</b> A {@link TemplateModel} on which the {@code ?api} operation can be applied.
+ * 
+ * <p>
+ * <b>Experimental status warning:</b> This interface is subject to change on non-backward compatible ways, hence, it
+ * shouldn't be implemented outside FreeMarker yet.
+ * 
+ * @since 2.3.22
+ */
+public interface TemplateModelWithAPISupport extends TemplateModel {
+
+    /**
+     * Returns the model that exposes the (Java) API of the value. This is usually implemented by delegating to
+     * {@link ObjectWrapperWithAPISupport#wrapAsAPI(Object)}.
+     */
+    TemplateModel getAPI() throws TemplateModelException;
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateNodeModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateNodeModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateNodeModel.java
new file mode 100644
index 0000000..afa9da6
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateNodeModel.java
@@ -0,0 +1,78 @@
+/*
+ * 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;
+
+/**
+ * "node" template language data type: an object that is a node in a tree.
+ * A tree of nodes can be recursively <em>visited</em> using the &lt;#visit...&gt; and &lt;#recurse...&gt;
+ * directives. This API is largely based on the W3C Document Object Model
+ * (DOM_WRAPPER) API. However, it's meant to be generally useful for describing
+ * any tree of objects that you wish to navigate using a recursive visitor
+ * design pattern (or simply through being able to get the parent
+ * and child nodes).
+ * 
+ * <p>See the <a href="http://freemarker.org/docs/xgui.html" target="_blank">XML
+ * Processing Guide</a> for a concrete application.
+ *
+ * @since FreeMarker 2.3
+ */
+public interface TemplateNodeModel extends TemplateModel {
+    
+    /**
+     * @return the parent of this node or null, in which case
+     * this node is the root of the tree.
+     */
+    TemplateNodeModel getParentNode() throws TemplateModelException;
+    
+    /**
+     * @return a sequence containing this node's children.
+     * If the returned value is null or empty, this is essentially 
+     * a leaf node.
+     */
+    TemplateSequenceModel getChildNodes() throws TemplateModelException;
+
+    /**
+     * @return a String that is used to determine the processing
+     * routine to use. In the XML implementation, if the node 
+     * is an element, it returns the element's tag name.  If it
+     * is an attribute, it returns the attribute's name. It 
+     * returns "@text" for text nodes, "@pi" for processing instructions,
+     * and so on.
+     */    
+    String getNodeName() throws TemplateModelException;
+    
+    /**
+     * @return a String describing the <em>type</em> of node this is.
+     * In the W3C DOM_WRAPPER, this should be "element", "text", "attribute", etc.
+     * A TemplateNodeModel implementation that models other kinds of
+     * trees could return whatever it appropriate for that application. It
+     * can be null, if you don't want to use node-types.
+     */
+    String getNodeType() throws TemplateModelException;
+    
+    
+    /**
+     * @return the XML namespace URI with which this node is 
+     * associated. If this TemplateNodeModel implementation is 
+     * not XML-related, it will almost certainly be null. Even 
+     * for XML nodes, this will often be null.
+     */
+    String getNodeNamespace() throws TemplateModelException;
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateNodeModelEx.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateNodeModelEx.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateNodeModelEx.java
new file mode 100644
index 0000000..acf43df
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateNodeModelEx.java
@@ -0,0 +1,40 @@
+/*
+ * 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;
+
+import org.apache.freemarker.dom.NodeModel;
+
+/**
+ * A {@link NodeModel} that supports navigating to the previous and next sibling nodes.
+ * 
+ * @since 2.3.26
+ */
+public interface TemplateNodeModelEx extends TemplateNodeModel {
+
+    /**
+     * @return The immediate previous sibling of this node, or {@code null} if there's no such node.
+     */
+    TemplateNodeModelEx getPreviousSibling() throws TemplateModelException;
+
+    /**
+     * @return The immediate next sibling of this node, or {@code null} if there's no such node.
+     */
+    TemplateNodeModelEx getNextSibling() throws TemplateModelException;
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateNumberModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateNumberModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateNumberModel.java
new file mode 100644
index 0000000..ba1240d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateNumberModel.java
@@ -0,0 +1,42 @@
+/*
+ * 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;
+
+import org.apache.freemarker.core.arithmetic.ArithmeticEngine;
+
+/**
+ * "number" template language data type; an object that stores a number. There's only one numerical type as far as the
+ * template language is concerned, but it can store its value using whatever Java number type. Making operations between
+ * numbers (and so the coercion rules) is up to the {@link ArithmeticEngine}.
+ * 
+ * <p>
+ * Objects of this type should be immutable, that is, calling {@link #getAsNumber()} should always return the same value
+ * as for the first time.
+ */
+public interface TemplateNumberModel extends TemplateModel {
+
+    /**
+     * Returns the numeric value. The return value must not be {@code null}.
+     *
+     * @return the {@link Number} instance associated with this number model.
+     */
+    Number getAsNumber() throws TemplateModelException;
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateScalarModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateScalarModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateScalarModel.java
new file mode 100644
index 0000000..b76a097
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateScalarModel.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;
+
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+
+/**
+ * "string" template language data-type; like in Java, an unmodifiable UNICODE character sequence.
+ * (The name of this interface should be {@code TemplateStringModel}. The misnomer is inherited from the
+ * old times, when this was the only single-value type in FreeMarker.)
+ */
+public interface TemplateScalarModel extends TemplateModel {
+
+    /**
+     * A constant value to use as the empty string.
+     */
+    TemplateModel EMPTY_STRING = new SimpleScalar("");
+
+    /**
+     * Returns the string representation of this model. Don't return {@code null}, as that will cause exception.
+     * 
+     * <p>
+     * Objects of this type should be immutable, that is, calling {@link #getAsString()} should always return the same
+     * value as for the first time.
+     */
+    String getAsString() throws TemplateModelException;
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateSequenceModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateSequenceModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateSequenceModel.java
new file mode 100644
index 0000000..8ca3944
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateSequenceModel.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+/**
+ * "sequence" template language data type; an object that contains other objects accessible through an integer 0-based
+ * index.
+ * 
+ * <p>
+ * Used in templates like: {@code mySeq[index]}, {@code <#list mySeq as i>...</#list>}, {@code mySeq?size}, etc.
+ * 
+ * @see TemplateCollectionModel
+ */
+public interface TemplateSequenceModel extends TemplateModel {
+
+    /**
+     * Retrieves the i-th template model in this sequence.
+     * 
+     * @return the item at the specified index, or <code>null</code> if the index is out of bounds. Note that a
+     *         <code>null</code> value is interpreted by FreeMarker as "variable does not exist", and accessing a
+     *         missing variables is usually considered as an error in the FreeMarker Template Language, so the usage of
+     *         a bad index will not remain hidden, unless the default value for that case was also specified in the
+     *         template.
+     */
+    TemplateModel get(int index) throws TemplateModelException;
+
+    /**
+     * @return the number of items in the list.
+     */
+    int size() throws TemplateModelException;
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateTransformModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateTransformModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateTransformModel.java
new file mode 100644
index 0000000..789d9bb
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateTransformModel.java
@@ -0,0 +1,54 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Map;
+
+import org.apache.freemarker.core.util.DeepUnwrap;
+
+/**
+ * "transform" template language data type: user-defined directives 
+ * (much like macros) specialized on filtering output; you should rather use the newer {@link TemplateDirectiveModel}
+ * instead. This certainly will be deprecated in FreeMarker 2.4.
+ */
+public interface TemplateTransformModel extends TemplateModel {
+
+     /**
+      * Returns a writer that will be used by the engine to feed the
+      * transformation input to the transform. Each call to this method
+      * must return a new instance of the writer so that the transformation
+      * is thread-safe.
+      * @param out the character stream to which to write the transformed output
+      * @param args the arguments (if any) passed to the transformation as a 
+      * map of key/value pairs where the keys are strings and the arguments are
+      * TemplateModel instances. This is never null. If you need to convert the
+      * template models to POJOs, you can use the utility methods in the 
+      * {@link DeepUnwrap} class.
+      * @return a writer to which the engine will feed the transformation 
+      * input, or null if the transform does not support nested content (body).
+      * The returned writer can implement the {@link TransformControl}
+      * interface if it needs advanced control over the evaluation of the 
+      * transformation body.
+      */
+     Writer getWriter(Writer out, Map args) 
+         throws TemplateModelException, IOException;
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/TransformControl.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TransformControl.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TransformControl.java
new file mode 100644
index 0000000..cd3965c
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TransformControl.java
@@ -0,0 +1,101 @@
+/*
+ * 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;
+
+import java.io.IOException;
+
+import org.apache.freemarker.core.TemplateException;
+
+/**
+ * An interface that can be implemented by writers returned from
+ * {@link TemplateTransformModel#getWriter(java.io.Writer, java.util.Map)}. The
+ * methods on this
+ * interfaces are callbacks that will be called by the template engine and that
+ * give the writer a chance to better control the evaluation of the transform
+ * body. The writer can instruct the engine to skip or to repeat body 
+ * evaluation, and gets notified about exceptions that are thrown during the
+ * body evaluation.
+ */
+public interface TransformControl {
+    /**
+     * Constant returned from {@link #afterBody()} that tells the
+     * template engine to repeat transform body evaluation and feed
+     * it again to the transform.
+     */
+    int REPEAT_EVALUATION = 0;
+
+    /**
+     * Constant returned from {@link #afterBody()} that tells the
+     * template engine to end the transform and close the writer.
+     */
+    int END_EVALUATION = 1;
+ 
+    /**
+     * Constant returned from {@link #onStart()} that tells the
+     * template engine to skip evaluation of the body.
+     */
+    int SKIP_BODY = 0;
+    
+    /**
+     * Constant returned from {@link #onStart()} that tells the
+     * template engine to evaluate the body.
+     */
+    int EVALUATE_BODY = 1;
+
+    /**
+     * Called before the body is evaluated for the first time.
+     * @return 
+     * <ul>
+     * <li><tt>SKIP_BODY</tt> if the transform wants to ignore the body. In this
+     * case, only {@link java.io.Writer#close()} is called next and processing ends.</li>
+     * <li><tt>EVALUATE_BODY</tt> to normally evaluate the body of the transform
+     * and feed it to the writer</li>
+     * </ul>
+     */
+    int onStart() throws TemplateModelException, IOException;
+    
+    /**
+     * Called after the body has been evaluated.
+     * @return
+     * <ul>
+     * <li><tt>END_EVALUATION</tt> if the transformation should be ended.</li>
+     * <li><tt>REPEAT_EVALUATION</tt> to have the engine re-evaluate the 
+     * transform body and feed it again to the writer.</li>
+     * </ul>
+     */
+    int afterBody() throws TemplateModelException, IOException;
+    
+    /**
+     * Called if any exception occurs during the transform between the
+     * {@link TemplateTransformModel#getWriter(java.io.Writer, java.util.Map)} call
+     * and the {@link java.io.Writer#close()} call.
+     * @param t the throwable that represents the exception. It can be any 
+     * non-checked throwable, as well as {@link TemplateException} and 
+     * {@link java.io.IOException}.
+     * 
+     * @throws Throwable is recommended that the methods rethrow the received 
+     * throwable. If the method wants to throw another throwable, it should
+     * either throw a non-checked throwable, or an instance of 
+     * {@link TemplateException} and {@link java.io.IOException}. Throwing any
+     * other checked exception will cause the engine to rethrow it as
+     * a {@link java.lang.reflect.UndeclaredThrowableException}.
+     */
+    void onError(Throwable t) throws Throwable;
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/TrueTemplateBooleanModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TrueTemplateBooleanModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TrueTemplateBooleanModel.java
new file mode 100644
index 0000000..f10ae71
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TrueTemplateBooleanModel.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+/**
+ * Used for the {@link TemplateBooleanModel#FALSE} singleton. 
+ */
+final class TrueTemplateBooleanModel implements SerializableTemplateBooleanModel {
+
+    @Override
+    public boolean getAsBoolean() {
+        return true;
+    }
+
+    private Object readResolve() {
+        return TRUE;
+    }
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/WrapperTemplateModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/WrapperTemplateModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/WrapperTemplateModel.java
new file mode 100644
index 0000000..1a30bf1
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/WrapperTemplateModel.java
@@ -0,0 +1,33 @@
+/*
+ * 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;
+
+/**
+ * A generic interface for template models that wrap some underlying
+ * object, and wish to provide access to that wrapped object.
+ * 
+ * <p>You may also want to implement {@link org.apache.freemarker.core.model.AdapterTemplateModel}.
+ */
+public interface WrapperTemplateModel extends TemplateModel {
+    /**
+     * Retrieves the original object wrapped by this model.
+     */
+    Object getWrappedObject();
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/WrappingTemplateModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/WrappingTemplateModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/WrappingTemplateModel.java
new file mode 100644
index 0000000..206d9d4
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/WrappingTemplateModel.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.model;
+
+import org.apache.freemarker.core.util._NullArgumentException;
+
+/**
+ * Convenience base-class for containers that wrap their contained arbitrary Java objects into {@link TemplateModel}
+ * instances.
+ */
+abstract public class WrappingTemplateModel {
+
+    private final ObjectWrapper objectWrapper;
+
+    /**
+     * Protected constructor that creates a new wrapping template model using the specified object wrapper.
+     * 
+     * @param objectWrapper the wrapper to use. Passing {@code null} to it
+     *     is allowed but deprecated. Not {@code null}.
+     */
+    protected WrappingTemplateModel(ObjectWrapper objectWrapper) {
+        _NullArgumentException.check("objectWrapper", objectWrapper);
+        this.objectWrapper = objectWrapper;
+    }
+    
+    /**
+     * Returns the object wrapper instance used by this wrapping template model.
+     */
+    public ObjectWrapper getObjectWrapper() {
+        return objectWrapper;
+    }
+
+    /**
+     * Wraps the passed object into a template model using this object's object
+     * wrapper.
+     * @param obj the object to wrap
+     * @return the template model that wraps the object
+     * @throws TemplateModelException if the wrapper does not know how to
+     * wrap the passed object.
+     */
+    protected final TemplateModel wrap(Object obj) throws TemplateModelException {
+            return objectWrapper.wrap(obj);
+    }
+    
+}



[03/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/CustomAttributeTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/CustomAttributeTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/CustomAttributeTest.java
new file mode 100644
index 0000000..726a20c
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/CustomAttributeTest.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.math.BigDecimal;
+import java.util.Arrays;
+
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+@SuppressWarnings("boxing")
+public class CustomAttributeTest {
+    
+    private static final String KEY_1 = "key1";
+    private static final String KEY_2 = "key2";
+    private static final String KEY_3 = "key3";
+    private static final Integer KEY_4 = 4;
+
+    private static final Integer VALUE_1 = 1; // Serializable
+    private static final Object VALUE_2 = new Object();
+    private static final Object VALUE_3 = new Object();
+    private static final Object VALUE_4 = new Object();
+    private static final Object VALUE_LIST = ImmutableList.<Object>of(
+            "s", BigDecimal.valueOf(2), Boolean.TRUE, ImmutableMap.of("a", "A"));
+    private static final Object VALUE_BIGDECIMAL = BigDecimal.valueOf(22);
+
+    private static final Object CUST_ATT_KEY = new Object();
+
+    @Test
+    public void testStringKey() throws Exception {
+        // Need some MutableProcessingConfiguration:
+        TemplateConfiguration.Builder mpc = new TemplateConfiguration.Builder();
+
+        assertEquals(0, mpc.getCustomAttributeNames().length);
+        assertNull(mpc.getCustomAttribute(KEY_1));
+        
+        mpc.setCustomAttribute(KEY_1, VALUE_1);
+        assertArrayEquals(new String[] { KEY_1 }, mpc.getCustomAttributeNames());
+        assertSame(VALUE_1, mpc.getCustomAttribute(KEY_1));
+        
+        mpc.setCustomAttribute(KEY_2, VALUE_2);
+        assertArrayEquals(new String[] { KEY_1, KEY_2 }, sort(mpc.getCustomAttributeNames()));
+        assertSame(VALUE_1, mpc.getCustomAttribute(KEY_1));
+        assertSame(VALUE_2, mpc.getCustomAttribute(KEY_2));
+
+        mpc.setCustomAttribute(KEY_1, VALUE_2);
+        assertArrayEquals(new String[] { KEY_1, KEY_2 }, sort(mpc.getCustomAttributeNames()));
+        assertSame(VALUE_2, mpc.getCustomAttribute(KEY_1));
+        assertSame(VALUE_2, mpc.getCustomAttribute(KEY_2));
+
+        mpc.setCustomAttribute(KEY_1, null);
+        assertArrayEquals(new String[] { KEY_1, KEY_2 }, sort(mpc.getCustomAttributeNames()));
+        assertNull(mpc.getCustomAttribute(KEY_1));
+        assertSame(VALUE_2, mpc.getCustomAttribute(KEY_2));
+
+        mpc.removeCustomAttribute(KEY_1);
+        assertArrayEquals(new String[] { KEY_2 }, mpc.getCustomAttributeNames());
+        assertNull(mpc.getCustomAttribute(KEY_1));
+        assertSame(VALUE_2, mpc.getCustomAttribute(KEY_2));
+    }
+
+    @Test
+    public void testRemoveFromEmptySet() throws Exception {
+        // Need some MutableProcessingConfiguration:
+        TemplateConfiguration.Builder mpc = new TemplateConfiguration.Builder();
+
+        mpc.removeCustomAttribute(KEY_1);
+        assertEquals(0, mpc.getCustomAttributeNames().length);
+        assertNull(mpc.getCustomAttribute(KEY_1));
+
+        mpc.setCustomAttribute(KEY_1, VALUE_1);
+        assertArrayEquals(new String[] { KEY_1 }, mpc.getCustomAttributeNames());
+        assertSame(VALUE_1, mpc.getCustomAttribute(KEY_1));
+    }
+
+    @Test
+    public void testAttrsFromFtlHeaderOnly() throws Exception {
+        Template t = new Template(null, "<#ftl attributes={"
+                + "'" + KEY_1 + "': [ 's', 2, true, {  'a': 'A' } ], "
+                + "'" + KEY_2 + "': " + VALUE_BIGDECIMAL + " "
+                + "}>",
+                new Configuration.Builder(Configuration.VERSION_3_0_0).build());
+
+        assertEquals(ImmutableSet.of(KEY_1, KEY_2), t.getCustomAttributes().keySet());
+        assertEquals(VALUE_LIST, t.getCustomAttribute(KEY_1));
+        assertEquals(VALUE_BIGDECIMAL, t.getCustomAttribute(KEY_2));
+
+        t.setCustomAttribute(KEY_1, VALUE_1);
+        assertEquals(VALUE_1, t.getCustomAttribute(KEY_1));
+        assertEquals(VALUE_BIGDECIMAL, t.getCustomAttribute(KEY_2));
+
+        t.setCustomAttribute(KEY_1, null);
+        assertEquals(ImmutableSet.of(KEY_1, KEY_2), t.getCustomAttributes().keySet());
+        assertNull(t.getCustomAttribute(KEY_1));
+    }
+
+    @Test
+    public void testAttrsFromFtlHeaderAndFromTemplateConfiguration() throws Exception {
+        TemplateConfiguration.Builder tcb = new TemplateConfiguration.Builder();
+        tcb.setCustomAttribute(KEY_3, VALUE_3);
+        tcb.setCustomAttribute(KEY_4, VALUE_4);
+        Template t = new Template(null, "<#ftl attributes={"
+                + "'" + KEY_1 + "': 'a', "
+                + "'" + KEY_2 + "': 'b', "
+                + "'" + KEY_3 + "': 'c' "
+                + "}>",
+                new Configuration.Builder(Configuration.VERSION_3_0_0).build(),
+                tcb.build());
+
+        assertEquals(ImmutableSet.of(KEY_1, KEY_2, KEY_3, KEY_4), t.getCustomAttributes().keySet());
+        assertEquals("a", t.getCustomAttribute(KEY_1));
+        assertEquals("b", t.getCustomAttribute(KEY_2));
+        assertEquals("c", t.getCustomAttribute(KEY_3)); // Has overridden TC attribute
+        assertEquals(VALUE_4, t.getCustomAttribute(KEY_4)); // Inherited TC attribute
+
+        t.setCustomAttribute(KEY_3, null);
+        assertEquals(ImmutableSet.of(KEY_1, KEY_2, KEY_3, KEY_4), t.getCustomAttributes().keySet());
+        assertNull("null value shouldn't cause fallback to TC attribute", t.getCustomAttribute(KEY_3));
+    }
+
+
+    @Test
+    public void testAttrsFromTemplateConfigurationOnly() throws Exception {
+        TemplateConfiguration.Builder tcb = new TemplateConfiguration.Builder();
+        tcb.setCustomAttribute(KEY_3, VALUE_3);
+        tcb.setCustomAttribute(KEY_4, VALUE_4);
+        Template t = new Template(null, "",
+                new Configuration.Builder(Configuration.VERSION_3_0_0).build(),
+                tcb.build());
+
+        assertEquals(ImmutableSet.of(KEY_3, KEY_4), t.getCustomAttributes().keySet());
+        assertEquals(VALUE_3, t.getCustomAttribute(KEY_3));
+        assertEquals(VALUE_4, t.getCustomAttribute(KEY_4));
+    }
+
+    private Object[] sort(String[] customAttributeNames) {
+        Arrays.sort(customAttributeNames);
+        return customAttributeNames;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/DateFormatTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/DateFormatTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/DateFormatTest.java
new file mode 100644
index 0000000..4ad5937
--- /dev/null
+++ b/freemarker-core/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/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/DirectiveCallPlaceTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/DirectiveCallPlaceTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/DirectiveCallPlaceTest.java
new file mode 100644
index 0000000..29220c4
--- /dev/null
+++ b/freemarker-core/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/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/EncodingOverrideTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/EncodingOverrideTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/EncodingOverrideTest.java
new file mode 100644
index 0000000..b1acead
--- /dev/null
+++ b/freemarker-core/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/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/EnvironmentGetTemplateVariantsTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/EnvironmentGetTemplateVariantsTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/EnvironmentGetTemplateVariantsTest.java
new file mode 100644
index 0000000..b79559c
--- /dev/null
+++ b/freemarker-core/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/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/ExceptionTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/ExceptionTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/ExceptionTest.java
new file mode 100644
index 0000000..19c3b6e
--- /dev/null
+++ b/freemarker-core/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/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/GetSourceTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/GetSourceTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/GetSourceTest.java
new file mode 100644
index 0000000..1462bfc
--- /dev/null
+++ b/freemarker-core/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/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/HeaderParsingTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/HeaderParsingTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/HeaderParsingTest.java
new file mode 100644
index 0000000..6f8851d
--- /dev/null
+++ b/freemarker-core/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/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/IncludeAndImportConfigurableLayersTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/IncludeAndImportConfigurableLayersTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/IncludeAndImportConfigurableLayersTest.java
new file mode 100644
index 0000000..738b4de
--- /dev/null
+++ b/freemarker-core/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);
+    }
+
+}


[20/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/OverloadedMethodsSubset.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/OverloadedMethodsSubset.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/OverloadedMethodsSubset.java
new file mode 100644
index 0000000..e783af8
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/OverloadedMethodsSubset.java
@@ -0,0 +1,402 @@
+/*
+ * 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.io.Serializable;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.util._ClassUtil;
+import org.apache.freemarker.core.util._NullArgumentException;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+/**
+ * Encapsulates the rules and data structures (including cache) for choosing of the best matching callable member for
+ * a parameter list, from a given set of callable members. There are two subclasses of this, one for non-varags methods,
+ * and one for varargs methods.
+ */
+abstract class OverloadedMethodsSubset {
+    
+    /** 
+     * Used for an optimization trick to substitute an array of whatever size that contains only 0-s. Since this array
+     * is 0 long, this means that the code that reads the int[] always have to check if the int[] has this value, and
+     * then it has to act like if was all 0-s.  
+     */
+    static final int[] ALL_ZEROS_ARRAY = new int[0];
+
+    private static final int[][] ZERO_PARAM_COUNT_TYPE_FLAGS_ARRAY = new int[1][];
+    static {
+        ZERO_PARAM_COUNT_TYPE_FLAGS_ARRAY[0] = ALL_ZEROS_ARRAY;
+    }
+
+    private Class[/*number of args*/][/*arg index*/] unwrappingHintsByParamCount;
+    
+    /**
+     * Tells what types occur at a given parameter position with a bit field. See {@link TypeFlags}.
+     */
+    private int[/*number of args*/][/*arg index*/] typeFlagsByParamCount;
+    
+    // TODO: This can cause memory-leak when classes are re-loaded. However, first the genericClassIntrospectionCache
+    // and such need to be oms in this regard. 
+    private final Map/*<ArgumentTypes, MaybeEmptyCallableMemberDescriptor>*/ argTypesToMemberDescCache
+            = new ConcurrentHashMap(6, 0.75f, 1);
+    
+    private final List/*<ReflectionCallableMemberDescriptor>*/ memberDescs = new LinkedList();
+    
+    OverloadedMethodsSubset() {
+        //
+    }
+    
+    void addCallableMemberDescriptor(ReflectionCallableMemberDescriptor memberDesc) {
+        memberDescs.add(memberDesc);
+        
+        // Warning: Do not modify this array, or put it into unwrappingHintsByParamCount by reference, as the arrays
+        // inside that are modified!
+        final Class[] prepedParamTypes = preprocessParameterTypes(memberDesc);
+        final int paramCount = prepedParamTypes.length;  // Must be the same as the length of the original param list
+        
+        // Merge these unwrapping hints with the existing table of hints:
+        if (unwrappingHintsByParamCount == null) {
+            unwrappingHintsByParamCount = new Class[paramCount + 1][];
+            unwrappingHintsByParamCount[paramCount] = prepedParamTypes.clone();
+        } else if (unwrappingHintsByParamCount.length <= paramCount) {
+            Class[][] newUnwrappingHintsByParamCount = new Class[paramCount + 1][];
+            System.arraycopy(unwrappingHintsByParamCount, 0, newUnwrappingHintsByParamCount, 0,
+                    unwrappingHintsByParamCount.length);
+            unwrappingHintsByParamCount = newUnwrappingHintsByParamCount;
+            unwrappingHintsByParamCount[paramCount] = prepedParamTypes.clone();
+        } else {
+            Class[] unwrappingHints = unwrappingHintsByParamCount[paramCount]; 
+            if (unwrappingHints == null) {
+                unwrappingHintsByParamCount[paramCount] = prepedParamTypes.clone();
+            } else {
+                for (int paramIdx = 0; paramIdx < prepedParamTypes.length; paramIdx++) {
+                    // For each parameter list length, we merge the argument type arrays into a single Class[] that
+                    // stores the most specific common types for each position. Hence we will possibly use a too generic
+                    // hint for the unwrapping. For correct behavior, for each overloaded methods its own parameter
+                    // types should be used as a hint. But without unwrapping the arguments, we couldn't select the
+                    // overloaded method. So we had to unwrap with all possible target types of each parameter position,
+                    // which would be slow and its result would be uncacheable (as we don't have anything usable as
+                    // a lookup key). So we just use this compromise.
+                    unwrappingHints[paramIdx] = getCommonSupertypeForUnwrappingHint(
+                            unwrappingHints[paramIdx], prepedParamTypes[paramIdx]);
+                }
+            }
+        }
+
+        int[] typeFlagsByParamIdx = ALL_ZEROS_ARRAY;
+        // Fill typeFlagsByParamCount (if necessary)
+        for (int paramIdx = 0; paramIdx < paramCount; paramIdx++) {
+            final int typeFlags = TypeFlags.classToTypeFlags(prepedParamTypes[paramIdx]);
+            if (typeFlags != 0) {
+                if (typeFlagsByParamIdx == ALL_ZEROS_ARRAY) {
+                    typeFlagsByParamIdx = new int[paramCount];
+                }
+                typeFlagsByParamIdx[paramIdx] = typeFlags;
+            }
+        }
+        mergeInTypesFlags(paramCount, typeFlagsByParamIdx);
+        
+        afterWideningUnwrappingHints(prepedParamTypes, typeFlagsByParamIdx);
+    }
+    
+    Class[][] getUnwrappingHintsByParamCount() {
+        return unwrappingHintsByParamCount;
+    }
+    
+    @SuppressFBWarnings(value="JLM_JSR166_UTILCONCURRENT_MONITORENTER",
+            justification="Locks for member descriptor creation only")
+    final MaybeEmptyCallableMemberDescriptor getMemberDescriptorForArgs(Object[] args, boolean varArg) {
+        ArgumentTypes argTypes = new ArgumentTypes(args);
+        MaybeEmptyCallableMemberDescriptor memberDesc
+                = (MaybeEmptyCallableMemberDescriptor) argTypesToMemberDescCache.get(argTypes);
+        if (memberDesc == null) {
+            // Synchronized so that we won't unnecessarily invoke the same member desc. for multiple times in parallel.
+            synchronized (argTypesToMemberDescCache) {
+                memberDesc = (MaybeEmptyCallableMemberDescriptor) argTypesToMemberDescCache.get(argTypes);
+                if (memberDesc == null) {
+                    memberDesc = argTypes.getMostSpecific(memberDescs, varArg);
+                    argTypesToMemberDescCache.put(argTypes, memberDesc);
+                }
+            }
+        }
+        return memberDesc;
+    }
+    
+    Iterator/*<ReflectionCallableMemberDescriptor>*/ getMemberDescriptors() {
+        return memberDescs.iterator();
+    }
+    
+    abstract Class[] preprocessParameterTypes(CallableMemberDescriptor memberDesc);
+    abstract void afterWideningUnwrappingHints(Class[] paramTypes, int[] paramNumericalTypes);
+    
+    abstract MaybeEmptyMemberAndArguments getMemberAndArguments(List/*<TemplateModel>*/ tmArgs, 
+            DefaultObjectWrapper unwrapper) throws TemplateModelException;
+
+    /**
+     * Returns the most specific common class (or interface) of two parameter types for the purpose of unwrapping.
+     * This is trickier than finding the most specific overlapping superclass of two classes, because:
+     * <ul>
+     *   <li>It considers primitive classes as the subclasses of the boxing classes.</li>
+     *   <li>If the only common class is {@link Object}, it will try to find a common interface. If there are more
+     *       of them, it will start removing those that are known to be uninteresting as unwrapping hints.</li>
+     * </ul>
+     * 
+     * @param c1 Parameter type 1
+     * @param c2 Parameter type 2
+     */
+    protected Class getCommonSupertypeForUnwrappingHint(Class c1, Class c2) {
+        if (c1 == c2) return c1;
+        // This also means that the hint for (Integer, Integer) will be Integer, not just Number. This is consistent
+        // with how non-overloaded method hints work.
+        
+        // c1 primitive class to boxing class:
+        final boolean c1WasPrim; 
+        if (c1.isPrimitive()) {
+            c1 = _ClassUtil.primitiveClassToBoxingClass(c1);
+            c1WasPrim = true;
+        } else {
+            c1WasPrim = false;
+        }
+        
+        // c2 primitive class to boxing class:
+        final boolean c2WasPrim; 
+        if (c2.isPrimitive()) {
+            c2 = _ClassUtil.primitiveClassToBoxingClass(c2);
+            c2WasPrim = true;
+        } else {
+            c2WasPrim = false;
+        }
+
+        if (c1 == c2) {
+            // If it was like int and Integer, boolean and Boolean, etc., we return the boxing type (as that's the
+            // less specific, because it allows null.)
+            // (If it was two equivalent primitives, we don't get here, because of the 1st line of the method.) 
+            return c1;
+        } else if (Number.class.isAssignableFrom(c1) && Number.class.isAssignableFrom(c2)) {
+            // We don't want the unwrapper to convert to a numerical super-type [*] as it's not yet known what the
+            // actual number type of the chosen method will be. We will postpone the actual numerical conversion
+            // until that, especially as some conversions (like oms point to floating point) can be lossy.
+            // * Numerical super-type: Like long > int > short > byte.  
+            return Number.class;
+        } else if (c1WasPrim || c2WasPrim) {
+            // At this point these all stand:
+            // - At least one of them was primitive
+            // - No more than one of them was numerical
+            // - They don't have the same wrapper (boxing) class
+            return Object.class;
+        }
+        
+        // We never get to this point if buxfixed is true and any of these stands:
+        // - One of classes was a primitive type
+        // - One of classes was a numerical type (either boxing type or primitive)
+        
+        Set commonTypes = _MethodUtil.getAssignables(c1, c2);
+        commonTypes.retainAll(_MethodUtil.getAssignables(c2, c1));
+        if (commonTypes.isEmpty()) {
+            // Can happen when at least one of the arguments is an interface, as
+            // they don't have Object at the root of their hierarchy
+            return Object.class;
+        }
+        
+        // Gather maximally specific elements. Yes, there can be more than one 
+        // because of interfaces. I.e., if you call this method for String.class 
+        // and Number.class, you'll have Comparable, Serializable, and Object as 
+        // maximal elements. 
+        List max = new ArrayList();
+        listCommonTypes:  for (Iterator commonTypesIter = commonTypes.iterator(); commonTypesIter.hasNext(); ) {
+            Class clazz = (Class) commonTypesIter.next();
+            for (Iterator maxIter = max.iterator(); maxIter.hasNext(); ) {
+                Class maxClazz = (Class) maxIter.next();
+                if (_MethodUtil.isMoreOrSameSpecificParameterType(maxClazz, clazz, false /*bugfixed [1]*/, 0) != 0) {
+                    // clazz can't be maximal, if there's already a more specific or equal maximal than it.
+                    continue listCommonTypes;
+                }
+                if (_MethodUtil.isMoreOrSameSpecificParameterType(clazz, maxClazz, false /*bugfixed [1]*/, 0) != 0) {
+                    // If it's more specific than a currently maximal element,
+                    // that currently maximal is no longer a maximal.
+                    maxIter.remove();
+                }
+                // 1: We don't use bugfixed at the "[1]"-marked points because it's slower and doesn't make any
+                //    difference here as it's ensured that nor c1 nor c2 is primitive or numerical. The bugfix has only
+                //    affected the treatment of primitives and numerical types. 
+            }
+            // If we get here, no current maximal is more specific than the
+            // current class, so clazz is a new maximal so far.
+            max.add(clazz);
+        }
+        
+        if (max.size() > 1) {  // we have an ambiguity
+            // Find the non-interface class
+            for (Iterator it = max.iterator(); it.hasNext(); ) {
+                Class maxCl = (Class) it.next();
+                if (!maxCl.isInterface()) {
+                    if (maxCl != Object.class) {  // This actually shouldn't ever happen, but to be sure...
+                        // If it's not Object, we use it as the most specific
+                        return maxCl;
+                    } else {
+                        // Otherwise remove Object, and we will try with the interfaces 
+                        it.remove();
+                    }
+                }
+            }
+            
+            // At this point we only have interfaces left.
+            // Try removing interfaces about which we know that they are useless as unwrapping hints:
+            max.remove(Cloneable.class);
+            if (max.size() > 1) {  // Still have an ambiguity...
+                max.remove(Serializable.class);
+                if (max.size() > 1) {  // Still had an ambiguity...
+                    max.remove(Comparable.class);
+                    if (max.size() > 1) {
+                        return Object.class; // Still had an ambiguity... no luck.
+                    }
+                }
+            }
+        }
+        
+        return (Class) max.get(0);
+    }
+    
+    /**
+     * Gets the "type flags" of each parameter positions, or {@code null} if there's no method with this parameter
+     * count or if we are in pre-2.3.21 mode, or {@link #ALL_ZEROS_ARRAY} if there were no parameters that turned
+     * on a flag. The returned {@code int}-s are one or more {@link TypeFlags} constants binary "or"-ed together.  
+     */
+    final protected int[] getTypeFlags(int paramCount) {
+        return typeFlagsByParamCount != null && typeFlagsByParamCount.length > paramCount
+                ? typeFlagsByParamCount[paramCount]
+                : null;
+    }
+
+    /**
+     * Updates the content of the {@link #typeFlagsByParamCount} field with the parameter type flags of a method.
+     * 
+     * @param dstParamCount The parameter count for which we want to merge in the type flags 
+     * @param srcTypeFlagsByParamIdx If shorter than {@code dstParamCount}, its last item will be repeated until
+     *        dstParamCount length is reached. If longer, the excessive items will be ignored.
+     *        Maybe {@link #ALL_ZEROS_ARRAY}. Maybe a 0-length array. Can't be {@code null}.
+     */
+    final protected void mergeInTypesFlags(int dstParamCount, int[] srcTypeFlagsByParamIdx) {
+        _NullArgumentException.check("srcTypesFlagsByParamIdx", srcTypeFlagsByParamIdx);
+        
+        // Special case of 0 param count:
+        if (dstParamCount == 0) {
+            if (typeFlagsByParamCount == null) {
+                typeFlagsByParamCount = ZERO_PARAM_COUNT_TYPE_FLAGS_ARRAY;
+            } else if (typeFlagsByParamCount != ZERO_PARAM_COUNT_TYPE_FLAGS_ARRAY) {
+                typeFlagsByParamCount[0] = ALL_ZEROS_ARRAY;
+            }
+            return;
+        }
+        
+        // Ensure that typesFlagsByParamCount[dstParamCount] exists:
+        if (typeFlagsByParamCount == null) {
+            typeFlagsByParamCount = new int[dstParamCount + 1][];
+        } else if (typeFlagsByParamCount.length <= dstParamCount) {
+            int[][] newTypeFlagsByParamCount = new int[dstParamCount + 1][];
+            System.arraycopy(typeFlagsByParamCount, 0, newTypeFlagsByParamCount, 0,
+                    typeFlagsByParamCount.length);
+            typeFlagsByParamCount = newTypeFlagsByParamCount;
+        }
+        
+        int[] dstTypeFlagsByParamIdx = typeFlagsByParamCount[dstParamCount];
+        if (dstTypeFlagsByParamIdx == null) {
+            // This is the first method added with this number of params => no merging
+            
+            if (srcTypeFlagsByParamIdx != ALL_ZEROS_ARRAY) {
+                int srcParamCount = srcTypeFlagsByParamIdx.length;
+                dstTypeFlagsByParamIdx = new int[dstParamCount];
+                for (int paramIdx = 0; paramIdx < dstParamCount; paramIdx++) {
+                    dstTypeFlagsByParamIdx[paramIdx]
+                            = srcTypeFlagsByParamIdx[paramIdx < srcParamCount ? paramIdx : srcParamCount - 1];
+                }
+            } else {
+                dstTypeFlagsByParamIdx = ALL_ZEROS_ARRAY;
+            }
+            
+            typeFlagsByParamCount[dstParamCount] = dstTypeFlagsByParamIdx;
+        } else {
+            // dstTypeFlagsByParamIdx != null, so we need to merge into it.
+            
+            if (srcTypeFlagsByParamIdx == dstTypeFlagsByParamIdx) {
+                // Used to occur when both are ALL_ZEROS_ARRAY
+                return;
+            }
+            
+            // As we will write dstTypeFlagsByParamIdx, it can't remain ALL_ZEROS_ARRAY anymore. 
+            if (dstTypeFlagsByParamIdx == ALL_ZEROS_ARRAY && dstParamCount > 0) {
+                dstTypeFlagsByParamIdx = new int[dstParamCount];
+                typeFlagsByParamCount[dstParamCount] = dstTypeFlagsByParamIdx;
+            }
+            
+            for (int paramIdx = 0; paramIdx < dstParamCount; paramIdx++) {
+                final int srcParamTypeFlags;
+                if (srcTypeFlagsByParamIdx != ALL_ZEROS_ARRAY) {
+                    int srcParamCount = srcTypeFlagsByParamIdx.length;
+                    srcParamTypeFlags = srcTypeFlagsByParamIdx[paramIdx < srcParamCount ? paramIdx : srcParamCount - 1]; 
+                } else {
+                    srcParamTypeFlags = 0;
+                }
+                
+                final int dstParamTypesFlags = dstTypeFlagsByParamIdx[paramIdx];
+                if (dstParamTypesFlags != srcParamTypeFlags) {
+                    int mergedTypeFlags = dstParamTypesFlags | srcParamTypeFlags;
+                    if ((mergedTypeFlags & TypeFlags.MASK_ALL_NUMERICALS) != 0) {
+                        // Must not be set if we don't have numerical type at this index! 
+                        mergedTypeFlags |= TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT;
+                    }
+                    dstTypeFlagsByParamIdx[paramIdx] = mergedTypeFlags; 
+                }
+            }
+        }
+    }
+    
+    protected void forceNumberArgumentsToParameterTypes(
+            Object[] args, Class[] paramTypes, int[] typeFlagsByParamIndex) {
+        final int paramTypesLen = paramTypes.length;
+        final int argsLen = args.length;
+        for (int argIdx = 0; argIdx < argsLen; argIdx++) {
+            final int paramTypeIdx = argIdx < paramTypesLen ? argIdx : paramTypesLen - 1;
+            final int typeFlags = typeFlagsByParamIndex[paramTypeIdx];
+            
+            // Forcing the number type can only be interesting if there are numerical parameter types on that index,
+            // and the unwrapping was not to an exact numerical type.
+            if ((typeFlags & TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT) != 0) {
+                final Object arg = args[argIdx];
+                // If arg isn't a number, we can't do any conversions anyway, regardless of the param type.
+                if (arg instanceof Number) {
+                    final Class targetType = paramTypes[paramTypeIdx];
+                    final Number convertedArg = DefaultObjectWrapper.forceUnwrappedNumberToType((Number) arg, targetType);
+                    if (convertedArg != null) {
+                        args[argIdx] = convertedArg;
+                    }
+                }
+            }
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/OverloadedNumberUtil.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/OverloadedNumberUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/OverloadedNumberUtil.java
new file mode 100644
index 0000000..f501576
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/OverloadedNumberUtil.java
@@ -0,0 +1,1289 @@
+/*
+ * 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.math.BigDecimal;
+import java.math.BigInteger;
+
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.util._ClassUtil;
+import org.apache.freemarker.core.util._NumberUtil;
+
+/**
+ * Everything related to coercion to ambiguous numerical types.  
+ */
+class OverloadedNumberUtil {
+
+    // Can't be instantiated
+    private OverloadedNumberUtil() { }
+
+    /**
+     * The lower limit of conversion prices where there's a risk of significant mantissa loss.
+     * The value comes from misc/overloadedNumberRules/prices.ods and generator.ftl.
+     */
+    static final int BIG_MANTISSA_LOSS_PRICE = 4 * 10000;
+    
+    /** The highest long that can be stored in double without precision loss: 2**53. */
+    private static final long MAX_DOUBLE_OR_LONG = 9007199254740992L;
+    /** The lowest long that can be stored in double without precision loss: -(2**53). */
+    private static final long MIN_DOUBLE_OR_LONG = -9007199254740992L;
+    private static final int MAX_DOUBLE_OR_LONG_LOG_2 = 53;
+    
+    /** The highest long that can be stored in float without precision loss: 2**24. */
+    private static final int MAX_FLOAT_OR_INT = 16777216;
+    /** The lowest long that can be stored in float without precision loss: -(2**24). */
+    private static final int MIN_FLOAT_OR_INT = -16777216;
+    private static final int MAX_FLOAT_OR_INT_LOG_2 = 24;
+    /** Lowest number that we don't thread as possible integer 0. */
+    private static final double LOWEST_ABOVE_ZERO = 0.000001;
+    /** Highest number that we don't thread as possible integer 1. */
+    private static final double HIGHEST_BELOW_ONE = 0.999999;
+
+    /**
+     * Attaches the lowest alternative number type to the parameter number via {@link NumberWithFallbackType}, if
+     * that's useful according the possible target number types. This transformation is applied on the method call
+     * argument list before overloaded method selection.
+     * 
+     * <p>Note that as of this writing, this method is only used when
+     * {@link DefaultObjectWrapper#getIncompatibleImprovements()} >= 2.3.21.
+     * 
+     * <p>Why's this needed, how it works: Overloaded method selection only selects methods where the <em>type</em>
+     * (not the value!) of the argument is "smaller" or the same as the parameter type. This is similar to how it's in
+     * the Java language. That it only decides based on the parameter type is important because this way
+     * {@link OverloadedMethodsSubset} can cache method lookup decisions using the types as the cache key. Problem is,
+     * since you don't declare the exact numerical types in FTL, and FTL has only a single generic numeric type
+     * anyway, what Java type a {@link TemplateNumberModel} uses internally is often seen as a technical detail of which
+     * the template author can't always keep track of. So we investigate the <em>value</em> of the number too,
+     * then coerce it down without overflow to a type that will match the most overloaded methods. (This
+     * is especially important as FTL often stores numbers in {@link BigDecimal}-s, which will hardly ever match any
+     * method parameters.) We could simply return that number, like {@code Byte(0)} for an {@code Integer(0)},
+     * however, then we would lose the information about what the original type was. The original type is sometimes
+     * important, as in ambiguous situations the method where there's an exact type match should be selected (like,
+     * when someone wants to select an overload explicitly with {@code m(x?int)}). Also, if an overload wins where
+     * the parameter type at the position of the number is {@code Number} or {@code Object} (or {@code Comparable}
+     * etc.), it's expected that we pass in the original value (an {@code Integer} in this example), especially if that
+     * value is the return value of another Java method. That's why we use
+     * {@link NumberWithFallbackType} numerical classes like {@link IntegerOrByte}, which represents both the original
+     * type and the coerced type, all encoded into the class of the value, which is used as the overloaded method lookup
+     * cache key.
+     *  
+     * <p>See also: <tt>src\main\misc\overloadedNumberRules\prices.ods</tt>.
+     * 
+     * @param num the number to coerce
+     * @param typeFlags the type flags of the target parameter position; see {@link TypeFlags}
+     * 
+     * @returns The original number or a {@link NumberWithFallbackType}, depending on the actual value and the types
+     *     indicated in the {@code targetNumTypes} parameter.
+     */
+    static Number addFallbackType(final Number num, final int typeFlags) {
+        final Class numClass = num.getClass();
+        if (numClass == BigDecimal.class) {
+            // For now we only support the backward-compatible mode that doesn't prevent roll overs and magnitude loss.
+            // However, we push the overloaded selection to the right direction, so we will at least indicate if the
+            // number has decimals.
+            BigDecimal n = (BigDecimal) num; 
+            if ((typeFlags & TypeFlags.MASK_KNOWN_INTEGERS) != 0
+                    && (typeFlags & TypeFlags.MASK_KNOWN_NONINTEGERS) != 0
+                    && _NumberUtil.isIntegerBigDecimal(n) /* <- can be expensive */) {
+                return new IntegerBigDecimal(n);
+            } else {
+                // Either it was a non-integer, or it didn't mater what it was, as we don't have both integer and
+                // non-integer target types. 
+                return n;
+            }
+        } else if (numClass == Integer.class) {
+            int pn = num.intValue();
+            // Note that we try to return the most specific type (i.e., the numerical type with the smallest range), but
+            // only among the types that are possible targets. Like if the only target is int and the value is 1, we
+            // will return Integer 1, not Byte 1, even though byte is automatically converted to int so it would
+            // work too. Why we avoid unnecessarily specific types is that they generate more overloaded method lookup
+            // cache entries, since the cache key is the array of the types of the argument values. So we want as few
+            // permutations as possible. 
+            if ((typeFlags & TypeFlags.BYTE) != 0 && pn <= Byte.MAX_VALUE && pn >= Byte.MIN_VALUE) {
+                return new IntegerOrByte((Integer) num, (byte) pn);
+            } else if ((typeFlags & TypeFlags.SHORT) != 0 && pn <= Short.MAX_VALUE && pn >= Short.MIN_VALUE) {
+                return new IntegerOrShort((Integer) num, (short) pn);
+            } else {
+                return num;
+            }
+        } else if (numClass == Long.class) {
+            final long pn = num.longValue(); 
+            if ((typeFlags & TypeFlags.BYTE) != 0 && pn <= Byte.MAX_VALUE && pn >= Byte.MIN_VALUE) {
+                return new LongOrByte((Long) num, (byte) pn);
+            } else if ((typeFlags & TypeFlags.SHORT) != 0 && pn <= Short.MAX_VALUE && pn >= Short.MIN_VALUE) {
+                return new LongOrShort((Long) num, (short) pn);
+            } else if ((typeFlags & TypeFlags.INTEGER) != 0 && pn <= Integer.MAX_VALUE && pn >= Integer.MIN_VALUE) {
+                return new LongOrInteger((Long) num, (int) pn);
+            } else {
+                return num;
+            }
+        } else if (numClass == Double.class) {
+            final double doubleN = num.doubleValue();
+            
+            // Can we store it in an integer type?
+            checkIfWholeNumber: do {
+                if ((typeFlags & TypeFlags.MASK_KNOWN_INTEGERS) == 0) break checkIfWholeNumber;
+                
+                // There's no hope to be 1-precise outside this region. (Although problems can occur even inside it...)
+                if (doubleN > MAX_DOUBLE_OR_LONG || doubleN < MIN_DOUBLE_OR_LONG) break checkIfWholeNumber;
+                
+                long longN = num.longValue(); 
+                double diff = doubleN - longN;
+                boolean exact;  // We will try to ignore precision glitches (like 0.3 - 0.2 - 0.1 = -2.7E-17)
+                if (diff == 0) {
+                    exact = true;
+                } else if (diff > 0) {
+                    if (diff < LOWEST_ABOVE_ZERO) {
+                        exact = false;
+                    } else if (diff > HIGHEST_BELOW_ONE) {
+                        exact = false;
+                        longN++;
+                    } else {
+                        break checkIfWholeNumber;
+                    }
+                } else {  // => diff < 0
+                    if (diff > -LOWEST_ABOVE_ZERO) {
+                        exact = false;
+                    } else if (diff < -HIGHEST_BELOW_ONE) {
+                        exact = false;
+                        longN--;
+                    } else {
+                        break checkIfWholeNumber;
+                    }
+                }
+                
+                // If we reach this, it can be treated as a whole number.
+                
+                if ((typeFlags & TypeFlags.BYTE) != 0
+                        && longN <= Byte.MAX_VALUE && longN >= Byte.MIN_VALUE) {
+                    return new DoubleOrByte((Double) num, (byte) longN);
+                } else if ((typeFlags & TypeFlags.SHORT) != 0
+                        && longN <= Short.MAX_VALUE && longN >= Short.MIN_VALUE) {
+                    return new DoubleOrShort((Double) num, (short) longN);
+                } else if ((typeFlags & TypeFlags.INTEGER) != 0
+                        && longN <= Integer.MAX_VALUE && longN >= Integer.MIN_VALUE) {
+                    final int intN = (int) longN; 
+                    return (typeFlags & TypeFlags.FLOAT) != 0 && intN >= MIN_FLOAT_OR_INT && intN <= MAX_FLOAT_OR_INT
+                                    ? new DoubleOrIntegerOrFloat((Double) num, intN)
+                                    : new DoubleOrInteger((Double) num, intN);
+                } else if ((typeFlags & TypeFlags.LONG) != 0) {
+                    if (exact) {
+                        return new DoubleOrLong((Double) num, longN);
+                    } else {
+                        // We don't deal with non-exact numbers outside the range of int, as we already reach
+                        // ULP 2.384185791015625E-7 there.
+                        if (longN >= Integer.MIN_VALUE && longN <= Integer.MAX_VALUE) {
+                            return new DoubleOrLong((Double) num, longN);
+                        } else {
+                            break checkIfWholeNumber;
+                        }
+                    }
+                }
+                // This point is reached if the double value was out of the range of target integer type(s). 
+                // Falls through!
+            } while (false);
+            // If we reach this that means that it can't be treated as a whole number.
+            
+            if ((typeFlags & TypeFlags.FLOAT) != 0 && doubleN >= -Float.MAX_VALUE && doubleN <= Float.MAX_VALUE) {
+                return new DoubleOrFloat((Double) num);
+            } else {
+                // Simply Double:
+                return num;
+            }
+        } else if (numClass == Float.class) {
+            final float floatN = num.floatValue();
+            
+            // Can we store it in an integer type?
+            checkIfWholeNumber: do {
+                if ((typeFlags & TypeFlags.MASK_KNOWN_INTEGERS) == 0) break checkIfWholeNumber;
+                
+                // There's no hope to be 1-precise outside this region. (Although problems can occur even inside it...)
+                if (floatN > MAX_FLOAT_OR_INT || floatN < MIN_FLOAT_OR_INT) break checkIfWholeNumber;
+                
+                int intN = num.intValue();
+                double diff = floatN - intN;
+                boolean exact;  // We will try to ignore precision glitches (like 0.3 - 0.2 - 0.1 = -2.7E-17)
+                if (diff == 0) {
+                    exact = true;
+                // We already reach ULP 7.6293945E-6 with bytes, so we don't continue with shorts.
+                } else if (intN >= Byte.MIN_VALUE && intN <= Byte.MAX_VALUE) {
+                    if (diff > 0) {
+                        if (diff < 0.00001) {
+                            exact = false;
+                        } else if (diff > 0.99999) {
+                            exact = false;
+                            intN++;
+                        } else {
+                            break checkIfWholeNumber;
+                        }
+                    } else {  // => diff < 0
+                        if (diff > -0.00001) {
+                            exact = false;
+                        } else if (diff < -0.99999) {
+                            exact = false;
+                            intN--;
+                        } else {
+                            break checkIfWholeNumber;
+                        }
+                    }
+                } else {
+                    break checkIfWholeNumber;
+                }
+                
+                // If we reach this, it can be treated as a whole number.
+                
+                if ((typeFlags & TypeFlags.BYTE) != 0 && intN <= Byte.MAX_VALUE && intN >= Byte.MIN_VALUE) {
+                    return new FloatOrByte((Float) num, (byte) intN);
+                } else if ((typeFlags & TypeFlags.SHORT) != 0 && intN <= Short.MAX_VALUE && intN >= Short.MIN_VALUE) {
+                    return new FloatOrShort((Float) num, (short) intN);
+                } else if ((typeFlags & TypeFlags.INTEGER) != 0) {
+                    return new FloatOrInteger((Float) num, intN);
+                } else if ((typeFlags & TypeFlags.LONG) != 0) {
+                    // We can't even go outside the range of integers, so we don't need Long variation:
+                    return exact
+                            ? new FloatOrInteger((Float) num, intN)
+                            : new FloatOrByte((Float) num, (byte) intN);  // as !exact implies (-128..127)
+                }
+                // This point is reached if the float value was out of the range of target integer type(s). 
+                // Falls through!
+            } while (false);
+            // If we reach this that means that it can't be treated as a whole number. So it's simply a Float:
+            return num;
+        } else if (numClass == Byte.class) {
+            return num;
+        } else if (numClass == Short.class) {
+            short pn = num.shortValue(); 
+            if ((typeFlags & TypeFlags.BYTE) != 0 && pn <= Byte.MAX_VALUE && pn >= Byte.MIN_VALUE) {
+                return new ShortOrByte((Short) num, (byte) pn);
+            } else {
+                return num;
+            }
+        } else if (numClass == BigInteger.class) {
+            if ((typeFlags
+                    & ((TypeFlags.MASK_KNOWN_INTEGERS | TypeFlags.MASK_KNOWN_NONINTEGERS)
+                            ^ (TypeFlags.BIG_INTEGER | TypeFlags.BIG_DECIMAL))) != 0) {
+                BigInteger biNum = (BigInteger) num;
+                final int bitLength = biNum.bitLength();  // Doesn't include sign bit, so it's one less than expected
+                if ((typeFlags & TypeFlags.BYTE) != 0 && bitLength <= 7) {
+                    return new BigIntegerOrByte(biNum);
+                } else if ((typeFlags & TypeFlags.SHORT) != 0 && bitLength <= 15) {
+                    return new BigIntegerOrShort(biNum);
+                } else if ((typeFlags & TypeFlags.INTEGER) != 0 && bitLength <= 31) {
+                    return new BigIntegerOrInteger(biNum);
+                } else if ((typeFlags & TypeFlags.LONG) != 0 && bitLength <= 63) {
+                    return new BigIntegerOrLong(biNum);
+                } else if ((typeFlags & TypeFlags.FLOAT) != 0
+                        && (bitLength <= MAX_FLOAT_OR_INT_LOG_2
+                            || bitLength == MAX_FLOAT_OR_INT_LOG_2 + 1
+                               && biNum.getLowestSetBit() >= MAX_FLOAT_OR_INT_LOG_2)) {
+                    return new BigIntegerOrFloat(biNum);
+                } else if ((typeFlags & TypeFlags.DOUBLE) != 0
+                        && (bitLength <= MAX_DOUBLE_OR_LONG_LOG_2
+                            || bitLength == MAX_DOUBLE_OR_LONG_LOG_2 + 1
+                               && biNum.getLowestSetBit() >= MAX_DOUBLE_OR_LONG_LOG_2)) {
+                    return new BigIntegerOrDouble(biNum);
+                } else {
+                    return num;
+                }
+            } else {
+                // No relevant coercion target types; return the BigInteger as is:
+                return num;
+            }
+        } else {
+            // Unknown number type:
+            return num;
+        }
+    }
+
+    interface ByteSource { Byte byteValue(); }
+    interface ShortSource { Short shortValue(); }
+    interface IntegerSource { Integer integerValue(); }
+    interface LongSource { Long longValue(); }
+    interface FloatSource { Float floatValue(); }
+    interface DoubleSource { Double doubleValue(); }
+    interface BigIntegerSource { BigInteger bigIntegerValue(); }
+    interface BigDecimalSource { BigDecimal bigDecimalValue(); }
+    
+    /**
+     * Superclass of "Or"-ed numerical types. With an example, a {@code int} 1 has the fallback type {@code byte}, as
+     * that's the smallest type that can store the value, so it can be represented as an {@link IntegerOrByte}.
+     * This is useful as overloaded method selection only examines the type of the arguments, not the value of them,
+     * but with "Or"-ed types we can encode this value-related information into the argument type, hence influencing the
+     * method selection.
+     */
+    abstract static class NumberWithFallbackType extends Number implements Comparable {
+        
+        protected abstract Number getSourceNumber();
+
+        @Override
+        public int intValue() {
+            return getSourceNumber().intValue();
+        }
+
+        @Override
+        public long longValue() {
+            return getSourceNumber().longValue();
+        }
+
+        @Override
+        public float floatValue() {
+            return getSourceNumber().floatValue();
+        }
+
+        @Override
+        public double doubleValue() {
+            return getSourceNumber().doubleValue();
+        }
+
+        @Override
+        public byte byteValue() {
+            return getSourceNumber().byteValue();
+        }
+
+        @Override
+        public short shortValue() {
+            return getSourceNumber().shortValue();
+        }
+
+        @Override
+        public int hashCode() {
+            return getSourceNumber().hashCode();
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj != null && getClass() == obj.getClass()) {
+                return getSourceNumber().equals(((NumberWithFallbackType) obj).getSourceNumber());
+            } else {
+                return false;
+            }
+        }
+
+        @Override
+        public String toString() {
+            return getSourceNumber().toString();
+        }
+
+        // We have to implement this, so that if a potential matching method expects a Comparable, which is implemented
+        // by all the supported numerical types, the "Or" type will be a match. 
+        @Override
+        public int compareTo(Object o) {
+            Number n = getSourceNumber();
+            if (n instanceof Comparable) {
+                return ((Comparable) n).compareTo(o); 
+            } else {
+                throw new ClassCastException(n.getClass().getName() + " is not Comparable.");
+            }
+        }
+        
+    }
+
+    /**
+     * Holds a {@link BigDecimal} that stores a whole number. When selecting a overloaded method, FreeMarker tries to
+     * associate {@link BigDecimal} values to parameters of types that can hold non-whole numbers, unless the
+     * {@link BigDecimal} is wrapped into this class, in which case it does the opposite. This mechanism is, however,
+     * too rough to prevent roll overs or magnitude losses. Those are not yet handled for backward compatibility (they
+     * were suppressed earlier too).
+     */
+    static final class IntegerBigDecimal extends NumberWithFallbackType {
+
+        private final BigDecimal n;
+        
+        IntegerBigDecimal(BigDecimal n) {
+            this.n = n;
+        }
+
+        @Override
+        protected Number getSourceNumber() {
+            return n;
+        }
+        
+        public BigInteger bigIntegerValue() {
+            return n.toBigInteger();
+        }
+        
+    }
+
+    static abstract class LongOrSmallerInteger extends NumberWithFallbackType {
+        
+        private final Long n;
+        
+        protected LongOrSmallerInteger(Long n) {
+            this.n = n;
+        }
+
+        @Override
+        protected Number getSourceNumber() {
+            return n;
+        }
+
+        @Override
+        public long longValue() {
+            return n.longValue();
+        }
+        
+    }
+    
+    static class LongOrByte extends LongOrSmallerInteger {
+        
+        private final byte w; 
+
+        LongOrByte(Long n, byte w) {
+            super(n);
+            this.w = w;
+        }
+
+        @Override
+        public byte byteValue() {
+            return w;
+        }
+        
+    }
+    
+    static class LongOrShort extends LongOrSmallerInteger {
+        
+        private final short w; 
+
+        LongOrShort(Long n, short w) {
+            super(n);
+            this.w = w;
+        }
+
+        @Override
+        public short shortValue() {
+            return w;
+        }
+        
+    }
+    
+    static class LongOrInteger extends LongOrSmallerInteger {
+        
+        private final int w; 
+
+        LongOrInteger(Long n, int w) {
+            super(n);
+            this.w = w;
+        }
+
+        @Override
+        public int intValue() {
+            return w;
+        }
+        
+    }
+    
+    static abstract class IntegerOrSmallerInteger extends NumberWithFallbackType {
+        
+        private final Integer n;
+        
+        protected IntegerOrSmallerInteger(Integer n) {
+            this.n = n;
+        }
+
+        @Override
+        protected Number getSourceNumber() {
+            return n;
+        }
+
+        @Override
+        public int intValue() {
+            return n.intValue();
+        }
+        
+    }
+    
+    static class IntegerOrByte extends IntegerOrSmallerInteger {
+        
+        private final byte w; 
+
+        IntegerOrByte(Integer n, byte w) {
+            super(n);
+            this.w = w;
+        }
+
+        @Override
+        public byte byteValue() {
+            return w;
+        }
+        
+    }
+    
+    static class IntegerOrShort extends IntegerOrSmallerInteger {
+        
+        private final short w; 
+
+        IntegerOrShort(Integer n, short w) {
+            super(n);
+            this.w = w;
+        }
+
+        @Override
+        public short shortValue() {
+            return w;
+        }
+        
+    }
+    
+    static class ShortOrByte extends NumberWithFallbackType {
+        
+        private final Short n;
+        private final byte w;
+        
+        protected ShortOrByte(Short n, byte w) {
+            this.n = n;
+            this.w = w;
+        }
+
+        @Override
+        protected Number getSourceNumber() {
+            return n;
+        }
+
+        @Override
+        public short shortValue() {
+            return n.shortValue();
+        }
+
+        @Override
+        public byte byteValue() {
+            return w;
+        }
+        
+    }
+    
+    static abstract class DoubleOrWholeNumber extends NumberWithFallbackType {
+        
+        private final Double n; 
+
+        protected DoubleOrWholeNumber(Double n) {
+            this.n = n;
+        }
+
+        @Override
+        protected Number getSourceNumber() {
+            return n;
+        }
+        
+        @Override
+        public double doubleValue() {
+            return n.doubleValue();
+        }
+        
+    }
+    
+    static final class DoubleOrByte extends DoubleOrWholeNumber {
+        
+        private final byte w;
+
+        DoubleOrByte(Double n, byte w) {
+            super(n);
+            this.w = w;
+        }
+        
+        @Override
+        public byte byteValue() {
+            return w;
+        }
+        
+        @Override
+        public short shortValue() {
+            return w;
+        }
+        
+        @Override
+        public int intValue() {
+            return w;
+        }
+        
+        @Override
+        public long longValue() {
+            return w;
+        }
+        
+    }
+    
+    static final class DoubleOrShort extends DoubleOrWholeNumber {
+        
+        private final short w;
+
+        DoubleOrShort(Double n, short w) {
+            super(n);
+            this.w = w;
+        }
+        
+        @Override
+        public short shortValue() {
+            return w;
+        }
+        
+        @Override
+        public int intValue() {
+            return w;
+        }
+        
+        @Override
+        public long longValue() {
+            return w;
+        }
+        
+    }
+    
+    static final class DoubleOrIntegerOrFloat extends DoubleOrWholeNumber {
+
+        private final int w;
+
+        DoubleOrIntegerOrFloat(Double n, int w) {
+            super(n);
+            this.w = w;
+        }
+        
+        @Override
+        public int intValue() {
+            return w;
+        }
+        
+        @Override
+        public long longValue() {
+            return w;
+        }
+        
+    }
+    
+    static final class DoubleOrInteger extends DoubleOrWholeNumber {
+
+        private final int w;
+
+        DoubleOrInteger(Double n, int w) {
+            super(n);
+            this.w = w;
+        }
+        
+        @Override
+        public int intValue() {
+            return w;
+        }
+        
+        @Override
+        public long longValue() {
+            return w;
+        }
+        
+    }
+    
+    static final class DoubleOrLong extends DoubleOrWholeNumber {
+
+        private final long w;
+
+        DoubleOrLong(Double n, long w) {
+            super(n);
+            this.w = w;
+        }
+        
+        @Override
+        public long longValue() {
+            return w;
+        }
+        
+    }
+    
+    static final class DoubleOrFloat extends NumberWithFallbackType {
+        
+        private final Double n;
+
+        DoubleOrFloat(Double n) {
+            this.n = n;
+        }
+        
+        @Override
+        public float floatValue() {
+            return n.floatValue();
+        }
+        
+        @Override
+        public double doubleValue() {
+            return n.doubleValue();
+        }
+
+        @Override
+        protected Number getSourceNumber() {
+            return n;
+        }
+        
+    }
+
+    static abstract class FloatOrWholeNumber extends NumberWithFallbackType {
+        
+        private final Float n; 
+
+        FloatOrWholeNumber(Float n) {
+            this.n = n;
+        }
+
+        @Override
+        protected Number getSourceNumber() {
+            return n;
+        }
+        
+        @Override
+        public float floatValue() {
+            return n.floatValue();
+        }
+        
+    }
+    
+    static final class FloatOrByte extends FloatOrWholeNumber {
+        
+        private final byte w;
+
+        FloatOrByte(Float n, byte w) {
+            super(n);
+            this.w = w;
+        }
+        
+        @Override
+        public byte byteValue() {
+            return w;
+        }
+        
+        @Override
+        public short shortValue() {
+            return w;
+        }
+        
+        @Override
+        public int intValue() {
+            return w;
+        }
+        
+        @Override
+        public long longValue() {
+            return w;
+        }
+        
+    }
+    
+    static final class FloatOrShort extends FloatOrWholeNumber {
+        
+        private final short w;
+
+        FloatOrShort(Float n, short w) {
+            super(n);
+            this.w = w;
+        }
+        
+        @Override
+        public short shortValue() {
+            return w;
+        }
+        
+        @Override
+        public int intValue() {
+            return w;
+        }
+        
+        @Override
+        public long longValue() {
+            return w;
+        }
+        
+    }
+
+    static final class FloatOrInteger extends FloatOrWholeNumber {
+        
+        private final int w;
+
+        FloatOrInteger(Float n, int w) {
+            super(n);
+            this.w = w;
+        }
+        
+        @Override
+        public int intValue() {
+            return w;
+        }
+        
+        @Override
+        public long longValue() {
+            return w;
+        }
+        
+    }
+
+    abstract static class BigIntegerOrPrimitive extends NumberWithFallbackType {
+
+        protected final BigInteger n;
+        
+        BigIntegerOrPrimitive(BigInteger n) {
+            this.n = n;
+        }
+
+        @Override
+        protected Number getSourceNumber() {
+            return n;
+        }
+        
+    }
+    
+    final static class BigIntegerOrByte extends BigIntegerOrPrimitive {
+
+        BigIntegerOrByte(BigInteger n) {
+            super(n);
+        }
+
+    }
+    
+    final static class BigIntegerOrShort extends BigIntegerOrPrimitive {
+
+        BigIntegerOrShort(BigInteger n) {
+            super(n);
+        }
+
+    }
+    
+    final static class BigIntegerOrInteger extends BigIntegerOrPrimitive {
+
+        BigIntegerOrInteger(BigInteger n) {
+            super(n);
+        }
+
+    }
+    
+    final static class BigIntegerOrLong extends BigIntegerOrPrimitive {
+
+        BigIntegerOrLong(BigInteger n) {
+            super(n);
+        }
+
+    }
+
+    abstract static class BigIntegerOrFPPrimitive extends BigIntegerOrPrimitive {
+
+        BigIntegerOrFPPrimitive(BigInteger n) {
+            super(n);
+        }
+
+        /** Faster version of {@link BigDecimal#floatValue()}, utilizes that the number known to fit into a long. */
+        @Override
+        public float floatValue() {
+            return n.longValue(); 
+        }
+        
+        /** Faster version of {@link BigDecimal#doubleValue()}, utilizes that the number known to fit into a long. */
+        @Override
+        public double doubleValue() {
+            return n.longValue(); 
+        }
+
+    }
+    
+    final static class BigIntegerOrFloat extends BigIntegerOrFPPrimitive {
+
+        BigIntegerOrFloat(BigInteger n) {
+            super(n);
+        }
+
+    }
+    
+    final static class BigIntegerOrDouble extends BigIntegerOrFPPrimitive {
+
+        BigIntegerOrDouble(BigInteger n) {
+            super(n);
+        }
+        
+    }
+    
+    /**
+     * Returns a non-negative number that indicates how much we want to avoid a given numerical type conversion. Since
+     * we only consider the types here, not the actual value, we always consider the worst case scenario. Like it will
+     * say that converting int to short is not allowed, although int 1 can be converted to byte without loss. To account
+     * for such situations, "Or"-ed types, like {@link IntegerOrByte} has to be used. 
+     * 
+     * @param fromC the non-primitive type of the argument (with other words, the actual type).
+     *        Must be {@link Number} or its subclass. This is possibly an {@link NumberWithFallbackType} subclass.
+     * @param toC the <em>non-primitive</em> type of the target parameter (with other words, the format type).
+     *        Must be a {@link Number} subclass, not {@link Number} itself.
+     *        Must <em>not</em> be {@link NumberWithFallbackType} or its subclass.
+     * 
+     * @return
+     *     <p>The possible values are:
+     *     <ul>
+     *       <li>0: No conversion is needed
+     *       <li>[0, 30000): Lossless conversion
+     *       <li>[30000, 40000): Smaller precision loss in mantissa is possible.
+     *       <li>[40000, 50000): Bigger precision loss in mantissa is possible.
+     *       <li>{@link Integer#MAX_VALUE}: Conversion not allowed due to the possibility of magnitude loss or
+     *          overflow</li>
+     *     </ul>
+     * 
+     *     <p>At some places, we only care if the conversion is possible, i.e., whether the return value is
+     *     {@link Integer#MAX_VALUE} or not. But when multiple overloaded methods have an argument type to which we
+     *     could convert to, this number will influence which of those will be chosen.
+     */
+    static int getArgumentConversionPrice(Class fromC, Class toC) {
+        // DO NOT EDIT, generated code!
+        // See: src\main\misc\overloadedNumberRules\README.txt
+        if (toC == fromC) {
+            return 0;
+        } else if (toC == Integer.class) {
+            if (fromC == IntegerBigDecimal.class) return 31003;
+            else if (fromC == BigDecimal.class) return 41003;
+            else if (fromC == Long.class) return Integer.MAX_VALUE;
+            else if (fromC == Double.class) return Integer.MAX_VALUE;
+            else if (fromC == Float.class) return Integer.MAX_VALUE;
+            else if (fromC == Byte.class) return 10003;
+            else if (fromC == BigInteger.class) return Integer.MAX_VALUE;
+            else if (fromC == LongOrInteger.class) return 21003;
+            else if (fromC == DoubleOrFloat.class) return Integer.MAX_VALUE;
+            else if (fromC == DoubleOrIntegerOrFloat.class) return 22003;
+            else if (fromC == DoubleOrInteger.class) return 22003;
+            else if (fromC == DoubleOrLong.class) return Integer.MAX_VALUE;
+            else if (fromC == IntegerOrByte.class) return 0;
+            else if (fromC == DoubleOrByte.class) return 22003;
+            else if (fromC == LongOrByte.class) return 21003;
+            else if (fromC == Short.class) return 10003;
+            else if (fromC == LongOrShort.class) return 21003;
+            else if (fromC == ShortOrByte.class) return 10003;
+            else if (fromC == FloatOrInteger.class) return 21003;
+            else if (fromC == FloatOrByte.class) return 21003;
+            else if (fromC == FloatOrShort.class) return 21003;
+            else if (fromC == BigIntegerOrInteger.class) return 16003;
+            else if (fromC == BigIntegerOrLong.class) return Integer.MAX_VALUE;
+            else if (fromC == BigIntegerOrDouble.class) return Integer.MAX_VALUE;
+            else if (fromC == BigIntegerOrFloat.class) return Integer.MAX_VALUE;
+            else if (fromC == BigIntegerOrByte.class) return 16003;
+            else if (fromC == IntegerOrShort.class) return 0;
+            else if (fromC == DoubleOrShort.class) return 22003;
+            else if (fromC == BigIntegerOrShort.class) return 16003;
+            else return Integer.MAX_VALUE;
+        } else if (toC == Long.class) {
+            if (fromC == Integer.class) return 10004;
+            else if (fromC == IntegerBigDecimal.class) return 31004;
+            else if (fromC == BigDecimal.class) return 41004;
+            else if (fromC == Double.class) return Integer.MAX_VALUE;
+            else if (fromC == Float.class) return Integer.MAX_VALUE;
+            else if (fromC == Byte.class) return 10004;
+            else if (fromC == BigInteger.class) return Integer.MAX_VALUE;
+            else if (fromC == LongOrInteger.class) return 0;
+            else if (fromC == DoubleOrFloat.class) return Integer.MAX_VALUE;
+            else if (fromC == DoubleOrIntegerOrFloat.class) return 21004;
+            else if (fromC == DoubleOrInteger.class) return 21004;
+            else if (fromC == DoubleOrLong.class) return 21004;
+            else if (fromC == IntegerOrByte.class) return 10004;
+            else if (fromC == DoubleOrByte.class) return 21004;
+            else if (fromC == LongOrByte.class) return 0;
+            else if (fromC == Short.class) return 10004;
+            else if (fromC == LongOrShort.class) return 0;
+            else if (fromC == ShortOrByte.class) return 10004;
+            else if (fromC == FloatOrInteger.class) return 21004;
+            else if (fromC == FloatOrByte.class) return 21004;
+            else if (fromC == FloatOrShort.class) return 21004;
+            else if (fromC == BigIntegerOrInteger.class) return 15004;
+            else if (fromC == BigIntegerOrLong.class) return 15004;
+            else if (fromC == BigIntegerOrDouble.class) return Integer.MAX_VALUE;
+            else if (fromC == BigIntegerOrFloat.class) return Integer.MAX_VALUE;
+            else if (fromC == BigIntegerOrByte.class) return 15004;
+            else if (fromC == IntegerOrShort.class) return 10004;
+            else if (fromC == DoubleOrShort.class) return 21004;
+            else if (fromC == BigIntegerOrShort.class) return 15004;
+            else return Integer.MAX_VALUE;
+        } else if (toC == Double.class) {
+            if (fromC == Integer.class) return 20007;
+            else if (fromC == IntegerBigDecimal.class) return 32007;
+            else if (fromC == BigDecimal.class) return 32007;
+            else if (fromC == Long.class) return 30007;
+            else if (fromC == Float.class) return 10007;
+            else if (fromC == Byte.class) return 20007;
+            else if (fromC == BigInteger.class) return Integer.MAX_VALUE;
+            else if (fromC == LongOrInteger.class) return 21007;
+            else if (fromC == DoubleOrFloat.class) return 0;
+            else if (fromC == DoubleOrIntegerOrFloat.class) return 0;
+            else if (fromC == DoubleOrInteger.class) return 0;
+            else if (fromC == DoubleOrLong.class) return 0;
+            else if (fromC == IntegerOrByte.class) return 20007;
+            else if (fromC == DoubleOrByte.class) return 0;
+            else if (fromC == LongOrByte.class) return 21007;
+            else if (fromC == Short.class) return 20007;
+            else if (fromC == LongOrShort.class) return 21007;
+            else if (fromC == ShortOrByte.class) return 20007;
+            else if (fromC == FloatOrInteger.class) return 10007;
+            else if (fromC == FloatOrByte.class) return 10007;
+            else if (fromC == FloatOrShort.class) return 10007;
+            else if (fromC == BigIntegerOrInteger.class) return 20007;
+            else if (fromC == BigIntegerOrLong.class) return 30007;
+            else if (fromC == BigIntegerOrDouble.class) return 20007;
+            else if (fromC == BigIntegerOrFloat.class) return 20007;
+            else if (fromC == BigIntegerOrByte.class) return 20007;
+            else if (fromC == IntegerOrShort.class) return 20007;
+            else if (fromC == DoubleOrShort.class) return 0;
+            else if (fromC == BigIntegerOrShort.class) return 20007;
+            else return Integer.MAX_VALUE;
+        } else if (toC == Float.class) {
+            if (fromC == Integer.class) return 30006;
+            else if (fromC == IntegerBigDecimal.class) return 33006;
+            else if (fromC == BigDecimal.class) return 33006;
+            else if (fromC == Long.class) return 40006;
+            else if (fromC == Double.class) return Integer.MAX_VALUE;
+            else if (fromC == Byte.class) return 20006;
+            else if (fromC == BigInteger.class) return Integer.MAX_VALUE;
+            else if (fromC == LongOrInteger.class) return 30006;
+            else if (fromC == DoubleOrFloat.class) return 30006;
+            else if (fromC == DoubleOrIntegerOrFloat.class) return 23006;
+            else if (fromC == DoubleOrInteger.class) return 30006;
+            else if (fromC == DoubleOrLong.class) return 40006;
+            else if (fromC == IntegerOrByte.class) return 24006;
+            else if (fromC == DoubleOrByte.class) return 23006;
+            else if (fromC == LongOrByte.class) return 24006;
+            else if (fromC == Short.class) return 20006;
+            else if (fromC == LongOrShort.class) return 24006;
+            else if (fromC == ShortOrByte.class) return 20006;
+            else if (fromC == FloatOrInteger.class) return 0;
+            else if (fromC == FloatOrByte.class) return 0;
+            else if (fromC == FloatOrShort.class) return 0;
+            else if (fromC == BigIntegerOrInteger.class) return 30006;
+            else if (fromC == BigIntegerOrLong.class) return 40006;
+            else if (fromC == BigIntegerOrDouble.class) return 40006;
+            else if (fromC == BigIntegerOrFloat.class) return 24006;
+            else if (fromC == BigIntegerOrByte.class) return 24006;
+            else if (fromC == IntegerOrShort.class) return 24006;
+            else if (fromC == DoubleOrShort.class) return 23006;
+            else if (fromC == BigIntegerOrShort.class) return 24006;
+            else return Integer.MAX_VALUE;
+        } else if (toC == Byte.class) {
+            if (fromC == Integer.class) return Integer.MAX_VALUE;
+            else if (fromC == IntegerBigDecimal.class) return 35001;
+            else if (fromC == BigDecimal.class) return 45001;
+            else if (fromC == Long.class) return Integer.MAX_VALUE;
+            else if (fromC == Double.class) return Integer.MAX_VALUE;
+            else if (fromC == Float.class) return Integer.MAX_VALUE;
+            else if (fromC == BigInteger.class) return Integer.MAX_VALUE;
+            else if (fromC == LongOrInteger.class) return Integer.MAX_VALUE;
+            else if (fromC == DoubleOrFloat.class) return Integer.MAX_VALUE;
+            else if (fromC == DoubleOrIntegerOrFloat.class) return Integer.MAX_VALUE;
+            else if (fromC == DoubleOrInteger.class) return Integer.MAX_VALUE;
+            else if (fromC == DoubleOrLong.class) return Integer.MAX_VALUE;
+            else if (fromC == IntegerOrByte.class) return 22001;
+            else if (fromC == DoubleOrByte.class) return 25001;
+            else if (fromC == LongOrByte.class) return 23001;
+            else if (fromC == Short.class) return Integer.MAX_VALUE;
+            else if (fromC == LongOrShort.class) return Integer.MAX_VALUE;
+            else if (fromC == ShortOrByte.class) return 21001;
+            else if (fromC == FloatOrInteger.class) return Integer.MAX_VALUE;
+            else if (fromC == FloatOrByte.class) return 23001;
+            else if (fromC == FloatOrShort.class) return Integer.MAX_VALUE;
+            else if (fromC == BigIntegerOrInteger.class) return Integer.MAX_VALUE;
+            else if (fromC == BigIntegerOrLong.class) return Integer.MAX_VALUE;
+            else if (fromC == BigIntegerOrDouble.class) return Integer.MAX_VALUE;
+            else if (fromC == BigIntegerOrFloat.class) return Integer.MAX_VALUE;
+            else if (fromC == BigIntegerOrByte.class) return 18001;
+            else if (fromC == IntegerOrShort.class) return Integer.MAX_VALUE;
+            else if (fromC == DoubleOrShort.class) return Integer.MAX_VALUE;
+            else if (fromC == BigIntegerOrShort.class) return Integer.MAX_VALUE;
+            else return Integer.MAX_VALUE;
+        } else if (toC == Short.class) {
+            if (fromC == Integer.class) return Integer.MAX_VALUE;
+            else if (fromC == IntegerBigDecimal.class) return 34002;
+            else if (fromC == BigDecimal.class) return 44002;
+            else if (fromC == Long.class) return Integer.MAX_VALUE;
+            else if (fromC == Double.class) return Integer.MAX_VALUE;
+            else if (fromC == Float.class) return Integer.MAX_VALUE;
+            else if (fromC == Byte.class) return 10002;
+            else if (fromC == BigInteger.class) return Integer.MAX_VALUE;
+            else if (fromC == LongOrInteger.class) return Integer.MAX_VALUE;
+            else if (fromC == DoubleOrFloat.class) return Integer.MAX_VALUE;
+            else if (fromC == DoubleOrIntegerOrFloat.class) return Integer.MAX_VALUE;
+            else if (fromC == DoubleOrInteger.class) return Integer.MAX_VALUE;
+            else if (fromC == DoubleOrLong.class) return Integer.MAX_VALUE;
+            else if (fromC == IntegerOrByte.class) return 21002;
+            else if (fromC == DoubleOrByte.class) return 24002;
+            else if (fromC == LongOrByte.class) return 22002;
+            else if (fromC == LongOrShort.class) return 22002;
+            else if (fromC == ShortOrByte.class) return 0;
+            else if (fromC == FloatOrInteger.class) return Integer.MAX_VALUE;
+            else if (fromC == FloatOrByte.class) return 22002;
+            else if (fromC == FloatOrShort.class) return 22002;
+            else if (fromC == BigIntegerOrInteger.class) return Integer.MAX_VALUE;
+            else if (fromC == BigIntegerOrLong.class) return Integer.MAX_VALUE;
+            else if (fromC == BigIntegerOrDouble.class) return Integer.MAX_VALUE;
+            else if (fromC == BigIntegerOrFloat.class) return Integer.MAX_VALUE;
+            else if (fromC == BigIntegerOrByte.class) return 17002;
+            else if (fromC == IntegerOrShort.class) return 21002;
+            else if (fromC == DoubleOrShort.class) return 24002;
+            else if (fromC == BigIntegerOrShort.class) return 17002;
+            else return Integer.MAX_VALUE;
+        } else if (toC == BigDecimal.class) {
+            if (fromC == Integer.class) return 20008;
+            else if (fromC == IntegerBigDecimal.class) return 0;
+            else if (fromC == Long.class) return 20008;
+            else if (fromC == Double.class) return 20008;
+            else if (fromC == Float.class) return 20008;
+            else if (fromC == Byte.class) return 20008;
+            else if (fromC == BigInteger.class) return 10008;
+            else if (fromC == LongOrInteger.class) return 20008;
+            else if (fromC == DoubleOrFloat.class) return 20008;
+            else if (fromC == DoubleOrIntegerOrFloat.class) return 20008;
+            else if (fromC == DoubleOrInteger.class) return 20008;
+            else if (fromC == DoubleOrLong.class) return 20008;
+            else if (fromC == IntegerOrByte.class) return 20008;
+            else if (fromC == DoubleOrByte.class) return 20008;
+            else if (fromC == LongOrByte.class) return 20008;
+            else if (fromC == Short.class) return 20008;
+            else if (fromC == LongOrShort.class) return 20008;
+            else if (fromC == ShortOrByte.class) return 20008;
+            else if (fromC == FloatOrInteger.class) return 20008;
+            else if (fromC == FloatOrByte.class) return 20008;
+            else if (fromC == FloatOrShort.class) return 20008;
+            else if (fromC == BigIntegerOrInteger.class) return 10008;
+            else if (fromC == BigIntegerOrLong.class) return 10008;
+            else if (fromC == BigIntegerOrDouble.class) return 10008;
+            else if (fromC == BigIntegerOrFloat.class) return 10008;
+            else if (fromC == BigIntegerOrByte.class) return 10008;
+            else if (fromC == IntegerOrShort.class) return 20008;
+            else if (fromC == DoubleOrShort.class) return 20008;
+            else if (fromC == BigIntegerOrShort.class) return 10008;
+            else return Integer.MAX_VALUE;
+        } else if (toC == BigInteger.class) {
+            if (fromC == Integer.class) return 10005;
+            else if (fromC == IntegerBigDecimal.class) return 10005;
+            else if (fromC == BigDecimal.class) return 40005;
+            else if (fromC == Long.class) return 10005;
+            else if (fromC == Double.class) return Integer.MAX_VALUE;
+            else if (fromC == Float.class) return Integer.MAX_VALUE;
+            else if (fromC == Byte.class) return 10005;
+            else if (fromC == LongOrInteger.class) return 10005;
+            else if (fromC == DoubleOrFloat.class) return Integer.MAX_VALUE;
+            else if (fromC == DoubleOrIntegerOrFloat.class) return 21005;
+            else if (fromC == DoubleOrInteger.class) return 21005;
+            else if (fromC == DoubleOrLong.class) return 21005;
+            else if (fromC == IntegerOrByte.class) return 10005;
+            else if (fromC == DoubleOrByte.class) return 21005;
+            else if (fromC == LongOrByte.class) return 10005;
+            else if (fromC == Short.class) return 10005;
+            else if (fromC == LongOrShort.class) return 10005;
+            else if (fromC == ShortOrByte.class) return 10005;
+            else if (fromC == FloatOrInteger.class) return 25005;
+            else if (fromC == FloatOrByte.class) return 25005;
+            else if (fromC == FloatOrShort.class) return 25005;
+            else if (fromC == BigIntegerOrInteger.class) return 0;
+            else if (fromC == BigIntegerOrLong.class) return 0;
+            else if (fromC == BigIntegerOrDouble.class) return 0;
+            else if (fromC == BigIntegerOrFloat.class) return 0;
+            else if (fromC == BigIntegerOrByte.class) return 0;
+            else if (fromC == IntegerOrShort.class) return 10005;
+            else if (fromC == DoubleOrShort.class) return 21005;
+            else if (fromC == BigIntegerOrShort.class) return 0;
+            else return Integer.MAX_VALUE;
+        } else {
+            // Unknown toC; we don't know how to convert to it:
+            return Integer.MAX_VALUE;
+        }        
+    }
+
+    static int compareNumberTypeSpecificity(Class c1, Class c2) {
+        // DO NOT EDIT, generated code!
+        // See: src\main\misc\overloadedNumberRules\README.txt
+        c1 = _ClassUtil.primitiveClassToBoxingClass(c1);
+        c2 = _ClassUtil.primitiveClassToBoxingClass(c2);
+        
+        if (c1 == c2) return 0;
+        
+        if (c1 == Integer.class) {
+            if (c2 == Long.class) return 4 - 3;
+            if (c2 == Double.class) return 7 - 3;
+            if (c2 == Float.class) return 6 - 3;
+            if (c2 == Byte.class) return 1 - 3;
+            if (c2 == Short.class) return 2 - 3;
+            if (c2 == BigDecimal.class) return 8 - 3;
+            if (c2 == BigInteger.class) return 5 - 3;
+            return 0;
+        }
+        if (c1 == Long.class) {
+            if (c2 == Integer.class) return 3 - 4;
+            if (c2 == Double.class) return 7 - 4;
+            if (c2 == Float.class) return 6 - 4;
+            if (c2 == Byte.class) return 1 - 4;
+            if (c2 == Short.class) return 2 - 4;
+            if (c2 == BigDecimal.class) return 8 - 4;
+            if (c2 == BigInteger.class) return 5 - 4;
+            return 0;
+        }
+        if (c1 == Double.class) {
+            if (c2 == Integer.class) return 3 - 7;
+            if (c2 == Long.class) return 4 - 7;
+            if (c2 == Float.class) return 6 - 7;
+            if (c2 == Byte.class) return 1 - 7;
+            if (c2 == Short.class) return 2 - 7;
+            if (c2 == BigDecimal.class) return 8 - 7;
+            if (c2 == BigInteger.class) return 5 - 7;
+            return 0;
+        }
+        if (c1 == Float.class) {
+            if (c2 == Integer.class) return 3 - 6;
+            if (c2 == Long.class) return 4 - 6;
+            if (c2 == Double.class) return 7 - 6;
+            if (c2 == Byte.class) return 1 - 6;
+            if (c2 == Short.class) return 2 - 6;
+            if (c2 == BigDecimal.class) return 8 - 6;
+            if (c2 == BigInteger.class) return 5 - 6;
+            return 0;
+        }
+        if (c1 == Byte.class) {
+            if (c2 == Integer.class) return 3 - 1;
+            if (c2 == Long.class) return 4 - 1;
+            if (c2 == Double.class) return 7 - 1;
+            if (c2 == Float.class) return 6 - 1;
+            if (c2 == Short.class) return 2 - 1;
+            if (c2 == BigDecimal.class) return 8 - 1;
+            if (c2 == BigInteger.class) return 5 - 1;
+            return 0;
+        }
+        if (c1 == Short.class) {
+            if (c2 == Integer.class) return 3 - 2;
+            if (c2 == Long.class) return 4 - 2;
+            if (c2 == Double.class) return 7 - 2;
+            if (c2 == Float.class) return 6 - 2;
+            if (c2 == Byte.class) return 1 - 2;
+            if (c2 == BigDecimal.class) return 8 - 2;
+            if (c2 == BigInteger.class) return 5 - 2;
+            return 0;
+        }
+        if (c1 == BigDecimal.class) {
+            if (c2 == Integer.class) return 3 - 8;
+            if (c2 == Long.class) return 4 - 8;
+            if (c2 == Double.class) return 7 - 8;
+            if (c2 == Float.class) return 6 - 8;
+            if (c2 == Byte.class) return 1 - 8;
+            if (c2 == Short.class) return 2 - 8;
+            if (c2 == BigInteger.class) return 5 - 8;
+            return 0;
+        }
+        if (c1 == BigInteger.class) {
+            if (c2 == Integer.class) return 3 - 5;
+            if (c2 == Long.class) return 4 - 5;
+            if (c2 == Double.class) return 7 - 5;
+            if (c2 == Float.class) return 6 - 5;
+            if (c2 == Byte.class) return 1 - 5;
+            if (c2 == Short.class) return 2 - 5;
+            if (c2 == BigDecimal.class) return 8 - 5;
+            return 0;
+        }
+        return 0;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/OverloadedVarArgsMethods.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/OverloadedVarArgsMethods.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/OverloadedVarArgsMethods.java
new file mode 100644
index 0000000..6547923
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/OverloadedVarArgsMethods.java
@@ -0,0 +1,245 @@
+/*
+ * 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.lang.reflect.Array;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.freemarker.core.model.ObjectWrapperAndUnwrapper;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.util.BugException;
+
+/**
+ * Stores the varargs methods for a {@link OverloadedMethods} object.
+ */
+class OverloadedVarArgsMethods extends OverloadedMethodsSubset {
+
+    OverloadedVarArgsMethods() {
+        super();
+    }
+    
+    /**
+     * Replaces the last parameter type with the array component type of it.
+     */
+    @Override
+    Class[] preprocessParameterTypes(CallableMemberDescriptor memberDesc) {
+        final Class[] preprocessedParamTypes = memberDesc.getParamTypes().clone();
+        int ln = preprocessedParamTypes.length;
+        final Class varArgsCompType = preprocessedParamTypes[ln - 1].getComponentType();
+        if (varArgsCompType == null) {
+            throw new BugException("Only varargs methods should be handled here");
+        }
+        preprocessedParamTypes[ln - 1] = varArgsCompType;
+        return preprocessedParamTypes;
+    }
+
+    @Override
+    void afterWideningUnwrappingHints(Class[] paramTypes, int[] paramNumericalTypes) {
+        // Overview
+        // --------
+        //
+        // So far, m(t1, t2...) was treated by the hint widening like m(t1, t2). So now we have to continue hint
+        // widening like if we had further methods:
+        // - m(t1, t2, t2), m(t1, t2, t2, t2), ...
+        // - m(t1), because a varargs array can be 0 long
+        //
+        // But we can't do that for real, because we had to add infinite number of methods. Also, for efficiency we
+        // don't want to invoke unwrappingHintsByParamCount entries at the indices which are still unused.
+        // So we only update the already existing hints. Remember that we already have m(t1, t2) there.
+        
+        final int paramCount = paramTypes.length;
+        final Class[][] unwrappingHintsByParamCount = getUnwrappingHintsByParamCount();
+        
+        // The case of e(t1, t2), e(t1, t2, t2), e(t1, t2, t2, t2), ..., where e is an *earlierly* added method.
+        // When that was added, this method wasn't added yet, so it had no chance updating the hints of this method,
+        // so we do that now:
+        // FIXME: Only needed if m(t1, t2) was filled an empty slot, otherwise whatever was there was already
+        // widened by the preceding hints, so this will be a no-op.
+        for (int i = paramCount - 1; i >= 0; i--) {
+            final Class[] previousHints = unwrappingHintsByParamCount[i];
+            if (previousHints != null) {
+                widenHintsToCommonSupertypes(
+                        paramCount,
+                        previousHints, getTypeFlags(i));
+                break;  // we only do this for the first hit, as the methods before that has already widened it.
+            }
+        }
+        // The case of e(t1), where e is an *earlier* added method.
+        // When that was added, this method wasn't added yet, so it had no chance updating the hints of this method,
+        // so we do that now:
+        // FIXME: Same as above; it's often unnecessary.
+        if (paramCount + 1 < unwrappingHintsByParamCount.length) {
+            Class[] oneLongerHints = unwrappingHintsByParamCount[paramCount + 1];
+            if (oneLongerHints != null) {
+                widenHintsToCommonSupertypes(
+                        paramCount,
+                        oneLongerHints, getTypeFlags(paramCount + 1));
+            }
+        }
+        
+        // The case of m(t1, t2, t2), m(t1, t2, t2, t2), ..., where m is the currently added method.
+        // Update the longer hints-arrays:  
+        for (int i = paramCount + 1; i < unwrappingHintsByParamCount.length; i++) {
+            widenHintsToCommonSupertypes(
+                    i,
+                    paramTypes, paramNumericalTypes);
+        }
+        // The case of m(t1) where m is the currently added method.
+        // update the one-shorter hints-array:  
+        if (paramCount > 0) {  // (should be always true, or else it wasn't a varags method)
+            widenHintsToCommonSupertypes(
+                    paramCount - 1,
+                    paramTypes, paramNumericalTypes);
+        }
+        
+    }
+    
+    private void widenHintsToCommonSupertypes(
+            int paramCountOfWidened, Class[] wideningTypes, int[] wideningTypeFlags) {
+        final Class[] typesToWiden = getUnwrappingHintsByParamCount()[paramCountOfWidened];
+        if (typesToWiden == null) { 
+            return;  // no such overload exists; nothing to widen
+        }
+        
+        final int typesToWidenLen = typesToWiden.length;
+        final int wideningTypesLen = wideningTypes.length;
+        int min = Math.min(wideningTypesLen, typesToWidenLen);
+        for (int i = 0; i < min; ++i) {
+            typesToWiden[i] = getCommonSupertypeForUnwrappingHint(typesToWiden[i], wideningTypes[i]);
+        }
+        if (typesToWidenLen > wideningTypesLen) {
+            Class varargsComponentType = wideningTypes[wideningTypesLen - 1];
+            for (int i = wideningTypesLen; i < typesToWidenLen; ++i) {
+                typesToWiden[i] = getCommonSupertypeForUnwrappingHint(typesToWiden[i], varargsComponentType);
+            }
+        }
+        
+        mergeInTypesFlags(paramCountOfWidened, wideningTypeFlags);
+    }
+    
+    @Override
+    MaybeEmptyMemberAndArguments getMemberAndArguments(List tmArgs, DefaultObjectWrapper unwrapper) 
+    throws TemplateModelException {
+        if (tmArgs == null) {
+            // null is treated as empty args
+            tmArgs = Collections.EMPTY_LIST;
+        }
+        final int argsLen = tmArgs.size();
+        final Class[][] unwrappingHintsByParamCount = getUnwrappingHintsByParamCount();
+        final Object[] pojoArgs = new Object[argsLen];
+        int[] typesFlags = null;
+        // Going down starting from methods with args.length + 1 parameters, because we must try to match against a case
+        // where all specified args are fixargs, and we have 0 varargs.
+        outer: for (int paramCount = Math.min(argsLen + 1, unwrappingHintsByParamCount.length - 1); paramCount >= 0; --paramCount) {
+            Class[] unwarappingHints = unwrappingHintsByParamCount[paramCount];
+            if (unwarappingHints == null) {
+                if (paramCount == 0) {
+                    return EmptyMemberAndArguments.WRONG_NUMBER_OF_ARGUMENTS;
+                }
+                continue;
+            }
+            
+            typesFlags = getTypeFlags(paramCount);
+            if (typesFlags == ALL_ZEROS_ARRAY) {
+                typesFlags = null;
+            }
+            
+            // Try to unwrap the arguments
+            Iterator it = tmArgs.iterator();
+            for (int i = 0; i < argsLen; ++i) {
+                int paramIdx = i < paramCount ? i : paramCount - 1;
+                Object pojo = unwrapper.tryUnwrapTo(
+                        (TemplateModel) it.next(),
+                        unwarappingHints[paramIdx],
+                        typesFlags != null ? typesFlags[paramIdx] : 0);
+                if (pojo == ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS) {
+                    continue outer;
+                }
+                pojoArgs[i] = pojo;
+            }
+            break outer;
+        }
+        
+        MaybeEmptyCallableMemberDescriptor maybeEmtpyMemberDesc = getMemberDescriptorForArgs(pojoArgs, true);
+        if (maybeEmtpyMemberDesc instanceof CallableMemberDescriptor) {
+            CallableMemberDescriptor memberDesc = (CallableMemberDescriptor) maybeEmtpyMemberDesc;
+            Object[] pojoArgsWithArray;
+            Object argsOrErrorIdx = replaceVarargsSectionWithArray(pojoArgs, tmArgs, memberDesc, unwrapper);
+            if (argsOrErrorIdx instanceof Object[]) {
+                pojoArgsWithArray = (Object[]) argsOrErrorIdx;
+            } else {
+                return EmptyMemberAndArguments.noCompatibleOverload(((Integer) argsOrErrorIdx).intValue());
+            }
+            if (typesFlags != null) {
+                // Note that overloaded method selection has already accounted for overflow errors when the method
+                // was selected. So this forced conversion shouldn't cause such corruption. Except, conversion from
+                // BigDecimal is allowed to overflow for backward-compatibility.
+                forceNumberArgumentsToParameterTypes(pojoArgsWithArray, memberDesc.getParamTypes(), typesFlags);
+            }
+            return new MemberAndArguments(memberDesc, pojoArgsWithArray);
+        } else {
+            return EmptyMemberAndArguments.from((EmptyCallableMemberDescriptor) maybeEmtpyMemberDesc, pojoArgs);
+        }
+    }
+    
+    /**
+     * Converts a flat argument list to one where the last argument is an array that collects the varargs, also
+     * re-unwraps the varargs to the component type. Note that this couldn't be done until we had the concrete
+     * member selected.
+     * 
+     * @return An {@code Object[]} if everything went well, or an {@code Integer} the
+     *    order (1-based index) of the argument that couldn't be unwrapped. 
+     */
+    private Object replaceVarargsSectionWithArray(
+            Object[] args, List modelArgs, CallableMemberDescriptor memberDesc, DefaultObjectWrapper unwrapper) 
+    throws TemplateModelException {
+        final Class[] paramTypes = memberDesc.getParamTypes();
+        final int paramCount = paramTypes.length;
+        final Class varArgsCompType = paramTypes[paramCount - 1].getComponentType(); 
+        final int totalArgCount = args.length;
+        final int fixArgCount = paramCount - 1;
+        if (args.length != paramCount) {
+            Object[] packedArgs = new Object[paramCount];
+            System.arraycopy(args, 0, packedArgs, 0, fixArgCount);
+            Object varargs = Array.newInstance(varArgsCompType, totalArgCount - fixArgCount);
+            for (int i = fixArgCount; i < totalArgCount; ++i) {
+                Object val = unwrapper.tryUnwrapTo((TemplateModel) modelArgs.get(i), varArgsCompType);
+                if (val == ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS) {
+                    return Integer.valueOf(i + 1);
+                }
+                Array.set(varargs, i - fixArgCount, val);
+            }
+            packedArgs[fixArgCount] = varargs;
+            return packedArgs;
+        } else {
+            Object val = unwrapper.tryUnwrapTo((TemplateModel) modelArgs.get(fixArgCount), varArgsCompType);
+            if (val == ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS) {
+                return Integer.valueOf(fixArgCount + 1);
+            }
+            Object array = Array.newInstance(varArgsCompType, 1);
+            Array.set(array, 0, val);
+            args[fixArgCount] = array;
+            return args;
+        }
+    }
+    
+}
\ No newline at end of file



[32/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/RegexpHelper.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/RegexpHelper.java b/freemarker-core/src/main/java/org/apache/freemarker/core/RegexpHelper.java
new file mode 100644
index 0000000..3d0e853
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/RegexpHelper.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 java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.templateresolver.impl.MruCacheStorage;
+import org.apache.freemarker.core.util._StringUtil;
+import org.slf4j.Logger;
+
+/**
+ * Helper for language features (like built-ins) that use regular expressions. 
+ */
+final class RegexpHelper {
+
+    private static final Logger LOG = _CoreLogs.RUNTIME;
+    
+    private static volatile boolean flagWarningsEnabled = LOG.isWarnEnabled();
+    private static final int MAX_FLAG_WARNINGS_LOGGED = 25;
+    private static final Object flagWarningsCntSync = new Object();
+    private static int flagWarningsCnt;
+    
+    private static final MruCacheStorage patternCache = new MruCacheStorage(50, 150);
+
+    static private long intFlagToLong(int flag) {
+        return flag & 0x0000FFFFL;
+    }
+
+    // Standard regular expression flags converted to long:
+    static final long RE_FLAG_CASE_INSENSITIVE = intFlagToLong(Pattern.CASE_INSENSITIVE);
+
+    static final long RE_FLAG_MULTILINE = intFlagToLong(Pattern.MULTILINE);
+
+    static final long RE_FLAG_COMMENTS = intFlagToLong(Pattern.COMMENTS);
+
+    static final long RE_FLAG_DOTALL = intFlagToLong(Pattern.DOTALL);
+
+    // FreeMarker-specific regular expression flags (using the higher 32 bits):
+    static final long RE_FLAG_REGEXP = 0x100000000L;
+
+    static final long RE_FLAG_FIRST_ONLY = 0x200000000L;
+    
+    // Can't be instantiated
+    private RegexpHelper() { }
+
+    static Pattern getPattern(String patternString, int flags)
+    throws TemplateModelException {
+        PatternCacheKey patternKey = new PatternCacheKey(patternString, flags);
+        
+        Pattern result;
+        
+        synchronized (patternCache) {
+            result = (Pattern) patternCache.get(patternKey);
+        }
+        if (result != null) {
+            return result;
+        }
+        
+        try {
+            result = Pattern.compile(patternString, flags);
+        } catch (PatternSyntaxException e) {
+            throw new _TemplateModelException(e,
+                    "Malformed regular expression: ", new _DelayedGetMessage(e));
+        }
+        synchronized (patternCache) {
+            patternCache.put(patternKey, result);
+        }
+        return result;
+    }
+
+    private static class PatternCacheKey {
+        private final String patternString;
+        private final int flags;
+        private final int hashCode;
+        
+        public PatternCacheKey(String patternString, int flags) {
+            this.patternString = patternString;
+            this.flags = flags;
+            hashCode = patternString.hashCode() + 31 * flags;
+        }
+        
+        @Override
+        public boolean equals(Object that) {
+            if (that instanceof PatternCacheKey) {
+                PatternCacheKey thatPCK = (PatternCacheKey) that; 
+                return thatPCK.flags == flags
+                        && thatPCK.patternString.equals(patternString);
+            } else {
+                return false;
+            }
+        }
+
+        @Override
+        public int hashCode() {
+            return hashCode;
+        }
+        
+    }
+
+    static long parseFlagString(String flagString) {
+        long flags = 0;
+        for (int i = 0; i < flagString.length(); i++) {
+            char c = flagString.charAt(i);
+            switch (c) {
+                case 'i':
+                    flags |= RE_FLAG_CASE_INSENSITIVE;
+                    break;
+                case 'm':
+                    flags |= RE_FLAG_MULTILINE;
+                    break;
+                case 'c':
+                    flags |= RE_FLAG_COMMENTS;
+                    break;
+                case 's':
+                    flags |= RE_FLAG_DOTALL;
+                    break;
+                case 'r':
+                    flags |= RE_FLAG_REGEXP;
+                    break;
+                case 'f':
+                    flags |= RE_FLAG_FIRST_ONLY;
+                    break;
+                default:
+                    if (flagWarningsEnabled) {
+                        // [FM3] Should be an error
+                        RegexpHelper.logFlagWarning(
+                                "Unrecognized regular expression flag: "
+                                + _StringUtil.jQuote(String.valueOf(c)) + ".");
+                    }
+            }  // switch
+        }
+        return flags;
+    }
+
+    /**
+     * Logs flag warning for a limited number of times. This is used to prevent
+     * log flooding.
+     */
+    static void logFlagWarning(String message) {
+        if (!flagWarningsEnabled) return;
+        
+        int cnt;
+        synchronized (flagWarningsCntSync) {
+            cnt = flagWarningsCnt;
+            if (cnt < MAX_FLAG_WARNINGS_LOGGED) {
+                flagWarningsCnt++;
+            } else {
+                flagWarningsEnabled = false;
+                return;
+            }
+        }
+        message += " This will be an error in some later FreeMarker version!";
+        if (cnt + 1 == MAX_FLAG_WARNINGS_LOGGED) {
+            message += " [Will not log more regular expression flag problems until restart!]";
+        }
+        LOG.warn(message);
+    }
+
+    static void checkNonRegexpFlags(String biName, long flags) throws _TemplateModelException {
+        checkOnlyHasNonRegexpFlags(biName, flags, false);
+    }
+    
+    static void checkOnlyHasNonRegexpFlags(String biName, long flags, boolean strict)
+            throws _TemplateModelException {
+        if (!strict && !flagWarningsEnabled) return;
+        
+        String flag; 
+        if ((flags & RE_FLAG_MULTILINE) != 0) {
+            flag = "m";
+        } else if ((flags & RE_FLAG_DOTALL) != 0) {
+            flag = "s";
+        } else if ((flags & RE_FLAG_COMMENTS) != 0) {
+            flag = "c";
+        } else {
+            return;
+        }
+
+        final Object[] msg = { "?", biName ," doesn't support the \"", flag, "\" flag "
+                + "without the \"r\" flag." };
+        if (strict) {
+            throw new _TemplateModelException(msg);
+        } else {
+            // Suppress error for backward compatibility
+            logFlagWarning(new _ErrorDescriptionBuilder(msg).toString());
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/RightUnboundedRangeModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/RightUnboundedRangeModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/RightUnboundedRangeModel.java
new file mode 100644
index 0000000..e135d18
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/RightUnboundedRangeModel.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+abstract class RightUnboundedRangeModel extends RangeModel {
+    
+    RightUnboundedRangeModel(int begin) {
+        super(begin);
+    }
+
+    @Override
+    final int getStep() {
+        return 1;
+    }
+
+    @Override
+    final boolean isRightUnbounded() {
+        return true;
+    }
+    
+    @Override
+    final boolean isRightAdaptive() {
+        return true;
+    }
+
+    @Override
+    final boolean isAffactedByStringSlicingBug() {
+        return false;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/SettingValueNotSetException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/SettingValueNotSetException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/SettingValueNotSetException.java
new file mode 100644
index 0000000..1ce895d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/SettingValueNotSetException.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.util._StringUtil;
+
+public class SettingValueNotSetException extends IllegalStateException {
+
+    private final String settingName;
+
+    public SettingValueNotSetException(String settingName) {
+        super("The " + _StringUtil.jQuote(settingName)
+                + " setting is not set in this layer and has no default here either.");
+        this.settingName = settingName;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/SpecialBuiltIn.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/SpecialBuiltIn.java b/freemarker-core/src/main/java/org/apache/freemarker/core/SpecialBuiltIn.java
new file mode 100644
index 0000000..5d43b59
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/SpecialBuiltIn.java
@@ -0,0 +1,27 @@
+/*
+ * 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;
+
+
+/**
+ * Marker class for built-ins that has special treatment during parsing.
+ */
+abstract class SpecialBuiltIn extends ASTExpBuiltIn {
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/StopException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/StopException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/StopException.java
new file mode 100644
index 0000000..9706456
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/StopException.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 java.io.PrintStream;
+import java.io.PrintWriter;
+
+/**
+ * This exception is thrown when a <tt>#stop</tt> directive is encountered. 
+ */
+public class StopException extends TemplateException {
+    
+    StopException(Environment env) {
+        super(env);
+    }
+
+    StopException(Environment env, String s) {
+        super(s, env);
+    }
+
+    @Override
+    public void printStackTrace(PrintWriter pw) {
+        synchronized (pw) {
+            String msg = getMessage();
+            pw.print("Encountered stop instruction");
+            if (msg != null && !msg.equals("")) {
+                pw.println("\nCause given: " + msg);
+            } else pw.println();
+            super.printStackTrace(pw);
+        }
+    }
+
+    @Override
+    public void printStackTrace(PrintStream ps) {
+        synchronized (ps) {
+            String msg = getMessage();
+            ps.print("Encountered stop instruction");
+            if (msg != null && !msg.equals("")) {
+                ps.println("\nCause given: " + msg);
+            } else ps.println();
+            super.printStackTrace(ps);
+        }
+    }
+    
+}
+
+

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java b/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java
new file mode 100644
index 0000000..c4787e4
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java
@@ -0,0 +1,1341 @@
+/*
+ * 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.BufferedReader;
+import java.io.FilterReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.PrintStream;
+import java.io.Reader;
+import java.io.Serializable;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.lang.reflect.UndeclaredThrowableException;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.freemarker.core.arithmetic.ArithmeticEngine;
+import org.apache.freemarker.core.debug._DebuggerService;
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateNodeModel;
+import org.apache.freemarker.core.model.impl.SimpleHash;
+import org.apache.freemarker.core.outputformat.OutputFormat;
+import org.apache.freemarker.core.templateresolver.TemplateLoader;
+import org.apache.freemarker.core.templateresolver.TemplateLookupStrategy;
+import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateResolver;
+import org.apache.freemarker.core.templateresolver.impl.FileTemplateLoader;
+import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.util._NullArgumentException;
+import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+/**
+ * <p>
+ * Stores an already parsed template, ready to be processed (rendered) for unlimited times, possibly from multiple
+ * threads.
+ * 
+ * <p>
+ * Typically, you will use {@link Configuration#getTemplate(String)} to invoke/get {@link Template} objects, so you
+ * don't construct them directly. But you can also construct a template from a {@link Reader} or a {@link String} that
+ * contains the template source code. But then it's important to know that while the resulting {@link Template} is
+ * efficient for later processing, creating a new {@link Template} itself is relatively expensive. So try to re-use
+ * {@link Template} objects if possible. {@link Configuration#getTemplate(String)} (and its overloads) does that
+ * (caching {@link Template}-s) for you, but the constructor of course doesn't, so it's up to you to solve then.
+ * 
+ * <p>
+ * Objects of this class meant to be handled as immutable and thus thread-safe. However, it has some setter methods for
+ * changing FreeMarker settings. Those must not be used while the template is being processed, or if the template object
+ * is already accessible from multiple threads. If some templates need different settings that those coming from the
+ * shared {@link Configuration}, and you are using {@link Configuration#getTemplate(String)} (or its overloads), then
+ * use the {@link Configuration#getTemplateConfigurations() templateConfigurations} setting to achieve that.
+ */
+// TODO [FM3] Try to make Template serializable for distributed caching. Transient fields will have to be restored.
+public class Template implements ProcessingConfiguration, CustomStateScope {
+    public static final String DEFAULT_NAMESPACE_PREFIX = "D";
+    public static final String NO_NS_PREFIX = "N";
+
+    private static final int READER_BUFFER_SIZE = 8192;
+
+    private ASTElement rootElement;
+    private Map macros = new HashMap(); // TODO Don't create new object if it remains empty.
+    private List imports = new ArrayList(); // TODO Don't create new object if it remains empty.
+
+    // Source (TemplateLoader) related information:
+    private final String sourceName;
+    private final ArrayList lines = new ArrayList();
+
+    // TODO [FM3] We want to get rid of these, thenthe same Template object could be reused for different lookups.
+    // Template lookup parameters:
+    private final String lookupName;
+    private Locale lookupLocale;
+    private Serializable customLookupCondition;
+
+    // Inherited settings:
+    private final transient Configuration cfg;
+    private final transient TemplateConfiguration tCfg;
+    private final transient ParsingConfiguration parsingConfiguration;
+
+    // Values from the template content (#ftl header parameters usually), as opposed to from the TemplateConfiguration:
+    private transient OutputFormat outputFormat; // TODO Deserialization: use the name of the output format
+    private String defaultNS;
+    private Map prefixToNamespaceURILookup = new HashMap();
+    private Map namespaceURIToPrefixLookup = new HashMap();
+    private Map<String, Serializable> customAttributes;
+    private transient Map<Object, Object> mergedCustomAttributes;
+
+    private Integer autoEscapingPolicy;
+    // Values from template content that are detected automatically:
+    private Charset actualSourceEncoding;
+    private int actualTagSyntax;
+
+    private int actualNamingConvention;
+    // Custom state:
+    private final Object customStateMapLock = new Object();
+    private final ConcurrentHashMap<CustomStateKey, Object> customStateMap = new ConcurrentHashMap<>(0);
+
+    /**
+     * Same as {@link #Template(String, String, Reader, Configuration)} with {@code null} {@code sourceName} parameter.
+     */
+    public Template(String lookupName, Reader reader, Configuration cfg) throws IOException {
+        this(lookupName, null, reader, cfg);
+    }
+
+    /**
+     * Convenience constructor for {@link #Template(String, Reader, Configuration)
+     * Template(lookupName, new StringReader(reader), cfg)}.
+     * 
+     * @since 2.3.20
+     */
+    public Template(String lookupName, String sourceCode, Configuration cfg) throws IOException {
+        this(lookupName, new StringReader(sourceCode), cfg);
+    }
+
+    /**
+     * Convenience constructor for {@link #Template(String, String, Reader, Configuration, TemplateConfiguration,
+     * Charset) Template(lookupName, null, new StringReader(reader), cfg), tc, null}.
+     *
+     * @since 2.3.20
+     */
+    public Template(String lookupName, String sourceCode, Configuration cfg, TemplateConfiguration tc) throws IOException {
+        this(lookupName, null, new StringReader(sourceCode), cfg, tc, null);
+    }
+
+    /**
+     * Convenience constructor for {@link #Template(String, String, Reader, Configuration, Charset) Template(lookupName, null,
+     * reader, cfg, sourceEncoding)}.
+     */
+    public Template(String lookupName, Reader reader, Configuration cfg, Charset sourceEncoding) throws IOException {
+        this(lookupName, null, reader, cfg, sourceEncoding);
+    }
+
+    /**
+     * Constructs a template from a character stream. Note that this is a relatively expensive operation; where higher
+     * performance matters, you should re-use (cache) {@link Template} instances instead of re-creating them from the
+     * same source again and again. ({@link Configuration#getTemplate(String) and its overloads already do such reuse.})
+     * 
+     * @param lookupName
+     *            The name (path) with which the template was get (usually via
+     *            {@link Configuration#getTemplate(String)}), after basic normalization. (Basic normalization means
+     *            things that doesn't require accessing the backing storage, such as {@code "/a/../b/foo.ftl"}
+     *            becomes to {@code "b/foo.ftl"}).
+     *            This is usually the path of the template file relatively to the (virtual) directory that you use to
+     *            store the templates (except if the {@link #getSourceName()}  sourceName} differs from it).
+     *            Shouldn't start with {@code '/'}. Should use {@code '/'}, not {@code '\'}. Check
+     *            {@link #getLookupName()} to see how the name will be used. The name should be independent of the actual
+     *            storage mechanism and physical location as far as possible. Even when the templates are stored
+     *            straightforwardly in real files (they often aren't; see {@link TemplateLoader}), the name shouldn't be
+     *            an absolute file path. Like if the template is stored in {@code "/www/templates/forum/main.ftl"}, and
+     *            you are using {@code "/www/templates/"} as the template root directory via
+     *            {@link FileTemplateLoader#FileTemplateLoader(java.io.File)}, then the template name will be
+     *            {@code "forum/main.ftl"}. The name can be {@code null} (should be used for template made on-the-fly
+     *            instead of being loaded from somewhere), in which case relative paths in it will be relative to
+     *            the template root directory (and here again, it's the {@link TemplateLoader} that knows what that
+     *            "physically" means).
+     * @param sourceName
+     *            Often the same as the {@code lookupName}; see {@link #getSourceName()} for more. Can be
+     *            {@code null}, in which case error messages will fall back to use {@link #getLookupName()}.
+     * @param reader
+     *            The character stream to read from. The {@link Reader} is <em>not</em> closed by this method (unlike
+     *            in FreeMarker 2.x.x), so be sure that it's closed somewhere. (Except of course, readers like
+     *            {@link StringReader} need not be closed.) The {@link Reader} need not be buffered, because this
+     *            method ensures that it will be read in few kilobyte chunks, not byte by byte.
+     * @param cfg
+     *            The Configuration object that this Template is associated with. Can't be {@code null}.
+     * 
+     * @since 2.3.22
+     */
+   public Template(
+           String lookupName, String sourceName, Reader reader, Configuration cfg) throws IOException {
+       this(lookupName, sourceName, reader, cfg, null);
+   }
+
+    /**
+     * Same as {@link #Template(String, String, Reader, Configuration)}, but also specifies the template's source
+     * encoding.
+     *
+     * @param actualSourceEncoding
+     *            This is the charset that was used to read the template. This can be {@code null} if the template
+     *            was loaded from a source that returns it already as text. If this is not {@code null} and there's an
+     *            {@code #ftl} header with {@code encoding} parameter, they must match, or else a
+     *            {@link WrongTemplateCharsetException} is thrown.
+     * 
+     * @since 2.3.22
+     */
+   public Template(
+           String lookupName, String sourceName, Reader reader, Configuration cfg, Charset actualSourceEncoding) throws
+           IOException {
+       this(lookupName, sourceName, reader, cfg, null, actualSourceEncoding);
+   }
+
+    /**
+     * Same as {@link #Template(String, String, Reader, Configuration, Charset)}, but also specifies a
+     * {@link TemplateConfiguration}. This is mostly meant to be used by FreeMarker internally, but advanced users might
+     * still find this useful.
+     * 
+     * @param templateConfiguration
+     *            Overrides the configuration settings of the {@link Configuration} parameter; can be
+     *            {@code null}. This is useful as the {@link Configuration} is normally a singleton shared by all
+     *            templates, and so it's not good for specifying template-specific settings. Settings that influence
+     *            parsing always have an effect, while settings that influence processing only have effect when the
+     *            template is the main template of the {@link Environment}.
+     * @param actualSourceEncoding
+     *            Same as in {@link #Template(String, String, Reader, Configuration, Charset)}.
+     * 
+     * @since 2.3.24
+     */
+   public Template(
+           String lookupName, String sourceName, Reader reader,
+           Configuration cfg, TemplateConfiguration templateConfiguration,
+           Charset actualSourceEncoding) throws IOException {
+       this(lookupName, sourceName, reader, cfg, templateConfiguration, actualSourceEncoding, null);
+    }
+
+    /**
+     * Same as {@link #Template(String, String, Reader, Configuration, TemplateConfiguration, Charset)}, but allows
+     * specifying the {@code streamToUnmarkWhenEncEstabd}.
+     *
+     * @param streamToUnmarkWhenEncEstabd
+     *         If not {@code null}, when during the parsing we reach a point where we know that no {@link
+     *         WrongTemplateCharsetException} will be thrown, {@link InputStream#mark(int) mark(0)} will be called on this.
+     *         This is meant to be used when the reader parameter is a {@link InputStreamReader}, and this parameter is
+     *         the underlying {@link InputStream}, and you have a mark at the beginning of the {@link InputStream} so
+     *         that you can retry if a {@link WrongTemplateCharsetException} is thrown without extra I/O. As keeping that
+     *         mark consumes some resources, so you may want to release it as soon as possible.
+     */
+    public Template(
+            String lookupName, String sourceName, Reader reader,
+            Configuration cfg, TemplateConfiguration templateConfiguration,
+            Charset actualSourceEncoding, InputStream streamToUnmarkWhenEncEstabd) throws IOException, ParseException {
+        this(lookupName, sourceName, reader,
+                cfg, templateConfiguration,
+                null, null,
+                actualSourceEncoding, streamToUnmarkWhenEncEstabd);
+    }
+
+    /**
+     * Same as {@link #Template(String, String, Reader, Configuration, TemplateConfiguration, Charset, InputStream)},
+     * but allows specifying the output format and the auto escaping policy, with similar effect as if they were
+     * specified in the template content (like in the #ftl header).
+     * <p>
+     * <p>This method is currently only used internally, as it's not generalized enough and so it carries too much
+     * backward compatibility risk. Also, the same functionality can be achieved by constructing an appropriate
+     * {@link TemplateConfiguration}, only that's somewhat slower.
+     *
+     * @param contextOutputFormat
+     *         The output format of the enclosing lexical context, used when a template snippet is parsed on runtime. If
+     *         not {@code null}, this will override the value coming from the {@link TemplateConfiguration} or the
+     *         {@link Configuration}.
+     * @param contextAutoEscapingPolicy
+     *         Similar to {@code contextOutputFormat}; usually this and the that is set together.
+     */
+   Template(
+            String lookupName, String sourceName, Reader reader,
+            Configuration configuration, TemplateConfiguration templateConfiguration,
+            OutputFormat contextOutputFormat, Integer contextAutoEscapingPolicy,
+            Charset actualSourceEncoding, InputStream streamToUnmarkWhenEncEstabd) throws IOException, ParseException {
+        _NullArgumentException.check("configuration", configuration);
+        this.cfg = configuration;
+        this.tCfg = templateConfiguration;
+        this.parsingConfiguration = tCfg != null ? new TemplateParsingConfigurationWithFallback(cfg, tCfg) : cfg;
+        this.lookupName = lookupName;
+        this.sourceName = sourceName;
+
+        setActualSourceEncoding(actualSourceEncoding);
+        LineTableBuilder ltbReader;
+        try {
+            // Ensure that the parameter Reader is only read in bigger chunks, as we don't know if the it's buffered.
+            // In particular, inside the FreeMarker code, we assume that the stream stages need not be buffered.
+            if (!(reader instanceof BufferedReader) && !(reader instanceof StringReader)) {
+                reader = new BufferedReader(reader, READER_BUFFER_SIZE);
+            }
+            
+            ltbReader = new LineTableBuilder(reader, parsingConfiguration);
+            reader = ltbReader;
+            
+            try {
+                FMParser parser = new FMParser(
+                        this, reader,
+                        parsingConfiguration, contextOutputFormat, contextAutoEscapingPolicy,
+                        streamToUnmarkWhenEncEstabd);
+                try {
+                    rootElement = parser.Root();
+                } catch (IndexOutOfBoundsException exc) {
+                    // There's a JavaCC bug where the Reader throws a RuntimeExcepton and then JavaCC fails with
+                    // IndexOutOfBoundsException. If that wasn't the case, we just rethrow. Otherwise we suppress the
+                    // IndexOutOfBoundsException and let the real cause to be thrown later. 
+                    if (!ltbReader.hasFailure()) {
+                        throw exc;
+                    }
+                    rootElement = null;
+                }
+                actualTagSyntax = parser._getLastTagSyntax();
+                actualNamingConvention = parser._getLastNamingConvention();
+            } catch (TokenMgrError exc) {
+                // TokenMgrError VS ParseException is not an interesting difference for the user, so we just convert it
+                // to ParseException
+                throw exc.toParseException(this);
+            }
+        } catch (ParseException e) {
+            e.setTemplate(this);
+            throw e;
+        }
+        
+        // Throws any exception that JavaCC has silently treated as EOF:
+        ltbReader.throwFailure();
+        
+        _DebuggerService.registerTemplate(this);
+        namespaceURIToPrefixLookup = Collections.unmodifiableMap(namespaceURIToPrefixLookup);
+        prefixToNamespaceURILookup = Collections.unmodifiableMap(prefixToNamespaceURILookup);
+    }
+
+    /**
+     * Same as {@link #createPlainTextTemplate(String, String, String, Configuration, Charset)} with {@code null}
+     * {@code sourceName} argument.
+     */
+    static public Template createPlainTextTemplate(String lookupName, String content, Configuration config) {
+        return createPlainTextTemplate(lookupName, null, content, config, null);
+    }
+
+    /**
+     * Creates a {@link Template} that only contains a single block of static text, no dynamic content.
+     * 
+     * @param lookupName
+     *            See {@link #getLookupName} for more details.
+     * @param sourceName
+     *            See {@link #getSourceName} for more details. If {@code null}, it will be the same as the {@code name}.
+     * @param content
+     *            the block of text that this template represents
+     * @param config
+     *            the configuration to which this template belongs
+     *
+     * @param sourceEncoding The charset used to decode the template content to the {@link String} passed in with the
+     *            {@code content} parameter. If that information is not known or irrelevant, this should be
+     *            {@code null}.
+     *
+     * @since 2.3.22
+     */
+    static public Template createPlainTextTemplate(String lookupName, String sourceName, String content, Configuration config,
+               Charset sourceEncoding) {
+        Template template;
+        try {
+            template = new Template(lookupName, sourceName, new StringReader("X"), config);
+        } catch (IOException e) {
+            throw new BugException("Plain text template creation failed", e);
+        }
+        ((ASTStaticText) template.rootElement).replaceText(content);
+        template.setActualSourceEncoding(sourceEncoding);
+
+        _DebuggerService.registerTemplate(template);
+
+        return template;
+    }
+
+    /**
+     * Executes template, using the data-model provided, writing the generated output to the supplied {@link Writer}.
+     * 
+     * <p>
+     * For finer control over the runtime environment setup, such as per-HTTP-request configuring of FreeMarker
+     * settings, you may need to use {@link #createProcessingEnvironment(Object, Writer)} instead.
+     * 
+     * @param dataModel
+     *            the holder of the variables visible from the template (name-value pairs); usually a
+     *            {@code Map<String, Object>} or a JavaBean (where the JavaBean properties will be the variables). Can
+     *            be any object that the {@link ObjectWrapper} in use turns into a {@link TemplateHashModel}. You can
+     *            also use an object that already implements {@link TemplateHashModel}; in that case it won't be
+     *            wrapped. If it's {@code null}, an empty data model is used.
+     * @param out
+     *            The {@link Writer} where the output of the template will go. Note that unless you have set
+     *            {@link ProcessingConfiguration#getAutoFlush() autoFlush} to {@code false} to disable this,
+     *            {@link Writer#flush()} will be called at the when the template processing was finished.
+     *            {@link Writer#close()} is not called. Can't be {@code null}.
+     * 
+     * @throws TemplateException
+     *             if an exception occurs during template processing
+     * @throws IOException
+     *             if an I/O exception occurs during writing to the writer.
+     */
+    public void process(Object dataModel, Writer out)
+    throws TemplateException, IOException {
+        createProcessingEnvironment(dataModel, out, null).process();
+    }
+
+    /**
+     * Like {@link #process(Object, Writer)}, but also sets a (XML-)node to be recursively processed by the template.
+     * That node is accessed in the template with <tt>.node</tt>, <tt>#recurse</tt>, etc. See the
+     * <a href="http://freemarker.org/docs/xgui_declarative.html" target="_blank">Declarative XML Processing</a> as a
+     * typical example of recursive node processing.
+     * 
+     * @param rootNode The root node for recursive processing or {@code null}.
+     * 
+     * @throws TemplateException if an exception occurs during template processing
+     * @throws IOException if an I/O exception occurs during writing to the writer.
+     */
+    public void process(Object dataModel, Writer out, ObjectWrapper wrapper, TemplateNodeModel rootNode)
+    throws TemplateException, IOException {
+        Environment env = createProcessingEnvironment(dataModel, out, wrapper);
+        if (rootNode != null) {
+            env.setCurrentVisitorNode(rootNode);
+        }
+        env.process();
+    }
+    
+    /**
+     * Like {@link #process(Object, Writer)}, but overrides the {@link Configuration#getObjectWrapper()}.
+     * 
+     * @param wrapper The {@link ObjectWrapper} to be used instead of what {@link Configuration#getObjectWrapper()}
+     *      provides, or {@code null} if you don't want to override that. 
+     */
+    public void process(Object dataModel, Writer out, ObjectWrapper wrapper)
+    throws TemplateException, IOException {
+        createProcessingEnvironment(dataModel, out, wrapper).process();
+    }
+    
+   /**
+    * Creates a {@link org.apache.freemarker.core.Environment Environment} object, using this template, the data-model provided as
+    * parameter. You have to call {@link Environment#process()} on the return value to set off the actual rendering.
+    * 
+    * <p>Use this method if you want to do some special initialization on the {@link Environment} before template
+    * processing, or if you want to read the {@link Environment} after template processing. Otherwise using
+    * {@link Template#process(Object, Writer)} is simpler.
+    *
+    * <p>Example:
+    *
+    * <pre>
+    * Environment env = myTemplate.createProcessingEnvironment(root, out, null);
+    * env.process();</pre>
+    * 
+    * <p>The above is equivalent with this:
+    * 
+    * <pre>
+    * myTemplate.process(root, out);</pre>
+    * 
+    * <p>But with <tt>createProcessingEnvironment</tt>, you can manipulate the environment
+    * before and after the processing:
+    * 
+    * <pre>
+    * Environment env = myTemplate.createProcessingEnvironment(root, out);
+    * 
+    * env.setLocale(myUsersPreferredLocale);
+    * env.setTimeZone(myUsersPreferredTimezone);
+    * 
+    * env.process();  // output is rendered here
+    * 
+    * TemplateModel x = env.getVariable("x");  // read back a variable set by the template</pre>
+    *
+    * @param dataModel the holder of the variables visible from all templates; see {@link #process(Object, Writer)} for
+    *     more details.
+    * @param wrapper The {@link ObjectWrapper} to use to wrap objects into {@link TemplateModel}
+    *     instances. Normally you left it {@code null}, in which case {@link MutableProcessingConfiguration#getObjectWrapper()} will be
+    *     used.
+    * @param out The {@link Writer} where the output of the template will go; see {@link #process(Object, Writer)} for
+    *     more details.
+    *     
+    * @return the {@link Environment} object created for processing. Call {@link Environment#process()} to process the
+    *    template.
+    * 
+    * @throws TemplateException if an exception occurs while setting up the Environment object.
+    * @throws IOException if an exception occurs doing any auto-imports
+    */
+    public Environment createProcessingEnvironment(Object dataModel, Writer out, ObjectWrapper wrapper)
+    throws TemplateException, IOException {
+        final TemplateHashModel dataModelHash;
+        if (dataModel instanceof TemplateHashModel) {
+            dataModelHash = (TemplateHashModel) dataModel;
+        } else {
+            if (wrapper == null) {
+                wrapper = getObjectWrapper();
+            }
+
+            if (dataModel == null) {
+                dataModelHash = new SimpleHash(wrapper);
+            } else {
+                TemplateModel wrappedDataModel = wrapper.wrap(dataModel);
+                if (wrappedDataModel instanceof TemplateHashModel) {
+                    dataModelHash = (TemplateHashModel) wrappedDataModel;
+                } else if (wrappedDataModel == null) {
+                    throw new IllegalArgumentException(
+                            wrapper.getClass().getName() + " converted " + dataModel.getClass().getName() + " to null.");
+                } else {
+                    throw new IllegalArgumentException(
+                            wrapper.getClass().getName() + " didn't convert " + dataModel.getClass().getName()
+                            + " to a TemplateHashModel. Generally, you want to use a Map<String, Object> or a "
+                            + "JavaBean as the root-map (aka. data-model) parameter. The Map key-s or JavaBean "
+                            + "property names will be the variable names in the template.");
+                }
+            }
+        }
+        return new Environment(this, dataModelHash, out);
+    }
+
+    /**
+     * Same as {@link #createProcessingEnvironment(Object, Writer, ObjectWrapper)
+     * createProcessingEnvironment(dataModel, out, null)}.
+     */
+    public Environment createProcessingEnvironment(Object dataModel, Writer out)
+    throws TemplateException, IOException {
+        return createProcessingEnvironment(dataModel, out, null);
+    }
+    
+    /**
+     * Returns a string representing the raw template
+     * text in canonical form.
+     */
+    @Override
+    public String toString() {
+        StringWriter sw = new StringWriter();
+        try {
+            dump(sw);
+        } catch (IOException ioe) {
+            throw new RuntimeException(ioe.getMessage());
+        }
+        return sw.toString();
+    }
+
+
+    /**
+     * The usually path-like (or URL-like) normalized identifier of the template, with which the template was get
+     * (usually via {@link Configuration#getTemplate(String)}), or possibly {@code null} for non-stored templates.
+     * It usually looks like a relative UN*X path; it should use {@code /}, not {@code \}, and shouldn't
+     * start with {@code /} (but there are no hard guarantees). It's not a real path in a file-system, it's just a name
+     * that a {@link TemplateLoader} used to load the backing resource (in simple cases; actually that name is
+     * {@link #getSourceName()}, but see it there). Or, it can also be a name that was never used to load the template
+     * (directly created with {@link #Template(String, Reader, Configuration)}). Even if the templates are stored
+     * straightforwardly in files, this is relative to the base directory of the {@link TemplateLoader}. So it really
+     * could be anything, except that it has importance in these situations:
+     * 
+     * <p>
+     * Relative paths to other templates in this template will be resolved relatively to the directory part of this.
+     * Like if the template name is {@code "foo/this.ftl"}, then {@code <#include "other.ftl">} gets the template with
+     * name {@code "foo/other.ftl"}.
+     * </p>
+     * 
+     * <p>
+     * You should not use this name to indicate error locations, or to find the actual templates in general, because
+     * localized lookup, acquisition and other lookup strategies can transform names before they get to the
+     * {@link TemplateLoader} (the template storage) mechanism. Use {@link #getSourceName()} for these purposes.
+     * </p>
+     * 
+     * <p>
+     * Some frameworks use URL-like template names like {@code "someSchema://foo/bar.ftl"}. FreeMarker understands this
+     * notation, so an absolute path like {@code "/baaz.ftl"} in that template will be resolved too
+     * {@code "someSchema://baaz.ftl"}.
+     */
+    public String getLookupName() {
+        return lookupName;
+    }
+
+    /**
+     * The name that was actually used to load this template from the {@link TemplateLoader} (or from other custom
+     * storage mechanism). This is what should be shown in error messages as the error location. This is usually the
+     * same as {@link #getLookupName()}, except when localized lookup, template acquisition ({@code *} step in the
+     * name), or other {@link TemplateLookupStrategy} transforms the requested name ({@link #getLookupName()}) to a
+     * different final {@link TemplateLoader}-level name. For example, when you get a template with name {@code "foo
+     * .ftl"} then because of localized lookup, it's possible that something like {@code "foo_en.ftl"} will be loaded
+     * behind the scenes. While the template name will be still the same as the requested template name ({@code "foo
+     * .ftl"}), errors should point to {@code "foo_de.ftl"}. Note that relative paths are always resolved relatively
+     * to the {@code name}, not to the {@code sourceName}.
+     */
+    public String getSourceName() {
+        return sourceName;
+    }
+
+    /**
+     * Returns the {@linkplain #getSourceName() source name}, or if that's {@code null} then the
+     * {@linkplain #getLookupName() lookup name}. This name is primarily meant to be used in error messages.
+     */
+    public String getSourceOrLookupName() {
+        return getSourceName() != null ? getSourceName() : getLookupName();
+    }
+
+    /**
+     * Returns the Configuration object associated with this template.
+     */
+    public Configuration getConfiguration() {
+        return cfg;
+    }
+
+    /**
+     * The {@link TemplateConfiguration} associated to this template, or {@code null} if there was none.
+     */
+    public TemplateConfiguration getTemplateConfiguration() {
+        return tCfg;
+    }
+
+    public ParsingConfiguration getParsingConfiguration() {
+        return parsingConfiguration;
+    }
+
+
+    /**
+     * @param actualSourceEncoding
+     *            The sourceEncoding that was used to read this template, or {@code null} if the source of the template
+     *            already gives back text (as opposed to binary data), so no decoding with a charset was needed.
+     */
+    void setActualSourceEncoding(Charset actualSourceEncoding) {
+        this.actualSourceEncoding = actualSourceEncoding;
+    }
+
+    /**
+     * The charset that was actually used to read this template from the binary source, or {@code null} if that
+     * information is not known.
+     * When using {@link DefaultTemplateResolver}, this is {@code null} exactly if the {@link TemplateLoader}
+     * returns text instead of binary content, which should only be the case for data sources that naturally return
+     * text (such as varchar and CLOB columns in a database).
+     */
+    public Charset getActualSourceEncoding() {
+        return actualSourceEncoding;
+    }
+    
+    /**
+     * Gets the custom lookup condition with which this template was found. See the {@code customLookupCondition}
+     * parameter of {@link Configuration#getTemplate(String, Locale, Serializable, boolean)} for more
+     * explanation.
+     */
+    public Serializable getCustomLookupCondition() {
+        return customLookupCondition;
+    }
+
+    /**
+     * Mostly only used internally; setter pair of {@link #getCustomLookupCondition()}. This meant to be called directly
+     * after instantiating the template with its constructor, after a successfull lookup that used this condition. So
+     * this should only be called from code that deals with creating new {@code Template} objects, like from
+     * {@link DefaultTemplateResolver}.
+     */
+    public void setCustomLookupCondition(Serializable customLookupCondition) {
+        this.customLookupCondition = customLookupCondition;
+    }
+
+    /**
+     * Returns the tag syntax the parser has chosen for this template. If the syntax could be determined, it's
+     * {@link ParsingConfiguration#SQUARE_BRACKET_TAG_SYNTAX} or {@link ParsingConfiguration#ANGLE_BRACKET_TAG_SYNTAX}. If the syntax
+     * couldn't be determined (like because there was no tags in the template, or it was a plain text template), this
+     * returns whatever the default is in the current configuration, so it's maybe
+     * {@link ParsingConfiguration#AUTO_DETECT_TAG_SYNTAX}.
+     * 
+     * @since 2.3.20
+     */
+    public int getActualTagSyntax() {
+        return actualTagSyntax;
+    }
+    
+    /**
+     * Returns the naming convention the parser has chosen for this template. If it could be determined, it's
+     * {@link ParsingConfiguration#LEGACY_NAMING_CONVENTION} or {@link ParsingConfiguration#CAMEL_CASE_NAMING_CONVENTION}. If it
+     * couldn't be determined (like because there no identifier that's part of the template language was used where
+     * the naming convention matters), this returns whatever the default is in the current configuration, so it's maybe
+     * {@link ParsingConfiguration#AUTO_DETECT_TAG_SYNTAX}.
+     * 
+     * @since 2.3.23
+     */
+    public int getActualNamingConvention() {
+        return actualNamingConvention;
+    }
+    
+    /**
+     * Returns the output format (see {@link Configuration#getOutputFormat()}) used for this template.
+     * The output format of a template can come from various places, in order of increasing priority:
+     * {@link Configuration#getOutputFormat()}, {@link ParsingConfiguration#getOutputFormat()} (which is usually
+     * provided by {@link Configuration#getTemplateConfigurations()}) and the {@code #ftl} header's
+     * {@code output_format} option in the template.
+     * 
+     * @since 2.3.24
+     */
+    public OutputFormat getOutputFormat() {
+        return outputFormat;
+    }
+
+    /**
+     * Should be called by the parser, for example to apply the output format specified in the #ftl header.
+     */
+    void setOutputFormat(OutputFormat outputFormat) {
+        this.outputFormat = outputFormat;
+    }
+    
+    /**
+     * Returns the {@link Configuration#getAutoEscapingPolicy()} autoEscapingPolicy) that this template uses.
+     * This is decided from these, in increasing priority:
+     * {@link Configuration#getAutoEscapingPolicy()}, {@link ParsingConfiguration#getAutoEscapingPolicy()},
+     * {@code #ftl} header's {@code auto_esc} option in the template.
+     */
+    public int getAutoEscapingPolicy() {
+        return autoEscapingPolicy != null ? autoEscapingPolicy
+                : tCfg != null && tCfg.isAutoEscapingPolicySet() ? tCfg.getAutoEscapingPolicy()
+                : cfg.getAutoEscapingPolicy();
+    }
+
+    /**
+     * Should be called by the parser, for example to apply the auto escaping policy specified in the #ftl header.
+     */
+    void setAutoEscapingPolicy(int autoEscapingPolicy) {
+        this.autoEscapingPolicy = autoEscapingPolicy;
+    }
+    
+    /**
+     * Dump the raw template in canonical form.
+     */
+    public void dump(PrintStream ps) {
+        ps.print(rootElement.getCanonicalForm());
+    }
+
+    /**
+     * Dump the raw template in canonical form.
+     */
+    public void dump(Writer out) throws IOException {
+        out.write(rootElement != null ? rootElement.getCanonicalForm() : "Unfinished template");
+    }
+
+    void addMacro(ASTDirMacro macro) {
+        macros.put(macro.getName(), macro);
+    }
+
+    void addImport(ASTDirImport ll) {
+        imports.add(ll);
+    }
+
+    /**
+     * Returns the template source at the location specified by the coordinates given, or {@code null} if unavailable.
+     * A strange legacy in the behavior of this method is that it replaces tab characters with spaces according the
+     * value of {@link Template#getParsingConfiguration()}/{@link ParsingConfiguration#getTabSize()} (which usually
+     * comes from {@link Configuration#getTabSize()}), because tab characters move the column number with more than
+     * 1 in error messages. However, if you set the tab size to 1, this method leaves the tab characters as is.
+     * 
+     * @param beginColumn the first column of the requested source, 1-based
+     * @param beginLine the first line of the requested source, 1-based
+     * @param endColumn the last column of the requested source, 1-based
+     * @param endLine the last line of the requested source, 1-based
+     * 
+     * @see org.apache.freemarker.core.ASTNode#getSource()
+     */
+    public String getSource(int beginColumn,
+                            int beginLine,
+                            int endColumn,
+                            int endLine) {
+        if (beginLine < 1 || endLine < 1) return null;  // dynamically ?eval-ed expressions has no source available
+        
+        // Our container is zero-based.
+        --beginLine;
+        --beginColumn;
+        --endColumn;
+        --endLine;
+        StringBuilder buf = new StringBuilder();
+        for (int i = beginLine ; i <= endLine; i++) {
+            if (i < lines.size()) {
+                buf.append(lines.get(i));
+            }
+        }
+        int lastLineLength = lines.get(endLine).toString().length();
+        int trailingCharsToDelete = lastLineLength - endColumn - 1;
+        buf.delete(0, beginColumn);
+        buf.delete(buf.length() - trailingCharsToDelete, buf.length());
+        return buf.toString();
+    }
+
+    @Override
+    public Locale getLocale() {
+        // TODO [FM3] Temporary hack; See comment above the locale field
+        if (lookupLocale != null) {
+            return lookupLocale;
+        }
+
+        return tCfg != null && tCfg.isLocaleSet() ? tCfg.getLocale() : cfg.getLocale();
+    }
+
+    // TODO [FM3] Temporary hack; See comment above the locale field
+    public void setLookupLocale(Locale lookupLocale) {
+        this.lookupLocale = lookupLocale;
+    }
+
+    @Override
+    public boolean isLocaleSet() {
+        return tCfg != null && tCfg.isLocaleSet();
+    }
+
+    @Override
+    public TimeZone getTimeZone() {
+        return tCfg != null && tCfg.isTimeZoneSet() ? tCfg.getTimeZone() : cfg.getTimeZone();
+    }
+
+    @Override
+    public boolean isTimeZoneSet() {
+        return tCfg != null && tCfg.isTimeZoneSet();
+    }
+
+    @Override
+    public TimeZone getSQLDateAndTimeTimeZone() {
+        return tCfg != null && tCfg.isSQLDateAndTimeTimeZoneSet() ? tCfg.getSQLDateAndTimeTimeZone() : cfg.getSQLDateAndTimeTimeZone();
+    }
+
+    @Override
+    public boolean isSQLDateAndTimeTimeZoneSet() {
+        return tCfg != null && tCfg.isSQLDateAndTimeTimeZoneSet();
+    }
+
+    @Override
+    public String getNumberFormat() {
+        return tCfg != null && tCfg.isNumberFormatSet() ? tCfg.getNumberFormat() : cfg.getNumberFormat();
+    }
+
+    @Override
+    public boolean isNumberFormatSet() {
+        return tCfg != null && tCfg.isNumberFormatSet();
+    }
+
+    @Override
+    public Map<String, TemplateNumberFormatFactory> getCustomNumberFormats() {
+        return tCfg != null && tCfg.isCustomNumberFormatsSet() ? tCfg.getCustomNumberFormats()
+                : cfg.getCustomNumberFormats();
+    }
+
+    @Override
+    public TemplateNumberFormatFactory getCustomNumberFormat(String name) {
+        if (tCfg != null && tCfg.isCustomNumberFormatsSet()) {
+            TemplateNumberFormatFactory value = tCfg.getCustomNumberFormats().get(name);
+            if (value != null) {
+                return value;
+            }
+        }
+        return cfg.getCustomNumberFormat(name);
+    }
+
+    @Override
+    public boolean isCustomNumberFormatsSet() {
+        return tCfg != null && tCfg.isCustomNumberFormatsSet();
+    }
+
+    @Override
+    public String getBooleanFormat() {
+        return tCfg != null && tCfg.isBooleanFormatSet() ? tCfg.getBooleanFormat() : cfg.getBooleanFormat();
+    }
+
+    @Override
+    public boolean isBooleanFormatSet() {
+        return tCfg != null && tCfg.isBooleanFormatSet();
+    }
+
+    @Override
+    public String getTimeFormat() {
+        return tCfg != null && tCfg.isTimeFormatSet() ? tCfg.getTimeFormat() : cfg.getTimeFormat();
+    }
+
+    @Override
+    public boolean isTimeFormatSet() {
+        return tCfg != null && tCfg.isTimeFormatSet();
+    }
+
+    @Override
+    public String getDateFormat() {
+        return tCfg != null && tCfg.isDateFormatSet() ? tCfg.getDateFormat() : cfg.getDateFormat();
+    }
+
+    @Override
+    public boolean isDateFormatSet() {
+        return tCfg != null && tCfg.isDateFormatSet();
+    }
+
+    @Override
+    public String getDateTimeFormat() {
+        return tCfg != null && tCfg.isDateTimeFormatSet() ? tCfg.getDateTimeFormat() : cfg.getDateTimeFormat();
+    }
+
+    @Override
+    public boolean isDateTimeFormatSet() {
+        return tCfg != null && tCfg.isDateTimeFormatSet();
+    }
+
+    @Override
+    public Map<String, TemplateDateFormatFactory> getCustomDateFormats() {
+        return tCfg != null && tCfg.isCustomDateFormatsSet() ? tCfg.getCustomDateFormats() : cfg.getCustomDateFormats();
+    }
+
+    @Override
+    public TemplateDateFormatFactory getCustomDateFormat(String name) {
+        if (tCfg != null && tCfg.isCustomDateFormatsSet()) {
+            TemplateDateFormatFactory value = tCfg.getCustomDateFormats().get(name);
+            if (value != null) {
+                return value;
+            }
+        }
+        return cfg.getCustomDateFormat(name);
+    }
+
+    @Override
+    public boolean isCustomDateFormatsSet() {
+        return tCfg != null && tCfg.isCustomDateFormatsSet();
+    }
+
+    @Override
+    public TemplateExceptionHandler getTemplateExceptionHandler() {
+        return tCfg != null && tCfg.isTemplateExceptionHandlerSet() ? tCfg.getTemplateExceptionHandler() : cfg.getTemplateExceptionHandler();
+    }
+
+    @Override
+    public boolean isTemplateExceptionHandlerSet() {
+        return tCfg != null && tCfg.isTemplateExceptionHandlerSet();
+    }
+
+    @Override
+    public ArithmeticEngine getArithmeticEngine() {
+        return tCfg != null && tCfg.isArithmeticEngineSet() ? tCfg.getArithmeticEngine() : cfg.getArithmeticEngine();
+    }
+
+    @Override
+    public boolean isArithmeticEngineSet() {
+        return tCfg != null && tCfg.isArithmeticEngineSet();
+    }
+
+    @Override
+    public ObjectWrapper getObjectWrapper() {
+        return tCfg != null && tCfg.isObjectWrapperSet() ? tCfg.getObjectWrapper() : cfg.getObjectWrapper();
+    }
+
+    @Override
+    public boolean isObjectWrapperSet() {
+        return tCfg != null && tCfg.isObjectWrapperSet();
+    }
+
+    @Override
+    public Charset getOutputEncoding() {
+        return tCfg != null && tCfg.isOutputEncodingSet() ? tCfg.getOutputEncoding() : cfg.getOutputEncoding();
+    }
+
+    @Override
+    public boolean isOutputEncodingSet() {
+        return tCfg != null && tCfg.isOutputEncodingSet();
+    }
+
+    @Override
+    public Charset getURLEscapingCharset() {
+        return tCfg != null && tCfg.isURLEscapingCharsetSet() ? tCfg.getURLEscapingCharset() : cfg.getURLEscapingCharset();
+    }
+
+    @Override
+    public boolean isURLEscapingCharsetSet() {
+        return tCfg != null && tCfg.isURLEscapingCharsetSet();
+    }
+
+    @Override
+    public TemplateClassResolver getNewBuiltinClassResolver() {
+        return tCfg != null && tCfg.isNewBuiltinClassResolverSet() ? tCfg.getNewBuiltinClassResolver() : cfg.getNewBuiltinClassResolver();
+    }
+
+    @Override
+    public boolean isNewBuiltinClassResolverSet() {
+        return tCfg != null && tCfg.isNewBuiltinClassResolverSet();
+    }
+
+    @Override
+    public boolean getAPIBuiltinEnabled() {
+        return tCfg != null && tCfg.isAPIBuiltinEnabledSet() ? tCfg.getAPIBuiltinEnabled() : cfg.getAPIBuiltinEnabled();
+    }
+
+    @Override
+    public boolean isAPIBuiltinEnabledSet() {
+        return tCfg != null && tCfg.isAPIBuiltinEnabledSet();
+    }
+
+    @Override
+    public boolean getAutoFlush() {
+        return tCfg != null && tCfg.isAutoFlushSet() ? tCfg.getAutoFlush() : cfg.getAutoFlush();
+    }
+
+    @Override
+    public boolean isAutoFlushSet() {
+        return tCfg != null && tCfg.isAutoFlushSet();
+    }
+
+    @Override
+    public boolean getShowErrorTips() {
+        return tCfg != null && tCfg.isShowErrorTipsSet() ? tCfg.getShowErrorTips() : cfg.getShowErrorTips();
+    }
+
+    @Override
+    public boolean isShowErrorTipsSet() {
+        return tCfg != null && tCfg.isShowErrorTipsSet();
+    }
+
+    @Override
+    public boolean getLogTemplateExceptions() {
+        return tCfg != null && tCfg.isLogTemplateExceptionsSet() ? tCfg.getLogTemplateExceptions() : cfg.getLogTemplateExceptions();
+    }
+
+    @Override
+    public boolean isLogTemplateExceptionsSet() {
+        return tCfg != null && tCfg.isLogTemplateExceptionsSet();
+    }
+
+    @Override
+    public boolean getLazyImports() {
+        return tCfg != null && tCfg.isLazyImportsSet() ? tCfg.getLazyImports() : cfg.getLazyImports();
+    }
+
+    @Override
+    public boolean isLazyImportsSet() {
+        return tCfg != null && tCfg.isLazyImportsSet();
+    }
+
+    @Override
+    public Boolean getLazyAutoImports() {
+        return tCfg != null && tCfg.isLazyAutoImportsSet() ? tCfg.getLazyAutoImports() : cfg.getLazyAutoImports();
+    }
+
+    @Override
+    public boolean isLazyAutoImportsSet() {
+        return tCfg != null && tCfg.isLazyAutoImportsSet();
+    }
+
+    @Override
+    public Map<String, String> getAutoImports() {
+        return tCfg != null && tCfg.isAutoImportsSet() ? tCfg.getAutoImports() : cfg.getAutoImports();
+    }
+
+    @Override
+    public boolean isAutoImportsSet() {
+        return tCfg != null && tCfg.isAutoImportsSet();
+    }
+
+    @Override
+    public List<String> getAutoIncludes() {
+        return tCfg != null && tCfg.isAutoIncludesSet() ? tCfg.getAutoIncludes() : cfg.getAutoIncludes();
+    }
+
+    @Override
+    public boolean isAutoIncludesSet() {
+        return tCfg != null && tCfg.isAutoIncludesSet();
+    }
+
+    /**
+     * This exists to provide the functionality required by {@link ProcessingConfiguration}, but try not call it
+     * too frequently as it has some overhead compared to an usual getter.
+     */
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    @Override
+    public Map<Object, Object> getCustomAttributes() {
+        if (mergedCustomAttributes != null) {
+            return Collections.unmodifiableMap(mergedCustomAttributes);
+        } else if (customAttributes != null) {
+            return (Map) Collections.unmodifiableMap(customAttributes);
+        } else if (tCfg != null && tCfg.isCustomAttributesSet()) {
+            return tCfg.getCustomAttributes();
+        } else {
+            return cfg.getCustomAttributes();
+        }
+    }
+
+    @Override
+    public boolean isCustomAttributesSet() {
+        return customAttributes != null || tCfg != null && tCfg.isCustomAttributesSet();
+    }
+
+    @Override
+    public Object getCustomAttribute(Object name) {
+        // Extra step for custom attributes specified in the #ftl header:
+        if (mergedCustomAttributes != null) {
+            Object value = mergedCustomAttributes.get(name);
+            if (value != null || mergedCustomAttributes.containsKey(name)) {
+                return value;
+            }
+        } else if (customAttributes != null) {
+            Object value = customAttributes.get(name);
+            if (value != null || customAttributes.containsKey(name)) {
+                return value;
+            }
+        } else if (tCfg != null && tCfg.isCustomAttributesSet()) {
+            Object value = tCfg.getCustomAttributes().get(name);
+            if (value != null || tCfg.getCustomAttributes().containsKey(name)) {
+                return value;
+            }
+        }
+        return cfg.getCustomAttribute(name);
+    }
+
+    /**
+     * Should be called by the parser, for example to add the attributes specified in the #ftl header.
+     */
+    void setCustomAttribute(String attName, Serializable attValue) {
+        if (customAttributes == null) {
+            customAttributes = new LinkedHashMap<>();
+        }
+        customAttributes.put(attName, attValue);
+
+        if (tCfg != null && tCfg.isCustomAttributesSet()) {
+            if (mergedCustomAttributes == null) {
+                mergedCustomAttributes = new LinkedHashMap<>(tCfg.getCustomAttributes());
+            }
+            mergedCustomAttributes.put(attName, attValue);
+        }
+    }
+
+    /**
+     * Reader that builds up the line table info for us, and also helps in working around JavaCC's exception
+     * suppression.
+     */
+    private class LineTableBuilder extends FilterReader {
+        
+        private final int tabSize;
+        private final StringBuilder lineBuf = new StringBuilder();
+        int lastChar;
+        boolean closed;
+        
+        /** Needed to work around JavaCC behavior where it silently treats any errors as EOF. */ 
+        private Exception failure; 
+
+        /**
+         * @param r the character stream to wrap
+         */
+        LineTableBuilder(Reader r, ParsingConfiguration parserConfiguration) {
+            super(r);
+            tabSize = parserConfiguration.getTabSize();
+        }
+        
+        public boolean hasFailure() {
+            return failure != null;
+        }
+
+        public void throwFailure() throws IOException {
+            if (failure != null) {
+                if (failure instanceof IOException) {
+                    throw (IOException) failure;
+                }
+                if (failure instanceof RuntimeException) {
+                    throw (RuntimeException) failure;
+                }
+                throw new UndeclaredThrowableException(failure);
+            }
+        }
+
+        @Override
+        public int read() throws IOException {
+            try {
+                int c = in.read();
+                handleChar(c);
+                return c;
+            } catch (Exception e) {
+                throw rememberException(e);
+            }
+        }
+
+        private IOException rememberException(Exception e) throws IOException {
+            // JavaCC used to read from the Reader after it was closed. So we must not treat that as a failure. 
+            if (!closed) {
+                failure = e;
+            }
+            if (e instanceof IOException) {
+                return (IOException) e;
+            }
+            if (e instanceof RuntimeException) {
+                throw (RuntimeException) e;
+            }
+            throw new UndeclaredThrowableException(e);
+        }
+
+        @Override
+        public int read(char cbuf[], int off, int len) throws IOException {
+            try {
+                int numchars = in.read(cbuf, off, len);
+                for (int i = off; i < off + numchars; i++) {
+                    char c = cbuf[i];
+                    handleChar(c);
+                }
+                return numchars;
+            } catch (Exception e) {
+                throw rememberException(e);
+            }
+        }
+
+        @Override
+        public void close() throws IOException {
+            if (lineBuf.length() > 0) {
+                lines.add(lineBuf.toString());
+                lineBuf.setLength(0);
+            }
+            super.close();
+            closed = true;
+        }
+
+        private void handleChar(int c) {
+            if (c == '\n' || c == '\r') {
+                if (lastChar == '\r' && c == '\n') { // CRLF under Windoze
+                    int lastIndex = lines.size() - 1;
+                    String lastLine = (String) lines.get(lastIndex);
+                    lines.set(lastIndex, lastLine + '\n');
+                } else {
+                    lineBuf.append((char) c);
+                    lines.add(lineBuf.toString());
+                    lineBuf.setLength(0);
+                }
+            } else if (c == '\t' && tabSize != 1) {
+                int numSpaces = tabSize - (lineBuf.length() % tabSize);
+                for (int i = 0; i < numSpaces; i++) {
+                    lineBuf.append(' ');
+                }
+            } else {
+                lineBuf.append((char) c);
+            }
+            lastChar = c;
+        }
+    }
+
+    ASTElement getRootASTNode() {
+        return rootElement;
+    }
+    
+    Map getMacros() {
+        return macros;
+    }
+
+    List getImports() {
+        return imports;
+    }
+
+    void addPrefixNSMapping(String prefix, String nsURI) {
+        if (nsURI.length() == 0) {
+            throw new IllegalArgumentException("Cannot map empty string URI");
+        }
+        if (prefix.length() == 0) {
+            throw new IllegalArgumentException("Cannot map empty string prefix");
+        }
+        if (prefix.equals(NO_NS_PREFIX)) {
+            throw new IllegalArgumentException("The prefix: " + prefix + " cannot be registered, it's reserved for special internal use.");
+        }
+        if (prefixToNamespaceURILookup.containsKey(prefix)) {
+            throw new IllegalArgumentException("The prefix: '" + prefix + "' was repeated. This is illegal.");
+        }
+        if (namespaceURIToPrefixLookup.containsKey(nsURI)) {
+            throw new IllegalArgumentException("The namespace URI: " + nsURI + " cannot be mapped to 2 different prefixes.");
+        }
+        if (prefix.equals(DEFAULT_NAMESPACE_PREFIX)) {
+            defaultNS = nsURI;
+        } else {
+            prefixToNamespaceURILookup.put(prefix, nsURI);
+            namespaceURIToPrefixLookup.put(nsURI, prefix);
+        }
+    }
+    
+    public String getDefaultNS() {
+        return defaultNS;
+    }
+    
+    /**
+     * @return the NamespaceUri mapped to this prefix in this template. (Or null if there is none.)
+     */
+    public String getNamespaceForPrefix(String prefix) {
+        if (prefix.equals("")) {
+            return defaultNS == null ? "" : defaultNS;
+        }
+        return (String) prefixToNamespaceURILookup.get(prefix);
+    }
+    
+    /**
+     * @return the prefix mapped to this nsURI in this template. (Or null if there is none.)
+     */
+    public String getPrefixForNamespace(String nsURI) {
+        if (nsURI == null) {
+            return null;
+        }
+        if (nsURI.length() == 0) {
+            return defaultNS == null ? "" : NO_NS_PREFIX;
+        }
+        if (nsURI.equals(defaultNS)) {
+            return "";
+        }
+        return (String) namespaceURIToPrefixLookup.get(nsURI);
+    }
+    
+    /**
+     * @return the prefixed name, based on the ns_prefixes defined
+     * in this template's header for the local name and node namespace
+     * passed in as parameters.
+     */
+    public String getPrefixedName(String localName, String nsURI) {
+        if (nsURI == null || nsURI.length() == 0) {
+            if (defaultNS != null) {
+                return NO_NS_PREFIX + ":" + localName;
+            } else {
+                return localName;
+            }
+        } 
+        if (nsURI.equals(defaultNS)) {
+            return localName;
+        } 
+        String prefix = getPrefixForNamespace(nsURI);
+        if (prefix == null) {
+            return null;
+        }
+        return prefix + ":" + localName;
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    @SuppressFBWarnings("AT_OPERATION_SEQUENCE_ON_CONCURRENT_ABSTRACTION")
+    public <T> T getCustomState(CustomStateKey<T> customStateKey) {
+        T customState = (T) customStateMap.get(customStateKey);
+        if (customState == null) {
+            synchronized (customStateMapLock) {
+                customState = (T) customStateMap.get(customStateKey);
+                if (customState == null) {
+                    customState = customStateKey.create();
+                    if (customState == null) {
+                        throw new IllegalStateException("CustomStateKey.create() must not return null (for key: "
+                                + customStateKey + ")");
+                    }
+                    customStateMap.put(customStateKey, customState);
+                }
+            }
+        }
+        return customState;
+    }
+
+}
+

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateBooleanFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateBooleanFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateBooleanFormat.java
new file mode 100644
index 0000000..52f753d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateBooleanFormat.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.util._StringUtil;
+import org.apache.freemarker.core.valueformat.TemplateValueFormat;
+
+// TODO Should be public and moved over to core.valueformat?
+final class TemplateBooleanFormat extends TemplateValueFormat {
+
+    static final String C_TRUE_FALSE = "true,false";
+    static final TemplateBooleanFormat C_TRUE_FALSE_FORMAT = new TemplateBooleanFormat();
+
+    static TemplateBooleanFormat getInstance(String format) {
+        return format.equals(C_TRUE_FALSE) ? C_TRUE_FALSE_FORMAT : new TemplateBooleanFormat(format);
+    }
+
+    private final String formatString;
+    private final String trueStringValue;  // deduced from booleanFormat
+    private final String falseStringValue;  // deduced from booleanFormat
+
+    /**
+     * Use for {@link #C_TRUE_FALSE} only!
+     */
+    private TemplateBooleanFormat() {
+        formatString = C_TRUE_FALSE;
+        trueStringValue = null;
+        falseStringValue = null;
+    }
+
+    private TemplateBooleanFormat(String formatString) {
+        int commaIdx = formatString.indexOf(',');
+        if (commaIdx == -1) {
+            throw new IllegalArgumentException(
+                    "Setting value must be string that contains two comma-separated values for true and false, " +
+                            "respectively.");
+        }
+
+        this.formatString = formatString;
+        trueStringValue = formatString.substring(0, commaIdx);
+        falseStringValue = formatString.substring(commaIdx + 1);
+    }
+
+    public String getFormatString() {
+        return formatString;
+    }
+
+    /**
+     * Returns the string to which {@code true} is converted to for human audience, or {@code null} if automatic
+     * coercion to string is not allowed. The default value is {@code null}.
+     *
+     * <p>This value is deduced from the {@code "boolean_format"} setting.
+     * Confusingly, for backward compatibility (at least until 2.4) that defaults to {@code "true,false"}, yet this
+     * defaults to {@code null}. That's so because {@code "true,false"} is treated exceptionally, as that default is a
+     * historical mistake in FreeMarker, since it targets computer language output, not human writing. Thus it's
+     * ignored.
+     */
+    public String getTrueStringValue() {
+        return trueStringValue;
+    }
+
+    /**
+     * Same as {@link #getTrueStringValue()} but with {@code false}.
+     */
+    public String getFalseStringValue() {
+        return falseStringValue;
+    }
+
+    @Override
+    public String getDescription() {
+        return _StringUtil.jQuote(formatString);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateClassResolver.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateClassResolver.java b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateClassResolver.java
new file mode 100644
index 0000000..c49e3fa
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateClassResolver.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.util._ClassUtil;
+
+/**
+ * Used by built-ins and other template language features that get a class
+ * based on a string. This can be handy both for implementing security
+ * restrictions and for working around local class-loader issues. 
+ * 
+ * The implementation should be thread-safe, unless an
+ * instance is always only used in a single {@link Environment} object.
+ * 
+ * @see MutableProcessingConfiguration#setNewBuiltinClassResolver(TemplateClassResolver)
+ * 
+ * @since 2.3.17
+ */
+public interface TemplateClassResolver {
+    
+    /**
+     * Simply calls {@link _ClassUtil#forName(String)}.
+     */
+    TemplateClassResolver UNRESTRICTED_RESOLVER = new TemplateClassResolver() {
+
+        @Override
+        public Class resolve(String className, Environment env, Template template)
+        throws TemplateException {
+            try {
+                return _ClassUtil.forName(className);
+            } catch (ClassNotFoundException e) {
+                throw new _MiscTemplateException(e, env);
+            }
+        }
+        
+    };
+    
+    /**
+     * Doesn't allow resolving any classes.
+     */
+    TemplateClassResolver ALLOWS_NOTHING_RESOLVER =  new TemplateClassResolver() {
+
+        @Override
+        public Class resolve(String className, Environment env, Template template)
+        throws TemplateException {
+            throw MessageUtil.newInstantiatingClassNotAllowedException(className, env);
+        }
+        
+    };
+
+    /**
+     * Gets a {@link Class} based on the class name.
+     * 
+     * @param className the full-qualified class name
+     * @param env the environment in which the template executes
+     * @param template the template where the operation that require the
+     *        class resolution resides in. This is <code>null</code> if the
+     *        call doesn't come from a template.
+     *        
+     * @throws TemplateException if the class can't be found or shouldn't be
+     *   accessed from a template for security reasons.
+     */
+    Class resolve(String className, Environment env, Template template) throws TemplateException;
+    
+}


[22/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapper.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapper.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapper.java
new file mode 100644
index 0000000..7e04776
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapper.java
@@ -0,0 +1,1773 @@
+/*
+ * 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.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.lang.reflect.AccessibleObject;
+import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.ResourceBundle;
+import java.util.Set;
+import java.util.WeakHashMap;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.Version;
+import org.apache.freemarker.core._CoreAPI;
+import org.apache.freemarker.core._DelayedFTLTypeDescription;
+import org.apache.freemarker.core._DelayedShortClassName;
+import org.apache.freemarker.core._TemplateModelException;
+import org.apache.freemarker.core.model.AdapterTemplateModel;
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.ObjectWrapperAndUnwrapper;
+import org.apache.freemarker.core.model.RichObjectWrapper;
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateMethodModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelAdapter;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.WrapperTemplateModel;
+import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.util.CommonBuilder;
+import org.apache.freemarker.core.util._ClassUtil;
+import org.apache.freemarker.dom.NodeModel;
+import org.w3c.dom.Node;
+
+/**
+ * The default implementation of the {@link ObjectWrapper} interface. Usually, you don't need to invoke instances of
+ * this, as an instance of this is already the default value of the
+ * {@link Configuration#getObjectWrapper() objectWrapper} setting. Then the
+ * {@link ExtendableBuilder#ExtendableBuilder(Version, boolean) incompatibleImprovements} of the
+ * {@link DefaultObjectWrapper} will be the same that you have set for the {@link Configuration} itself.
+ * 
+ * <p>
+ * If you still need to invoke an instance, that should be done with {@link Builder#build()} (or
+ * with {@link org.apache.freemarker.core.Configuration.ExtendableBuilder#setSetting(String, String)} with
+ * {@code "objectWrapper"} key); the constructor isn't public.
+ *
+ * <p>
+ * This class is thread-safe.
+ */
+public class DefaultObjectWrapper implements RichObjectWrapper {
+
+    /**
+     * At this level of exposure, all methods and properties of the
+     * wrapped objects are exposed to the template.
+     */
+    public static final int EXPOSE_ALL = 0;
+
+    /**
+     * At this level of exposure, all methods and properties of the wrapped
+     * objects are exposed to the template except methods that are deemed
+     * not safe. The not safe methods are java.lang.Object methods wait() and
+     * notify(), java.lang.Class methods getClassLoader() and newInstance(),
+     * java.lang.reflect.Method and java.lang.reflect.Constructor invoke() and
+     * newInstance() methods, all java.lang.reflect.Field set methods, all
+     * java.lang.Thread and java.lang.ThreadGroup methods that can change its
+     * state, as well as the usual suspects in java.lang.System and
+     * java.lang.Runtime.
+     */
+    public static final int EXPOSE_SAFE = 1;
+
+    /**
+     * At this level of exposure, only property getters are exposed.
+     * Additionally, property getters that map to unsafe methods are not
+     * exposed (i.e. Class.classLoader and Thread.contextClassLoader).
+     */
+    public static final int EXPOSE_PROPERTIES_ONLY = 2;
+
+    /**
+     * At this level of exposure, no bean properties and methods are exposed.
+     * Only map items, resource bundle items, and objects retrieved through
+     * the generic get method (on objects of classes that have a generic get
+     * method) can be retrieved through the hash interface.
+     */
+    public static final int EXPOSE_NOTHING = 3;
+
+    // -----------------------------------------------------------------------------------------------------------------
+    // Introspection cache:
+
+    private final Object sharedIntrospectionLock;
+
+    /**
+     * {@link Class} to class info cache.
+     * This object is possibly shared with other {@link DefaultObjectWrapper}-s!
+     *
+     * <p>When reading this, it's good idea to synchronize on sharedInrospectionLock when it doesn't hurt overall
+     * performance. In theory that's not needed, but apps might fail to keep the rules.
+     */
+    private final ClassIntrospector classIntrospector;
+
+    /**
+     * {@link String} class name to {@link StaticModel} cache.
+     * This object only belongs to a single {@link DefaultObjectWrapper}.
+     * This has to be final as {@link #getStaticModels()} might returns it any time and then it has to remain a good
+     * reference.
+     */
+    private final StaticModels staticModels;
+
+    /**
+     * {@link String} class name to an enum value hash.
+     * This object only belongs to a single {@link DefaultObjectWrapper}.
+     * This has to be final as {@link #getStaticModels()} might returns it any time and then it has to remain a good
+     * reference.
+     */
+    private final ClassBasedModelFactory enumModels;
+
+    // -----------------------------------------------------------------------------------------------------------------
+
+    private final int defaultDateType;
+    private final ObjectWrapper outerIdentity;
+    private final boolean strict;
+    @Deprecated // Only exists to keep some JUnit tests working... [FM3]
+    private final boolean useModelCache;
+
+    private final Version incompatibleImprovements;
+
+    /**
+     * Initializes the instance based on the the {@link ExtendableBuilder} specified.
+     *
+     * @param finalizeConstruction Decides if the construction is finalized now, or the caller will do some more
+     *     adjustments on the instance and then call {@link #finalizeConstruction()} itself.
+     */
+    protected DefaultObjectWrapper(ExtendableBuilder builder, boolean finalizeConstruction) {
+        incompatibleImprovements = builder.getIncompatibleImprovements();  // normalized
+
+        defaultDateType = builder.getDefaultDateType();
+        outerIdentity = builder.getOuterIdentity() != null ? builder.getOuterIdentity() : this;
+        strict = builder.isStrict();
+
+        if (builder.getUsePrivateCaches()) {
+            // As this is not a read-only DefaultObjectWrapper, the classIntrospector will be possibly replaced for a few times,
+            // but we need to use the same sharedInrospectionLock forever, because that's what the model factories
+            // synchronize on, even during the classIntrospector is being replaced.
+            sharedIntrospectionLock = new Object();
+            classIntrospector = new ClassIntrospector(builder.classIntrospectorBuilder, sharedIntrospectionLock);
+        } else {
+            // As this is a read-only DefaultObjectWrapper, the classIntrospector is never replaced, and since it's shared by
+            // other DefaultObjectWrapper instances, we use the lock belonging to the shared ClassIntrospector.
+            classIntrospector = builder.classIntrospectorBuilder.build();
+            sharedIntrospectionLock = classIntrospector.getSharedLock();
+        }
+
+        staticModels = new StaticModels(this);
+        enumModels = new EnumModels(this);
+        useModelCache = builder.getUseModelCache();
+
+        finalizeConstruction();
+    }
+
+    /**
+     * Meant to be called after {@link DefaultObjectWrapper#DefaultObjectWrapper(ExtendableBuilder, boolean)} when
+     * its last argument was {@code false}; makes the instance read-only if necessary, then registers the model
+     * factories in the class introspector. No further changes should be done after calling this, if
+     * {@code writeProtected} was {@code true}.
+     *
+     * @since 2.3.22
+     */
+    protected void finalizeConstruction() {
+        // Attention! At this point, the DefaultObjectWrapper must be fully initialized, as when the model factories are
+        // registered below, the DefaultObjectWrapper can immediately get concurrent callbacks. That those other threads will
+        // see consistent image of the DefaultObjectWrapper is ensured that callbacks are always sync-ed on
+        // classIntrospector.sharedLock, and so is classIntrospector.registerModelFactory(...).
+
+        registerModelFactories();
+    }
+
+    Object getSharedIntrospectionLock() {
+        return sharedIntrospectionLock;
+    }
+
+    /**
+     * @see ExtendableBuilder#setStrict(boolean)
+     */
+    public boolean isStrict() {
+        return strict;
+    }
+
+    /**
+     * By default returns <tt>this</tt>.
+     * @see ExtendableBuilder#setOuterIdentity(ObjectWrapper)
+     */
+    public ObjectWrapper getOuterIdentity() {
+        return outerIdentity;
+    }
+
+    // I have commented this out, as it won't be in 2.3.20 yet.
+    /*
+    /**
+     * Tells which non-backward-compatible overloaded method selection fixes to apply;
+     * see {@link #setOverloadedMethodSelection(Version)}.
+     * /
+    public Version getOverloadedMethodSelection() {
+        return overloadedMethodSelection;
+    }
+
+    /**
+     * Sets which non-backward-compatible overloaded method selection fixes to apply.
+     * This has similar logic as {@link Configuration#setIncompatibleImprovements(Version)},
+     * but only applies to this aspect.
+     *
+     * Currently significant values:
+     * <ul>
+     *   <li>2.3.21: Completetlly rewritten overloaded method selection, fixes several issues with the old one.</li>
+     * </ul>
+     * /
+    public void setOverloadedMethodSelection(Version version) {
+        overloadedMethodSelection = version;
+    }
+    */
+
+    /**
+     * @since 2.3.21
+     */
+    public int getExposureLevel() {
+        return classIntrospector.getExposureLevel();
+    }
+
+    /**
+     * Returns whether exposure of public instance fields of classes is
+     * enabled. See {@link ExtendableBuilder#setExposeFields(boolean)} for details.
+     * @return true if public instance fields are exposed, false otherwise.
+     */
+    public boolean isExposeFields() {
+        return classIntrospector.getExposeFields();
+    }
+
+    public MethodAppearanceFineTuner getMethodAppearanceFineTuner() {
+        return classIntrospector.getMethodAppearanceFineTuner();
+    }
+
+    MethodSorter getMethodSorter() {
+        return classIntrospector.getMethodSorter();
+    }
+
+    /**
+     * Tells if this instance acts like if its class introspection cache is sharable with other {@link DefaultObjectWrapper}-s.
+     * A restricted cache denies certain too "antisocial" operations, like {@link #clearClassIntrospecitonCache()}.
+     * The value depends on how the instance
+     * was created; with a public constructor (then this is {@code false}), or with {@link Builder}
+     * (then it's {@code true}). Note that in the last case it's possible that the introspection cache
+     * will not be actually shared because there's no one to share with, but this will {@code true} even then.
+     *
+     * @since 2.3.21
+     */
+    public boolean isClassIntrospectionCacheRestricted() {
+        return classIntrospector.getHasSharedInstanceRestrictons();
+    }
+
+    private void registerModelFactories() {
+        if (staticModels != null) {
+            classIntrospector.registerModelFactory(staticModels);
+        }
+        if (enumModels != null) {
+            classIntrospector.registerModelFactory(enumModels);
+        }
+    }
+
+    /**
+     * Returns the default date type. See {@link ExtendableBuilder#setDefaultDateType(int)} for
+     * details.
+     * @return the default date type
+     */
+    public int getDefaultDateType() {
+        return defaultDateType;
+    }
+
+    /**
+     * @deprecated Does nothing in FreeMarker 3 - we kept it for now to postopne reworking some JUnit tests.
+     */
+    // [FM3] Remove
+    @Deprecated
+    public boolean getUseModelCache() {
+        return useModelCache;
+    }
+
+    /**
+     * Returns the version given with {@link Builder (Version)}, normalized to the lowest version
+     * where a change has occurred. Thus, this is not necessarily the same version than that was given to the
+     * constructor.
+     *
+     * @since 2.3.21
+     */
+    public Version getIncompatibleImprovements() {
+        return incompatibleImprovements;
+    }
+
+    /**
+     * Wraps the parameter object to {@link TemplateModel} interface(s). Simple types like numbers, strings, booleans
+     * and dates will be wrapped into the corresponding {@code SimpleXxx} classes (like {@link SimpleNumber}).
+     * {@link Map}-s, {@link List}-s, other {@link Collection}-s, arrays and {@link Iterator}-s will be wrapped into the
+     * corresponding {@code DefaultXxxAdapter} classes ({@link DefaultMapAdapter}), depending on). After that, the
+     * wrapping is handled by {@link #handleNonBasicTypes(Object)}, so see more there.
+     */
+    @Override
+    public TemplateModel wrap(Object obj) throws TemplateModelException {
+        if (obj == null) {
+            return null;
+        }
+        if (obj instanceof TemplateModel) {
+            return (TemplateModel) obj;
+        }
+        if (obj instanceof TemplateModelAdapter) {
+            return ((TemplateModelAdapter) obj).getTemplateModel();
+        }
+
+        if (obj instanceof String) {
+            return new SimpleScalar((String) obj);
+        }
+        if (obj instanceof Number) {
+            return new SimpleNumber((Number) obj);
+        }
+        if (obj instanceof Boolean) {
+            return obj.equals(Boolean.TRUE) ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+        if (obj instanceof java.util.Date) {
+            if (obj instanceof java.sql.Date) {
+                return new SimpleDate((java.sql.Date) obj);
+            }
+            if (obj instanceof java.sql.Time) {
+                return new SimpleDate((java.sql.Time) obj);
+            }
+            if (obj instanceof java.sql.Timestamp) {
+                return new SimpleDate((java.sql.Timestamp) obj);
+            }
+            return new SimpleDate((java.util.Date) obj, getDefaultDateType());
+        }
+        final Class<?> objClass = obj.getClass();
+        if (objClass.isArray()) {
+            return DefaultArrayAdapter.adapt(obj, this);
+        }
+        if (obj instanceof Collection) {
+            return obj instanceof List
+                    ? DefaultListAdapter.adapt((List<?>) obj, this)
+                    : DefaultNonListCollectionAdapter.adapt((Collection<?>) obj, this);
+        }
+        if (obj instanceof Map) {
+            return DefaultMapAdapter.adapt((Map<?, ?>) obj, this);
+        }
+        if (obj instanceof Iterator) {
+            return DefaultIteratorAdapter.adapt((Iterator<?>) obj, this);
+        }
+        if (obj instanceof Iterable) {
+            return DefaultIterableAdapter.adapt((Iterable<?>) obj, this);
+        }
+        if (obj instanceof Enumeration) {
+            return DefaultEnumerationAdapter.adapt((Enumeration<?>) obj, this);
+        }
+
+        return handleNonBasicTypes(obj);
+    }
+
+    /**
+     * Called for an object that isn't considered to be of a "basic" Java type, like for all application specific types,
+     * but currently also for {@link Node}-s and {@link ResourceBundle}-s.
+     *
+     * <p>
+     * When you override this method, you should first decide if you want to wrap the object in a custom way (and if so
+     * then do it and return with the result), and if not, then you should call the super method (assuming the default
+     * behavior is fine with you).
+     */
+    // [FM3] This is an awkward temporary solution, rework it.
+    protected TemplateModel handleNonBasicTypes(Object obj) throws TemplateModelException {
+        // [FM3] Via plugin mechanism, not by default anymore
+        if (obj instanceof Node) {
+            return NodeModel.wrap((Node) obj);
+        }
+
+        if (obj instanceof ResourceBundle) {
+            return new ResourceBundleModel((ResourceBundle) obj, this);
+        }
+
+        return new BeanAndStringModel(obj, this);
+    }
+
+    /**
+     * Wraps a Java method so that it can be called from templates, without wrapping its parent ("this") object. The
+     * result is almost the same as that you would get by wrapping the parent object then getting the method from the
+     * resulting {@link TemplateHashModel} by name. Except, if the wrapped method is overloaded, with this method you
+     * explicitly select a an overload, while otherwise you would get a {@link TemplateMethodModelEx} that selects an
+     * overload each time it's called based on the argument values.
+     *
+     * @param object The object whose method will be called, or {@code null} if {@code method} is a static method.
+     *          This object will be used "as is", like without unwrapping it if it's a {@link TemplateModelAdapter}.
+     * @param method The method to call, which must be an (inherited) member of the class of {@code object}, as
+     *          described by {@link Method#invoke(Object, Object...)}
+     *
+     * @since 2.3.22
+     */
+    public TemplateMethodModelEx wrap(Object object, Method method) {
+        return new JavaMethodModel(object, method, method.getParameterTypes(), this);
+    }
+
+    /**
+     * @since 2.3.22
+     */
+    @Override
+    public TemplateHashModel wrapAsAPI(Object obj) throws TemplateModelException {
+        return new APIModel(obj, this);
+    }
+
+    /**
+     * Attempts to unwrap a model into underlying object. Generally, this
+     * method is the inverse of the {@link #wrap(Object)} method. In addition
+     * it will unwrap arbitrary {@link TemplateNumberModel} instances into
+     * a number, arbitrary {@link TemplateDateModel} instances into a date,
+     * {@link TemplateScalarModel} instances into a String, arbitrary
+     * {@link TemplateBooleanModel} instances into a Boolean, arbitrary
+     * {@link TemplateHashModel} instances into a Map, arbitrary
+     * {@link TemplateSequenceModel} into a List, and arbitrary
+     * {@link TemplateCollectionModel} into a Set. All other objects are
+     * returned unchanged.
+     * @throws TemplateModelException if an attempted unwrapping fails.
+     */
+    @Override
+    public Object unwrap(TemplateModel model) throws TemplateModelException {
+        return unwrap(model, Object.class);
+    }
+
+    /**
+     * Attempts to unwrap a model into an object of the desired class.
+     * Generally, this method is the inverse of the {@link #wrap(Object)}
+     * method. It recognizes a wide range of target classes - all Java built-in
+     * primitives, primitive wrappers, numbers, dates, sets, lists, maps, and
+     * native arrays.
+     * @param model the model to unwrap
+     * @param targetClass the class of the unwrapped result; {@code Object.class} of we don't know what the expected type is.
+     * @return the unwrapped result of the desired class
+     * @throws TemplateModelException if an attempted unwrapping fails.
+     *
+     * @see #tryUnwrapTo(TemplateModel, Class)
+     */
+    public Object unwrap(TemplateModel model, Class<?> targetClass)
+            throws TemplateModelException {
+        final Object obj = tryUnwrapTo(model, targetClass);
+        if (obj == ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS) {
+            throw new TemplateModelException("Can not unwrap model of type " +
+                    model.getClass().getName() + " to type " + targetClass.getName());
+        }
+        return obj;
+    }
+
+    /**
+     * @since 2.3.22
+     */
+    @Override
+    public Object tryUnwrapTo(TemplateModel model, Class<?> targetClass) throws TemplateModelException {
+        return tryUnwrapTo(model, targetClass, 0);
+    }
+
+    /**
+     * @param typeFlags
+     *            Used when unwrapping for overloaded methods and so the {@code targetClass} is possibly too generic
+     *            (as it's the most specific common superclass). Must be 0 when unwrapping parameter values for
+     *            non-overloaded methods.
+     * @return {@link ObjectWrapperAndUnwrapper#CANT_UNWRAP_TO_TARGET_CLASS} or the unwrapped object.
+     */
+    Object tryUnwrapTo(TemplateModel model, Class<?> targetClass, int typeFlags) throws TemplateModelException {
+        Object res = tryUnwrapTo(model, targetClass, typeFlags, null);
+        if ((typeFlags & TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT) != 0
+                && res instanceof Number) {
+            return OverloadedNumberUtil.addFallbackType((Number) res, typeFlags);
+        } else {
+            return res;
+        }
+    }
+
+    /**
+     * See {@link #tryUnwrapTo(TemplateModel, Class, int)}.
+     */
+    private Object tryUnwrapTo(final TemplateModel model, Class<?> targetClass, final int typeFlags,
+                               final Map<Object, Object> recursionStops)
+            throws TemplateModelException {
+        if (model == null) {
+            return null;
+        }
+
+        if (targetClass.isPrimitive()) {
+            targetClass = _ClassUtil.primitiveClassToBoxingClass(targetClass);
+        }
+
+        // This is for transparent interop with other wrappers (and ourselves)
+        // Passing the targetClass allows e.g. a Jython-aware method that declares a
+        // PyObject as its argument to receive a PyObject from a Jython-aware TemplateModel
+        // passed as an argument to TemplateMethodModelEx etc.
+        if (model instanceof AdapterTemplateModel) {
+            Object wrapped = ((AdapterTemplateModel) model).getAdaptedObject(targetClass);
+            if (targetClass == Object.class || targetClass.isInstance(wrapped)) {
+                return wrapped;
+            }
+
+            // Attempt numeric conversion:
+            if (targetClass != Object.class && (wrapped instanceof Number && _ClassUtil.isNumerical(targetClass))) {
+                Number number = forceUnwrappedNumberToType((Number) wrapped, targetClass);
+                if (number != null) return number;
+            }
+        }
+
+        if (model instanceof WrapperTemplateModel) {
+            Object wrapped = ((WrapperTemplateModel) model).getWrappedObject();
+            if (targetClass == Object.class || targetClass.isInstance(wrapped)) {
+                return wrapped;
+            }
+
+            // Attempt numeric conversion:
+            if (targetClass != Object.class && (wrapped instanceof Number && _ClassUtil.isNumerical(targetClass))) {
+                Number number = forceUnwrappedNumberToType((Number) wrapped, targetClass);
+                if (number != null) {
+                    return number;
+                }
+            }
+        }
+
+        // Translation of generic template models to POJOs. First give priority
+        // to various model interfaces based on the targetClass. This helps us
+        // select the appropriate interface in multi-interface models when we
+        // know what is expected as the return type.
+        if (targetClass != Object.class) {
+
+            // [2.4][IcI]: Should also check for CharSequence at the end
+            if (String.class == targetClass) {
+                if (model instanceof TemplateScalarModel) {
+                    return ((TemplateScalarModel) model).getAsString();
+                }
+                // String is final, so no other conversion will work
+                return ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS;
+            }
+
+            // Primitive numeric types & Number.class and its subclasses
+            if (_ClassUtil.isNumerical(targetClass)) {
+                if (model instanceof TemplateNumberModel) {
+                    Number number = forceUnwrappedNumberToType(
+                            ((TemplateNumberModel) model).getAsNumber(), targetClass);
+                    if (number != null) {
+                        return number;
+                    }
+                }
+            }
+
+            if (boolean.class == targetClass || Boolean.class == targetClass) {
+                if (model instanceof TemplateBooleanModel) {
+                    return Boolean.valueOf(((TemplateBooleanModel) model).getAsBoolean());
+                }
+                // Boolean is final, no other conversion will work
+                return ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS;
+            }
+
+            if (Map.class == targetClass) {
+                if (model instanceof TemplateHashModel) {
+                    return new HashAdapter((TemplateHashModel) model, this);
+                }
+            }
+
+            if (List.class == targetClass) {
+                if (model instanceof TemplateSequenceModel) {
+                    return new SequenceAdapter((TemplateSequenceModel) model, this);
+                }
+            }
+
+            if (Set.class == targetClass) {
+                if (model instanceof TemplateCollectionModel) {
+                    return new SetAdapter((TemplateCollectionModel) model, this);
+                }
+            }
+
+            if (Collection.class == targetClass || Iterable.class == targetClass) {
+                if (model instanceof TemplateCollectionModel) {
+                    return new CollectionAdapter((TemplateCollectionModel) model,
+                            this);
+                }
+                if (model instanceof TemplateSequenceModel) {
+                    return new SequenceAdapter((TemplateSequenceModel) model, this);
+                }
+            }
+
+            // TemplateSequenceModels can be converted to arrays
+            if (targetClass.isArray()) {
+                if (model instanceof TemplateSequenceModel) {
+                    return unwrapSequenceToArray((TemplateSequenceModel) model, targetClass, true, recursionStops);
+                }
+                // array classes are final, no other conversion will work
+                return ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS;
+            }
+
+            // Allow one-char strings to be coerced to characters
+            if (char.class == targetClass || targetClass == Character.class) {
+                if (model instanceof TemplateScalarModel) {
+                    String s = ((TemplateScalarModel) model).getAsString();
+                    if (s.length() == 1) {
+                        return Character.valueOf(s.charAt(0));
+                    }
+                }
+                // Character is final, no other conversion will work
+                return ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS;
+            }
+
+            if (Date.class.isAssignableFrom(targetClass) && model instanceof TemplateDateModel) {
+                Date date = ((TemplateDateModel) model).getAsDate();
+                if (targetClass.isInstance(date)) {
+                    return date;
+                }
+            }
+        }  //  End: if (targetClass != Object.class)
+
+        // Since the targetClass was of no help initially, now we use
+        // a quite arbitrary order in which we walk through the TemplateModel subinterfaces, and unwrapp them to
+        // their "natural" Java correspondent. We still try exclude unwrappings that won't fit the target parameter
+        // type(s). This is mostly important because of multi-typed FTL values that could be unwrapped on multiple ways.
+        int itf = typeFlags; // Iteration's Type Flags. Should be always 0 for non-overloaded and when !is2321Bugfixed.
+        // If itf != 0, we possibly execute the following loop body at twice: once with utilizing itf, and if it has not
+        // returned, once more with itf == 0. Otherwise we execute this once with itf == 0.
+        do {
+            if ((itf == 0 || (itf & TypeFlags.ACCEPTS_NUMBER) != 0)
+                    && model instanceof TemplateNumberModel) {
+                Number number = ((TemplateNumberModel) model).getAsNumber();
+                if (itf != 0 || targetClass.isInstance(number)) {
+                    return number;
+                }
+            }
+            if ((itf == 0 || (itf & TypeFlags.ACCEPTS_DATE) != 0)
+                    && model instanceof TemplateDateModel) {
+                Date date = ((TemplateDateModel) model).getAsDate();
+                if (itf != 0 || targetClass.isInstance(date)) {
+                    return date;
+                }
+            }
+            if ((itf == 0 || (itf & (TypeFlags.ACCEPTS_STRING | TypeFlags.CHARACTER)) != 0)
+                    && model instanceof TemplateScalarModel
+                    && (itf != 0 || targetClass.isAssignableFrom(String.class))) {
+                String strVal = ((TemplateScalarModel) model).getAsString();
+                if (itf == 0 || (itf & TypeFlags.CHARACTER) == 0) {
+                    return strVal;
+                } else { // TypeFlags.CHAR == 1
+                    if (strVal.length() == 1) {
+                        if ((itf & TypeFlags.ACCEPTS_STRING) != 0) {
+                            return new CharacterOrString(strVal);
+                        } else {
+                            return Character.valueOf(strVal.charAt(0));
+                        }
+                    } else if ((itf & TypeFlags.ACCEPTS_STRING) != 0) {
+                        return strVal;
+                    }
+                    // It had to be unwrapped to Character, but the string length wasn't 1 => Fall through
+                }
+            }
+            // Should be earlier than TemplateScalarModel, but we keep it here until FM 2.4 or such
+            if ((itf == 0 || (itf & TypeFlags.ACCEPTS_BOOLEAN) != 0)
+                    && model instanceof TemplateBooleanModel
+                    && (itf != 0 || targetClass.isAssignableFrom(Boolean.class))) {
+                return Boolean.valueOf(((TemplateBooleanModel) model).getAsBoolean());
+            }
+            if ((itf == 0 || (itf & TypeFlags.ACCEPTS_MAP) != 0)
+                    && model instanceof TemplateHashModel
+                    && (itf != 0 || targetClass.isAssignableFrom(HashAdapter.class))) {
+                return new HashAdapter((TemplateHashModel) model, this);
+            }
+            if ((itf == 0 || (itf & TypeFlags.ACCEPTS_LIST) != 0)
+                    && model instanceof TemplateSequenceModel
+                    && (itf != 0 || targetClass.isAssignableFrom(SequenceAdapter.class))) {
+                return new SequenceAdapter((TemplateSequenceModel) model, this);
+            }
+            if ((itf == 0 || (itf & TypeFlags.ACCEPTS_SET) != 0)
+                    && model instanceof TemplateCollectionModel
+                    && (itf != 0 || targetClass.isAssignableFrom(SetAdapter.class))) {
+                return new SetAdapter((TemplateCollectionModel) model, this);
+            }
+
+            if ((itf & TypeFlags.ACCEPTS_ARRAY) != 0
+                    && model instanceof TemplateSequenceModel) {
+                return new SequenceAdapter((TemplateSequenceModel) model, this);
+            }
+
+            if (itf == 0) {
+                break;
+            }
+            itf = 0; // start 2nd iteration
+        } while (true);
+
+        // Last ditch effort - is maybe the model itself is an instance of the required type?
+        // Note that this will be always true for Object.class targetClass.
+        if (targetClass.isInstance(model)) {
+            return model;
+        }
+
+        return ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS;
+    }
+
+    /**
+     * @param tryOnly
+     *            If {@code true}, if the conversion of an item to the component type isn't possible, the method returns
+     *            {@link ObjectWrapperAndUnwrapper#CANT_UNWRAP_TO_TARGET_CLASS} instead of throwing a
+     *            {@link TemplateModelException}.
+     */
+    Object unwrapSequenceToArray(
+            TemplateSequenceModel seq, Class<?> arrayClass, boolean tryOnly, Map<Object, Object> recursionStops)
+            throws TemplateModelException {
+        if (recursionStops != null) {
+            Object retval = recursionStops.get(seq);
+            if (retval != null) {
+                return retval;
+            }
+        } else {
+            recursionStops = new IdentityHashMap<>();
+        }
+        Class<?> componentType = arrayClass.getComponentType();
+        Object array = Array.newInstance(componentType, seq.size());
+        recursionStops.put(seq, array);
+        try {
+            final int size = seq.size();
+            for (int i = 0; i < size; i++) {
+                final TemplateModel seqItem = seq.get(i);
+                Object val = tryUnwrapTo(seqItem, componentType, 0, recursionStops);
+                if (val == ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS) {
+                    if (tryOnly) {
+                        return ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS;
+                    } else {
+                        throw new _TemplateModelException(
+                                "Failed to convert ",  new _DelayedFTLTypeDescription(seq),
+                                " object to ", new _DelayedShortClassName(array.getClass()),
+                                ": Problematic sequence item at index ", Integer.valueOf(i) ," with value type: ",
+                                new _DelayedFTLTypeDescription(seqItem));
+                    }
+
+                }
+                Array.set(array, i, val);
+            }
+        } finally {
+            recursionStops.remove(seq);
+        }
+        return array;
+    }
+
+    Object listToArray(List<?> list, Class<?> arrayClass, Map<Object, Object> recursionStops)
+            throws TemplateModelException {
+        if (list instanceof SequenceAdapter) {
+            return unwrapSequenceToArray(
+                    ((SequenceAdapter) list).getTemplateSequenceModel(),
+                    arrayClass, false,
+                    recursionStops);
+        }
+
+        if (recursionStops != null) {
+            Object retval = recursionStops.get(list);
+            if (retval != null) {
+                return retval;
+            }
+        } else {
+            recursionStops = new IdentityHashMap<>();
+        }
+        Class<?> componentType = arrayClass.getComponentType();
+        Object array = Array.newInstance(componentType, list.size());
+        recursionStops.put(list, array);
+        try {
+            boolean isComponentTypeExamined = false;
+            boolean isComponentTypeNumerical = false;  // will be filled on demand
+            boolean isComponentTypeList = false;  // will be filled on demand
+            int i = 0;
+            for (Object listItem : list) {
+                if (listItem != null && !componentType.isInstance(listItem)) {
+                    // Type conversion is needed. If we can't do it, we just let it fail at Array.set later.
+                    if (!isComponentTypeExamined) {
+                        isComponentTypeNumerical = _ClassUtil.isNumerical(componentType);
+                        isComponentTypeList = List.class.isAssignableFrom(componentType);
+                        isComponentTypeExamined = true;
+                    }
+                    if (isComponentTypeNumerical && listItem instanceof Number) {
+                        listItem = forceUnwrappedNumberToType((Number) listItem, componentType);
+                    } else if (componentType == String.class && listItem instanceof Character) {
+                        listItem = String.valueOf(((Character) listItem).charValue());
+                    } else if ((componentType == Character.class || componentType == char.class)
+                            && listItem instanceof String) {
+                        String listItemStr = (String) listItem;
+                        if (listItemStr.length() == 1) {
+                            listItem = Character.valueOf(listItemStr.charAt(0));
+                        }
+                    } else if (componentType.isArray()) {
+                        if (listItem instanceof List) {
+                            listItem = listToArray((List<?>) listItem, componentType, recursionStops);
+                        } else if (listItem instanceof TemplateSequenceModel) {
+                            listItem = unwrapSequenceToArray((TemplateSequenceModel) listItem, componentType, false, recursionStops);
+                        }
+                    } else if (isComponentTypeList && listItem.getClass().isArray()) {
+                        listItem = arrayToList(listItem);
+                    }
+                }
+                try {
+                    Array.set(array, i, listItem);
+                } catch (IllegalArgumentException e) {
+                    throw new TemplateModelException(
+                            "Failed to convert " + _ClassUtil.getShortClassNameOfObject(list)
+                                    + " object to " + _ClassUtil.getShortClassNameOfObject(array)
+                                    + ": Problematic List item at index " + i + " with value type: "
+                                    + _ClassUtil.getShortClassNameOfObject(listItem), e);
+                }
+                i++;
+            }
+        } finally {
+            recursionStops.remove(list);
+        }
+        return array;
+    }
+
+    /**
+     * @param array Must be an array (of either a reference or primitive type)
+     */
+    List<?> arrayToList(Object array) throws TemplateModelException {
+        if (array instanceof Object[]) {
+            // Array of any non-primitive type.
+            // Note that an array of non-primitive type is always instanceof Object[].
+            Object[] objArray = (Object[]) array;
+            return objArray.length == 0 ? Collections.EMPTY_LIST : new NonPrimitiveArrayBackedReadOnlyList(objArray);
+        } else {
+            // Array of any primitive type
+            return Array.getLength(array) == 0 ? Collections.EMPTY_LIST : new PrimtiveArrayBackedReadOnlyList(array);
+        }
+    }
+
+    /**
+     * Converts a number to the target type aggressively (possibly with overflow or significant loss of precision).
+     * @param n Non-{@code null}
+     * @return {@code null} if the conversion has failed.
+     */
+    static Number forceUnwrappedNumberToType(final Number n, final Class<?> targetType) {
+        // We try to order the conditions by decreasing probability.
+        if (targetType == n.getClass()) {
+            return n;
+        } else if (targetType == int.class || targetType == Integer.class) {
+            return n instanceof Integer ? (Integer) n : Integer.valueOf(n.intValue());
+        } else if (targetType == long.class || targetType == Long.class) {
+            return n instanceof Long ? (Long) n : Long.valueOf(n.longValue());
+        } else if (targetType == double.class || targetType == Double.class) {
+            return n instanceof Double ? (Double) n : Double.valueOf(n.doubleValue());
+        } else if (targetType == BigDecimal.class) {
+            if (n instanceof BigDecimal) {
+                return n;
+            } else if (n instanceof BigInteger) {
+                return new BigDecimal((BigInteger) n);
+            } else if (n instanceof Long) {
+                // Because we can't represent long accurately as double
+                return BigDecimal.valueOf(n.longValue());
+            } else {
+                return new BigDecimal(n.doubleValue());
+            }
+        } else if (targetType == float.class || targetType == Float.class) {
+            return n instanceof Float ? (Float) n : Float.valueOf(n.floatValue());
+        } else if (targetType == byte.class || targetType == Byte.class) {
+            return n instanceof Byte ? (Byte) n : Byte.valueOf(n.byteValue());
+        } else if (targetType == short.class || targetType == Short.class) {
+            return n instanceof Short ? (Short) n : Short.valueOf(n.shortValue());
+        } else if (targetType == BigInteger.class) {
+            if (n instanceof BigInteger) {
+                return n;
+            } else {
+                if (n instanceof OverloadedNumberUtil.IntegerBigDecimal) {
+                    return ((OverloadedNumberUtil.IntegerBigDecimal) n).bigIntegerValue();
+                } else if (n instanceof BigDecimal) {
+                    return ((BigDecimal) n).toBigInteger();
+                } else {
+                    return BigInteger.valueOf(n.longValue());
+                }
+            }
+        } else {
+            final Number oriN = n instanceof OverloadedNumberUtil.NumberWithFallbackType
+                    ? ((OverloadedNumberUtil.NumberWithFallbackType) n).getSourceNumber() : n;
+            if (targetType.isInstance(oriN)) {
+                // Handle nonstandard Number subclasses as well as directly java.lang.Number.
+                return oriN;
+            } else {
+                // Fails
+                return null;
+            }
+        }
+    }
+
+    /**
+     * Invokes the specified method, wrapping the return value. The specialty
+     * of this method is that if the return value is null, and the return type
+     * of the invoked method is void, {@link TemplateModel#NOTHING} is returned.
+     * @param object the object to invoke the method on
+     * @param method the method to invoke
+     * @param args the arguments to the method
+     * @return the wrapped return value of the method.
+     * @throws InvocationTargetException if the invoked method threw an exception
+     * @throws IllegalAccessException if the method can't be invoked due to an
+     * access restriction.
+     * @throws TemplateModelException if the return value couldn't be wrapped
+     * (this can happen if the wrapper has an outer identity or is subclassed,
+     * and the outer identity or the subclass throws an exception. Plain
+     * DefaultObjectWrapper never throws TemplateModelException).
+     */
+    TemplateModel invokeMethod(Object object, Method method, Object[] args)
+            throws InvocationTargetException,
+            IllegalAccessException,
+            TemplateModelException {
+        // [2.4]: Java's Method.invoke truncates numbers if the target type has not enough bits to hold the value.
+        // There should at least be an option to check this.
+        Object retval = method.invoke(object, args);
+        return
+                method.getReturnType() == void.class
+                        ? TemplateModel.NOTHING
+                        : getOuterIdentity().wrap(retval);
+    }
+
+    /**
+     * Returns a hash model that represents the so-called class static models.
+     * Every class static model is itself a hash through which you can call
+     * static methods on the specified class. To obtain a static model for a
+     * class, get the element of this hash with the fully qualified class name.
+     * For example, if you place this hash model inside the root data model
+     * under name "statics", you can use i.e. <code>statics["java.lang.
+     * System"]. currentTimeMillis()</code> to call the {@link
+     * java.lang.System#currentTimeMillis()} method.
+     * @return a hash model whose keys are fully qualified class names, and
+     * that returns hash models whose elements are the static models of the
+     * classes.
+     */
+    public TemplateHashModel getStaticModels() {
+        return staticModels;
+    }
+
+    /**
+     * Returns a hash model that represents the so-called class enum models.
+     * Every class' enum model is itself a hash through which you can access
+     * enum value declared by the specified class, assuming that class is an
+     * enumeration. To obtain an enum model for a class, get the element of this
+     * hash with the fully qualified class name. For example, if you place this
+     * hash model inside the root data model under name "enums", you can use
+     * i.e. <code>statics["java.math.RoundingMode"].UP</code> to access the
+     * {@link java.math.RoundingMode#UP} value.
+     * @return a hash model whose keys are fully qualified class names, and
+     * that returns hash models whose elements are the enum models of the
+     * classes.
+     */
+    public TemplateHashModel getEnumModels() {
+        return enumModels;
+    }
+
+    /**
+     * Creates a new instance of the specified class using the method call logic of this object wrapper for calling the
+     * constructor. Overloaded constructors and varargs are supported. Only public constructors will be called.
+     *
+     * @param clazz The class whose constructor we will call.
+     * @param arguments The list of {@link TemplateModel}-s to pass to the constructor after unwrapping them
+     * @return The instance created; it's not wrapped into {@link TemplateModel}.
+     */
+    public Object newInstance(Class<?> clazz, List/*<? extends TemplateModel>*/ arguments)
+            throws TemplateModelException {
+        try {
+            Object ctors = classIntrospector.get(clazz).get(ClassIntrospector.CONSTRUCTORS_KEY);
+            if (ctors == null) {
+                throw new TemplateModelException("Class " + clazz.getName() +
+                        " has no public constructors.");
+            }
+            Constructor<?> ctor = null;
+            Object[] objargs;
+            if (ctors instanceof SimpleMethod) {
+                SimpleMethod sm = (SimpleMethod) ctors;
+                ctor = (Constructor<?>) sm.getMember();
+                objargs = sm.unwrapArguments(arguments, this);
+                try {
+                    return ctor.newInstance(objargs);
+                } catch (Exception e) {
+                    if (e instanceof TemplateModelException) throw (TemplateModelException) e;
+                    throw _MethodUtil.newInvocationTemplateModelException(null, ctor, e);
+                }
+            } else if (ctors instanceof OverloadedMethods) {
+                final MemberAndArguments mma = ((OverloadedMethods) ctors).getMemberAndArguments(arguments, this);
+                try {
+                    return mma.invokeConstructor(this);
+                } catch (Exception e) {
+                    if (e instanceof TemplateModelException) throw (TemplateModelException) e;
+
+                    throw _MethodUtil.newInvocationTemplateModelException(null, mma.getCallableMemberDescriptor(), e);
+                }
+            } else {
+                // Cannot happen
+                throw new BugException();
+            }
+        } catch (TemplateModelException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new TemplateModelException(
+                    "Error while creating new instance of class " + clazz.getName() + "; see cause exception", e);
+        }
+    }
+
+    /**
+     * Removes the introspection data for a class from the cache.
+     * Use this if you know that a class is not used anymore in templates.
+     * If the class will be still used, the cache entry will be silently
+     * re-created, so this isn't a dangerous operation.
+     *
+     * @since 2.3.20
+     */
+    public void removeFromClassIntrospectionCache(Class<?> clazz) {
+        classIntrospector.remove(clazz);
+    }
+
+    /**
+     * Removes all class introspection data from the cache.
+     *
+     * <p>Use this if you want to free up memory on the expense of recreating
+     * the cache entries for the classes that will be used later in templates.
+     *
+     * @throws IllegalStateException if {@link #isClassIntrospectionCacheRestricted()} is {@code true}.
+     *
+     * @since 2.3.20
+     */
+    public void clearClassIntrospecitonCache() {
+        classIntrospector.clearCache();
+    }
+
+    ClassIntrospector getClassIntrospector() {
+        return classIntrospector;
+    }
+
+    /**
+     * Converts any {@link BigDecimal}s in the passed array to the type of
+     * the corresponding formal argument of the method.
+     */
+    // Unused?
+    public static void coerceBigDecimals(AccessibleObject callable, Object[] args) {
+        Class<?>[] formalTypes = null;
+        for (int i = 0; i < args.length; ++i) {
+            Object arg = args[i];
+            if (arg instanceof BigDecimal) {
+                if (formalTypes == null) {
+                    if (callable instanceof Method) {
+                        formalTypes = ((Method) callable).getParameterTypes();
+                    } else if (callable instanceof Constructor) {
+                        formalTypes = ((Constructor<?>) callable).getParameterTypes();
+                    } else {
+                        throw new IllegalArgumentException("Expected method or "
+                                + " constructor; callable is " +
+                                callable.getClass().getName());
+                    }
+                }
+                args[i] = coerceBigDecimal((BigDecimal) arg, formalTypes[i]);
+            }
+        }
+    }
+
+    /**
+     * Converts any {@link BigDecimal}-s in the passed array to the type of
+     * the corresponding formal argument of the method via {@link #coerceBigDecimal(BigDecimal, Class)}.
+     */
+    public static void coerceBigDecimals(Class<?>[] formalTypes, Object[] args) {
+        int typeLen = formalTypes.length;
+        int argsLen = args.length;
+        int min = Math.min(typeLen, argsLen);
+        for (int i = 0; i < min; ++i) {
+            Object arg = args[i];
+            if (arg instanceof BigDecimal) {
+                args[i] = coerceBigDecimal((BigDecimal) arg, formalTypes[i]);
+            }
+        }
+        if (argsLen > typeLen) {
+            Class<?> varArgType = formalTypes[typeLen - 1];
+            for (int i = typeLen; i < argsLen; ++i) {
+                Object arg = args[i];
+                if (arg instanceof BigDecimal) {
+                    args[i] = coerceBigDecimal((BigDecimal) arg, varArgType);
+                }
+            }
+        }
+    }
+
+    /**
+     * Converts {@link BigDecimal} to the class given in the {@code formalType} argument if that's a known numerical
+     * type, returns the {@link BigDecimal} as is otherwise. Overflow and precision loss are possible, similarly as
+     * with casting in Java.
+     */
+    public static Object coerceBigDecimal(BigDecimal bd, Class<?> formalType) {
+        // int is expected in most situations, so we check it first
+        if (formalType == int.class || formalType == Integer.class) {
+            return Integer.valueOf(bd.intValue());
+        } else if (formalType == double.class || formalType == Double.class) {
+            return Double.valueOf(bd.doubleValue());
+        } else if (formalType == long.class || formalType == Long.class) {
+            return Long.valueOf(bd.longValue());
+        } else if (formalType == float.class || formalType == Float.class) {
+            return Float.valueOf(bd.floatValue());
+        } else if (formalType == short.class || formalType == Short.class) {
+            return Short.valueOf(bd.shortValue());
+        } else if (formalType == byte.class || formalType == Byte.class) {
+            return Byte.valueOf(bd.byteValue());
+        } else if (java.math.BigInteger.class.isAssignableFrom(formalType)) {
+            return bd.toBigInteger();
+        } else {
+            return bd;
+        }
+    }
+
+    /**
+     * Returns the lowest version number that is equivalent with the parameter version.
+     *
+     * @since 2.3.22
+     */
+    protected static Version normalizeIncompatibleImprovementsVersion(Version incompatibleImprovements) {
+        _CoreAPI.checkVersionNotNullAndSupported(incompatibleImprovements);
+        return Configuration.VERSION_3_0_0;
+    }
+
+
+    /**
+     * Returns the name-value pairs that describe the configuration of this {@link DefaultObjectWrapper}; called from
+     * {@link #toString()}. The expected format is like {@code "foo=bar, baaz=wombat"}. When overriding this, you should
+     * call the super method, and then insert the content before it with a following {@code ", "}, or after it with a
+     * preceding {@code ", "}.
+     */
+    protected String toPropertiesString() {
+        // Start with "simpleMapWrapper", because the override in DefaultObjectWrapper expects it to be there!
+        return "exposureLevel=" + classIntrospector.getExposureLevel() + ", "
+                + "exposeFields=" + classIntrospector.getExposeFields() + ", "
+                + "sharedClassIntrospCache="
+                + (classIntrospector.isShared() ? "@" + System.identityHashCode(classIntrospector) : "none");
+    }
+
+    /**
+     * Returns the exact class name and the identity hash, also the values of the most often used
+     * {@link DefaultObjectWrapper} configuration properties, also if which (if any) shared class introspection
+     * cache it uses.
+     */
+    @Override
+    public String toString() {
+        final String propsStr = toPropertiesString();
+        return _ClassUtil.getShortClassNameOfObject(this) + "@" + System.identityHashCode(this)
+                + "(" + incompatibleImprovements + ", "
+                + (propsStr.length() != 0 ? propsStr + ", ..." : "")
+                + ")";
+    }
+
+    /**
+     * Gets/creates a {@link DefaultObjectWrapper} singleton instance that's already configured as specified in the
+     * properties of this object; this is recommended over using the {@link DefaultObjectWrapper} constructors. The
+     * returned instance can't be further configured (it's write protected).
+     *
+     * <p>The builder meant to be used as a drop-away object (not stored in a field), like in this example:
+     * <pre>
+     *    DefaultObjectWrapper dow = new Builder(Configuration.VERSION_3_0_0).build();
+     * </pre>
+     *
+     * <p>Or, a more complex example:</p>
+     * <pre>
+     *    // Create the builder:
+     *    DefaultObjectWrapper dow = new Builder(Configuration.VERSION_3_0_0)
+     *            .exposeFields(true)
+     *            .build();
+     * </pre>
+     *
+     * <p>Despite that builders aren't meant to be used as long-lived objects (singletons), the builder is thread-safe after
+     * you have stopped calling its setters and it was safely published (see JSR 133) to other threads. This can be useful
+     * if you have to put the builder into an IoC container, rather than the singleton it produces.
+     *
+     * <p>The main benefit of using a builder instead of a {@link DefaultObjectWrapper} constructor is that this way the
+     * internal object wrapping-related caches (most notably the class introspection cache) will come from a global,
+     * JVM-level (more precisely, {@code freemarker-core.jar}-class-loader-level) cache. Also the
+     * {@link DefaultObjectWrapper} singletons
+     * themselves are stored in this global cache. Some of the wrapping-related caches are expensive to build and can take
+     * significant amount of memory. Using builders, components that use FreeMarker will share {@link DefaultObjectWrapper}
+     * instances and said caches even if they use separate FreeMarker {@link Configuration}-s. (Many Java libraries use
+     * FreeMarker internally, so {@link Configuration} sharing is not an option.)
+     *
+     * <p>Note that the returned {@link DefaultObjectWrapper} instances are only weak-referenced from inside the builder mechanism,
+     * so singletons are garbage collected when they go out of usage, just like non-singletons.
+     *
+     * <p>About the object wrapping-related caches:
+     * <ul>
+     *   <li><p>Class introspection cache: Stores information about classes that once had to be wrapped. The cache is
+     *     stored in the static fields of certain FreeMarker classes. Thus, if you have two {@link DefaultObjectWrapper}
+     *     instances, they might share the same class introspection cache. But if you have two
+     *     {@code freemarker.jar}-s (typically, in two Web Application's {@code WEB-INF/lib} directories), those won't
+     *     share their caches (as they don't share the same FreeMarker classes).
+     *     Also, currently there's a separate cache for each permutation of the property values that influence class
+     *     introspection: {@link Builder#setExposeFields(boolean) expose_fields} and
+     *     {@link Builder#setExposureLevel(int) exposure_level}. So only {@link DefaultObjectWrapper} where those
+     *     properties are the same may share class introspection caches among each other.
+     *   </li>
+     *   <li><p>Model caches: These are local to a {@link DefaultObjectWrapper}. {@link Builder} returns the same
+     *     {@link DefaultObjectWrapper} instance for equivalent properties (unless the existing instance was garbage collected
+     *     and thus a new one had to be created), hence these caches will be re-used too. {@link DefaultObjectWrapper} instances
+     *     are cached in the static fields of FreeMarker too, but there's a separate cache for each
+     *     Thread Context Class Loader, which in a servlet container practically means a separate cache for each Web
+     *     Application (each servlet context). (This is like so because for resolving class names to classes FreeMarker
+     *     uses the Thread Context Class Loader, so the result of the resolution can be different for different
+     *     Thread Context Class Loaders.) The model caches are:
+     *     <ul>
+     *       <li><p>
+     *         Static model caches: These are used by the hash returned by {@link DefaultObjectWrapper#getEnumModels()} and
+     *         {@link DefaultObjectWrapper#getStaticModels()}, for caching {@link TemplateModel}-s for the static methods/fields
+     *         and Java enums that were accessed through them. To use said hashes, you have to put them
+     *         explicitly into the data-model or expose them to the template explicitly otherwise, so in most applications
+     *         these caches aren't unused.
+     *       </li>
+     *       <li><p>
+     *         Instance model cache: By default off (see {@link ExtendableBuilder#setUseModelCache(boolean)}). Caches the
+     *         {@link TemplateModel}-s for all Java objects that were accessed from templates.
+     *       </li>
+     *     </ul>
+     *   </li>
+     * </ul>
+     *
+     * <p>Note that what this method documentation says about {@link DefaultObjectWrapper} also applies to
+     * {@link Builder}.
+     */
+    public static final class Builder extends ExtendableBuilder<DefaultObjectWrapper, Builder> {
+
+        private final static Map<ClassLoader, Map<Builder, WeakReference<DefaultObjectWrapper>>>
+                INSTANCE_CACHE = new WeakHashMap<>();
+        private final static ReferenceQueue<DefaultObjectWrapper> INSTANCE_CACHE_REF_QUEUE = new ReferenceQueue<>();
+
+        /**
+         * See {@link ExtendableBuilder#ExtendableBuilder(Version, boolean)}
+         */
+        public Builder(Version incompatibleImprovements) {
+            super(incompatibleImprovements, false);
+        }
+
+        /** For unit testing only */
+        static void clearInstanceCache() {
+            synchronized (INSTANCE_CACHE) {
+                INSTANCE_CACHE.clear();
+            }
+        }
+
+        /**
+         * Returns a {@link DefaultObjectWrapper} instance that matches the settings of this builder. This will be possibly
+         * a singleton that is also in use elsewhere.
+         */
+        @Override
+        public DefaultObjectWrapper build() {
+            return DefaultObjectWrapperTCCLSingletonUtil.getSingleton(
+                    this, INSTANCE_CACHE, INSTANCE_CACHE_REF_QUEUE, ConstructorInvoker.INSTANCE);
+        }
+
+        /**
+         * Calls {@link ExtendableBuilder#hashCodeForCacheKey(ExtendableBuilder)}.
+         */
+        @Override
+        public int hashCode() {
+            return hashCodeForCacheKey(this);
+        }
+
+        /**
+         * Calls {@link ExtendableBuilder#equalsForCacheKey(ExtendableBuilder, Object)}.
+         */
+        @Override
+        public boolean equals(Object obj) {
+            return equalsForCacheKey(this, obj);
+        }
+
+        /**
+         * For unit testing only
+         */
+        static Map<ClassLoader, Map<Builder, WeakReference<DefaultObjectWrapper>>> getInstanceCache() {
+            return INSTANCE_CACHE;
+        }
+
+        private static class ConstructorInvoker
+            implements DefaultObjectWrapperTCCLSingletonUtil._ConstructorInvoker<DefaultObjectWrapper, Builder> {
+
+            private static final ConstructorInvoker INSTANCE = new ConstructorInvoker();
+
+            @Override
+            public DefaultObjectWrapper invoke(Builder builder) {
+                return new DefaultObjectWrapper(builder, true);
+            }
+        }
+
+    }
+
+    /**
+     * You will not use this abstract class directly, but concrete subclasses like {@link Builder}, unless you are
+     * developing a builder for a custom {@link DefaultObjectWrapper} subclass. In that case, note that overriding the
+     * {@link #equals} and {@link #hashCode} is important, as these objects are used as {@link ObjectWrapper} singleton
+     * lookup keys.
+     */
+    protected abstract static class ExtendableBuilder<
+            ProductT extends DefaultObjectWrapper, SelfT extends ExtendableBuilder<ProductT, SelfT>>
+            implements CommonBuilder<ProductT>, Cloneable {
+
+        private final Version incompatibleImprovements;
+
+        // Can't be final because deep cloning must replace it
+        private ClassIntrospector.Builder classIntrospectorBuilder;
+
+        // Properties and their *defaults*:
+        private int defaultDateType = TemplateDateModel.UNKNOWN;
+        private boolean defaultDataTypeSet;
+        private ObjectWrapper outerIdentity;
+        private boolean outerIdentitySet;
+        private boolean strict;
+        private boolean strictSet;
+        private boolean useModelCache;
+        private boolean useModelCacheSet;
+        private boolean usePrivateCaches;
+        private boolean usePrivateCachesSet;
+        // Attention!
+        // - As this object is a cache key, non-normalized field values should be avoided.
+        // - Fields with default values must be set until the end of the constructor to ensure that when the lookup happens,
+        //   there will be no unset fields.
+        // - If you add a new field, review all methods in this class
+
+        /**
+         * @param incompatibleImprovements
+         *         Sets which of the non-backward-compatible improvements should be enabled. Not {@code null}. This
+         *         version number is the same as the FreeMarker version number with which the improvements were
+         *         implemented.
+         *         <p>
+         *         For new projects, it's recommended to set this to the FreeMarker version that's used during the
+         *         development. For released products that are still actively developed it's a low risk change to
+         *         increase the 3rd version number further as FreeMarker is updated, but of course you should always
+         *         check the list of effects below. Increasing the 2nd or 1st version number possibly mean substantial
+         *         changes with higher risk of breaking the application, but again, see the list of effects below.
+         *         <p>
+         *         The reason it's separate from {@link Configuration#getIncompatibleImprovements()} is that
+         *         {@link ObjectWrapper} objects are sometimes shared among multiple {@link Configuration}-s, so the two
+         *         version numbers are technically independent. But it's recommended to keep those two version numbers
+         *         the same.
+         *         <p>
+         *         The changes enabled by {@code incompatibleImprovements} are:
+         *         <ul>
+         *             <li><p>3.0.0: No changes; this is the starting point, the version used in older projects.</li>
+         *         </ul>
+         *         <p>
+         *         Note that the version will be normalized to the lowest version where the same incompatible {@link
+         *         DefaultObjectWrapper} improvements were already present, so {@link #getIncompatibleImprovements()}
+         *         might returns a lower version than what you have specified.
+         * @param isIncompImprsAlreadyNormalized
+         *         Tells if the {@code incompatibleImprovements} parameter contains an <em>already normalized</em>
+         *         value. This parameter meant to be {@code true} when the class that extends {@link
+         *         DefaultObjectWrapper} needs to add additional breaking versions over those of {@link
+         *         DefaultObjectWrapper}. Thus, if this parameter is {@code true}, the versions where {@link
+         *         DefaultObjectWrapper} had breaking changes must be already factored into the {@code
+         *         incompatibleImprovements} parameter value, as no more normalization will happen. (You can use {@link
+         *         DefaultObjectWrapper#normalizeIncompatibleImprovementsVersion(Version)} to discover those.)
+         */
+        protected ExtendableBuilder(Version incompatibleImprovements, boolean isIncompImprsAlreadyNormalized) {
+            _CoreAPI.checkVersionNotNullAndSupported(incompatibleImprovements);
+
+            incompatibleImprovements = isIncompImprsAlreadyNormalized
+                    ? incompatibleImprovements
+                    : normalizeIncompatibleImprovementsVersion(incompatibleImprovements);
+            this.incompatibleImprovements = incompatibleImprovements;
+
+            classIntrospectorBuilder = new ClassIntrospector.Builder(incompatibleImprovements);
+        }
+
+        @SuppressWarnings("unchecked")
+        protected SelfT self() {
+            return (SelfT) this;
+        }
+
+        /**
+         * Calculate a content-based hash that could be used when looking up the product object that {@link #build()}
+         * returns from a cache. If you override {@link ExtendableBuilder} and add new fields, don't forget to take
+         * those into account too!
+         *
+         * <p>{@link Builder#hashCode()} is delegated to this.
+         *
+         * @see #equalsForCacheKey(ExtendableBuilder, Object)
+         * @see #cloneForCacheKey()
+         */
+        protected static int hashCodeForCacheKey(ExtendableBuilder<?, ?> builder) {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + builder.getIncompatibleImprovements().hashCode();
+            result = prime * result + builder.getDefaultDateType();
+            result = prime * result + (builder.getOuterIdentity() != null ? builder.getOuterIdentity().hashCode() : 0);
+            result = prime * result + (builder.isStrict() ? 1231 : 1237);
+            result = prime * result + (builder.getUseModelCache() ? 1231 : 1237);
+            result = prime * result + (builder.getUsePrivateCaches() ? 1231 : 1237);
+            result = prime * result + builder.classIntrospectorBuilder.hashCode();
+            return result;
+        }
+
+        /**
+         * A content-based {@link Object#equals(Object)} that could be used to look up the product object that
+         * {@link #build()} returns from a cache. If you override {@link ExtendableBuilder} and add new fields, don't
+         * forget to take those into account too!
+         *
+         * <p>
+         * {@link Builder#equals(Object)} is delegated to this.
+         *
+         * @see #hashCodeForCacheKey(ExtendableBuilder)
+         * @see #cloneForCacheKey()
+         */
+        protected static boolean equalsForCacheKey(ExtendableBuilder<?, ?> thisBuilder,  Object thatObj) {
+            if (thisBuilder == thatObj) return true;
+            if (thatObj == null) return false;
+            if (thisBuilder.getClass() != thatObj.getClass()) return false;
+            ExtendableBuilder<?, ?> thatBuilder = (ExtendableBuilder<?, ?>) thatObj;
+
+            if (!thisBuilder.getIncompatibleImprovements().equals(thatBuilder.getIncompatibleImprovements())) {
+                return false;
+            }
+            if (thisBuilder.getDefaultDateType() != thatBuilder.getDefaultDateType()) return false;
+            if (thisBuilder.getOuterIdentity() != thatBuilder.getOuterIdentity()) return false;
+            if (thisBuilder.isStrict() != thatBuilder.isStrict()) return false;
+            if (thisBuilder.getUseModelCache() != thatBuilder.getUseModelCache()) return false;
+            if (thisBuilder.getUsePrivateCaches() != thatBuilder.getUsePrivateCaches()) return false;
+            return thisBuilder.classIntrospectorBuilder.equals(thatBuilder.classIntrospectorBuilder);
+        }
+
+        /**
+         * If the builder is used as a cache key, this is used to clone it before it's stored in the cache as a key, so
+         * that further changes in the original builder won't change the key (aliasing). It calls {@link Object#clone()}
+         * internally, so all fields are automatically copied, but it will also individually clone field values that are
+         * both mutable and has a content-based equals method (deep cloning).
+         * <p>
+         * If you extend {@link ExtendableBuilder} with new fields with mutable values that have a content-based equals
+         * method, and you will also cache product instances, you need to clone those values manually to prevent
+         * aliasing problems, so don't forget to override this method!
+         *
+         * @see #equalsForCacheKey(ExtendableBuilder, Object)
+         * @see #hashCodeForCacheKey(ExtendableBuilder)
+         */
+        protected SelfT cloneForCacheKey() {
+            try {
+                @SuppressWarnings("unchecked") SelfT clone = (SelfT) super.clone();
+                ((ExtendableBuilder<?, ?>) clone).classIntrospectorBuilder = (ClassIntrospector.Builder)
+                        classIntrospectorBuilder.clone();
+                return clone;
+            } catch (CloneNotSupportedException e) {
+                throw new RuntimeException("Failed to deepClone Builder", e);
+            }
+        }
+
+        public Version getIncompatibleImprovements() {
+            return incompatibleImprovements;
+        }
+
+        /**
+         * Getter pair of {@link #setDefaultDateType(int)}
+         */
+        public int getDefaultDateType() {
+            return defaultDateType;
+        }
+
+        /**
+         * Sets the default date type to use for date models that result from
+         * a plain <tt>java.util.Date</tt> instead of <tt>java.sql.Date</tt> or
+         * <tt>java.sql.Time</tt> or <tt>java.sql.Timestamp</tt>. Default value is
+         * {@link TemplateDateModel#UNKNOWN}.
+         * @param defaultDateType the new default date type.
+         */
+        public void setDefaultDateType(int defaultDateType) {
+            this.defaultDateType = defaultDateType;
+            defaultDataTypeSet = true;
+        }
+
+        /**
+         * Fluent API equivalent of {@link #setDefaultDateType(int)}.
+         */
+        public SelfT defaultDateType(int defaultDateType) {
+            setDefaultDateType(defaultDateType);
+            return self();
+        }
+
+        /**
+         * Tells if the property was explicitly set, as opposed to just holding its default value.
+         */
+        public boolean isDefaultDateTypeSet() {
+            return defaultDataTypeSet;
+        }
+
+        /**
+         * Getter pair of {@link #setOuterIdentity(ObjectWrapper)}.
+         */
+        public ObjectWrapper getOuterIdentity() {
+            return outerIdentity;
+        }
+
+        /**
+         * When wrapping an object, the DefaultObjectWrapper commonly needs to wrap "sub-objects", for example each
+         * element in a wrapped collection. Normally it wraps these objects using itself. However, this makes it
+         * difficult to delegate to a DefaultObjectWrapper as part of a custom aggregate ObjectWrapper. This method lets
+         * you set the ObjectWrapper which will be used to wrap the sub-objects.
+         *
+         * @param outerIdentity
+         *         the aggregate ObjectWrapper, or {@code null} if we will use the object created by this builder.
+         */
+        public void setOuterIdentity(ObjectWrapper outerIdentity) {
+            this.outerIdentity = outerIdentity;
+            outerIdentitySet = true;
+        }
+
+        /**
+         * Fluent API equivalent of {@link #setOuterIdentity(ObjectWrapper)}.
+         */
+        public SelfT outerIdentity(ObjectWrapper outerIdentity) {
+            setOuterIdentity(outerIdentity);
+            return self();
+        }
+
+        /**
+         * Tells if the property was explicitly set, as opposed to just holding its default value.
+         */
+        public boolean isOuterIdentitySet() {
+            return outerIdentitySet;
+        }
+
+        /**
+         * Getter pair of {@link #setStrict(boolean)}.
+         */
+        public boolean isStrict() {
+            return strict;
+        }
+
+        /**
+         * Specifies if an attempt to read a bean property that doesn't exist in the
+         * wrapped object should throw an {@link InvalidPropertyException}.
+         *
+         * <p>If this property is <tt>false</tt> (the default) then an attempt to read
+         * a missing bean property is the same as reading an existing bean property whose
+         * value is <tt>null</tt>. The template can't tell the difference, and thus always
+         * can use <tt>?default('something')</tt> and <tt>?exists</tt> and similar built-ins
+         * to handle the situation.
+         *
+         * <p>If this property is <tt>true</tt> then an attempt to read a bean propertly in
+         * the template (like <tt>myBean.aProperty</tt>) that doesn't exist in the bean
+         * object (as opposed to just holding <tt>null</tt> value) will cause
+         * {@link InvalidPropertyException}, which can't be suppressed in the template
+         * (not even with <tt>myBean.noSuchProperty?default('something')</tt>). This way
+         * <tt>?default('something')</tt> and <tt>?exists</tt> and similar built-ins can be used to
+         * handle existing properties whose value is <tt>null</tt>, without the risk of
+         * hiding typos in the property names. Typos will always cause error. But mind you, it
+         * goes against the basic approach of FreeMarker, so use this feature only if you really
+         * know what you are doing.
+         */
+        public void setStrict(boolean strict) {
+            this.strict = strict;
+            strictSet = true;
+        }
+
+        /**
+         * Fluent API equivalent of {@link #setStrict(boolean)}.
+         */
+        public SelfT strict(boolean strict) {
+            setStrict(strict);
+            return self();
+        }
+
+        /**
+         * Tells if the property was explicitly set, as opposed to just holding its default value.
+         */
+        public boolean isStrictSet() {
+            return strictSet;
+        }
+
+        public boolean getUseModelCache() {
+            return useModelCache;
+        }
+
+        /**
+         * @deprecated Does nothing in FreeMarker 3 - we kept it for now to postopne reworking some JUnit tests.
+         */
+        // [FM3] Remove
+        @Deprecated
+        public void setUseModelCache(boolean useModelCache) {
+            this.useModelCache = useModelCache;
+            useModelCacheSet = true;
+        }
+
+        /**
+         * Fluent API equivalent of {@link #setUseModelCache(boolean)}.
+         * @deprecated Does nothing in FreeMarker 3 - we kept it for now to postopne reworking some JUnit tests.
+         */
+        @Deprecated
+        public SelfT useModelCache(boolean useModelCache) {
+            setUseModelCache(useModelCache);
+            return self();
+        }
+
+        /**
+         * Tells if the property was explicitly set, as opposed to just holding its default value.
+         */
+        public boolean isUseModelCacheSet() {
+            return useModelCacheSet;
+        }
+
+        /**
+         * Getter pair of {@link #setUsePrivateCaches(boolean)}.
+         */
+        public boolean getUsePrivateCaches() {
+            return usePrivateCaches;
+        }
+
+        /**
+         * Tells if the instance cerates should try to caches with other {@link DefaultObjectWrapper} instances (where
+         * possible), or it should always invoke its own caches and not share that with anyone else.
+         * */
+        public void setUsePrivateCaches(boolean usePrivateCaches) {
+            this.usePrivateCaches = usePrivateCaches;
+            usePrivateCachesSet = true;
+        }
+
+        /**
+         * Tells if the property was explicitly set, as opposed to just holding its default value.
+         */
+        public boolean isUsePrivateCachesSet() {
+            return usePrivateCachesSet;
+        }
+
+        /**
+         * Fluent API equivalent of {@link #setUsePrivateCaches(boolean)}
+         */
+        public SelfT usePrivateCaches(boolean usePrivateCaches) {
+            setUsePrivateCaches(usePrivateCaches);
+            return self();
+        }
+
+        public int getExposureLevel() {
+            return classIntrospectorBuilder.getExposureLevel();
+        }
+
+        /**
+         * Sets the method exposure level. By default, set to <code>EXPOSE_SAFE</code>.
+         * @param exposureLevel can be any of the <code>EXPOSE_xxx</code>
+         * constants.
+         */
+        public void setExposureLevel(int exposureLevel) {
+            classIntrospectorBuilder.setExposureLevel(exposureLevel);
+        }
+
+        public SelfT exposureLevel(int exposureLevel) {
+            setExposureLevel(exposureLevel);
+            return self();
+        }
+
+        /**
+         * Tells if the property was explicitly set, as opposed to just holding its default value.
+         */
+        public boolean setExposureLevelSet() {
+            return classIntrospectorBuilder.isExposureLevelSet();
+        }
+
+        /**
+         * Getter pair of {@link #setExposeFields(boolean)}
+         */
+        public boolean getExposeFields() {
+            return classIntrospectorBuilder.getExposeFields();
+        }
+
+        /**
+         * Controls whether public instance fields of classes are exposed to
+         * templates.
+         * @param exposeFields if set to true, public instance fields of classes
+         * that do not have a property getter defined can be accessed directly by
+         * their name. If there is a property getter for a property of the same
+         * name as the field (i.e. getter "getFoo()" and field "foo"), then
+         * referring to "foo" in template invokes the getter. If set to false, no
+         * access to public instance fields of classes is given. Default is false.
+         */
+        public void setExposeFields(boolean exposeFields) {
+            classIntrospectorBuilder.setExposeFields(exposeFields);
+        }
+
+        /**
+         * Fluent API equivalent of {@link #setExposeFields(boolean)}
+         */
+        public SelfT exposeFields(boolean exposeFields) {
+            setExposeFields(exposeFields);
+            return self();
+        }
+
+        /**
+         * Tells if the property was explicitly set, as opposed to just holding its default value.
+         */
+        public boolean isExposeFieldsSet() {
+            return classIntrospectorBuilder.isExposeFieldsSet();
+        }
+
+        /**
+         * Getter pair of {@link #setMethodAppearanceFineTuner(MethodAppearanceFineTuner)}
+         */
+        public MethodAppearanceFineTuner getMethodAppearanceFineTuner() {
+            return classIntrospectorBuilder.getMethodAppearanceFineTuner();
+        }
+
+        /**
+         * Used to tweak certain aspects of how methods appear in the data-model;
+         * see {@link MethodAppearanceFineTuner} for more.
+         * Setting this to non-{@code null} will disable class introspection cache sharing, unless
+         * the value implements {@link SingletonCustomizer}.
+         */
+        public void setMethodAppearanceFineTuner(MethodAppearanceFineTuner methodAppearanceFineTuner) {
+            classIntrospectorBuilder.setMethodAppearanceFineTuner(methodAppearanceFineTuner);
+        }
+
+        /**
+         * Fluent API equivalent of {@link #setMethodAppearanceFineTuner(MethodAppearanceFineTuner)}
+         */
+        public SelfT methodAppearanceFineTuner(MethodAppearanceFineTuner methodAppearanceFineTuner) {
+            setMethodAppearanceFineTuner(methodAppearanceFineTuner);
+            return self();
+        }
+
+        /**
+         * Tells if the property was explicitly set, as opposed to just holding its default value.
+         */
+        public boolean isMethodAppearanceFineTunerSet() {
+            return classIntrospectorBuilder.isMethodAppearanceFineTunerSet();
+        }
+
+        /**
+         * Used internally for testing.
+         */
+        MethodSorter getMethodSorter() {
+            return classIntrospectorBuilder.getMethodSorter();
+        }
+
+        /**
+         * Used internally for testing.
+         */
+        void setMethodSorter(MethodSorter methodSorter) {
+            classIntrospectorBuilder.setMethodSorter(methodSorter);
+        }
+
+        /**
+         * Used internally for testing.
+         */
+        SelfT methodSorter(MethodSorter methodSorter) {
+            setMethodSorter(methodSorter);
+            return self();
+        }
+
+    }
+}


[49/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBean.java
----------------------------------------------------------------------
diff --git a/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBean.java b/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBean.java
new file mode 100644
index 0000000..c7d27a6
--- /dev/null
+++ b/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBean.java
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+public class Java8BridgeMethodsWithDefaultMethodBean implements Java8BridgeMethodsWithDefaultMethodBeanBase<String> {
+
+    static final String M1_RETURN_VALUE = "m1ReturnValue"; 
+    
+    public String m1() {
+        return M1_RETURN_VALUE;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBean2.java
----------------------------------------------------------------------
diff --git a/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBean2.java b/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBean2.java
new file mode 100644
index 0000000..7dfb39a
--- /dev/null
+++ b/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBean2.java
@@ -0,0 +1,23 @@
+/*
+ * 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;
+
+public class Java8BridgeMethodsWithDefaultMethodBean2 implements Java8BridgeMethodsWithDefaultMethodBeanBase2 {
+    // All inherited
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBeanBase.java
----------------------------------------------------------------------
diff --git a/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBeanBase.java b/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBeanBase.java
new file mode 100644
index 0000000..fdd8821
--- /dev/null
+++ b/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBeanBase.java
@@ -0,0 +1,31 @@
+/*
+ * 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;
+
+public interface Java8BridgeMethodsWithDefaultMethodBeanBase<T> {
+
+    default T m1() {
+        return null;
+    }
+    
+    default T m2() {
+        return null;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBeanBase2.java
----------------------------------------------------------------------
diff --git a/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBeanBase2.java b/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBeanBase2.java
new file mode 100644
index 0000000..6f68dc7
--- /dev/null
+++ b/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBeanBase2.java
@@ -0,0 +1,28 @@
+/*
+ * 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;
+
+public interface Java8BridgeMethodsWithDefaultMethodBeanBase2 extends Java8BridgeMethodsWithDefaultMethodBeanBase<String> {
+
+    @Override
+    default String m1() {
+        return Java8BridgeMethodsWithDefaultMethodBean.M1_RETURN_VALUE;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultMethodsBean.java
----------------------------------------------------------------------
diff --git a/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultMethodsBean.java b/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultMethodsBean.java
new file mode 100644
index 0000000..eabc3d0
--- /dev/null
+++ b/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultMethodsBean.java
@@ -0,0 +1,84 @@
+/*
+ * 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;
+
+public class Java8DefaultMethodsBean implements Java8DefaultMethodsBeanBase {
+    
+    static final String NORMAL_PROP = "normalProp";
+    static final String NORMAL_PROP_VALUE = "normalPropValue";
+    static final String PROP_2_OVERRIDE_VALUE = "prop2OverrideValue";
+    static final int NOT_AN_INDEXED_PROP_VALUE = 1;
+    static final String ARRAY_PROP_2_VALUE_0 = "arrayProp2[0].value";
+    private static final int NOT_AN_INDEXED_PROP_3_VALUE = 3;
+    private static final String NOT_AN_INDEXED_PROP_2_VALUE = "notAnIndecedProp2Value";
+    static final String INDEXED_PROP_4 = "indexedProp4";
+    static final String INDEXED_PROP_GETTER_4 = "getIndexedProp4";
+    static final String INDEXED_PROP_4_VALUE = "indexedProp4Value[0]";
+    static final String NORMAL_ACTION = "normalAction";
+    static final String NORMAL_ACTION_RETURN_VALUE = "normalActionReturnValue";
+    static final String OVERRIDDEN_DEFAULT_METHOD_ACTION_RETURN_VALUE = "overriddenValue";
+    
+    public String getNormalProp() {
+        return NORMAL_PROP_VALUE;
+    }
+    
+    @Override
+    public String getDefaultMethodProp2() {
+        return PROP_2_OVERRIDE_VALUE;
+    }
+    
+    public String[] getDefaultMethodIndexedProp2() {
+        return new String[] { ARRAY_PROP_2_VALUE_0 };
+    }
+
+    /**
+     * There's a matching non-indexed reader method in the base class, but as this is indexed, it takes over. 
+     */
+    public String getDefaultMethodIndexedProp3(int index) {
+        return "";
+    }
+    
+    public int getDefaultMethodNotAnIndexedProp() {
+        return NOT_AN_INDEXED_PROP_VALUE;
+    }
+
+    /** Actually, this will be indexed if the default method support is off. */
+    public String getDefaultMethodNotAnIndexedProp2(int index) {
+        return NOT_AN_INDEXED_PROP_2_VALUE;
+    }
+    
+    /** Actually, this will be indexed if the default method support is off. */
+    public int getDefaultMethodNotAnIndexedProp3(int index) {
+        return NOT_AN_INDEXED_PROP_3_VALUE;
+    }
+    
+    public String getIndexedProp4(int index) {
+        return INDEXED_PROP_4_VALUE;
+    }
+    
+    public String normalAction() {
+        return NORMAL_ACTION_RETURN_VALUE;
+    }
+    
+    @Override
+    public String overriddenDefaultMethodAction() {
+        return OVERRIDDEN_DEFAULT_METHOD_ACTION_RETURN_VALUE;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultMethodsBeanBase.java
----------------------------------------------------------------------
diff --git a/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultMethodsBeanBase.java b/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultMethodsBeanBase.java
new file mode 100644
index 0000000..c01422e
--- /dev/null
+++ b/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultMethodsBeanBase.java
@@ -0,0 +1,97 @@
+/*
+ * 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;
+
+public interface Java8DefaultMethodsBeanBase {
+    
+    static final String DEFAULT_METHOD_PROP = "defaultMethodProp";
+    static final String DEFAULT_METHOD_PROP_VALUE = "defaultMethodPropValue";
+    static final String DEFAULT_METHOD_PROP_2 = "defaultMethodProp2";
+    static final String DEFAULT_METHOD_INDEXED_PROP = "defaultMethodIndexedProp";
+    static final String DEFAULT_METHOD_INDEXED_PROP_GETTER = "getDefaultMethodIndexedProp";
+    static final String DEFAULT_METHOD_INDEXED_PROP_VALUE = "defaultMethodIndexedPropValue";
+    static final String DEFAULT_METHOD_INDEXED_PROP_2 = "defaultMethodIndexedProp2";
+    static final String DEFAULT_METHOD_INDEXED_PROP_2_VALUE_0 = "defaultMethodIndexedProp2(0).value";
+    static final String DEFAULT_METHOD_INDEXED_PROP_3 = "defaultMethodIndexedProp3";
+    static final String DEFAULT_METHOD_INDEXED_PROP_3_VALUE_0 = "indexedProp3Value[0]";
+    static final String DEFAULT_METHOD_NOT_AN_INDEXED_PROP = "defaultMethodNotAnIndexedProp";
+    static final String DEFAULT_METHOD_NOT_AN_INDEXED_PROP_VALUE = "defaultMethodNotAnIndexedPropValue";
+    static final String DEFAULT_METHOD_NOT_AN_INDEXED_PROP_2 = "defaultMethodNotAnIndexedProp2";
+    static final String DEFAULT_METHOD_NOT_AN_INDEXED_PROP_2_VALUE = "defaultMethodNotAnIndexedProp2Value";
+    static final String DEFAULT_METHOD_NOT_AN_INDEXED_PROP_3 = "defaultMethodNotAnIndexedProp3";
+    static final String DEFAULT_METHOD_NOT_AN_INDEXED_PROP_3_VALUE_0 = "defaultMethodNotAnIndexedProp3Value[0]";
+    static final String DEFAULT_METHOD_ACTION = "defaultMethodAction";
+    static final String DEFAULT_METHOD_ACTION_RETURN_VALUE = "defaultMethodActionReturnValue";
+    static final String OVERRIDDEN_DEFAULT_METHOD_ACTION = "overriddenDefaultMethodAction";
+
+    default String getDefaultMethodProp() {
+        return DEFAULT_METHOD_PROP_VALUE;
+    }
+
+    default String getDefaultMethodProp2() {
+        return "";
+    }
+
+    /**
+     * Will be kept as there's no non-indexed read methods for this.
+     */
+    default String getDefaultMethodIndexedProp(int i) {
+        return DEFAULT_METHOD_INDEXED_PROP_VALUE;
+    }
+
+    /**
+     * Will be kept as there will be a matching non-indexed read method in the subclass.
+     * However, as of FM3, the non-indexed read method is used if it's available.
+     */
+    default String getDefaultMethodIndexedProp2(int i) {
+        return DEFAULT_METHOD_INDEXED_PROP_2_VALUE_0;
+    }
+
+    /**
+     * This is not an indexed reader method, but a matching indexed reader method will be added in the subclass. 
+     * However, as of FM3, the non-indexed read method is used if it's available.
+     */
+    default String[] getDefaultMethodIndexedProp3() {
+        return new String[] {DEFAULT_METHOD_INDEXED_PROP_3_VALUE_0};
+    }
+    
+    /** Will be discarded because of a non-matching non-indexed read method in a subclass */
+    default String getDefaultMethodNotAnIndexedProp(int i) {
+        return "";
+    }
+    
+    /** The subclass will try to override this with a non-matching indexed reader, but this will be stronger. */
+    default String getDefaultMethodNotAnIndexedProp2() {
+        return DEFAULT_METHOD_NOT_AN_INDEXED_PROP_2_VALUE;
+    }
+
+    /** The subclass will try to override this with a non-matching indexed reader, but this will be stronger. */
+    default String[] getDefaultMethodNotAnIndexedProp3() {
+        return new String[] { DEFAULT_METHOD_NOT_AN_INDEXED_PROP_3_VALUE_0 };
+    }
+    
+    default String defaultMethodAction() {
+        return DEFAULT_METHOD_ACTION_RETURN_VALUE;
+    }
+
+    default Object overriddenDefaultMethodAction() {
+        return null;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultObjectWrapperBridgeMethodsTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultObjectWrapperBridgeMethodsTest.java b/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultObjectWrapperBridgeMethodsTest.java
new file mode 100644
index 0000000..495f3f9
--- /dev/null
+++ b/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultObjectWrapperBridgeMethodsTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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 static org.junit.Assert.*;
+
+import java.util.Collections;
+
+import org.junit.Test;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateMethodModelEx;
+import org.apache.freemarker.core.model.TemplateModelException;
+
+public class Java8DefaultObjectWrapperBridgeMethodsTest {
+    
+    @Test
+    public void testWithoutDefaultMethod() throws TemplateModelException {
+        test(BridgeMethodsBean.class);
+    }
+
+    @Test
+    public void testWithDefaultMethod() throws TemplateModelException {
+        test(Java8BridgeMethodsWithDefaultMethodBean.class);
+    }
+
+    @Test
+    public void testWithDefaultMethod2() throws TemplateModelException {
+        test(Java8BridgeMethodsWithDefaultMethodBean2.class);
+    }
+
+    private void test(Class<?> pClass) throws TemplateModelException {
+        DefaultObjectWrapper ow = new DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
+        TemplateHashModel wrapped;
+        try {
+            wrapped = (TemplateHashModel) ow.wrap(pClass.newInstance());
+        } catch (Exception e) {
+            throw new IllegalStateException(e);
+        }
+        
+        TemplateMethodModelEx m1 = (TemplateMethodModelEx) wrapped.get("m1");
+        assertEquals(BridgeMethodsBean.M1_RETURN_VALUE, "" + m1.exec(Collections.emptyList()));
+        
+        TemplateMethodModelEx m2 = (TemplateMethodModelEx) wrapped.get("m2");
+        assertNull(m2.exec(Collections.emptyList()));
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultObjectWrapperTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultObjectWrapperTest.java b/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultObjectWrapperTest.java
new file mode 100644
index 0000000..905d536
--- /dev/null
+++ b/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultObjectWrapperTest.java
@@ -0,0 +1,160 @@
+/*
+ * 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 static org.junit.Assert.*;
+
+import java.util.Collections;
+
+import org.junit.Test;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateMethodModelEx;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+
+public class Java8DefaultObjectWrapperTest {
+
+    @Test
+    public void testDefaultMethodRecognized() throws TemplateModelException {
+        DefaultObjectWrapper.Builder owb = new DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0);
+        DefaultObjectWrapper ow = owb.build();
+        TemplateHashModel wrappedBean = (TemplateHashModel) ow.wrap(new Java8DefaultMethodsBean());
+        
+        {
+            TemplateScalarModel prop = (TemplateScalarModel) wrappedBean.get(Java8DefaultMethodsBean.NORMAL_PROP);
+            assertNotNull(prop);
+            assertEquals(Java8DefaultMethodsBean.NORMAL_PROP_VALUE, prop.getAsString());
+        }
+        {
+            // This is overridden in the subclass, so it's visible even without default method support: 
+            TemplateScalarModel prop = (TemplateScalarModel) wrappedBean.get(
+                    Java8DefaultMethodsBean.DEFAULT_METHOD_PROP_2);
+            assertNotNull(prop);
+            assertEquals(Java8DefaultMethodsBean.PROP_2_OVERRIDE_VALUE, prop.getAsString());
+        }
+        {
+            TemplateScalarModel prop = (TemplateScalarModel) wrappedBean.get(
+                    Java8DefaultMethodsBeanBase.DEFAULT_METHOD_PROP);
+            assertNotNull(prop);
+            assertEquals(Java8DefaultMethodsBeanBase.DEFAULT_METHOD_PROP_VALUE, prop.getAsString());
+        }
+        {
+            // Has only indexed read method, so it's not exposed as a property
+            assertNull(wrappedBean.get(Java8DefaultMethodsBeanBase.DEFAULT_METHOD_INDEXED_PROP));
+
+            TemplateMethodModelEx indexedReadMethod = (TemplateMethodModelEx) wrappedBean.get(
+                    Java8DefaultMethodsBeanBase.DEFAULT_METHOD_INDEXED_PROP_GETTER);
+            assertNotNull(indexedReadMethod);
+            assertEquals(Java8DefaultMethodsBeanBase.DEFAULT_METHOD_INDEXED_PROP_VALUE,
+                    ((TemplateScalarModel) indexedReadMethod.exec(Collections.singletonList(new SimpleNumber(0))))
+                            .getAsString
+                            ());
+        }
+        {
+            // We see default method indexed read method, but it's invalidated by normal getter in the subclass
+            TemplateNumberModel prop = (TemplateNumberModel) wrappedBean.get(
+                    Java8DefaultMethodsBean.DEFAULT_METHOD_NOT_AN_INDEXED_PROP);
+            assertNotNull(prop);
+            assertEquals(Java8DefaultMethodsBean.NOT_AN_INDEXED_PROP_VALUE, prop.getAsNumber());
+        }
+        {
+            // The default method read method invalidates the indexed read method in the subclass
+            TemplateScalarModel prop = (TemplateScalarModel) wrappedBean.get(
+                    Java8DefaultMethodsBean.DEFAULT_METHOD_NOT_AN_INDEXED_PROP_2);
+            assertNotNull(prop);
+            assertEquals(Java8DefaultMethodsBean.DEFAULT_METHOD_NOT_AN_INDEXED_PROP_2_VALUE, prop.getAsString());
+        }
+        {
+            // The default method read method invalidates the indexed read method in the subclass
+            TemplateSequenceModel prop = (TemplateSequenceModel) wrappedBean.get(
+                    Java8DefaultMethodsBean.DEFAULT_METHOD_NOT_AN_INDEXED_PROP_3);
+            assertNotNull(prop);
+            assertEquals(Java8DefaultMethodsBean.DEFAULT_METHOD_NOT_AN_INDEXED_PROP_3_VALUE_0,
+                    ((TemplateScalarModel) prop.get(0)).getAsString());
+        }
+        {
+            // We see the default method indexed reader, which overrides the plain array reader in the subclass.
+            TemplateSequenceModel prop = (TemplateSequenceModel) wrappedBean.get(
+                    Java8DefaultMethodsBean.DEFAULT_METHOD_INDEXED_PROP_2);
+            assertNotNull(prop);
+            assertEquals(Java8DefaultMethodsBean.ARRAY_PROP_2_VALUE_0,
+                    ((TemplateScalarModel) prop.get(0)).getAsString());
+        }
+        {
+            // We do see the default method non-indexed reader, but the subclass has a matching indexed reader, so that
+            // takes over.
+            TemplateSequenceModel prop = (TemplateSequenceModel) wrappedBean.get(
+                    Java8DefaultMethodsBean.DEFAULT_METHOD_INDEXED_PROP_3);
+            assertNotNull(prop);
+            assertEquals(Java8DefaultMethodsBeanBase.DEFAULT_METHOD_INDEXED_PROP_3_VALUE_0,
+                    ((TemplateScalarModel) prop.get(0)).getAsString());
+        }        
+        {
+            // Only present in the subclass.
+
+            // Has only indexed read method, so it's not exposed as a property
+            assertNull(wrappedBean.get(Java8DefaultMethodsBean.INDEXED_PROP_4));
+
+            TemplateMethodModelEx indexedReadMethod = (TemplateMethodModelEx) wrappedBean.get(
+                    Java8DefaultMethodsBean.INDEXED_PROP_GETTER_4);
+            assertNotNull(indexedReadMethod);
+            assertEquals(Java8DefaultMethodsBean.INDEXED_PROP_4_VALUE,
+                    ((TemplateScalarModel) indexedReadMethod.exec(Collections.singletonList(new SimpleNumber(0))))
+                            .getAsString());
+        }        
+        {
+            TemplateMethodModelEx action = (TemplateMethodModelEx) wrappedBean.get(
+                    Java8DefaultMethodsBean.NORMAL_ACTION);
+            assertNotNull(action);
+            assertEquals(
+                    Java8DefaultMethodsBean.NORMAL_ACTION_RETURN_VALUE,
+                    ((TemplateScalarModel) action.exec(Collections.emptyList())).getAsString());
+        }
+        
+        {
+            TemplateMethodModelEx action = (TemplateMethodModelEx) wrappedBean.get(
+                    Java8DefaultMethodsBean.NORMAL_ACTION);
+            assertNotNull(action);
+            assertEquals(
+                    Java8DefaultMethodsBean.NORMAL_ACTION_RETURN_VALUE,
+                    ((TemplateScalarModel) action.exec(Collections.emptyList())).getAsString());
+        }
+        {
+            TemplateMethodModelEx action = (TemplateMethodModelEx) wrappedBean.get(
+                    Java8DefaultMethodsBean.DEFAULT_METHOD_ACTION);
+            assertNotNull(action);
+            assertEquals(
+                    Java8DefaultMethodsBean.DEFAULT_METHOD_ACTION_RETURN_VALUE,
+                    ((TemplateScalarModel) action.exec(Collections.emptyList())).getAsString());
+        }
+        {
+            TemplateMethodModelEx action = (TemplateMethodModelEx) wrappedBean.get(
+                    Java8DefaultMethodsBean.OVERRIDDEN_DEFAULT_METHOD_ACTION);
+            assertNotNull(action);
+            assertEquals(
+                    Java8DefaultMethodsBean.OVERRIDDEN_DEFAULT_METHOD_ACTION_RETURN_VALUE,
+                    ((TemplateScalarModel) action.exec(Collections.emptyList())).getAsString());
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/build.gradle
----------------------------------------------------------------------
diff --git a/freemarker-core/build.gradle b/freemarker-core/build.gradle
new file mode 100644
index 0000000..3419439
--- /dev/null
+++ b/freemarker-core/build.gradle
@@ -0,0 +1,155 @@
+/*
+ * 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.
+ */
+
+plugins {
+    id "ca.coglinc.javacc" version "2.4.0"
+}
+
+String moduleNiceName = "Apache FreeMarker Core"
+
+dependencies {
+    // Note that commond dependencies are added in the root project.
+
+    // ------------------------------------------------------------------------
+    // For the main artifact
+
+    compileOnly "org.zeroturnaround:javarebel-sdk:1.2.2"
+
+    // TODO These will be moved to freemarker-dom module:
+    
+    compileOnly "jaxen:jaxen:1.0-FCS"
+    compileOnly "saxpath:saxpath:1.0-FCS"
+    compileOnly("xalan:xalan:2.7.0") {
+        // xml-apis is part of Java SE since version 1.4:
+        exclude group: "xml-apis", module: "xml-apis"
+    }
+
+    testRuntime "jaxen:jaxen:1.0-FCS"
+    testRuntime "saxpath:saxpath:1.0-FCS"
+    testRuntime("xalan:xalan:2.7.0") {
+        // xml-apis is part of Java SE since version 1.4:
+        exclude group: "xml-apis", module: "xml-apis"
+    }
+
+}
+
+compileJavacc {
+    arguments = [ grammar_encoding: "UTF-8" ]
+    outputDirectory = new File(outputDirectory, 'org/apache/freemarker/core')
+    doLast {
+        ant.replace(
+          file: "${outputDirectory}/FMParser.java",
+          token: "public class FMParser",
+          value: "class FMParser"
+        )
+        ant.replace(
+          file: "${outputDirectory}/FMParser.java",
+          token: "private final LookaheadSuccess",
+          value: "private static final LookaheadSuccess"
+        )
+        ant.replace(
+          file: "${outputDirectory}/FMParserConstants.java",
+          token: "public interface FMParserConstants",
+          value: "interface FMParserConstants"
+        )
+        ant.replace(
+          file: "${outputDirectory}/FMParserTokenManager.java",
+          token: "public class FMParserTokenManager",
+          value: "class FMParserTokenManager"
+        )
+        ant.replace(
+          file: "${outputDirectory}/Token.java",
+          token: "public class Token",
+          value: "class Token"
+        )
+        ant.replace(
+          file: "${outputDirectory}/SimpleCharStream.java",
+          token: "public class SimpleCharStream",
+          value: "class SimpleCharStream"
+        )
+
+        // Note: The Gradle JavaCC plugin automatically removes generated java files that are already in
+        // src/main/java, so we don't need to get rid of ParseException.java and TokenMgrError.java (unlike in Ant)
+    }
+}
+sourceSets.main.java.srcDir new File(buildDir, 'generated/javacc') // Wasn't needed for the build, but for IDE-s
+idea {
+    module {
+        generatedSourceDirs += file('build/generated/javacc') // Marks the already(!) added srcDir as "generated"
+    }
+}
+
+jar {
+    manifest {
+        // TODO Import exclusions has to be adjusted as we factor out to external modules!
+        instructionReplace 'Import-Package', '!org.apache.freemarker.*', 'org.slf4j.*', '*;resolution:="optional"'
+        // The above makes all imports optional (like servlet API-s, etc.),
+        // except those that were explicitly listed (or are inside java.*).
+        // Thus, even when the Java platfrom includes a package, it won't
+        // be automatically imported, unless bnd generates the import statement
+        // for them.
+
+        // This is needed for "a.class.from.another.Bundle"?new() to work.
+        instructionReplace 'DynamicImport-Package', '*'
+
+        // The required minimum is 1.7, but we utilize 1.8 if available.
+        // See also: http://wiki.eclipse.org/Execution_Environments, "Compiling
+        // against more than is required"
+        instructionReplace 'Bundle-RequiredExecutionEnvironment', 'JavaSE-1.8, JavaSE-1.7'
+        // TODO is this the right way in Require-Capability to specify a version range?
+        instructionReplace 'Require-Capability', 'osgi.ee;filter:="(&(osgi.ee=JavaSE)(version>=1.7))"'
+
+        attributes(
+            "Extension-name": moduleNiceName,
+            "Specification-Title": moduleNiceName,
+            "Implementation-Title": moduleNiceName
+        )
+    }
+}
+
+javadoc {
+    title "${moduleNiceName} ${versionCanonical} API"
+}
+
+// The identical parts of Maven "deployer" and "installer" configuration:
+def mavenCommons = { callerDelegate ->
+    delegate = callerDelegate
+    
+    pom.project {
+        description(
+                "FreeMarker template engine, core module. This module covers all basic functionality, "
+                + "and is all that's needed for many applications.")
+    }
+}
+
+uploadArchives {
+    repositories {
+        mavenDeployer {
+            mavenCommons(delegate)
+        }
+    }
+}
+
+install {
+    repositories {
+        mavenInstaller {
+            mavenCommons(delegate)
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/dist/bin/LICENSE
----------------------------------------------------------------------
diff --git a/freemarker-core/src/dist/bin/LICENSE b/freemarker-core/src/dist/bin/LICENSE
new file mode 100644
index 0000000..a0f6dc4
--- /dev/null
+++ b/freemarker-core/src/dist/bin/LICENSE
@@ -0,0 +1,232 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed 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.
+
+=========================================================================
+
+The documentation includes a selection of icons from various icon sets
+(fonts), stored together inside these files, which were geneated with
+https://icomoon.io/app/:
+
+    documentation/_html/docgen-resources/fonts/icomoon.eot
+    documentation/_html/docgen-resources/fonts/icomoon.svg
+    documentation/_html/docgen-resources/fonts/icomoon.ttf
+    documentation/_html/docgen-resources/fonts/icomoon.woff
+    
+The name, license, and attribution of each icon sets (fonts) used follows:
+    
+- The documentation includes icons from Entypo pictograms, version 2.0,
+  by Daniel Bruce (http://www.entypo.com/, http://www.danielbruce.se/),
+  licensed under Creative Commons Attribution-ShareAlike 3.0 (CC BY-SA 3.0)
+  (http://creativecommons.org/licenses/by-sa/3.0/legalcode) and under SIL
+  Open Font License 1.1 (http://scripts.sil.org/OFL).
+
+- The documentation includes icons from Font Awesome by Dave Gandy
+  (http://fontawesome.io), licensed under SIL Open Font License 1.1
+  (http://scripts.sil.org/OFL).
+
+- The documentation includes icons from Material Design icons by Google
+  (http://google.github.io/material-design-icons/), licensed under
+  Creative Common Attribution 4.0 International License (CC-BY 4.0)
+  (https://creativecommons.org/licenses/by/4.0/).
+
+=========================================================================

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/dist/bin/documentation/index.html
----------------------------------------------------------------------
diff --git a/freemarker-core/src/dist/bin/documentation/index.html b/freemarker-core/src/dist/bin/documentation/index.html
new file mode 100644
index 0000000..a482423
--- /dev/null
+++ b/freemarker-core/src/dist/bin/documentation/index.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<!--
+  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.
+-->
+
+<html lang="hu">
+
+<head>
+  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+  <meta http-equiv="Content-Script-Type" content="text/javascript">
+  <title>FreeMarker Documention</title>
+  <style type="text/css">
+    body {
+      background: #FFF;
+      padding: 2em;
+      color: #000000;
+      font-family: Arial,sans-serif;
+      font-size: 16px;
+    }
+    h1 {
+      font-size: 166%;
+      margin-top: 1.5em;
+      margin-bottom: 0.75em;
+      color: #0050B2;
+      font-family: Arial,sans-serif;
+      font-weight: bold;
+    }
+    .top {
+      margin-top: 0;
+    }
+    a:link,
+    a:visited,
+    a:hover,
+    a:active {
+      color:#00C;
+      text-decoration: none;
+    }
+  </style>
+</head>
+
+<body>
+    <p class="top">Offline FreeMarker Documentation:</p>
+    <ul>
+      <li><a href="_html/index.html">Manual</a></li>
+      <li><a href="_html/api/index.html">Java API</a></li>
+    </ul>
+    
+    <p><a href="http://freemarker.org/">Visit the FreeMarker home page</a> (help, editor plugins, latest downloads, etc.)</p>
+    
+    <p><i><b>Disclaimer:</b> Apache FreeMarker is an effort undergoing incubation at The Apache Software Foundation (ASF), sponsored by the Apache Incubator. Incubation is required of all newly accepted projects until a further review indicates that the infrastructure, communications, and decision making process have stabilized in a manner consistent with other successful ASF projects. While incubation status is not necessarily a reflection of the completeness or stability of the code, it does indicate that the project has yet to be fully endorsed by the ASF.</i></p>
+</body>
+</html>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/dist/javadoc/META-INF/LICENSE
----------------------------------------------------------------------
diff --git a/freemarker-core/src/dist/javadoc/META-INF/LICENSE b/freemarker-core/src/dist/javadoc/META-INF/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/freemarker-core/src/dist/javadoc/META-INF/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed 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.


[10/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateFormatUtil.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateFormatUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateFormatUtil.java
new file mode 100644
index 0000000..4029c78
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateFormatUtil.java
@@ -0,0 +1,77 @@
+/*
+ * 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.valueformat;
+
+import java.util.Date;
+
+import org.apache.freemarker.core._EvalUtil;
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+
+/**
+ * Utility classes for implementing {@link TemplateValueFormat}-s.
+ * 
+ * @since 2.3.24 
+ */
+public final class TemplateFormatUtil {
+    
+    private TemplateFormatUtil() {
+        // Not meant to be instantiated
+    }
+
+    public static void checkHasNoParameters(String params) throws InvalidFormatParametersException
+             {
+        if (params.length() != 0) {
+            throw new InvalidFormatParametersException(
+                    "This number format doesn't support any parameters.");
+        }
+    }
+
+    /**
+     * Utility method to extract the {@link Number} from an {@link TemplateNumberModel}, and throws
+     * {@link TemplateModelException} with a standard error message if that's {@code null}. {@link TemplateNumberModel}
+     * that store {@code null} are in principle not allowed, and so are considered to be bugs in the
+     * {@link ObjectWrapper} or {@link TemplateNumberModel} implementation.
+     */
+    public static Number getNonNullNumber(TemplateNumberModel numberModel)
+            throws TemplateModelException, UnformattableValueException {
+        Number number = numberModel.getAsNumber();
+        if (number == null) {
+            throw _EvalUtil.newModelHasStoredNullException(Number.class, numberModel, null);
+        }
+        return number;
+    }
+
+    /**
+     * Utility method to extract the {@link Date} from an {@link TemplateDateModel}, and throw
+     * {@link TemplateModelException} with a standard error message if that's {@code null}. {@link TemplateDateModel}
+     * that store {@code null} are in principle not allowed, and so are considered to be bugs in the
+     * {@link ObjectWrapper} or {@link TemplateNumberModel} implementation.
+     */
+    public static Date getNonNullDate(TemplateDateModel dateModel) throws TemplateModelException {
+        Date date = dateModel.getAsDate();
+        if (date == null) {
+            throw _EvalUtil.newModelHasStoredNullException(Date.class, dateModel, null);
+        }
+        return date;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormat.java
new file mode 100644
index 0000000..54873d6
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormat.java
@@ -0,0 +1,93 @@
+/*
+ * 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.valueformat;
+
+import java.text.NumberFormat;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.arithmetic.ArithmeticEngine;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+
+/**
+ * Represents a number format; used in templates for formatting and parsing with that format. This is similar to Java's
+ * {@link NumberFormat}, but made to fit the requirements of FreeMarker. Also, it makes easier to define formats that
+ * can't be represented with Java's existing {@link NumberFormat} implementations.
+ * 
+ * <p>
+ * Implementations need not be thread-safe if the {@link TemplateNumberFormatFactory} doesn't recycle them among
+ * different {@link Environment}-s. As far as FreeMarker's concerned, instances are bound to a single
+ * {@link Environment}, and {@link Environment}-s are thread-local objects.
+ * 
+ * @since 2.3.24
+ */
+public abstract class TemplateNumberFormat extends TemplateValueFormat {
+
+    /**
+     * @param numberModel
+     *            The number to format; not {@code null}. Most implementations will just work with the return value of
+     *            {@link TemplateDateModel#getAsDate()}, but some may format differently depending on the properties of
+     *            a custom {@link TemplateDateModel} implementation.
+     *            
+     * @return The number as text, with no escaping (like no HTML escaping); can't be {@code null}.
+     * 
+     * @throws TemplateValueFormatException
+     *             If any problem occurs while parsing/getting the format. Notable subclass:
+     *             {@link UnformattableValueException}.
+     * @throws TemplateModelException
+     *             Exception thrown by the {@code dateModel} object when calling its methods.
+     */
+    public abstract String formatToPlainText(TemplateNumberModel numberModel)
+            throws TemplateValueFormatException, TemplateModelException;
+
+    /**
+     * Formats the model to markup instead of to plain text if the result markup will be more than just plain text
+     * escaped, otherwise falls back to formatting to plain text. If the markup result would be just the result of
+     * {@link #formatToPlainText(TemplateNumberModel)} escaped, it must return the {@link String} that
+     * {@link #formatToPlainText(TemplateNumberModel)} does.
+     * 
+     * <p>
+     * The implementation in {@link TemplateNumberFormat} simply calls {@link #formatToPlainText(TemplateNumberModel)}.
+     * 
+     * @return A {@link String} or a {@link TemplateMarkupOutputModel}; not {@code null}.
+     */
+    public Object format(TemplateNumberModel numberModel)
+            throws TemplateValueFormatException, TemplateModelException {
+        return formatToPlainText(numberModel);
+    }
+    
+    /**
+     * Tells if this formatter should be re-created if the locale changes.
+     */
+    public abstract boolean isLocaleBound();
+
+    /**
+     * This method is reserved for future purposes; currently it always throws {@link ParsingNotSupportedException}. We
+     * don't yet support number parsing with {@link TemplateNumberFormat}-s, because currently FTL parses strings to
+     * number with the {@link ArithmeticEngine} ({@link TemplateNumberFormat} were only introduced in 2.3.24). If it
+     * will be support, it will be similar to {@link TemplateDateFormat#parse(String, int)}.
+     */
+    public final Object parse(String s) throws TemplateValueFormatException {
+        throw new ParsingNotSupportedException("Number formats currenly don't support parsing");
+    }
+    
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormatFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormatFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormatFactory.java
new file mode 100644
index 0000000..a4cac22
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormatFactory.java
@@ -0,0 +1,67 @@
+/*
+ * 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.valueformat;
+
+import java.util.Locale;
+
+import org.apache.freemarker.core.CustomStateKey;
+import org.apache.freemarker.core.MutableProcessingConfiguration;
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.Environment;
+
+/**
+ * Factory for a certain kind of number formatting ({@link TemplateNumberFormat}). Usually a singleton (one-per-VM or
+ * one-per-{@link Configuration}), and so must be thread-safe.
+ * 
+ * @see MutableProcessingConfiguration#setCustomNumberFormats(java.util.Map)
+ * 
+ * @since 2.3.24
+ */
+public abstract class TemplateNumberFormatFactory extends TemplateValueFormatFactory {
+
+    /**
+     * Returns a formatter for the given parameters.
+     * 
+     * <p>
+     * The returned formatter can be a new instance or a reused (cached) instance. Note that {@link Environment} itself
+     * caches the returned instances, though that cache is lost with the {@link Environment} (i.e., when the top-level
+     * template execution ends), also it might flushes lot of entries if the locale or time zone is changed during
+     * template execution. So caching on the factory level is still useful, unless creating the formatters is
+     * sufficiently cheap.
+     * 
+     * @param params
+     *            The string that further describes how the format should look. For example, when the
+     *            {@link MutableProcessingConfiguration#getNumberFormat() numberFormat} is {@code "@fooBar 1, 2"}, then it will be
+     *            {@code "1, 2"} (and {@code "@fooBar"} selects the factory). The format of this string is up to the
+     *            {@link TemplateNumberFormatFactory} implementation. Not {@code null}, often an empty string.
+     * @param locale
+     *            The locale to format for. Not {@code null}. The resulting format must be bound to this locale
+     *            forever (i.e. locale changes in the {@link Environment} must not be followed).
+     * @param env
+     *            The runtime environment from which the formatting was called. This is mostly meant to be used for
+     *            {@link Environment#getCustomState(CustomStateKey)}.
+     *            
+     * @throws TemplateValueFormatException
+     *             if any problem occurs while parsing/getting the format. Notable subclasses:
+     *             {@link InvalidFormatParametersException} if the {@code params} is malformed.
+     */
+    public abstract TemplateNumberFormat get(String params, Locale locale, Environment env)
+            throws TemplateValueFormatException;
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormat.java
new file mode 100644
index 0000000..9203e5a
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormat.java
@@ -0,0 +1,42 @@
+/*
+ * 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.valueformat;
+
+/**
+ * Superclass of all value format objects; objects that convert values to strings, or parse strings.
+ * 
+ * @since 2.3.24
+ */
+public abstract class TemplateValueFormat {
+
+    /**
+     * Meant to be used in error messages to tell what format the parsed string didn't fit.
+     */
+    public abstract String getDescription();
+
+    /**
+     * The implementation in {@link TemplateValueFormat} returns {@code package.className(description)}, where
+     * description comes from {@link #getDescription()}.
+     */
+    @Override
+    public String toString() {
+        return getClass().getName() + "(" + getDescription() + ")";
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatException.java
new file mode 100644
index 0000000..dd538e6
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatException.java
@@ -0,0 +1,37 @@
+/*
+ * 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.valueformat;
+
+/**
+ * Error while getting, creating or applying {@link TemplateValueFormat}-s (including its subclasses, like
+ * {@link TemplateNumberFormat}).
+ * 
+ * @since 2.3.24
+ */
+public abstract class TemplateValueFormatException extends Exception {
+
+    public TemplateValueFormatException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public TemplateValueFormatException(String message) {
+        super(message);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatFactory.java
new file mode 100644
index 0000000..04f706e
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatFactory.java
@@ -0,0 +1,28 @@
+/*
+ * 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.valueformat;
+
+/**
+ * Superclass of all format factories.
+ * 
+ * @since 2.3.24
+ */
+public abstract class TemplateValueFormatFactory {
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UndefinedCustomFormatException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UndefinedCustomFormatException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UndefinedCustomFormatException.java
new file mode 100644
index 0000000..fa4ed3d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UndefinedCustomFormatException.java
@@ -0,0 +1,34 @@
+/*
+ * 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.valueformat;
+
+/**
+ * @since 2.3.24
+ */
+public class UndefinedCustomFormatException extends InvalidFormatStringException {
+
+    public UndefinedCustomFormatException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public UndefinedCustomFormatException(String message) {
+        super(message);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnformattableValueException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnformattableValueException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnformattableValueException.java
new file mode 100644
index 0000000..6ef3b10
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnformattableValueException.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.valueformat;
+
+import org.apache.freemarker.core.model.TemplateModel;
+
+/**
+ * Thrown when a {@link TemplateModel} can't be formatted because of the value/properties of it are outside of that the
+ * {@link TemplateValueFormat} supports. For example, a formatter may not support dates before year 1, or can't format
+ * NaN. The most often used subclass is {@link UnknownDateTypeFormattingUnsupportedException}.
+ * 
+ * @since 2.3.24
+ */
+public class UnformattableValueException extends TemplateValueFormatException {
+
+    public UnformattableValueException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public UnformattableValueException(String message) {
+        super(message);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeFormattingUnsupportedException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeFormattingUnsupportedException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeFormattingUnsupportedException.java
new file mode 100644
index 0000000..90ae4be
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeFormattingUnsupportedException.java
@@ -0,0 +1,36 @@
+/*
+ * 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.valueformat;
+
+import org.apache.freemarker.core.model.TemplateDateModel;
+
+/**
+ * Thrown when a {@link TemplateDateModel} can't be formatted because its type is {@link TemplateDateModel#UNKNOWN}.
+ * 
+ * @since 2.3.24
+ */
+public final class UnknownDateTypeFormattingUnsupportedException extends UnformattableValueException {
+
+    public UnknownDateTypeFormattingUnsupportedException() {
+        super("Can't format the date-like value because it isn't "
+                + "known if it's desired result should be a date (no time part), a time, or a date-time value.");
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeParsingUnsupportedException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeParsingUnsupportedException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeParsingUnsupportedException.java
new file mode 100644
index 0000000..ef6cca2
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeParsingUnsupportedException.java
@@ -0,0 +1,37 @@
+/*
+ * 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.valueformat;
+
+import org.apache.freemarker.core.model.TemplateDateModel;
+
+/**
+ * Thrown when a string can't be parsed to {@link TemplateDateModel}, because the provided target type is
+ * {@link TemplateDateModel#UNKNOWN}.
+ * 
+ * @since 2.3.24
+ */
+public final class UnknownDateTypeParsingUnsupportedException extends UnformattableValueException {
+
+    public UnknownDateTypeParsingUnsupportedException() {
+        super("Can't parse the string to date-like value because it isn't "
+                + "known if it's desired result should be a date (no time part), a time, or a date-time value.");
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnparsableValueException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnparsableValueException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnparsableValueException.java
new file mode 100644
index 0000000..78af935
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnparsableValueException.java
@@ -0,0 +1,38 @@
+/*
+ * 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.valueformat;
+
+/**
+ * Thrown when the content of the string that should be parsed by the {@link TemplateValueFormat} doesn't match what the
+ * format expects.
+ * 
+ * @since 2.3.24
+ */
+public class UnparsableValueException extends TemplateValueFormatException {
+
+    public UnparsableValueException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public UnparsableValueException(String message) {
+        this(message, null);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTargetTemplateValueFormatException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTargetTemplateValueFormatException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTargetTemplateValueFormatException.java
new file mode 100644
index 0000000..b4625a4
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTargetTemplateValueFormatException.java
@@ -0,0 +1,38 @@
+/*
+ * 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.valueformat.impl;
+
+import org.apache.freemarker.core.valueformat.TemplateValueFormatException;
+
+/**
+ * Can't invoke a template format that the template format refers to (typically thrown by alias template formats).
+ * 
+ * @since 2.3.24
+ */
+class AliasTargetTemplateValueFormatException extends TemplateValueFormatException {
+
+    public AliasTargetTemplateValueFormatException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public AliasTargetTemplateValueFormatException(String message) {
+        super(message);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateDateFormatFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateDateFormatFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateDateFormatFactory.java
new file mode 100644
index 0000000..a964bc2
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateDateFormatFactory.java
@@ -0,0 +1,97 @@
+/*
+ * 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.valueformat.impl;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.util._LocaleUtil;
+import org.apache.freemarker.core.util._StringUtil;
+import org.apache.freemarker.core.valueformat.TemplateDateFormat;
+import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory;
+import org.apache.freemarker.core.valueformat.TemplateFormatUtil;
+import org.apache.freemarker.core.valueformat.TemplateValueFormatException;
+
+/**
+ * Creates an alias to another format, so that the format can be referred to with a simple name in the template, rather
+ * than as a concrete pattern or other kind of format string.
+ * 
+ * @since 2.3.24
+ */
+public final class AliasTemplateDateFormatFactory extends TemplateDateFormatFactory {
+
+    private final String defaultTargetFormatString;
+    private final Map<Locale, String> localizedTargetFormatStrings;
+
+    /**
+     * @param targetFormatString
+     *            The format string this format will be an alias to.
+     */
+    public AliasTemplateDateFormatFactory(String targetFormatString) {
+        defaultTargetFormatString = targetFormatString;
+        localizedTargetFormatStrings = null;
+    }
+
+    /**
+     * @param defaultTargetFormatString
+     *            The format string this format will be an alias to if there's no locale-specific format string for the
+     *            requested locale in {@code localizedTargetFormatStrings}
+     * @param localizedTargetFormatStrings
+     *            Maps {@link Locale}-s to format strings. If the desired locale doesn't occur in the map, a less
+     *            specific locale is tried, repeatedly until only the language part remains. For example, if locale is
+     *            {@code new Locale("en", "US", "Linux")}, then these keys will be attempted untol a match is found, in
+     *            this order: {@code new Locale("en", "US", "Linux")}, {@code new Locale("en", "US")},
+     *            {@code new Locale("en")}. If there's still no matching key, the value of the
+     *            {@code targetFormatString} will be used.
+     */
+    public AliasTemplateDateFormatFactory(
+            String defaultTargetFormatString, Map<Locale, String> localizedTargetFormatStrings) {
+        this.defaultTargetFormatString = defaultTargetFormatString;
+        this.localizedTargetFormatStrings = localizedTargetFormatStrings;
+    }
+    
+    @Override
+    public TemplateDateFormat get(String params, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput,
+                                  Environment env) throws TemplateValueFormatException {
+        TemplateFormatUtil.checkHasNoParameters(params);
+        try {
+            String targetFormatString;
+            if (localizedTargetFormatStrings != null) {
+                Locale lookupLocale = locale;
+                targetFormatString = localizedTargetFormatStrings.get(lookupLocale);
+                while (targetFormatString == null
+                        && (lookupLocale = _LocaleUtil.getLessSpecificLocale(lookupLocale)) != null) {
+                    targetFormatString = localizedTargetFormatStrings.get(lookupLocale);
+                }
+            } else {
+                targetFormatString = null;
+            }
+            if (targetFormatString == null) {
+                targetFormatString = defaultTargetFormatString;
+            }
+            return env.getTemplateDateFormat(targetFormatString, dateType, locale, timeZone, zonelessInput);
+        } catch (TemplateValueFormatException e) {
+            throw new AliasTargetTemplateValueFormatException("Failed to invoke format based on target format string,  "
+                    + _StringUtil.jQuote(params) + ". Reason given: " + e.getMessage(), e);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateNumberFormatFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateNumberFormatFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateNumberFormatFactory.java
new file mode 100644
index 0000000..72e8abd
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateNumberFormatFactory.java
@@ -0,0 +1,96 @@
+/*
+ * 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.valueformat.impl;
+
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.util._LocaleUtil;
+import org.apache.freemarker.core.util._StringUtil;
+import org.apache.freemarker.core.valueformat.TemplateFormatUtil;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormat;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory;
+import org.apache.freemarker.core.valueformat.TemplateValueFormatException;
+
+/**
+ * Creates an alias to another format, so that the format can be referred to with a simple name in the template, rather
+ * than as a concrete pattern or other kind of format string.
+ * 
+ * @since 2.3.24
+ */
+public final class AliasTemplateNumberFormatFactory extends TemplateNumberFormatFactory {
+
+    private final String defaultTargetFormatString;
+    private final Map<Locale, String> localizedTargetFormatStrings;
+
+    /**
+     * @param targetFormatString
+     *            The format string this format will be an alias to
+     */
+    public AliasTemplateNumberFormatFactory(String targetFormatString) {
+        defaultTargetFormatString = targetFormatString;
+        localizedTargetFormatStrings = null;
+    }
+
+    /**
+     * @param defaultTargetFormatString
+     *            The format string this format will be an alias to if there's no locale-specific format string for the
+     *            requested locale in {@code localizedTargetFormatStrings}
+     * @param localizedTargetFormatStrings
+     *            Maps {@link Locale}-s to format strings. If the desired locale doesn't occur in the map, a less
+     *            specific locale is tried, repeatedly until only the language part remains. For example, if locale is
+     *            {@code new Locale("en", "US", "Linux")}, then these keys will be attempted untol a match is found, in
+     *            this order: {@code new Locale("en", "US", "Linux")}, {@code new Locale("en", "US")},
+     *            {@code new Locale("en")}. If there's still no matching key, the value of the
+     *            {@code targetFormatString} will be used.
+     */
+    public AliasTemplateNumberFormatFactory(
+            String defaultTargetFormatString, Map<Locale, String> localizedTargetFormatStrings) {
+        this.defaultTargetFormatString = defaultTargetFormatString;
+        this.localizedTargetFormatStrings = localizedTargetFormatStrings;
+    }
+
+    @Override
+    public TemplateNumberFormat get(String params, Locale locale, Environment env)
+            throws TemplateValueFormatException {
+        TemplateFormatUtil.checkHasNoParameters(params);
+        try {
+            String targetFormatString;
+            if (localizedTargetFormatStrings != null) {
+                Locale lookupLocale = locale;
+                targetFormatString = localizedTargetFormatStrings.get(lookupLocale);
+                while (targetFormatString == null
+                        && (lookupLocale = _LocaleUtil.getLessSpecificLocale(lookupLocale)) != null) {
+                    targetFormatString = localizedTargetFormatStrings.get(lookupLocale);
+                }
+            } else {
+                targetFormatString = null;
+            }
+            if (targetFormatString == null) {
+                targetFormatString = defaultTargetFormatString;
+            }
+            return env.getTemplateNumberFormat(targetFormatString, locale);
+        } catch (TemplateValueFormatException e) {
+            throw new AliasTargetTemplateValueFormatException("Failed to invoke format based on target format string,  "
+                    + _StringUtil.jQuote(params) + ". Reason given: " + e.getMessage(), e);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ExtendedDecimalFormatParser.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ExtendedDecimalFormatParser.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ExtendedDecimalFormatParser.java
new file mode 100644
index 0000000..dc1709c
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ExtendedDecimalFormatParser.java
@@ -0,0 +1,530 @@
+/*
+ * 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.valueformat.impl;
+
+import java.math.RoundingMode;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.ParseException;
+import java.util.Arrays;
+import java.util.Currency;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Set;
+
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * Parses a {@link DecimalFormat} pattern string to a {@link DecimalFormat} instance, with the pattern string extensions
+ * described in the Manual (see "Extended Java decimal format"). The result is a standard {@link DecimalFormat} object,
+ * but further configured according the extension part.
+ */
+class ExtendedDecimalFormatParser {
+    
+    private static final String PARAM_ROUNDING_MODE = "roundingMode";
+    private static final String PARAM_MULTIPIER = "multipier";
+    private static final String PARAM_DECIMAL_SEPARATOR = "decimalSeparator";
+    private static final String PARAM_MONETARY_DECIMAL_SEPARATOR = "monetaryDecimalSeparator";
+    private static final String PARAM_GROUP_SEPARATOR = "groupingSeparator";
+    private static final String PARAM_EXPONENT_SEPARATOR = "exponentSeparator";
+    private static final String PARAM_MINUS_SIGN = "minusSign";
+    private static final String PARAM_INFINITY = "infinity";
+    private static final String PARAM_NAN = "nan";
+    private static final String PARAM_PERCENT = "percent";
+    private static final String PARAM_PER_MILL = "perMill";
+    private static final String PARAM_ZERO_DIGIT = "zeroDigit";
+    private static final String PARAM_CURRENCY_CODE = "currencyCode";
+    private static final String PARAM_CURRENCY_SYMBOL = "currencySymbol";
+
+    private static final String PARAM_VALUE_RND_UP = "up";
+    private static final String PARAM_VALUE_RND_DOWN = "down";
+    private static final String PARAM_VALUE_RND_CEILING = "ceiling";
+    private static final String PARAM_VALUE_RND_FLOOR = "floor";
+    private static final String PARAM_VALUE_RND_HALF_DOWN = "halfDown";
+    private static final String PARAM_VALUE_RND_HALF_EVEN = "halfEven";
+    private static final String PARAM_VALUE_RND_HALF_UP = "halfUp";
+    private static final String PARAM_VALUE_RND_UNNECESSARY = "unnecessary";
+    
+    private static final HashMap<String, ? extends ParameterHandler> PARAM_HANDLERS;
+    static {
+        HashMap<String, ParameterHandler> m = new HashMap<>();
+        m.put(PARAM_ROUNDING_MODE, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String value)
+                    throws InvalidParameterValueException {
+                RoundingMode parsedValue;
+                if (value.equals(PARAM_VALUE_RND_UP)) {
+                    parsedValue = RoundingMode.UP;
+                } else if (value.equals(PARAM_VALUE_RND_DOWN)) {
+                    parsedValue = RoundingMode.DOWN;
+                } else if (value.equals(PARAM_VALUE_RND_CEILING)) {
+                    parsedValue = RoundingMode.CEILING;
+                } else if (value.equals(PARAM_VALUE_RND_FLOOR)) {
+                    parsedValue = RoundingMode.FLOOR;
+                } else if (value.equals(PARAM_VALUE_RND_HALF_DOWN)) {
+                    parsedValue = RoundingMode.HALF_DOWN;
+                } else if (value.equals(PARAM_VALUE_RND_HALF_EVEN)) {
+                    parsedValue = RoundingMode.HALF_EVEN;
+                } else if (value.equals(PARAM_VALUE_RND_HALF_UP)) {
+                    parsedValue = RoundingMode.HALF_UP;
+                } else if (value.equals(PARAM_VALUE_RND_UNNECESSARY)) {
+                    parsedValue = RoundingMode.UNNECESSARY;
+                } else {
+                    throw new InvalidParameterValueException("Should be one of: u, d, c, f, hd, he, hu, un");
+                }
+
+                parser.roundingMode = parsedValue;
+            }
+        });
+        m.put(PARAM_MULTIPIER, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String value)
+                    throws InvalidParameterValueException {
+                try {
+                    parser.multipier = Integer.valueOf(value);
+                } catch (NumberFormatException e) {
+                    throw new InvalidParameterValueException("Malformed integer.");
+                }
+            }
+        });
+        m.put(PARAM_DECIMAL_SEPARATOR, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String value)
+                    throws InvalidParameterValueException {
+                if (value.length() != 1) {
+                    throw new InvalidParameterValueException("Must contain exactly 1 character.");
+                }
+                parser.symbols.setDecimalSeparator(value.charAt(0));
+            }
+        });
+        m.put(PARAM_MONETARY_DECIMAL_SEPARATOR, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String value)
+                    throws InvalidParameterValueException {
+                if (value.length() != 1) {
+                    throw new InvalidParameterValueException("Must contain exactly 1 character.");
+                }
+                parser.symbols.setMonetaryDecimalSeparator(value.charAt(0));
+            }
+        });
+        m.put(PARAM_GROUP_SEPARATOR, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String value)
+                    throws InvalidParameterValueException {
+                if (value.length() != 1) {
+                    throw new InvalidParameterValueException("Must contain exactly 1 character.");
+                }
+                parser.symbols.setGroupingSeparator(value.charAt(0));
+            }
+        });
+        m.put(PARAM_EXPONENT_SEPARATOR, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String value)
+                    throws InvalidParameterValueException {
+                parser.symbols.setExponentSeparator(value);
+            }
+        });
+        m.put(PARAM_MINUS_SIGN, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String value)
+                    throws InvalidParameterValueException {
+                if (value.length() != 1) {
+                    throw new InvalidParameterValueException("Must contain exactly 1 character.");
+                }
+                parser.symbols.setMinusSign(value.charAt(0));
+            }
+        });
+        m.put(PARAM_INFINITY, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String value)
+                    throws InvalidParameterValueException {
+                parser.symbols.setInfinity(value);
+            }
+        });
+        m.put(PARAM_NAN, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String value)
+                    throws InvalidParameterValueException {
+                parser.symbols.setNaN(value);
+            }
+        });
+        m.put(PARAM_PERCENT, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String value)
+                    throws InvalidParameterValueException {
+                if (value.length() != 1) {
+                    throw new InvalidParameterValueException("Must contain exactly 1 character.");
+                }
+                parser.symbols.setPercent(value.charAt(0));
+            }
+        });
+        m.put(PARAM_PER_MILL, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String value)
+                    throws InvalidParameterValueException {
+                if (value.length() != 1) {
+                    throw new InvalidParameterValueException("Must contain exactly 1 character.");
+                }
+                parser.symbols.setPerMill(value.charAt(0));
+            }
+        });
+        m.put(PARAM_ZERO_DIGIT, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String value)
+                    throws InvalidParameterValueException {
+                if (value.length() != 1) {
+                    throw new InvalidParameterValueException("Must contain exactly 1 character.");
+                }
+                parser.symbols.setZeroDigit(value.charAt(0));
+            }
+        });
+        m.put(PARAM_CURRENCY_CODE, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String value)
+                    throws InvalidParameterValueException {
+                Currency currency;
+                try {
+                    currency = Currency.getInstance(value);
+                } catch (IllegalArgumentException e) {
+                    throw new InvalidParameterValueException("Not a known ISO 4217 code.");
+                }
+                parser.symbols.setCurrency(currency);
+            }
+        });
+        PARAM_HANDLERS = m;
+    }
+
+    private static final String SNIP_MARK = "[...]";
+    private static final int MAX_QUOTATION_LENGTH = 10; // Must be more than SNIP_MARK.length!
+
+    private final String src;
+    private int pos = 0;
+
+    private final DecimalFormatSymbols symbols;
+    private RoundingMode roundingMode;
+    private Integer multipier;
+
+    static DecimalFormat parse(String formatString, Locale locale) throws ParseException {
+        return new ExtendedDecimalFormatParser(formatString, locale).parse();
+    }
+
+    private DecimalFormat parse() throws ParseException {
+        String stdPattern = fetchStandardPattern();
+        skipWS();
+        parseFormatStringExtension();
+
+        DecimalFormat decimalFormat;
+        try {
+            decimalFormat = new DecimalFormat(stdPattern, symbols);
+        } catch (IllegalArgumentException e) {
+            ParseException pe = new ParseException(e.getMessage(), 0);
+            if (e.getCause() != null) {
+                try {
+                    e.initCause(e.getCause());
+                } catch (Exception e2) {
+                    // Supress
+                }
+            }
+            throw pe;
+        }
+
+        if (roundingMode != null) {
+            decimalFormat.setRoundingMode(roundingMode);
+        }
+
+        if (multipier != null) {
+            decimalFormat.setMultiplier(multipier.intValue());
+        }
+
+        return decimalFormat;
+    }
+
+    private void parseFormatStringExtension() throws ParseException {
+        int ln = src.length();
+
+        if (pos == ln) {
+            return;
+        }
+
+        String currencySymbol = null;  // Exceptional, as must be applied after "currency code"
+        fetchParamters: do {
+            int namePos = pos;
+            String name = fetchName();
+            if (name == null) {
+                throw newExpectedSgParseException("name");
+            }
+
+            skipWS();
+
+            if (!fetchChar('=')) {
+                throw newExpectedSgParseException("\"=\"");
+            }
+
+            skipWS();
+
+            int valuePos = pos;
+            String value = fetchValue();
+            if (value == null) {
+                throw newExpectedSgParseException("value");
+            }
+            int paramEndPos = pos;
+
+            ParameterHandler handler = PARAM_HANDLERS.get(name);
+            if (handler == null) {
+                if (name.equals(PARAM_CURRENCY_SYMBOL)) {
+                    currencySymbol = value;
+                } else {
+                    throw newUnknownParameterException(name, namePos);
+                }
+            } else {
+                try {
+                    handler.handle(this, value);
+                } catch (InvalidParameterValueException e) {
+                    throw newInvalidParameterValueException(name, value, valuePos, e);
+                }
+            }
+
+            skipWS();
+
+            // Optional comma
+            if (fetchChar(',')) {
+                skipWS();
+            } else {
+                if (pos == ln) {
+                    break fetchParamters;
+                }
+                if (pos == paramEndPos) {
+                    throw newExpectedSgParseException("parameter separator whitespace or comma");
+                }
+            }
+        } while (true);
+        
+        // This is brought out to here to ensure that it's applied after "currency code":
+        if (currencySymbol != null) {
+            symbols.setCurrencySymbol(currencySymbol);
+        }
+    }
+
+    private ParseException newInvalidParameterValueException(String name, String value, int valuePos,
+            InvalidParameterValueException e) {
+        return new java.text.ParseException(
+                _StringUtil.jQuote(value) + " is an invalid value for the \"" + name + "\" parameter: "
+                + e.message,
+                valuePos);
+    }
+
+    private ParseException newUnknownParameterException(String name, int namePos) throws ParseException {
+        StringBuilder sb = new StringBuilder(128);
+        sb.append("Unsupported parameter name, ").append(_StringUtil.jQuote(name));
+        sb.append(". The supported names are: ");
+        Set<String> legalNames = PARAM_HANDLERS.keySet();
+        String[] legalNameArr = legalNames.toArray(new String[legalNames.size()]);
+        Arrays.sort(legalNameArr);
+        for (int i = 0; i < legalNameArr.length; i++) {
+            if (i != 0) {
+                sb.append(", ");
+            }
+            sb.append(legalNameArr[i]);
+        }
+        return new java.text.ParseException(sb.toString(), namePos);
+    }
+
+    private void skipWS() {
+        int ln = src.length();
+        while (pos < ln && isWS(src.charAt(pos))) {
+            pos++;
+        }
+    }
+
+    private boolean fetchChar(char fetchedChar) {
+        if (pos < src.length() && src.charAt(pos) == fetchedChar) {
+            pos++;
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    private boolean isWS(char c) {
+        return c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\u00A0';
+    }
+
+    private String fetchName() throws ParseException {
+        int ln = src.length();
+        int startPos = pos;
+        boolean firstChar = true;
+        scanUntilEnd: while (pos < ln) {
+            char c = src.charAt(pos);
+            if (firstChar) {
+                if (!Character.isJavaIdentifierStart(c)) {
+                    break scanUntilEnd;
+                }
+                firstChar = false;
+            } else if (!Character.isJavaIdentifierPart(c)) {
+                break scanUntilEnd;
+            }
+            pos++;
+        }
+        return !firstChar ? src.substring(startPos, pos) : null;
+    }
+
+    private String fetchValue() throws ParseException {
+        int ln = src.length();
+        int startPos = pos;
+        char openedQuot = 0;
+        boolean needsUnescaping = false;
+        scanUntilEnd: while (pos < ln) {
+            char c = src.charAt(pos);
+            if (c == '\'' || c == '"') {
+                if (openedQuot == 0) {
+                    if (startPos != pos) {
+                        throw new java.text.ParseException(
+                                "The " + c + " character can only be used for quoting values, "
+                                        + "but it was in the middle of an non-quoted value.",
+                                pos);
+                    }
+                    openedQuot = c;
+                } else if (c == openedQuot) {
+                    if (pos + 1 < ln && src.charAt(pos + 1) == openedQuot) {
+                        pos++; // skip doubled quote (escaping)
+                        needsUnescaping = true;
+                    } else {
+                        String str = src.substring(startPos + 1, pos);
+                        pos++;
+                        return needsUnescaping ? unescape(str, openedQuot) : str;
+                    }
+                }
+            } else {
+                if (openedQuot == 0 && !Character.isJavaIdentifierPart(c)) {
+                    break scanUntilEnd;
+                }
+            }
+            pos++;
+        } // while
+        if (openedQuot != 0) {
+            throw new java.text.ParseException(
+                    "The " + openedQuot 
+                    + " quotation wasn't closed when the end of the source was reached.",
+                    pos);
+        }
+        return startPos == pos ? null : src.substring(startPos, pos);
+    }
+
+    private String unescape(String s, char openedQuot) {
+        return openedQuot == '\'' ? _StringUtil.replace(s, "\'\'", "\'") : _StringUtil.replace(s, "\"\"", "\"");
+    }
+
+    private String fetchStandardPattern() {
+        int pos = this.pos;
+        int ln = src.length();
+        int semicolonCnt = 0;
+        boolean quotedMode = false;
+        findStdPartEnd: while (pos < ln) {
+            char c = src.charAt(pos);
+            if (c == ';' && !quotedMode) {
+                semicolonCnt++;
+                if (semicolonCnt == 2) {
+                    break findStdPartEnd;
+                }
+            } else if (c == '\'') {
+                if (quotedMode) {
+                    if (pos + 1 < ln && src.charAt(pos + 1) == '\'') {
+                        // Skips "''" used for escaping "'"
+                        pos++;
+                    } else {
+                        quotedMode = false;
+                    }
+                } else {
+                    quotedMode = true;
+                }
+            }
+            pos++;
+        }
+
+        String stdFormatStr;
+        if (semicolonCnt < 2) { // We have a standard DecimalFormat string
+            // Note that "0.0;" and "0.0" gives the same result with DecimalFormat, so we leave a ';' there
+            stdFormatStr = src;
+        } else { // `pos` points to the 2nd ';'
+            int stdEndPos = pos;
+            if (src.charAt(pos - 1) == ';') { // we have a ";;"
+                // Note that ";;" is illegal in DecimalFormat, so this is backward compatible.
+                stdEndPos--;
+            }
+            stdFormatStr = src.substring(0, stdEndPos);
+        }
+
+        if (pos < ln) {
+            pos++; // Skips closing ';'
+        }
+        this.pos = pos;
+
+        return stdFormatStr;
+    }
+
+    private ExtendedDecimalFormatParser(String formatString, Locale locale) {
+        src = formatString;
+        symbols = new DecimalFormatSymbols(locale);
+    }
+
+    private ParseException newExpectedSgParseException(String expectedThing) {
+        String quotation;
+
+        // Ignore trailing WS when calculating the length:
+        int i = src.length() - 1;
+        while (i >= 0 && Character.isWhitespace(src.charAt(i))) {
+            i--;
+        }
+        int ln = i + 1;
+
+        if (pos < ln) {
+            int qEndPos = pos + MAX_QUOTATION_LENGTH;
+            if (qEndPos >= ln) {
+                quotation = src.substring(pos, ln);
+            } else {
+                quotation = src.substring(pos, qEndPos - SNIP_MARK.length()) + SNIP_MARK;
+            }
+        } else {
+            quotation = null;
+        }
+
+        return new ParseException(
+                "Expected a(n) " + expectedThing + " at position " + pos + " (0-based), but "
+                        + (quotation == null ? "reached the end of the input." : "found: " + quotation),
+                pos);
+    }
+
+    private interface ParameterHandler {
+
+        void handle(ExtendedDecimalFormatParser parser, String value)
+                throws InvalidParameterValueException;
+
+    }
+
+    private static class InvalidParameterValueException extends Exception {
+
+        private final String message;
+
+        public InvalidParameterValueException(String message) {
+            this.message = message;
+        }
+
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormat.java
new file mode 100644
index 0000000..8790d00
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormat.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.valueformat.impl;
+
+import java.util.Date;
+import java.util.TimeZone;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.util._DateUtil;
+import org.apache.freemarker.core.util._DateUtil.CalendarFieldsToDateConverter;
+import org.apache.freemarker.core.util._DateUtil.DateParseException;
+import org.apache.freemarker.core.util._DateUtil.DateToISO8601CalendarFactory;
+import org.apache.freemarker.core.util._StringUtil;
+import org.apache.freemarker.core.valueformat.InvalidFormatParametersException;
+import org.apache.freemarker.core.valueformat.TemplateDateFormat;
+import org.apache.freemarker.core.valueformat.TemplateFormatUtil;
+import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException;
+import org.apache.freemarker.core.valueformat.UnparsableValueException;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+abstract class ISOLikeTemplateDateFormat  extends TemplateDateFormat {
+    
+    private static final String XS_LESS_THAN_SECONDS_ACCURACY_ERROR_MESSAGE
+            = "Less than seconds accuracy isn't allowed by the XML Schema format";
+    private final ISOLikeTemplateDateFormatFactory factory;
+    private final Environment env;
+    protected final int dateType;
+    protected final boolean zonelessInput;
+    protected final TimeZone timeZone;
+    protected final Boolean forceUTC;
+    protected final Boolean showZoneOffset;
+    protected final int accuracy;
+
+    /**
+     * @param formatString The value of the ..._format setting, like "iso nz".
+     * @param parsingStart The index of the char in the {@code settingValue} that directly after the prefix that has
+     *     indicated the exact formatter class (like "iso" or "xs") 
+     */
+    public ISOLikeTemplateDateFormat(
+            final String formatString, int parsingStart,
+            int dateType, boolean zonelessInput,
+            TimeZone timeZone,
+            ISOLikeTemplateDateFormatFactory factory, Environment env)
+            throws InvalidFormatParametersException, UnknownDateTypeFormattingUnsupportedException {
+        this.factory = factory;
+        this.env = env;
+        if (dateType == TemplateDateModel.UNKNOWN) {
+            throw new UnknownDateTypeFormattingUnsupportedException();
+        }
+        
+        this.dateType = dateType;
+        this.zonelessInput = zonelessInput;
+        
+        final int ln = formatString.length();
+        boolean afterSeparator = false;
+        int i = parsingStart;
+        int accuracy = _DateUtil.ACCURACY_MILLISECONDS;
+        Boolean showZoneOffset = null;
+        Boolean forceUTC = Boolean.FALSE;
+        while (i < ln) {
+            final char c = formatString.charAt(i++);
+            if (c == '_' || c == ' ') {
+                afterSeparator = true;
+            } else {
+                if (!afterSeparator) {
+                    throw new InvalidFormatParametersException(
+                            "Missing space or \"_\" before \"" + c + "\" (at char pos. " + i + ").");
+                }
+                
+                switch (c) {
+                case 'h':
+                case 'm':
+                case 's':
+                    if (accuracy != _DateUtil.ACCURACY_MILLISECONDS) {
+                        throw new InvalidFormatParametersException(
+                                "Character \"" + c + "\" is unexpected as accuracy was already specified earlier "
+                                + "(at char pos. " + i + ").");
+                    }
+                    switch (c) {
+                    case 'h':
+                        if (isXSMode()) {
+                            throw new InvalidFormatParametersException(
+                                    XS_LESS_THAN_SECONDS_ACCURACY_ERROR_MESSAGE);
+                        }
+                        accuracy = _DateUtil.ACCURACY_HOURS;
+                        break;
+                    case 'm':
+                        if (i < ln && formatString.charAt(i) == 's') {
+                            i++;
+                            accuracy = _DateUtil.ACCURACY_MILLISECONDS_FORCED;
+                        } else {
+                            if (isXSMode()) {
+                                throw new InvalidFormatParametersException(
+                                        XS_LESS_THAN_SECONDS_ACCURACY_ERROR_MESSAGE);
+                            }
+                            accuracy = _DateUtil.ACCURACY_MINUTES;
+                        }
+                        break;
+                    case 's':
+                        accuracy = _DateUtil.ACCURACY_SECONDS;
+                        break;
+                    }
+                    break;
+                case 'f':
+                    if (i < ln && formatString.charAt(i) == 'u') {
+                        checkForceUTCNotSet(forceUTC);
+                        i++;
+                        forceUTC = Boolean.TRUE;
+                        break;
+                    }
+                    // Falls through
+                case 'n':
+                    if (showZoneOffset != null) {
+                        throw new InvalidFormatParametersException(
+                                "Character \"" + c + "\" is unexpected as zone offset visibility was already "
+                                + "specified earlier. (at char pos. " + i + ").");
+                    }
+                    switch (c) {
+                    case 'n':
+                        if (i < ln && formatString.charAt(i) == 'z') {
+                            i++;
+                            showZoneOffset = Boolean.FALSE;
+                        } else {
+                            throw new InvalidFormatParametersException(
+                                    "\"n\" must be followed by \"z\" (at char pos. " + i + ").");
+                        }
+                        break;
+                    case 'f':
+                        if (i < ln && formatString.charAt(i) == 'z') {
+                            i++;
+                            showZoneOffset = Boolean.TRUE;
+                        } else {
+                            throw new InvalidFormatParametersException(
+                                    "\"f\" must be followed by \"z\" (at char pos. " + i + ").");
+                        }
+                        break;
+                    }
+                    break;
+                case 'u':
+                    checkForceUTCNotSet(forceUTC);
+                    forceUTC = null;  // means UTC will be used except for zonelessInput
+                    break;
+                default:
+                    throw new InvalidFormatParametersException(
+                            "Unexpected character, " + _StringUtil.jQuote(String.valueOf(c))
+                            + ". Expected the beginning of one of: h, m, s, ms, nz, fz, u"
+                            + " (at char pos. " + i + ").");
+                } // switch
+                afterSeparator = false;
+            } // else
+        } // while
+        
+        this.accuracy = accuracy;
+        this.showZoneOffset = showZoneOffset;
+        this.forceUTC = forceUTC;
+        this.timeZone = timeZone;
+    }
+
+    private void checkForceUTCNotSet(Boolean fourceUTC) throws InvalidFormatParametersException {
+        if (fourceUTC != Boolean.FALSE) {
+            throw new InvalidFormatParametersException(
+                    "The UTC usage option was already set earlier.");
+        }
+    }
+    
+    @Override
+    public final String formatToPlainText(TemplateDateModel dateModel) throws TemplateModelException {
+        final Date date = TemplateFormatUtil.getNonNullDate(dateModel);
+        return format(
+                date,
+                dateType != TemplateDateModel.TIME,
+                dateType != TemplateDateModel.DATE,
+                showZoneOffset == null
+                        ? !zonelessInput
+                        : showZoneOffset.booleanValue(),
+                accuracy,
+                (forceUTC == null ? !zonelessInput : forceUTC.booleanValue()) ? _DateUtil.UTC : timeZone,
+                factory.getISOBuiltInCalendar(env));
+    }
+    
+    protected abstract String format(Date date,
+            boolean datePart, boolean timePart, boolean offsetPart,
+            int accuracy,
+            TimeZone timeZone,
+            DateToISO8601CalendarFactory calendarFactory);
+
+    @Override
+    @SuppressFBWarnings(value = "RC_REF_COMPARISON_BAD_PRACTICE_BOOLEAN",
+            justification = "Known to use the singleton Boolean-s only")
+    public final Date parse(String s, int dateType) throws UnparsableValueException {
+        CalendarFieldsToDateConverter calToDateConverter = factory.getCalendarFieldsToDateCalculator(env);
+        TimeZone tz = forceUTC != Boolean.FALSE ? _DateUtil.UTC : timeZone;
+        try {
+            if (dateType == TemplateDateModel.DATE) {
+                return parseDate(s, tz, calToDateConverter);
+            } else if (dateType == TemplateDateModel.TIME) {
+                return parseTime(s, tz, calToDateConverter);
+            } else if (dateType == TemplateDateModel.DATETIME) {
+                return parseDateTime(s, tz, calToDateConverter);
+            } else {
+                throw new BugException("Unexpected date type: " + dateType);
+            }
+        } catch (DateParseException e) {
+            throw new UnparsableValueException(e.getMessage(), e);
+        }
+    }
+    
+    protected abstract Date parseDate(
+            String s, TimeZone tz,
+            CalendarFieldsToDateConverter calToDateConverter) 
+            throws DateParseException;
+    
+    protected abstract Date parseTime(
+            String s, TimeZone tz,
+            CalendarFieldsToDateConverter calToDateConverter) 
+            throws DateParseException;
+    
+    protected abstract Date parseDateTime(
+            String s, TimeZone tz,
+            CalendarFieldsToDateConverter calToDateConverter) 
+            throws DateParseException;
+
+    @Override
+    public final String getDescription() {
+        switch (dateType) {
+            case TemplateDateModel.DATE: return getDateDescription();
+            case TemplateDateModel.TIME: return getTimeDescription();
+            case TemplateDateModel.DATETIME: return getDateTimeDescription();
+            default: return "<error: wrong format dateType>";
+        }
+    }
+    
+    protected abstract String getDateDescription();
+    protected abstract String getTimeDescription();
+    protected abstract String getDateTimeDescription();
+    
+    @Override
+    public final boolean isLocaleBound() {
+        return false;
+    }
+    
+    @Override
+    public boolean isTimeZoneBound() {
+        return true;
+    }
+
+    protected abstract boolean isXSMode();
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormatFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormatFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormatFactory.java
new file mode 100644
index 0000000..5db8f46
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormatFactory.java
@@ -0,0 +1,57 @@
+/*
+ * 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.valueformat.impl;
+
+import org.apache.freemarker.core.CustomStateKey;
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.util._DateUtil.CalendarFieldsToDateConverter;
+import org.apache.freemarker.core.util._DateUtil.DateToISO8601CalendarFactory;
+import org.apache.freemarker.core.util._DateUtil.TrivialCalendarFieldsToDateConverter;
+import org.apache.freemarker.core.util._DateUtil.TrivialDateToISO8601CalendarFactory;
+import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory;
+
+abstract class ISOLikeTemplateDateFormatFactory extends TemplateDateFormatFactory {
+    
+    private static final CustomStateKey<TrivialDateToISO8601CalendarFactory> DATE_TO_CAL_CONVERTER_KEY
+            = new CustomStateKey<TrivialDateToISO8601CalendarFactory>() {
+        @Override
+        protected TrivialDateToISO8601CalendarFactory create() {
+            return new TrivialDateToISO8601CalendarFactory();
+        }
+    };
+    private static final CustomStateKey<TrivialCalendarFieldsToDateConverter> CAL_TO_DATE_CONVERTER_KEY
+            = new CustomStateKey<TrivialCalendarFieldsToDateConverter>() {
+        @Override
+        protected TrivialCalendarFieldsToDateConverter create() {
+            return new TrivialCalendarFieldsToDateConverter();
+        }
+    };
+    
+    protected ISOLikeTemplateDateFormatFactory() { }
+
+    public DateToISO8601CalendarFactory getISOBuiltInCalendar(Environment env) {
+        return (DateToISO8601CalendarFactory) env.getCustomState(DATE_TO_CAL_CONVERTER_KEY);
+    }
+
+    public CalendarFieldsToDateConverter getCalendarFieldsToDateCalculator(Environment env) {
+        return (CalendarFieldsToDateConverter) env.getCustomState(CAL_TO_DATE_CONVERTER_KEY);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormat.java
new file mode 100644
index 0000000..4856ee0
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormat.java
@@ -0,0 +1,90 @@
+/*
+ * 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.valueformat.impl;
+
+import java.util.Date;
+import java.util.TimeZone;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.util._DateUtil;
+import org.apache.freemarker.core.util._DateUtil.CalendarFieldsToDateConverter;
+import org.apache.freemarker.core.util._DateUtil.DateParseException;
+import org.apache.freemarker.core.util._DateUtil.DateToISO8601CalendarFactory;
+import org.apache.freemarker.core.valueformat.InvalidFormatParametersException;
+import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException;
+
+class ISOTemplateDateFormat extends ISOLikeTemplateDateFormat {
+
+    ISOTemplateDateFormat(
+            String settingValue, int parsingStart,
+            int dateType, boolean zonelessInput,
+            TimeZone timeZone,
+            ISOLikeTemplateDateFormatFactory factory,
+            Environment env)
+            throws InvalidFormatParametersException, UnknownDateTypeFormattingUnsupportedException {
+        super(settingValue, parsingStart, dateType, zonelessInput, timeZone, factory, env);
+    }
+
+    @Override
+    protected String format(Date date, boolean datePart, boolean timePart, boolean offsetPart, int accuracy,
+            TimeZone timeZone, DateToISO8601CalendarFactory calendarFactory) {
+        return _DateUtil.dateToISO8601String(
+                date, datePart, timePart, timePart && offsetPart, accuracy, timeZone, calendarFactory);
+    }
+
+    @Override
+    protected Date parseDate(String s, TimeZone tz, CalendarFieldsToDateConverter calToDateConverter)
+            throws DateParseException {
+        return _DateUtil.parseISO8601Date(s, tz, calToDateConverter);
+    }
+
+    @Override
+    protected Date parseTime(String s, TimeZone tz, CalendarFieldsToDateConverter calToDateConverter)
+            throws DateParseException {
+        return _DateUtil.parseISO8601Time(s, tz, calToDateConverter);
+    }
+
+    @Override
+    protected Date parseDateTime(String s, TimeZone tz,
+            CalendarFieldsToDateConverter calToDateConverter) throws DateParseException {
+        return _DateUtil.parseISO8601DateTime(s, tz, calToDateConverter);
+    }
+    
+    @Override
+    protected String getDateDescription() {
+        return "ISO 8601 (subset) date";
+    }
+
+    @Override
+    protected String getTimeDescription() {
+        return "ISO 8601 (subset) time";
+    }
+
+    @Override
+    protected String getDateTimeDescription() {
+        return "ISO 8601 (subset) date-time";
+    }
+
+    @Override
+    protected boolean isXSMode() {
+        return false;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormatFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormatFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormatFactory.java
new file mode 100644
index 0000000..ddace3d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormatFactory.java
@@ -0,0 +1,56 @@
+/*
+ * 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.valueformat.impl;
+
+import java.util.Locale;
+import java.util.TimeZone;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.valueformat.InvalidFormatParametersException;
+import org.apache.freemarker.core.valueformat.TemplateDateFormat;
+import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException;
+
+/**
+ * Creates {@link TemplateDateFormat}-s that follows ISO 8601 extended format that is also compatible with the XML
+ * Schema format (as far as you don't have dates in the BC era). Examples of possible outputs: {@code
+ * "2005-11-27T15:30:00+02:00"}, {@code "2005-11-27"}, {@code "15:30:00Z"}. Note the {@code ":00"} in the time zone
+ * offset; this is not required by ISO 8601, but included for compatibility with the XML Schema format. Regarding the
+ * B.C. issue, those dates will be one year off when read back according the XML Schema format, because of a mismatch
+ * between that format and ISO 8601:2000 Second Edition.
+ */
+public final class ISOTemplateDateFormatFactory extends ISOLikeTemplateDateFormatFactory {
+    
+    public static final ISOTemplateDateFormatFactory INSTANCE = new ISOTemplateDateFormatFactory();
+
+    private ISOTemplateDateFormatFactory() {
+        // Not meant to be instantiated
+    }
+
+    @Override
+    public TemplateDateFormat get(String params, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput,
+                                  Environment env) throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatParametersException {
+        // We don't cache these as creating them is cheap (only 10% speedup of ${d?string.xs} with caching)
+        return new ISOTemplateDateFormat(
+                params, 3,
+                dateType, zonelessInput,
+                timeZone, this, env);
+    }
+
+}


[14/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/StringTemplateLoader.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/StringTemplateLoader.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/StringTemplateLoader.java
new file mode 100644
index 0000000..887bfce
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/StringTemplateLoader.java
@@ -0,0 +1,199 @@
+/*
+ * 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.templateresolver.impl;
+
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.io.StringReader;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.apache.freemarker.core.templateresolver.CacheStorage;
+import org.apache.freemarker.core.templateresolver.TemplateLoader;
+import org.apache.freemarker.core.templateresolver.TemplateLoaderSession;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingResult;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingSource;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * A {@link TemplateLoader} that uses a {@link Map} with {@code String} as its source of templates. This is similar to
+ * {@link StringTemplateLoader}, but uses {@code String} instead of {@link String}; see more details there.
+ * 
+ * <p>Note that {@link StringTemplateLoader} can't be used with a distributed (cluster-wide) {@link CacheStorage},
+ * as it produces {@link TemplateLoadingSource}-s that deliberately throw exception on serialization (because the
+ * content is only accessible within a single JVM, and is also volatile).
+ */
+// TODO JUnit tests
+public class StringTemplateLoader implements TemplateLoader {
+    
+    private static final AtomicLong INSTANCE_COUNTER = new AtomicLong();
+    
+    private final long instanceId = INSTANCE_COUNTER.get();
+    private final AtomicLong templatesRevision = new AtomicLong();
+    private final ConcurrentMap<String, ContentHolder> templates = new ConcurrentHashMap<>();
+    
+    /**
+     * Puts a template into the template loader. The name can contain slashes to denote logical directory structure, but
+     * must not start with a slash. Each template will get an unique revision number, thus replacing a template will
+     * cause the template cache to reload it (when the update delay expires).
+     * 
+     * <p>This method is thread-safe.
+     * 
+     * @param name
+     *            the name of the template.
+     * @param content
+     *            the source code of the template.
+     */
+    public void putTemplate(String name, String content) {
+        templates.put(
+                name,
+                new ContentHolder(content, new Source(instanceId, name), templatesRevision.incrementAndGet()));
+    }
+    
+    /**
+     * Removes the template with the specified name if it was added earlier.
+     * 
+     * <p>
+     * This method is thread-safe.
+     * 
+     * @param name
+     *            Exactly the key with which the template was added.
+     * 
+     * @return Whether a template was found with the given key (and hence was removed now)
+     */ 
+    public boolean removeTemplate(String name) {
+        return templates.remove(name) != null;
+    }
+    
+    @Override
+    public TemplateLoaderSession createSession() {
+        return null;
+    }
+
+    @Override
+    public TemplateLoadingResult load(String name, TemplateLoadingSource ifSourceDiffersFrom,
+            Serializable ifVersionDiffersFrom, TemplateLoaderSession session) throws IOException {
+        ContentHolder contentHolder = templates.get(name);
+        if (contentHolder == null) {
+            return TemplateLoadingResult.NOT_FOUND;
+        } else if (ifSourceDiffersFrom != null && ifSourceDiffersFrom.equals(contentHolder.source)
+                && Objects.equals(ifVersionDiffersFrom, contentHolder.version)) {
+            return TemplateLoadingResult.NOT_MODIFIED;
+        } else {
+            return new TemplateLoadingResult(
+                    contentHolder.source, contentHolder.version,
+                    new StringReader(contentHolder.content),
+                    null);
+        }
+    }
+
+    @Override
+    public void resetState() {
+        // Do nothing
+    }
+
+    /**
+     * Show class name and some details that are useful in template-not-found errors.
+     */
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append(_TemplateLoaderUtils.getClassNameForToString(this));
+        sb.append("(Map { ");
+        int cnt = 0;
+        for (String name : templates.keySet()) {
+            cnt++;
+            if (cnt != 1) {
+                sb.append(", ");
+            }
+            if (cnt > 10) {
+                sb.append("...");
+                break;
+            }
+            sb.append(_StringUtil.jQuote(name));
+            sb.append("=...");
+        }
+        if (cnt != 0) {
+            sb.append(' ');
+        }
+        sb.append("})");
+        return sb.toString();
+    }
+
+    private static class ContentHolder {
+        private final String content;
+        private final Source source;
+        private final long version;
+        
+        public ContentHolder(String content, Source source, long version) {
+            this.content = content;
+            this.source = source;
+            this.version = version;
+        }
+        
+    }
+    
+    @SuppressWarnings("serial")
+    private static class Source implements TemplateLoadingSource {
+        
+        private final long instanceId;
+        private final String name;
+        
+        public Source(long instanceId, String name) {
+            this.instanceId = instanceId;
+            this.name = name;
+        }
+    
+        @Override
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + (int) (instanceId ^ (instanceId >>> 32));
+            result = prime * result + ((name == null) ? 0 : name.hashCode());
+            return result;
+        }
+    
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) return true;
+            if (obj == null) return false;
+            if (getClass() != obj.getClass()) return false;
+            Source other = (Source) obj;
+            if (instanceId != other.instanceId) return false;
+            if (name == null) {
+                if (other.name != null) return false;
+            } else if (!name.equals(other.name)) {
+                return false;
+            }
+            return true;
+        }
+        
+        private void writeObject(ObjectOutputStream out) throws IOException {
+            throw new IOException(StringTemplateLoader.class.getName()
+                    + " sources can't be serialized, as they don't support clustering.");
+        }
+        
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/StrongCacheStorage.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/StrongCacheStorage.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/StrongCacheStorage.java
new file mode 100644
index 0000000..1d0533b
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/StrongCacheStorage.java
@@ -0,0 +1,70 @@
+/*
+ * 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.templateresolver.impl;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.templateresolver.CacheStorage;
+import org.apache.freemarker.core.templateresolver.CacheStorageWithGetSize;
+
+/**
+ * Strong cache storage is a cache storage that simply wraps a {@link Map}. It holds a strong reference to all objects
+ * it was passed, therefore prevents the cache from being purged during garbage collection. This class is always
+ * thread-safe since 2.3.24, before that if we are running on Java 5 or later.
+ *
+ * @see Configuration#getCacheStorage()
+ */
+public class StrongCacheStorage implements CacheStorage, CacheStorageWithGetSize {
+    
+    private final ConcurrentMap map = new ConcurrentHashMap();
+
+    @Override
+    public Object get(Object key) {
+        return map.get(key);
+    }
+
+    @Override
+    public void put(Object key, Object value) {
+        map.put(key, value);
+    }
+
+    @Override
+    public void remove(Object key) {
+        map.remove(key);
+    }
+    
+    /**
+     * Returns a close approximation of the number of cache entries.
+     * 
+     * @since 2.3.21
+     */
+    @Override
+    public int getSize() {
+        return map.size();
+    }
+    
+    @Override
+    public void clear() {
+        map.clear();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/TemplateLoaderBasedTemplateLookupContext.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/TemplateLoaderBasedTemplateLookupContext.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/TemplateLoaderBasedTemplateLookupContext.java
new file mode 100644
index 0000000..e9e1c00
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/TemplateLoaderBasedTemplateLookupContext.java
@@ -0,0 +1,66 @@
+/*
+ * 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.templateresolver.impl;
+
+import java.io.Serializable;
+import java.util.Locale;
+
+import org.apache.freemarker.core.templateresolver.TemplateLoader;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingResult;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingSource;
+import org.apache.freemarker.core.templateresolver.TemplateLookupContext;
+
+/**
+ * Base class for implementing a {@link TemplateLookupContext} that works with {@link TemplateLoader}-s.
+ */
+public abstract class TemplateLoaderBasedTemplateLookupContext
+        extends TemplateLookupContext<TemplateLoaderBasedTemplateLookupResult> {
+
+    private final TemplateLoadingSource cachedResultSource;
+    private final Serializable cachedResultVersion;
+
+    protected TemplateLoaderBasedTemplateLookupContext(String templateName, Locale templateLocale,
+            Object customLookupCondition, TemplateLoadingSource cachedResultSource, Serializable cachedResultVersion) {
+        super(templateName, templateLocale, customLookupCondition);
+        this.cachedResultSource = cachedResultSource;
+        this.cachedResultVersion = cachedResultVersion;
+    }
+    
+    protected TemplateLoadingSource getCachedResultSource() {
+        return cachedResultSource;
+    }
+
+    protected Serializable getCachedResultVersion() {
+        return cachedResultVersion;
+    }
+
+    @Override
+    public final TemplateLoaderBasedTemplateLookupResult createNegativeLookupResult() {
+        return TemplateLoaderBasedTemplateLookupResult.getNegativeResult();
+    }
+
+    /**
+     * Creates a positive or negative lookup result depending on {@link TemplateLoadingResult#getStatus()}.
+     */
+    protected final TemplateLoaderBasedTemplateLookupResult createLookupResult(
+            String templateSourceName, TemplateLoadingResult templateLoaderResult) {
+        return TemplateLoaderBasedTemplateLookupResult.from(templateSourceName, templateLoaderResult);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/TemplateLoaderBasedTemplateLookupResult.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/TemplateLoaderBasedTemplateLookupResult.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/TemplateLoaderBasedTemplateLookupResult.java
new file mode 100644
index 0000000..fe7a54c
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/TemplateLoaderBasedTemplateLookupResult.java
@@ -0,0 +1,124 @@
+/*
+ * 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.templateresolver.impl;
+
+import org.apache.freemarker.core.templateresolver.TemplateLoader;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingResult;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingResultStatus;
+import org.apache.freemarker.core.templateresolver.TemplateLookupResult;
+import org.apache.freemarker.core.util._NullArgumentException;
+
+/**
+ * Class of {@link TemplateLookupResult} instances created by {@link TemplateLoaderBasedTemplateLookupContext}. To
+ * invoke instances of this inside your own {@link TemplateLoaderBasedTemplateLookupContext} subclass, call
+ * {@link TemplateLoaderBasedTemplateLookupContext#createLookupResult(String, TemplateLoadingResult)} and
+ * {@link TemplateLoaderBasedTemplateLookupContext#createNegativeLookupResult()}. You should not try to invoke instances
+ * anywhere else. Also, this class deliberately can't be subclassed (except inside FreeMarker).
+ */
+public abstract class TemplateLoaderBasedTemplateLookupResult extends TemplateLookupResult {
+    
+    /** Used internally to get a not-found result (currently just a static singleton). */
+    static TemplateLoaderBasedTemplateLookupResult getNegativeResult() {
+        return NegativeTemplateLookupResult.INSTANCE;
+    }
+    
+    /** Used internally to invoke the appropriate kind of result from the parameters. */
+    static TemplateLoaderBasedTemplateLookupResult from(String templateSourceName, TemplateLoadingResult templateLoaderResult) {
+        return templateLoaderResult.getStatus() != TemplateLoadingResultStatus.NOT_FOUND
+                ? new PositiveTemplateLookupResult(templateSourceName, templateLoaderResult)
+                : getNegativeResult();
+    }
+    
+    private TemplateLoaderBasedTemplateLookupResult() {
+        //
+    }
+    
+    /**
+     * Used internally to extract the {@link TemplateLoadingResult}; {@code null} if {@link #isPositive()} is
+     * {@code false}.
+     */
+    public abstract TemplateLoadingResult getTemplateLoaderResult();
+
+    private static final class PositiveTemplateLookupResult extends TemplateLoaderBasedTemplateLookupResult {
+
+        private final String templateSourceName;
+        private final TemplateLoadingResult templateLoaderResult;
+
+        /**
+         * @param templateSourceName
+         *            The name of the matching template found. This is not necessarily the same as the template name
+         *            with which the template was originally requested. For example, one may gets a template for the
+         *            {@code "foo.ftl"} name, but due to localized lookup the template is actually loaded from
+         *            {@code "foo_de.ftl"}. Then this parameter must be {@code "foo_de.ftl"}, not {@code "foo.ftl"}. Not
+         *            {@code null}.
+         * 
+         * @param templateLoaderResult
+         *            See {@link TemplateLoader#load} to understand what that means. Not
+         *            {@code null}.
+         */
+        private PositiveTemplateLookupResult(String templateSourceName, TemplateLoadingResult templateLoaderResult) {
+            _NullArgumentException.check("templateName", templateSourceName);
+            _NullArgumentException.check("templateLoaderResult", templateLoaderResult);
+
+            this.templateSourceName = templateSourceName;
+            this.templateLoaderResult = templateLoaderResult;
+        }
+
+        @Override
+        public String getTemplateSourceName() {
+            return templateSourceName;
+        }
+
+        @Override
+        public TemplateLoadingResult getTemplateLoaderResult() {
+            return templateLoaderResult;
+        }
+
+        @Override
+        public boolean isPositive() {
+            return true;
+        }
+    }
+
+    private static final class NegativeTemplateLookupResult extends TemplateLoaderBasedTemplateLookupResult {
+        
+        private static final TemplateLoaderBasedTemplateLookupResult.NegativeTemplateLookupResult INSTANCE = new NegativeTemplateLookupResult();
+                
+        private NegativeTemplateLookupResult() {
+            // nop
+        }
+
+        @Override
+        public String getTemplateSourceName() {
+            return null;
+        }
+
+        @Override
+        public TemplateLoadingResult getTemplateLoaderResult() {
+            return null;
+        }
+
+        @Override
+        public boolean isPositive() {
+            return false;
+        }
+        
+    }
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/URLTemplateLoader.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/URLTemplateLoader.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/URLTemplateLoader.java
new file mode 100644
index 0000000..dcb9222
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/URLTemplateLoader.java
@@ -0,0 +1,229 @@
+/*
+ * 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.templateresolver.impl;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Serializable;
+import java.net.JarURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.Objects;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core._CoreLogs;
+import org.apache.freemarker.core.templateresolver.TemplateLoader;
+import org.apache.freemarker.core.templateresolver.TemplateLoaderSession;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingResult;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingSource;
+import org.apache.freemarker.core.templateresolver.TemplateLookupStrategy;
+import org.slf4j.Logger;
+
+/**
+ * This is an abstract template loader that can load templates whose location can be described by an URL. Subclasses
+ * only need to override the {@link #getURL(String)}, {@link #extractNegativeResult(URLConnection)}, and perhaps the
+ * {@link #prepareConnection(URLConnection)} method.
+ */
+// TODO JUnit test (including implementing a HTTP-based template loader to test the new protected methods)
+public abstract class URLTemplateLoader implements TemplateLoader {
+    
+    private static final Logger LOG = _CoreLogs.TEMPLATE_RESOLVER;
+    
+    private Boolean urlConnectionUsesCaches = false;
+    
+    /**
+     * Getter pair of {@link #setURLConnectionUsesCaches(Boolean)}.
+     * 
+     * @since 2.3.21
+     */
+    public Boolean getURLConnectionUsesCaches() {
+        return urlConnectionUsesCaches;
+    }
+
+    /**
+     * Sets if {@link URLConnection#setUseCaches(boolean)} will be called, and with what value. By default this is
+     * {@code false}, because FreeMarker has its own template cache with its own update delay setting
+     * ({@link Configuration#getTemplateUpdateDelayMilliseconds()}). If this is set to {@code null},
+     * {@link URLConnection#setUseCaches(boolean)} won't be called.
+     */
+    public void setURLConnectionUsesCaches(Boolean urlConnectionUsesCaches) {
+        this.urlConnectionUsesCaches = urlConnectionUsesCaches;
+    }
+
+    @Override
+    public TemplateLoaderSession createSession() {
+        return null;
+    }
+
+    @Override
+    public TemplateLoadingResult load(String name, TemplateLoadingSource ifSourceDiffersFrom,
+            Serializable ifVersionDiffersFrom, TemplateLoaderSession session) throws IOException {
+        URL url = getURL(name);
+        if (url == null) {
+            return TemplateLoadingResult.NOT_FOUND;             
+        }
+        
+        URLConnection conn = url.openConnection();
+        Boolean urlConnectionUsesCaches = getURLConnectionUsesCaches();
+        if (urlConnectionUsesCaches != null) {
+            conn.setUseCaches(urlConnectionUsesCaches);
+        }
+        
+        prepareConnection(conn);
+        conn.connect();
+        
+        InputStream inputStream = null;
+        Long version;
+        URLTemplateLoadingSource source;
+        try {
+            TemplateLoadingResult negativeResult = extractNegativeResult(conn);
+            if (negativeResult != null) {
+                return negativeResult;
+            }
+            
+            // To prevent clustering issues, getLastModified(fallbackToJarLMD=false)
+            long lmd = getLastModified(conn, false);
+            version = lmd != -1 ? lmd : null;
+            
+            source = new URLTemplateLoadingSource(url);
+            
+            if (ifSourceDiffersFrom != null && ifSourceDiffersFrom.equals(source)
+                    && Objects.equals(ifVersionDiffersFrom, version)) {
+                return TemplateLoadingResult.NOT_MODIFIED;
+            }
+            
+            inputStream = conn.getInputStream();
+        } catch (Throwable e) {
+            try {
+                if (inputStream == null) {
+                    // There's no URLConnection.close(), so we do this hack. In case of HttpURLConnection we could call
+                    // disconnect(), but that's perhaps too aggressive.
+                    conn.getInputStream().close();
+                }
+            } catch (IOException e2) {
+                LOG.debug("Failed to close connection inputStream", e2);
+            }
+            throw e;
+        }
+        return new TemplateLoadingResult(source, version, inputStream, null);
+    }
+
+    @Override
+    public void resetState() {
+        // Do nothing
+    }
+
+    /**
+     * {@link URLConnection#getLastModified()} with JDK bug workarounds. Because of JDK-6956385, for files inside a jar,
+     * it returns the last modification time of the jar itself, rather than the last modification time of the file
+     * inside the jar.
+     * 
+     * @param fallbackToJarLMD
+     *            Tells if the file is in side jar, then we should return the last modification time of the jar itself,
+     *            or -1 (to work around JDK-6956385).
+     */
+    public static long getLastModified(URLConnection conn, boolean fallbackToJarLMD) throws IOException {
+        if (conn instanceof JarURLConnection) {
+            // There is a bug in sun's jar url connection that causes file handle leaks when calling getLastModified()
+            // (see https://bugs.openjdk.java.net/browse/JDK-6956385).
+            // Since the time stamps of jar file contents can't vary independent from the jar file timestamp, just use
+            // the jar file timestamp
+            if (fallbackToJarLMD) {
+                URL jarURL = ((JarURLConnection) conn).getJarFileURL();
+                if (jarURL.getProtocol().equals("file")) {
+                    // Return the last modified time of the underlying file - saves some opening and closing
+                    return new File(jarURL.getFile()).lastModified();
+                } else {
+                    // Use the URL mechanism
+                    URLConnection jarConn = null;
+                    try {
+                        jarConn = jarURL.openConnection();
+                        return jarConn.getLastModified();
+                    } finally {
+                        try {
+                            if (jarConn != null) {
+                                jarConn.getInputStream().close();
+                            }
+                        } catch (IOException e) {
+                            LOG.warn("Failed to close URL connection for: {}", conn, e);
+                        }
+                    }
+                }
+            } else {
+                return -1;
+            }
+        } else {
+          return conn.getLastModified();
+        }
+    }
+
+    /**
+     * Given a template name (plus potential locale decorations) retrieves an URL that points the template source.
+     * 
+     * @param name
+     *            the name of the sought template (including the locale decorations, or other decorations the
+     *            {@link TemplateLookupStrategy} uses).
+     *            
+     * @return An URL that points to the template source, or null if it can be determined that the template source does
+     *         not exist. For many implementations the existence of the template can't be decided at this point, and you
+     *         rely on {@link #extractNegativeResult(URLConnection)} instead.
+     */
+    protected abstract URL getURL(String name);
+
+    /**
+     * Called before the resource if read, checks if we can immediately return a {@link TemplateLoadingResult#NOT_FOUND}
+     * or {@link TemplateLoadingResult#NOT_MODIFIED}, or throw an {@link IOException}. For example, for a HTTP-based
+     * storage, the HTTP response status 404 could result in {@link TemplateLoadingResult#NOT_FOUND}, 304 in
+     * {@link TemplateLoadingResult#NOT_MODIFIED}, and some others, like 500 in throwing an {@link IOException}.
+     * 
+     * <p>Some
+     * implementations rely on {@link #getURL(String)} returning {@code null} when a template is missing, in which case
+     * this method is certainly not applicable.
+     */
+    protected abstract TemplateLoadingResult extractNegativeResult(URLConnection conn) throws IOException;
+
+    /**
+     * Called before anything that causes the connection to actually build up. This is where
+     * {@link URLConnection#setIfModifiedSince(long)} and such can be called if someone overrides this.
+     * The default implementation in {@link URLTemplateLoader} does nothing. 
+     */
+    protected void prepareConnection(URLConnection conn) {
+        // Does nothing
+    }
+
+    /**
+     * Can be used by subclasses to canonicalize URL path prefixes.
+     * @param prefix the path prefix to canonicalize
+     * @return the canonicalized prefix. All backslashes are replaced with
+     * forward slashes, and a trailing slash is appended if the original
+     * prefix wasn't empty and didn't already end with a slash.
+     */
+    protected static String canonicalizePrefix(String prefix) {
+        // make it foolproof
+        prefix = prefix.replace('\\', '/');
+        // ensure there's a trailing slash
+        if (prefix.length() > 0 && !prefix.endsWith("/")) {
+            prefix += "/";
+        }
+        return prefix;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/URLTemplateLoadingSource.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/URLTemplateLoadingSource.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/URLTemplateLoadingSource.java
new file mode 100644
index 0000000..cbc1080
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/URLTemplateLoadingSource.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.templateresolver.impl;
+
+import java.net.URL;
+
+import org.apache.freemarker.core.templateresolver.TemplateLoadingSource;
+import org.apache.freemarker.core.util._NullArgumentException;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+@SuppressWarnings("serial")
+public class URLTemplateLoadingSource implements TemplateLoadingSource {
+
+    private final URL url;
+
+    public URLTemplateLoadingSource(URL url) {
+        _NullArgumentException.check("url", url);
+        this.url = url;
+    }
+
+    public URL getUrl() {
+        return url;
+    }
+
+    @Override
+    public int hashCode() {
+        return url.hashCode();
+    }
+
+    @Override
+    @SuppressFBWarnings("EQ_CHECK_FOR_OPERAND_NOT_COMPATIBLE_WITH_THIS")
+    public boolean equals(Object obj) {
+        return url.equals(obj);
+    }
+
+    @Override
+    public String toString() {
+        return url.toString();
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/_TemplateLoaderUtils.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/_TemplateLoaderUtils.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/_TemplateLoaderUtils.java
new file mode 100644
index 0000000..7511fa3
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/_TemplateLoaderUtils.java
@@ -0,0 +1,43 @@
+/*
+ * 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.templateresolver.impl;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.templateresolver.TemplateLoader;
+
+/**
+ * For internal use only; don't depend on this, there's no backward compatibility guarantee at all!
+ * This class is to work around the lack of module system in Java, i.e., so that other FreeMarker packages can
+ * access things inside this package that users shouldn't. 
+ */ 
+public final class _TemplateLoaderUtils {
+
+    private _TemplateLoaderUtils() {
+        // Not meant to be instantiated
+    }
+
+    public static String getClassNameForToString(TemplateLoader templateLoader) {
+        final Class<? extends TemplateLoader> tlClass = templateLoader.getClass();
+        final Package tlPackage = tlClass.getPackage();
+        return tlPackage == Configuration.class.getPackage() || tlPackage == TemplateLoader.class.getPackage()
+                ? tlClass.getSimpleName() : tlClass.getName();
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/package.html
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/package.html b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/package.html
new file mode 100644
index 0000000..c595bd8
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/package.html
@@ -0,0 +1,26 @@
+<!--
+  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.
+-->
+<html>
+<head>
+</head>
+<body>
+<p>Template lookup, loading and caching: Standard implementations. This package is part of the
+published API, that is, user code can safely depend on it.</p>
+</body>
+</html>

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/package.html
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/package.html b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/package.html
new file mode 100644
index 0000000..dd01586
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/package.html
@@ -0,0 +1,25 @@
+<!--
+  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.
+-->
+<html>
+<head>
+</head>
+<body>
+<p>Template lookup, loading, and caching: Base classes/interfaces</p>
+</body>
+</html>

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/BugException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/BugException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/BugException.java
new file mode 100644
index 0000000..399778a
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/BugException.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.util;
+
+/**
+ * An unexpected state was reached that is certainly caused by a bug in FreeMarker.
+ * 
+ * @since 2.3.21
+ */
+public class BugException extends RuntimeException {
+
+    private static final String COMMON_MESSAGE
+        = "A bug was detected in FreeMarker; please report it with stack-trace";
+
+    public BugException() {
+        this((Throwable) null);
+    }
+
+    public BugException(String message) {
+        this(message, null);
+    }
+
+    public BugException(Throwable cause) {
+        super(COMMON_MESSAGE, cause);
+    }
+
+    public BugException(String message, Throwable cause) {
+        super(COMMON_MESSAGE + ": " + message, cause);
+    }
+    
+    public BugException(int value) {
+        this(String.valueOf(value));
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/CaptureOutput.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/CaptureOutput.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/CaptureOutput.java
new file mode 100644
index 0000000..93109e0
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/CaptureOutput.java
@@ -0,0 +1,147 @@
+/*
+ * 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.util;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Map;
+
+import org.apache.freemarker.core.Environment;
+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.model.TemplateTransformModel;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+
+/**
+ * A transform that captures the output of a block of FTL code and stores that in a variable.
+ *
+ * <p>As this transform is initially present in the shared variable set, you can always
+ * access it from the templates:</p>
+ *
+ * <pre>
+ * &lt;@capture_output var="captured"&gt;
+ *   ...
+ * &lt;/@capture_output&gt;
+ * </pre>
+ *
+ * <p>And later in the template you can use the captured output:</p>
+ *
+ * ${captured}
+ *
+ * <p>This transform requires one of three parameters: <code>var</code>, <code>local</code>, or <code>global</code>.
+ * Each of them specifies the name of the variable that stores the captured output, but the first creates a
+ * variable in a name-space (as &lt;#assign&gt;), the second creates a macro-local variable (as &lt;#local&gt;),
+ * and the last creates a global variable (as &lt;#global&gt;).
+ * </p>
+ * <p>In the case of an assignment within a namespace, there is an optional parameter
+ * <code>namespace</code> that indicates in which namespace to do the assignment.
+ * if this is omitted, the current namespace is used, and this will be, by far, the most
+ * common usage pattern.</p>
+ *
+ * @deprecated Use block-assignments instead, like <code>&lt;assign x&gt;...&lt;/assign&gt;</code>.
+ */
+@Deprecated
+public class CaptureOutput implements TemplateTransformModel {
+
+    @Override
+    public Writer getWriter(final Writer out, final Map args) throws TemplateModelException {
+        String errmsg = "Must specify the name of the variable in "
+                + "which to capture the output with the 'var' or 'local' or 'global' parameter.";
+        if (args == null) throw new TemplateModelException(errmsg);
+
+        boolean local = false, global = false;
+        final TemplateModel nsModel = (TemplateModel) args.get("namespace");
+        Object varNameModel = args.get("var");
+        if (varNameModel == null) {
+            varNameModel = args.get("local");
+            if (varNameModel == null) {
+                varNameModel = args.get("global");
+                global = true;
+            } else {
+                local = true;
+            }
+            if (varNameModel == null) {
+                throw new TemplateModelException(errmsg);
+            }
+        }
+        if (args.size() == 2) {
+            if (nsModel == null) {
+                throw new TemplateModelException("Second parameter can only be namespace");
+            }
+            if (local) {
+                throw new TemplateModelException("Cannot specify namespace for a local assignment");
+            }
+            if (global) {
+                throw new TemplateModelException("Cannot specify namespace for a global assignment");
+            }
+            if (!(nsModel instanceof Environment.Namespace)) {
+                throw new TemplateModelException("namespace parameter does not specify a namespace. It is a " + nsModel.getClass().getName());
+            }
+        } else if (args.size() != 1) throw new TemplateModelException(
+                "Bad parameters. Use only one of 'var' or 'local' or 'global' parameters.");
+
+        if (!(varNameModel instanceof TemplateScalarModel)) {
+            throw new TemplateModelException("'var' or 'local' or 'global' parameter doesn't evaluate to a string");
+        }
+        final String varName = ((TemplateScalarModel) varNameModel).getAsString();
+        if (varName == null) {
+            throw new TemplateModelException("'var' or 'local' or 'global' parameter evaluates to null string");
+        }
+
+        final StringBuilder buf = new StringBuilder();
+        final Environment env = Environment.getCurrentEnvironment();
+        final boolean localVar = local;
+        final boolean globalVar = global;
+
+        return new Writer() {
+
+            @Override
+            public void write(char cbuf[], int off, int len) {
+                buf.append(cbuf, off, len);
+            }
+
+            @Override
+            public void flush() throws IOException {
+                out.flush();
+            }
+
+            @Override
+            public void close() throws IOException {
+                SimpleScalar result = new SimpleScalar(buf.toString());
+                try {
+                    if (localVar) {
+                        env.setLocalVariable(varName, result);
+                    } else if (globalVar) {
+                        env.setGlobalVariable(varName, result);
+                    } else {
+                        if (nsModel == null) {
+                            env.setVariable(varName, result);
+                        } else {
+                            ((Environment.Namespace) nsModel).put(varName, result);
+                        }
+                    }
+                } catch (java.lang.IllegalStateException ise) { // if somebody uses 'local' outside a macro
+                    throw new IOException("Could not set variable " + varName + ": " + ise.getMessage());
+                }
+            }
+        };
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/CommonBuilder.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/CommonBuilder.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/CommonBuilder.java
new file mode 100644
index 0000000..11aab33
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/CommonBuilder.java
@@ -0,0 +1,35 @@
+/*
+ * 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.util;
+
+import org.apache.freemarker.core.ConfigurationException;
+
+/**
+ * Interface of builders (used for implementing the builder pattern).
+ */
+public interface CommonBuilder<ProductT> {
+
+    /**
+     * Creates an instance of the product class. This is usually a new instance, though if the product is stateless,
+     * it's possibly a shared object instead of a new one.
+     */
+    ProductT build() throws ConfigurationException;
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/DeepUnwrap.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/DeepUnwrap.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/DeepUnwrap.java
new file mode 100644
index 0000000..d2e361e
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/DeepUnwrap.java
@@ -0,0 +1,153 @@
+/*
+ * 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.util;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.model.AdapterTemplateModel;
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.WrapperTemplateModel;
+
+/**
+ * Utility methods for unwrapping {@link TemplateModel}-s.
+ */
+public class DeepUnwrap {
+    private static final Class OBJECT_CLASS = Object.class;
+    /**
+     * Unwraps {@link TemplateModel}-s recursively.
+     * The converting of the {@link TemplateModel} object happens with the following rules:
+     * <ol>
+     *   <li>If the object implements {@link AdapterTemplateModel}, then the result
+     *       of {@link AdapterTemplateModel#getAdaptedObject(Class)} for <tt>Object.class</tt> is returned.
+     *   <li>If the object implements {@link WrapperTemplateModel}, then the result
+     *       of {@link WrapperTemplateModel#getWrappedObject()} is returned.
+     *   <li>If the object is identical to the null model of the current object 
+     *       wrapper, null is returned. 
+     *   <li>If the object implements {@link TemplateScalarModel}, then the result
+     *       of {@link TemplateScalarModel#getAsString()} is returned.
+     *   <li>If the object implements {@link TemplateNumberModel}, then the result
+     *       of {@link TemplateNumberModel#getAsNumber()} is returned.
+     *   <li>If the object implements {@link TemplateDateModel}, then the result
+     *       of {@link TemplateDateModel#getAsDate()} is returned.
+     *   <li>If the object implements {@link TemplateBooleanModel}, then the result
+     *       of {@link TemplateBooleanModel#getAsBoolean()} is returned.
+     *   <li>If the object implements {@link TemplateSequenceModel} or
+     *       {@link TemplateCollectionModel}, then a <code>java.util.ArrayList</code> is
+     *       constructed from the subvariables, and each subvariable is unwrapped with
+     *       the rules described here (recursive unwrapping).
+     *   <li>If the object implements {@link TemplateHashModelEx}, then a
+     *       <code>java.util.HashMap</code> is constructed from the subvariables, and each
+     *       subvariable is unwrapped with the rules described here (recursive unwrapping).
+     *   <li>Throw a <code>TemplateModelException</code>, because it doesn't know how to
+     *       unwrap the object.
+     * </ol>
+     */
+    public static Object unwrap(TemplateModel model) throws TemplateModelException {
+        return unwrap(model, false);
+    }
+
+    /**
+     * Same as {@link #unwrap(TemplateModel)}, but it doesn't throw exception 
+     * if it doesn't know how to unwrap the model, but rather returns it as-is.
+     * @since 2.3.14
+     */
+    public static Object permissiveUnwrap(TemplateModel model) throws TemplateModelException {
+        return unwrap(model, true);
+    }
+    
+    private static Object unwrap(TemplateModel model, boolean permissive) throws TemplateModelException {
+        Environment env = Environment.getCurrentEnvironment();
+        TemplateModel nullModel = null;
+        if (env != null) {
+            ObjectWrapper wrapper = env.getObjectWrapper();
+            if (wrapper != null) {
+                nullModel = wrapper.wrap(null);
+            }
+        }
+        return unwrap(model, nullModel, permissive);
+    }
+
+    private static Object unwrap(TemplateModel model, TemplateModel nullModel, boolean permissive) throws TemplateModelException {
+        if (model instanceof AdapterTemplateModel) {
+            return ((AdapterTemplateModel) model).getAdaptedObject(OBJECT_CLASS);
+        }
+        if (model instanceof WrapperTemplateModel) {
+            return ((WrapperTemplateModel) model).getWrappedObject();
+        }
+        if (model == nullModel) {
+            return null;
+        }
+        if (model instanceof TemplateScalarModel) {
+            return ((TemplateScalarModel) model).getAsString();
+        }
+        if (model instanceof TemplateNumberModel) {
+            return ((TemplateNumberModel) model).getAsNumber();
+        }
+        if (model instanceof TemplateDateModel) {
+            return ((TemplateDateModel) model).getAsDate();
+        }
+        if (model instanceof TemplateBooleanModel) {
+            return Boolean.valueOf(((TemplateBooleanModel) model).getAsBoolean());
+        }
+        if (model instanceof TemplateSequenceModel) {
+            TemplateSequenceModel seq = (TemplateSequenceModel) model;
+            ArrayList list = new ArrayList(seq.size());
+            for (int i = 0; i < seq.size(); ++i) {
+                list.add(unwrap(seq.get(i), nullModel, permissive));
+            }
+            return list;
+        }
+        if (model instanceof TemplateCollectionModel) {
+            TemplateCollectionModel coll = (TemplateCollectionModel) model;
+            ArrayList list = new ArrayList();
+            TemplateModelIterator it = coll.iterator();            
+            while (it.hasNext()) {
+                list.add(unwrap(it.next(), nullModel, permissive));
+            }
+            return list;
+        }
+        if (model instanceof TemplateHashModelEx) {
+            TemplateHashModelEx hash = (TemplateHashModelEx) model;
+            HashMap map = new HashMap();
+            TemplateModelIterator keys = hash.keys().iterator();
+            while (keys.hasNext()) {
+                String key = (String) unwrap(keys.next(), nullModel, permissive); 
+                map.put(key, unwrap(hash.get(key), nullModel, permissive));
+            }
+            return map;
+        }
+        if (permissive) {
+            return model;
+        }
+        throw new TemplateModelException("Cannot deep-unwrap model of type " + model.getClass().getName());
+    }
+}
\ No newline at end of file


[38/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ConfigurationException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ConfigurationException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ConfigurationException.java
new file mode 100644
index 0000000..5b61cca
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ConfigurationException.java
@@ -0,0 +1,37 @@
+/*
+ * 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;
+
+/**
+ * Error while configuring FreeMarker.
+ */
+@SuppressWarnings("serial")
+public class ConfigurationException extends RuntimeException {
+
+    public ConfigurationException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public ConfigurationException(String message) {
+        super(message);
+    }
+
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ConfigurationSettingValueException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ConfigurationSettingValueException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ConfigurationSettingValueException.java
new file mode 100644
index 0000000..3ed6512
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ConfigurationSettingValueException.java
@@ -0,0 +1,86 @@
+/*
+ * 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.util.Date;
+
+import org.apache.freemarker.core.Configuration.ExtendableBuilder;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * Thrown by {@link ExtendableBuilder#setSetting(String, String)}; The setting name was recognized, but its value
+ * couldn't be parsed or the setting couldn't be set for some other reason. This exception should always have a
+ * cause exception.
+ */
+@SuppressWarnings("serial")
+public class ConfigurationSettingValueException extends ConfigurationException {
+
+    public ConfigurationSettingValueException(String name, String value, Throwable cause) {
+        this(name, value, true, null, cause);
+    }
+
+    public ConfigurationSettingValueException(String name, String value, String reason) {
+        this(name, value, true, reason, null);
+    }
+
+    /**
+     * @param name
+     *         The name of the setting
+     * @param value
+     *         The value of the setting
+     * @param showValue
+     *         Whether the value of the setting should be shown in the error message. Set to {@code false} if you want
+     *         to avoid {@link #toString()}-ing the {@code value}.
+     * @param reason
+     *         The explanation of why setting the setting has failed; maybe {@code null}, especially if you have a cause
+     *         exception anyway.
+     * @param cause
+     *         The cause exception of this exception (why setting the setting was failed)
+     */
+    public ConfigurationSettingValueException(String name, Object value, boolean showValue, String reason,
+            Throwable cause) {
+        super(
+                createMessage(
+                    name, value, true,
+                    reason != null ? ", because: " : (cause != null ? "; see cause exception." : null),
+                    reason),
+                cause);
+    }
+
+    private static String createMessage(String name, Object value, boolean showValue, String detail1, String detail2) {
+        StringBuilder sb = new StringBuilder(64);
+        sb.append("Failed to set FreeMarker configuration setting ").append(_StringUtil.jQuote(name));
+        if (showValue) {
+            sb.append(" to value ")
+                    .append(
+                            value instanceof Number || value instanceof Boolean || value instanceof Date ? value
+                            : _StringUtil.jQuote(value));
+        } else {
+            sb.append(" to the specified value");
+        }
+        if (detail1 != null) {
+            sb.append(detail1);
+        }
+        if (detail2 != null) {
+            sb.append(detail2);
+        }
+        return sb.toString();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/CustomStateKey.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/CustomStateKey.java b/freemarker-core/src/main/java/org/apache/freemarker/core/CustomStateKey.java
new file mode 100644
index 0000000..fedf096
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/CustomStateKey.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;
+
+/**
+ * Used with {@link CustomStateScope}-s; each subclass must have exactly one instance, which should be stored in
+ * a static final field. So the usual usage is like this:
+ *
+ * <pre>
+ *     static final CustomStateKey MY_STATE = new CustomStateKey() {
+ *         &#x40;Override
+ *         protected Object create() {
+ *             return new ...;
+ *         }
+ *     };
+ * </pre>
+ */
+public abstract class CustomStateKey<T> {
+
+    /**
+     * This will be invoked when the state for this {@link CustomStateKey} is get via {@link
+     * CustomStateScope#getCustomState(CustomStateKey)}, but it doesn't yet exists in the given scope. Then the created
+     * object will be stored in the scope and then it's returned. Must not return {@code null}.
+     */
+    protected abstract T create();
+
+    /**
+     * Does identity comparison (like operator {@code ==}).
+     */
+    @Override
+    final public boolean equals(Object o) {
+        return o == this;
+    }
+
+    /**
+     * Returns {@link Object#hashCode()}.
+     */
+    @Override
+    final public int hashCode() {
+        return super.hashCode();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/CustomStateScope.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/CustomStateScope.java b/freemarker-core/src/main/java/org/apache/freemarker/core/CustomStateScope.java
new file mode 100644
index 0000000..4067823
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/CustomStateScope.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+/**
+ * An object that's a scope that can store custom state objects.
+ */
+public interface CustomStateScope {
+
+    /**
+     * Gets the custom state belonging to the key, automatically creating it if it doesn't yet exists in the scope.
+     * If the scope is {@link Configuration} or {@link Template}, then this method is thread safe. If the scope is
+     * {@link Environment}, then this method is not thread safe ({@link Environment} is not thread safe either).
+     */
+    <T> T getCustomState(CustomStateKey<T> customStateKey);
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/DirectiveCallPlace.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/DirectiveCallPlace.java b/freemarker-core/src/main/java/org/apache/freemarker/core/DirectiveCallPlace.java
new file mode 100644
index 0000000..5793ad3
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/DirectiveCallPlace.java
@@ -0,0 +1,137 @@
+/*
+ * 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.util.IdentityHashMap;
+
+import org.apache.freemarker.core.model.TemplateDirectiveModel;
+import org.apache.freemarker.core.model.TemplateTransformModel;
+import org.apache.freemarker.core.util.ObjectFactory;
+
+/**
+ * Gives information about the place where a directive is called from, also lets you attach a custom data object to that
+ * place. Each directive call in a template has its own {@link DirectiveCallPlace} object (even when they call the same
+ * directive with the same parameters). The life cycle of the {@link DirectiveCallPlace} object is bound to the
+ * {@link Template} object that contains the directive call. Hence, the {@link DirectiveCallPlace} object and the custom
+ * data you put into it is cached together with the {@link Template} (and templates are normally cached - see
+ * {@link Configuration#getTemplate(String)}). The custom data is normally initialized on demand, that is, when the
+ * directive call is first executed, via {@link #getOrCreateCustomData(Object, ObjectFactory)}.
+ * 
+ * <p>
+ * Currently this method doesn't give you access to the {@link Template} object, because it's probable that future
+ * versions of FreeMarker will be able to use the same parsed representation of a "file" for multiple {@link Template}
+ * objects. Then the call place will be bound to the parsed representation, not to the {@link Template} objects that are
+ * based on it.
+ * 
+ * <p>
+ * <b>Don't implement this interface yourself</b>, as new methods can be added to it any time! It's only meant to be
+ * implemented by the FreeMarker core.
+ * 
+ * <p>
+ * This interface is currently only used for custom directive calls (that is, a {@code <@...>} that calls a
+ * {@link TemplateDirectiveModel}, {@link TemplateTransformModel}, or a macro).
+ * 
+ * @see Environment#getCurrentDirectiveCallPlace()
+ * 
+ * @since 2.3.22
+ */
+public interface DirectiveCallPlace {
+
+    /**
+     * The 1-based column number of the first character of the directive call in the template source code, or -1 if it's
+     * not known.
+     */
+    int getBeginColumn();
+
+    /**
+     * The 1-based line number of the first character of the directive call in the template source code, or -1 if it's
+     * not known.
+     */
+    int getBeginLine();
+
+    /**
+     * The 1-based column number of the last character of the directive call in the template source code, or -1 if it's
+     * not known. If the directive has an end-tag ({@code </...@...>}), then it points to the last character of that.
+     */
+    int getEndColumn();
+
+    /**
+     * The 1-based line number of the last character of the directive call in the template source code, or -1 if it's
+     * not known. If the directive has an end-tag ({@code </...@...>}), then it points to the last character of that.
+     */
+    int getEndLine();
+
+    /**
+     * Returns the custom data, or if that's {@code null}, then it creates and stores it in an atomic operation then
+     * returns it. This method is thread-safe, however, it doesn't ensure thread safe (like synchronized) access to the
+     * custom data itself. See the top-level documentation of {@link DirectiveCallPlace} to understand the scope and
+     * life-cycle of the custom data. Be sure that the custom data only depends on things that get their final value
+     * during template parsing, not on runtime settings.
+     * 
+     * <p>
+     * This method will block other calls while the {@code objectFactory} is executing, thus, the object will be
+     * <em>usually</em> created only once, even if multiple threads request the value when it's still {@code null}. It
+     * doesn't stand though when {@code providerIdentity} mismatches occur (see later). Furthermore, then it's also
+     * possible that multiple objects created by the same {@link ObjectFactory} will be in use on the same time, because
+     * of directive executions already running in parallel, and because of memory synchronization delays (hardware
+     * dependent) between the threads.
+     * 
+     * @param providerIdentity
+     *            This is usually the class of the {@link TemplateDirectiveModel} that creates (and uses) the custom
+     *            data, or if you are using your own class for the custom data object (as opposed to a class from some
+     *            more generic API), then that class. This is needed as the same call place might calls different
+     *            directives depending on runtime conditions, and so it must be ensured that these directives won't
+     *            accidentally read each other's custom data, ending up with class cast exceptions or worse. In the
+     *            current implementation, if there's a {@code providerIdentity} mismatch (means, the
+     *            {@code providerIdentity} object used when the custom data was last set isn't the exactly same object
+     *            as the one provided with the parameter now), the previous custom data will be just ignored as if it
+     *            was {@code null}. So if multiple directives that use the custom data feature use the same call place,
+     *            the caching of the custom data can be inefficient, as they will keep overwriting each other's custom
+     *            data. (In a more generic implementation the {@code providerIdentity} would be a key in a
+     *            {@link IdentityHashMap}, but then this feature would be slower, while {@code providerIdentity}
+     *            mismatches aren't occurring in most applications.)
+     * @param objectFactory
+     *            Called when the custom data wasn't yet set, to invoke its initial value. If this parameter is
+     *            {@code null} and the custom data wasn't set yet, then {@code null} will be returned. The returned
+     *            value of {@link ObjectFactory#createObject()} can be any kind of object, but can't be {@code null}.
+     * 
+     * @return The current custom data object, or possibly {@code null} if there was no {@link ObjectFactory} provided.
+     * 
+     * @throws CallPlaceCustomDataInitializationException
+     *             If the {@link ObjectFactory} had to be invoked but failed.
+     */
+    Object getOrCreateCustomData(Object providerIdentity, ObjectFactory objectFactory)
+            throws CallPlaceCustomDataInitializationException;
+
+    /**
+     * Tells if the nested content (the body) can be safely cached, as it only depends on the template content (not on
+     * variable values and such) and has no side-effects (other than writing to the output). Examples of cases that give
+     * {@code false}: {@code <@foo>Name: } <tt...@foo>},
+     * {@code <@foo>Name: <#if showIt>Joe</#...@foo>}. Examples of cases that give {@code true}:
+     * {@code <@foo>Name: Joe</...@foo>}, {@code <@foo />}. Note that we get {@code true} for no nested content, because
+     * that's equivalent with 0-length nested content in FTL.
+     * 
+     * <p>
+     * This method returns a pessimistic result. For example, if it sees a custom directive call, it can't know what it
+     * does, so it will assume that it's not cacheable.
+     */
+    boolean isNestedOutputCacheable();
+
+}


[33/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ParseException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ParseException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ParseException.java
new file mode 100644
index 0000000..9e5dad3
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ParseException.java
@@ -0,0 +1,518 @@
+/*
+ * 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.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import org.apache.freemarker.core.util._NullArgumentException;
+import org.apache.freemarker.core.util._SecurityUtil;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * Parsing-time exception in a template (as opposed to a runtime exception, a {@link TemplateException}). This usually
+ * signals syntactical/lexical errors.
+ * 
+ * Note that on JavaCC-level lexical errors throw {@link TokenMgrError} instead of this, however with the API-s that
+ * most users use those will be wrapped into {@link ParseException}-s. 
+ *
+ * This is a modified version of file generated by JavaCC from FTL.jj.
+ * You can modify this class to customize the error reporting mechanisms so long as the public interface
+ * remains compatible with the original.
+ * 
+ * @see TokenMgrError
+ */
+public class ParseException extends IOException implements FMParserConstants {
+
+    /**
+     * This is the last token that has been consumed successfully.  If
+     * this object has been created due to a parse error, the token
+     * following this token will (therefore) be the first error token.
+     */
+    public Token currentToken;
+
+    private static volatile Boolean jbossToolsMode;
+
+    private boolean messageAndDescriptionRendered;
+    private String message;
+    private String description; 
+
+    public int columnNumber, lineNumber;
+    public int endColumnNumber, endLineNumber;
+
+    /**
+     * Each entry in this array is an array of integers.  Each array
+     * of integers represents a sequence of tokens (by their ordinal
+     * values) that is expected at this point of the parse.
+     */
+    public int[][] expectedTokenSequences;
+
+    /**
+     * This is a reference to the "tokenImage" array of the generated
+     * parser within which the parse error occurred.  This array is
+     * defined in the generated ...Constants interface.
+     */
+    public String[] tokenImage;
+
+    /**
+     * The end of line string for this machine.
+     */
+    protected String eol = _SecurityUtil.getSystemProperty("line.separator", "\n");
+
+    private String templateSourceName;
+    private String templateLookupName;
+
+    /**
+     * This constructor is used by the method "generateParseException"
+     * in the generated parser.  Calling this constructor generates
+     * a new object of this type with the fields "currentToken",
+     * "expectedTokenSequences", and "tokenImage" set.
+     * This constructor calls its super class with the empty string
+     * to force the "toString" method of parent class "Throwable" to
+     * print the error message in the form:
+     *     ParseException: &lt;result of getMessage&gt;
+     */
+    public ParseException(Token currentTokenVal,
+            int[][] expectedTokenSequencesVal,
+            String[] tokenImageVal
+            ) {
+        super("");
+        currentToken = currentTokenVal;
+        expectedTokenSequences = expectedTokenSequencesVal;
+        tokenImage = tokenImageVal;
+        lineNumber = currentToken.next.beginLine;
+        columnNumber = currentToken.next.beginColumn;
+        endLineNumber = currentToken.next.endLine;
+        endColumnNumber = currentToken.next.endColumn;
+    }
+
+    /**
+     * Used by JavaCC generated code.
+     */
+    protected ParseException() {
+        super();
+    }
+
+    /**
+     * @since 2.3.21
+     */
+    public ParseException(String description, Template template,
+            int lineNumber, int columnNumber, int endLineNumber, int endColumnNumber) {
+        this(description, template, lineNumber, columnNumber, endLineNumber, endColumnNumber, null);      
+    }
+
+    /**
+     * @since 2.3.21
+     */
+    public ParseException(String description, Template template,
+            int lineNumber, int columnNumber, int endLineNumber, int endColumnNumber,
+            Throwable cause) {
+        super(description);  // but we override getMessage, so it will be different
+        try {
+            initCause(cause);
+        } catch (Exception e) {
+            // Suppressed; we can't do more
+        }
+        this.description = description;
+        if (template != null) { // Allowed because sometimes the template is set later via setTemplate(Template)
+            templateSourceName = template.getSourceName();
+            templateLookupName = template.getLookupName();
+        }
+        this.lineNumber = lineNumber;
+        this.columnNumber = columnNumber;
+        this.endLineNumber = endLineNumber;
+        this.endColumnNumber = endColumnNumber;
+    }
+    
+    /**
+     * @since 2.3.20
+     */
+    public ParseException(String description, Template template, Token tk) {
+        this(description, template, tk, null);
+    }
+
+    /**
+     * @since 2.3.20
+     */
+    public ParseException(String description, Template template, Token tk, Throwable cause) {
+        this(description,
+                template,
+                tk.beginLine, tk.beginColumn,
+                tk.endLine, tk.endColumn,
+                cause);
+    }
+
+    /**
+     * @since 2.3.20
+     */
+    public ParseException(String description, ASTNode astNode) {
+        this(description, astNode, null);
+    }
+
+    /**
+     * @since 2.3.20
+     */
+    public ParseException(String description, ASTNode astNode, Throwable cause) {
+        this(description,
+                astNode.getTemplate(),
+                astNode.beginLine, astNode.beginColumn,
+                astNode.endLine, astNode.endColumn,
+                cause);
+    }
+
+    /**
+     * Should be used internally only; sets the name of the template that contains the error.
+     * This is needed as the constructor that JavaCC automatically calls doesn't pass in the template, so we
+     * set it somewhere later in an exception handler. 
+     */
+    public void setTemplate(Template template) {
+        _NullArgumentException.check("template", template);
+        templateSourceName = template.getSourceName();
+        templateLookupName = template.getLookupName();
+        synchronized (this) {
+            messageAndDescriptionRendered = false;
+            message = null;
+        }
+    }
+
+    /**
+     * Returns the error location plus the error description.
+     * 
+     * @see #getDescription()
+     * @see #getTemplateSourceName()
+     * @see #getTemplateLookupName()
+     * @see #getLineNumber()
+     * @see #getColumnNumber()
+     */
+    @Override
+    public String getMessage() {
+        synchronized (this) {
+            if (messageAndDescriptionRendered) return message;
+        }
+        renderMessageAndDescription();
+        synchronized (this) {
+            return message;
+        }
+    }
+
+    private String getDescription() {
+        synchronized (this) {
+            if (messageAndDescriptionRendered) return description;
+        }
+        renderMessageAndDescription();
+        synchronized (this) {
+            return description;
+        }
+    }
+    
+    /**
+     * Returns the description of the error without error location or source quotations, or {@code null} if there's no
+     * description available. This is useful in editors (IDE-s) where the error markers and the editor window itself
+     * already carry this information, so it's redundant the repeat in the error dialog.
+     */
+    public String getEditorMessage() {
+        return getDescription();
+    }
+
+    /**
+     * Returns the {@linkplain Template#getLookupName()} lookup name} of the template whose parsing was failed.
+     * Maybe {@code null}, for example if this is a non-stored template.
+     */
+    public String getTemplateLookupName() {
+        return templateLookupName;
+    }
+
+    /**
+     * Returns the {@linkplain Template#getSourceName()} source name} of the template whose parsing was failed.
+     * Maybe {@code null}, for example if this is a non-stored template.
+     */
+    public String getTemplateSourceName() {
+        return templateSourceName;
+    }
+
+    /**
+     * Returns the {@linkplain #getTemplateSourceName() template source name}, or if that's {@code null} then the
+     * {@linkplain #getTemplateLookupName() template lookup name}. This name is primarily meant to be used in error
+     * messages.
+     */
+    public String getTemplateSourceOrLookupName() {
+        return getTemplateSourceName() != null ? getTemplateSourceName() : getTemplateLookupName();
+    }
+
+    /**
+     * 1-based line number of the failing section, or 0 is the information is not available.
+     */
+    public int getLineNumber() {
+        return lineNumber;
+    }
+
+    /**
+     * 1-based column number of the failing section, or 0 is the information is not available.
+     */
+    public int getColumnNumber() {
+        return columnNumber;
+    }
+
+    /**
+     * 1-based line number of the last line that contains the failing section, or 0 if the information is not available.
+     * 
+     * @since 2.3.21
+     */
+    public int getEndLineNumber() {
+        return endLineNumber;
+    }
+
+    /**
+     * 1-based column number of the last character of the failing section, or 0 if the information is not available.
+     * Note that unlike with Java string API-s, this column number is inclusive.
+     * 
+     * @since 2.3.21
+     */
+    public int getEndColumnNumber() {
+        return endColumnNumber;
+    }
+
+    private void renderMessageAndDescription() {
+        String desc = getOrRenderDescription();
+
+        String prefix;
+        if (!isInJBossToolsMode()) {
+            prefix = "Syntax error "
+                    + MessageUtil.formatLocationForSimpleParsingError(getTemplateSourceOrLookupName(), lineNumber,
+                    columnNumber)
+                    + ":\n";  
+        } else {
+            prefix = "[col. " + columnNumber + "] ";
+        }
+
+        String msg = prefix + desc;
+        desc = msg.substring(prefix.length());  // so we reuse the backing char[]
+
+        synchronized (this) {
+            message = msg;
+            description = desc;
+            messageAndDescriptionRendered = true;
+        }
+    }
+
+    private boolean isInJBossToolsMode() {
+        if (jbossToolsMode == null) {
+            try {
+                jbossToolsMode = Boolean.valueOf(
+                        ParseException.class.getClassLoader().toString().indexOf(
+                                "[org.jboss.ide.eclipse.freemarker:") != -1);
+            } catch (Throwable e) {
+                jbossToolsMode = Boolean.FALSE;
+            }
+        }
+        return jbossToolsMode.booleanValue();
+    }
+
+    /**
+     * Returns the description of the error without the error location, or {@code null} if there's no description
+     * available.
+     */
+    private String getOrRenderDescription() {
+        synchronized (this) {
+            if (description != null) return description;  // When we already have it from the constructor
+        }
+
+        String tokenErrDesc;
+        if (currentToken != null) {
+            tokenErrDesc = getCustomTokenErrorDescription();
+            if (tokenErrDesc == null) {
+                // The default JavaCC message generation stuff follows.
+                StringBuilder expected = new StringBuilder();
+                int maxSize = 0;
+                for (int i = 0; i < expectedTokenSequences.length; i++) {
+                    if (i != 0) {
+                        expected.append(eol);
+                    }
+                    expected.append("    ");
+                    if (maxSize < expectedTokenSequences[i].length) {
+                        maxSize = expectedTokenSequences[i].length;
+                    }
+                    for (int j = 0; j < expectedTokenSequences[i].length; j++) {
+                        if (j != 0) expected.append(' ');
+                        expected.append(tokenImage[expectedTokenSequences[i][j]]);
+                    }
+                }
+                tokenErrDesc = "Encountered \"";
+                Token tok = currentToken.next;
+                for (int i = 0; i < maxSize; i++) {
+                    if (i != 0) tokenErrDesc += " ";
+                    if (tok.kind == 0) {
+                        tokenErrDesc += tokenImage[0];
+                        break;
+                    }
+                    tokenErrDesc += add_escapes(tok.image);
+                    tok = tok.next;
+                }
+                tokenErrDesc += "\", but ";
+
+                if (expectedTokenSequences.length == 1) {
+                    tokenErrDesc += "was expecting:" + eol;
+                } else {
+                    tokenErrDesc += "was expecting one of:" + eol;
+                }
+                tokenErrDesc += expected;
+            }
+        } else {
+            tokenErrDesc = null;
+        }
+        return tokenErrDesc;
+    }
+
+    private String getCustomTokenErrorDescription() {
+        final Token nextToken = currentToken.next;
+        final int kind = nextToken.kind;
+        if (kind == EOF) {
+            Set/*<String>*/ endNames = new HashSet();
+            for (int[] sequence : expectedTokenSequences) {
+                for (int aSequence : sequence) {
+                    switch (aSequence) {
+                        case END_LIST:
+                            endNames.add("#list");
+                            break;
+                        case END_SWITCH:
+                            endNames.add("#switch");
+                            break;
+                        case END_IF:
+                            endNames.add("#if");
+                            break;
+                        case END_COMPRESS:
+                            endNames.add("#compress");
+                            break;
+                        case END_MACRO:
+                            endNames.add("#macro");
+                        case END_FUNCTION:
+                            endNames.add("#function");
+                            break;
+                        case END_ESCAPE:
+                            endNames.add("#escape");
+                            break;
+                        case END_NOESCAPE:
+                            endNames.add("#noescape");
+                            break;
+                        case END_ASSIGN:
+                            endNames.add("#assign");
+                            break;
+                        case END_LOCAL:
+                            endNames.add("#local");
+                            break;
+                        case END_GLOBAL:
+                            endNames.add("#global");
+                            break;
+                        case END_ATTEMPT:
+                            endNames.add("#attempt");
+                            break;
+                        case CLOSING_CURLY_BRACKET:
+                            endNames.add("\"{\"");
+                            break;
+                        case CLOSE_BRACKET:
+                            endNames.add("\"[\"");
+                            break;
+                        case CLOSE_PAREN:
+                            endNames.add("\"(\"");
+                            break;
+                        case UNIFIED_CALL_END:
+                            endNames.add("@...");
+                            break;
+                    }
+                }
+            }
+            return "Unexpected end of file reached."
+                    + (endNames.size() == 0 ? "" : " You have an unclosed " + concatWithOrs(endNames) + ".");
+        } else if (kind == ELSE) {
+            return "Unexpected directive, \"#else\". "
+                    + "Check if you have a valid #if-#elseif-#else or #list-#else structure.";
+        } else if (kind == END_IF || kind == ELSE_IF) {
+            return "Unexpected directive, "
+                    + _StringUtil.jQuote(nextToken)
+                    + ". Check if you have a valid #if-#elseif-#else structure.";
+        }
+        return null;
+    }
+
+    private String concatWithOrs(Set/*<String>*/ endNames) {
+        StringBuilder sb = new StringBuilder(); 
+        for (Iterator/*<String>*/ it = endNames.iterator(); it.hasNext(); ) {
+            String endName = (String) it.next();
+            if (sb.length() != 0) {
+                sb.append(" or ");
+            }
+            sb.append(endName);
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Used to convert raw characters to their escaped version
+     * when these raw version cannot be used as part of an ASCII
+     * string literal.
+     */
+    protected String add_escapes(String str) {
+        StringBuilder retval = new StringBuilder();
+        char ch;
+        for (int i = 0; i < str.length(); i++) {
+            switch (str.charAt(i))
+            {
+            case 0 :
+                continue;
+            case '\b':
+                retval.append("\\b");
+                continue;
+            case '\t':
+                retval.append("\\t");
+                continue;
+            case '\n':
+                retval.append("\\n");
+                continue;
+            case '\f':
+                retval.append("\\f");
+                continue;
+            case '\r':
+                retval.append("\\r");
+                continue;
+            case '\"':
+                retval.append("\\\"");
+                continue;
+            case '\'':
+                retval.append("\\\'");
+                continue;
+            case '\\':
+                retval.append("\\\\");
+                continue;
+            default:
+                if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) {
+                    String s = "0000" + Integer.toString(ch, 16);
+                    retval.append("\\u" + s.substring(s.length() - 4, s.length()));
+                } else {
+                    retval.append(ch);
+                }
+                continue;
+            }
+        }
+        return retval.toString();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ParsingAndProcessingConfiguration.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ParsingAndProcessingConfiguration.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ParsingAndProcessingConfiguration.java
new file mode 100644
index 0000000..719af93
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ParsingAndProcessingConfiguration.java
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+/**
+ * <b>Don't implement this interface yourself</b>; use the existing implementation(s). This interface is the union of
+ * {@link ProcessingConfiguration} and {@link ParsingConfiguration}, which is useful for declaring types for values
+ * that must implement both interfaces.
+ */
+public interface ParsingAndProcessingConfiguration extends ParsingConfiguration, ProcessingConfiguration {
+    // No additional method
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ParsingConfiguration.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ParsingConfiguration.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ParsingConfiguration.java
new file mode 100644
index 0000000..0eb9569
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ParsingConfiguration.java
@@ -0,0 +1,299 @@
+/*
+ * 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.nio.charset.Charset;
+
+import org.apache.freemarker.core.arithmetic.ArithmeticEngine;
+import org.apache.freemarker.core.outputformat.MarkupOutputFormat;
+import org.apache.freemarker.core.outputformat.OutputFormat;
+import org.apache.freemarker.core.outputformat.impl.HTMLOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.UndefinedOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.XMLOutputFormat;
+
+/**
+ * Implemented by FreeMarker core classes (not by you) that provide configuration settings that affect template parsing
+ * (as opposed to {@linkplain Template#process (Object, Writer) template processing}). <b>New methods may be added
+ * any time in future FreeMarker versions, so don't try to implement this interface yourself!</b>
+ *
+ * @see ProcessingConfiguration
+ * @see ParsingAndProcessingConfiguration
+ */
+public interface ParsingConfiguration {
+
+    int AUTO_DETECT_NAMING_CONVENTION = 10;
+    int LEGACY_NAMING_CONVENTION = 11;
+    int CAMEL_CASE_NAMING_CONVENTION = 12;
+
+    int AUTO_DETECT_TAG_SYNTAX = 0;
+    int ANGLE_BRACKET_TAG_SYNTAX = 1;
+    int SQUARE_BRACKET_TAG_SYNTAX = 2;
+
+    /**
+     * Don't enable auto-escaping, regardless of what the {@link OutputFormat} is. Note that a {@code
+     * <#ftl auto_esc=true>} in the template will override this.
+     */
+    int DISABLE_AUTO_ESCAPING_POLICY = 20;
+    /**
+     * Enable auto-escaping if the output format supports it and {@link MarkupOutputFormat#isAutoEscapedByDefault()} is
+     * {@code true}.
+     */
+    int ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY = 21;
+    /** Enable auto-escaping if the {@link OutputFormat} supports it. */
+    int ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY = 22;
+
+    /**
+     * The template language used; this is often overridden for certain file extension with the
+     * {@link Configuration#getTemplateConfigurations() templateConfigurations} setting of the {@link Configuration}.
+     */
+    TemplateLanguage getTemplateLanguage();
+
+    boolean isTemplateLanguageSet();
+
+    /**
+     * Determines the syntax of the template files (angle bracket VS square bracket)
+     * that has no {@code #ftl} in it. The {@code tagSyntax}
+     * parameter must be one of:
+     * <ul>
+     *   <li>{@link #AUTO_DETECT_TAG_SYNTAX}:
+     *     use the syntax of the first FreeMarker tag (can be anything, like <tt>#list</tt>,
+     *     <tt>#include</tt>, user defined, etc.)
+     *   <li>{@link #ANGLE_BRACKET_TAG_SYNTAX}:
+     *     use the angle bracket syntax (the normal syntax)
+     *   <li>{@link #SQUARE_BRACKET_TAG_SYNTAX}:
+     *     use the square bracket syntax
+     * </ul>
+     *
+     * <p>In FreeMarker 2.3.x {@link #ANGLE_BRACKET_TAG_SYNTAX} is the
+     * default for better backward compatibility. Starting from 2.4.x {@link
+     * ParsingConfiguration#AUTO_DETECT_TAG_SYNTAX} is the default, so it's recommended to use
+     * that even for 2.3.x.
+     *
+     * <p>This setting is ignored for the templates that have {@code ftl} directive in
+     * it. For those templates the syntax used for the {@code ftl} directive determines
+     * the syntax.
+     */
+    int getTagSyntax();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting might returns a default value, or returns the value of the setting from a parent parsing
+     * configuration or throws a {@link SettingValueNotSetException}.
+     */
+    boolean isTagSyntaxSet();
+
+    /**
+     * The naming convention used for the identifiers that are part of the template language. The available naming
+     * conventions are legacy (directive (tag) names are all-lower-case {@code likethis}, others are snake case
+     * {@code like_this}), and camel case ({@code likeThis}). The default is auto-detect, which detects the naming
+     * convention used and enforces that same naming convention for the whole template.
+     *
+     * <p>
+     * This setting doesn't influence what naming convention is used for the setting names outside templates. Also, it
+     * won't ever convert the names of user-defined things, like of data-model members, or the names of user defined
+     * macros/functions. It only influences the names of the built-in directives ({@code #elseIf} VS {@code elseif}),
+     * built-ins ({@code ?upper_case} VS {@code ?upperCase} ), special variables ({@code .data_model} VS
+     * {@code .dataModel}).
+     *
+     * <p>
+     * Which convention to use: FreeMarker prior to 2.3.23 has only supported
+     * {@link #LEGACY_NAMING_CONVENTION}, so that's how most templates and examples out there are written
+     * as of 2015. But as templates today are mostly written by programmers and often access Java API-s which already
+     * use camel case, {@link #CAMEL_CASE_NAMING_CONVENTION} is the recommended option for most projects.
+     * However, it's no necessary to make a application-wide decision; see auto-detection below.
+     *
+     * <p>
+     * FreeMarker will decide the naming convention automatically for each template individually when this setting is
+     * set to {@link #AUTO_DETECT_NAMING_CONVENTION} (which is the default). The naming convention of a template is
+     * decided when the first core (non-user-defined) identifier is met during parsing (not during processing) where the
+     * naming convention is relevant (like for {@code s?upperCase} or {@code s?upper_case} it's relevant, but for
+     * {@code s?length} it isn't). At that point, the naming convention of the template is decided, and any later core
+     * identifier that uses a different convention will be a parsing error. As the naming convention is decided per
+     * template, it's not a problem if a template and the other template it {@code #include}-s/{@code #import} uses a
+     * different convention.
+     *
+     * <p>
+     * FreeMarker always enforces the same naming convention to be used consistently within the same template "file".
+     * Additionally, when this setting is set to non-{@link #AUTO_DETECT_NAMING_CONVENTION}, the selected naming
+     * convention is enforced on all templates. Thus such a setup can be used to enforce an application-wide naming
+     * convention.
+     *
+     * @return
+     *            One of the {@link #AUTO_DETECT_NAMING_CONVENTION} or
+     *            {@link #LEGACY_NAMING_CONVENTION} or {@link #CAMEL_CASE_NAMING_CONVENTION}.
+     */
+    int getNamingConvention();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting might returns a default value, or returns the value of the setting from a parent parsing
+     * configuration or throws a {@link SettingValueNotSetException}.
+     */
+    boolean isNamingConventionSet();
+
+    /**
+     * Whether the template parser will try to remove superfluous white-space around certain tags.
+     */
+    boolean getWhitespaceStripping();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting might returns a default value, or returns the value of the setting from a parent parsing
+     * configuration or throws a {@link SettingValueNotSetException}.
+     */
+    boolean isWhitespaceStrippingSet();
+
+    /**
+     * Overlaps with {@link ProcessingConfiguration#getArithmeticEngine()}; the parser needs this for creating numerical
+     * literals.
+     */
+    ArithmeticEngine getArithmeticEngine();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting might returns a default value, or returns the value of the setting from a parent parsing
+     * configuration or throws a {@link SettingValueNotSetException}.
+     */
+    boolean isArithmeticEngineSet();
+
+    /**
+     * See {@link Configuration#getAutoEscapingPolicy()}.
+     */
+    int getAutoEscapingPolicy();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting might returns a default value, or returns the value of the setting from a parent parsing
+     * configuration or throws a {@link SettingValueNotSetException}.
+     */
+    boolean isAutoEscapingPolicySet();
+
+    /**
+     * The output format to use, which among others influences auto-escaping (see {@link #getAutoEscapingPolicy}
+     * autoEscapingPolicy}), and possibly the MIME type of the output.
+     * <p>
+     * On the {@link Configuration} level, usually, you should leave this on its default, which is
+     * {@link UndefinedOutputFormat#INSTANCE}, and then use standard file extensions like "ftlh" (for HTML) or "ftlx"
+     * (for XML) (and ensure that {@link #getRecognizeStandardFileExtensions() recognizeStandardFileExtensions} is
+     * {@code true}; see more there). Where you can't use the standard extensions, templates still can be associated
+     * to output formats with patterns matching their name (their path) using the
+     * {@link Configuration#getTemplateConfigurations() templateConfigurations} setting of the {@link Configuration}.
+     * But if all templates will have the same output format, you may set the
+     * {@link #getOutputFormat() outputFormat} setting of the {@link Configuration}
+     * after all, to a value like {@link HTMLOutputFormat#INSTANCE}, {@link XMLOutputFormat#INSTANCE}, etc. Also
+     * note that templates can specify their own output format like {@code <#ftl output_format="HTML">}, which
+     * overrides any configuration settings.
+     *
+     * @see Configuration#getRegisteredCustomOutputFormats()
+     * @see Configuration#getTemplateConfigurations()
+     * @see #getRecognizeStandardFileExtensions()
+     * @see #getAutoEscapingPolicy()
+     */
+    OutputFormat getOutputFormat();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting might returns a default value, or returns the value of the setting from a parent parsing
+     * configuration or throws a {@link SettingValueNotSetException}.
+     */
+    boolean isOutputFormatSet();
+
+    /**
+     * Tells if the "file" extension part of the source name ({@link Template#getSourceName()}) will influence certain
+     * parsing settings. For backward compatibility, it defaults to {@code false} if
+     * {@link #getIncompatibleImprovements()} is less than 2.3.24. Starting from {@code incompatibleImprovements}
+     * 2.3.24, it defaults to {@code true}, so the following standard file extensions take their effect:
+     *
+     * <ul>
+     *   <li>{@code ftlh}: Sets the {@link #getOutputFormat() outputFormat} setting to {@code "HTML"}
+     *       (i.e., {@link HTMLOutputFormat#INSTANCE}, unless the {@code "HTML"} name is overridden by
+     *       the {@link Configuration#getRegisteredCustomOutputFormats registeredOutputFormats} setting) and
+     *       the {@link #getAutoEscapingPolicy() autoEscapingPolicy} setting to
+     *       {@link #ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY}.
+     *   <li>{@code ftlx}: Sets the {@link #getOutputFormat() outputFormat} setting to
+     *       {@code "XML"} (i.e., {@link XMLOutputFormat#INSTANCE}, unless the {@code "XML"} name is overridden by
+     *       the {@link Configuration#getRegisteredCustomOutputFormats registeredOutputFormats} setting) and
+     *       the {@link #getAutoEscapingPolicy() autoEscapingPolicy} setting to
+     *       {@link #ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY}.
+     * </ul>
+     *
+     * <p>These file extensions are not case sensitive. The file extension is the part after the last dot in the source
+     * name. If the source name contains no dot, then it has no file extension.
+     *
+     * <p>The settings activated by these file extensions override the setting values dictated by the
+     * {@link Configuration#getTemplateConfigurations templateConfigurations} setting of the {@link Configuration}.
+     */
+    boolean getRecognizeStandardFileExtensions();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting might returns a default value, or returns the value of the setting from a parent parsing
+     * configuration or throws a {@link SettingValueNotSetException}.
+     */
+    boolean isRecognizeStandardFileExtensionsSet();
+
+    /**
+     * See {@link TopLevelConfiguration#getIncompatibleImprovements()}; this is normally directly delegates to
+     * {@link Configuration#getIncompatibleImprovements()}, and it's always set.
+     */
+    Version getIncompatibleImprovements();
+
+    /**
+     * The assumed display width of the tab character (ASCII 9), which influences the column number shown in error
+     * messages (or the column number you get through other API-s). So for example if the users edit templates in an
+     * editor where the tab width is set to 4, you should set this to 4 so that the column numbers printed by FreeMarker
+     * will match the column number shown in the editor. This setting doesn't affect the output of templates, as a tab
+     * in the template will remain a tab in the output too.
+     * It's value is at least 1, at most 256.
+     */
+    int getTabSize();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting might returns a default value, or returns the value of the setting from a parent parsing
+     * configuration or throws a {@link SettingValueNotSetException}.
+     */
+    boolean isTabSizeSet();
+
+    /**
+     * Sets the charset used for decoding template files.
+     * <p>
+     * Defaults to the default system {@code fileEncoding}, which can change from one server to
+     * another, so <b>you should always set this setting</b>. If you don't know what charset your should chose,
+     * {@code "UTF-8"} is usually a good choice.
+     * <p>
+     * When a project contains groups (like folders) of template files where the groups use different encodings,
+     * consider using the {@link Configuration#getTemplateConfigurations() templateConfigurations} setting on the
+     * {@link Configuration} level.
+     * <p>
+     * Individual templates may specify their own charset by starting with
+     * <tt>&lt;#ftl sourceEncoding="..."&gt;</tt>. However, before that's detected, at least part of template must be
+     * decoded with some charset first, so this setting (and
+     * {@link Configuration#getTemplateConfigurations() templateConfigurations}) still have role.
+     */
+    Charset getSourceEncoding();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting might returns a default value, or returns the value of the setting from a parent parsing
+     * configuration or throws a {@link SettingValueNotSetException}.
+     */
+    boolean isSourceEncodingSet();
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ProcessingConfiguration.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ProcessingConfiguration.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ProcessingConfiguration.java
new file mode 100644
index 0000000..545a313
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ProcessingConfiguration.java
@@ -0,0 +1,704 @@
+/*
+ * 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.Writer;
+import java.nio.charset.Charset;
+import java.text.NumberFormat;
+import java.text.SimpleDateFormat;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+
+import org.apache.freemarker.core.arithmetic.ArithmeticEngine;
+import org.apache.freemarker.core.arithmetic.impl.BigDecimalArithmeticEngine;
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.impl.DefaultObjectWrapper;
+import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormat;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory;
+
+/**
+ * Implemented by FreeMarker core classes (not by you) that provide configuration settings that affect {@linkplain
+ * Template#process(Object, Writer) template processing} (as opposed to template parsing). <b>New methods may be added
+ * any time in future FreeMarker versions, so don't try to implement this interface yourself!</b>
+ *
+ * @see ParsingConfiguration
+ * @see ParsingAndProcessingConfiguration
+ */
+public interface ProcessingConfiguration {
+
+    /**
+     * The locale used for number and date formatting (among others), also the locale used for searching localized
+     * template variations when no locale was explicitly specified where the template is requested.
+     *
+     * @see Configuration#getTemplate(String, Locale)
+     */
+    Locale getLocale();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting mights returns a default value, or returns the value of the setting from a parent object, or throws
+     * an {@link SettingValueNotSetException}.
+     */
+    boolean isLocaleSet();
+
+    /**
+     * The time zone to use when formatting date/time values. It {@link Configuration}-level default
+     * is the system time zone ({@link TimeZone#getDefault()}), regardless of the "locale" FreeMarker setting,
+     * so in a server application you probably want to set it explicitly in the {@link Environment} to match the
+     * preferred time zone of the target audience (like the Web page visitor).
+     *
+     * <p>If you or the templates set the time zone, you should probably also set
+     * {@link #getSQLDateAndTimeTimeZone()}!
+     *
+     * @see #getSQLDateAndTimeTimeZone()
+     */
+    TimeZone getTimeZone();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting mights returns a default value, or returns the value of the setting from a parent object, or throws
+     * an {@link SettingValueNotSetException}.
+     */
+    boolean isTimeZoneSet();
+
+    /**
+     * The time zone used when dealing with {@link java.sql.Date java.sql.Date} and
+     * {@link java.sql.Time java.sql.Time} values. Its {@link Configuration}-level defaults is {@code null} for
+     * backward compatibility, but in most applications this should be set to the JVM default time zone (server
+     * default time zone), because that's what most JDBC drivers will use when constructing the
+     * {@link java.sql.Date java.sql.Date} and {@link java.sql.Time java.sql.Time} values. If this setting is {@code
+     * null} FreeMarker will use the value of ({@link #getTimeZone()}) for {@link java.sql.Date java.sql.Date} and
+     * {@link java.sql.Time java.sql.Time} values, which often gives bad results.
+     *
+     * <p>This setting doesn't influence the formatting of other kind of values (like of
+     * {@link java.sql.Timestamp java.sql.Timestamp} or plain {@link java.util.Date java.util.Date} values).
+     *
+     * <p>To decide what value you need, a few things has to be understood:
+     * <ul>
+     *   <li>Date-only and time-only values in SQL-oriented databases are usually store calendar and clock field
+     *   values directly (year, month, day, or hour, minute, seconds (with decimals)), as opposed to a set of points
+     *   on the physical time line. Thus, unlike SQL timestamps, these values usually aren't meant to be shown
+     *   differently depending on the time zone of the audience.
+     *
+     *   <li>When a JDBC query has to return a date-only or time-only value, it has to convert it to a point on the
+     *   physical time line, because that's what {@link java.util.Date} and its subclasses store (milliseconds since
+     *   the epoch). Obviously, this is impossible to do. So JDBC just chooses a physical time which, when rendered
+     *   <em>with the JVM default time zone</em>, will give the same field values as those stored
+     *   in the database. (Actually, you can give JDBC a calendar, and so it can use other time zones too, but most
+     *   application won't care using those overloads.) For example, assume that the system time zone is GMT+02:00.
+     *   Then, 2014-07-12 in the database will be translated to physical time 2014-07-11 22:00:00 UTC, because that
+     *   rendered in GMT+02:00 gives 2014-07-12 00:00:00. Similarly, 11:57:00 in the database will be translated to
+     *   physical time 1970-01-01 09:57:00 UTC. Thus, the physical time stored in the returned value depends on the
+     *   default system time zone of the JDBC client, not just on the content of the database. (This used to be the
+     *   default behavior of ORM-s, like Hibernate, too.)
+     *
+     *   <li>The value of the {@code time_zone} FreeMarker configuration setting sets the time zone used for the
+     *   template output. For example, when a web page visitor has a preferred time zone, the web application framework
+     *   may calls {@link Environment#setTimeZone(TimeZone)} with that time zone. Thus, the visitor will
+     *   see {@link java.sql.Timestamp java.sql.Timestamp} and plain {@link java.util.Date java.util.Date} values as
+     *   they look in his own time zone. While
+     *   this is desirable for those types, as they meant to represent physical points on the time line, this is not
+     *   necessarily desirable for date-only and time-only values. When {@code sql_date_and_time_time_zone} is
+     *   {@code null}, {@code time_zone} is used for rendering all kind of date/time/dateTime values, including
+     *   {@link java.sql.Date java.sql.Date} and {@link java.sql.Time java.sql.Time}, and then if, for example,
+     *   {@code time_zone} is GMT+00:00, the
+     *   values from the earlier examples will be shown as 2014-07-11 (one day off) and 09:57:00 (2 hours off). While
+     *   those are the time zone correct renderings, those values are probably meant to be shown "as is".
+     *
+     *   <li>You may wonder why this setting isn't simply "SQL time zone", that is, why's this time zone not applied to
+     *   {@link java.sql.Timestamp java.sql.Timestamp} values as well. Timestamps in databases refer to a point on
+     *   the physical time line, and thus doesn't have the inherent problem of date-only and time-only values.
+     *   FreeMarker assumes that the JDBC driver converts time stamps coming from the database so that they store
+     *   the distance from the epoch (1970-01-01 00:00:00 UTC), as requested by the {@link java.util.Date} API.
+     *   Then time stamps can be safely rendered in different time zones, and thus need no special treatment.
+     * </ul>
+     *
+     * @see #getTimeZone()
+     */
+    TimeZone getSQLDateAndTimeTimeZone();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting mights returns a default value, or returns the value of the setting from a parent object, or throws
+     * an {@link SettingValueNotSetException}.
+     */
+    boolean isSQLDateAndTimeTimeZoneSet();
+
+    /**
+     * The number format used to convert numbers to strings (where no number format is explicitly given). Its
+     * {@link Configuration}-level default is {@code "number"}. The possible values are:
+     * <ul>
+     *   <li>{@code "number"}: The number format returned by {@link NumberFormat#getNumberInstance(Locale)}</li>
+     *   <li>{@code "currency"}: The number format returned by {@link NumberFormat#getCurrencyInstance(Locale)}</li>
+     *   <li>{@code "percent"}: The number format returned by {@link NumberFormat#getPercentInstance(Locale)}</li>
+     *   <li>{@code "computer"}: The number format used by FTL's {@code c} built-in (like in {@code someNumber?c}).</li>
+     *   <li>A {@link java.text.DecimalFormat} pattern (like {@code "0.##"}). This syntax is extended by FreeMarker
+     *       so that you can specify options like the rounding mode and the symbols used after a 2nd semicolon. For
+     *       example, {@code ",000;; roundingMode=halfUp groupingSeparator=_"} will format numbers like {@code ",000"}
+     *       would, but with half-up rounding mode, and {@code _} as the group separator. See more about "extended Java
+     *       decimal format" in the FreeMarker Manual.
+     *       </li>
+     *   <li>If the string starts with {@code @} character followed by a letter then it's interpreted as a custom number
+     *       format. The format of a such string is <code>"@<i>name</i>"</code> or <code>"@<i>name</i>
+     *       <i>parameters</i>"</code>, where <code><i>name</i></code> is the key in the {@link Map} set by
+     *       {@link MutableProcessingConfiguration#setCustomNumberFormats(Map)}, and <code><i>parameters</i></code> is
+     *       parsed by the custom {@link TemplateNumberFormat}.
+     *   </li>
+     * </ul>
+     */
+    String getNumberFormat();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting mights returns a default value, or returns the value of the setting from a parent object, or throws
+     * an {@link SettingValueNotSetException}.
+     */
+    boolean isNumberFormatSet();
+
+    /**
+     * A {@link Map} that associates {@link TemplateNumberFormatFactory}-es to names, which then can be referred by the
+     * {@link #getNumberFormat() number_format} setting with values starting with <code>@<i>name</i></code>. The keys in
+     * the {@link Map} should start with an UNICODE letter, and should only contain UNICODE letters and digits (not
+     * {@code _}), otherwise accessing the custom format from templates can be difficult or impossible. The
+     * {@link Configuration}-level default of this setting is an empty  {@link Map}.
+     * <p>
+     * When the {@link ProcessingConfiguration} is part of a setting inheritance chain ({@link Environment} inherits
+     * settings from the main {@link Template}, which inherits from the {@link Configuration}), you still only get the
+     * {@link Map} from the closest {@link ProcessingConfiguration} where it was set, not a {@link Map} that respects
+     * inheritance. Thus, to get a custom format you shouldn't use this {@link Map} directly, but
+     * {@link #getCustomNumberFormat(String)}, which will search the format in the inheritance chain.
+     *
+     * @return Never {@code null}. Unless the method was called on a builder class, the returned {@link Map} shouldn't
+     * be modified.
+     */
+    Map<String, TemplateNumberFormatFactory> getCustomNumberFormats();
+
+    /**
+     * Gets the custom number format registered for the name. This differs from calling {@link #getCustomNumberFormats()
+     * getCustomNumberFormats().get(name)}, because if there's {@link ProcessingConfiguration} from which setting values
+     * are inherited then this method will search the custom format there as well if it isn't found here. For example,
+     * {@link Environment#getCustomNumberFormat(String)} will check if the {@link Environment} contains the custom
+     * format with the name, and if not, it will try {@link Template#getCustomNumberFormat(String)} on the main
+     * template, which in turn might falls back to calling {@link Configuration#getCustomNumberFormat(String)}.
+     */
+    TemplateNumberFormatFactory getCustomNumberFormat(String name);
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting mights returns a default value, or returns the value of the setting from a parent object, or throws
+     * an {@link SettingValueNotSetException}.
+     */
+    boolean isCustomNumberFormatsSet();
+
+    /**
+     * The string value for the boolean {@code true} and {@code false} values, intended for human audience (not for a
+     * computer language), separated with comma. For example, {@code "yes,no"}. Note that white-space is significant,
+     * so {@code "yes, no"} is WRONG (unless you want that leading space before "no").
+     *
+     * <p>For backward compatibility the default is {@code "true,false"}, but using that value is denied for automatic
+     * boolean-to-string conversion (like <code>${myBoolean}</code> will fail with it), only {@code myBool?string} will
+     * allow it, which is deprecated since FreeMarker 2.3.20.
+     */
+    String getBooleanFormat();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting mights returns a default value, or returns the value of the setting from a parent object, or throws
+     * an {@link SettingValueNotSetException}.
+     */
+    boolean isBooleanFormatSet();
+
+    /**
+     * The format used to convert {@link java.util.Date}-s that are time (no date part) values to string-s, also the
+     * format that {@code someString?time} will use to parse strings.
+     *
+     * <p>For the possible values see {@link #getDateTimeFormat()}.
+     *
+     * <p>Its {@link Configuration}-level default is {@code ""}, which is equivalent to {@code "medium"}.
+     */
+    String getTimeFormat();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting mights returns a default value, or returns the value of the setting from a parent object, or throws
+     * an {@link SettingValueNotSetException}.
+     */
+    boolean isTimeFormatSet();
+
+    /**
+     * The format used to convert {@link java.util.Date}-s that are date-only (no time part) values to string-s,
+     * also the format that {@code someString?date} will use to parse strings.
+     *
+     * <p>For the possible values see {@link #getDateTimeFormat()}.
+     *
+     * <p>Its {@link Configuration}-level default is {@code ""} which is equivalent to {@code "medium"}.
+     */
+    String getDateFormat();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting mights returns a default value, or returns the value of the setting from a parent object, or throws
+     * an {@link SettingValueNotSetException}.
+     */
+    boolean isDateFormatSet();
+
+    /**
+     * The format used to convert {@link java.util.Date}-s that are date-time (timestamp) values to string-s,
+     * also the format that {@code someString?datetime} will use to parse strings.
+     *
+     * <p>The possible setting values are (the quotation marks aren't part of the value itself):
+     *
+     * <ul>
+     *   <li><p>Patterns accepted by Java's {@link SimpleDateFormat}, for example {@code "dd.MM.yyyy HH:mm:ss"} (where
+     *       {@code HH} means 24 hours format) or {@code "MM/dd/yyyy hh:mm:ss a"} (where {@code a} prints AM or PM, if
+     *       the current language is English).
+     *
+     *   <li><p>{@code "xs"} for XML Schema format, or {@code "iso"} for ISO 8601:2004 format.
+     *       These formats allow various additional options, separated with space, like in
+     *       {@code "iso m nz"} (or with {@code _}, like in {@code "iso_m_nz"}; this is useful in a case like
+     *       {@code lastModified?string.iso_m_nz}). The options and their meanings are:
+     *
+     *       <ul>
+     *         <li><p>Accuracy options:<br>
+     *             {@code ms} = Milliseconds, always shown with all 3 digits, even if it's all 0-s.
+     *                     Example: {@code 13:45:05.800}<br>
+     *             {@code s} = Seconds (fraction seconds are dropped even if non-0), like {@code 13:45:05}<br>
+     *             {@code m} = Minutes, like {@code 13:45}. This isn't allowed for "xs".<br>
+     *             {@code h} = Hours, like {@code 13}. This isn't allowed for "xs".<br>
+     *             Neither = Up to millisecond accuracy, but trailing millisecond 0-s are removed, also the whole
+     *                     milliseconds part if it would be 0 otherwise. Example: {@code 13:45:05.8}
+     *
+     *         <li><p>Time zone offset visibility options:<br>
+     *             {@code fz} = "Force Zone", always show time zone offset (even for for
+     *                     {@link java.sql.Date java.sql.Date} and {@link java.sql.Time java.sql.Time} values).
+     *                     But, because ISO 8601 doesn't allow for dates (means date without time of the day) to
+     *                     show the zone offset, this option will have no effect in the case of {@code "iso"} with
+     *                     dates.<br>
+     *             {@code nz} = "No Zone", never show time zone offset<br>
+     *             Neither = always show time zone offset, except for {@link java.sql.Date java.sql.Date}
+     *                     and {@link java.sql.Time java.sql.Time}, and for {@code "iso"} date values.
+     *
+     *         <li><p>Time zone options:<br>
+     *             {@code u} = Use UTC instead of what the {@code time_zone} setting suggests. However,
+     *                     {@link java.sql.Date java.sql.Date} and {@link java.sql.Time java.sql.Time} aren't affected
+     *                     by this (see {@link #getSQLDateAndTimeTimeZone()} to understand why)<br>
+     *             {@code fu} = "Force UTC", that is, use UTC instead of what the {@code time_zone} or the
+     *                     {@code sql_date_and_time_time_zone} setting suggests. This also effects
+     *                     {@link java.sql.Date java.sql.Date} and {@link java.sql.Time java.sql.Time} values<br>
+     *             Neither = Use the time zone suggested by the {@code time_zone} or the
+     *                     {@code sql_date_and_time_time_zone} configuration setting ({@link #getTimeZone()} and
+     *                     {@link #getSQLDateAndTimeTimeZone()}).
+     *       </ul>
+     *
+     *       <p>The options can be specified in any order.</p>
+     *
+     *       <p>Options from the same category are mutually exclusive, like using {@code m} and {@code s}
+     *       together is an error.
+     *
+     *       <p>The accuracy and time zone offset visibility options don't influence parsing, only formatting.
+     *       For example, even if you use "iso m nz", "2012-01-01T15:30:05.125+01" will be parsed successfully and with
+     *       milliseconds accuracy.
+     *       The time zone options (like "u") influence what time zone is chosen only when parsing a string that doesn't
+     *       contain time zone offset.
+     *
+     *       <p>Parsing with {@code "iso"} understands both extend format and basic format, like
+     *       {@code 20141225T235018}. It doesn't, however, support the parsing of all kind of ISO 8601 strings: if
+     *       there's a date part, it must use year, month and day of the month values (not week of the year), and the
+     *       day can't be omitted.
+     *
+     *       <p>The output of {@code "iso"} is deliberately so that it's also a good representation of the value with
+     *       XML Schema format, except for 0 and negative years, where it's impossible. Also note that the time zone
+     *       offset is omitted for date values in the {@code "iso"} format, while it's preserved for the {@code "xs"}
+     *       format.
+     *
+     *   <li><p>{@code "short"}, {@code "medium"}, {@code "long"}, or {@code "full"}, which that has locale-dependent
+     *       meaning defined by the Java platform (see in the documentation of {@link java.text.DateFormat}).
+     *       For date-time values, you can specify the length of the date and time part independently, be separating
+     *       them with {@code _}, like {@code "short_medium"}. ({@code "medium"} means
+     *       {@code "medium_medium"} for date-time values.)
+     *
+     *   <li><p>Anything that starts with {@code "@"} followed by a letter is interpreted as a custom
+     *       date/time/dateTime format, but only if either {@link Configuration#getIncompatibleImprovements()}
+     *       is at least 2.3.24, or there's any custom formats defined (even if custom number format). The format of
+     *       such string is <code>"@<i>name</i>"</code> or <code>"@<i>name</i> <i>parameters</i>"</code>, where
+     *       <code><i>name</i></code> is the name parameter to {@link #getCustomDateFormat(String)}, and
+     *       <code><i>parameters</i></code> is parsed by the custom number format.
+     *
+     * </ul>
+     *
+     * <p>Its {@link Configuration}-level default is {@code ""}, which is equivalent to {@code "medium_medium"}.
+     */
+    String getDateTimeFormat();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting mights returns a default value, or returns the value of the setting from a parent object, or throws
+     * an {@link SettingValueNotSetException}.
+     */
+    boolean isDateTimeFormatSet();
+
+    /**
+     * A {@link Map} that associates {@link TemplateDateFormatFactory}-es to names, which then can be referred by the
+     * {@link #getDateFormat() date_format}/{@link #getDateFormat() date_format }/{@link #getDateTimeFormat()
+     * datetime_format} settings with values starting with <code>@<i>name</i></code>. The keys in the {@link Map} should
+     * start with an UNICODE letter, and should only contain UNICODE letters and digits (not {@code _}), otherwise
+     * accessing the custom format from templates can be difficult or impossible. The {@link Configuration}-level
+     * default of this setting is an empty {@link Map}.
+     * <p>
+     * When the {@link ProcessingConfiguration} is part of a setting inheritance chain ({@link Environment} inherits
+     * settings from the main {@link Template}, which inherits from the {@link Configuration}), you still only get the
+     * {@link Map} from the closest {@link ProcessingConfiguration} where it was set, not a {@link Map} that respects
+     * inheritance. Thus, to get a custom format you shouldn't use this {@link Map} directly, but {@link
+     * #getCustomDateFormat(String)}, which will search the format in the inheritance chain.
+     *
+     * @return Never {@code null}. Unless the method was called on a builder class, the returned {@link Map} shouldn't
+     * be modified.
+     */
+    Map<String, TemplateDateFormatFactory> getCustomDateFormats();
+
+    /**
+     * Gets the custom date or time or date-time format registered for the name. This differs from calling {@link
+     * #getCustomDateFormats() getCustomDateFormats.get(name)}, because if there's {@link ProcessingConfiguration} from
+     * which setting values are inherited then this method will search the custom format there as well if it isn't found
+     * here. For example, {@link Environment#getCustomNumberFormat(String)} will check if the {@link Environment}
+     * contains the custom format with the name, and if not, it will try {@link Template#getCustomDateFormat(String)} on
+     * the main template, which in turn might falls back to calling {@link Configuration#getCustomDateFormat(String)}.
+     */
+    TemplateDateFormatFactory getCustomDateFormat(String name);
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting mights returns a default value, or returns the value of the setting from a parent object, or throws
+     * an {@link SettingValueNotSetException}.
+     */
+    boolean isCustomDateFormatsSet();
+
+    /**
+     * The exception handler used to handle exceptions occurring inside templates.
+     * Its {@link Configuration}-level default is {@link TemplateExceptionHandler#DEBUG_HANDLER}. The recommended
+     * values are:
+     *
+     * <ul>
+     *   <li>In production systems: {@link TemplateExceptionHandler#RETHROW_HANDLER}
+     *   <li>During development of HTML templates: {@link TemplateExceptionHandler#HTML_DEBUG_HANDLER}
+     *   <li>During development of non-HTML templates: {@link TemplateExceptionHandler#DEBUG_HANDLER}
+     * </ul>
+     *
+     * <p>All of these will let the exception propagate further, so that you can catch it around
+     * {@link Template#process(Object, Writer)} for example. The difference is in what they print on the output before
+     * they do that.
+     *
+     * <p>Note that the {@link TemplateExceptionHandler} is not meant to be used for generating HTTP error pages.
+     * Neither is it meant to be used to roll back the printed output. These should be solved outside template
+     * processing when the exception raises from {@link Template#process(Object, Writer) Template.process}.
+     * {@link TemplateExceptionHandler} meant to be used if you want to include special content <em>in</em> the template
+     * output, or if you want to suppress certain exceptions.
+     */
+    TemplateExceptionHandler getTemplateExceptionHandler();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting mights returns a default value, or returns the value of the setting from a parent object, or throws
+     * an {@link SettingValueNotSetException}.
+     */
+    boolean isTemplateExceptionHandlerSet();
+
+    /**
+     * The arithmetic engine used to perform arithmetic operations.
+     * Its {@link Configuration}-level default is {@link BigDecimalArithmeticEngine#INSTANCE}.
+     * Note that this setting overlaps with {@link ParsingConfiguration#getArithmeticEngine()}.
+     */
+    ArithmeticEngine getArithmeticEngine();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting mights returns a default value, or returns the value of the setting from a parent object, or throws
+     * an {@link SettingValueNotSetException}.
+     */
+    boolean isArithmeticEngineSet();
+
+    /**
+     * The object wrapper used to wrap objects to {@link TemplateModel}-s.
+     * Its {@link Configuration}-level default is a {@link DefaultObjectWrapper} with all its setting on default
+     * values, and {@code incompatibleImprovements} set to {@link Configuration#getIncompatibleImprovements()}.
+     */
+    ObjectWrapper getObjectWrapper();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting mights returns a default value, or returns the value of the setting from a parent object, or throws
+     * an {@link SettingValueNotSetException}.
+     */
+    boolean isObjectWrapperSet();
+
+    /**
+     * Informs FreeMarker about the charset used for the output. As FreeMarker outputs character stream (not
+     * byte stream), it's not aware of the output charset unless the software that encloses it tells it
+     * with this setting. Some templates may use FreeMarker features that require this information.
+     * Setting this to {@code null} means that the output encoding is not known.
+     *
+     * <p>Its {@link Configuration}-level default is {@code null}.
+     */
+    Charset getOutputEncoding();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting mights returns a default value, or returns the value of the setting from a parent object, or throws
+     * an {@link SettingValueNotSetException}.
+     */
+    boolean isOutputEncodingSet();
+
+    /**
+     * The URL escaping (URL encoding, percentage encoding) charset. If ({@code null}), the output encoding
+     * ({@link #getOutputEncoding()}) will be used. Its {@link Configuration}-level default is {@code null}.
+     */
+    Charset getURLEscapingCharset();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting mights returns a default value, or returns the value of the setting from a parent object, or throws
+     * an {@link SettingValueNotSetException}.
+     */
+    boolean isURLEscapingCharsetSet();
+
+    /**
+     * The {@link TemplateClassResolver} that is used when the <code>new</code> built-in is called in a template. That
+     * is, when a template contains the <code>"com.example.SomeClassName"?new</code> expression, this object will be
+     * called to resolve the <code>"com.example.SomeClassName"</code> string to a class. The default value is {@link
+     * TemplateClassResolver#UNRESTRICTED_RESOLVER}. If you allow users to upload templates, it's important to use a
+     * custom restrictive {@link TemplateClassResolver} or {@link TemplateClassResolver#ALLOWS_NOTHING_RESOLVER}.
+     */
+    TemplateClassResolver getNewBuiltinClassResolver();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting mights returns a default value, or returns the value of the setting from a parent object, or throws
+     * an {@link SettingValueNotSetException}.
+     */
+    boolean isNewBuiltinClassResolverSet();
+
+    /**
+     * Specifies if {@code ?api} can be used in templates. Its {@link Configuration}-level is {@code false} (which
+     * is the safest option).
+     */
+    boolean getAPIBuiltinEnabled();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting mights returns a default value, or returns the value of the setting from a parent object, or throws
+     * an {@link SettingValueNotSetException}.
+     */
+    boolean isAPIBuiltinEnabledSet();
+
+    /**
+     * Whether the output {@link Writer} is automatically flushed at the end of {@link Template#process(Object, Writer)}
+     * (and its overloads). Its {@link Configuration}-level default is {@code true}.
+     * <p>
+     * Using {@code false} is needed for example when a Web page is composed from several boxes (like portlets, GUI
+     * panels, etc.) that aren't inserted with <tt>#include</tt> (or with similar directives) into a master FreeMarker
+     * template, rather they are all processed with a separate {@link Template#process(Object, Writer)} call. In a such
+     * scenario the automatic flushes would commit the HTTP response after each box, hence interfering with full-page
+     * buffering, and also possibly decreasing performance with too frequent and too early response buffer flushes.
+     */
+    boolean getAutoFlush();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting mights returns a default value, or returns the value of the setting from a parent object, or throws
+     * an {@link SettingValueNotSetException}.
+     */
+    boolean isAutoFlushSet();
+
+    /**
+     * Whether tips should be shown in error messages of errors arising during template processing.
+     * Its {@link Configuration}-level default is {@code true}.
+     */
+    boolean getShowErrorTips();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting mights returns a default value, or returns the value of the setting from a parent object, or throws
+     * an {@link SettingValueNotSetException}.
+     */
+    boolean isShowErrorTipsSet();
+
+    /**
+     * Specifies if {@link TemplateException}-s thrown by template processing are logged by FreeMarker or not. The
+     * default is {@code true} for backward compatibility, but that results in logging the exception twice in properly
+     * written applications, because there the {@link TemplateException} thrown by the public FreeMarker API is also
+     * logged by the caller (even if only as the cause exception of a higher level exception). Hence, in modern
+     * applications it should be set to {@code false}. Note that this setting has no effect on the logging of exceptions
+     * caught by {@code #attempt}; those are always logged, no mater what (because those exceptions won't bubble up
+     * until the API caller).
+     */
+    boolean getLogTemplateExceptions();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting mights returns a default value, or returns the value of the setting from a parent object, or throws
+     * an {@link SettingValueNotSetException}.
+     */
+    boolean isLogTemplateExceptionsSet();
+
+    /**
+     * Specifies if {@code <#import ...>} (and {@link Environment#importLib(String, String)}) should delay the loading
+     * and processing of the imported templates until the content of the imported namespace is actually accessed. This
+     * makes the overhead of <em>unused</em> imports negligible. A drawback is that importing a missing or otherwise
+     * broken template will be successful, and the problem will remain hidden until (and if) the namespace content is
+     * actually used. Also, you lose the strict control over when the namespace initializing code in the imported
+     * template will be executed, though it shouldn't mater for well written imported templates anyway. Note that the
+     * namespace initializing code will run with the same {@linkplain #getLocale() locale} as it was at the
+     * point of the {@code <#import ...>} call (other settings won't be handled specially like that).
+     * <p>
+     * The default is {@code false} (and thus imports are eager) for backward compatibility, which can cause
+     * perceivable overhead if you have many imports and only a few of them is used.
+     * <p>
+     * This setting also affects {@linkplain #getAutoImports() auto-imports}, unless you have set a non-{@code null}
+     * value with {@link #getLazyAutoImports()}.
+     *
+     * @see #getLazyAutoImports()
+     */
+    boolean getLazyImports();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting mights returns a default value, or returns the value of the setting from a parent object, or throws
+     * an {@link SettingValueNotSetException}.
+     */
+    boolean isLazyImportsSet();
+
+    /**
+     * Specifies if {@linkplain #getAutoImports() auto-imports} will be
+     * {@link #getLazyImports() lazy imports}. This is useful to make the overhead of <em>unused</em>
+     * auto-imports negligible. If this is set to {@code null}, {@link #getLazyImports()} specifies the behavior of
+     * auto-imports too. The default value is {@code null}.
+     */
+    Boolean getLazyAutoImports();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting mights returns a default value, or returns the value of the setting from a parent object, or throws
+     * an {@link SettingValueNotSetException}.
+     */
+    boolean isLazyAutoImportsSet();
+
+    /**
+     * Adds invisible <code>#import <i>templateName</i> as <i>namespaceVarName</i></code> statements at the beginning of
+     * the main template (that's the top-level template that wasn't included/imported from another template). While
+     * it only affects the main template directly, as the imports will create a global variable there, the imports
+     * will be visible from the further imported templates too.
+     * <p>
+     * It's recommended to set the {@link Configuration#getLazyAutoImports() lazyAutoImports} setting to {@code true}
+     * when using this, so that auto-imports that are unused in a template won't degrade performance by unnecessary
+     * loading and initializing the imported library.
+     * <p>
+     * If the imports aren't lazy, the order of the imports will be the same as the order in which the {@link Map}
+     * iterates through its entries.
+     * <p>
+     * When the {@link ProcessingConfiguration} is part of a setting inheritance chain ({@link Environment} inherits
+     * settings from the main {@link Template}, which inherits from the {@link Configuration}), you still only get the
+     * {@link Map} from the closest {@link ProcessingConfiguration} where it was set, not a {@link Map} that respects
+     * inheritance. But FreeMarker will walk the whole inheritance chain, executing all auto-imports starting
+     * from the ancestors. If, however, the same auto-import <code><i>namespaceVarName</i></code> occurs in multiple
+     * {@link ProcessingConfiguration}-s of the chain, only the one in the last (child)
+     * {@link ProcessingConfiguration} will be executed.
+     * <p>
+     * If there are also auto-includes (see {@link #getAutoIncludes()}), those will be executed after the auto-imports.
+     * <p>
+     * The {@link Configuration}-level default of this setting is an empty {@link Map}.
+     *
+     * @return Never {@code null}
+     */
+    Map<String, String> getAutoImports();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting mights returns a default value, or returns the value of the setting from a parent object, or throws
+     * an {@link SettingValueNotSetException}.
+     */
+    boolean isAutoImportsSet();
+
+    /**
+     * Adds an invisible <code>#include <i>templateName</i></code> at the beginning of the main template (that's the
+     * top-level template that wasn't included/imported from another template).
+     * <p>
+     * The order of the inclusions will be the same as the order in this {@link List}.
+     * <p>
+     * When the {@link ProcessingConfiguration} is part of a setting inheritance chain ({@link Environment} inherits
+     * settings from the main {@link Template}, which inherits from the {@link Configuration}), you still only get the
+     * {@link List} from the closest {@link ProcessingConfiguration} where it was set, not a {@link List} that respects
+     * inheritance. But FreeMarker will walk the whole inheritance chain, executing all auto-imports starting
+     * from the ancestors. If, however, the same auto-included template name occurs in multiple
+     * {@link ProcessingConfiguration}-s of the chain, only the one in the last (child)
+     * {@link ProcessingConfiguration} will be executed.
+     * <p>
+     * If there are also auto-imports ({@link #getAutoImports()}), those imports will be executed before
+     * the auto-includes, hence the namespace variables are alrady accessible for the auto-included templates.
+     */
+    List<String> getAutoIncludes();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting mights returns a default value, or returns the value of the setting from a parent object, or throws
+     * an {@link SettingValueNotSetException}.
+     */
+    boolean isAutoIncludesSet();
+
+    /**
+     * The {@code Map} of custom attributes. Custom attributes are key-value pairs associated to a
+     * {@link ProcessingConfiguration} objects, which meant to be used for storing application or framework specific
+     * configuration settings. The FreeMarker core doesn't define any attributes. Note that to store
+     * {@link ProcessingConfiguration}-scoped state (such as application or framework specific caches) you should use
+     * the methods provided by the {@link CustomStateScope} instead.
+     * <p>
+     * When the {@link ProcessingConfiguration} is part of a setting inheritance chain ({@link Environment} inherits
+     * settings from the main {@link Template}, which inherits from the {@link Configuration}), you still only get the
+     * {@link Map} from the closest {@link ProcessingConfiguration} where it was set, not a {@link Map} that respects
+     * inheritance. Thus to get attributes, you shouldn't use this {@link Map} directly, but
+     * {@link #getCustomAttribute(Object)} that will search the custom attribute in the whole inheritance chain.
+     */
+    Map<Object, Object> getCustomAttributes();
+
+    /**
+     * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+     * the setting mights returns a default value, or returns the value of the setting from a parent object, or throws
+     * an {@link SettingValueNotSetException}.
+     */
+    boolean isCustomAttributesSet();
+
+    /**
+     * Retrieves a custom attribute for this {@link ProcessingConfiguration}. If the attribute is not present in the
+     * {@link ProcessingConfiguration}, but it inherits from another {@link ProcessingConfiguration}, then the attribute
+     * is searched the as well.
+     *
+     * @param key
+     *         the identifier (usually a name) of the custom attribute
+     *
+     * @return the value of the custom attribute. Note that if the custom attribute was created with
+     * <tt>&lt;#ftl&nbsp;attributes={...}&gt;</tt>, then this value is already unwrapped (i.e. it's a
+     * <code>String</code>, or a <code>List</code>, or a <code>Map</code>, ...etc., not a FreeMarker specific class).
+     */
+    Object getCustomAttribute(Object key);
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/RangeModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/RangeModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/RangeModel.java
new file mode 100644
index 0000000..45f9345
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/RangeModel.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 org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+
+abstract class RangeModel implements TemplateSequenceModel, java.io.Serializable {
+    
+    private final int begin;
+
+    public RangeModel(int begin) {
+        this.begin = begin;
+    }
+
+    final int getBegining() {
+        return begin;
+    }
+    
+    @Override
+    final public TemplateModel get(int index) throws TemplateModelException {
+        if (index < 0 || index >= size()) {
+            throw new _TemplateModelException("Range item index ", Integer.valueOf(index), " is out of bounds.");
+        }
+        long value = begin + getStep() * (long) index;
+        return value <= Integer.MAX_VALUE ? new SimpleNumber((int) value) : new SimpleNumber(value);
+    }
+    
+    /**
+     * @return {@code 1} or {@code -1}; other return values need not be properly handled until FTL supports other steps.
+     */
+    abstract int getStep();
+    
+    abstract boolean isRightUnbounded();
+    
+    abstract boolean isRightAdaptive();
+    
+    abstract boolean isAffactedByStringSlicingBug();
+
+}


[13/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/FTLUtil.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/FTLUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/FTLUtil.java
new file mode 100644
index 0000000..b9c9e80
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/FTLUtil.java
@@ -0,0 +1,805 @@
+/*
+ * 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.util;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core._CoreAPI;
+import org.apache.freemarker.core.model.AdapterTemplateModel;
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateCollectionModelEx;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateDirectiveModel;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.TemplateMethodModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+import org.apache.freemarker.core.model.TemplateNodeModel;
+import org.apache.freemarker.core.model.TemplateNodeModelEx;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.TemplateTransformModel;
+import org.apache.freemarker.core.model.WrapperTemplateModel;
+import org.apache.freemarker.core.model.impl.BeanAndStringModel;
+import org.apache.freemarker.core.model.impl.BeanModel;
+
+/**
+ * Static utility methods that perform tasks specific to the FreeMarker Template Language (FTL).
+ * This is meant to be used from outside FreeMarker (i.e., it's an official, published API), not just from inside it.
+ *
+ * @since 3.0.0
+ */
+public final class FTLUtil {
+
+    private static final char[] ESCAPES = createEscapes();
+
+    private FTLUtil() {
+        // Not meant to be instantiated
+    }
+
+    private static char[] createEscapes() {
+        char[] escapes = new char['\\' + 1];
+        for (int i = 0; i < 32; ++i) {
+            escapes[i] = 1;
+        }
+        escapes['\\'] = '\\';
+        escapes['\''] = '\'';
+        escapes['"'] = '"';
+        escapes['<'] = 'l';
+        escapes['>'] = 'g';
+        escapes['&'] = 'a';
+        escapes['\b'] = 'b';
+        escapes['\t'] = 't';
+        escapes['\n'] = 'n';
+        escapes['\f'] = 'f';
+        escapes['\r'] = 'r';
+        return escapes;
+    }
+
+    /**
+     * Escapes a string according the FTL string literal escaping rules, assuming the literal is quoted with
+     * {@code quotation}; it doesn't add the quotation marks themselves.
+     *
+     * @param quotation Either {@code '"'} or {@code '\''}. It's assumed that the string literal whose part we calculate is
+     *                  enclosed within this kind of quotation mark. Thus, the other kind of quotation character will not be
+     *                  escaped in the result.
+     * @since 2.3.22
+     */
+    public static String escapeStringLiteralPart(String s, char quotation) {
+        return escapeStringLiteralPart(s, quotation, false);
+    }
+
+    /**
+     * Escapes a string according the FTL string literal escaping rules; it doesn't add the quotation marks themselves.
+     * As this method doesn't know if the string literal is quoted with regular quotation marks or apostrophe quote, it
+     * will escape both.
+     *
+     * @see #escapeStringLiteralPart(String, char)
+     */
+    public static String escapeStringLiteralPart(String s) {
+        return escapeStringLiteralPart(s, (char) 0, false);
+    }
+
+    private static String escapeStringLiteralPart(String s, char quotation, boolean addQuotation) {
+        final int ln = s.length();
+
+        final char otherQuotation;
+        if (quotation == 0) {
+            otherQuotation = 0;
+        } else if (quotation == '"') {
+            otherQuotation = '\'';
+        } else if (quotation == '\'') {
+            otherQuotation = '"';
+        } else {
+            throw new IllegalArgumentException("Unsupported quotation character: " + quotation);
+        }
+
+        final int escLn = ESCAPES.length;
+        StringBuilder buf = null;
+        for (int i = 0; i < ln; i++) {
+            char c = s.charAt(i);
+            char escape =
+                    c < escLn ? ESCAPES[c] :
+                            c == '{' && i > 0 && isInterpolationStart(s.charAt(i - 1)) ? '{' :
+                                    0;
+            if (escape == 0 || escape == otherQuotation) {
+                if (buf != null) {
+                    buf.append(c);
+                }
+            } else {
+                if (buf == null) {
+                    buf = new StringBuilder(s.length() + 4 + (addQuotation ? 2 : 0));
+                    if (addQuotation) {
+                        buf.append(quotation);
+                    }
+                    buf.append(s.substring(0, i));
+                }
+                if (escape == 1) {
+                    // hex encoding for characters below 0x20
+                    // that have no other escape representation
+                    buf.append("\\x00");
+                    int c2 = (c >> 4) & 0x0F;
+                    c = (char) (c & 0x0F);
+                    buf.append((char) (c2 < 10 ? c2 + '0' : c2 - 10 + 'A'));
+                    buf.append((char) (c < 10 ? c + '0' : c - 10 + 'A'));
+                } else {
+                    buf.append('\\');
+                    buf.append(escape);
+                }
+            }
+        }
+
+        if (buf == null) {
+            return addQuotation ? quotation + s + quotation : s;
+        } else {
+            if (addQuotation) {
+                buf.append(quotation);
+            }
+            return buf.toString();
+        }
+    }
+
+    private static boolean isInterpolationStart(char c) {
+        return c == '$' || c == '#';
+    }
+
+    /**
+     * Unescapes a string that was escaped to be part of an FTL string literal. The string to unescape most not include
+     * the two quotation marks or two apostrophe-quotes that delimit the literal.
+     * <p>
+     * \\, \", \', \n, \t, \r, \b and \f will be replaced according to
+     * Java rules. In additional, it knows \g, \l, \a and \{ which are
+     * replaced with &lt;, &gt;, &amp; and { respectively.
+     * \x works as hexadecimal character code escape. The character
+     * codes are interpreted according to UCS basic plane (Unicode).
+     * "f\x006Fo", "f\x06Fo" and "f\x6Fo" will be "foo".
+     * "f\x006F123" will be "foo123" as the maximum number of digits is 4.
+     * <p>
+     * All other \X (where X is any character not mentioned above or End-of-string)
+     * will cause a ParseException.
+     *
+     * @param s String literal <em>without</em> the surrounding quotation marks
+     * @return String with all escape sequences resolved
+     * @throws GenericParseException if there string contains illegal escapes
+     */
+    public static String unescapeStringLiteralPart(String s) throws GenericParseException {
+
+        int idx = s.indexOf('\\');
+        if (idx == -1) {
+            return s;
+        }
+
+        int lidx = s.length() - 1;
+        int bidx = 0;
+        StringBuilder buf = new StringBuilder(lidx);
+        do {
+            buf.append(s.substring(bidx, idx));
+            if (idx >= lidx) {
+                throw new GenericParseException("The last character of string literal is backslash");
+            }
+            char c = s.charAt(idx + 1);
+            switch (c) {
+                case '"':
+                    buf.append('"');
+                    bidx = idx + 2;
+                    break;
+                case '\'':
+                    buf.append('\'');
+                    bidx = idx + 2;
+                    break;
+                case '\\':
+                    buf.append('\\');
+                    bidx = idx + 2;
+                    break;
+                case 'n':
+                    buf.append('\n');
+                    bidx = idx + 2;
+                    break;
+                case 'r':
+                    buf.append('\r');
+                    bidx = idx + 2;
+                    break;
+                case 't':
+                    buf.append('\t');
+                    bidx = idx + 2;
+                    break;
+                case 'f':
+                    buf.append('\f');
+                    bidx = idx + 2;
+                    break;
+                case 'b':
+                    buf.append('\b');
+                    bidx = idx + 2;
+                    break;
+                case 'g':
+                    buf.append('>');
+                    bidx = idx + 2;
+                    break;
+                case 'l':
+                    buf.append('<');
+                    bidx = idx + 2;
+                    break;
+                case 'a':
+                    buf.append('&');
+                    bidx = idx + 2;
+                    break;
+                case '{':
+                    buf.append('{');
+                    bidx = idx + 2;
+                    break;
+                case 'x': {
+                    idx += 2;
+                    int x = idx;
+                    int y = 0;
+                    int z = lidx > idx + 3 ? idx + 3 : lidx;
+                    while (idx <= z) {
+                        char b = s.charAt(idx);
+                        if (b >= '0' && b <= '9') {
+                            y <<= 4;
+                            y += b - '0';
+                        } else if (b >= 'a' && b <= 'f') {
+                            y <<= 4;
+                            y += b - 'a' + 10;
+                        } else if (b >= 'A' && b <= 'F') {
+                            y <<= 4;
+                            y += b - 'A' + 10;
+                        } else {
+                            break;
+                        }
+                        idx++;
+                    }
+                    if (x < idx) {
+                        buf.append((char) y);
+                    } else {
+                        throw new GenericParseException("Invalid \\x escape in a string literal");
+                    }
+                    bidx = idx;
+                    break;
+                }
+                default:
+                    throw new GenericParseException("Invalid escape sequence (\\" + c + ") in a string literal");
+            }
+            idx = s.indexOf('\\', bidx);
+        } while (idx != -1);
+        buf.append(s.substring(bidx));
+
+        return buf.toString();
+    }
+
+    /**
+     * Creates a <em>quoted</em> FTL string literal from a string, using escaping where necessary. The result either
+     * uses regular quotation marks (UCS 0x22) or apostrophe-quotes (UCS 0x27), depending on the string content.
+     * (Currently, apostrophe-quotes will be chosen exactly when the string contains regular quotation character and
+     * doesn't contain apostrophe-quote character.)
+     *
+     * @param s The value that should be converted to an FTL string literal whose evaluated value equals to {@code s}
+     * @since 2.3.22
+     */
+    public static String toStringLiteral(String s) {
+        char quotation;
+        if (s.indexOf('"') != -1 && s.indexOf('\'') == -1) {
+            quotation = '\'';
+        } else {
+            quotation = '\"';
+        }
+        return escapeStringLiteralPart(s, quotation, true);
+    }
+
+    /**
+     * Tells if a character can occur on the beginning of an FTL identifier expression (without escaping).
+     *
+     * @since 2.3.22
+     */
+    public static boolean isNonEscapedIdentifierStart(final char c) {
+        // This code was generated on JDK 1.8.0_20 Win64 with src/main/misc/identifierChars/IdentifierCharGenerator.java
+        if (c < 0xAA) { // This branch was edited for speed.
+            if (c >= 'a' && c <= 'z' || c >= '@' && c <= 'Z') {
+                return true;
+            } else {
+                return c == '$' || c == '_';
+            }
+        } else { // c >= 0xAA
+            if (c < 0xA7F8) {
+                if (c < 0x2D6F) {
+                    if (c < 0x2128) {
+                        if (c < 0x2090) {
+                            if (c < 0xD8) {
+                                if (c < 0xBA) {
+                                    return c == 0xAA || c == 0xB5;
+                                } else { // c >= 0xBA
+                                    return c == 0xBA || c >= 0xC0 && c <= 0xD6;
+                                }
+                            } else { // c >= 0xD8
+                                if (c < 0x2071) {
+                                    return c >= 0xD8 && c <= 0xF6 || c >= 0xF8 && c <= 0x1FFF;
+                                } else { // c >= 0x2071
+                                    return c == 0x2071 || c == 0x207F;
+                                }
+                            }
+                        } else { // c >= 0x2090
+                            if (c < 0x2115) {
+                                if (c < 0x2107) {
+                                    return c >= 0x2090 && c <= 0x209C || c == 0x2102;
+                                } else { // c >= 0x2107
+                                    return c == 0x2107 || c >= 0x210A && c <= 0x2113;
+                                }
+                            } else { // c >= 0x2115
+                                if (c < 0x2124) {
+                                    return c == 0x2115 || c >= 0x2119 && c <= 0x211D;
+                                } else { // c >= 0x2124
+                                    return c == 0x2124 || c == 0x2126;
+                                }
+                            }
+                        }
+                    } else { // c >= 0x2128
+                        if (c < 0x2C30) {
+                            if (c < 0x2145) {
+                                if (c < 0x212F) {
+                                    return c == 0x2128 || c >= 0x212A && c <= 0x212D;
+                                } else { // c >= 0x212F
+                                    return c >= 0x212F && c <= 0x2139 || c >= 0x213C && c <= 0x213F;
+                                }
+                            } else { // c >= 0x2145
+                                if (c < 0x2183) {
+                                    return c >= 0x2145 && c <= 0x2149 || c == 0x214E;
+                                } else { // c >= 0x2183
+                                    return c >= 0x2183 && c <= 0x2184 || c >= 0x2C00 && c <= 0x2C2E;
+                                }
+                            }
+                        } else { // c >= 0x2C30
+                            if (c < 0x2D00) {
+                                if (c < 0x2CEB) {
+                                    return c >= 0x2C30 && c <= 0x2C5E || c >= 0x2C60 && c <= 0x2CE4;
+                                } else { // c >= 0x2CEB
+                                    return c >= 0x2CEB && c <= 0x2CEE || c >= 0x2CF2 && c <= 0x2CF3;
+                                }
+                            } else { // c >= 0x2D00
+                                if (c < 0x2D2D) {
+                                    return c >= 0x2D00 && c <= 0x2D25 || c == 0x2D27;
+                                } else { // c >= 0x2D2D
+                                    return c == 0x2D2D || c >= 0x2D30 && c <= 0x2D67;
+                                }
+                            }
+                        }
+                    }
+                } else { // c >= 0x2D6F
+                    if (c < 0x31F0) {
+                        if (c < 0x2DD0) {
+                            if (c < 0x2DB0) {
+                                if (c < 0x2DA0) {
+                                    return c == 0x2D6F || c >= 0x2D80 && c <= 0x2D96;
+                                } else { // c >= 0x2DA0
+                                    return c >= 0x2DA0 && c <= 0x2DA6 || c >= 0x2DA8 && c <= 0x2DAE;
+                                }
+                            } else { // c >= 0x2DB0
+                                if (c < 0x2DC0) {
+                                    return c >= 0x2DB0 && c <= 0x2DB6 || c >= 0x2DB8 && c <= 0x2DBE;
+                                } else { // c >= 0x2DC0
+                                    return c >= 0x2DC0 && c <= 0x2DC6 || c >= 0x2DC8 && c <= 0x2DCE;
+                                }
+                            }
+                        } else { // c >= 0x2DD0
+                            if (c < 0x3031) {
+                                if (c < 0x2E2F) {
+                                    return c >= 0x2DD0 && c <= 0x2DD6 || c >= 0x2DD8 && c <= 0x2DDE;
+                                } else { // c >= 0x2E2F
+                                    return c == 0x2E2F || c >= 0x3005 && c <= 0x3006;
+                                }
+                            } else { // c >= 0x3031
+                                if (c < 0x3040) {
+                                    return c >= 0x3031 && c <= 0x3035 || c >= 0x303B && c <= 0x303C;
+                                } else { // c >= 0x3040
+                                    return c >= 0x3040 && c <= 0x318F || c >= 0x31A0 && c <= 0x31BA;
+                                }
+                            }
+                        }
+                    } else { // c >= 0x31F0
+                        if (c < 0xA67F) {
+                            if (c < 0xA4D0) {
+                                if (c < 0x3400) {
+                                    return c >= 0x31F0 && c <= 0x31FF || c >= 0x3300 && c <= 0x337F;
+                                } else { // c >= 0x3400
+                                    return c >= 0x3400 && c <= 0x4DB5 || c >= 0x4E00 && c <= 0xA48C;
+                                }
+                            } else { // c >= 0xA4D0
+                                if (c < 0xA610) {
+                                    return c >= 0xA4D0 && c <= 0xA4FD || c >= 0xA500 && c <= 0xA60C;
+                                } else { // c >= 0xA610
+                                    return c >= 0xA610 && c <= 0xA62B || c >= 0xA640 && c <= 0xA66E;
+                                }
+                            }
+                        } else { // c >= 0xA67F
+                            if (c < 0xA78B) {
+                                if (c < 0xA717) {
+                                    return c >= 0xA67F && c <= 0xA697 || c >= 0xA6A0 && c <= 0xA6E5;
+                                } else { // c >= 0xA717
+                                    return c >= 0xA717 && c <= 0xA71F || c >= 0xA722 && c <= 0xA788;
+                                }
+                            } else { // c >= 0xA78B
+                                if (c < 0xA7A0) {
+                                    return c >= 0xA78B && c <= 0xA78E || c >= 0xA790 && c <= 0xA793;
+                                } else { // c >= 0xA7A0
+                                    return c >= 0xA7A0 && c <= 0xA7AA;
+                                }
+                            }
+                        }
+                    }
+                }
+            } else { // c >= 0xA7F8
+                if (c < 0xAB20) {
+                    if (c < 0xAA44) {
+                        if (c < 0xA8FB) {
+                            if (c < 0xA840) {
+                                if (c < 0xA807) {
+                                    return c >= 0xA7F8 && c <= 0xA801 || c >= 0xA803 && c <= 0xA805;
+                                } else { // c >= 0xA807
+                                    return c >= 0xA807 && c <= 0xA80A || c >= 0xA80C && c <= 0xA822;
+                                }
+                            } else { // c >= 0xA840
+                                if (c < 0xA8D0) {
+                                    return c >= 0xA840 && c <= 0xA873 || c >= 0xA882 && c <= 0xA8B3;
+                                } else { // c >= 0xA8D0
+                                    return c >= 0xA8D0 && c <= 0xA8D9 || c >= 0xA8F2 && c <= 0xA8F7;
+                                }
+                            }
+                        } else { // c >= 0xA8FB
+                            if (c < 0xA984) {
+                                if (c < 0xA930) {
+                                    return c == 0xA8FB || c >= 0xA900 && c <= 0xA925;
+                                } else { // c >= 0xA930
+                                    return c >= 0xA930 && c <= 0xA946 || c >= 0xA960 && c <= 0xA97C;
+                                }
+                            } else { // c >= 0xA984
+                                if (c < 0xAA00) {
+                                    return c >= 0xA984 && c <= 0xA9B2 || c >= 0xA9CF && c <= 0xA9D9;
+                                } else { // c >= 0xAA00
+                                    return c >= 0xAA00 && c <= 0xAA28 || c >= 0xAA40 && c <= 0xAA42;
+                                }
+                            }
+                        }
+                    } else { // c >= 0xAA44
+                        if (c < 0xAAC0) {
+                            if (c < 0xAA80) {
+                                if (c < 0xAA60) {
+                                    return c >= 0xAA44 && c <= 0xAA4B || c >= 0xAA50 && c <= 0xAA59;
+                                } else { // c >= 0xAA60
+                                    return c >= 0xAA60 && c <= 0xAA76 || c == 0xAA7A;
+                                }
+                            } else { // c >= 0xAA80
+                                if (c < 0xAAB5) {
+                                    return c >= 0xAA80 && c <= 0xAAAF || c == 0xAAB1;
+                                } else { // c >= 0xAAB5
+                                    return c >= 0xAAB5 && c <= 0xAAB6 || c >= 0xAAB9 && c <= 0xAABD;
+                                }
+                            }
+                        } else { // c >= 0xAAC0
+                            if (c < 0xAAF2) {
+                                if (c < 0xAADB) {
+                                    return c == 0xAAC0 || c == 0xAAC2;
+                                } else { // c >= 0xAADB
+                                    return c >= 0xAADB && c <= 0xAADD || c >= 0xAAE0 && c <= 0xAAEA;
+                                }
+                            } else { // c >= 0xAAF2
+                                if (c < 0xAB09) {
+                                    return c >= 0xAAF2 && c <= 0xAAF4 || c >= 0xAB01 && c <= 0xAB06;
+                                } else { // c >= 0xAB09
+                                    return c >= 0xAB09 && c <= 0xAB0E || c >= 0xAB11 && c <= 0xAB16;
+                                }
+                            }
+                        }
+                    }
+                } else { // c >= 0xAB20
+                    if (c < 0xFB46) {
+                        if (c < 0xFB13) {
+                            if (c < 0xAC00) {
+                                if (c < 0xABC0) {
+                                    return c >= 0xAB20 && c <= 0xAB26 || c >= 0xAB28 && c <= 0xAB2E;
+                                } else { // c >= 0xABC0
+                                    return c >= 0xABC0 && c <= 0xABE2 || c >= 0xABF0 && c <= 0xABF9;
+                                }
+                            } else { // c >= 0xAC00
+                                if (c < 0xD7CB) {
+                                    return c >= 0xAC00 && c <= 0xD7A3 || c >= 0xD7B0 && c <= 0xD7C6;
+                                } else { // c >= 0xD7CB
+                                    return c >= 0xD7CB && c <= 0xD7FB || c >= 0xF900 && c <= 0xFB06;
+                                }
+                            }
+                        } else { // c >= 0xFB13
+                            if (c < 0xFB38) {
+                                if (c < 0xFB1F) {
+                                    return c >= 0xFB13 && c <= 0xFB17 || c == 0xFB1D;
+                                } else { // c >= 0xFB1F
+                                    return c >= 0xFB1F && c <= 0xFB28 || c >= 0xFB2A && c <= 0xFB36;
+                                }
+                            } else { // c >= 0xFB38
+                                if (c < 0xFB40) {
+                                    return c >= 0xFB38 && c <= 0xFB3C || c == 0xFB3E;
+                                } else { // c >= 0xFB40
+                                    return c >= 0xFB40 && c <= 0xFB41 || c >= 0xFB43 && c <= 0xFB44;
+                                }
+                            }
+                        }
+                    } else { // c >= 0xFB46
+                        if (c < 0xFF21) {
+                            if (c < 0xFDF0) {
+                                if (c < 0xFD50) {
+                                    return c >= 0xFB46 && c <= 0xFBB1 || c >= 0xFBD3 && c <= 0xFD3D;
+                                } else { // c >= 0xFD50
+                                    return c >= 0xFD50 && c <= 0xFD8F || c >= 0xFD92 && c <= 0xFDC7;
+                                }
+                            } else { // c >= 0xFDF0
+                                if (c < 0xFE76) {
+                                    return c >= 0xFDF0 && c <= 0xFDFB || c >= 0xFE70 && c <= 0xFE74;
+                                } else { // c >= 0xFE76
+                                    return c >= 0xFE76 && c <= 0xFEFC || c >= 0xFF10 && c <= 0xFF19;
+                                }
+                            }
+                        } else { // c >= 0xFF21
+                            if (c < 0xFFCA) {
+                                if (c < 0xFF66) {
+                                    return c >= 0xFF21 && c <= 0xFF3A || c >= 0xFF41 && c <= 0xFF5A;
+                                } else { // c >= 0xFF66
+                                    return c >= 0xFF66 && c <= 0xFFBE || c >= 0xFFC2 && c <= 0xFFC7;
+                                }
+                            } else { // c >= 0xFFCA
+                                if (c < 0xFFDA) {
+                                    return c >= 0xFFCA && c <= 0xFFCF || c >= 0xFFD2 && c <= 0xFFD7;
+                                } else { // c >= 0xFFDA
+                                    return c >= 0xFFDA && c <= 0xFFDC;
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Tells if a character can occur in an FTL identifier expression (without escaping) as other than the first
+     * character.
+     */
+    public static boolean isNonEscapedIdentifierPart(final char c) {
+        return isNonEscapedIdentifierStart(c) || (c >= '0' && c <= '9');
+    }
+
+    /**
+     * Tells if a given character, for which {@link #isNonEscapedIdentifierStart(char)} and
+     * {@link #isNonEscapedIdentifierPart(char)} is {@code false}, can occur in an identifier if it's preceded by a
+     * backslash. Currently it return {@code true} for these: {@code '-'}, {@code '.'} and {@code ':'}.
+     */
+    public static boolean isEscapedIdentifierCharacter(final char c) {
+        return c == '-' || c == '.' || c == ':';
+    }
+
+    /**
+     * Escapes characters in the string that can only occur in FTL identifiers (variable names) escaped.
+     * This means adding a backslash before any character for which {@link #isEscapedIdentifierCharacter(char)}
+     * is {@code true}. Other characters will be left unescaped, even if they aren't valid in FTL identifiers.
+     *
+     * @param s The identifier to escape. If {@code null}, {@code null} is returned.
+     */
+    public static String escapeIdentifier(String s) {
+        if (s == null) {
+            return null;
+        }
+
+        int ln = s.length();
+
+        // First we find out if we need to escape, and if so, what the length of the output will be:
+        int firstEscIdx = -1;
+        int lastEscIdx = 0;
+        int plusOutLn = 0;
+        for (int i = 0; i < ln; i++) {
+            char c = s.charAt(i);
+            if (isEscapedIdentifierCharacter(c)) {
+                if (firstEscIdx == -1) {
+                    firstEscIdx = i;
+                }
+                lastEscIdx = i;
+                plusOutLn++;
+            }
+        }
+
+        if (firstEscIdx == -1) {
+            return s; // Nothing to escape
+        } else {
+            char[] esced = new char[ln + plusOutLn];
+            if (firstEscIdx != 0) {
+                s.getChars(0, firstEscIdx, esced, 0);
+            }
+            int dst = firstEscIdx;
+            for (int i = firstEscIdx; i <= lastEscIdx; i++) {
+                char c = s.charAt(i);
+                if (isEscapedIdentifierCharacter(c)) {
+                    esced[dst++] = '\\';
+                }
+                esced[dst++] = c;
+            }
+            if (lastEscIdx != ln - 1) {
+                s.getChars(lastEscIdx + 1, ln, esced, dst);
+            }
+
+            return String.valueOf(esced);
+        }
+    }
+
+    /**
+     * Returns the type description of a value with FTL terms (not plain class name), as it should be used in
+     * type-related error messages and for debugging purposes. The exact format is not specified and might change over
+     * time, but currently it's something like {@code "string (wrapper: f.t.SimpleScalar)"} or
+     * {@code "sequence+hash+string (ArrayList wrapped into f.e.b.CollectionModel)"}.
+     *
+     * @param tm The value whose type we will describe. If {@code null}, then {@code "Null"} is returned (without the
+     *           quotation marks).
+     *
+     * @since 2.3.20
+     */
+    public static String getTypeDescription(TemplateModel tm) {
+        if (tm == null) {
+            return "Null";
+        } else {
+            Set typeNamesAppended = new HashSet();
+
+            StringBuilder sb = new StringBuilder();
+
+            Class primaryInterface = getPrimaryTemplateModelInterface(tm);
+            if (primaryInterface != null) {
+                appendTemplateModelTypeName(sb, typeNamesAppended, primaryInterface);
+            }
+
+            if (_CoreAPI.isMacroOrFunction(tm)) {
+                appendTypeName(sb, typeNamesAppended, _CoreAPI.isFunction(tm) ? "function" : "macro");
+            }
+
+            appendTemplateModelTypeName(sb, typeNamesAppended, tm.getClass());
+
+            String javaClassName;
+            Class unwrappedClass = getUnwrappedClass(tm);
+            if (unwrappedClass != null) {
+                javaClassName = _ClassUtil.getShortClassName(unwrappedClass, true);
+            } else {
+                javaClassName = null;
+            }
+
+            sb.append(" (");
+            String modelClassName = _ClassUtil.getShortClassName(tm.getClass(), true);
+            if (javaClassName == null) {
+                sb.append("wrapper: ");
+                sb.append(modelClassName);
+            } else {
+                sb.append(javaClassName);
+                sb.append(" wrapped into ");
+                sb.append(modelClassName);
+            }
+            sb.append(")");
+
+            return sb.toString();
+        }
+    }
+
+    /**
+     * Returns the {@link TemplateModel} interface that is the most characteristic of the object, or {@code null}.
+     */
+    private static Class getPrimaryTemplateModelInterface(TemplateModel tm) {
+        if (tm instanceof BeanModel) {
+            if (tm instanceof BeanAndStringModel) {
+                Object wrapped = ((BeanModel) tm).getWrappedObject();
+                return wrapped instanceof String
+                        ? TemplateScalarModel.class
+                        : (tm instanceof TemplateHashModelEx ? TemplateHashModelEx.class : null);
+            } else {
+                return null;
+            }
+        } else {
+            return null;
+        }
+    }
+
+    private static void appendTemplateModelTypeName(StringBuilder sb, Set typeNamesAppended, Class cl) {
+        int initalLength = sb.length();
+
+        if (TemplateNodeModelEx.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "extended node");
+        } else if (TemplateNodeModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "node");
+        }
+
+        if (TemplateDirectiveModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "directive");
+        } else if (TemplateTransformModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "transform");
+        }
+
+        if (TemplateSequenceModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "sequence");
+        } else if (TemplateCollectionModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended,
+                    TemplateCollectionModelEx.class.isAssignableFrom(cl) ? "extended_collection" : "collection");
+        } else if (TemplateModelIterator.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "iterator");
+        }
+
+        if (TemplateMethodModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "method");
+        }
+
+        if (Environment.Namespace.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "namespace");
+        } else if (TemplateHashModelEx.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "extended_hash");
+        } else if (TemplateHashModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "hash");
+        }
+
+        if (TemplateNumberModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "number");
+        }
+
+        if (TemplateDateModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "date_or_time_or_datetime");
+        }
+
+        if (TemplateBooleanModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "boolean");
+        }
+
+        if (TemplateScalarModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "string");
+        }
+
+        if (TemplateMarkupOutputModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "markup_output");
+        }
+
+        if (sb.length() == initalLength) {
+            appendTypeName(sb, typeNamesAppended, "misc_template_model");
+        }
+    }
+
+    private static Class getUnwrappedClass(TemplateModel tm) {
+        Object unwrapped;
+        try {
+            if (tm instanceof WrapperTemplateModel) {
+                unwrapped = ((WrapperTemplateModel) tm).getWrappedObject();
+            } else if (tm instanceof AdapterTemplateModel) {
+                unwrapped = ((AdapterTemplateModel) tm).getAdaptedObject(Object.class);
+            } else {
+                unwrapped = null;
+            }
+        } catch (Throwable e) {
+            unwrapped = null;
+        }
+        return unwrapped != null ? unwrapped.getClass() : null;
+    }
+
+    private static void appendTypeName(StringBuilder sb, Set typeNamesAppended, String name) {
+        if (!typeNamesAppended.contains(name)) {
+            if (sb.length() != 0) sb.append("+");
+            sb.append(name);
+            typeNamesAppended.add(name);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/GenericParseException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/GenericParseException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/GenericParseException.java
new file mode 100644
index 0000000..6e53a3c
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/GenericParseException.java
@@ -0,0 +1,40 @@
+/*
+ * 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.util;
+
+import org.apache.freemarker.core.ParseException;
+
+/**
+ * Exception thrown when a we want to parse some text but its format doesn't match the expectations. This is a quite
+ * generic exception, which we use in cases that don't deserve a dedicated exception.
+ * 
+ * @see ParseException
+ */
+@SuppressWarnings("serial")
+public class GenericParseException extends Exception {
+
+    public GenericParseException(String message) {
+        super(message);
+    }
+
+    public GenericParseException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/HtmlEscape.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/HtmlEscape.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/HtmlEscape.java
new file mode 100644
index 0000000..3aa8d1d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/HtmlEscape.java
@@ -0,0 +1,109 @@
+/*
+ * 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.util;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Map;
+
+import org.apache.freemarker.core.model.TemplateTransformModel;
+
+/**
+ * Performs an HTML escape of a given template fragment. Specifically,
+ * &lt; &gt; &quot; and &amp; are all turned into entities.
+ *
+ * <p>Usage:<br>
+ * From java:</p>
+ * <pre>
+ * SimpleHash root = new SimpleHash();
+ *
+ * root.put( "htmlEscape", new org.apache.freemarker.core.util.HtmlEscape() );
+ *
+ * ...
+ * </pre>
+ *
+ * <p>From your FreeMarker template:</p>
+ * <pre>
+ *
+ * The following is HTML-escaped:
+ * &lt;transform htmlEscape&gt;
+ *   &lt;p&gt;This paragraph has all HTML special characters escaped.&lt;/p&gt;
+ * &lt;/transform&gt;
+ *
+ * ...
+ * </pre>
+ *
+ * @see org.apache.freemarker.core.util.XmlEscape
+ */
+// [FM3] Remove (or move to o.a.f.test)
+public class HtmlEscape implements TemplateTransformModel {
+
+    private static final char[] LT = "&lt;".toCharArray();
+    private static final char[] GT = "&gt;".toCharArray();
+    private static final char[] AMP = "&amp;".toCharArray();
+    private static final char[] QUOT = "&quot;".toCharArray();
+
+    @Override
+    public Writer getWriter(final Writer out, Map args) {
+        return new Writer()
+        {
+            @Override
+            public void write(int c)
+            throws IOException {
+                switch(c)
+                {
+                    case '<': out.write(LT, 0, 4); break;
+                    case '>': out.write(GT, 0, 4); break;
+                    case '&': out.write(AMP, 0, 5); break;
+                    case '"': out.write(QUOT, 0, 6); break;
+                    default: out.write(c);
+                }
+            }
+
+            @Override
+            public void write(char cbuf[], int off, int len)
+            throws IOException {
+                int lastoff = off;
+                int lastpos = off + len;
+                for (int i = off; i < lastpos; i++) {
+                    switch (cbuf[i])
+                    {
+                        case '<': out.write(cbuf, lastoff, i - lastoff); out.write(LT, 0, 4); lastoff = i + 1; break;
+                        case '>': out.write(cbuf, lastoff, i - lastoff); out.write(GT, 0, 4); lastoff = i + 1; break;
+                        case '&': out.write(cbuf, lastoff, i - lastoff); out.write(AMP, 0, 5); lastoff = i + 1; break;
+                        case '"': out.write(cbuf, lastoff, i - lastoff); out.write(QUOT, 0, 6); lastoff = i + 1; break;
+                    }
+                }
+                int remaining = lastpos - lastoff;
+                if (remaining > 0) {
+                    out.write(cbuf, lastoff, remaining);
+                }
+            }
+            @Override
+            public void flush() throws IOException {
+                out.flush();
+            }
+
+            @Override
+            public void close() {
+            }
+        };
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/NormalizeNewlines.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/NormalizeNewlines.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/NormalizeNewlines.java
new file mode 100644
index 0000000..f4bc5a6
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/NormalizeNewlines.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.util;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.Map;
+
+import org.apache.freemarker.core.model.TemplateTransformModel;
+
+/**
+ * <p>Transformer that supports FreeMarker legacy behavior: all newlines appearing
+ * within the transformed area will be transformed into the platform's default
+ * newline. Unlike the old behavior, however, newlines generated by the data
+ * model are also converted. Legacy behavior was to leave newlines in the
+ * data model unaltered.</p>
+ *
+ * <p>Usage:<br>
+ * From java:</p>
+ * <pre>
+ * SimpleHash root = new SimpleHash();
+ *
+ * root.put( "normalizeNewlines", new org.apache.freemarker.core.util.NormalizeNewlines() );
+ *
+ * ...
+ * </pre>
+ *
+ * <p>From your FreeMarker template:</p>
+ * <pre>
+ * &lt;transform normalizeNewlines&gt;
+ *   &lt;html&gt;
+ *   &lt;head&gt;
+ *   ...
+ *   &lt;p&gt;This template has all newlines normalized to the current platform's
+ *   default.&lt;/p&gt;
+ *   ...
+ *   &lt;/body&gt;
+ *   &lt;/html&gt;
+ * &lt;/transform&gt;
+ * </pre>
+ */
+// [FM3] Remove (or move to o.a.f.test)
+public class NormalizeNewlines implements TemplateTransformModel {
+
+    @Override
+    public Writer getWriter(final Writer out,
+                            final Map args) {
+        final StringBuilder buf = new StringBuilder();
+        return new Writer() {
+            @Override
+            public void write(char cbuf[], int off, int len) {
+                buf.append(cbuf, off, len);
+            }
+
+            @Override
+            public void flush() throws IOException {
+                out.flush();
+            }
+
+            @Override
+            public void close() throws IOException {
+                StringReader sr = new StringReader(buf.toString());
+                StringWriter sw = new StringWriter();
+                transform(sr, sw);
+                out.write(sw.toString());
+            }
+        };
+    }
+
+    /**
+     * Performs newline normalization on FreeMarker output.
+     *
+     * @param in the input to be transformed
+     * @param out the destination of the transformation
+     */
+    public void transform(Reader in, Writer out) throws IOException {
+        BufferedReader br = (in instanceof BufferedReader)
+                            ? (BufferedReader) in
+                            : new BufferedReader(in);
+        PrintWriter pw = (out instanceof PrintWriter)
+                         ? (PrintWriter) out
+                         : new PrintWriter(out);
+        String line = br.readLine();
+        if (line != null) {
+            if ( line.length() > 0 ) {
+                pw.println(line);
+            }
+        }
+        while ((line = br.readLine()) != null) {
+            pw.println(line);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/ObjectFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/ObjectFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/ObjectFactory.java
new file mode 100644
index 0000000..370d08d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/ObjectFactory.java
@@ -0,0 +1,31 @@
+/*
+ * 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.util;
+
+/**
+ * Used for the trivial cases of the factory pattern. Has a generic type argument since 2.3.24.
+ * 
+ * @since 2.3.22
+ */
+public interface ObjectFactory<T> {
+    
+    T createObject() throws Exception;
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/OptInTemplateClassResolver.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/OptInTemplateClassResolver.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/OptInTemplateClassResolver.java
new file mode 100644
index 0000000..e1edfcb
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/OptInTemplateClassResolver.java
@@ -0,0 +1,160 @@
+/*
+ * 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.util;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.freemarker.core.MutableProcessingConfiguration;
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core.TemplateClassResolver;
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core._MiscTemplateException;
+
+/**
+ * A {@link TemplateClassResolver} that resolves only the classes whose name
+ * was specified in the constructor.
+ */
+public class OptInTemplateClassResolver implements TemplateClassResolver {
+    
+    private final Set/*<String>*/ allowedClasses;
+    private final List/*<String>*/ trustedTemplatePrefixes;
+    private final Set/*<String>*/ trustedTemplateNames;
+    
+    /**
+     * Creates a new instance. 
+     *
+     * @param allowedClasses the {@link Set} of {@link String}-s that contains
+     *     the full-qualified names of the allowed classes.
+     *     Can be <code>null</code> (means not class is allowed).
+     * @param trustedTemplates the {@link List} of {@link String}-s that contains
+     *     template names (i.e., template root directory relative paths)
+     *     and prefix patterns (like <code>"include/*"</code>) of templates
+     *     for which {@link TemplateClassResolver#UNRESTRICTED_RESOLVER} will be 
+     *     used (which is not as safe as {@link OptInTemplateClassResolver}).
+     *     The list items need not start with <code>"/"</code> (if they are, it
+     *     will be removed). List items ending with <code>"*"</code> are treated
+     *     as prefixes (i.e. <code>"foo*"</code> matches <code>"foobar"</code>,
+     *     <code>"foo/bar/baaz"</code>, <code>"foowhatever/bar/baaz"</code>,
+     *     etc.). The <code>"*"</code> has no special meaning anywhere else.
+     *     The matched template name is the name (template root directory
+     *     relative path) of the template that directly (lexically) contains the
+     *     operation (like <code>?new</code>) that wants to get the class. Thus,
+     *     if a trusted template includes a non-trusted template, the
+     *     <code>allowedClasses</code> restriction will apply in the included
+     *     template.
+     *     This parameter can be <code>null</code> (means no trusted templates).
+     */
+    public OptInTemplateClassResolver(
+            Set allowedClasses, List<String> trustedTemplates) {
+        this.allowedClasses = allowedClasses != null ? allowedClasses : Collections.EMPTY_SET;
+        if (trustedTemplates != null) {
+            trustedTemplateNames = new HashSet();
+            trustedTemplatePrefixes = new ArrayList();
+            
+            Iterator<String> it = trustedTemplates.iterator();
+            while (it.hasNext()) {
+                String li = it.next();
+                if (li.startsWith("/")) li = li.substring(1);
+                if (li.endsWith("*")) {
+                    trustedTemplatePrefixes.add(li.substring(0, li.length() - 1));
+                } else {
+                    trustedTemplateNames.add(li);
+                }
+            }
+        } else {
+            trustedTemplateNames = Collections.EMPTY_SET;
+            trustedTemplatePrefixes = Collections.EMPTY_LIST;
+        }
+    }
+
+    @Override
+    public Class resolve(String className, Environment env, Template template)
+    throws TemplateException {
+        String templateName = safeGetTemplateName(template);
+        
+        if (templateName != null
+                && (trustedTemplateNames.contains(templateName)
+                        || hasMatchingPrefix(templateName))) {
+            return TemplateClassResolver.UNRESTRICTED_RESOLVER.resolve(className, env, template);
+        } else {
+            if (!allowedClasses.contains(className)) {
+                throw new _MiscTemplateException(env,
+                        "Instantiating ", className, " is not allowed in the template for security reasons. (If you "
+                        + "run into this problem when using ?new in a template, you may want to check the \"",
+                        MutableProcessingConfiguration.NEW_BUILTIN_CLASS_RESOLVER_KEY,
+                        "\" setting in the FreeMarker configuration.)");
+            } else {
+                try {
+                    return _ClassUtil.forName(className);
+                } catch (ClassNotFoundException e) {
+                    throw new _MiscTemplateException(e, env);
+                }
+            }
+        }
+    }
+
+    /**
+     * Extract the template name from the template object which will be matched
+     * against the trusted template names and pattern. 
+     */
+    protected String safeGetTemplateName(Template template) {
+        if (template == null) return null;
+        
+        String name = template.getLookupName();
+        if (name == null) return null;
+
+        // Detect exploits, return null if one is suspected:
+        String decodedName = name;
+        if (decodedName.indexOf('%') != -1) {
+            decodedName = _StringUtil.replace(decodedName, "%2e", ".", false, false);
+            decodedName = _StringUtil.replace(decodedName, "%2E", ".", false, false);
+            decodedName = _StringUtil.replace(decodedName, "%2f", "/", false, false);
+            decodedName = _StringUtil.replace(decodedName, "%2F", "/", false, false);
+            decodedName = _StringUtil.replace(decodedName, "%5c", "\\", false, false);
+            decodedName = _StringUtil.replace(decodedName, "%5C", "\\", false, false);
+        }
+        int dotDotIdx = decodedName.indexOf("..");
+        if (dotDotIdx != -1) {
+            int before = dotDotIdx - 1 >= 0 ? decodedName.charAt(dotDotIdx - 1) : -1;
+            int after = dotDotIdx + 2 < decodedName.length() ? decodedName.charAt(dotDotIdx + 2) : -1;
+            if ((before == -1 || before == '/' || before == '\\')
+                    && (after == -1 || after == '/' || after == '\\')) {
+                return null;
+            }
+        }
+        
+        return name.startsWith("/") ? name.substring(1) : name;
+    }
+
+    private boolean hasMatchingPrefix(String name) {
+        for (int i = 0; i < trustedTemplatePrefixes.size(); i++) {
+            String prefix = (String) trustedTemplatePrefixes.get(i);
+            if (name.startsWith(prefix)) return true;
+        }
+        return false;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/ProductWrappingBuilder.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/ProductWrappingBuilder.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/ProductWrappingBuilder.java
new file mode 100644
index 0000000..4b76dc5
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/ProductWrappingBuilder.java
@@ -0,0 +1,38 @@
+/*
+ * 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.util;
+
+/**
+ * A builder that encloses an already built product. {@link #build()} will always return the same product object.
+ */
+public class ProductWrappingBuilder<ProductT> implements CommonBuilder<ProductT> {
+
+    private final ProductT product;
+
+    public ProductWrappingBuilder(ProductT product) {
+        _NullArgumentException.check("product", product);
+        this.product = product;
+    }
+
+    @Override
+    public ProductT build() {
+        return product;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/StandardCompress.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/StandardCompress.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/StandardCompress.java
new file mode 100644
index 0000000..0943622
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/StandardCompress.java
@@ -0,0 +1,239 @@
+/*
+ * 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.util;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Map;
+
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateTransformModel;
+
+/**
+ * <p>A filter that compresses each sequence of consecutive whitespace
+ * to a single line break (if the sequence contains a line break) or a
+ * single space. In addition, leading and trailing whitespace is
+ * completely removed.</p>
+ * 
+ * <p>Specify the transform parameter <code>single_line = true</code>
+ * to always compress to a single space instead of a line break.</p>
+ * 
+ * <p>The default buffer size can be overridden by specifying a
+ * <code>buffer_size</code> transform parameter (in bytes).</p>
+ *
+ * <p><b>Note:</b> The compress tag is implemented using this filter</p>
+ * 
+ * <p>Usage:<br>
+ * From java:</p>
+ * <pre>
+ * SimpleHash root = new SimpleHash();
+ *
+ * root.put( "standardCompress", new org.apache.freemarker.core.util.StandardCompress() );
+ *
+ * ...
+ * </pre>
+ *
+ * <p>From your FreeMarker template:</p>
+ * <pre>
+ * &lt;transform standardCompress&gt;
+ *   &lt;p&gt;This    paragraph will have
+ *       extraneous
+ *
+ * whitespace removed.&lt;/p&gt;
+ * &lt;/transform&gt;
+ * </pre>
+ *
+ * <p>Output:</p>
+ * <pre>
+ * &lt;p&gt;This paragraph will have
+ * extraneous
+ * whitespace removed.&lt;/p&gt;
+ * </pre>
+ */
+// [FM3] Remove (or move to o.a.f.test), instead extend #compress
+public class StandardCompress implements TemplateTransformModel {
+    private static final String BUFFER_SIZE_KEY = "buffer_size";
+    private static final String SINGLE_LINE_KEY = "single_line";
+    private int defaultBufferSize;
+
+    public static final StandardCompress INSTANCE = new StandardCompress();
+    
+    public StandardCompress() {
+        this(2048);
+    }
+
+    /**
+     * @param defaultBufferSize the default amount of characters to buffer
+     */
+    public StandardCompress(int defaultBufferSize) {
+        this.defaultBufferSize = defaultBufferSize;
+    }
+
+    @Override
+    public Writer getWriter(final Writer out, Map args)
+    throws TemplateModelException {
+        int bufferSize = defaultBufferSize;
+        boolean singleLine = false;
+        if (args != null) {
+            try {
+                TemplateNumberModel num = (TemplateNumberModel) args.get(BUFFER_SIZE_KEY);
+                if (num != null)
+                    bufferSize = num.getAsNumber().intValue();
+            } catch (ClassCastException e) {
+                throw new TemplateModelException("Expecting numerical argument to " + BUFFER_SIZE_KEY);
+            }
+            try {
+                TemplateBooleanModel flag = (TemplateBooleanModel) args.get(SINGLE_LINE_KEY);
+                if (flag != null)
+                    singleLine = flag.getAsBoolean();
+            } catch (ClassCastException e) {
+                throw new TemplateModelException("Expecting boolean argument to " + SINGLE_LINE_KEY);
+            }
+        }
+        return new StandardCompressWriter(out, bufferSize, singleLine);
+    }
+
+    private static class StandardCompressWriter extends Writer {
+        private static final int MAX_EOL_LENGTH = 2; // CRLF is two bytes
+        
+        private static final int AT_BEGINNING = 0;
+        private static final int SINGLE_LINE = 1;
+        private static final int INIT = 2;
+        private static final int SAW_CR = 3;
+        private static final int LINEBREAK_CR = 4;
+        private static final int LINEBREAK_CRLF = 5;
+        private static final int LINEBREAK_LF = 6;
+
+        private final Writer out;
+        private final char[] buf;
+        private final boolean singleLine;
+    
+        private int pos = 0;
+        private boolean inWhitespace = true;
+        private int lineBreakState = AT_BEGINNING;
+
+        public StandardCompressWriter(Writer out, int bufSize, boolean singleLine) {
+            this.out = out;
+            this.singleLine = singleLine;
+            buf = new char[bufSize];
+        }
+
+        @Override
+        public void write(char[] cbuf, int off, int len) throws IOException {
+            for (; ; ) {
+                // Need to reserve space for the EOL potentially left in the state machine
+                int room = buf.length - pos - MAX_EOL_LENGTH; 
+                if (room >= len) {
+                    writeHelper(cbuf, off, len);
+                    break;
+                } else if (room <= 0) {
+                    flushInternal();
+                } else {
+                    writeHelper(cbuf, off, room);
+                    flushInternal();
+                    off += room;
+                    len -= room;
+                }
+            }
+        }
+
+        private void writeHelper(char[] cbuf, int off, int len) {
+            for (int i = off, end = off + len; i < end; i++) {
+                char c = cbuf[i];
+                if (Character.isWhitespace(c)) {
+                    inWhitespace = true;
+                    updateLineBreakState(c);
+                } else if (inWhitespace) {
+                    inWhitespace = false;
+                    writeLineBreakOrSpace();
+                    buf[pos++] = c;
+                } else {
+                    buf[pos++] = c;
+                }
+            }
+        }
+
+        /*
+          \r\n    => CRLF
+          \r[^\n] => CR
+          \r$     => CR
+          [^\r]\n => LF
+          ^\n     => LF
+        */
+        private void updateLineBreakState(char c) {
+            switch (lineBreakState) {
+            case INIT:
+                if (c == '\r') {
+                    lineBreakState = SAW_CR;
+                } else if (c == '\n') {
+                    lineBreakState = LINEBREAK_LF;
+                }
+                break;
+            case SAW_CR:
+                if (c == '\n') {
+                    lineBreakState = LINEBREAK_CRLF;
+                } else {
+                    lineBreakState = LINEBREAK_CR;
+                }
+            }
+        }
+
+        private void writeLineBreakOrSpace() {
+            switch (lineBreakState) {
+            case SAW_CR:
+                // whitespace ended with CR, fall through
+            case LINEBREAK_CR:
+                buf[pos++] = '\r';
+                break;
+            case LINEBREAK_CRLF:
+                buf[pos++] = '\r';
+                // fall through
+            case LINEBREAK_LF:
+                buf[pos++] = '\n';
+                break;
+            case AT_BEGINNING:
+                // ignore leading whitespace
+                break;
+            case INIT:
+            case SINGLE_LINE:
+                buf[pos++] = ' ';
+            }
+            lineBreakState = (singleLine) ? SINGLE_LINE : INIT;
+        }
+
+        private void flushInternal() throws IOException {
+            out.write(buf, 0, pos);
+            pos = 0;
+        }
+
+        @Override
+        public void flush() throws IOException {
+            flushInternal();
+            out.flush();
+        }
+
+        @Override
+        public void close() throws IOException {
+            flushInternal();
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/UndeclaredThrowableException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/UndeclaredThrowableException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/UndeclaredThrowableException.java
new file mode 100644
index 0000000..5b5cf97
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/UndeclaredThrowableException.java
@@ -0,0 +1,43 @@
+/*
+ * 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.util;
+
+
+/**
+ * The equivalent of JDK 1.3 UndeclaredThrowableException.
+ */
+public class UndeclaredThrowableException extends RuntimeException {
+    
+    public UndeclaredThrowableException(Throwable t) {
+        super(t);
+    }
+
+    /**
+     * @since 2.3.22
+     */
+    public UndeclaredThrowableException(String message, Throwable t) {
+        super(message, t);
+    }
+    
+    public Throwable getUndeclaredThrowable() {
+        return getCause();
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/UnrecognizedTimeZoneException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/UnrecognizedTimeZoneException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/UnrecognizedTimeZoneException.java
new file mode 100644
index 0000000..4a820a0
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/UnrecognizedTimeZoneException.java
@@ -0,0 +1,38 @@
+/*
+ * 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.util;
+
+/**
+ * Indicates that the time zone name is not recognized.
+ */
+public class UnrecognizedTimeZoneException extends Exception {
+    
+    private final String timeZoneName;
+
+    public UnrecognizedTimeZoneException(String timeZoneName) {
+        super("Unrecognized time zone: " + _StringUtil.jQuote(timeZoneName));
+        this.timeZoneName = timeZoneName;
+    }
+    
+    public String getTimeZoneName() {
+        return timeZoneName;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/UnsupportedNumberClassException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/UnsupportedNumberClassException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/UnsupportedNumberClassException.java
new file mode 100644
index 0000000..bcd9375
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/UnsupportedNumberClassException.java
@@ -0,0 +1,38 @@
+/*
+ * 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.util;
+
+/**
+ * Thrown when FreeMarker runs into a {@link Number} subclass that it doesn't yet support.  
+ */
+public class UnsupportedNumberClassException extends RuntimeException {
+
+    private final Class fClass;
+    
+    public UnsupportedNumberClassException(Class pClass) {
+        super("Unsupported number class: " + pClass.getName());
+        fClass = pClass;
+    }
+    
+    public Class getUnsupportedClass() {
+        return fClass;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/XmlEscape.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/XmlEscape.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/XmlEscape.java
new file mode 100644
index 0000000..43a2344
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/XmlEscape.java
@@ -0,0 +1,92 @@
+/*
+ * 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.util;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Map;
+
+import org.apache.freemarker.core.model.TemplateTransformModel;
+
+/**
+ * Performs an XML escaping of a given template fragment. Specifically,
+ * <tt>&lt;</tt> <tt>&gt;</tt> <tt>&quot;</tt> <tt>'</tt> and <tt>&amp;</tt> are all turned into entity references.
+ *
+ * <p>An instance of this transform is initially visible as shared
+ * variable called <tt>xml_escape</tt>.</p>
+ */
+// [FM3] Remove (or move to o.a.f.test)
+public class XmlEscape implements TemplateTransformModel {
+
+    private static final char[] LT = "&lt;".toCharArray();
+    private static final char[] GT = "&gt;".toCharArray();
+    private static final char[] AMP = "&amp;".toCharArray();
+    private static final char[] QUOT = "&quot;".toCharArray();
+    private static final char[] APOS = "&apos;".toCharArray();
+
+    @Override
+    public Writer getWriter(final Writer out, Map args) {
+        return new Writer()
+        {
+            @Override
+            public void write(int c)
+            throws IOException {
+                switch(c)
+                {
+                    case '<': out.write(LT, 0, 4); break;
+                    case '>': out.write(GT, 0, 4); break;
+                    case '&': out.write(AMP, 0, 5); break;
+                    case '"': out.write(QUOT, 0, 6); break;
+                    case '\'': out.write(APOS, 0, 6); break;
+                    default: out.write(c);
+                }
+            }
+
+            @Override
+            public void write(char cbuf[], int off, int len)
+            throws IOException {
+                int lastoff = off;
+                int lastpos = off + len;
+                for (int i = off; i < lastpos; i++) {
+                    switch (cbuf[i])
+                    {
+                        case '<': out.write(cbuf, lastoff, i - lastoff); out.write(LT, 0, 4); lastoff = i + 1; break;
+                        case '>': out.write(cbuf, lastoff, i - lastoff); out.write(GT, 0, 4); lastoff = i + 1; break;
+                        case '&': out.write(cbuf, lastoff, i - lastoff); out.write(AMP, 0, 5); lastoff = i + 1; break;
+                        case '"': out.write(cbuf, lastoff, i - lastoff); out.write(QUOT, 0, 6); lastoff = i + 1; break;
+                        case '\'': out.write(cbuf, lastoff, i - lastoff); out.write(APOS, 0, 6); lastoff = i + 1; break;
+                    }
+                }
+                int remaining = lastpos - lastoff;
+                if (remaining > 0) {
+                    out.write(cbuf, lastoff, remaining);
+                }
+            }
+            @Override
+            public void flush() throws IOException {
+                out.flush();
+            }
+
+            @Override
+            public void close() {
+            }
+        };
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ArrayEnumeration.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ArrayEnumeration.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ArrayEnumeration.java
new file mode 100644
index 0000000..1c82658
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ArrayEnumeration.java
@@ -0,0 +1,51 @@
+/*
+ * 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.util;
+
+import java.util.Enumeration;
+import java.util.NoSuchElementException;
+
+/** Don't use this; used internally by FreeMarker, might changes without notice. */
+public class _ArrayEnumeration implements Enumeration {
+
+    private final Object[] array;
+    private final int size;
+    private int nextIndex;
+
+    public _ArrayEnumeration(Object[] array, int size) {
+        this.array = array;
+        this.size = size;
+        nextIndex = 0;
+    }
+
+    @Override
+    public boolean hasMoreElements() {
+        return nextIndex < size;
+    }
+
+    @Override
+    public Object nextElement() {
+        if (nextIndex >= size) {
+            throw new NoSuchElementException();
+        }
+        return array[nextIndex++];
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ArrayIterator.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ArrayIterator.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ArrayIterator.java
new file mode 100644
index 0000000..7e02449
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ArrayIterator.java
@@ -0,0 +1,54 @@
+/*
+ * 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.util;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/** Don't use this; used internally by FreeMarker, might changes without notice. */
+public class _ArrayIterator implements Iterator {
+
+    private final Object[] array;
+    private int nextIndex;
+
+    public _ArrayIterator(Object[] array) {
+        this.array = array;
+        nextIndex = 0;
+    }
+
+    @Override
+    public boolean hasNext() {
+        return nextIndex < array.length;
+    }
+
+    @Override
+    public Object next() {
+        if (nextIndex >= array.length) {
+            throw new NoSuchElementException();
+        }
+        return array[nextIndex++];
+    }
+
+    @Override
+    public void remove() {
+        throw new UnsupportedOperationException();
+    }
+
+}


[12/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ClassUtil.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ClassUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ClassUtil.java
new file mode 100644
index 0000000..2670c8c
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ClassUtil.java
@@ -0,0 +1,182 @@
+/*
+ * 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.util;
+
+import org.apache.freemarker.core.model.impl.BeanModel;
+
+public class _ClassUtil {
+    
+    private static final String ORG_APACHE_FREEMARKER = "org.apache.freemarker.";
+    private static final String ORG_APACHE_FREEMARKER_CORE = "org.apache.freemarker.core.";
+    private static final String ORG_APACHE_FREEMARKER_CORE_TEMPLATERESOLVER
+            = "org.apache.freemarker.core.templateresolver.";
+    private static final String ORG_APACHE_FREEMARKER_CORE_MODEL = "org.apache.freemarker.core.model.";
+
+    private _ClassUtil() {
+    }
+    
+    /**
+     * Similar to {@link Class#forName(java.lang.String)}, but attempts to load
+     * through the thread context class loader. Only if thread context class
+     * loader is inaccessible, or it can't find the class will it attempt to
+     * fall back to the class loader that loads the FreeMarker classes.
+     */
+    public static Class forName(String className)
+    throws ClassNotFoundException {
+        try {
+            ClassLoader ctcl = Thread.currentThread().getContextClassLoader();
+            if (ctcl != null) {  // not null: we don't want to fall back to the bootstrap class loader
+                return Class.forName(className, true, ctcl);
+            }
+        } catch (ClassNotFoundException e) {
+            // Intentionally ignored
+        } catch (SecurityException e) {
+            // Intentionally ignored
+        }
+        // Fall back to the defining class loader of the FreeMarker classes 
+        return Class.forName(className);
+    }
+    
+    /**
+     * Same as {@link #getShortClassName(Class, boolean) getShortClassName(pClass, false)}.
+     * 
+     * @since 2.3.20
+     */
+    public static String getShortClassName(Class pClass) {
+        return getShortClassName(pClass, false);
+    }
+    
+    /**
+     * Returns a class name without "java.lang." and "java.util." prefix, also shows array types in a format like
+     * {@code int[]}; useful for printing class names in error messages.
+     * 
+     * @param pClass can be {@code null}, in which case the method returns {@code null}.
+     * @param shortenFreeMarkerClasses if {@code true}, it will also shorten FreeMarker class names. The exact rules
+     *     aren't specified and might change over time, but right now, {@link BeanModel} for
+     *     example becomes to {@code o.a.f.c.m.BeanModel}.
+     * 
+     * @since 2.3.20
+     */
+    public static String getShortClassName(Class pClass, boolean shortenFreeMarkerClasses) {
+        if (pClass == null) {
+            return null;
+        } else if (pClass.isArray()) {
+            return getShortClassName(pClass.getComponentType()) + "[]";
+        } else {
+            String cn = pClass.getName();
+            if (cn.startsWith("java.lang.") || cn.startsWith("java.util.")) {
+                return cn.substring(10);
+            } else {
+                if (shortenFreeMarkerClasses) {
+                    if (cn.startsWith(ORG_APACHE_FREEMARKER_CORE_MODEL)) {
+                        return "o.a.f.c.m." + cn.substring(ORG_APACHE_FREEMARKER_CORE_MODEL.length());
+                    } else if (cn.startsWith(ORG_APACHE_FREEMARKER_CORE_TEMPLATERESOLVER)) {
+                        return "o.a.f.c.t." + cn.substring(ORG_APACHE_FREEMARKER_CORE_TEMPLATERESOLVER.length());
+                    } else if (cn.startsWith(ORG_APACHE_FREEMARKER_CORE)) {
+                        return "o.a.f.c." + cn.substring(ORG_APACHE_FREEMARKER_CORE.length());
+                    } else if (cn.startsWith(ORG_APACHE_FREEMARKER)) {
+                        return "o.a.f." + cn.substring(ORG_APACHE_FREEMARKER.length());
+                    }
+                    // Falls through
+                }
+                return cn;
+            }
+        }
+    }
+
+    /**
+     * Same as {@link #getShortClassNameOfObject(Object, boolean) getShortClassNameOfObject(pClass, false)}.
+     * 
+     * @since 2.3.20
+     */
+    public static String getShortClassNameOfObject(Object obj) {
+        return getShortClassNameOfObject(obj, false);
+    }
+    
+    /**
+     * {@link #getShortClassName(Class, boolean)} called with {@code object.getClass()}, but returns the fictional
+     * class name {@code Null} for a {@code null} value.
+     * 
+     * @since 2.3.20
+     */
+    public static String getShortClassNameOfObject(Object obj, boolean shortenFreeMarkerClasses) {
+        if (obj == null) {
+            return "Null";
+        } else {
+            return _ClassUtil.getShortClassName(obj.getClass(), shortenFreeMarkerClasses);
+        }
+    }
+
+    /**
+     * Gets the wrapper class for a primitive class, like {@link Integer} for {@code int}, also returns {@link Void}
+     * for {@code void}. 
+     * 
+     * @param primitiveClass A {@link Class} like {@code int.type}, {@code boolean.type}, etc. If it's not a primitive
+     *     class, or it's {@code null}, then the parameter value is returned as is. Note that performance-wise the
+     *     method assumes that it's a primitive class.
+     *     
+     * @since 2.3.21
+     */
+    public static Class primitiveClassToBoxingClass(Class primitiveClass) {
+        // Tried to sort these with decreasing frequency in API-s:
+        if (primitiveClass == int.class) return Integer.class;
+        if (primitiveClass == boolean.class) return Boolean.class;
+        if (primitiveClass == long.class) return Long.class;
+        if (primitiveClass == double.class) return Double.class;
+        if (primitiveClass == char.class) return Character.class;
+        if (primitiveClass == float.class) return Float.class;
+        if (primitiveClass == byte.class) return Byte.class;
+        if (primitiveClass == short.class) return Short.class;
+        if (primitiveClass == void.class) return Void.class;  // not really a primitive, but we normalize it
+        return primitiveClass;
+    }
+
+    /**
+     * The exact reverse of {@link #primitiveClassToBoxingClass}.
+     *     
+     * @since 2.3.21
+     */
+    public static Class boxingClassToPrimitiveClass(Class boxingClass) {
+        // Tried to sort these with decreasing frequency in API-s:
+        if (boxingClass == Integer.class) return int.class;
+        if (boxingClass == Boolean.class) return boolean.class;
+        if (boxingClass == Long.class) return long.class;
+        if (boxingClass == Double.class) return double.class;
+        if (boxingClass == Character.class) return char.class;
+        if (boxingClass == Float.class) return float.class;
+        if (boxingClass == Byte.class) return byte.class;
+        if (boxingClass == Short.class) return short.class;
+        if (boxingClass == Void.class) return void.class;  // not really a primitive, but we normalize to it
+        return boxingClass;
+    }
+    
+    /**
+     * Tells if a type is numerical; works both for primitive types and classes.
+     * 
+     * @param type can't be {@code null}
+     * 
+     * @since 2.3.21
+     */
+    public static boolean isNumerical(Class type) {
+        return Number.class.isAssignableFrom(type)
+                || type.isPrimitive() && type != Boolean.TYPE && type != Character.TYPE && type != Void.TYPE;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_CollectionUtil.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_CollectionUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_CollectionUtil.java
new file mode 100644
index 0000000..5d532de
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_CollectionUtil.java
@@ -0,0 +1,36 @@
+/*
+ * 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.util;
+
+/** Don't use this; used internally by FreeMarker, might changes without notice. */
+public class _CollectionUtil {
+    
+    private _CollectionUtil() { }
+
+    public static final Object[] EMPTY_OBJECT_ARRAY = new Object[] { };
+    public static final Class[] EMPTY_CLASS_ARRAY = new Class[] { };
+    public static final String[] EMPTY_STRING_ARRAY = new String[] { };
+
+    /**
+     * @since 2.3.22
+     */
+    public static final char[] EMPTY_CHAR_ARRAY = new char[] { };
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_DateUtil.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_DateUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_DateUtil.java
new file mode 100644
index 0000000..0cf2fea
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_DateUtil.java
@@ -0,0 +1,914 @@
+/*
+ * 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.util;
+
+import java.text.ParseException;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.TimeZone;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Don't use this; used internally by FreeMarker, might changes without notice.
+ * Date and time related utilities.
+ */
+public class _DateUtil {
+
+    /**
+     * Show hours (24h); always 2 digits, like {@code 00}, {@code 05}, etc.
+     */
+    public static final int ACCURACY_HOURS = 4;
+    
+    /**
+     * Show hours and minutes (even if minutes is 00).
+     */
+    public static final int ACCURACY_MINUTES = 5;
+    
+    /**
+     * Show hours, minutes and seconds (even if seconds is 00).
+     */
+    public static final int ACCURACY_SECONDS = 6;
+    
+    /**
+     * Show hours, minutes and seconds and up to 3 fraction second digits, without trailing 0-s in the fraction part. 
+     */
+    public static final int ACCURACY_MILLISECONDS = 7;
+    
+    /**
+     * Show hours, minutes and seconds and exactly 3 fraction second digits (even if it's 000)
+     */
+    public static final int ACCURACY_MILLISECONDS_FORCED = 8;
+    
+    public static final TimeZone UTC = TimeZone.getTimeZone("UTC");
+    
+    private static final String REGEX_XS_TIME_ZONE
+            = "Z|(?:[-+][0-9]{2}:[0-9]{2})";
+    private static final String REGEX_ISO8601_BASIC_TIME_ZONE
+            = "Z|(?:[-+][0-9]{2}(?:[0-9]{2})?)";
+    private static final String REGEX_ISO8601_EXTENDED_TIME_ZONE
+            = "Z|(?:[-+][0-9]{2}(?::[0-9]{2})?)";
+    
+    private static final String REGEX_XS_OPTIONAL_TIME_ZONE
+            = "(" + REGEX_XS_TIME_ZONE + ")?";
+    private static final String REGEX_ISO8601_BASIC_OPTIONAL_TIME_ZONE
+            = "(" + REGEX_ISO8601_BASIC_TIME_ZONE + ")?";
+    private static final String REGEX_ISO8601_EXTENDED_OPTIONAL_TIME_ZONE
+            = "(" + REGEX_ISO8601_EXTENDED_TIME_ZONE + ")?";
+    
+    private static final String REGEX_XS_DATE_BASE
+            = "(-?[0-9]+)-([0-9]{2})-([0-9]{2})";
+    private static final String REGEX_ISO8601_BASIC_DATE_BASE
+            = "(-?[0-9]{4,}?)([0-9]{2})([0-9]{2})";
+    private static final String REGEX_ISO8601_EXTENDED_DATE_BASE
+            = "(-?[0-9]{4,})-([0-9]{2})-([0-9]{2})";
+    
+    private static final String REGEX_XS_TIME_BASE
+            = "([0-9]{2}):([0-9]{2}):([0-9]{2})(?:\\.([0-9]+))?";
+    private static final String REGEX_ISO8601_BASIC_TIME_BASE
+            = "([0-9]{2})(?:([0-9]{2})(?:([0-9]{2})(?:[\\.,]([0-9]+))?)?)?";
+    private static final String REGEX_ISO8601_EXTENDED_TIME_BASE
+            = "([0-9]{2})(?::([0-9]{2})(?::([0-9]{2})(?:[\\.,]([0-9]+))?)?)?";
+        
+    private static final Pattern PATTERN_XS_DATE = Pattern.compile(
+            REGEX_XS_DATE_BASE + REGEX_XS_OPTIONAL_TIME_ZONE);
+    private static final Pattern PATTERN_ISO8601_BASIC_DATE = Pattern.compile(
+            REGEX_ISO8601_BASIC_DATE_BASE); // No time zone allowed here
+    private static final Pattern PATTERN_ISO8601_EXTENDED_DATE = Pattern.compile(
+            REGEX_ISO8601_EXTENDED_DATE_BASE); // No time zone allowed here
+
+    private static final Pattern PATTERN_XS_TIME = Pattern.compile(
+            REGEX_XS_TIME_BASE + REGEX_XS_OPTIONAL_TIME_ZONE);
+    private static final Pattern PATTERN_ISO8601_BASIC_TIME = Pattern.compile(
+            REGEX_ISO8601_BASIC_TIME_BASE + REGEX_ISO8601_BASIC_OPTIONAL_TIME_ZONE);
+    private static final Pattern PATTERN_ISO8601_EXTENDED_TIME = Pattern.compile(
+            REGEX_ISO8601_EXTENDED_TIME_BASE + REGEX_ISO8601_EXTENDED_OPTIONAL_TIME_ZONE);
+    
+    private static final Pattern PATTERN_XS_DATE_TIME = Pattern.compile(
+            REGEX_XS_DATE_BASE
+            + "T" + REGEX_XS_TIME_BASE
+            + REGEX_XS_OPTIONAL_TIME_ZONE);
+    private static final Pattern PATTERN_ISO8601_BASIC_DATE_TIME = Pattern.compile(
+            REGEX_ISO8601_BASIC_DATE_BASE
+            + "T" + REGEX_ISO8601_BASIC_TIME_BASE
+            + REGEX_ISO8601_BASIC_OPTIONAL_TIME_ZONE);
+    private static final Pattern PATTERN_ISO8601_EXTENDED_DATE_TIME = Pattern.compile(
+            REGEX_ISO8601_EXTENDED_DATE_BASE
+            + "T" + REGEX_ISO8601_EXTENDED_TIME_BASE
+            + REGEX_ISO8601_EXTENDED_OPTIONAL_TIME_ZONE);
+    
+    private static final Pattern PATTERN_XS_TIME_ZONE = Pattern.compile(
+            REGEX_XS_TIME_ZONE);
+    
+    private static final String MSG_YEAR_0_NOT_ALLOWED
+            = "Year 0 is not allowed in XML schema dates. BC 1 is -1, AD 1 is 1.";
+    
+    private _DateUtil() {
+        // can't be instantiated
+    }
+    
+    /**
+     * Returns the time zone object for the name (or ID). This differs from
+     * {@link TimeZone#getTimeZone(String)} in that the latest returns GMT
+     * if it doesn't recognize the name, while this throws an
+     * {@link UnrecognizedTimeZoneException}.
+     * 
+     * @throws UnrecognizedTimeZoneException If the time zone name wasn't understood
+     */
+    public static TimeZone getTimeZone(String name)
+    throws UnrecognizedTimeZoneException {
+        if (isGMTish(name)) {
+            if (name.equalsIgnoreCase("UTC")) {
+                return UTC;
+            }
+            return TimeZone.getTimeZone(name);
+        }
+        TimeZone tz = TimeZone.getTimeZone(name);
+        if (isGMTish(tz.getID())) {
+            throw new UnrecognizedTimeZoneException(name);
+        }
+        return tz;
+    }
+
+    /**
+     * Tells if a offset or time zone is GMT. GMT is a fuzzy term, it used to
+     * referred both to UTC and UT1.
+     */
+    private static boolean isGMTish(String name) {
+        if (name.length() < 3) {
+            return false;
+        }
+        char c1 = name.charAt(0);
+        char c2 = name.charAt(1);
+        char c3 = name.charAt(2);
+        if (
+                !(
+                       (c1 == 'G' || c1 == 'g')
+                    && (c2 == 'M' || c2 == 'm')
+                    && (c3 == 'T' || c3 == 't')
+                )
+                &&
+                !(
+                       (c1 == 'U' || c1 == 'u')
+                    && (c2 == 'T' || c2 == 't')
+                    && (c3 == 'C' || c3 == 'c')
+                )
+                &&
+                !(
+                       (c1 == 'U' || c1 == 'u')
+                    && (c2 == 'T' || c2 == 't')
+                    && (c3 == '1')
+                )
+                ) {
+            return false;
+        }
+        
+        if (name.length() == 3) {
+            return true;
+        }
+        
+        String offset = name.substring(3);
+        if (offset.startsWith("+")) {
+            return offset.equals("+0") || offset.equals("+00")
+                    || offset.equals("+00:00");
+        } else {
+            return offset.equals("-0") || offset.equals("-00")
+            || offset.equals("-00:00");
+        }
+    }
+
+    /**
+     * Format a date, time or dateTime with one of the ISO 8601 extended
+     * formats that is also compatible with the XML Schema format (as far as you
+     * don't have dates in the BC era). Examples of possible outputs:
+     * {@code "2005-11-27T15:30:00+02:00"}, {@code "2005-11-27"},
+     * {@code "15:30:00Z"}. Note the {@code ":00"} in the time zone offset;
+     * this is not required by ISO 8601, but included for compatibility with
+     * the XML Schema format. Regarding the B.C. issue, those dates will be
+     * one year off when read back according the XML Schema format, because of a
+     * mismatch between that format and ISO 8601:2000 Second Edition.  
+     * 
+     * <p>This method is thread-safe.
+     * 
+     * @param date the date to convert to ISO 8601 string
+     * @param datePart whether the date part (year, month, day) will be included
+     *        or not
+     * @param timePart whether the time part (hours, minutes, seconds,
+     *        milliseconds) will be included or not
+     * @param offsetPart whether the time zone offset part will be included or
+     *        not. This will be shown as an offset to UTC (examples:
+     *        {@code "+01"}, {@code "-02"}, {@code "+04:30"}) or as {@code "Z"}
+     *        for UTC (and for UT1 and for GMT+00, since the Java platform
+     *        doesn't really care about the difference).
+     *        Note that this can't be {@code true} when {@code timePart} is
+     *        {@code false}, because ISO 8601 (2004) doesn't mention such
+     *        patterns.
+     * @param accuracy tells which parts of the date/time to drop. The
+     *        {@code datePart} and {@code timePart} parameters are stronger than
+     *        this. Note that when {@link #ACCURACY_MILLISECONDS} is specified,
+     *        the milliseconds part will be displayed as fraction seconds
+     *        (like {@code "15:30.00.25"}) with the minimum number of
+     *        digits needed to show the milliseconds without precision lose.
+     *        Thus, if the milliseconds happen to be exactly 0, no fraction
+     *        seconds will be shown at all.
+     * @param timeZone the time zone in which the date/time will be shown. (You
+     *        may find {@link _DateUtil#UTC} handy here.) Note
+     *        that although date-only formats has no time zone offset part,
+     *        the result still depends on the time zone, as days start and end
+     *        at different points on the time line in different zones.      
+     * @param calendarFactory the factory that will invoke the calendar used
+     *        internally for calculations. The point of this parameter is that
+     *        creating a new calendar is relatively expensive, so it's desirable
+     *        to reuse calendars and only set their time and zone. (This was
+     *        tested on Sun JDK 1.6 x86 Win, where it gave 2x-3x speedup.) 
+     */
+    public static String dateToISO8601String(
+            Date date,
+            boolean datePart, boolean timePart, boolean offsetPart,
+            int accuracy,
+            TimeZone timeZone,
+            DateToISO8601CalendarFactory calendarFactory) {
+        return dateToString(date, datePart, timePart, offsetPart, accuracy, timeZone, false, calendarFactory);
+    }
+
+    /**
+     * Same as {@link #dateToISO8601String}, but gives XML Schema compliant format.
+     */
+    public static String dateToXSString(
+            Date date,
+            boolean datePart, boolean timePart, boolean offsetPart,
+            int accuracy,
+            TimeZone timeZone,
+            DateToISO8601CalendarFactory calendarFactory) {
+        return dateToString(date, datePart, timePart, offsetPart, accuracy, timeZone, true, calendarFactory);
+    }
+    
+    private static String dateToString(
+            Date date,
+            boolean datePart, boolean timePart, boolean offsetPart,
+            int accuracy,
+            TimeZone timeZone, boolean xsMode,
+            DateToISO8601CalendarFactory calendarFactory) {
+        if (!xsMode && !timePart && offsetPart) {
+            throw new IllegalArgumentException(
+                    "ISO 8601:2004 doesn't specify any formats where the "
+                    + "offset is shown but the time isn't.");
+        }
+        
+        if (timeZone == null) {
+            timeZone = UTC;
+        }
+        
+        GregorianCalendar cal = calendarFactory.get(timeZone, date);
+
+        int maxLength;
+        if (!timePart) {
+            maxLength = 10 + (xsMode ? 6 : 0);  // YYYY-MM-DD+00:00
+        } else {
+            if (!datePart) {
+                maxLength = 12 + 6;  // HH:MM:SS.mmm+00:00
+            } else {
+                maxLength = 10 + 1 + 12 + 6;
+            }
+        }
+        char[] res = new char[maxLength];
+        int dstIdx = 0;
+        
+        if (datePart) {
+            int x = cal.get(Calendar.YEAR);
+            if (x > 0 && cal.get(Calendar.ERA) == GregorianCalendar.BC) {
+                x = -x + (xsMode ? 0 : 1);
+            }
+            if (x >= 0 && x < 9999) {
+                res[dstIdx++] = (char) ('0' + x / 1000);
+                res[dstIdx++] = (char) ('0' + x % 1000 / 100);
+                res[dstIdx++] = (char) ('0' + x % 100 / 10);
+                res[dstIdx++] = (char) ('0' + x % 10);
+            } else {
+                String yearString = String.valueOf(x);
+                
+                // Re-allocate buffer:
+                maxLength = maxLength - 4 + yearString.length();
+                res = new char[maxLength];
+                
+                for (int i = 0; i < yearString.length(); i++) {
+                    res[dstIdx++] = yearString.charAt(i);
+                }
+            }
+    
+            res[dstIdx++] = '-';
+            
+            x = cal.get(Calendar.MONTH) + 1;
+            dstIdx = append00(res, dstIdx, x);
+    
+            res[dstIdx++] = '-';
+            
+            x = cal.get(Calendar.DAY_OF_MONTH);
+            dstIdx = append00(res, dstIdx, x);
+
+            if (timePart) {
+                res[dstIdx++] = 'T';
+            }
+        }
+
+        if (timePart) {
+            int x = cal.get(Calendar.HOUR_OF_DAY);
+            dstIdx = append00(res, dstIdx, x);
+    
+            if (accuracy >= ACCURACY_MINUTES) {
+                res[dstIdx++] = ':';
+        
+                x = cal.get(Calendar.MINUTE);
+                dstIdx = append00(res, dstIdx, x);
+        
+                if (accuracy >= ACCURACY_SECONDS) {
+                    res[dstIdx++] = ':';
+            
+                    x = cal.get(Calendar.SECOND);
+                    dstIdx = append00(res, dstIdx, x);
+            
+                    if (accuracy >= ACCURACY_MILLISECONDS) {
+                        x = cal.get(Calendar.MILLISECOND);
+                        int forcedDigits = accuracy == ACCURACY_MILLISECONDS_FORCED ? 3 : 0;
+                        if (x != 0 || forcedDigits != 0) {
+                            if (x > 999) {
+                                // Shouldn't ever happen...
+                                throw new RuntimeException(
+                                        "Calendar.MILLISECOND > 999");
+                            }
+                            res[dstIdx++] = '.';
+                            do {
+                                res[dstIdx++] = (char) ('0' + (x / 100));
+                                forcedDigits--;
+                                x = x % 100 * 10;
+                            } while (x != 0 || forcedDigits > 0);
+                        }
+                    }
+                }
+            }
+        }
+
+        if (offsetPart) {
+            if (timeZone == UTC) {
+                res[dstIdx++] = 'Z';
+            } else {
+                int dt = timeZone.getOffset(date.getTime());
+                boolean positive;
+                if (dt < 0) {
+                    positive = false;
+                    dt = -dt;
+                } else {
+                    positive = true;
+                }
+                
+                dt /= 1000;
+                int offS = dt % 60;
+                dt /= 60;
+                int offM = dt % 60;
+                dt /= 60;
+                int offH = dt;
+                
+                if (offS == 0 && offM == 0 && offH == 0) {
+                    res[dstIdx++] = 'Z';
+                } else {
+                    res[dstIdx++] = positive ? '+' : '-';
+                    dstIdx = append00(res, dstIdx, offH);
+                    res[dstIdx++] = ':';
+                    dstIdx = append00(res, dstIdx, offM);
+                    if (offS != 0) {
+                        res[dstIdx++] = ':';
+                        dstIdx = append00(res, dstIdx, offS);
+                    }
+                }
+            }
+        }
+        
+        return new String(res, 0, dstIdx);
+    }
+    
+    /** 
+     * Appends a number between 0 and 99 padded to 2 digits.
+     */
+    private static int append00(char[] res, int dstIdx, int x) {
+        res[dstIdx++] = (char) ('0' + x / 10);
+        res[dstIdx++] = (char) ('0' + x % 10);
+        return dstIdx;
+    }
+    
+    /**
+     * Parses an W3C XML Schema date string (not time or date-time).
+     * Unlike in ISO 8601:2000 Second Edition, year -1 means B.C 1, and year 0 is invalid. 
+     * 
+     * @param dateStr the string to parse. 
+     * @param defaultTimeZone used if the date doesn't specify the
+     *     time zone offset explicitly. Can't be {@code null}.
+     * @param calToDateConverter Used internally to calculate the result from the calendar field values.
+     *     If you don't have a such object around, you can just use
+     *     {@code new }{@link TrivialCalendarFieldsToDateConverter}{@code ()}. 
+     * 
+     * @throws DateParseException if the date is malformed, or if the time
+     *     zone offset is unspecified and the {@code defaultTimeZone} is
+     *     {@code null}.
+     */
+    public static Date parseXSDate(
+            String dateStr, TimeZone defaultTimeZone,
+            CalendarFieldsToDateConverter calToDateConverter) 
+            throws DateParseException {
+        Matcher m = PATTERN_XS_DATE.matcher(dateStr);
+        if (!m.matches()) {
+            throw new DateParseException("The value didn't match the expected pattern: " + PATTERN_XS_DATE); 
+        }
+        return parseDate_parseMatcher(
+                m, defaultTimeZone, true, calToDateConverter);
+    }
+
+    /**
+     * Same as {@link #parseXSDate(String, TimeZone, CalendarFieldsToDateConverter)}, but for ISO 8601 dates.
+     */
+    public static Date parseISO8601Date(
+            String dateStr, TimeZone defaultTimeZone,
+            CalendarFieldsToDateConverter calToDateConverter) 
+            throws DateParseException {
+        Matcher m = PATTERN_ISO8601_EXTENDED_DATE.matcher(dateStr);
+        if (!m.matches()) {
+            m = PATTERN_ISO8601_BASIC_DATE.matcher(dateStr);
+            if (!m.matches()) {
+                throw new DateParseException("The value didn't match the expected pattern: "
+                            + PATTERN_ISO8601_EXTENDED_DATE + " or "
+                            + PATTERN_ISO8601_BASIC_DATE);
+            }
+        }
+        return parseDate_parseMatcher(
+                m, defaultTimeZone, false, calToDateConverter);
+    }
+    
+    private static Date parseDate_parseMatcher(
+            Matcher m, TimeZone defaultTZ,
+            boolean xsMode,
+            CalendarFieldsToDateConverter calToDateConverter) 
+            throws DateParseException {
+        _NullArgumentException.check("defaultTZ", defaultTZ);
+        try {
+            int year = groupToInt(m.group(1), "year", Integer.MIN_VALUE, Integer.MAX_VALUE);
+            
+            int era;
+            // Starting from ISO 8601:2000 Second Edition, 0001 is AD 1, 0000 is BC 1, -0001 is BC 2.
+            // However, according to http://www.w3.org/TR/2004/REC-xmlschema-2-20041028/, XML schemas are based
+            // on the earlier version where 0000 didn't exist, and year -1 is BC 1.
+            if (year <= 0) {
+                era = GregorianCalendar.BC;
+                year = -year + (xsMode ? 0 : 1);
+                if (year == 0) {
+                    throw new DateParseException(MSG_YEAR_0_NOT_ALLOWED);
+                }
+            } else {
+                era = GregorianCalendar.AD;
+            }
+            
+            int month = groupToInt(m.group(2), "month", 1, 12) - 1;
+            int day = groupToInt(m.group(3), "day-of-month", 1, 31);
+
+            TimeZone tz = xsMode ? parseMatchingTimeZone(m.group(4), defaultTZ) : defaultTZ;
+            
+            return calToDateConverter.calculate(era, year, month, day, 0, 0, 0, 0, false, tz);
+        } catch (IllegalArgumentException e) {
+            // Calendar methods used to throw this for illegal dates.
+            throw new DateParseException(
+                    "Date calculation faliure. "
+                    + "Probably the date is formally correct, but refers "
+                    + "to an unexistent date (like February 30)."); 
+        }
+    }
+    
+    /**
+     * Parses an W3C XML Schema time string (not date or date-time).
+     * If the time string doesn't specify the time zone offset explicitly,
+     * the value of the {@code defaultTZ} paramter will be used. 
+     */  
+    public static Date parseXSTime(
+            String timeStr, TimeZone defaultTZ, CalendarFieldsToDateConverter calToDateConverter) 
+            throws DateParseException {
+        Matcher m = PATTERN_XS_TIME.matcher(timeStr);
+        if (!m.matches()) {
+            throw new DateParseException("The value didn't match the expected pattern: " + PATTERN_XS_TIME);
+        }
+        return parseTime_parseMatcher(m, defaultTZ, calToDateConverter);
+    }
+
+    /**
+     * Same as {@link #parseXSTime(String, TimeZone, CalendarFieldsToDateConverter)} but for ISO 8601 times.
+     */
+    public static Date parseISO8601Time(
+            String timeStr, TimeZone defaultTZ, CalendarFieldsToDateConverter calToDateConverter) 
+            throws DateParseException {
+        Matcher m = PATTERN_ISO8601_EXTENDED_TIME.matcher(timeStr);
+        if (!m.matches()) {
+            m = PATTERN_ISO8601_BASIC_TIME.matcher(timeStr);
+            if (!m.matches()) {
+                throw new DateParseException("The value didn't match the expected pattern: "
+                            + PATTERN_ISO8601_EXTENDED_TIME + " or "
+                            + PATTERN_ISO8601_BASIC_TIME);
+            }
+        }
+        return parseTime_parseMatcher(m, defaultTZ, calToDateConverter);
+    }
+    
+    private static Date parseTime_parseMatcher(
+            Matcher m, TimeZone defaultTZ,
+            CalendarFieldsToDateConverter calToDateConverter) 
+            throws DateParseException {
+        _NullArgumentException.check("defaultTZ", defaultTZ);
+        try {
+            // ISO 8601 allows both 00:00 and 24:00,
+            // but Calendar.set(...) doesn't if the Calendar is not lenient.
+            int hours = groupToInt(m.group(1), "hour-of-day", 0, 24);
+            boolean hourWas24;
+            if (hours == 24) {
+                hours = 0;
+                hourWas24 = true;
+                // And a day will be added later...
+            } else {
+                hourWas24 = false;
+            }
+            
+            final String minutesStr = m.group(2);
+            int minutes = minutesStr != null ? groupToInt(minutesStr, "minute", 0, 59) : 0;
+            
+            final String secsStr = m.group(3);
+            // Allow 60 because of leap seconds
+            int secs = secsStr != null ? groupToInt(secsStr, "second", 0, 60) : 0;
+            
+            int millisecs = groupToMillisecond(m.group(4));
+            
+            // As a time is just the distance from the beginning of the day,
+            // the time-zone offest should be 0 usually.
+            TimeZone tz = parseMatchingTimeZone(m.group(5), defaultTZ);
+            
+            // Continue handling the 24:00 special case
+            int day;
+            if (hourWas24) {
+                if (minutes == 0 && secs == 0 && millisecs == 0) {
+                    day = 2;
+                } else {
+                    throw new DateParseException(
+                            "Hour 24 is only allowed in the case of "
+                            + "midnight."); 
+                }
+            } else {
+                day = 1;
+            }
+            
+            return calToDateConverter.calculate(
+                    GregorianCalendar.AD, 1970, 0, day, hours, minutes, secs, millisecs, false, tz);
+        } catch (IllegalArgumentException e) {
+            // Calendar methods used to throw this for illegal dates.
+            throw new DateParseException(
+                    "Unexpected time calculation faliure."); 
+        }
+    }
+    
+    /**
+     * Parses an W3C XML Schema date-time string (not date or time).
+     * Unlike in ISO 8601:2000 Second Edition, year -1 means B.C 1, and year 0 is invalid. 
+     * 
+     * @param dateTimeStr the string to parse. 
+     * @param defaultTZ used if the dateTime doesn't specify the
+     *     time zone offset explicitly. Can't be {@code null}. 
+     * 
+     * @throws DateParseException if the dateTime is malformed.
+     */
+    public static Date parseXSDateTime(
+            String dateTimeStr, TimeZone defaultTZ, CalendarFieldsToDateConverter calToDateConverter) 
+            throws DateParseException {
+        Matcher m = PATTERN_XS_DATE_TIME.matcher(dateTimeStr);
+        if (!m.matches()) {
+            throw new DateParseException(
+                    "The value didn't match the expected pattern: " + PATTERN_XS_DATE_TIME);
+        }
+        return parseDateTime_parseMatcher(
+                m, defaultTZ, true, calToDateConverter);
+    }
+
+    /**
+     * Same as {@link #parseXSDateTime(String, TimeZone, CalendarFieldsToDateConverter)} but for ISO 8601 format. 
+     */
+    public static Date parseISO8601DateTime(
+            String dateTimeStr, TimeZone defaultTZ, CalendarFieldsToDateConverter calToDateConverter) 
+            throws DateParseException {
+        Matcher m = PATTERN_ISO8601_EXTENDED_DATE_TIME.matcher(dateTimeStr);
+        if (!m.matches()) {
+            m = PATTERN_ISO8601_BASIC_DATE_TIME.matcher(dateTimeStr);
+            if (!m.matches()) {
+                throw new DateParseException("The value (" + dateTimeStr + ") didn't match the expected pattern: "
+                            + PATTERN_ISO8601_EXTENDED_DATE_TIME + " or "
+                            + PATTERN_ISO8601_BASIC_DATE_TIME);
+            }
+        }
+        return parseDateTime_parseMatcher(
+                m, defaultTZ, false, calToDateConverter);
+    }
+    
+    private static Date parseDateTime_parseMatcher(
+            Matcher m, TimeZone defaultTZ,
+            boolean xsMode,
+            CalendarFieldsToDateConverter calToDateConverter) 
+            throws DateParseException {
+        _NullArgumentException.check("defaultTZ", defaultTZ);
+        try {
+            int year = groupToInt(m.group(1), "year", Integer.MIN_VALUE, Integer.MAX_VALUE);
+            
+            int era;
+            // Starting from ISO 8601:2000 Second Edition, 0001 is AD 1, 0000 is BC 1, -0001 is BC 2.
+            // However, according to http://www.w3.org/TR/2004/REC-xmlschema-2-20041028/, XML schemas are based
+            // on the earlier version where 0000 didn't exist, and year -1 is BC 1.
+            if (year <= 0) {
+                era = GregorianCalendar.BC;
+                year = -year + (xsMode ? 0 : 1);
+                if (year == 0) {
+                    throw new DateParseException(MSG_YEAR_0_NOT_ALLOWED);
+                }
+            } else {
+                era = GregorianCalendar.AD;
+            }
+            
+            int month = groupToInt(m.group(2), "month", 1, 12) - 1;
+            int day = groupToInt(m.group(3), "day-of-month", 1, 31);
+            
+            // ISO 8601 allows both 00:00 and 24:00,
+            // but cal.set(...) doesn't if the Calendar is not lenient.
+            int hours = groupToInt(m.group(4), "hour-of-day", 0, 24);
+            boolean hourWas24;
+            if (hours == 24) {
+                hours = 0;
+                hourWas24 = true;
+                // And a day will be added later...
+            } else {
+                hourWas24 = false;
+            }
+            
+            final String minutesStr = m.group(5);
+            int minutes = minutesStr != null ? groupToInt(minutesStr, "minute", 0, 59) : 0;
+            
+            final String secsStr = m.group(6);
+            // Allow 60 because of leap seconds
+            int secs = secsStr != null ? groupToInt(secsStr, "second", 0, 60) : 0;
+            
+            int millisecs = groupToMillisecond(m.group(7));
+            
+            // As a time is just the distance from the beginning of the day,
+            // the time-zone offest should be 0 usually.
+            TimeZone tz = parseMatchingTimeZone(m.group(8), defaultTZ);
+            
+            // Continue handling the 24:00 specail case
+            if (hourWas24) {
+                if (minutes != 0 || secs != 0 || millisecs != 0) {
+                    throw new DateParseException(
+                            "Hour 24 is only allowed in the case of "
+                            + "midnight."); 
+                }
+            }
+            
+            return calToDateConverter.calculate(
+                    era, year, month, day, hours, minutes, secs, millisecs, hourWas24, tz);
+        } catch (IllegalArgumentException e) {
+            // Calendar methods used to throw this for illegal dates.
+            throw new DateParseException(
+                    "Date-time calculation faliure. "
+                    + "Probably the date-time is formally correct, but "
+                    + "refers to an unexistent date-time "
+                    + "(like February 30)."); 
+        }
+    }
+
+    /**
+     * Parses the time zone part from a W3C XML Schema date/time/dateTime. 
+     * @throws DateParseException if the zone is malformed.
+     */
+    public static TimeZone parseXSTimeZone(String timeZoneStr)
+            throws DateParseException {
+        Matcher m = PATTERN_XS_TIME_ZONE.matcher(timeZoneStr);
+        if (!m.matches()) {
+            throw new DateParseException(
+                    "The time zone offset didn't match the expected pattern: " + PATTERN_XS_TIME_ZONE);
+        }
+        return parseMatchingTimeZone(timeZoneStr, null);
+    }
+
+    private static int groupToInt(String g, String gName,
+            int min, int max)
+            throws DateParseException {
+        if (g == null) {
+            throw new DateParseException("The " + gName + " part "
+                    + "is missing.");
+        }
+
+        int start;
+        
+        // Remove minus sign, so we can remove the 0-s later:
+        boolean negative;
+        if (g.startsWith("-")) {
+            negative = true;
+            start = 1;
+        } else {
+            negative = false;
+            start = 0;
+        }
+        
+        // Remove leading 0-s:
+        while (start < g.length() - 1 && g.charAt(start) == '0') {
+            start++;
+        }
+        if (start != 0) {
+            g = g.substring(start);
+        }
+        
+        try {
+            int r = Integer.parseInt(g);
+            if (negative) {
+                r = -r;
+            }
+            if (r < min) {
+                throw new DateParseException("The " + gName + " part "
+                    + "must be at least " + min + ".");
+            }
+            if (r > max) {
+                throw new DateParseException("The " + gName + " part "
+                    + "can't be more than " + max + ".");
+            }
+            return r;
+        } catch (NumberFormatException e) {
+            throw new DateParseException("The " + gName + " part "
+                    + "is a malformed integer.");
+        }
+    }
+
+    private static TimeZone parseMatchingTimeZone(
+            String s, TimeZone defaultZone)
+            throws DateParseException {
+        if (s == null) {
+            return defaultZone;
+        }
+        if (s.equals("Z")) {
+            return _DateUtil.UTC;
+        }
+        
+        StringBuilder sb = new StringBuilder(9);
+        sb.append("GMT");
+        sb.append(s.charAt(0));
+        
+        String h = s.substring(1, 3);
+        groupToInt(h, "offset-hours", 0, 23);
+        sb.append(h);
+        
+        String m;
+        int ln = s.length();
+        if (ln > 3) {
+            int startIdx = s.charAt(3) == ':' ? 4 : 3;
+            m = s.substring(startIdx, startIdx + 2);
+            groupToInt(m, "offset-minutes", 0, 59);
+            sb.append(':');
+            sb.append(m);
+        }
+        
+        return TimeZone.getTimeZone(sb.toString());
+    }
+
+    private static int groupToMillisecond(String g)
+            throws DateParseException {
+        if (g == null) {
+            return 0;
+        }
+        
+        if (g.length() > 3) {
+            g = g.substring(0, 3);
+        }
+        int i = groupToInt(g, "partial-seconds", 0, Integer.MAX_VALUE);
+        return g.length() == 1 ? i * 100 : (g.length() == 2 ? i * 10 : i);
+    }
+    
+    /**
+     * Used internally by {@link _DateUtil}; don't use its implementations for
+     * anything else.
+     */
+    public interface DateToISO8601CalendarFactory {
+        
+        /**
+         * Returns a {@link GregorianCalendar} with the desired time zone and
+         * time and US locale. The returned calendar is used as read-only.
+         * It must be guaranteed that within a thread the instance returned last time
+         * is not in use anymore when this method is called again.
+         */
+        GregorianCalendar get(TimeZone tz, Date date);
+        
+    }
+
+    /**
+     * Used internally by {@link _DateUtil}; don't use its implementations for anything else.
+     */
+    public interface CalendarFieldsToDateConverter {
+
+        /**
+         * Calculates the {@link Date} from the specified calendar fields.
+         */
+        Date calculate(int era, int year, int month, int day, int hours, int minutes, int secs, int millisecs,
+                boolean addOneDay,
+                TimeZone tz);
+
+    }
+
+    /**
+     * Non-thread-safe factory that hard-references a calendar internally.
+     */
+    public static final class TrivialDateToISO8601CalendarFactory
+            implements DateToISO8601CalendarFactory {
+        
+        private GregorianCalendar calendar;
+        private TimeZone lastlySetTimeZone;
+    
+        @Override
+        public GregorianCalendar get(TimeZone tz, Date date) {
+            if (calendar == null) {
+                calendar = new GregorianCalendar(tz, Locale.US);
+                calendar.setGregorianChange(new Date(Long.MIN_VALUE));  // never use Julian calendar
+            } else {
+                // At least on Java 6, calendar.getTimeZone is slow due to a bug, so we need lastlySetTimeZone.
+                if (lastlySetTimeZone != tz) {  // Deliberately `!=` instead of `!<...>.equals()`  
+                    calendar.setTimeZone(tz);
+                    lastlySetTimeZone = tz;
+                }
+            }
+            calendar.setTime(date);
+            return calendar;
+        }
+        
+    }
+
+    /**
+     * Non-thread-safe implementation that hard-references a calendar internally.
+     */
+    public static final class TrivialCalendarFieldsToDateConverter
+            implements CalendarFieldsToDateConverter {
+
+        private GregorianCalendar calendar;
+        private TimeZone lastlySetTimeZone;
+
+        @Override
+        public Date calculate(int era, int year, int month, int day, int hours, int minutes, int secs, int millisecs,
+                              boolean addOneDay, TimeZone tz) {
+            if (calendar == null) {
+                calendar = new GregorianCalendar(tz, Locale.US);
+                calendar.setLenient(false);
+                calendar.setGregorianChange(new Date(Long.MIN_VALUE));  // never use Julian calendar
+            } else {
+                // At least on Java 6, calendar.getTimeZone is slow due to a bug, so we need lastlySetTimeZone.
+                if (lastlySetTimeZone != tz) {  // Deliberately `!=` instead of `!<...>.equals()`  
+                    calendar.setTimeZone(tz);
+                    lastlySetTimeZone = tz;
+                }
+            }
+
+            calendar.set(Calendar.ERA, era);
+            calendar.set(Calendar.YEAR, year);
+            calendar.set(Calendar.MONTH, month);
+            calendar.set(Calendar.DAY_OF_MONTH, day);
+            calendar.set(Calendar.HOUR_OF_DAY, hours);
+            calendar.set(Calendar.MINUTE, minutes);
+            calendar.set(Calendar.SECOND, secs);
+            calendar.set(Calendar.MILLISECOND, millisecs);
+            if (addOneDay) {
+                calendar.add(Calendar.DAY_OF_MONTH, 1);
+            }
+            
+            return calendar.getTime();
+        }
+
+    }
+    
+    public static final class DateParseException extends ParseException {
+        
+        public DateParseException(String message) {
+            super(message, 0);
+        }
+        
+    }
+        
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_JavaVersions.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_JavaVersions.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_JavaVersions.java
new file mode 100644
index 0000000..10f79fe
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_JavaVersions.java
@@ -0,0 +1,80 @@
+/*
+ * 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.util;
+
+import org.apache.freemarker.core.Version;
+import org.apache.freemarker.core._CoreLogs;
+import org.apache.freemarker.core._Java8;
+
+/**
+ * Used internally only, might changes without notice!
+ */
+public final class _JavaVersions {
+    
+    private _JavaVersions() {
+        // Not meant to be instantiated
+    }
+
+    private static final boolean IS_AT_LEAST_8;
+    static {
+        boolean result = false;
+        String vStr = _SecurityUtil.getSystemProperty("java.version", null);
+        if (vStr != null) {
+            try {
+                Version v = new Version(vStr);
+                result = v.getMajor() == 1 && v.getMinor() >= 8 || v.getMajor() > 1;
+            } catch (Exception e) {
+                // Ignore
+            }
+        } else {
+            try {
+                Class.forName("java.time.Instant");
+                result = true;
+            } catch (Exception e) {
+                // Ignore
+            }
+        }
+        IS_AT_LEAST_8 = result;
+    }
+    
+    /**
+     * {@code null} if Java 8 is not available, otherwise the object through with the Java 8 operations are available.
+     */
+    static public final _Java8 JAVA_8;
+    static {
+        _Java8 java8;
+        if (IS_AT_LEAST_8) {
+            try {
+                java8 = (_Java8) Class.forName("org.apache.freemarker.core._Java8Impl")
+                        .getField("INSTANCE").get(null);
+            } catch (Exception e) {
+                try {
+                    _CoreLogs.RUNTIME.error("Failed to access Java 8 functionality", e);
+                } catch (Exception e2) {
+                    // Suppressed
+                }
+                java8 = null;
+            }
+        } else {
+            java8 = null;
+        }
+        JAVA_8 = java8;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_KeyValuePair.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_KeyValuePair.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_KeyValuePair.java
new file mode 100644
index 0000000..d88d8e4
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_KeyValuePair.java
@@ -0,0 +1,61 @@
+/*
+ * 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.util;
+
+public class _KeyValuePair<K, V> {
+    private final K key;
+    private final V value;
+
+    public _KeyValuePair(K key, V value) {
+        this.key = key;
+        this.value = value;
+    }
+
+    public K getKey() {
+        return key;
+    }
+
+    public V getValue() {
+        return value;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        _KeyValuePair<?, ?> that = (_KeyValuePair<?, ?>) o;
+
+        if (key != null ? !key.equals(that.key) : that.key != null) return false;
+        return value != null ? value.equals(that.value) : that.value == null;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = key != null ? key.hashCode() : 0;
+        result = 31 * result + (value != null ? value.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "_KeyValuePair{key=" + key + ", value=" + value + '}';
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_LocaleUtil.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_LocaleUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_LocaleUtil.java
new file mode 100644
index 0000000..2f09c88
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_LocaleUtil.java
@@ -0,0 +1,43 @@
+/*
+ * 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.util;
+
+import java.util.Locale;
+
+/**
+ * For internal use only; don't depend on this, there's no backward compatibility guarantee at all!
+ */
+public class _LocaleUtil {
+
+    /**
+     * Returns a locale that's one less specific, or {@code null} if there's no less specific locale.
+     */
+    public static Locale getLessSpecificLocale(Locale locale) {
+        String country = locale.getCountry();
+        if (locale.getVariant().length() != 0) {
+            String language = locale.getLanguage();
+            return country != null ? new Locale(language, country) : new Locale(language);
+        }
+        if (country.length() != 0) {
+            return new Locale(locale.getLanguage());
+        }
+        return null;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_NullArgumentException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_NullArgumentException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_NullArgumentException.java
new file mode 100644
index 0000000..5b3ea5f
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_NullArgumentException.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.util;
+
+/**
+ * Indicates that an argument that must be non-{@code null} was {@code null}. 
+ * 
+ * @since 2.3.20
+ */
+public class _NullArgumentException extends IllegalArgumentException {
+
+    public _NullArgumentException() {
+        super("The argument can't be null");
+    }
+    
+    public _NullArgumentException(String argumentName) {
+        super("The \"" + argumentName + "\" argument can't be null");
+    }
+
+    public _NullArgumentException(String argumentName, String details) {
+        super("The \"" + argumentName + "\" argument can't be null. " + details);
+    }
+    
+    /**
+     * Convenience method to protect against a {@code null} argument.
+     */
+    public static void check(String argumentName, Object argumentValue) {
+        if (argumentValue == null) {
+            throw new _NullArgumentException(argumentName);
+        }
+    }
+
+    /**
+     * @since 2.3.22
+     */
+    public static void check(Object argumentValue) {
+        if (argumentValue == null) {
+            throw new _NullArgumentException();
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_NullWriter.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_NullWriter.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_NullWriter.java
new file mode 100644
index 0000000..399fca4
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_NullWriter.java
@@ -0,0 +1,90 @@
+/*
+ * 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.util;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * A {@link Writer} that simply drops what it gets.
+ * 
+ * @since 2.3.20
+ */
+public final class _NullWriter extends Writer {
+    
+    public static final _NullWriter INSTANCE = new _NullWriter();
+    
+    /** Can't be instantiated; use {@link #INSTANCE}. */
+    private _NullWriter() { }
+    
+    @Override
+    public void write(char[] cbuf, int off, int len) throws IOException {
+        // Do nothing
+    }
+
+    @Override
+    public void flush() throws IOException {
+        // Do nothing
+    }
+
+    @Override
+    public void close() throws IOException {
+        // Do nothing
+    }
+
+    @Override
+    public void write(int c) throws IOException {
+        // Do nothing
+    }
+
+    @Override
+    public void write(char[] cbuf) throws IOException {
+        // Do nothing
+    }
+
+    @Override
+    public void write(String str) throws IOException {
+        // Do nothing
+    }
+
+    @Override
+    public void write(String str, int off, int len) throws IOException {
+        // Do nothing
+    }
+
+    @Override
+    public Writer append(CharSequence csq) throws IOException {
+        // Do nothing
+        return this;
+    }
+
+    @Override
+    public Writer append(CharSequence csq, int start, int end) throws IOException {
+        // Do nothing
+        return this;
+    }
+
+    @Override
+    public Writer append(char c) throws IOException {
+        // Do nothing
+        return this;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_NumberUtil.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_NumberUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_NumberUtil.java
new file mode 100644
index 0000000..500a185
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_NumberUtil.java
@@ -0,0 +1,228 @@
+/*
+ * 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.util;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+/** Don't use this; used internally by FreeMarker, might changes without notice. */
+public class _NumberUtil {
+
+    private static final BigDecimal BIG_DECIMAL_INT_MIN = BigDecimal.valueOf(Integer.MIN_VALUE);
+    private static final BigDecimal BIG_DECIMAL_INT_MAX = BigDecimal.valueOf(Integer.MAX_VALUE);
+    private static final BigInteger BIG_INTEGER_INT_MIN = BIG_DECIMAL_INT_MIN.toBigInteger();
+    private static final BigInteger BIG_INTEGER_INT_MAX = BIG_DECIMAL_INT_MAX.toBigInteger();
+    private static final BigInteger BIG_INTEGER_LONG_MIN = BigInteger.valueOf(Long.MIN_VALUE);
+    private static final BigInteger BIG_INTEGER_LONG_MAX = BigInteger.valueOf(Long.MAX_VALUE);
+
+    private _NumberUtil() { }
+    
+    public static boolean isInfinite(Number num) {
+        if (num instanceof Double) {
+            return ((Double) num).isInfinite();
+        } else if (num instanceof Float) {
+            return ((Float) num).isInfinite();
+        } else if (isNonFPNumberOfSupportedClass(num)) {
+            return false;
+        } else {
+            throw new UnsupportedNumberClassException(num.getClass());
+        }           
+    }
+
+    public static boolean isNaN(Number num) {
+        if (num instanceof Double) {
+            return ((Double) num).isNaN();
+        } else if (num instanceof Float) {
+            return ((Float) num).isNaN();
+        } else if (isNonFPNumberOfSupportedClass(num)) {
+            return false;
+        } else {
+            throw new UnsupportedNumberClassException(num.getClass());
+        }           
+    }
+
+    /**
+     * @return -1 for negative, 0 for zero, 1 for positive.
+     * @throws ArithmeticException if the number is NaN
+     */
+    public static int getSignum(Number num) throws ArithmeticException {
+        if (num instanceof Integer) {
+            int n = num.intValue();
+            return n > 0 ? 1 : (n == 0 ? 0 : -1);
+        } else if (num instanceof BigDecimal) {
+            BigDecimal n = (BigDecimal) num;
+            return n.signum();
+        } else if (num instanceof Double) {
+            double n = num.doubleValue();
+            if (n > 0) return 1;
+            else if (n == 0) return 0;
+            else if (n < 0) return -1;
+            else throw new ArithmeticException("The signum of " + n + " is not defined.");  // NaN
+        } else if (num instanceof Float) {
+            float n = num.floatValue();
+            if (n > 0) return 1;
+            else if (n == 0) return 0;
+            else if (n < 0) return -1;
+            else throw new ArithmeticException("The signum of " + n + " is not defined.");  // NaN
+        } else if (num instanceof Long) {
+            long n = num.longValue();
+            return n > 0 ? 1 : (n == 0 ? 0 : -1);
+        } else if (num instanceof Short) {
+            short n = num.shortValue();
+            return n > 0 ? 1 : (n == 0 ? 0 : -1);
+        } else if (num instanceof Byte) {
+            byte n = num.byteValue();
+            return n > 0 ? 1 : (n == 0 ? 0 : -1);
+        } else if (num instanceof BigInteger) {
+            BigInteger n = (BigInteger) num;
+            return n.signum();
+        } else {
+            throw new UnsupportedNumberClassException(num.getClass());
+        }
+    }
+    
+    /**
+     * Tells if a {@link BigDecimal} stores a whole number. For example, it returns {@code true} for {@code 1.0000},
+     * but {@code false} for {@code 1.0001}.
+     * 
+     * @since 2.3.21
+     */
+    static public boolean isIntegerBigDecimal(BigDecimal bd) {
+        // [Java 1.5] Try to utilize BigDecimal.toXxxExact methods
+        return bd.scale() <= 0  // A fast check that whole numbers usually (not always) match
+               || bd.setScale(0, BigDecimal.ROUND_DOWN).compareTo(bd) == 0;  // This is rather slow
+        // Note that `bd.signum() == 0 || bd.stripTrailingZeros().scale() <= 0` was also tried for the last
+        // condition, but stripTrailingZeros was slower than setScale + compareTo.
+    }
+    
+    private static boolean isNonFPNumberOfSupportedClass(Number num) {
+        return num instanceof Integer || num instanceof BigDecimal || num instanceof Long
+                || num instanceof Short || num instanceof Byte || num instanceof BigInteger;
+    }
+
+    /**
+     * Converts a {@link Number} to {@code int} whose mathematical value is exactly the same as of the original number.
+     * 
+     * @throws ArithmeticException
+     *             if the conversion to {@code int} is not possible without losing precision or overflow/underflow.
+     * 
+     * @since 2.3.22
+     */
+    public static int toIntExact(Number num) {
+        if (num instanceof Integer || num instanceof Short || num instanceof Byte) {
+            return num.intValue();
+        } else if (num instanceof Long) {
+            final long n = num.longValue();
+            final int result = (int) n;
+            if (n != result) {
+                throw newLossyConverionException(num, Integer.class);
+            }
+            return result;
+        } else if (num instanceof Double || num instanceof Float) {
+            final double n = num.doubleValue();
+            if (n % 1 != 0 || n < Integer.MIN_VALUE || n > Integer.MAX_VALUE) {
+                throw newLossyConverionException(num, Integer.class);
+            }
+            return (int) n;
+        } else if (num instanceof BigDecimal) {
+            // [Java 1.5] Use BigDecimal.toIntegerExact()
+            BigDecimal n = (BigDecimal) num;
+            if (!isIntegerBigDecimal(n)
+                    || n.compareTo(BIG_DECIMAL_INT_MAX) > 0 || n.compareTo(BIG_DECIMAL_INT_MIN) < 0) {
+                throw newLossyConverionException(num, Integer.class);
+            }
+            return n.intValue();
+        } else if (num instanceof BigInteger) {
+            BigInteger n = (BigInteger) num;
+            if (n.compareTo(BIG_INTEGER_INT_MAX) > 0 || n.compareTo(BIG_INTEGER_INT_MIN) < 0) {
+                throw newLossyConverionException(num, Integer.class);
+            }
+            return n.intValue();
+        } else {
+            throw new UnsupportedNumberClassException(num.getClass());
+        }
+    }
+
+    private static ArithmeticException newLossyConverionException(Number fromValue, Class/*<Number>*/ toType) {
+        return new ArithmeticException(
+                "Can't convert " + fromValue + " to type " + _ClassUtil.getShortClassName(toType) + " without loss.");
+    }
+
+    /**
+     * This is needed to reverse the extreme conversions in arithmetic
+     * operations so that numbers can be meaningfully used with models that
+     * don't know what to do with a BigDecimal. Of course, this will make
+     * impossible for these models (i.e. Jython) to receive a BigDecimal even if
+     * it was originally placed as such in the data model. However, since
+     * arithmetic operations aggressively erase the information regarding the
+     * original number type, we have no other choice to ensure expected operation
+     * in majority of cases.
+     */
+    public static Number optimizeNumberRepresentation(Number number) {
+        if (number instanceof BigDecimal) {
+            BigDecimal bd = (BigDecimal) number;
+            if (bd.scale() == 0) {
+                // BigDecimal -> BigInteger
+                number = bd.unscaledValue();
+            } else {
+                double d = bd.doubleValue();
+                if (d != Double.POSITIVE_INFINITY && d != Double.NEGATIVE_INFINITY) {
+                    // BigDecimal -> Double
+                    return Double.valueOf(d);
+                }
+            }
+        }
+        if (number instanceof BigInteger) {
+            BigInteger bi = (BigInteger) number;
+            if (bi.compareTo(BIG_INTEGER_INT_MAX) <= 0 && bi.compareTo(BIG_INTEGER_INT_MIN) >= 0) {
+                // BigInteger -> Integer
+                return Integer.valueOf(bi.intValue());
+            }
+            if (bi.compareTo(BIG_INTEGER_LONG_MAX) <= 0 && bi.compareTo(BIG_INTEGER_LONG_MIN) >= 0) {
+                // BigInteger -> Long
+                return Long.valueOf(bi.longValue());
+            }
+        }
+        return number;
+    }
+
+    public static BigDecimal toBigDecimal(Number num) {
+        try {
+            return num instanceof BigDecimal ? (BigDecimal) num : new BigDecimal(num.toString());
+        } catch (NumberFormatException e) {
+            // The exception message is useless, so we add a new one:
+            throw new NumberFormatException("Can't parse this as BigDecimal number: " + _StringUtil.jQuote(num));
+        }
+    }
+
+    public static Number toBigDecimalOrDouble(String s) {
+        if (s.length() > 2) {
+            char c = s.charAt(0);
+            if (c == 'I' && (s.equals("INF") || s.equals("Infinity"))) {
+                return Double.valueOf(Double.POSITIVE_INFINITY);
+            } else if (c == 'N' && s.equals("NaN")) {
+                return Double.valueOf(Double.NaN);
+            } else if (c == '-' && s.charAt(1) == 'I' && (s.equals("-INF") || s.equals("-Infinity"))) {
+                return Double.valueOf(Double.NEGATIVE_INFINITY);
+            }
+        }
+        return new BigDecimal(s);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ObjectHolder.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ObjectHolder.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ObjectHolder.java
new file mode 100644
index 0000000..cbd7e11
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ObjectHolder.java
@@ -0,0 +1,55 @@
+/*
+ * 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.util;
+
+/**
+ * For internal use only; don't depend on this, there's no backward compatibility guarantee at all!
+ */
+public class _ObjectHolder<T> {
+
+    private T object;
+
+    public _ObjectHolder(T object) {
+        this.object = object;
+    }
+
+    public T get() {
+        return object;
+    }
+
+    public void set(T object) {
+        this.object = object;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        _ObjectHolder<?> that = (_ObjectHolder<?>) o;
+
+        return object != null ? object.equals(that.object) : that.object == null;
+    }
+
+    @Override
+    public int hashCode() {
+        return object != null ? object.hashCode() : 0;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_SecurityUtil.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_SecurityUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_SecurityUtil.java
new file mode 100644
index 0000000..60125a5
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_SecurityUtil.java
@@ -0,0 +1,87 @@
+/*
+ * 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.util;
+
+import java.security.AccessControlException;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+
+import org.apache.freemarker.core._CoreLogs;
+import org.slf4j.Logger;
+
+/**
+ */
+public class _SecurityUtil {
+    
+    private static final Logger LOG = _CoreLogs.SECURITY;
+    
+    private _SecurityUtil() {
+    }
+    
+    public static String getSystemProperty(final String key) {
+        return (String) AccessController.doPrivileged(
+            new PrivilegedAction()
+            {
+                @Override
+                public Object run() {
+                    return System.getProperty(key);
+                }
+            });
+    }
+
+    public static String getSystemProperty(final String key, final String defValue) {
+        try {
+            return (String) AccessController.doPrivileged(
+                new PrivilegedAction()
+                {
+                    @Override
+                    public Object run() {
+                        return System.getProperty(key, defValue);
+                    }
+                });
+        } catch (AccessControlException e) {
+            if (LOG.isWarnEnabled()) {
+                LOG.warn("Insufficient permissions to read system property " + 
+                        _StringUtil.jQuoteNoXSS(key) + ", using default value " +
+                        _StringUtil.jQuoteNoXSS(defValue));
+            }
+            return defValue;
+        }
+    }
+
+    public static Integer getSystemProperty(final String key, final int defValue) {
+        try {
+            return (Integer) AccessController.doPrivileged(
+                new PrivilegedAction()
+                {
+                    @Override
+                    public Object run() {
+                        return Integer.getInteger(key, defValue);
+                    }
+                });
+        } catch (AccessControlException e) {
+            if (LOG.isWarnEnabled()) {
+                LOG.warn("Insufficient permissions to read system property " + 
+                        _StringUtil.jQuote(key) + ", using default value " + defValue);
+            }
+            return Integer.valueOf(defValue);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_SortedArraySet.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_SortedArraySet.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_SortedArraySet.java
new file mode 100644
index 0000000..e60d08d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_SortedArraySet.java
@@ -0,0 +1,80 @@
+/*
+ * 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.util;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+
+/** Don't use this; used internally by FreeMarker, might changes without notice. */
+public class _SortedArraySet<E> extends _UnmodifiableSet<E> {
+
+    private final E[] array;
+
+    public _SortedArraySet(E[] array) {
+        this.array = array;
+    }
+
+    @Override
+    public int size() {
+        return array.length;
+    }
+
+    @Override
+    public boolean contains(Object o) {
+        return Arrays.binarySearch(array, o) >= 0;
+    }
+
+    @Override
+    public Iterator<E> iterator() {
+        return new _ArrayIterator(array);
+    }
+
+    @Override
+    public boolean add(E o) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean remove(Object o) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean addAll(Collection<? extends E> c) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean removeAll(Collection<?> c) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean retainAll(Collection<?> c) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void clear() {
+        throw new UnsupportedOperationException();
+    }
+    
+}


[35/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/MutableProcessingConfiguration.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/MutableProcessingConfiguration.java b/freemarker-core/src/main/java/org/apache/freemarker/core/MutableProcessingConfiguration.java
new file mode 100644
index 0000000..c5c4c82
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/MutableProcessingConfiguration.java
@@ -0,0 +1,2418 @@
+/*
+ * 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.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TimeZone;
+
+import org.apache.freemarker.core.arithmetic.ArithmeticEngine;
+import org.apache.freemarker.core.arithmetic.impl.BigDecimalArithmeticEngine;
+import org.apache.freemarker.core.arithmetic.impl.ConservativeArithmeticEngine;
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.impl.DefaultObjectWrapper;
+import org.apache.freemarker.core.model.impl.RestrictedObjectWrapper;
+import org.apache.freemarker.core.outputformat.OutputFormat;
+import org.apache.freemarker.core.outputformat.impl.HTMLOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.PlainTextOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.RTFOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.UndefinedOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.XMLOutputFormat;
+import org.apache.freemarker.core.templateresolver.AndMatcher;
+import org.apache.freemarker.core.templateresolver.ConditionalTemplateConfigurationFactory;
+import org.apache.freemarker.core.templateresolver.FileNameGlobMatcher;
+import org.apache.freemarker.core.templateresolver.FirstMatchTemplateConfigurationFactory;
+import org.apache.freemarker.core.templateresolver.MergingTemplateConfigurationFactory;
+import org.apache.freemarker.core.templateresolver.NotMatcher;
+import org.apache.freemarker.core.templateresolver.OrMatcher;
+import org.apache.freemarker.core.templateresolver.PathGlobMatcher;
+import org.apache.freemarker.core.templateresolver.PathRegexMatcher;
+import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateNameFormat;
+import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateNameFormatFM2;
+import org.apache.freemarker.core.util.FTLUtil;
+import org.apache.freemarker.core.util.GenericParseException;
+import org.apache.freemarker.core.util.OptInTemplateClassResolver;
+import org.apache.freemarker.core.util._ClassUtil;
+import org.apache.freemarker.core.util._CollectionUtil;
+import org.apache.freemarker.core.util._KeyValuePair;
+import org.apache.freemarker.core.util._NullArgumentException;
+import org.apache.freemarker.core.util._SortedArraySet;
+import org.apache.freemarker.core.util._StringUtil;
+import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory;
+
+/**
+ * Extended by FreeMarker core classes (not by you) that support specifying {@link ProcessingConfiguration} setting
+ * values. <b>New abstract methods may be added any time in future FreeMarker versions, so don't try to implement this
+ * interface yourself!</b>
+ */
+public abstract class MutableProcessingConfiguration<SelfT extends MutableProcessingConfiguration<SelfT>>
+        implements ProcessingConfiguration {
+    public static final String NULL_VALUE = "null";
+    public static final String DEFAULT_VALUE = "default";
+    public static final String JVM_DEFAULT_VALUE = "JVM default";
+    
+    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+    public static final String LOCALE_KEY_SNAKE_CASE = "locale";
+    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+    public static final String LOCALE_KEY_CAMEL_CASE = "locale";
+    /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+    public static final String LOCALE_KEY = LOCALE_KEY_SNAKE_CASE;
+    
+    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+    public static final String NUMBER_FORMAT_KEY_SNAKE_CASE = "number_format";
+    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+    public static final String NUMBER_FORMAT_KEY_CAMEL_CASE = "numberFormat";
+    /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+    public static final String NUMBER_FORMAT_KEY = NUMBER_FORMAT_KEY_SNAKE_CASE;
+    
+    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+    public static final String CUSTOM_NUMBER_FORMATS_KEY_SNAKE_CASE = "custom_number_formats";
+    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+    public static final String CUSTOM_NUMBER_FORMATS_KEY_CAMEL_CASE = "customNumberFormats";
+    /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+    public static final String CUSTOM_NUMBER_FORMATS_KEY = CUSTOM_NUMBER_FORMATS_KEY_SNAKE_CASE;
+    
+    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+    public static final String TIME_FORMAT_KEY_SNAKE_CASE = "time_format";
+    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+    public static final String TIME_FORMAT_KEY_CAMEL_CASE = "timeFormat";
+    /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+    public static final String TIME_FORMAT_KEY = TIME_FORMAT_KEY_SNAKE_CASE;
+    
+    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+    public static final String DATE_FORMAT_KEY_SNAKE_CASE = "date_format";
+    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+    public static final String DATE_FORMAT_KEY_CAMEL_CASE = "dateFormat";
+    /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+    public static final String DATE_FORMAT_KEY = DATE_FORMAT_KEY_SNAKE_CASE;
+    
+    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+    public static final String CUSTOM_DATE_FORMATS_KEY_SNAKE_CASE = "custom_date_formats";
+    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+    public static final String CUSTOM_DATE_FORMATS_KEY_CAMEL_CASE = "customDateFormats";
+    /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+    public static final String CUSTOM_DATE_FORMATS_KEY = CUSTOM_DATE_FORMATS_KEY_SNAKE_CASE;
+    
+    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+    public static final String DATETIME_FORMAT_KEY_SNAKE_CASE = "datetime_format";
+    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+    public static final String DATETIME_FORMAT_KEY_CAMEL_CASE = "datetimeFormat";
+    /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+    public static final String DATETIME_FORMAT_KEY = DATETIME_FORMAT_KEY_SNAKE_CASE;
+    
+    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+    public static final String TIME_ZONE_KEY_SNAKE_CASE = "time_zone";
+    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+    public static final String TIME_ZONE_KEY_CAMEL_CASE = "timeZone";
+    /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+    public static final String TIME_ZONE_KEY = TIME_ZONE_KEY_SNAKE_CASE;
+    
+    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+    public static final String SQL_DATE_AND_TIME_TIME_ZONE_KEY_SNAKE_CASE = "sql_date_and_time_time_zone";
+    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+    public static final String SQL_DATE_AND_TIME_TIME_ZONE_KEY_CAMEL_CASE = "sqlDateAndTimeTimeZone";
+    /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+    public static final String SQL_DATE_AND_TIME_TIME_ZONE_KEY = SQL_DATE_AND_TIME_TIME_ZONE_KEY_SNAKE_CASE;
+    
+    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+    public static final String TEMPLATE_EXCEPTION_HANDLER_KEY_SNAKE_CASE = "template_exception_handler";
+    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+    public static final String TEMPLATE_EXCEPTION_HANDLER_KEY_CAMEL_CASE = "templateExceptionHandler";
+    /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+    public static final String TEMPLATE_EXCEPTION_HANDLER_KEY = TEMPLATE_EXCEPTION_HANDLER_KEY_SNAKE_CASE;
+    
+    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+    public static final String ARITHMETIC_ENGINE_KEY_SNAKE_CASE = "arithmetic_engine";
+    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+    public static final String ARITHMETIC_ENGINE_KEY_CAMEL_CASE = "arithmeticEngine";
+    /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+    public static final String ARITHMETIC_ENGINE_KEY = ARITHMETIC_ENGINE_KEY_SNAKE_CASE;
+    
+    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+    public static final String OBJECT_WRAPPER_KEY_SNAKE_CASE = "object_wrapper";
+    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+    public static final String OBJECT_WRAPPER_KEY_CAMEL_CASE = "objectWrapper";
+    /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+    public static final String OBJECT_WRAPPER_KEY = OBJECT_WRAPPER_KEY_SNAKE_CASE;
+    
+    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+    public static final String BOOLEAN_FORMAT_KEY_SNAKE_CASE = "boolean_format";
+    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+    public static final String BOOLEAN_FORMAT_KEY_CAMEL_CASE = "booleanFormat";
+    /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+    public static final String BOOLEAN_FORMAT_KEY = BOOLEAN_FORMAT_KEY_SNAKE_CASE;
+    
+    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+    public static final String OUTPUT_ENCODING_KEY_SNAKE_CASE = "output_encoding";
+    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+    public static final String OUTPUT_ENCODING_KEY_CAMEL_CASE = "outputEncoding";
+    /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+    public static final String OUTPUT_ENCODING_KEY = OUTPUT_ENCODING_KEY_SNAKE_CASE;
+    
+    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+    public static final String URL_ESCAPING_CHARSET_KEY_SNAKE_CASE = "url_escaping_charset";
+    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+    public static final String URL_ESCAPING_CHARSET_KEY_CAMEL_CASE = "urlEscapingCharset";
+    /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+    public static final String URL_ESCAPING_CHARSET_KEY = URL_ESCAPING_CHARSET_KEY_SNAKE_CASE;
+    
+    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+    public static final String AUTO_FLUSH_KEY_SNAKE_CASE = "auto_flush";
+    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+    public static final String AUTO_FLUSH_KEY_CAMEL_CASE = "autoFlush";
+    /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. @since 2.3.17 */
+    public static final String AUTO_FLUSH_KEY = AUTO_FLUSH_KEY_SNAKE_CASE;
+    
+    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+    public static final String NEW_BUILTIN_CLASS_RESOLVER_KEY_SNAKE_CASE = "new_builtin_class_resolver";
+    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+    public static final String NEW_BUILTIN_CLASS_RESOLVER_KEY_CAMEL_CASE = "newBuiltinClassResolver";
+    /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. @since 2.3.17 */
+    public static final String NEW_BUILTIN_CLASS_RESOLVER_KEY = NEW_BUILTIN_CLASS_RESOLVER_KEY_SNAKE_CASE;
+    
+    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+    public static final String SHOW_ERROR_TIPS_KEY_SNAKE_CASE = "show_error_tips";
+    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+    public static final String SHOW_ERROR_TIPS_KEY_CAMEL_CASE = "showErrorTips";
+    /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. @since 2.3.21 */
+    public static final String SHOW_ERROR_TIPS_KEY = SHOW_ERROR_TIPS_KEY_SNAKE_CASE;
+    
+    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+    public static final String API_BUILTIN_ENABLED_KEY_SNAKE_CASE = "api_builtin_enabled";
+    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+    public static final String API_BUILTIN_ENABLED_KEY_CAMEL_CASE = "apiBuiltinEnabled";
+    /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. @since 2.3.22 */
+    public static final String API_BUILTIN_ENABLED_KEY = API_BUILTIN_ENABLED_KEY_SNAKE_CASE;
+    
+    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+    public static final String LOG_TEMPLATE_EXCEPTIONS_KEY_SNAKE_CASE = "log_template_exceptions";
+    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+    public static final String LOG_TEMPLATE_EXCEPTIONS_KEY_CAMEL_CASE = "logTemplateExceptions";
+    /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. @since 2.3.22 */
+    public static final String LOG_TEMPLATE_EXCEPTIONS_KEY = LOG_TEMPLATE_EXCEPTIONS_KEY_SNAKE_CASE;
+
+    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.25 */
+    public static final String LAZY_IMPORTS_KEY_SNAKE_CASE = "lazy_imports";
+    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.25 */
+    public static final String LAZY_IMPORTS_KEY_CAMEL_CASE = "lazyImports";
+    /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+    public static final String LAZY_IMPORTS_KEY = LAZY_IMPORTS_KEY_SNAKE_CASE;
+
+    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.25 */
+    public static final String LAZY_AUTO_IMPORTS_KEY_SNAKE_CASE = "lazy_auto_imports";
+    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.25 */
+    public static final String LAZY_AUTO_IMPORTS_KEY_CAMEL_CASE = "lazyAutoImports";
+    /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+    public static final String LAZY_AUTO_IMPORTS_KEY = LAZY_AUTO_IMPORTS_KEY_SNAKE_CASE;
+    
+    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.25 */
+    public static final String AUTO_IMPORT_KEY_SNAKE_CASE = "auto_import";
+    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.25 */
+    public static final String AUTO_IMPORT_KEY_CAMEL_CASE = "autoImport";
+    /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+    public static final String AUTO_IMPORT_KEY = AUTO_IMPORT_KEY_SNAKE_CASE;
+    
+    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.25 */
+    public static final String AUTO_INCLUDE_KEY_SNAKE_CASE = "auto_include";
+    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.25 */
+    public static final String AUTO_INCLUDE_KEY_CAMEL_CASE = "autoInclude";
+    /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+    public static final String AUTO_INCLUDE_KEY = AUTO_INCLUDE_KEY_SNAKE_CASE;
+    
+    private static final String[] SETTING_NAMES_SNAKE_CASE = new String[] {
+        // Must be sorted alphabetically!
+        API_BUILTIN_ENABLED_KEY_SNAKE_CASE,
+        ARITHMETIC_ENGINE_KEY_SNAKE_CASE,
+        AUTO_FLUSH_KEY_SNAKE_CASE,
+        AUTO_IMPORT_KEY_SNAKE_CASE,
+        AUTO_INCLUDE_KEY_SNAKE_CASE,
+        BOOLEAN_FORMAT_KEY_SNAKE_CASE,
+        CUSTOM_DATE_FORMATS_KEY_SNAKE_CASE,
+        CUSTOM_NUMBER_FORMATS_KEY_SNAKE_CASE,
+        DATE_FORMAT_KEY_SNAKE_CASE,
+        DATETIME_FORMAT_KEY_SNAKE_CASE,
+        LAZY_AUTO_IMPORTS_KEY_SNAKE_CASE,
+        LAZY_IMPORTS_KEY_SNAKE_CASE,
+        LOCALE_KEY_SNAKE_CASE,
+        LOG_TEMPLATE_EXCEPTIONS_KEY_SNAKE_CASE,
+        NEW_BUILTIN_CLASS_RESOLVER_KEY_SNAKE_CASE,
+        NUMBER_FORMAT_KEY_SNAKE_CASE,
+        OBJECT_WRAPPER_KEY_SNAKE_CASE,
+        OUTPUT_ENCODING_KEY_SNAKE_CASE,
+        SHOW_ERROR_TIPS_KEY_SNAKE_CASE,
+        SQL_DATE_AND_TIME_TIME_ZONE_KEY_SNAKE_CASE,
+        TEMPLATE_EXCEPTION_HANDLER_KEY_SNAKE_CASE,
+        TIME_FORMAT_KEY_SNAKE_CASE,
+        TIME_ZONE_KEY_SNAKE_CASE,
+        URL_ESCAPING_CHARSET_KEY_SNAKE_CASE
+    };
+    
+    private static final String[] SETTING_NAMES_CAMEL_CASE = new String[] {
+        // Must be sorted alphabetically!
+        API_BUILTIN_ENABLED_KEY_CAMEL_CASE,
+        ARITHMETIC_ENGINE_KEY_CAMEL_CASE,
+        AUTO_FLUSH_KEY_CAMEL_CASE,
+        AUTO_IMPORT_KEY_CAMEL_CASE,
+        AUTO_INCLUDE_KEY_CAMEL_CASE,
+        BOOLEAN_FORMAT_KEY_CAMEL_CASE,
+        CUSTOM_DATE_FORMATS_KEY_CAMEL_CASE,
+        CUSTOM_NUMBER_FORMATS_KEY_CAMEL_CASE,
+        DATE_FORMAT_KEY_CAMEL_CASE,
+        DATETIME_FORMAT_KEY_CAMEL_CASE,
+        LAZY_AUTO_IMPORTS_KEY_CAMEL_CASE,
+        LAZY_IMPORTS_KEY_CAMEL_CASE,
+        LOCALE_KEY_CAMEL_CASE,
+        LOG_TEMPLATE_EXCEPTIONS_KEY_CAMEL_CASE,
+        NEW_BUILTIN_CLASS_RESOLVER_KEY_CAMEL_CASE,
+        NUMBER_FORMAT_KEY_CAMEL_CASE,
+        OBJECT_WRAPPER_KEY_CAMEL_CASE,
+        OUTPUT_ENCODING_KEY_CAMEL_CASE,
+        SHOW_ERROR_TIPS_KEY_CAMEL_CASE,
+        SQL_DATE_AND_TIME_TIME_ZONE_KEY_CAMEL_CASE,
+        TEMPLATE_EXCEPTION_HANDLER_KEY_CAMEL_CASE,
+        TIME_FORMAT_KEY_CAMEL_CASE,
+        TIME_ZONE_KEY_CAMEL_CASE,
+        URL_ESCAPING_CHARSET_KEY_CAMEL_CASE
+    };
+
+    private Locale locale;
+    private String numberFormat;
+    private String timeFormat;
+    private String dateFormat;
+    private String dateTimeFormat;
+    private TimeZone timeZone;
+    private TimeZone sqlDateAndTimeTimeZone;
+    private boolean sqlDateAndTimeTimeZoneSet;
+    private String booleanFormat;
+    private TemplateExceptionHandler templateExceptionHandler;
+    private ArithmeticEngine arithmeticEngine;
+    private ObjectWrapper objectWrapper;
+    private Charset outputEncoding;
+    private boolean outputEncodingSet;
+    private Charset urlEscapingCharset;
+    private boolean urlEscapingCharsetSet;
+    private Boolean autoFlush;
+    private TemplateClassResolver newBuiltinClassResolver;
+    private Boolean showErrorTips;
+    private Boolean apiBuiltinEnabled;
+    private Boolean logTemplateExceptions;
+    private Map<String, TemplateDateFormatFactory> customDateFormats;
+    private Map<String, TemplateNumberFormatFactory> customNumberFormats;
+    private LinkedHashMap<String, String> autoImports;
+    private ArrayList<String> autoIncludes;
+    private Boolean lazyImports;
+    private Boolean lazyAutoImports;
+    private boolean lazyAutoImportsSet;
+    private Map<Object, Object> customAttributes;
+
+    /**
+     * Creates a new instance. Normally you do not need to use this constructor,
+     * as you don't use <code>MutableProcessingConfiguration</code> directly, but its subclasses.
+     */
+    protected MutableProcessingConfiguration() {
+        // Empty
+    }
+
+    @Override
+    public Locale getLocale() {
+         return isLocaleSet() ? locale : getDefaultLocale();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set (possibly by inheriting the setting value
+     * from another {@link ProcessingConfiguration}), or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract Locale getDefaultLocale();
+
+    @Override
+    public boolean isLocaleSet() {
+        return locale != null;
+    }
+
+    /**
+     * Setter pair of {@link ProcessingConfiguration#getLocale()}.
+     */
+    public void setLocale(Locale locale) {
+        _NullArgumentException.check("locale", locale);
+        this.locale = locale;
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setLocale(Locale)}
+     */
+    public SelfT locale(Locale value) {
+        setLocale(value);
+        return self();
+    }
+
+    /**
+     * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+     * {@link ProcessingConfiguration}).
+     */
+    public void unsetLocale() {
+        locale = null;
+    }
+
+    /**
+     * Setter pair of {@link ProcessingConfiguration#getTimeZone()}.
+     */
+    public void setTimeZone(TimeZone timeZone) {
+        _NullArgumentException.check("timeZone", timeZone);
+        this.timeZone = timeZone;
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setTimeZone(TimeZone)}
+     */
+    public SelfT timeZone(TimeZone value) {
+        setTimeZone(value);
+        return self();
+    }
+    /**
+     * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+     * {@link ProcessingConfiguration}).
+     */
+    public void unsetTimeZone() {
+        this.timeZone = null;
+    }
+
+    @Override
+    public TimeZone getTimeZone() {
+         return isTimeZoneSet() ? timeZone : getDefaultTimeZone();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set (possibly by inheriting the setting value
+     * from another {@link ProcessingConfiguration}), or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract TimeZone getDefaultTimeZone();
+
+    @Override
+    public boolean isTimeZoneSet() {
+        return timeZone != null;
+    }
+    
+    /**
+     * Setter pair of {@link ProcessingConfiguration#getSQLDateAndTimeTimeZone()}.
+     */
+    public void setSQLDateAndTimeTimeZone(TimeZone tz) {
+        sqlDateAndTimeTimeZone = tz;
+        sqlDateAndTimeTimeZoneSet = true;
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setSQLDateAndTimeTimeZone(TimeZone)}
+     */
+    public SelfT sqlDateAndTimeTimeZone(TimeZone value) {
+        setSQLDateAndTimeTimeZone(value);
+        return self();
+    }
+
+    /**
+     * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+     * {@link ProcessingConfiguration}).
+     */
+    public void unsetSQLDateAndTimeTimeZone() {
+        sqlDateAndTimeTimeZone = null;
+        sqlDateAndTimeTimeZoneSet = false;
+    }
+
+    @Override
+    public TimeZone getSQLDateAndTimeTimeZone() {
+        return sqlDateAndTimeTimeZoneSet
+                ? sqlDateAndTimeTimeZone
+                : getDefaultSQLDateAndTimeTimeZone();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set (possibly by inheriting the setting value
+     * from another {@link ProcessingConfiguration}), or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract TimeZone getDefaultSQLDateAndTimeTimeZone();
+
+    @Override
+    public boolean isSQLDateAndTimeTimeZoneSet() {
+        return sqlDateAndTimeTimeZoneSet;
+    }
+
+    /**
+     * Setter pair of {@link #getNumberFormat()}
+     */
+    public void setNumberFormat(String numberFormat) {
+        _NullArgumentException.check("numberFormat", numberFormat);
+        this.numberFormat = numberFormat;
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setNumberFormat(String)}
+     */
+    public SelfT numberFormat(String value) {
+        setNumberFormat(value);
+        return self();
+    }
+
+    /**
+     * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+     * {@link ProcessingConfiguration}).
+     */
+    public void unsetNumberFormat() {
+        numberFormat = null;
+    }
+
+    @Override
+    public String getNumberFormat() {
+         return isNumberFormatSet() ? numberFormat : getDefaultNumberFormat();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set (possibly by inheriting the setting value
+     * from another {@link ProcessingConfiguration}), or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract String getDefaultNumberFormat();
+
+    @Override
+    public boolean isNumberFormatSet() {
+        return numberFormat != null;
+    }
+    
+    @Override
+    public Map<String, TemplateNumberFormatFactory> getCustomNumberFormats() {
+         return isCustomNumberFormatsSet() ? customNumberFormats : getDefaultCustomNumberFormats();
+    }
+
+    protected abstract Map<String, TemplateNumberFormatFactory> getDefaultCustomNumberFormats();
+
+    /**
+     * Setter pair of {@link #getCustomNumberFormats()}. Note that custom number formats are get through
+     * {@link #getCustomNumberFormat(String)}, not directly though this {@link Map}, so number formats from
+     * {@link ProcessingConfiguration}-s on less specific levels are inherited without you copying them into this
+     * {@link Map}.
+     *
+     * @param customNumberFormats
+     *      Not {@code null}.
+     */
+    public void setCustomNumberFormats(Map<String, TemplateNumberFormatFactory> customNumberFormats) {
+        _NullArgumentException.check("customNumberFormats", customNumberFormats);
+        validateFormatNames(customNumberFormats.keySet());
+        this.customNumberFormats = customNumberFormats;
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setCustomNumberFormats(Map)}
+     */
+    public SelfT customNumberFormats(Map<String, TemplateNumberFormatFactory> value) {
+        setCustomNumberFormats(value);
+        return self();
+    }
+
+    /**
+     * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+     * {@link ProcessingConfiguration}).
+     */
+    public void unsetCustomNumberFormats() {
+        customNumberFormats = null;
+    }
+
+    private void validateFormatNames(Set<String> keySet) {
+        for (String name : keySet) {
+            if (name.length() == 0) {
+                throw new IllegalArgumentException("Format names can't be 0 length");
+            }
+            char firstChar = name.charAt(0);
+            if (firstChar == '@') {
+                throw new IllegalArgumentException(
+                        "Format names can't start with '@'. '@' is only used when referring to them from format "
+                        + "strings. In: " + name);
+            }
+            if (!Character.isLetter(firstChar)) {
+                throw new IllegalArgumentException("Format name must start with letter: " + name);
+            }
+            for (int i = 1; i < name.length(); i++) {
+                // Note that we deliberately don't allow "_" here.
+                if (!Character.isLetterOrDigit(name.charAt(i))) {
+                    throw new IllegalArgumentException("Format name can only contain letters and digits: " + name);
+                }
+            }
+        }
+    }
+
+    @Override
+    public boolean isCustomNumberFormatsSet() {
+        return customNumberFormats != null;
+    }
+
+    @Override
+    public TemplateNumberFormatFactory getCustomNumberFormat(String name) {
+        TemplateNumberFormatFactory r;
+        if (customNumberFormats != null) {
+            r = customNumberFormats.get(name);
+            if (r != null) {
+                return r;
+            }
+        }
+        return getDefaultCustomNumberFormat(name);
+    }
+
+    protected abstract TemplateNumberFormatFactory getDefaultCustomNumberFormat(String name);
+
+    /**
+     * Setter pair of {@link #getBooleanFormat()}.
+     */
+    public void setBooleanFormat(String booleanFormat) {
+        _NullArgumentException.check("booleanFormat", booleanFormat);
+        
+        int commaIdx = booleanFormat.indexOf(',');
+        if (commaIdx == -1) {
+            throw new IllegalArgumentException(
+                    "Setting value must be string that contains two comma-separated values for true and false, " +
+                    "respectively.");
+        }
+        
+        this.booleanFormat = booleanFormat; 
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setBooleanFormat(String)}
+     */
+    public SelfT booleanFormat(String value) {
+        setBooleanFormat(value);
+        return self();
+    }
+
+    /**
+     * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+     * {@link ProcessingConfiguration}).
+     */
+    public void unsetBooleanFormat() {
+        booleanFormat = null;
+    }
+    
+    @Override
+    public String getBooleanFormat() {
+         return isBooleanFormatSet() ? booleanFormat : getDefaultBooleanFormat();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set (possibly by inheriting the setting value
+     * from another {@link ProcessingConfiguration}), or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract String getDefaultBooleanFormat();
+
+    @Override
+    public boolean isBooleanFormatSet() {
+        return booleanFormat != null;
+    }
+
+    /**
+     * Setter pair of {@link #getTimeFormat()}
+     */
+    public void setTimeFormat(String timeFormat) {
+        _NullArgumentException.check("timeFormat", timeFormat);
+        this.timeFormat = timeFormat;
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setTimeFormat(String)}
+     */
+    public SelfT timeFormat(String value) {
+        setTimeFormat(value);
+        return self();
+    }
+
+    /**
+     * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+     * {@link ProcessingConfiguration}).
+     */
+    public void unsetTimeFormat() {
+        timeFormat = null;
+    }
+
+    @Override
+    public String getTimeFormat() {
+         return isTimeFormatSet() ? timeFormat : getDefaultTimeFormat();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set (possibly by inheriting the setting value
+     * from another {@link ProcessingConfiguration}), or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract String getDefaultTimeFormat();
+
+    @Override
+    public boolean isTimeFormatSet() {
+        return timeFormat != null;
+    }
+
+    /**
+     * Setter pair of {@link #getDateFormat()}.
+     */
+    public void setDateFormat(String dateFormat) {
+        _NullArgumentException.check("dateFormat", dateFormat);
+        this.dateFormat = dateFormat;
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setDateFormat(String)}
+     */
+    public SelfT dateFormat(String value) {
+        setDateFormat(value);
+        return self();
+    }
+
+    /**
+     * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+     * {@link ProcessingConfiguration}).
+     */
+    public void unsetDateFormat() {
+        dateFormat = null;
+    }
+
+    @Override
+    public String getDateFormat() {
+         return isDateFormatSet() ? dateFormat : getDefaultDateFormat();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set (possibly by inheriting the setting value
+     * from another {@link ProcessingConfiguration}), or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract String getDefaultDateFormat();
+
+    @Override
+    public boolean isDateFormatSet() {
+        return dateFormat != null;
+    }
+
+    /**
+     * Setter pair of {@link #getDateTimeFormat()}
+     */
+    public void setDateTimeFormat(String dateTimeFormat) {
+        _NullArgumentException.check("dateTimeFormat", dateTimeFormat);
+        this.dateTimeFormat = dateTimeFormat;
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setDateTimeFormat(String)}
+     */
+    public SelfT dateTimeFormat(String value) {
+        setDateTimeFormat(value);
+        return self();
+    }
+
+    /**
+     * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+     * {@link ProcessingConfiguration}).
+     */
+    public void unsetDateTimeFormat() {
+        this.dateTimeFormat = null;
+    }
+
+    @Override
+    public String getDateTimeFormat() {
+         return isDateTimeFormatSet() ? dateTimeFormat : getDefaultDateTimeFormat();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set (possibly by inheriting the setting value
+     * from another {@link ProcessingConfiguration}), or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract String getDefaultDateTimeFormat();
+
+    @Override
+    public boolean isDateTimeFormatSet() {
+        return dateTimeFormat != null;
+    }
+
+    @Override
+    public Map<String, TemplateDateFormatFactory> getCustomDateFormats() {
+         return isCustomDateFormatsSet() ? customDateFormats : getDefaultCustomDateFormats();
+    }
+
+    protected abstract Map<String, TemplateDateFormatFactory> getDefaultCustomDateFormats();
+
+    /**
+     * Setter pair of {@link #getCustomDateFormat(String)}.
+     */
+    public void setCustomDateFormats(Map<String, TemplateDateFormatFactory> customDateFormats) {
+        _NullArgumentException.check("customDateFormats", customDateFormats);
+        validateFormatNames(customDateFormats.keySet());
+        this.customDateFormats = customDateFormats;
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setCustomDateFormats(Map)}
+     */
+    public SelfT customDateFormats(Map<String, TemplateDateFormatFactory> value) {
+        setCustomDateFormats(value);
+        return self();
+    }
+
+    /**
+     * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+     * {@link ProcessingConfiguration}).
+     */
+    public void unsetCustomDateFormats() {
+        this.customDateFormats = null;
+    }
+
+    @Override
+    public boolean isCustomDateFormatsSet() {
+        return customDateFormats != null;
+    }
+
+    @Override
+    public TemplateDateFormatFactory getCustomDateFormat(String name) {
+        TemplateDateFormatFactory r;
+        if (customDateFormats != null) {
+            r = customDateFormats.get(name);
+            if (r != null) {
+                return r;
+            }
+        }
+        return getDefaultCustomDateFormat(name);
+    }
+
+    protected abstract TemplateDateFormatFactory getDefaultCustomDateFormat(String name);
+
+    /**
+     * Setter pair of {@link #getTemplateExceptionHandler()}
+     */
+    public void setTemplateExceptionHandler(TemplateExceptionHandler templateExceptionHandler) {
+        _NullArgumentException.check("templateExceptionHandler", templateExceptionHandler);
+        this.templateExceptionHandler = templateExceptionHandler;
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setTemplateExceptionHandler(TemplateExceptionHandler)}
+     */
+    public SelfT templateExceptionHandler(TemplateExceptionHandler value) {
+        setTemplateExceptionHandler(value);
+        return self();
+    }
+
+    /**
+     * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+     * {@link ProcessingConfiguration}).
+     */
+    public void unsetTemplateExceptionHandler() {
+        templateExceptionHandler = null;
+    }
+
+    @Override
+    public TemplateExceptionHandler getTemplateExceptionHandler() {
+         return isTemplateExceptionHandlerSet()
+                ? templateExceptionHandler : getDefaultTemplateExceptionHandler();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set (possibly by inheriting the setting value
+     * from another {@link ProcessingConfiguration}), or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract TemplateExceptionHandler getDefaultTemplateExceptionHandler();
+
+    @Override
+    public boolean isTemplateExceptionHandlerSet() {
+        return templateExceptionHandler != null;
+    }
+
+    /**
+     * Setter pair of {@link #getArithmeticEngine()}
+     */
+    public void setArithmeticEngine(ArithmeticEngine arithmeticEngine) {
+        _NullArgumentException.check("arithmeticEngine", arithmeticEngine);
+        this.arithmeticEngine = arithmeticEngine;
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setArithmeticEngine(ArithmeticEngine)}
+     */
+    public SelfT arithmeticEngine(ArithmeticEngine value) {
+        setArithmeticEngine(value);
+        return self();
+    }
+
+    /**
+     * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+     * {@link ProcessingConfiguration}).
+     */
+    public void unsetArithmeticEngine() {
+        this.arithmeticEngine = null;
+    }
+
+    @Override
+    public ArithmeticEngine getArithmeticEngine() {
+         return isArithmeticEngineSet() ? arithmeticEngine : getDefaultArithmeticEngine();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set (possibly by inheriting the setting value
+     * from another {@link ProcessingConfiguration}), or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract ArithmeticEngine getDefaultArithmeticEngine();
+
+    @Override
+    public boolean isArithmeticEngineSet() {
+        return arithmeticEngine != null;
+    }
+
+    /**
+     * Setter pair of {@link #getObjectWrapper()}
+     */
+    public void setObjectWrapper(ObjectWrapper objectWrapper) {
+        _NullArgumentException.check("objectWrapper", objectWrapper);
+        this.objectWrapper = objectWrapper;
+    }
+
+    /**
+     * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+     * {@link ProcessingConfiguration}).
+     */
+    public void unsetObjectWrapper() {
+        objectWrapper = null;
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setObjectWrapper(ObjectWrapper)}
+     */
+    public SelfT objectWrapper(ObjectWrapper value) {
+        setObjectWrapper(value);
+        return self();
+    }
+
+    @Override
+    public ObjectWrapper getObjectWrapper() {
+         return isObjectWrapperSet()
+                ? objectWrapper : getDefaultObjectWrapper();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set (possibly by inheriting the setting value
+     * from another {@link ProcessingConfiguration}), or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract ObjectWrapper getDefaultObjectWrapper();
+
+    @Override
+    public boolean isObjectWrapperSet() {
+        return objectWrapper != null;
+    }
+
+    /**
+     * The setter pair of {@link #getOutputEncoding()}
+     */
+    public void setOutputEncoding(Charset outputEncoding) {
+        this.outputEncoding = outputEncoding;
+        outputEncodingSet = true;
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setOutputEncoding(Charset)}
+     */
+    public SelfT outputEncoding(Charset value) {
+        setOutputEncoding(value);
+        return self();
+    }
+
+    /**
+     * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+     * {@link ProcessingConfiguration}).
+     */
+    public void unsetOutputEncoding() {
+        this.outputEncoding = null;
+        outputEncodingSet = false;
+    }
+
+    @Override
+    public Charset getOutputEncoding() {
+        return isOutputEncodingSet()
+                ? outputEncoding
+                : getDefaultOutputEncoding();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set (possibly by inheriting the setting value
+     * from another {@link ProcessingConfiguration}), or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract Charset getDefaultOutputEncoding();
+
+    @Override
+    public boolean isOutputEncodingSet() {
+        return outputEncodingSet;
+    }
+
+    /**
+     * The setter pair of {@link #getURLEscapingCharset()}.
+     */
+    public void setURLEscapingCharset(Charset urlEscapingCharset) {
+        this.urlEscapingCharset = urlEscapingCharset;
+        urlEscapingCharsetSet = true;
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setURLEscapingCharset(Charset)}
+     */
+    public SelfT urlEscapingCharset(Charset value) {
+        setURLEscapingCharset(value);
+        return self();
+    }
+
+    /**
+     * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+     * {@link ProcessingConfiguration}).
+     */
+    public void unsetURLEscapingCharset() {
+        this.urlEscapingCharset = null;
+        urlEscapingCharsetSet = false;
+    }
+
+    @Override
+    public Charset getURLEscapingCharset() {
+        return isURLEscapingCharsetSet() ? urlEscapingCharset : getDefaultURLEscapingCharset();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set (possibly by inheriting the setting value
+     * from another {@link ProcessingConfiguration}), or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract Charset getDefaultURLEscapingCharset();
+
+    @Override
+    public boolean isURLEscapingCharsetSet() {
+        return urlEscapingCharsetSet;
+    }
+
+    /**
+     * Setter pair of {@link #getNewBuiltinClassResolver()}
+     */
+    public void setNewBuiltinClassResolver(TemplateClassResolver newBuiltinClassResolver) {
+        _NullArgumentException.check("newBuiltinClassResolver", newBuiltinClassResolver);
+        this.newBuiltinClassResolver = newBuiltinClassResolver;
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setNewBuiltinClassResolver(TemplateClassResolver)}
+     */
+    public SelfT newBuiltinClassResolver(TemplateClassResolver value) {
+        setNewBuiltinClassResolver(value);
+        return self();
+    }
+
+    /**
+     * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+     * {@link ProcessingConfiguration}).
+     */
+    public void unsetNewBuiltinClassResolver() {
+        this.newBuiltinClassResolver = null;
+    }
+
+    @Override
+    public TemplateClassResolver getNewBuiltinClassResolver() {
+         return isNewBuiltinClassResolverSet()
+                ? newBuiltinClassResolver : getDefaultNewBuiltinClassResolver();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set (possibly by inheriting the setting value
+     * from another {@link ProcessingConfiguration}), or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract TemplateClassResolver getDefaultNewBuiltinClassResolver();
+
+    @Override
+    public boolean isNewBuiltinClassResolverSet() {
+        return newBuiltinClassResolver != null;
+    }
+
+    /**
+     * Setter pair of {@link #getAutoFlush()}
+     */
+    public void setAutoFlush(boolean autoFlush) {
+        this.autoFlush = autoFlush;
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setAutoFlush(boolean)}
+     */
+    public SelfT autoFlush(boolean value) {
+        setAutoFlush(value);
+        return self();
+    }
+
+    /**
+     * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+     * {@link ProcessingConfiguration}).
+     */
+    public void unsetAutoFlush() {
+        this.autoFlush = null;
+    }
+
+    @Override
+    public boolean getAutoFlush() {
+         return isAutoFlushSet() ? autoFlush : getDefaultAutoFlush();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set (possibly by inheriting the setting value
+     * from another {@link ProcessingConfiguration}), or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract boolean getDefaultAutoFlush();
+
+    @Override
+    public boolean isAutoFlushSet() {
+        return autoFlush != null;
+    }
+
+    /**
+     * Setter pair of {@link #getShowErrorTips()}
+     */
+    public void setShowErrorTips(boolean showTips) {
+        showErrorTips = showTips;
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setShowErrorTips(boolean)}
+     */
+    public SelfT showErrorTips(boolean value) {
+        setShowErrorTips(value);
+        return self();
+    }
+
+    /**
+     * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+     * {@link ProcessingConfiguration}).
+     */
+    public void unsetShowErrorTips() {
+        showErrorTips = null;
+    }
+
+    @Override
+    public boolean getShowErrorTips() {
+         return isShowErrorTipsSet() ? showErrorTips : getDefaultShowErrorTips();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set (possibly by inheriting the setting value
+     * from another {@link ProcessingConfiguration}), or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract boolean getDefaultShowErrorTips();
+
+    @Override
+    public boolean isShowErrorTipsSet() {
+        return showErrorTips != null;
+    }
+
+    /**
+     * Setter pair of {@link #getAPIBuiltinEnabled()}
+     */
+    public void setAPIBuiltinEnabled(boolean value) {
+        apiBuiltinEnabled = Boolean.valueOf(value);
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setAPIBuiltinEnabled(boolean)}
+     */
+    public SelfT apiBuiltinEnabled(boolean value) {
+        setAPIBuiltinEnabled(value);
+        return self();
+    }
+
+    /**
+     * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+     * {@link ProcessingConfiguration}).
+     */
+    public void unsetAPIBuiltinEnabled() {
+        apiBuiltinEnabled = null;
+    }
+
+    @Override
+    public boolean getAPIBuiltinEnabled() {
+         return isAPIBuiltinEnabledSet() ? apiBuiltinEnabled : getDefaultAPIBuiltinEnabled();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set (possibly by inheriting the setting value
+     * from another {@link ProcessingConfiguration}), or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract boolean getDefaultAPIBuiltinEnabled();
+
+    @Override
+    public boolean isAPIBuiltinEnabledSet() {
+        return apiBuiltinEnabled != null;
+    }
+
+    @Override
+    public boolean getLogTemplateExceptions() {
+         return isLogTemplateExceptionsSet() ? logTemplateExceptions : getDefaultLogTemplateExceptions();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set (possibly by inheriting the setting value
+     * from another {@link ProcessingConfiguration}), or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract boolean getDefaultLogTemplateExceptions();
+
+    @Override
+    public boolean isLogTemplateExceptionsSet() {
+        return logTemplateExceptions != null;
+    }
+
+    /**
+     * Setter pair of {@link #getLogTemplateExceptions()}
+     */
+    public void setLogTemplateExceptions(boolean value) {
+        logTemplateExceptions = value;
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setLogTemplateExceptions(boolean)}
+     */
+    public SelfT logTemplateExceptions(boolean value) {
+        setLogTemplateExceptions(value);
+        return self();
+    }
+
+    /**
+     * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+     * {@link ProcessingConfiguration}).
+     */
+    public void unsetLogTemplateExceptions() {
+        logTemplateExceptions = null;
+    }
+
+    @Override
+    public boolean getLazyImports() {
+         return isLazyImportsSet() ? lazyImports : getDefaultLazyImports();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set (possibly by inheriting the setting value
+     * from another {@link ProcessingConfiguration}), or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract boolean getDefaultLazyImports();
+
+    /**
+     * Setter pair of {@link #getLazyImports()}
+     */
+    public void setLazyImports(boolean lazyImports) {
+        this.lazyImports = lazyImports;
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setLazyImports(boolean)}
+     */
+    public SelfT lazyImports(boolean lazyImports) {
+        setLazyImports(lazyImports);
+        return  self();
+    }
+
+    /**
+     * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+     * {@link ProcessingConfiguration}).
+     */
+    public void unsetLazyImports() {
+        this.lazyImports = null;
+    }
+
+    @Override
+    public boolean isLazyImportsSet() {
+        return lazyImports != null;
+    }
+
+    @Override
+    public Boolean getLazyAutoImports() {
+        return isLazyAutoImportsSet() ? lazyAutoImports : getDefaultLazyAutoImports();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set (possibly by inheriting the setting value
+     * from another {@link ProcessingConfiguration}), or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract Boolean getDefaultLazyAutoImports();
+
+    /**
+     * Setter pair of {@link #getLazyAutoImports()}
+     */
+    public void setLazyAutoImports(Boolean lazyAutoImports) {
+        this.lazyAutoImports = lazyAutoImports;
+        lazyAutoImportsSet = true;
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setLazyAutoImports(Boolean)}
+     */
+    public SelfT lazyAutoImports(Boolean lazyAutoImports) {
+        setLazyAutoImports(lazyAutoImports);
+        return self();
+    }
+
+    /**
+     * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+     * {@link ProcessingConfiguration}).
+     */
+    public void unsetLazyAutoImports() {
+        lazyAutoImports = null;
+        lazyAutoImportsSet = false;
+    }
+
+    @Override
+    public boolean isLazyAutoImportsSet() {
+        return lazyAutoImportsSet;
+    }
+    
+    /**
+     * Adds the auto-import at the end of {@link #getAutoImports()}. If an auto-import with the same namespace variable
+     * name already exists in the {@link Map}, it will be removed before the new one is added.
+     */
+    public void addAutoImport(String namespaceVarName, String templateName) {
+        if (autoImports == null) {
+            initAutoImportsMap();
+        } else {
+            // This was a List earlier, so re-inserted items must go to the end, hence we remove() before put().
+            autoImports.remove(namespaceVarName);
+        }
+        autoImports.put(namespaceVarName, templateName);
+    }
+
+    private void initAutoImportsMap() {
+        autoImports = new LinkedHashMap<>(4);
+    }
+    
+    /**
+     * Removes an auto-import from {@link #getAutoImports()} (but doesn't affect auto-imports inherited from another
+     * {@link ParsingConfiguration}). Does nothing if the auto-import doesn't exist.
+     */
+    public void removeAutoImport(String namespaceVarName) {
+        if (autoImports != null) {
+            autoImports.remove(namespaceVarName);
+        }
+    }
+    
+    /**
+     * Setter pair of {@link #getAutoImports()}.
+     * 
+     * @param map
+     *            Maps the namespace variable names to the template names; not {@code null}. The content of the
+     *            {@link Map} is copied into another {@link Map}, to avoid aliasing problems.
+     */
+    public void setAutoImports(Map map) {
+        _NullArgumentException.check("map", map);
+        
+        if (autoImports != null) {
+            autoImports.clear();
+        }
+        for (Map.Entry<?, ?> entry : ((Map<?, ?>) map).entrySet()) {
+            Object key = entry.getKey();
+            if (!(key instanceof String)) {
+                throw new IllegalArgumentException(
+                        "Key in Map wasn't a String, but a(n) " + key.getClass().getName() + ".");
+            }
+
+            Object value = entry.getValue();
+            if (!(value instanceof String)) {
+                throw new IllegalArgumentException(
+                        "Value in Map wasn't a String, but a(n) " + value.getClass().getName() + ".");
+            }
+
+            addAutoImport((String) key, (String) value);
+        }
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setAutoImports(Map)}
+     */
+    public SelfT autoImports(Map map) {
+        setAutoImports(map);
+        return self();
+    }
+
+     /**
+     * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+     * {@link ProcessingConfiguration}).
+     */
+    public void unsetAutoImports() {
+        autoImports = null;
+    }
+
+    @Override
+    public Map<String, String> getAutoImports() {
+         return isAutoImportsSet() ? autoImports : getDefaultAutoImports();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set (possibly by inheriting the setting value
+     * from another {@link ProcessingConfiguration}), or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract Map<String,String> getDefaultAutoImports();
+
+    @Override
+    public boolean isAutoImportsSet() {
+        return autoImports != null;
+    }
+
+    /**
+     * Adds an auto-include to {@link #getAutoIncludes()}. If the template name is already in the {@link List}, then it
+     * will be removed before it's added again (so in effect it's moved to the end of the {@link List}).
+     */
+    public void addAutoInclude(String templateName) {
+        if (autoIncludes == null) {
+            initAutoIncludesList();
+        } else {
+            autoIncludes.remove(templateName);
+        }
+        autoIncludes.add(templateName);
+    }
+
+    private void initAutoIncludesList() {
+        autoIncludes = new ArrayList<>(4);
+    }
+    
+    /**
+     * Setter pair of {@link #getAutoIncludes()}
+     *
+     * @param templateNames Not {@code null}. The {@link List} will be copied to avoid aliasing problems.
+     */
+    public void setAutoIncludes(List templateNames) {
+        _NullArgumentException.check("templateNames", templateNames);
+        if (autoIncludes != null) {
+            autoIncludes.clear();
+        }
+        for (Object templateName : templateNames) {
+            if (!(templateName instanceof String)) {
+                throw new IllegalArgumentException("List items must be String-s.");
+            }
+            addAutoInclude((String) templateName);
+        }
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setAutoIncludes(List)}
+     */
+    public SelfT autoIncludes(List templateNames) {
+        setAutoIncludes(templateNames);
+        return self();
+    }
+
+    @Override
+    public List<String> getAutoIncludes() {
+         return isAutoIncludesSet() ? autoIncludes : getDefaultAutoIncludes();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set (possibly by inheriting the setting value
+     * from another {@link ProcessingConfiguration}), or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract List<String> getDefaultAutoIncludes();
+
+    @Override
+    public boolean isAutoIncludesSet() {
+        return autoIncludes != null;
+    }
+    
+    /**
+     * Removes the auto-include from the {@link #getAutoIncludes()} {@link List} (but it doesn't affect the
+     * {@link List}-s inherited from other {@link ProcessingConfiguration}-s). Does nothing if the template is not
+     * in the {@link List}.
+     */
+    public void removeAutoInclude(String templateName) {
+        // "synchronized" is removed from the API as it's not safe to set anything after publishing the Configuration
+        synchronized (this) {
+            if (autoIncludes != null) {
+                autoIncludes.remove(templateName);
+            }
+        }
+    }
+    
+    private static final String ALLOWED_CLASSES = "allowed_classes";
+    private static final String TRUSTED_TEMPLATES = "trusted_templates";
+    
+    /**
+     * Sets a FreeMarker setting by a name and string value. If you can configure FreeMarker directly with Java (or
+     * other programming language), you should use the dedicated setter methods instead (like
+     * {@link #setObjectWrapper(ObjectWrapper)}. This meant to be used only when you get settings from somewhere
+     * as {@link String}-{@link String} name-value pairs (typically, as a {@link Properties} object). Below you find an
+     * overview of the settings available.
+     * 
+     * <p>Note: As of FreeMarker 2.3.23, setting names can be written in camel case too. For example, instead of
+     * {@code date_format} you can also use {@code dateFormat}. It's likely that camel case will become to the
+     * recommended convention in the future.
+     * 
+     * <p>The list of settings commonly supported in all {@link MutableProcessingConfiguration} subclasses:
+     * <ul>
+     *   <li><p>{@code "locale"}:
+     *       See {@link #setLocale(Locale)}.
+     *       <br>String value: local codes with the usual format in Java, such as {@code "en_US"}, or
+     *       "JVM default" (ignoring case) to use the default locale of the Java environment.
+     *
+     *   <li><p>{@code "custom_number_formats"}: See {@link #setCustomNumberFormats(Map)}.
+     *   <br>String value: Interpreted as an <a href="#fm_obe">object builder expression</a>.
+     *   <br>Example: <code>{ "hex": com.example.HexTemplateNumberFormatFactory,
+     *   "gps": com.example.GPSTemplateNumberFormatFactory }</code>
+     *
+     *   <li><p>{@code "custom_date_formats"}: See {@link #setCustomDateFormats(Map)}.
+     *   <br>String value: Interpreted as an <a href="#fm_obe">object builder expression</a>.
+     *   <br>Example: <code>{ "trade": com.example.TradeTemplateDateFormatFactory,
+     *   "log": com.example.LogTemplateDateFormatFactory }</code>
+     *       
+     *   <li><p>{@code "template_exception_handler"}:
+     *       See {@link #setTemplateExceptionHandler(TemplateExceptionHandler)}.
+     *       <br>String value: If the value contains dot, then it's interpreted as an <a href="#fm_obe">object builder
+     *       expression</a>.
+     *       If the value does not contain dot, then it must be one of these predefined values (case insensitive):
+     *       {@code "rethrow"} (means {@link TemplateExceptionHandler#RETHROW_HANDLER}),
+     *       {@code "debug"} (means {@link TemplateExceptionHandler#DEBUG_HANDLER}),
+     *       {@code "html_debug"} (means {@link TemplateExceptionHandler#HTML_DEBUG_HANDLER}),
+     *       {@code "ignore"} (means {@link TemplateExceptionHandler#IGNORE_HANDLER}),
+     *       {@code "default"} (only allowed for {@link Configuration} instances) for the default.
+     *       
+     *   <li><p>{@code "arithmetic_engine"}:
+     *       See {@link #setArithmeticEngine(ArithmeticEngine)}.  
+     *       <br>String value: If the value contains dot, then it's interpreted as an <a href="#fm_obe">object builder
+     *       expression</a>.
+     *       If the value does not contain dot,
+     *       then it must be one of these special values (case insensitive):
+     *       {@code "bigdecimal"}, {@code "conservative"}.
+     *       
+     *   <li><p>{@code "object_wrapper"}:
+     *       See {@link #setObjectWrapper(ObjectWrapper)}.
+     *       <br>String value: If the value contains dot, then it's interpreted as an <a href="#fm_obe">object builder
+     *       expression</a>, with the addition that {@link DefaultObjectWrapper}, {@link DefaultObjectWrapper} and
+     *       {@link RestrictedObjectWrapper} can be referred without package name. For example, these strings are valid
+     *       values: {@code "DefaultObjectWrapper(3.0.0)"},
+     *       {@code "DefaultObjectWrapper(2.3.21, simpleMapWrapper=true)"}.
+     *       <br>If the value does not contain dot, then it must be one of these special values (case insensitive):
+     *       {@code "default"} means the default of {@link Configuration},
+     *       {@code "restricted"} means the a {@link RestrictedObjectWrapper} instance.
+     *
+     *   <li><p>{@code "number_format"}: See {@link #setNumberFormat(String)}.
+     *   
+     *   <li><p>{@code "boolean_format"}: See {@link #setBooleanFormat(String)} .
+     *   
+     *   <li><p>{@code "date_format", "time_format", "datetime_format"}:
+     *       See {@link #setDateFormat(String)}, {@link #setTimeFormat(String)}, {@link #setDateTimeFormat(String)}. 
+     *        
+     *   <li><p>{@code "time_zone"}:
+     *       See {@link #setTimeZone(TimeZone)}.
+     *       <br>String value: With the format as {@link TimeZone#getTimeZone} defines it. Also, since 2.3.21
+     *       {@code "JVM default"} can be used that will be replaced with the actual JVM default time zone when
+     *       {@link #setSetting(String, String)} is called.
+     *       For example {@code "GMT-8:00"} or {@code "America/Los_Angeles"}
+     *       <br>If you set this setting, consider setting {@code sql_date_and_time_time_zone}
+     *       too (see below)! 
+     *       
+     *   <li><p>{@code sql_date_and_time_time_zone}:
+     *       See {@link #setSQLDateAndTimeTimeZone(TimeZone)}.
+     *       Since 2.3.21.
+     *       <br>String value: With the format as {@link TimeZone#getTimeZone} defines it. Also, {@code "JVM default"}
+     *       can be used that will be replaced with the actual JVM default time zone when
+     *       {@link #setSetting(String, String)} is called. Also {@code "null"} can be used, which has the same effect
+     *       as {@link #setSQLDateAndTimeTimeZone(TimeZone) setSQLDateAndTimeTimeZone(null)}.
+     *       
+     *   <li><p>{@code "output_encoding"}:
+     *       See {@link #setOutputEncoding(Charset)}.
+     *       
+     *   <li><p>{@code "url_escaping_charset"}:
+     *       See {@link #setURLEscapingCharset(Charset)}.
+     *       
+     *   <li><p>{@code "auto_flush"}:
+     *       See {@link #setAutoFlush(boolean)}.
+     *       Since 2.3.17.
+     *       <br>String value: {@code "true"}, {@code "false"}, {@code "y"},  etc.
+     *       
+     *   <li><p>{@code "auto_import"}:
+     *       See {@link Configuration#getAutoImports()}
+     *       <br>String value is something like:
+     *       <br>{@code /lib/form.ftl as f, /lib/widget as w, "/lib/odd name.ftl" as odd}
+     *       
+     *   <li><p>{@code "auto_include"}: Sets the list of auto-includes.
+     *       See {@link Configuration#getAutoIncludes()}
+     *       <br>String value is something like:
+     *       <br>{@code /include/common.ftl, "/include/evil name.ftl"}
+     *       
+     *   <li><p>{@code "lazy_auto_imports"}:
+     *       See {@link Configuration#getLazyAutoImports()}.
+     *       <br>String value: {@code "true"}, {@code "false"} (also the equivalents: {@code "yes"}, {@code "no"},
+     *       {@code "t"}, {@code "f"}, {@code "y"}, {@code "n"}), case insensitive. Also can be {@code "null"}.
+
+     *   <li><p>{@code "lazy_imports"}:
+     *       See {@link Configuration#getLazyImports()}.
+     *       <br>String value: {@code "true"}, {@code "false"} (also the equivalents: {@code "yes"}, {@code "no"},
+     *       {@code "t"}, {@code "f"}, {@code "y"}, {@code "n"}), case insensitive.
+     *       
+     *   <li><p>{@code "new_builtin_class_resolver"}:
+     *       See {@link #setNewBuiltinClassResolver(TemplateClassResolver)}.
+     *       Since 2.3.17.
+     *       The value must be one of these (ignore the quotation marks):
+     *       <ol>
+     *         <li><p>{@code "unrestricted"}:
+     *             Use {@link TemplateClassResolver#UNRESTRICTED_RESOLVER}
+     *         <li><p>{@code "allows_nothing"}:
+     *             Use {@link TemplateClassResolver#ALLOWS_NOTHING_RESOLVER}
+     *         <li><p>Something that contains colon will use
+     *             {@link OptInTemplateClassResolver} and is expected to
+     *             store comma separated values (possibly quoted) segmented
+     *             with {@code "allowed_classes:"} and/or
+     *             {@code "trusted_templates:"}. Examples of valid values:
+     *             
+     *             <table style="width: auto; border-collapse: collapse" border="1"
+     *                  summary="trusted_template value examples">
+     *               <tr>
+     *                 <th>Setting value
+     *                 <th>Meaning
+     *               <tr>
+     *                 <td>
+     *                   {@code allowed_classes: com.example.C1, com.example.C2,
+     *                   trusted_templates: lib/*, safe.ftl}                 
+     *                 <td>
+     *                   Only allow instantiating the {@code com.example.C1} and
+     *                   {@code com.example.C2} classes. But, allow templates
+     *                   within the {@code lib/} directory (like
+     *                   {@code lib/foo/bar.ftl}) and template {@code safe.ftl}
+     *                   (that does not match {@code foo/safe.ftl}, only
+     *                   exactly {@code safe.ftl}) to instantiate anything
+     *                   that {@link TemplateClassResolver#UNRESTRICTED_RESOLVER} allows.
+     *               <tr>
+     *                 <td>
+     *                   {@code allowed_classes: com.example.C1, com.example.C2}
+     *                 <td>Only allow instantiating the {@code com.example.C1} and
+     *                   {@code com.example.C2} classes. There are no
+     *                   trusted templates.
+     *               <tr>
+     *                 <td>
+                         {@code trusted_templates: lib/*, safe.ftl}                 
+     *                 <td>
+     *                   Do not allow instantiating any classes, except in
+     *                   templates inside {@code lib/} or in template 
+     *                   {@code safe.ftl}.
+     *             </table>
+     *             
+     *             <p>For more details see {@link OptInTemplateClassResolver}.
+     *             
+     *         <li><p>Otherwise if the value contains dot, it's interpreted as an <a href="#fm_obe">object builder
+     *             expression</a>.
+     *       </ol>
+     *       Note that the {@code safer} option was removed in FreeMarker 3.0.0, as it has become equivalent with
+     *       {@code "unrestricted"}, as the classes it has blocked were removed from FreeMarker.
+     *   <li><p>{@code "show_error_tips"}:
+     *       See {@link #setShowErrorTips(boolean)}.
+     *       Since 2.3.21.
+     *       <br>String value: {@code "true"}, {@code "false"}, {@code "y"},  etc.
+     *       
+     *   <li><p>{@code api_builtin_enabled}:
+     *       See {@link #setAPIBuiltinEnabled(boolean)}.
+     *       Since 2.3.22.
+     *       <br>String value: {@code "true"}, {@code "false"}, {@code "y"},  etc.
+     *       
+     * </ul>
+     * 
+     * <p>{@link Configuration} (a subclass of {@link MutableProcessingConfiguration}) also understands these:</p>
+     * <ul>
+     *   <li><p>{@code "auto_escaping"}:
+     *       See {@link Configuration#getAutoEscapingPolicy()}
+     *       <br>String value: {@code "enable_if_default"} or {@code "enableIfDefault"} for
+     *       {@link ParsingConfiguration#ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY},
+     *       {@code "enable_if_supported"} or {@code "enableIfSupported"} for
+     *       {@link ParsingConfiguration#ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY}
+     *       {@code "disable"} for {@link ParsingConfiguration#DISABLE_AUTO_ESCAPING_POLICY}.
+     *       
+     *   <li><p>{@code "sourceEncoding"}:
+     *       See {@link Configuration#getSourceEncoding()}; since 2.3.26 also accepts value "JVM default"
+     *       (not case sensitive) to set the Java environment default value.
+     *       <br>As the default value is the system default, which can change
+     *       from one server to another, <b>you should always set this!</b>
+     *       
+     *   <li><p>{@code "localized_lookup"}:
+     *       See {@link Configuration#getLocalizedLookup()}.
+     *       <br>String value: {@code "true"}, {@code "false"} (also the equivalents: {@code "yes"}, {@code "no"},
+     *       {@code "t"}, {@code "f"}, {@code "y"}, {@code "n"}).
+     *       ASTDirCase insensitive.
+     *       
+     *   <li><p>{@code "output_format"}:
+     *       See {@link ParsingConfiguration#getOutputFormat()}.
+     *       <br>String value: {@code "default"} (case insensitive) for the default, or an
+     *       <a href="#fm_obe">object builder expression</a> that gives an {@link OutputFormat}, for example
+     *       {@code HTMLOutputFormat} or {@code XMLOutputFormat}.
+     *       
+     *   <li><p>{@code "registered_custom_output_formats"}:
+     *       See {@link Configuration#getRegisteredCustomOutputFormats()}.
+     *       <br>String value: an <a href="#fm_obe">object builder expression</a> that gives a {@link List} of
+     *       {@link OutputFormat}-s.
+     *       Example: {@code [com.example.MyOutputFormat(), com.example.MyOtherOutputFormat()]}
+     *       
+     *   <li><p>{@code "whitespace_stripping"}:
+     *       See {@link ParsingConfiguration#getWhitespaceStripping()}.
+     *       <br>String value: {@code "true"}, {@code "false"}, {@code yes}, etc.
+     *       
+     *   <li><p>{@code "cache_storage"}:
+     *       See {@link Configuration#getCacheStorage()}.
+     *       <br>String value: If the value contains dot, then it's interpreted as an <a href="#fm_obe">object builder
+     *       expression</a>.
+     *       If the value does not contain dot,
+     *       then a {@link org.apache.freemarker.core.templateresolver.impl.MruCacheStorage} will be used with the
+     *       maximum strong and soft sizes specified with the setting value. Examples
+     *       of valid setting values:
+     *       
+     *       <table style="width: auto; border-collapse: collapse" border="1" summary="cache_storage value examples">
+     *         <tr><th>Setting value<th>max. strong size<th>max. soft size
+     *         <tr><td>{@code "strong:50, soft:500"}<td>50<td>500
+     *         <tr><td>{@code "strong:100, soft"}<td>100<td>{@code Integer.MAX_VALUE}
+     *         <tr><td>{@code "strong:100"}<td>100<td>0
+     *         <tr><td>{@code "soft:100"}<td>0<td>100
+     *         <tr><td>{@code "strong"}<td>{@code Integer.MAX_VALUE}<td>0
+     *         <tr><td>{@code "soft"}<td>0<td>{@code Integer.MAX_VALUE}
+     *       </table>
+     *       
+     *       <p>The value is not case sensitive. The order of <tt>soft</tt> and <tt>strong</tt>
+     *       entries is not significant.
+     *       
+     *   <li><p>{@code "template_update_delay"}:
+     *       Template update delay in <b>seconds</b> (not in milliseconds) if no unit is specified; see
+     *       {@link Configuration#getTemplateUpdateDelayMilliseconds()} for more.
+     *       <br>String value: Valid positive integer, optionally followed by a time unit (recommended). The default
+     *       unit is seconds. It's strongly recommended to specify the unit for clarity, like in "500 ms" or "30 s".
+     *       Supported units are: "s" (seconds), "ms" (milliseconds), "m" (minutes), "h" (hours). The whitespace between
+     *       the unit and the number is optional. Units are only supported since 2.3.23.
+     *       
+     *   <li><p>{@code "tag_syntax"}:
+     *       See {@link ParsingConfiguration#getTagSyntax()}.
+     *       <br>String value: Must be one of
+     *       {@code "auto_detect"}, {@code "angle_bracket"}, and {@code "square_bracket"}. 
+     *       
+     *   <li><p>{@code "naming_convention"}:
+     *       See {@link ParsingConfiguration#getNamingConvention()}.
+     *       <br>String value: Must be one of
+     *       {@code "auto_detect"}, {@code "legacy"}, and {@code "camel_case"}.
+     *       
+     *   <li><p>{@code "incompatible_improvements"}:
+     *       See {@link Configuration#getIncompatibleImprovements()}.
+     *       <br>String value: version number like {@code 2.3.20}.
+     *       
+     *   <li><p>{@code "recognize_standard_file_extensions"}:
+     *       See {@link Configuration#getRecognizeStandardFileExtensions()}.
+     *       <br>String value: {@code "default"} (case insensitive) for the default, or {@code "true"}, {@code "false"},
+     *       {@code yes}, etc.
+     *       
+     *   <li><p>{@code "template_configurations"}:
+     *       See: {@link Configuration#getTemplateConfigurations()}.
+     *       <br>String value: Interpreted as an <a href="#fm_obe">object builder expression</a>,
+     *       can be {@code null}.
+     *       
+     *   <li><p>{@code "template_loader"}:
+     *       See: {@link Configuration#getTemplateLoader()}.
+     *       <br>String value: {@code "default"} (case insensitive) for the default, or else interpreted as an
+     *       <a href="#fm_obe">object builder expression</a>. {@code "null"} is also allowed.
+     *       
+     *   <li><p>{@code "template_lookup_strategy"}:
+     *       See: {@link Configuration#getTemplateLookupStrategy()}.
+     *       <br>String value: {@code "default"} (case insensitive) for the default, or else interpreted as an
+     *       <a href="#fm_obe">object builder expression</a>.
+     *       
+     *   <li><p>{@code "template_name_format"}:
+     *       See: {@link Configuration#getTemplateNameFormat()}.
+     *       <br>String value: {@code "default"} (case insensitive) for the default, {@code "default_2_3_0"}
+     *       for {@link DefaultTemplateNameFormatFM2#INSTANCE}, {@code "default_2_4_0"} for
+     *       {@link DefaultTemplateNameFormat#INSTANCE}.
+     * </ul>
+     * 
+     * <p><a name="fm_obe"></a>Regarding <em>object builder expressions</em> (used by the setting values where it was
+     * indicated):
+     * <ul>
+     *   <li><p>Before FreeMarker 2.3.21 it had to be a fully qualified class name, and nothing else.</li>
+     *   <li><p>Since 2.3.21, the generic syntax is:
+     *       <tt><i>className</i>(<i>constrArg1</i>, <i>constrArg2</i>, ... <i>constrArgN</i>,
+     *       <i>propName1</i>=<i>propValue1</i>, <i>propName2</i>=<i>propValue2</i>, ...
+     *       <i>propNameN</i>=<i>propValueN</i>)</tt>,
+     *       where
+     *       <tt><i>className</i></tt> is the fully qualified class name of the instance to invoke (except if we have
+     *       builder class or <tt>INSTANCE</tt> field around, but see that later),
+     *       <tt><i>constrArg</i></tt>-s are the values of constructor arguments,
+     *       and <tt><i>propName</i>=<i>propValue</i></tt>-s set JavaBean properties (like <tt>x=1</tt> means
+     *       <tt>setX(1)</tt>) on the created instance. You can have any number of constructor arguments and property
+     *       setters, including 0. Constructor arguments must precede any property setters.   
+     *   </li>
+     *   <li>
+     *     Example: <tt>com.example.MyObjectWrapper(1, 2, exposeFields=true, cacheSize=5000)</tt> is nearly
+     *     equivalent with this Java code:
+     *     <tt>obj = new com.example.MyObjectWrapper(1, 2); obj.setExposeFields(true); obj.setCacheSize(5000);</tt>
+     *   </li>
+     *   <li>
+     *      <p>If you have no constructor arguments and property setters, and the <tt><i>className</i></tt> class has
+     *      a public static {@code INSTANCE} field, the value of that filed will be the value of the expression, and
+     *      the constructor won't be called.
+     *   </li>
+     *   <li>
+     *      <p>If there exists a class named <tt><i>className</i>Builder</tt>, then that class will be instantiated
+     *      instead with the given constructor arguments, and the JavaBean properties of that builder instance will be
+     *      set. After that, the public <tt>build()</tt> method of the instance will be called, whose return value
+     *      will be the value of the whole expression. (The builder class and the <tt>build()</tt> method is simply
+     *      found by name, there's no special interface to implement.)Note that if you have a builder class, you don't
+     *      actually need a <tt><i>className</i></tt> class (since 2.3.24); after all,
+     *      <tt><i>className</i>Builder.build()</tt> can return any kind of object. 
+     *   </li>
+     *   <li>
+     *      <p>Currently, the values of arguments and properties can only be one of these:
+     *      <ul>
+     *        <li>A numerical literal, like {@code 123} or {@code -1.5}. The value will be automatically converted to
+     *        the type of the target (just like in FTL). However, a target type is only available if the number will
+     *        be a parameter to a method or constructor, not when it's a value (or key) in a {@code List} or
+     *        {@code Map} literal. Thus in the last case the type of number will be like in Java language, like
+     *        {@code 1} is an {@code int}, and {@code 1.0} is a {@code double}, and {@code 1.0f} is a {@code float},
+     *        etc. In all cases, the standard Java type postfixes can be used ("f", "d", "l"), plus "bd" for
+     *        {@code BigDecimal} and "bi" for {@code BigInteger}.</li>
+     *        <li>A boolean literal: {@code true} or {@code false}
+     *        <li>The null literal: {@code null}
+     *        <li>A string literal with FTL syntax, except that  it can't contain <tt>${...}</tt>-s and
+     *            <tt>#{...}</tt>-s. Examples: {@code "Line 1\nLine 2"} or {@code r"C:\temp"}.
+     *        <li>A list literal (since 2.3.24) with FTL-like syntax, for example {@code [ 'foo', 2, true ]}.
+     *            If the parameter is expected to be array, the list will be automatically converted to array.
+     *            The list items can be any kind of expression, like even object builder expressions.
+     *        <li>A map literal (since 2.3.24) with FTL-like syntax, for example <code>{ 'foo': 2, 'bar': true }</code>.
+     *            The keys and values can be any kind of expression, like even object builder expressions.
+     *            The resulting Java object will be a {@link Map} that keeps the item order ({@link LinkedHashMap} as
+     *            of this writing).
+     *        <li>A reference to a public static filed, like {@code Configuration.AUTO_DETECT_TAG_SYNTAX} or
+     *            {@code com.example.MyClass.MY_CONSTANT}.
+     *        <li>An object builder expression. That is, object builder expressions can be nested into each other. 
+     *      </ul>
+     *   </li>
+     *   <li>
+     *     The same kind of expression as for parameters can also be used as top-level expressions (though it's
+     *     rarely useful, apart from using {@code null}).
+     *   </li>
+     *   <li>
+     *     <p>The top-level object builder expressions may omit {@code ()}.
+     *   </li>
+     *   <li>
+     *     <p>The following classes can be referred to with simple (unqualified) name instead of fully qualified name:
+     *     {@link DefaultObjectWrapper}, {@link DefaultObjectWrapper}, {@link RestrictedObjectWrapper}, {@link Locale},
+     *     {@link TemplateConfiguration}, {@link PathGlobMatcher}, {@link FileNameGlobMatcher}, {@link PathRegexMatcher},
+     *     {@link AndMatcher}, {@link OrMatcher}, {@link NotMatcher}, {@link ConditionalTemplateConfigurationFactory},
+     *     {@link MergingTemplateConfigurationFactory}, {@link FirstMatchTemplateConfigurationFactory},
+     *     {@link HTMLOutputFormat}, {@link XMLOutputFormat}, {@link RTFOutputFormat}, {@link PlainTextOutputFormat},
+     *     {@link UndefinedOutputFormat}, {@link Configuration}, {@link TemplateLanguage}.
+     *   </li>
+     *   <li>
+     *     <p>{@link TimeZone} objects can be created like {@code TimeZone("UTC")}, despite that there's no a such
+     *     constructor.
+     *   </li>
+     *   <li>
+     *     <p>{@link Charset} objects can be created like {@code Charset("ISO-8859-5")}, despite that there's no a such
+     *     constructor.
+     *   </li>
+     *   <li>
+     *     <p>The classes and methods that the expression meant to access must be all public.
+     *   </li>
+     * </ul>
+     * 
+     * @param name the name of the setting.
+     * @param value the string that describes the new value of the setting.
+     * 
+     * @throws UnknownConfigurationSettingException if the name is wrong.
+     * @throws ConfigurationSettingValueException if the new value of the setting can't be set for any other reasons.
+     */
+    public void setSetting(String name, String value) throws ConfigurationException {
+        boolean unknown = false;
+        try {
+            if (LOCALE_KEY.equals(name)) {
+                if (JVM_DEFAULT_VALUE.equalsIgnoreCase(value)) {
+                    setLocale(Locale.getDefault());
+                } else {
+                    setLocale(_StringUtil.deduceLocale(value));
+                }
+            } else if (NUMBER_FORMAT_KEY_SNAKE_CASE.equals(name) || NUMBER_FORMAT_KEY_CAMEL_CASE.equals(name)) {
+                setNumberFormat(value);
+            } else if (CUSTOM_NUMBER_FORMATS_KEY_SNAKE_CASE.equals(name)
+                    || CUSTOM_NUMBER_FORMATS_KEY_CAMEL_CASE.equals(name)) {
+                Map map = (Map) _ObjectBuilderSettingEvaluator.eval(
+                                value, Map.class, false, _SettingEvaluationEnvironment.getCurrent());
+                checkSettingValueItemsType("Map keys", String.class, map.keySet());
+                checkSettingValueItemsType("Map values", TemplateNumberFormatFactory.class, map.values());
+                setCustomNumberFormats(map);
+            } else if (TIME_FORMAT_KEY_SNAKE_CASE.equals(name) || TIME_FORMAT_KEY_CAMEL_CASE.equals(name)) {
+                setTimeFormat(value);
+            } else if (DATE_FORMAT_KEY_SNAKE_CASE.equals(name) || DATE_FORMAT_KEY_CAMEL_CASE.equals(name)) {
+                setDateFormat(value);
+            } else if (DATETIME_FORMAT_KEY_SNAKE_CASE.equals(name) || DATETIME_FORMAT_KEY_CAMEL_CASE.equals(name)) {
+                setDateTimeFormat(value);
+            } else if (CUSTOM_DATE_FORMATS_KEY_SNAKE_CASE.equals(name)
+                    || CUSTOM_DATE_FORMATS_KEY_CAMEL_CASE.equals(name)) {
+                Map map = (Map) _ObjectBuilderSettingEvaluator.eval(
+                                value, Map.class, false, _SettingEvaluationEnvironment.getCurrent());
+                checkSettingValueItemsType("Map keys", String.class, map.keySet());
+                checkSettingValueItemsType("Map values", TemplateDateFormatFactory.class, map.values());
+                setCustomDateFormats(map);
+            } else if (TIME_ZONE_KEY_SNAKE_CASE.equals(name) || TIME_ZONE_KEY_CAMEL_CASE.equals(name)) {
+                setTimeZone(parseTimeZoneSettingValue(value));
+            } else if (SQL_DATE_AND_TIME_TIME_ZONE_KEY_SNAKE_CASE.equals(name)
+                    || SQL_DATE_AND_TIME_TIME_ZONE_KEY_CAMEL_CASE.equals(name)) {
+                setSQLDateAndTimeTimeZone(value.equals("null") ? null : parseTimeZoneSettingValue(value));
+            } else if (TEMPLATE_EXCEPTION_HANDLER_KEY_SNAKE_CASE.equals(name)
+                    || TEMPLATE_EXCEPTION_HANDLER_KEY_CAMEL_CASE.equals(name)) {
+                if (value.indexOf('.') == -1) {
+                    if ("debug".equalsIgnoreCase(value)) {
+                        setTemplateExceptionHandler(
+                                TemplateExceptionHandler.DEBUG_HANDLER);
+                    } else if ("html_debug".equalsIgnoreCase(value) || "htmlDebug".equals(value)) {
+                        setTemplateExceptionHandler(
+                                TemplateExceptionHandler.HTML_DEBUG_HANDLER);
+                    } else if ("ignore".equalsIgnoreCase(value)) {
+                        setTemplateExceptionHandler(
+                                TemplateExceptionHandler.IGNORE_HANDLER);
+                    } else if ("rethrow".equalsIgnoreCase(value)) {
+                        setTemplateExceptionHandler(
+                                TemplateExceptionHandler.RETHROW_HANDLER);
+                    } else if (DEFAULT_VALUE.equalsIgnoreCase(value)
+                            && this instanceof Configuration.ExtendableBuilder) {
+                        unsetTemplateExceptionHandler();
+                    } else {
+                        throw new ConfigurationSettingValueException(
+                                name, value,
+                                "No such predefined template exception handler name");
+                    }
+                } else {
+                    setTemplateExceptionHandler((TemplateExceptionHandler) _ObjectBuilderSettingEvaluator.eval(
+                            value, TemplateExceptionHandler.class, false, _SettingEvaluationEnvironment.getCurrent()));
+                }
+            } else if (ARITHMETIC_ENGINE_KEY_SNAKE_CASE.equals(name) || ARITHMETIC_ENGINE_KEY_CAMEL_CASE.equals(name)) {
+                if (value.indexOf('.') == -1) { 
+                    if ("bigdecimal".equalsIgnoreCase(value)) {
+                        setArithmeticEngine(BigDecimalArithmeticEngine.INSTANCE);
+                    } else if ("conservative".equalsIgnoreCase(value)) {
+                        setArithmeticEngine(ConservativeArithmeticEngine.INSTANCE);
+                    } else {
+                        throw new ConfigurationSettingValueException(
+                                name, value, "No such predefined arithmetical engine name");
+                    }
+                } else {
+                    setArithmeticEngine((ArithmeticEngine) _ObjectBuilderSettingEvaluator.eval(
+                            value, ArithmeticEngine.class, false, _SettingEvaluationEnvironment.getCurrent()));
+                }
+            } else if (OBJECT_WRAPPER_KEY_SNAKE_CASE.equals(name) || OBJECT_WRAPPER_KEY_CAMEL_CASE.equals(name)) {
+                if (DEFAULT_VALUE.equalsIgnoreCase(value)) {
+                    if (this instanceof Configuration.Extend

<TRUNCATED>


[46/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirRecurse.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirRecurse.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirRecurse.java
new file mode 100644
index 0000000..b95b3fc
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirRecurse.java
@@ -0,0 +1,130 @@
+/*
+ * 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.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateNodeModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+
+
+/**
+ * AST directive node: {@code #recurse}.
+ */
+final class ASTDirRecurse extends ASTDirective {
+    
+    ASTExpression targetNode, namespaces;
+    
+    ASTDirRecurse(ASTExpression targetNode, ASTExpression namespaces) {
+        this.targetNode = targetNode;
+        this.namespaces = namespaces;
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) throws IOException, TemplateException {
+        TemplateModel node = targetNode == null ? null : targetNode.eval(env);
+        if (node != null && !(node instanceof TemplateNodeModel)) {
+            throw new NonNodeException(targetNode, node, "node", env);
+        }
+        
+        TemplateModel nss = namespaces == null ? null : namespaces.eval(env);
+        if (namespaces instanceof ASTExpStringLiteral) {
+            nss = env.importLib(((TemplateScalarModel) nss).getAsString(), null);
+        } else if (namespaces instanceof ASTExpListLiteral) {
+            nss = ((ASTExpListLiteral) namespaces).evaluateStringsToNamespaces(env);
+        }
+        if (nss != null) {
+            if (nss instanceof TemplateHashModel) {
+                NativeSequence ss = new NativeSequence(1);
+                ss.add(nss);
+                nss = ss;
+            } else if (!(nss instanceof TemplateSequenceModel)) {
+                if (namespaces != null) {
+                    throw new NonSequenceException(namespaces, nss, env);
+                } else {
+                    // Should not occur
+                    throw new _MiscTemplateException(env, "Expecting a sequence of namespaces after \"using\"");
+                }
+            }
+        }
+        
+        env.recurse((TemplateNodeModel) node, (TemplateSequenceModel) nss);
+        return null;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder sb = new StringBuilder();
+        if (canonical) sb.append('<');
+        sb.append(getNodeTypeSymbol());
+        if (targetNode != null) {
+            sb.append(' ');
+            sb.append(targetNode.getCanonicalForm());
+        }
+        if (namespaces != null) {
+            sb.append(" using ");
+            sb.append(namespaces.getCanonicalForm());
+        }
+        if (canonical) sb.append("/>");
+        return sb.toString();
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return "#recurse";
+    }
+
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return targetNode;
+        case 1: return namespaces;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        switch (idx) {
+        case 0: return ParameterRole.NODE;
+        case 1: return ParameterRole.NAMESPACE;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+    @Override
+    boolean isShownInStackTrace() {
+        return true;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirReturn.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirReturn.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirReturn.java
new file mode 100644
index 0000000..0e82a74
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirReturn.java
@@ -0,0 +1,91 @@
+/*
+ * 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;
+
+/**
+ * AST directive node: {@code #return}.
+ */
+final class ASTDirReturn extends ASTDirective {
+
+    private ASTExpression exp;
+
+    ASTDirReturn(ASTExpression exp) {
+        this.exp = exp;
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) throws TemplateException {
+        if (exp != null) {
+            env.setLastReturnValue(exp.eval(env));
+        }
+        if (nextSibling() == null && getParent() instanceof ASTDirMacro) {
+            // Avoid unnecessary exception throwing 
+            return null;
+        }
+        throw Return.INSTANCE;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder sb = new StringBuilder();
+        if (canonical) sb.append('<');
+        sb.append(getNodeTypeSymbol());
+        if (exp != null) {
+            sb.append(' ');
+            sb.append(exp.getCanonicalForm());
+        }
+        if (canonical) sb.append("/>");
+        return sb.toString();
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return "#return";
+    }
+    
+    public static class Return extends RuntimeException {
+        static final Return INSTANCE = new Return();
+        private Return() {
+        }
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 1;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return exp;
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return ParameterRole.VALUE;
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSep.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSep.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSep.java
new file mode 100644
index 0000000..9e83e83
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSep.java
@@ -0,0 +1,89 @@
+/*
+ * 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.core.ASTDirList.IterationContext;
+
+/**
+ * AST directive node: {@code #sep}.
+ */
+class ASTDirSep extends ASTDirective {
+
+    public ASTDirSep(TemplateElements children) {
+        setChildren(children);
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) throws TemplateException, IOException {
+        final IterationContext iterCtx = ASTDirList.findEnclosingIterationContext(env, null);
+        if (iterCtx == null) {
+            // The parser should prevent this situation
+            throw new _MiscTemplateException(env,
+                    getNodeTypeSymbol(), " without iteration in context");
+        }
+        
+        if (iterCtx.hasNext()) {
+            return getChildBuffer();
+        }
+        return null;
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder sb = new StringBuilder();
+        if (canonical) sb.append('<');
+        sb.append(getNodeTypeSymbol());
+        if (canonical) {
+            sb.append('>');
+            sb.append(getChildrenCanonicalForm());
+            sb.append("</");
+            sb.append(getNodeTypeSymbol());
+            sb.append('>');
+        }
+        return sb.toString();
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return "#sep";
+    }
+
+    @Override
+    int getParameterCount() {
+        return 0;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSetting.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSetting.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSetting.java
new file mode 100644
index 0000000..68a0672
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSetting.java
@@ -0,0 +1,172 @@
+/*
+ * 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.util.Arrays;
+
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * AST directive node: {@code #setting}.
+ */
+final class ASTDirSetting extends ASTDirective {
+
+    private final String key;
+    private final ASTExpression value;
+    
+    static final String[] SETTING_NAMES = new String[] {
+            // Must be sorted alphabetically!
+            MutableProcessingConfiguration.BOOLEAN_FORMAT_KEY_CAMEL_CASE,
+            MutableProcessingConfiguration.BOOLEAN_FORMAT_KEY_SNAKE_CASE,
+            MutableProcessingConfiguration.DATE_FORMAT_KEY_CAMEL_CASE,
+            MutableProcessingConfiguration.DATE_FORMAT_KEY_SNAKE_CASE,
+            MutableProcessingConfiguration.DATETIME_FORMAT_KEY_CAMEL_CASE,
+            MutableProcessingConfiguration.DATETIME_FORMAT_KEY_SNAKE_CASE,
+            MutableProcessingConfiguration.LOCALE_KEY,
+            MutableProcessingConfiguration.NUMBER_FORMAT_KEY_CAMEL_CASE,
+            MutableProcessingConfiguration.NUMBER_FORMAT_KEY_SNAKE_CASE,
+            MutableProcessingConfiguration.OUTPUT_ENCODING_KEY_CAMEL_CASE,
+            MutableProcessingConfiguration.OUTPUT_ENCODING_KEY_SNAKE_CASE,
+            MutableProcessingConfiguration.SQL_DATE_AND_TIME_TIME_ZONE_KEY_CAMEL_CASE,
+            MutableProcessingConfiguration.SQL_DATE_AND_TIME_TIME_ZONE_KEY,
+            MutableProcessingConfiguration.TIME_FORMAT_KEY_CAMEL_CASE,
+            MutableProcessingConfiguration.TIME_ZONE_KEY_CAMEL_CASE,
+            MutableProcessingConfiguration.TIME_FORMAT_KEY_SNAKE_CASE,
+            MutableProcessingConfiguration.TIME_ZONE_KEY_SNAKE_CASE,
+            MutableProcessingConfiguration.URL_ESCAPING_CHARSET_KEY_CAMEL_CASE,
+            MutableProcessingConfiguration.URL_ESCAPING_CHARSET_KEY_SNAKE_CASE
+    };
+
+    ASTDirSetting(Token keyTk, FMParserTokenManager tokenManager, ASTExpression value, Configuration cfg)
+            throws ParseException {
+        String key = keyTk.image;
+        if (Arrays.binarySearch(SETTING_NAMES, key) < 0) {
+            StringBuilder sb = new StringBuilder();
+            if (Configuration.ExtendableBuilder.getSettingNames(true).contains(key)
+                    || Configuration.ExtendableBuilder.getSettingNames(false).contains(key)) {
+                sb.append("The setting name is recognized, but changing this setting from inside a template isn't "
+                        + "supported.");                
+            } else {
+                sb.append("Unknown setting name: ");
+                sb.append(_StringUtil.jQuote(key)).append(".");
+                sb.append(" The allowed setting names are: ");
+
+                int shownNamingConvention;
+                {
+                    int namingConvention = tokenManager.namingConvention;
+                    shownNamingConvention = namingConvention != ParsingConfiguration.AUTO_DETECT_NAMING_CONVENTION
+                            ? namingConvention : ParsingConfiguration.LEGACY_NAMING_CONVENTION /* [2.4] CAMEL_CASE */;
+                }
+                
+                boolean first = true;
+                for (String correctName : SETTING_NAMES) {
+                    int correctNameNamingConvention = _StringUtil.getIdentifierNamingConvention(correctName);
+                    if (shownNamingConvention == ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION
+                            ? correctNameNamingConvention != ParsingConfiguration.LEGACY_NAMING_CONVENTION
+                            : correctNameNamingConvention != ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION) {
+                        if (first) {
+                            first = false;
+                        } else {
+                            sb.append(", ");
+                        }
+
+                        sb.append(correctName);
+                    }
+                }
+            }
+            throw new ParseException(sb.toString(), null, keyTk);
+        }
+        
+        this.key = key;
+        this.value = value;
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) throws TemplateException {
+        TemplateModel mval = value.eval(env);
+        String strval;
+        if (mval instanceof TemplateScalarModel) {
+            strval = ((TemplateScalarModel) mval).getAsString();
+        } else if (mval instanceof TemplateBooleanModel) {
+            strval = ((TemplateBooleanModel) mval).getAsBoolean() ? "true" : "false";
+        } else if (mval instanceof TemplateNumberModel) {
+            strval = ((TemplateNumberModel) mval).getAsNumber().toString();
+        } else {
+            strval = value.evalAndCoerceToStringOrUnsupportedMarkup(env);
+        }
+        try {
+            env.setSetting(key, strval);
+        } catch (ConfigurationException e) {
+            throw new _MiscTemplateException(env, e.getMessage(), e.getCause());
+        }
+        return null;
+    }
+    
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder sb = new StringBuilder();
+        if (canonical) sb.append('<');
+        sb.append(getNodeTypeSymbol());
+        sb.append(' ');
+        sb.append(_StringUtil.toFTLTopLevelTragetIdentifier(key));
+        sb.append('=');
+        sb.append(value.getCanonicalForm());
+        if (canonical) sb.append("/>");
+        return sb.toString();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "#setting";
+    }
+
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return key;
+        case 1: return value;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        switch (idx) {
+        case 0: return ParameterRole.ITEM_KEY;
+        case 1: return ParameterRole.ITEM_VALUE;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirStop.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirStop.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirStop.java
new file mode 100644
index 0000000..f453734
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirStop.java
@@ -0,0 +1,81 @@
+/*
+ * 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;
+
+/**
+ * AST directive node: {@code #stop}.
+ */
+final class ASTDirStop extends ASTDirective {
+
+    private ASTExpression exp;
+
+    ASTDirStop(ASTExpression exp) {
+        this.exp = exp;
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) throws TemplateException {
+        if (exp == null) {
+            throw new StopException(env);
+        }
+        throw new StopException(env, exp.evalAndCoerceToPlainText(env));
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder sb = new StringBuilder();
+        if (canonical) sb.append('<');
+        sb.append(getNodeTypeSymbol());
+        if (exp != null) {
+            sb.append(' ');
+            sb.append(exp.getCanonicalForm());
+        }
+        if (canonical) sb.append("/>");
+        return sb.toString();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "#stop";
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 1;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return exp;
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return ParameterRole.MESSAGE;
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSwitch.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSwitch.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSwitch.java
new file mode 100644
index 0000000..e66c419
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSwitch.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import java.io.IOException;
+
+/**
+ * AST directive node: {@code #switch}.
+ */
+final class ASTDirSwitch extends ASTDirective {
+
+    private ASTDirCase defaultCase;
+    private final ASTExpression searched;
+
+    /**
+     * @param searched the expression to be tested.
+     */
+    ASTDirSwitch(ASTExpression searched) {
+        this.searched = searched;
+        setChildBufferCapacity(4);
+    }
+
+    void addCase(ASTDirCase cas) {
+        if (cas.condition == null) {
+            defaultCase = cas;
+        }
+        addChild(cas);
+    }
+
+    @Override
+    ASTElement[] accept(Environment env)
+        throws TemplateException, IOException {
+        boolean processedCase = false;
+        int ln = getChildCount();
+        try {
+            for (int i = 0; i < ln; i++) {
+                ASTDirCase cas = (ASTDirCase) getChild(i);
+                boolean processCase = false;
+
+                // Fall through if a previous case tested true.
+                if (processedCase) {
+                    processCase = true;
+                } else if (cas.condition != null) {
+                    // Otherwise, if this case isn't the default, test it.
+                    processCase = _EvalUtil.compare(
+                            searched,
+                            _EvalUtil.CMP_OP_EQUALS, "case==", cas.condition, cas.condition, env);
+                }
+                if (processCase) {
+                    env.visit(cas);
+                    processedCase = true;
+                }
+            }
+
+            // If we didn't process any nestedElements, and we have a default,
+            // process it.
+            if (!processedCase && defaultCase != null) {
+                env.visit(defaultCase);
+            }
+        } catch (ASTDirBreak.Break br) {
+            // #break was called
+        }
+        return null;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder buf = new StringBuilder();
+        if (canonical) buf.append('<');
+        buf.append(getNodeTypeSymbol());
+        buf.append(' ');
+        buf.append(searched.getCanonicalForm());
+        if (canonical) {
+            buf.append('>');
+            int ln = getChildCount();
+            for (int i = 0; i < ln; i++) {
+                ASTDirCase cas = (ASTDirCase) getChild(i);
+                buf.append(cas.getCanonicalForm());
+            }
+            buf.append("</").append(getNodeTypeSymbol()).append('>');
+        }
+        return buf.toString();
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return "#switch";
+    }
+
+    @Override
+    int getParameterCount() {
+        return 1;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return searched;
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return ParameterRole.VALUE;
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirTOrTrOrTl.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirTOrTrOrTl.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirTOrTrOrTl.java
new file mode 100644
index 0000000..937bc18
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirTOrTrOrTl.java
@@ -0,0 +1,109 @@
+/*
+ * 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;
+
+/**
+ * AST directive node: {@code #t}, {@code #tr}, {@code #tl}.
+ */
+final class ASTDirTOrTrOrTl extends ASTDirective {
+    
+    private static final int TYPE_T = 0;
+    private static final int TYPE_LT = 1;
+    private static final int TYPE_RT = 2;
+    private static final int TYPE_NT = 3;
+
+    final boolean left, right;
+
+    ASTDirTOrTrOrTl(boolean left, boolean right) {
+        this.left = left;
+        this.right = right;
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) {
+        // This instruction does nothing at render-time, only parse-time.
+        return null;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder sb = new StringBuilder();
+        if (canonical) sb.append('<');
+        sb.append(getNodeTypeSymbol());
+        if (canonical) sb.append("/>");
+        return sb.toString();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        if (left && right) {
+            return "#t";
+        } else if (left) {
+            return "#lt";
+        } else if (right) {
+            return "#rt";
+        } else {
+            return "#nt";
+        }
+    }
+    
+    @Override
+    boolean isIgnorable(boolean stripWhitespace) {
+        return true;
+    }
+
+    @Override
+    int getParameterCount() {
+        return 1;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        int type;
+        if (left && right) {
+            type = TYPE_T;
+        } else if (left) {
+            type = TYPE_LT;
+        } else if (right) {
+            type = TYPE_RT;
+        } else {
+            type = TYPE_NT;
+        }
+        return Integer.valueOf(type);
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return ParameterRole.AST_NODE_SUBTYPE;
+    }
+
+    @Override
+    boolean isOutputCacheable() {
+        return true;
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirUserDefined.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirUserDefined.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirUserDefined.java
new file mode 100644
index 0000000..6042bd8
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirUserDefined.java
@@ -0,0 +1,343 @@
+/*
+ * 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.lang.ref.Reference;
+import java.lang.ref.SoftReference;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.freemarker.core.model.TemplateDirectiveModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateTransformModel;
+import org.apache.freemarker.core.util.ObjectFactory;
+import org.apache.freemarker.core.util._StringUtil;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+/**
+ * AST directive node: {@code <@exp .../>} or {@code <@exp ...>...</...@...>}. Calls an user-defined directive (like a
+ * macro).
+ */
+final class ASTDirUserDefined extends ASTDirective implements DirectiveCallPlace {
+
+    private ASTExpression nameExp;
+    private Map namedArgs;
+    private List positionalArgs, bodyParameterNames;
+    private transient volatile SoftReference/*List<Map.Entry<String,ASTExpression>>*/ sortedNamedArgsCache;
+    private CustomDataHolder customDataHolder;
+
+    ASTDirUserDefined(ASTExpression nameExp,
+         Map namedArgs,
+         TemplateElements children,
+         List bodyParameterNames) {
+        this.nameExp = nameExp;
+        this.namedArgs = namedArgs;
+        setChildren(children);
+        this.bodyParameterNames = bodyParameterNames;
+    }
+
+    ASTDirUserDefined(ASTExpression nameExp,
+         List positionalArgs,
+         TemplateElements children,
+         List bodyParameterNames) {
+        this.nameExp = nameExp;
+        this.positionalArgs = positionalArgs;
+        setChildren(children);
+        this.bodyParameterNames = bodyParameterNames;
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) throws TemplateException, IOException {
+        TemplateModel tm = nameExp.eval(env);
+        if (tm == ASTDirMacro.DO_NOTHING_MACRO) return null; // shortcut here.
+        if (tm instanceof ASTDirMacro) {
+            ASTDirMacro macro = (ASTDirMacro) tm;
+            if (macro.isFunction()) {
+                throw new _MiscTemplateException(env,
+                        "Routine ", new _DelayedJQuote(macro.getName()), " is a function, not a directive. "
+                        + "Functions can only be called from expressions, like in ${f()}, ${x + f()} or ",
+                        "<@someDirective someParam=f() />", ".");
+            }    
+            env.invoke(macro, namedArgs, positionalArgs, bodyParameterNames, getChildBuffer());
+        } else {
+            boolean isDirectiveModel = tm instanceof TemplateDirectiveModel; 
+            if (isDirectiveModel || tm instanceof TemplateTransformModel) {
+                Map args;
+                if (namedArgs != null && !namedArgs.isEmpty()) {
+                    args = new HashMap();
+                    for (Iterator it = namedArgs.entrySet().iterator(); it.hasNext(); ) {
+                        Map.Entry entry = (Map.Entry) it.next();
+                        String key = (String) entry.getKey();
+                        ASTExpression valueExp = (ASTExpression) entry.getValue();
+                        TemplateModel value = valueExp.eval(env);
+                        args.put(key, value);
+                    }
+                } else {
+                    args = Collections.emptyMap();
+                }
+                if (isDirectiveModel) {
+                    env.visit(getChildBuffer(), (TemplateDirectiveModel) tm, args, bodyParameterNames);
+                } else { 
+                    env.visitAndTransform(getChildBuffer(), (TemplateTransformModel) tm, args);
+                }
+            } else if (tm == null) {
+                throw InvalidReferenceException.getInstance(nameExp, env);
+            } else {
+                throw new NonUserDefinedDirectiveLikeException(nameExp, tm, env);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder sb = new StringBuilder();
+        if (canonical) sb.append('<');
+        sb.append('@');
+        MessageUtil.appendExpressionAsUntearable(sb, nameExp);
+        boolean nameIsInParen = sb.charAt(sb.length() - 1) == ')';
+        if (positionalArgs != null) {
+            for (int i = 0; i < positionalArgs.size(); i++) {
+                ASTExpression argExp = (ASTExpression) positionalArgs.get(i);
+                if (i != 0) {
+                    sb.append(',');
+                }
+                sb.append(' ');
+                sb.append(argExp.getCanonicalForm());
+            }
+        } else {
+            List entries = getSortedNamedArgs();
+            for (int i = 0; i < entries.size(); i++) {
+                Map.Entry entry = (Map.Entry) entries.get(i);
+                ASTExpression argExp = (ASTExpression) entry.getValue();
+                sb.append(' ');
+                sb.append(_StringUtil.toFTLTopLevelIdentifierReference((String) entry.getKey()));
+                sb.append('=');
+                MessageUtil.appendExpressionAsUntearable(sb, argExp);
+            }
+        }
+        if (bodyParameterNames != null && !bodyParameterNames.isEmpty()) {
+            sb.append("; ");
+            for (int i = 0; i < bodyParameterNames.size(); i++) {
+                if (i != 0) {
+                    sb.append(", ");
+                }
+                sb.append(_StringUtil.toFTLTopLevelIdentifierReference((String) bodyParameterNames.get(i)));
+            }
+        }
+        if (canonical) {
+            if (getChildCount() == 0) {
+                sb.append("/>");
+            } else {
+                sb.append('>');
+                sb.append(getChildrenCanonicalForm());
+                sb.append("</@");
+                if (!nameIsInParen
+                        && (nameExp instanceof ASTExpVariable
+                            || (nameExp instanceof ASTExpDot && ((ASTExpDot) nameExp).onlyHasIdentifiers()))) {
+                    sb.append(nameExp.getCanonicalForm());
+                }
+                sb.append('>');
+            }
+        }
+        return sb.toString();
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return "@";
+    }
+
+    @Override
+    int getParameterCount() {
+        return 1/*nameExp*/
+                + (positionalArgs != null ? positionalArgs.size() : 0)
+                + (namedArgs != null ? namedArgs.size() * 2 : 0)
+                + (bodyParameterNames != null ? bodyParameterNames.size() : 0);
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        if (idx == 0) {
+            return nameExp;
+        } else {
+            int base = 1;
+            final int positionalArgsSize = positionalArgs != null ? positionalArgs.size() : 0;  
+            if (idx - base < positionalArgsSize) {
+                return positionalArgs.get(idx - base);
+            } else {
+                base += positionalArgsSize;
+                final int namedArgsSize = namedArgs != null ? namedArgs.size() : 0;
+                if (idx - base < namedArgsSize * 2) {
+                    Map.Entry namedArg = (Map.Entry) getSortedNamedArgs().get((idx - base) / 2);
+                    return (idx - base) % 2 == 0 ? namedArg.getKey() : namedArg.getValue();
+                } else {
+                    base += namedArgsSize * 2;
+                    final int bodyParameterNamesSize = bodyParameterNames != null ? bodyParameterNames.size() : 0;
+                    if (idx - base < bodyParameterNamesSize) {
+                        return bodyParameterNames.get(idx - base);
+                    } else {
+                        throw new IndexOutOfBoundsException();
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        if (idx == 0) {
+            return ParameterRole.CALLEE;
+        } else {
+            int base = 1;
+            final int positionalArgsSize = positionalArgs != null ? positionalArgs.size() : 0;  
+            if (idx - base < positionalArgsSize) {
+                return ParameterRole.ARGUMENT_VALUE;
+            } else {
+                base += positionalArgsSize;
+                final int namedArgsSize = namedArgs != null ? namedArgs.size() : 0;
+                if (idx - base < namedArgsSize * 2) {
+                    return (idx - base) % 2 == 0 ? ParameterRole.ARGUMENT_NAME : ParameterRole.ARGUMENT_VALUE;
+                } else {
+                    base += namedArgsSize * 2;
+                    final int bodyParameterNamesSize = bodyParameterNames != null ? bodyParameterNames.size() : 0;
+                    if (idx - base < bodyParameterNamesSize) {
+                        return ParameterRole.TARGET_LOOP_VARIABLE;
+                    } else {
+                        throw new IndexOutOfBoundsException();
+                    }
+                }
+            }
+        }
+    }
+    
+    /**
+     * Returns the named args by source-code order; it's not meant to be used during template execution, too slow for
+     * that!
+     */
+    private List/*<Map.Entry<String, ASTExpression>>*/ getSortedNamedArgs() {
+        Reference ref = sortedNamedArgsCache;
+        if (ref != null) {
+            List res = (List) ref.get();
+            if (res != null) return res;
+        }
+        
+        List res = MiscUtil.sortMapOfExpressions(namedArgs);
+        sortedNamedArgsCache = new SoftReference(res);
+        return res;
+    }
+
+    @Override
+    @SuppressFBWarnings(value={ "IS2_INCONSISTENT_SYNC", "DC_DOUBLECHECK" }, justification="Performance tricks")
+    public Object getOrCreateCustomData(Object providerIdentity, ObjectFactory objectFactory)
+            throws CallPlaceCustomDataInitializationException {
+        // We are using double-checked locking, utilizing Java memory model "final" trick.
+        // Note that this.customDataHolder is NOT volatile.
+        
+        CustomDataHolder customDataHolder = this.customDataHolder;  // Findbugs false alarm
+        if (customDataHolder == null) {  // Findbugs false alarm
+            synchronized (this) {
+                customDataHolder = this.customDataHolder;
+                if (customDataHolder == null || customDataHolder.providerIdentity != providerIdentity) {
+                    customDataHolder = createNewCustomData(providerIdentity, objectFactory);
+                    this.customDataHolder = customDataHolder; 
+                }
+            }
+        }
+        
+        if (customDataHolder.providerIdentity != providerIdentity) {
+            synchronized (this) {
+                customDataHolder = this.customDataHolder;
+                if (customDataHolder == null || customDataHolder.providerIdentity != providerIdentity) {
+                    customDataHolder = createNewCustomData(providerIdentity, objectFactory);
+                    this.customDataHolder = customDataHolder;
+                }
+            }
+        }
+        
+        return customDataHolder.customData;
+    }
+
+    private CustomDataHolder createNewCustomData(Object provierIdentity, ObjectFactory objectFactory)
+            throws CallPlaceCustomDataInitializationException {
+        CustomDataHolder customDataHolder;
+        Object customData;
+        try {
+            customData = objectFactory.createObject();
+        } catch (Exception e) {
+            throw new CallPlaceCustomDataInitializationException(
+                    "Failed to initialize custom data for provider identity "
+                    + _StringUtil.tryToString(provierIdentity) + " via factory "
+                    + _StringUtil.tryToString(objectFactory), e);
+        }
+        if (customData == null) {
+            throw new NullPointerException("ObjectFactory.createObject() has returned null");
+        }
+        customDataHolder = new CustomDataHolder(provierIdentity, customData);
+        return customDataHolder;
+    }
+
+    @Override
+    public boolean isNestedOutputCacheable() {
+        return isChildrenOutputCacheable();
+    }
+    
+/*
+    //REVISIT
+    boolean heedsOpeningWhitespace() {
+        return nestedBlock == null;
+    }
+
+    //REVISIT
+    boolean heedsTrailingWhitespace() {
+        return nestedBlock == null;
+    }*/
+    
+    /**
+     * Used for implementing double check locking in implementing the
+     * {@link DirectiveCallPlace#getOrCreateCustomData(Object, ObjectFactory)}.
+     */
+    private static class CustomDataHolder {
+        
+        private final Object providerIdentity;
+        private final Object customData;
+        public CustomDataHolder(Object providerIdentity, Object customData) {
+            this.providerIdentity = providerIdentity;
+            this.customData = customData;
+        }
+        
+    }
+    
+    @Override
+    boolean isNestedBlockRepeater() {
+        return true;
+    }
+
+    @Override
+    boolean isShownInStackTrace() {
+        return true;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirVisit.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirVisit.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirVisit.java
new file mode 100644
index 0000000..4a4023b
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirVisit.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;
+
+import java.io.IOException;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateNodeModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+
+
+/**
+ * AST directive node: {@code #visit}.
+ */
+final class ASTDirVisit extends ASTDirective {
+    
+    ASTExpression targetNode, namespaces;
+    
+    ASTDirVisit(ASTExpression targetNode, ASTExpression namespaces) {
+        this.targetNode = targetNode;
+        this.namespaces = namespaces;
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) throws IOException, TemplateException {
+        TemplateModel node = targetNode.eval(env);
+        if (!(node instanceof TemplateNodeModel)) {
+            throw new NonNodeException(targetNode, node, env);
+        }
+        
+        TemplateModel nss = namespaces == null ? null : namespaces.eval(env);
+        if (namespaces instanceof ASTExpStringLiteral) {
+            nss = env.importLib(((TemplateScalarModel) nss).getAsString(), null);
+        } else if (namespaces instanceof ASTExpListLiteral) {
+            nss = ((ASTExpListLiteral) namespaces).evaluateStringsToNamespaces(env);
+        }
+        if (nss != null) {
+            if (nss instanceof Environment.Namespace) {
+                NativeSequence ss = new NativeSequence(1);
+                ss.add(nss);
+                nss = ss;
+            } else if (!(nss instanceof TemplateSequenceModel)) {
+                if (namespaces != null) {
+                    throw new NonSequenceException(namespaces, nss, env);
+                } else {
+                    // Should not occur
+                    throw new _MiscTemplateException(env, "Expecting a sequence of namespaces after \"using\"");
+                }
+            }
+        }
+        env.invokeNodeHandlerFor((TemplateNodeModel) node, (TemplateSequenceModel) nss);
+        return null;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder sb = new StringBuilder();
+        if (canonical) sb.append('<');
+        sb.append(getNodeTypeSymbol());
+        sb.append(' ');
+        sb.append(targetNode.getCanonicalForm());
+        if (namespaces != null) {
+            sb.append(" using ");
+            sb.append(namespaces.getCanonicalForm());
+        }
+        if (canonical) sb.append("/>");
+        return sb.toString();
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return "#visit";
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return targetNode;
+        case 1: return namespaces;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        switch (idx) {
+        case 0: return ParameterRole.NODE;
+        case 1: return ParameterRole.NAMESPACE;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return true;
+    }
+
+    @Override
+    boolean isShownInStackTrace() {
+        return true;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirective.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirective.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirective.java
new file mode 100644
index 0000000..778fed1
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirective.java
@@ -0,0 +1,98 @@
+/*
+ * 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.util.Collections;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * AST directive node superclass.
+ */
+abstract class ASTDirective extends ASTElement {
+
+    private static void addName(Set<String> allNames, Set<String> lcNames, Set<String> ccNames,
+                                String commonName) {
+        allNames.add(commonName);
+        lcNames.add(commonName);
+        ccNames.add(commonName);
+    }
+
+    private static void addName(Set<String> allNames, Set<String> lcNames, Set<String> ccNames,
+                                String lcName, String ccName) {
+        allNames.add(lcName);
+        allNames.add(ccName);
+        lcNames.add(lcName);
+        ccNames.add(ccName);
+    }
+
+    static final Set<String> ALL_BUILT_IN_DIRECTIVE_NAMES;
+    static final Set<String> LEGACY_BUILT_IN_DIRECTIVE_NAMES;
+    static final Set<String> CAMEL_CASE_BUILT_IN_DIRECTIVE_NAMES;
+    static {
+        Set<String> allNames = new TreeSet();
+        Set<String> lcNames = new TreeSet();
+        Set<String> ccNames = new TreeSet();
+
+        addName(allNames, lcNames, ccNames, "assign");
+        addName(allNames, lcNames, ccNames, "attempt");
+        addName(allNames, lcNames, ccNames, "autoesc", "autoEsc");
+        addName(allNames, lcNames, ccNames, "break");
+        addName(allNames, lcNames, ccNames, "case");
+        addName(allNames, lcNames, ccNames, "compress");
+        addName(allNames, lcNames, ccNames, "default");
+        addName(allNames, lcNames, ccNames, "else");
+        addName(allNames, lcNames, ccNames, "elseif", "elseIf");
+        addName(allNames, lcNames, ccNames, "escape");
+        addName(allNames, lcNames, ccNames, "fallback");
+        addName(allNames, lcNames, ccNames, "flush");
+        addName(allNames, lcNames, ccNames, "ftl");
+        addName(allNames, lcNames, ccNames, "function");
+        addName(allNames, lcNames, ccNames, "global");
+        addName(allNames, lcNames, ccNames, "if");
+        addName(allNames, lcNames, ccNames, "import");
+        addName(allNames, lcNames, ccNames, "include");
+        addName(allNames, lcNames, ccNames, "items");
+        addName(allNames, lcNames, ccNames, "list");
+        addName(allNames, lcNames, ccNames, "local");
+        addName(allNames, lcNames, ccNames, "lt");
+        addName(allNames, lcNames, ccNames, "macro");
+        addName(allNames, lcNames, ccNames, "nested");
+        addName(allNames, lcNames, ccNames, "noautoesc", "noAutoEsc");
+        addName(allNames, lcNames, ccNames, "noescape", "noEscape");
+        addName(allNames, lcNames, ccNames, "noparse", "noParse");
+        addName(allNames, lcNames, ccNames, "nt");
+        addName(allNames, lcNames, ccNames, "outputformat", "outputFormat");
+        addName(allNames, lcNames, ccNames, "recover");
+        addName(allNames, lcNames, ccNames, "recurse");
+        addName(allNames, lcNames, ccNames, "return");
+        addName(allNames, lcNames, ccNames, "rt");
+        addName(allNames, lcNames, ccNames, "sep");
+        addName(allNames, lcNames, ccNames, "setting");
+        addName(allNames, lcNames, ccNames, "stop");
+        addName(allNames, lcNames, ccNames, "switch");
+        addName(allNames, lcNames, ccNames, "t");
+        addName(allNames, lcNames, ccNames, "visit");
+
+        ALL_BUILT_IN_DIRECTIVE_NAMES = Collections.unmodifiableSet(allNames);
+        LEGACY_BUILT_IN_DIRECTIVE_NAMES = Collections.unmodifiableSet(lcNames);
+        CAMEL_CASE_BUILT_IN_DIRECTIVE_NAMES = Collections.unmodifiableSet(ccNames);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDollarInterpolation.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDollarInterpolation.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDollarInterpolation.java
new file mode 100644
index 0000000..1e5a7b4
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDollarInterpolation.java
@@ -0,0 +1,151 @@
+/*
+ * 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.Writer;
+
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.outputformat.MarkupOutputFormat;
+import org.apache.freemarker.core.outputformat.OutputFormat;
+import org.apache.freemarker.core.util.FTLUtil;
+
+/**
+ * AST interpolation node: <tt>${exp}</tt>
+ */
+final class ASTDollarInterpolation extends ASTInterpolation {
+
+    private final ASTExpression expression;
+    
+    /** For {@code #escape x as ...} (legacy auto-escaping) */
+    private final ASTExpression escapedExpression;
+    
+    /** For OutputFormat-based auto-escaping */
+    private final OutputFormat outputFormat;
+    private final MarkupOutputFormat markupOutputFormat;
+    private final boolean autoEscape;
+
+    ASTDollarInterpolation(
+            ASTExpression expression, ASTExpression escapedExpression,
+            OutputFormat outputFormat, boolean autoEscape) {
+        this.expression = expression;
+        this.escapedExpression = escapedExpression;
+        this.outputFormat = outputFormat;
+        markupOutputFormat
+                = (MarkupOutputFormat) (outputFormat instanceof MarkupOutputFormat ? outputFormat : null);
+        this.autoEscape = autoEscape;
+    }
+
+    /**
+     * Outputs the string value of the enclosed expression.
+     */
+    @Override
+    ASTElement[] accept(Environment env) throws TemplateException, IOException {
+        final Object moOrStr = calculateInterpolatedStringOrMarkup(env);
+        final Writer out = env.getOut();
+        if (moOrStr instanceof String) {
+            final String s = (String) moOrStr;
+            if (autoEscape) {
+                markupOutputFormat.output(s, out);
+            } else {
+                out.write(s);
+            }
+        } else {
+            final TemplateMarkupOutputModel mo = (TemplateMarkupOutputModel) moOrStr;
+            final MarkupOutputFormat moOF = mo.getOutputFormat();
+            // ATTENTION: Keep this logic in sync. ?esc/?noEsc's logic!
+            if (moOF != outputFormat && !outputFormat.isOutputFormatMixingAllowed()) {
+                final String srcPlainText;
+                // ATTENTION: Keep this logic in sync. ?esc/?noEsc's logic!
+                srcPlainText = moOF.getSourcePlainText(mo);
+                if (srcPlainText == null) {
+                    throw new _TemplateModelException(escapedExpression,
+                            "The value to print is in ", new _DelayedToString(moOF),
+                            " format, which differs from the current output format, ",
+                            new _DelayedToString(outputFormat), ". Format conversion wasn't possible.");
+                }
+                if (outputFormat instanceof MarkupOutputFormat) {
+                    ((MarkupOutputFormat) outputFormat).output(srcPlainText, out);
+                } else {
+                    out.write(srcPlainText);
+                }
+            } else {
+                moOF.output(mo, out);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    protected Object calculateInterpolatedStringOrMarkup(Environment env) throws TemplateException {
+        return _EvalUtil.coerceModelToStringOrMarkup(escapedExpression.eval(env), escapedExpression, null, env);
+    }
+
+    @Override
+    protected String dump(boolean canonical, boolean inStringLiteral) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("${");
+        final String exprCF = expression.getCanonicalForm();
+        sb.append(inStringLiteral ? FTLUtil.escapeStringLiteralPart(exprCF, '"') : exprCF);
+        sb.append("}");
+        if (!canonical && expression != escapedExpression) {
+            sb.append(" auto-escaped");            
+        }
+        return sb.toString();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "${...}";
+    }
+
+    @Override
+    boolean heedsOpeningWhitespace() {
+        return true;
+    }
+
+    @Override
+    boolean heedsTrailingWhitespace() {
+        return true;
+    }
+
+    @Override
+    int getParameterCount() {
+        return 1;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return expression;
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return ParameterRole.CONTENT;
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTElement.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTElement.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTElement.java
new file mode 100644
index 0000000..a9cbfc0
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTElement.java
@@ -0,0 +1,445 @@
+/*
+ * 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.util.Collections;
+import java.util.Enumeration;
+
+import org.apache.freemarker.core.util._ArrayEnumeration;
+
+/**
+ * AST non-expression node superclass: Superclass of directive calls, interpolations, static text, top-level comments,
+ * or other such non-expression node in the parsed template. Some information that can be found here can be accessed
+ * through the {@link Environment#getCurrentDirectiveCallPlace()}, which is a published API, and thus promises backward
+ * compatibility.
+ */
+// TODO [FM3] Get rid of "public" and thus the "_" prefix
+abstract class ASTElement extends ASTNode {
+
+    private static final int INITIAL_CHILD_BUFFER_CAPACITY = 6;
+
+    private ASTElement parent;
+
+    /**
+     * Contains 1 or more nested elements with optional trailing {@code null}-s, or is {@code null} exactly if there are
+     * no nested elements.
+     */
+    private ASTElement[] childBuffer;
+
+    /**
+     * Contains the number of elements in the {@link #childBuffer}, not counting the trailing {@code null}-s. If this is
+     * 0, then and only then {@link #childBuffer} must be {@code null}.
+     */
+    private int childCount;
+
+    /**
+     * The index of the element in the parent's {@link #childBuffer} array.
+     * 
+     * @since 2.3.23
+     */
+    private int index;
+
+    /**
+     * Executes this {@link ASTElement}. Usually should not be called directly, but through
+     * {@link Environment#visit(ASTElement)} or a similar {@link Environment} method.
+     *
+     * @param env
+     *            The runtime environment
+     * 
+     * @return The template elements to execute (meant to be used for nested elements), or {@code null}. Can have
+     *         <em>trailing</em> {@code null}-s (unused buffer capacity). Returning the nested elements instead of
+     *         executing them inside this method is a trick used for decreasing stack usage when there's nothing to do
+     *         after the children was processed anyway.
+     */
+    abstract ASTElement[] accept(Environment env) throws TemplateException, IOException;
+
+    /**
+     * One-line description of the element, that contain all the information that is used in {@link #getCanonicalForm()}
+     * , except the nested content (elements) of the element. The expressions inside the element (the parameters) has to
+     * be shown. Meant to be used for stack traces, also for tree views that don't go down to the expression-level.
+     * There are no backward-compatibility guarantees regarding the format used ATM, but it must be regular enough to be
+     * machine-parseable, and it must contain all information necessary for restoring an AST equivalent to the original.
+     * 
+     * This final implementation calls {@link #dump(boolean) dump(false)}.
+     * 
+     * @see #getCanonicalForm()
+     * @see #getNodeTypeSymbol()
+     */
+    public final String getDescription() {
+        return dump(false);
+    }
+
+    /**
+     * This final implementation calls {@link #dump(boolean) dump(false)}.
+     */
+    @Override
+    public final String getCanonicalForm() {
+        return dump(true);
+    }
+
+    final String getChildrenCanonicalForm() {
+        return getChildrenCanonicalForm(childBuffer);
+    }
+    
+    static String getChildrenCanonicalForm(ASTElement[] children) {
+        if (children == null) {
+            return "";
+        }
+        StringBuilder sb = new StringBuilder();
+        for (ASTElement child : children) {
+            if (child == null) {
+                break;
+            }
+            sb.append(child.getCanonicalForm());
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Tells if the element should show up in error stack traces. Note that this will be ignored for the top (current)
+     * element of a stack trace, as that's always shown.
+     */
+    boolean isShownInStackTrace() {
+        return false;
+    }
+
+    /**
+     * Tells if this element possibly executes its nested content for many times. This flag is useful when a template
+     * AST is modified for running time limiting (see {@link ThreadInterruptionSupportTemplatePostProcessor}). Elements
+     * that use {@link #childBuffer} should not need this, as the insertion of the timeout checks is impossible there,
+     * given their rigid nested element schema.
+     */
+    abstract boolean isNestedBlockRepeater();
+
+    /**
+     * Brings the implementation of {@link #getCanonicalForm()} and {@link #getDescription()} to a single place. Don't
+     * call those methods in method on {@code this}, because that will result in infinite recursion!
+     * 
+     * @param canonical
+     *            if {@code true}, it calculates the return value of {@link #getCanonicalForm()}, otherwise of
+     *            {@link #getDescription()}.
+     */
+    abstract protected String dump(boolean canonical);
+
+    // Methods to implement TemplateNodeModel
+
+    public String getNodeName() {
+        String className = getClass().getName();
+        int shortNameOffset = className.lastIndexOf('.') + 1;
+        return className.substring(shortNameOffset);
+    }
+
+    // Methods so that we can implement the Swing TreeNode API.
+
+    public boolean isLeaf() {
+        return childCount == 0;
+    }
+
+    public int getIndex(ASTElement node) {
+        for (int i = 0; i < childCount; i++) {
+            if (childBuffer[i].equals(node)) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    public int getChildCount() {
+        return childCount;
+    }
+
+    /**
+     * Note: For element with {@code #nestedBlock}, this will hide the {@code #nestedBlock} when that's a
+     * {@link ASTImplicitParent}.
+     */
+    public Enumeration children() {
+        return childBuffer != null
+                ? new _ArrayEnumeration(childBuffer, childCount)
+                : Collections.enumeration(Collections.EMPTY_LIST);
+    }
+
+    public void setChildAt(int index, ASTElement element) {
+        if (index < childCount && index >= 0) {
+            childBuffer[index] = element;
+            element.index = index;
+            element.parent = this;
+        } else {
+            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + childCount);
+        }
+    }
+    
+    /**
+     * The element whose child this element is, or {@code null} if this is the root node.
+     */
+    final ASTElement getParent() {
+        return parent;
+    }
+
+    final void setChildBufferCapacity(int capacity) {
+        int ln = childCount;
+        ASTElement[] newChildBuffer = new ASTElement[capacity];
+        for (int i = 0; i < ln; i++) {
+            newChildBuffer[i] = childBuffer[i];
+        }
+        childBuffer = newChildBuffer;
+    }
+
+    /**
+     * Inserts a new nested element after the last nested element.
+     */
+    final void addChild(ASTElement nestedElement) {
+        addChild(childCount, nestedElement);
+    }
+
+    /**
+     * Inserts a new nested element at the given index, which can also be one higher than the current highest index.
+     */
+    final void addChild(int index, ASTElement nestedElement) {
+        final int lChildCount = childCount;
+
+        ASTElement[] lChildBuffer = childBuffer;
+        if (lChildBuffer == null) {
+            lChildBuffer = new ASTElement[INITIAL_CHILD_BUFFER_CAPACITY];
+            childBuffer = lChildBuffer;
+        } else if (lChildCount == lChildBuffer.length) {
+            setChildBufferCapacity(lChildCount != 0 ? lChildCount * 2 : 1);
+            lChildBuffer = childBuffer;
+        }
+        // At this point: nestedElements == this.nestedElements, and has sufficient capacity.
+
+        for (int i = lChildCount; i > index; i--) {
+            ASTElement movedElement = lChildBuffer[i - 1];
+            movedElement.index = i;
+            lChildBuffer[i] = movedElement;
+        }
+        nestedElement.index = index;
+        nestedElement.parent = this;
+        lChildBuffer[index] = nestedElement;
+        childCount = lChildCount + 1;
+    }
+
+    final ASTElement getChild(int index) {
+        return childBuffer[index];
+    }
+
+    /**
+     * @return Array containing 1 or more nested elements with optional trailing {@code null}-s, or is {@code null}
+     *         exactly if there are no nested elements.
+     */
+    final ASTElement[] getChildBuffer() {
+        return childBuffer;
+    }
+
+    /**
+     * @param buffWithCnt Maybe {@code null}
+     * 
+     * @since 2.3.24
+     */
+    final void setChildren(TemplateElements buffWithCnt) {
+        ASTElement[] childBuffer = buffWithCnt.getBuffer();
+        int childCount = buffWithCnt.getCount();
+        for (int i = 0; i < childCount; i++) {
+            ASTElement child = childBuffer[i];
+            child.index = i;
+            child.parent = this;
+        }
+        this.childBuffer = childBuffer;
+        this.childCount = childCount;
+    }
+
+    final int getIndex() {
+        return index;
+    }
+
+    /**
+     * This is a special case, because a root element is not contained in another element, so we couldn't set the
+     * private fields.
+     */
+    final void setFieldsForRootElement() {
+        index = 0;
+        parent = null;
+    }
+
+    /**
+     * Walk the AST subtree rooted by this element, and do simplifications where possible, also removes superfluous
+     * whitespace.
+     * 
+     * @param stripWhitespace
+     *            whether to remove superfluous whitespace
+     * 
+     * @return The element this element should be replaced with in the parent. If it's the same as this element, no
+     *         actual replacement will happen. Note that adjusting the {@link #parent} and {@link #index} of the result
+     *         is the duty of the caller, not of this method.
+     */
+    ASTElement postParseCleanup(boolean stripWhitespace) throws ParseException {
+        int childCount = this.childCount;
+        if (childCount != 0) {
+            for (int i = 0; i < childCount; i++) {
+                ASTElement te = childBuffer[i];
+                
+                /*
+                // Assertion:
+                if (te.getIndex() != i) {
+                    throw new BugException("Invalid index " + te.getIndex() + " (expected: "
+                            + i + ") for: " + te.dump(false));
+                }
+                if (te.getParent() != this) {
+                    throw new BugException("Invalid parent " + te.getParent() + " (expected: "
+                            + this.dump(false) + ") for: " + te.dump(false));
+                }
+                */
+                
+                te = te.postParseCleanup(stripWhitespace);
+                childBuffer[i] = te;
+                te.parent = this;
+                te.index = i;
+            }
+            for (int i = 0; i < childCount; i++) {
+                ASTElement te = childBuffer[i];
+                if (te.isIgnorable(stripWhitespace)) {
+                    childCount--;
+                    // As later isIgnorable calls might investigates the siblings, we have to move all the items now. 
+                    for (int j = i; j < childCount; j++) {
+                        final ASTElement te2 = childBuffer[j + 1];
+                        childBuffer[j] = te2;
+                        te2.index = j;
+                    }
+                    childBuffer[childCount] = null;
+                    this.childCount = childCount;
+                    i--;
+                }
+            }
+            if (childCount == 0) {
+                childBuffer = null;
+            } else if (childCount < childBuffer.length
+                    && childCount <= childBuffer.length * 3 / 4) {
+                ASTElement[] trimmedChildBuffer = new ASTElement[childCount];
+                for (int i = 0; i < childCount; i++) {
+                    trimmedChildBuffer[i] = childBuffer[i];
+                }
+                childBuffer = trimmedChildBuffer;
+            }
+        }
+        return this;
+    }
+
+    boolean isIgnorable(boolean stripWhitespace) {
+        return false;
+    }
+
+    // The following methods exist to support some fancier tree-walking
+    // and were introduced to support the whitespace cleanup feature in 2.2
+
+    ASTElement prevTerminalNode() {
+        ASTElement prev = previousSibling();
+        if (prev != null) {
+            return prev.getLastLeaf();
+        } else if (parent != null) {
+            return parent.prevTerminalNode();
+        }
+        return null;
+    }
+
+    ASTElement nextTerminalNode() {
+        ASTElement next = nextSibling();
+        if (next != null) {
+            return next.getFirstLeaf();
+        } else if (parent != null) {
+            return parent.nextTerminalNode();
+        }
+        return null;
+    }
+
+    ASTElement previousSibling() {
+        if (parent == null) {
+            return null;
+        }
+        return index > 0 ? parent.childBuffer[index - 1] : null;
+    }
+
+    ASTElement nextSibling() {
+        if (parent == null) {
+            return null;
+        }
+        return index + 1 < parent.childCount ? parent.childBuffer[index + 1] : null;
+    }
+
+    private ASTElement getFirstChild() {
+        return childCount == 0 ? null : childBuffer[0];
+    }
+
+    private ASTElement getLastChild() {
+        final int childCount = this.childCount;
+        return childCount == 0 ? null : childBuffer[childCount - 1];
+    }
+
+    private ASTElement getFirstLeaf() {
+        ASTElement te = this;
+        while (!te.isLeaf() && !(te instanceof ASTDirMacro) && !(te instanceof ASTDirCapturingAssignment)) {
+            // A macro or macro invocation is treated as a leaf here for special reasons
+            te = te.getFirstChild();
+        }
+        return te;
+    }
+
+    private ASTElement getLastLeaf() {
+        ASTElement te = this;
+        while (!te.isLeaf() && !(te instanceof ASTDirMacro) && !(te instanceof ASTDirCapturingAssignment)) {
+            // A macro or macro invocation is treated as a leaf here for special reasons
+            te = te.getLastChild();
+        }
+        return te;
+    }
+
+    /**
+     * Tells if executing this element has output that only depends on the template content and that has no side
+     * effects.
+     */
+    boolean isOutputCacheable() {
+        return false;
+    }
+
+    boolean isChildrenOutputCacheable() {
+        int ln = childCount;
+        for (int i = 0; i < ln; i++) {
+            if (!childBuffer[i].isOutputCacheable()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * determines whether this element's presence on a line indicates that we should not strip opening whitespace in the
+     * post-parse whitespace gobbling step.
+     */
+    boolean heedsOpeningWhitespace() {
+        return false;
+    }
+
+    /**
+     * determines whether this element's presence on a line indicates that we should not strip trailing whitespace in
+     * the post-parse whitespace gobbling step.
+     */
+    boolean heedsTrailingWhitespace() {
+        return false;
+    }
+}


[36/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/InvalidReferenceException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/InvalidReferenceException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/InvalidReferenceException.java
new file mode 100644
index 0000000..20d2c10
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/InvalidReferenceException.java
@@ -0,0 +1,167 @@
+/*
+ * 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;
+
+/**
+ * A subclass of {@link TemplateException} that says that an FTL expression has evaluated to {@code null} or it refers
+ * to something that doesn't exist. At least in FreeMarker 2.3.x these two cases aren't distinguished.
+ */
+public class InvalidReferenceException extends TemplateException {
+
+    static final InvalidReferenceException FAST_INSTANCE;
+    static {
+        Environment prevEnv = Environment.getCurrentEnvironment();
+        try {
+            Environment.setCurrentEnvironment(null);
+            FAST_INSTANCE = new InvalidReferenceException(
+                    "Invalid reference. Details are unavilable, as this should have been handled by an FTL construct. "
+                    + "If it wasn't, that's problably a bug in FreeMarker.",
+                    null);
+        } finally {
+            Environment.setCurrentEnvironment(prevEnv);
+        }
+    }
+    
+    private static final Object[] TIP = {
+        "If the failing expression is known to be legally refer to something that's sometimes null or missing, "
+        + "either specify a default value like myOptionalVar!myDefault, or use ",
+        "<#if myOptionalVar??>", "when-present", "<#else>", "when-missing", "</#if>",
+        ". (These only cover the last step of the expression; to cover the whole expression, "
+        + "use parenthesis: (myOptionalVar.foo)!myDefault, (myOptionalVar.foo)??"
+    };
+
+    private static final Object[] TIP_MISSING_ASSIGNMENT_TARGET = {
+            "If the target variable is known to be legally null or missing sometimes, instead of something like ",
+            "<#assign x += 1>", ", you could write ", "<#if x??>", "<#assign x += 1>", "</#if>",
+            " or ", "<#assign x = (x!0) + 1>"
+    };
+    
+    private static final String TIP_NO_DOLLAR =
+            "Variable references must not start with \"$\", unless the \"$\" is really part of the variable name.";
+
+    private static final String TIP_LAST_STEP_DOT =
+            "It's the step after the last dot that caused this error, not those before it.";
+
+    private static final String TIP_LAST_STEP_SQUARE_BRACKET =
+            "It's the final [] step that caused this error, not those before it.";
+    
+    private static final String TIP_JSP_TAGLIBS =
+            "The \"JspTaglibs\" variable isn't a core FreeMarker feature; "
+            + "it's only available when templates are invoked through org.apache.freemarker.servlet.FreemarkerServlet"
+            + " (or other custom FreeMarker-JSP integration solution).";
+    
+    /**
+     * Creates and invalid reference exception that contains no information about what was missing or null.
+     * As such, try to avoid this constructor.
+     */
+    public InvalidReferenceException(Environment env) {
+        super("Invalid reference: The expression has evaluated to null or refers to something that doesn't exist.",
+                env);
+    }
+
+    /**
+     * Creates and invalid reference exception that contains no programmatically extractable information about the
+     * blamed expression. As such, try to avoid this constructor, unless need to raise this expression from outside
+     * the FreeMarker core.
+     */
+    public InvalidReferenceException(String description, Environment env) {
+        super(description, env);
+    }
+
+    /**
+     * This is the recommended constructor, but it's only used internally, and has no backward compatibility guarantees.
+     * 
+     * @param expression The expression that evaluates to missing or null. The last step of the expression should be
+     *     the failing one, like in {@code goodStep.failingStep.furtherStep} it should only contain
+     *     {@code goodStep.failingStep}.
+     */
+    InvalidReferenceException(_ErrorDescriptionBuilder description, Environment env, ASTExpression expression) {
+        super(null, env, expression, description);
+    }
+
+    /**
+     * Use this whenever possible, as it returns {@link #FAST_INSTANCE} instead of creating a new instance, when
+     * appropriate.
+     */
+    static InvalidReferenceException getInstance(ASTExpression blamed, Environment env) {
+        if (env != null && env.getFastInvalidReferenceExceptions()) {
+            return FAST_INSTANCE;
+        } else {
+            if (blamed != null) {
+                final _ErrorDescriptionBuilder errDescBuilder
+                        = new _ErrorDescriptionBuilder("The following has evaluated to null or missing:").blame(blamed);
+                if (endsWithDollarVariable(blamed)) {
+                    errDescBuilder.tips(TIP_NO_DOLLAR, TIP);
+                } else if (blamed instanceof ASTExpDot) {
+                    final String rho = ((ASTExpDot) blamed).getRHO();
+                    String nameFixTip = null;
+                    if ("size".equals(rho)) {
+                        nameFixTip = "To query the size of a collection or map use ?size, like myList?size";
+                    } else if ("length".equals(rho)) {
+                        nameFixTip = "To query the length of a string use ?length, like myString?size";
+                    }
+                    errDescBuilder.tips(
+                            nameFixTip == null
+                                    ? new Object[] { TIP_LAST_STEP_DOT, TIP }
+                                    : new Object[] { TIP_LAST_STEP_DOT, nameFixTip, TIP });
+                } else if (blamed instanceof ASTExpDynamicKeyName) {
+                    errDescBuilder.tips(TIP_LAST_STEP_SQUARE_BRACKET, TIP);
+                } else if (blamed instanceof ASTExpVariable
+                        && ((ASTExpVariable) blamed).getName().equals("JspTaglibs")) {
+                    errDescBuilder.tips(TIP_JSP_TAGLIBS, TIP);
+                } else {
+                    errDescBuilder.tip(TIP);
+                }
+                return new InvalidReferenceException(errDescBuilder, env, blamed);
+            } else {
+                return new InvalidReferenceException(env);
+            }
+        }
+    }
+    
+    /**
+     * Used for assignments that use operators like {@code +=}, when the target variable was null/missing. 
+     */
+    static InvalidReferenceException getInstance(String missingAssignedVarName, String assignmentOperator,
+            Environment env) {
+        if (env != null && env.getFastInvalidReferenceExceptions()) {
+            return FAST_INSTANCE;
+        } else {
+            final _ErrorDescriptionBuilder errDescBuilder = new _ErrorDescriptionBuilder(
+                            "The target variable of the assignment, ",
+                            new _DelayedJQuote(missingAssignedVarName),
+                            ", was null or missing, but the \"",
+                            assignmentOperator, "\" operator needs to get its value before assigning to it."
+                    );
+            if (missingAssignedVarName.startsWith("$")) {
+                errDescBuilder.tips(TIP_NO_DOLLAR, TIP_MISSING_ASSIGNMENT_TARGET);
+            } else {
+                errDescBuilder.tip(TIP_MISSING_ASSIGNMENT_TARGET);
+            }
+            return new InvalidReferenceException(errDescBuilder, env, null);
+        }
+    }
+
+    private static boolean endsWithDollarVariable(ASTExpression blame) {
+        return blame instanceof ASTExpVariable && ((ASTExpVariable) blame).getName().startsWith("$")
+                || blame instanceof ASTExpDot && ((ASTExpDot) blame).getRHO().startsWith("$");
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ListableRightUnboundedRangeModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ListableRightUnboundedRangeModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ListableRightUnboundedRangeModel.java
new file mode 100644
index 0000000..a4a14b5
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ListableRightUnboundedRangeModel.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import java.math.BigInteger;
+
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+
+/**
+ * This is the model used for right-unbounded ranges since Incompatible Improvements 2.3.21.
+ * 
+ * @since 2.3.21
+ */
+final class ListableRightUnboundedRangeModel extends RightUnboundedRangeModel implements TemplateCollectionModel {
+
+    ListableRightUnboundedRangeModel(int begin) {
+        super(begin);
+    }
+
+    @Override
+    public int size() throws TemplateModelException {
+        return Integer.MAX_VALUE;
+    }
+
+    @Override
+    public TemplateModelIterator iterator() throws TemplateModelException {
+        return new TemplateModelIterator() {
+            boolean needInc;
+            int nextType = 1;
+            int nextInt = getBegining();
+            long nextLong;
+            BigInteger nextBigInteger;
+
+            @Override
+            public TemplateModel next() throws TemplateModelException {
+                if (needInc) {
+                    switch (nextType) {
+                    case 1:
+                        if (nextInt < Integer.MAX_VALUE) {
+                            nextInt++;
+                        } else {
+                            nextType = 2;
+                            nextLong = nextInt + 1L;
+                        }
+                        break;
+                        
+                    case 2:
+                        if (nextLong < Long.MAX_VALUE) {
+                            nextLong++;
+                        } else {
+                            nextType = 3;
+                            nextBigInteger = BigInteger.valueOf(nextLong);
+                            nextBigInteger = nextBigInteger.add(BigInteger.ONE);
+                        }
+                        break;
+                        
+                    default: // 3
+                        nextBigInteger = nextBigInteger.add(BigInteger.ONE);
+                    }
+                }
+                needInc = true;
+                return nextType == 1 ? new SimpleNumber(nextInt)
+                        : (nextType == 2 ? new SimpleNumber(nextLong)
+                        : new SimpleNumber(nextBigInteger)); 
+            }
+
+            @Override
+            public boolean hasNext() throws TemplateModelException {
+                return true;
+            }
+            
+        };
+        
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/LocalContext.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/LocalContext.java b/freemarker-core/src/main/java/org/apache/freemarker/core/LocalContext.java
new file mode 100644
index 0000000..1084470
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/LocalContext.java
@@ -0,0 +1,36 @@
+/*
+ * 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.util.Collection;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+
+/**
+  * An interface that represents a local context. This is used as the abstraction for  
+  * the context of a ASTDirMacro invocation, a loop, or the nested block call from within 
+  * a macro.
+  */
+public interface LocalContext {
+    TemplateModel getLocalVariable(String name) throws TemplateModelException;
+    Collection getLocalVariableNames() throws TemplateModelException;
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/LocalContextStack.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/LocalContextStack.java b/freemarker-core/src/main/java/org/apache/freemarker/core/LocalContextStack.java
new file mode 100644
index 0000000..aead89d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/LocalContextStack.java
@@ -0,0 +1,57 @@
+/*
+ * 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;
+
+/**
+ * Class that's a little bit more efficient than using an {@code ArrayList<LocalContext>}. 
+ * 
+ * @since 2.3.24
+ */
+final class LocalContextStack {
+
+    private LocalContext[] buffer = new LocalContext[8];
+    private int size;
+
+    void push(LocalContext localContext) {
+        final int newSize = ++size;
+        LocalContext[] buffer = this.buffer;
+        if (buffer.length < newSize) {
+            final LocalContext[] newBuffer = new LocalContext[newSize * 2];
+            for (int i = 0; i < buffer.length; i++) {
+                newBuffer[i] = buffer[i];
+            }
+            buffer = newBuffer;
+            this.buffer = newBuffer;
+        }
+        buffer[newSize - 1] = localContext;
+    }
+
+    void pop() {
+        buffer[--size] = null;
+    }
+
+    public LocalContext get(int index) {
+        return buffer[index];
+    }
+    
+    public int size() {
+        return size;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/MarkupOutputFormatBoundBuiltIn.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/MarkupOutputFormatBoundBuiltIn.java b/freemarker-core/src/main/java/org/apache/freemarker/core/MarkupOutputFormatBoundBuiltIn.java
new file mode 100644
index 0000000..d477963
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/MarkupOutputFormatBoundBuiltIn.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.outputformat.MarkupOutputFormat;
+import org.apache.freemarker.core.util._NullArgumentException;
+
+abstract class MarkupOutputFormatBoundBuiltIn extends SpecialBuiltIn {
+    
+    protected MarkupOutputFormat outputFormat;
+    
+    void bindToMarkupOutputFormat(MarkupOutputFormat outputFormat) {
+        _NullArgumentException.check(outputFormat);
+        this.outputFormat = outputFormat;
+    }
+    
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        if (outputFormat == null) {
+            // The parser should prevent this situation
+            throw new NullPointerException("outputFormat was null");
+        }
+        return calculateResult(env);
+    }
+
+    protected abstract TemplateModel calculateResult(Environment env)
+            throws TemplateException;
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/MessageUtil.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/MessageUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/MessageUtil.java
new file mode 100644
index 0000000..6a2bc2f
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/MessageUtil.java
@@ -0,0 +1,341 @@
+/*
+ * 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.util.ArrayList;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.util._StringUtil;
+import org.apache.freemarker.core.valueformat.TemplateDateFormat;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormat;
+import org.apache.freemarker.core.valueformat.TemplateValueFormatException;
+import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException;
+
+/**
+ * Utilities for creating error messages (and other messages).
+ */
+class MessageUtil {
+
+    static final String UNKNOWN_DATE_TO_STRING_ERROR_MESSAGE
+            = "Can't convert the date-like value to string because it isn't "
+              + "known if it's a date (no time part), time or date-time value.";
+    
+    static final String UNKNOWN_DATE_TYPE_ERROR_TIP =
+            "Use ?date, ?time, or ?datetime to tell FreeMarker the exact type.";
+
+    static final Object[] UNKNOWN_DATE_TO_STRING_TIPS = {
+            UNKNOWN_DATE_TYPE_ERROR_TIP,
+            "If you need a particular format only once, use ?string(pattern), like ?string('dd.MM.yyyy HH:mm:ss'), "
+            + "to specify which fields to display. "
+    };
+
+    static final String EMBEDDED_MESSAGE_BEGIN = "---begin-message---\n";
+
+    static final String EMBEDDED_MESSAGE_END = "\n---end-message---";
+
+    static final String ERROR_MESSAGE_HR = "----";
+
+    // Can't be instantiated
+    private MessageUtil() { }
+
+    static String formatLocationForSimpleParsingError(Template template, int line, int column) {
+        return formatLocation("in", template, line, column);
+    }
+
+    static String formatLocationForSimpleParsingError(String templateSourceOrLookupName, int line, int column) {
+        return formatLocation("in", templateSourceOrLookupName, line, column);
+    }
+
+    static String formatLocationForEvaluationError(Template template, int line, int column) {
+        return formatLocation("at", template, line, column);
+    }
+
+    static String formatLocationForEvaluationError(ASTDirMacro macro, int line, int column) {
+        Template t = macro.getTemplate();
+        return formatLocation("at", t != null ? t.getSourceOrLookupName() : null, macro.getName(), macro.isFunction(),
+                line, column);
+    }
+
+    private static String formatLocation(String preposition, Template template, int line, int column) {
+        return formatLocation(preposition, template != null ? template.getSourceOrLookupName() : null, line, column);
+    }
+
+    private static String formatLocation(String preposition, String templateSourceName, int line, int column) {
+        return formatLocation(
+                preposition, templateSourceName,
+                null, false,
+                line, column);
+    }
+
+    private static String formatLocation(
+            String preposition, String templateSourceName,
+            String macroOrFuncName, boolean isFunction,
+            int line, int column) {
+        String templateDesc;
+        if (line < 0) {
+            templateDesc = "?eval-ed string";
+            macroOrFuncName = null;
+        } else {
+            templateDesc = templateSourceName != null
+                ? "template " + _StringUtil.jQuoteNoXSS(templateSourceName)
+                : "nameless template";
+        }
+        return "in " + templateDesc
+              + (macroOrFuncName != null
+                      ? " in " + (isFunction ? "function " : "macro ") + _StringUtil.jQuote(macroOrFuncName)
+                      : "")
+              + " "
+              + preposition + " " + formatPosition(line, column);
+    }
+
+    static String formatPosition(int line, int column) {
+        return "line " + (line >= 0 ? line : line - (ASTNode.RUNTIME_EVAL_LINE_DISPLACEMENT - 1))
+                + ", column " + column;
+    }
+
+    /**
+     * Returns a single line string that is no longer than {@code maxLength}.
+     * If will truncate the string at line-breaks too.
+     * The truncation is always signaled with a a {@code "..."} at the end of the result string.
+     */
+    static String shorten(String s, int maxLength) {
+        if (maxLength < 5) maxLength = 5;
+
+        boolean isTruncated = false;
+        
+        int brIdx = s.indexOf('\n');
+        if (brIdx != -1) {
+            s = s.substring(0, brIdx);
+            isTruncated = true;
+        }
+        brIdx = s.indexOf('\r');
+        if (brIdx != -1) {
+            s = s.substring(0, brIdx);
+            isTruncated = true;
+        }
+        
+        if (s.length() > maxLength) {
+            s = s.substring(0, maxLength - 3);
+            isTruncated = true;
+        }
+        
+        if (!isTruncated) {
+            return s;
+        } else {
+            if (s.endsWith(".")) {
+                if (s.endsWith("..")) {
+                    if (s.endsWith("...")) {
+                        return s;
+                    } else {
+                        return s + ".";
+                    }
+                } else {
+                    return s + "..";
+                }
+            } else {
+                return s + "...";
+            }
+        }
+    }
+    
+    static StringBuilder appendExpressionAsUntearable(StringBuilder sb, ASTExpression argExp) {
+        boolean needParen =
+                !(argExp instanceof ASTExpNumberLiteral)
+                && !(argExp instanceof ASTExpStringLiteral)
+                && !(argExp instanceof ASTExpBooleanLiteral)
+                && !(argExp instanceof ASTExpListLiteral)
+                && !(argExp instanceof ASTExpHashLiteral)
+                && !(argExp instanceof ASTExpVariable)
+                && !(argExp instanceof ASTExpDot)
+                && !(argExp instanceof ASTExpDynamicKeyName)
+                && !(argExp instanceof ASTExpMethodCall)
+                && !(argExp instanceof ASTExpBuiltIn);
+        if (needParen) sb.append('(');
+        sb.append(argExp.getCanonicalForm());
+        if (needParen) sb.append(')');
+        return sb;
+    }
+
+    static TemplateModelException newArgCntError(String methodName, int argCnt, int expectedCnt) {
+        return newArgCntError(methodName, argCnt, expectedCnt, expectedCnt);
+    }
+    
+    static TemplateModelException newArgCntError(String methodName, int argCnt, int minCnt, int maxCnt) {
+        ArrayList/*<Object>*/ desc = new ArrayList(20);
+        
+        desc.add(methodName);
+        
+        desc.add("(");
+        if (maxCnt != 0) desc.add("...");
+        desc.add(") expects ");
+        
+        if (minCnt == maxCnt) {
+            if (maxCnt == 0) {
+                desc.add("no");
+            } else {
+                desc.add(Integer.valueOf(maxCnt));
+            }
+        } else if (maxCnt - minCnt == 1) {
+            desc.add(Integer.valueOf(minCnt));
+            desc.add(" or ");
+            desc.add(Integer.valueOf(maxCnt));
+        } else {
+            desc.add(Integer.valueOf(minCnt));
+            if (maxCnt != Integer.MAX_VALUE) {
+                desc.add(" to ");
+                desc.add(Integer.valueOf(maxCnt));
+            } else {
+                desc.add(" or more (unlimited)");
+            }
+        }
+        desc.add(" argument");
+        if (maxCnt > 1) desc.add("s");
+        
+        desc.add(" but has received ");
+        if (argCnt == 0) {
+            desc.add("none");
+        } else {
+            desc.add(Integer.valueOf(argCnt));
+        }
+        desc.add(".");
+        
+        return new _TemplateModelException(desc.toArray());
+    }
+
+    static TemplateModelException newMethodArgMustBeStringException(String methodName, int argIdx, TemplateModel arg) {
+        return newMethodArgUnexpectedTypeException(methodName, argIdx, "string", arg);
+    }
+
+    static TemplateModelException newMethodArgMustBeNumberException(String methodName, int argIdx, TemplateModel arg) {
+        return newMethodArgUnexpectedTypeException(methodName, argIdx, "number", arg);
+    }
+    
+    static TemplateModelException newMethodArgMustBeBooleanException(String methodName, int argIdx, TemplateModel arg) {
+        return newMethodArgUnexpectedTypeException(methodName, argIdx, "boolean", arg);
+    }
+    
+    static TemplateModelException newMethodArgMustBeExtendedHashException(
+            String methodName, int argIdx, TemplateModel arg) {
+        return newMethodArgUnexpectedTypeException(methodName, argIdx, "extended hash", arg);
+    }
+    
+    static TemplateModelException newMethodArgMustBeSequenceException(
+            String methodName, int argIdx, TemplateModel arg) {
+        return newMethodArgUnexpectedTypeException(methodName, argIdx, "sequence", arg);
+    }
+    
+    static TemplateModelException newMethodArgMustBeSequenceOrCollectionException(
+            String methodName, int argIdx, TemplateModel arg) {
+        return newMethodArgUnexpectedTypeException(methodName, argIdx, "sequence or collection", arg);
+    }
+    
+    static TemplateModelException newMethodArgUnexpectedTypeException(
+            String methodName, int argIdx, String expectedType, TemplateModel arg) {
+        return new _TemplateModelException(
+                methodName, "(...) expects ", new _DelayedAOrAn(expectedType), " as argument #", Integer.valueOf(argIdx + 1),
+                ", but received ", new _DelayedAOrAn(new _DelayedFTLTypeDescription(arg)), ".");
+    }
+    
+    /**
+     * The type of the argument was good, but it's value wasn't.
+     */
+    static TemplateModelException newMethodArgInvalidValueException(
+            String methodName, int argIdx, Object... details) {
+        return new _TemplateModelException(
+                methodName, "(...) argument #", Integer.valueOf(argIdx + 1),
+                " had invalid value: ", details);
+    }
+
+    /**
+     * The type of the argument was good, but the values of two or more arguments are inconsistent with each other.
+     */
+    static TemplateModelException newMethodArgsInvalidValueException(
+            String methodName, Object... details) {
+        return new _TemplateModelException(methodName, "(...) arguments have invalid value: ", details);
+    }
+    
+    static TemplateException newInstantiatingClassNotAllowedException(String className, Environment env) {
+        return new _MiscTemplateException(env,
+                "Instantiating ", className, " is not allowed in the template for security reasons.");
+    }
+    
+    static TemplateModelException newCantFormatUnknownTypeDateException(
+            ASTExpression dateSourceExpr, UnknownDateTypeFormattingUnsupportedException cause) {
+        return new _TemplateModelException(cause, null, new _ErrorDescriptionBuilder(
+                MessageUtil.UNKNOWN_DATE_TO_STRING_ERROR_MESSAGE)
+                .blame(dateSourceExpr)
+                .tips(MessageUtil.UNKNOWN_DATE_TO_STRING_TIPS));
+    }
+
+    static TemplateException newCantFormatDateException(TemplateDateFormat format, ASTExpression dataSrcExp,
+                                                        TemplateValueFormatException e, boolean useTempModelExc) {
+        _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder(
+                "Failed to format date/time/datetime with format ", new _DelayedJQuote(format.getDescription()), ": ",
+                e.getMessage())
+                .blame(dataSrcExp); 
+        return useTempModelExc
+                ? new _TemplateModelException(e, null, desc)
+                : new _MiscTemplateException(e, null, desc);
+    }
+    
+    static TemplateException newCantFormatNumberException(TemplateNumberFormat format, ASTExpression dataSrcExp,
+                                                          TemplateValueFormatException e, boolean useTempModelExc) {
+        _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder(
+                "Failed to format number with format ", new _DelayedJQuote(format.getDescription()), ": ",
+                e.getMessage())
+                .blame(dataSrcExp); 
+        return useTempModelExc
+                ? new _TemplateModelException(e, null, desc)
+                : new _MiscTemplateException(e, null, desc);
+    }
+    
+    /**
+     * @return "a" or "an" or "a(n)" (or "" for empty string) for an FTL type name
+     */
+    static String getAOrAn(String s) {
+        if (s == null) return null;
+        if (s.length() == 0) return "";
+        
+        char fc = Character.toLowerCase(s.charAt(0));
+        if (fc == 'a' || fc == 'e' || fc == 'i') {
+            return "an";
+        } else if (fc == 'h') { 
+            String ls = s.toLowerCase();
+            if (ls.startsWith("has") || ls.startsWith("hi")) { 
+                return "a";
+            } else if (ls.startsWith("ht")) { 
+                return "an";
+            } else {
+                return "a(n)";
+            }
+        } else if (fc == 'u' || fc == 'o') {
+            return "a(n)";
+        } else {
+            char sc = (s.length() > 1) ? s.charAt(1) : '\0'; 
+            if (fc == 'x' && !(sc == 'a' || sc == 'e' || sc == 'i' || sc == 'a' || sc == 'o' || sc == 'u')) {
+                return "an";
+            } else {
+                return "a";
+            }
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/MiscUtil.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/MiscUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/MiscUtil.java
new file mode 100644
index 0000000..35d5943
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/MiscUtil.java
@@ -0,0 +1,69 @@
+/*
+ * 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.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Utilities that didn't fit elsewhere. 
+ */
+class MiscUtil {
+    
+    // Can't be instatiated
+    private MiscUtil() { }
+
+    static final String C_FALSE = "false";
+    static final String C_TRUE = "true";
+    
+    /**
+     * Returns the map entries in source code order of the ASTExpression values.
+     */
+    static List/*Map.Entry*/ sortMapOfExpressions(Map/*<?, ASTExpression>*/ map) {
+        ArrayList res = new ArrayList(map.entrySet());
+        Collections.sort(res, 
+                new Comparator() {  // for sorting to source code order
+                    @Override
+                    public int compare(Object o1, Object o2) {
+                        Map.Entry ent1 = (Map.Entry) o1;
+                        ASTExpression exp1 = (ASTExpression) ent1.getValue();
+                        
+                        Map.Entry ent2 = (Map.Entry) o2;
+                        ASTExpression exp2 = (ASTExpression) ent2.getValue();
+                        
+                        int res = exp1.beginLine - exp2.beginLine;
+                        if (res != 0) return res;
+                        res = exp1.beginColumn - exp2.beginColumn;
+                        if (res != 0) return res;
+                        
+                        if (ent1 == ent2) return 0;
+                        
+                        // Should never reach this
+                        return ((String) ent1.getKey()).compareTo((String) ent1.getKey()); 
+                    }
+            
+        });
+        return res;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/MutableParsingAndProcessingConfiguration.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/MutableParsingAndProcessingConfiguration.java b/freemarker-core/src/main/java/org/apache/freemarker/core/MutableParsingAndProcessingConfiguration.java
new file mode 100644
index 0000000..00a387d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/MutableParsingAndProcessingConfiguration.java
@@ -0,0 +1,475 @@
+/*
+ * 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.InputStream;
+import java.nio.charset.Charset;
+
+import org.apache.freemarker.core.outputformat.OutputFormat;
+import org.apache.freemarker.core.outputformat.impl.UndefinedOutputFormat;
+import org.apache.freemarker.core.templateresolver.TemplateLoader;
+import org.apache.freemarker.core.util._NullArgumentException;
+
+public abstract class MutableParsingAndProcessingConfiguration<
+        SelfT extends MutableParsingAndProcessingConfiguration<SelfT>>
+        extends MutableProcessingConfiguration<SelfT>
+        implements ParsingAndProcessingConfiguration {
+
+    private TemplateLanguage templateLanguage;
+    private Integer tagSyntax;
+    private Integer namingConvention;
+    private Boolean whitespaceStripping;
+    private Integer autoEscapingPolicy;
+    private Boolean recognizeStandardFileExtensions;
+    private OutputFormat outputFormat;
+    private Charset sourceEncoding;
+    private Integer tabSize;
+
+    protected MutableParsingAndProcessingConfiguration() {
+        super();
+    }
+
+    /**
+     * Setter pair of {@link #getTagSyntax()}.
+     */
+    public void setTagSyntax(int tagSyntax) {
+        valideTagSyntaxValue(tagSyntax);
+        this.tagSyntax = tagSyntax;
+    }
+
+    // [FM3] Use enum; won't be needed
+    static void valideTagSyntaxValue(int tagSyntax) {
+        if (tagSyntax != ParsingConfiguration.AUTO_DETECT_TAG_SYNTAX
+                && tagSyntax != ParsingConfiguration.SQUARE_BRACKET_TAG_SYNTAX
+                && tagSyntax != ParsingConfiguration.ANGLE_BRACKET_TAG_SYNTAX) {
+            throw new IllegalArgumentException(
+                    "\"tagSyntax\" can only be set to one of these: "
+                    + "Configuration.AUTO_DETECT_TAG_SYNTAX, Configuration.ANGLE_BRACKET_SYNTAX, "
+                    + "or Configuration.SQUARE_BRACKET_SYNTAX");
+        }
+    }
+
+    /**
+     * Fluent API equivalent of {@link #tagSyntax(int)}
+     */
+    public SelfT tagSyntax(int tagSyntax) {
+        setTagSyntax(tagSyntax);
+        return self();
+    }
+
+    /**
+     * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+     * {@link ParsingConfiguration}).
+     */
+    public void unsetTagSyntax() {
+        this.tagSyntax = null;
+    }
+
+    @Override
+    public int getTagSyntax() {
+        return isTagSyntaxSet() ? tagSyntax : getDefaultTagSyntax();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set, possibly by inheriting the setting value
+     * from another {@link ParsingConfiguration}, or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract int getDefaultTagSyntax();
+
+    @Override
+    public boolean isTagSyntaxSet() {
+        return tagSyntax != null;
+    }
+
+    @Override
+    public TemplateLanguage getTemplateLanguage() {
+         return isTemplateLanguageSet() ? templateLanguage : getDefaultTemplateLanguage();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set, possibly by inheriting the setting value
+     * from another {@link ParsingConfiguration}, or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract TemplateLanguage getDefaultTemplateLanguage();
+
+    /**
+     * Setter pair of {@link #getTemplateLanguage()}.
+     */
+    public void setTemplateLanguage(TemplateLanguage templateLanguage) {
+        _NullArgumentException.check("templateLanguage", templateLanguage);
+        this.templateLanguage = templateLanguage;
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setTemplateLanguage(TemplateLanguage)}
+     */
+    public SelfT templateLanguage(TemplateLanguage templateLanguage) {
+        setTemplateLanguage(templateLanguage);
+        return self();
+    }
+
+    /**
+     * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+     * {@link ParsingConfiguration}).
+     */
+    public void unsetTemplateLanguage() {
+        this.templateLanguage = null;
+    }
+
+    @Override
+    public boolean isTemplateLanguageSet() {
+        return templateLanguage != null;
+    }
+
+    /**
+     * Setter pair of {@link #getNamingConvention()}.
+     */
+    public void setNamingConvention(int namingConvention) {
+        Configuration.validateNamingConventionValue(namingConvention);
+        this.namingConvention = namingConvention;
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setNamingConvention(int)}
+     */
+    public SelfT namingConvention(int namingConvention) {
+        setNamingConvention(namingConvention);
+        return self();
+    }
+
+    /**
+     * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+     * {@link ParsingConfiguration}).
+     */
+    public void unsetNamingConvention() {
+        this.namingConvention = null;
+    }
+
+    /**
+     * The getter pair of {@link #setNamingConvention(int)}.
+     */
+    @Override
+    public int getNamingConvention() {
+         return isNamingConventionSet() ? namingConvention
+                : getDefaultNamingConvention();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set, possibly by inheriting the setting value
+     * from another {@link ParsingConfiguration}, or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract int getDefaultNamingConvention();
+
+    /**
+     * Tells if this setting is set directly in this object or its value is inherited from the parent parsing configuration..
+     */
+    @Override
+    public boolean isNamingConventionSet() {
+        return namingConvention != null;
+    }
+
+    /**
+     * Setter pair of {@link ParsingConfiguration#getWhitespaceStripping()}.
+     */
+    public void setWhitespaceStripping(boolean whitespaceStripping) {
+        this.whitespaceStripping = whitespaceStripping;
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setWhitespaceStripping(boolean)}
+     */
+    public SelfT whitespaceStripping(boolean whitespaceStripping) {
+        setWhitespaceStripping(whitespaceStripping);
+        return self();
+    }
+
+    /**
+     * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+     * {@link ParsingConfiguration}).
+     */
+    public void unsetWhitespaceStripping() {
+        this.whitespaceStripping = null;
+    }
+
+    /**
+     * The getter pair of {@link #getWhitespaceStripping()}.
+     */
+    @Override
+    public boolean getWhitespaceStripping() {
+         return isWhitespaceStrippingSet() ? whitespaceStripping : getDefaultWhitespaceStripping();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set, possibly by inheriting the setting value
+     * from another {@link ParsingConfiguration}, or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract boolean getDefaultWhitespaceStripping();
+
+    /**
+     * Tells if this setting is set directly in this object or its value is inherited from the parent parsing configuration..
+     */
+    @Override
+    public boolean isWhitespaceStrippingSet() {
+        return whitespaceStripping != null;
+    }
+
+    /**
+     * * Setter pair of {@link #getAutoEscapingPolicy()}.
+     */
+    public void setAutoEscapingPolicy(int autoEscapingPolicy) {
+        validateAutoEscapingPolicyValue(autoEscapingPolicy);
+        this.autoEscapingPolicy = autoEscapingPolicy;
+    }
+
+    // [FM3] Use enum; won't be needed
+    static void validateAutoEscapingPolicyValue(int autoEscapingPolicy) {
+        if (autoEscapingPolicy != ParsingConfiguration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY
+                && autoEscapingPolicy != ParsingConfiguration.ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY
+                && autoEscapingPolicy != ParsingConfiguration.DISABLE_AUTO_ESCAPING_POLICY) {
+            throw new IllegalArgumentException(
+                    "\"tagSyntax\" can only be set to one of these: "
+                            + "Configuration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY,"
+                            + "Configuration.ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY, "
+                            + "or Configuration.DISABLE_AUTO_ESCAPING_POLICY");
+        }
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setAutoEscapingPolicy(int)}
+     */
+    public SelfT autoEscapingPolicy(int autoEscapingPolicy) {
+        setAutoEscapingPolicy(autoEscapingPolicy);
+        return self();
+    }
+
+    /**
+     * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+     * {@link ParsingConfiguration}).
+     */
+    public void unsetAutoEscapingPolicy() {
+        this.autoEscapingPolicy = null;
+    }
+
+    /**
+     * The getter pair of {@link #setAutoEscapingPolicy(int)}.
+     */
+    @Override
+    public int getAutoEscapingPolicy() {
+         return isAutoEscapingPolicySet() ? autoEscapingPolicy : getDefaultAutoEscapingPolicy();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set, possibly by inheriting the setting value
+     * from another {@link ParsingConfiguration}, or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract int getDefaultAutoEscapingPolicy();
+
+    /**
+     * Tells if this setting is set directly in this object or its value is inherited from the parent parsing configuration..
+     */
+    @Override
+    public boolean isAutoEscapingPolicySet() {
+        return autoEscapingPolicy != null;
+    }
+
+    /**
+     * Setter pair of {@link #getOutputFormat()}.
+     */
+    public void setOutputFormat(OutputFormat outputFormat) {
+        if (outputFormat == null) {
+            throw new _NullArgumentException(
+                    "outputFormat",
+                    "You may meant: " + UndefinedOutputFormat.class.getSimpleName() + ".INSTANCE");
+        }
+        this.outputFormat = outputFormat;
+    }
+
+    /**
+     * Resets this setting to its initial state, as if it was never set.
+     */
+    public void unsetOutputFormat() {
+        this.outputFormat = null;
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setOutputFormat(OutputFormat)}
+     */
+    public SelfT outputFormat(OutputFormat outputFormat) {
+        setOutputFormat(outputFormat);
+        return self();
+    }
+
+    @Override
+    public OutputFormat getOutputFormat() {
+         return isOutputFormatSet() ? outputFormat : getDefaultOutputFormat();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set, possibly by inheriting the setting value
+     * from another {@link ParsingConfiguration}, or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract OutputFormat getDefaultOutputFormat();
+
+    /**
+     * Tells if this setting is set directly in this object or its value is inherited from the parent parsing configuration..
+     */
+    @Override
+    public boolean isOutputFormatSet() {
+        return outputFormat != null;
+    }
+
+    /**
+     * Setter pair of {@link ParsingConfiguration#getRecognizeStandardFileExtensions()}.
+     */
+    public void setRecognizeStandardFileExtensions(boolean recognizeStandardFileExtensions) {
+        this.recognizeStandardFileExtensions = recognizeStandardFileExtensions;
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setRecognizeStandardFileExtensions(boolean)}
+     */
+    public SelfT recognizeStandardFileExtensions(boolean recognizeStandardFileExtensions) {
+        setRecognizeStandardFileExtensions(recognizeStandardFileExtensions);
+        return self();
+    }
+
+    /**
+     * Resets this setting to its initial state, as if it was never set.
+     */
+    public void unsetRecognizeStandardFileExtensions() {
+        recognizeStandardFileExtensions = null;
+    }
+
+    /**
+     * Getter pair of {@link #setRecognizeStandardFileExtensions(boolean)}.
+     */
+    @Override
+    public boolean getRecognizeStandardFileExtensions() {
+         return isRecognizeStandardFileExtensionsSet() ? recognizeStandardFileExtensions
+                : getDefaultRecognizeStandardFileExtensions();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set, possibly by inheriting the setting value
+     * from another {@link ParsingConfiguration}, or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract boolean getDefaultRecognizeStandardFileExtensions();
+
+    /**
+     * Tells if this setting is set directly in this object or its value is inherited from the parent parsing configuration..
+     */
+    @Override
+    public boolean isRecognizeStandardFileExtensionsSet() {
+        return recognizeStandardFileExtensions != null;
+    }
+
+    @Override
+    public Charset getSourceEncoding() {
+         return isSourceEncodingSet() ? sourceEncoding : getDefaultSourceEncoding();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set, possibly by inheriting the setting value
+     * from another {@link ParsingConfiguration}, or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract Charset getDefaultSourceEncoding();
+
+    /**
+     * The charset to be used when reading the template "file" that the {@link TemplateLoader} returns as binary
+     * ({@link InputStream}). If the {@code #ftl} header specifies an charset, that will override this.
+     */
+    public void setSourceEncoding(Charset sourceEncoding) {
+        _NullArgumentException.check("sourceEncoding", sourceEncoding);
+        this.sourceEncoding = sourceEncoding;
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setSourceEncoding(Charset)}
+     */
+    public SelfT sourceEncoding(Charset sourceEncoding) {
+        setSourceEncoding(sourceEncoding);
+        return self();
+    }
+
+    /**
+     * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+     * {@link ParsingConfiguration}).
+     */
+    public void unsetSourceEncoding() {
+        this.sourceEncoding = null;
+    }
+
+    @Override
+    public boolean isSourceEncodingSet() {
+        return sourceEncoding != null;
+    }
+
+    /**
+     * Setter pair of {@link #getTabSize()}.
+     */
+    public void setTabSize(int tabSize) {
+        if (tabSize < 1) {
+            throw new IllegalArgumentException("\"tabSize\" must be at least 1, but was " + tabSize);
+        }
+        // To avoid integer overflows:
+        if (tabSize > 256) {
+            throw new IllegalArgumentException("\"tabSize\" can't be more than 256, but was " + tabSize);
+        }
+        this.tabSize = Integer.valueOf(tabSize);
+    }
+
+    /**
+     * Fluent API equivalent of {@link #setTabSize(int)}
+     */
+    public SelfT tabSize(int tabSize) {
+        setTabSize(tabSize);
+        return self();
+    }
+
+    /**
+     * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+     * {@link ParsingConfiguration}).
+     */
+    public void unsetTabSize() {
+        this.tabSize = null;
+    }
+
+    @Override
+    public int getTabSize() {
+         return isTabSizeSet() ? tabSize.intValue() : getDefaultTabSize();
+    }
+
+    /**
+     * Returns the value the getter method returns when the setting is not set, possibly by inheriting the setting value
+     * from another {@link ParsingConfiguration}, or throws {@link SettingValueNotSetException}.
+     */
+    protected abstract int getDefaultTabSize();
+
+    /**
+     * Tells if this setting is set directly in this object or its value is inherited from the parent parsing configuration..
+     *
+     * @since 2.3.25
+     */
+    @Override
+    public boolean isTabSizeSet() {
+        return tabSize != null;
+    }
+
+}


[05/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/manual/en_US/docgen-originals/figures/model2sketch_with_alpha.png
----------------------------------------------------------------------
diff --git a/freemarker-core/src/manual/en_US/docgen-originals/figures/model2sketch_with_alpha.png b/freemarker-core/src/manual/en_US/docgen-originals/figures/model2sketch_with_alpha.png
new file mode 100644
index 0000000..ce120cc
Binary files /dev/null and b/freemarker-core/src/manual/en_US/docgen-originals/figures/model2sketch_with_alpha.png differ

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/manual/en_US/docgen-originals/figures/odg-convert-howto.txt
----------------------------------------------------------------------
diff --git a/freemarker-core/src/manual/en_US/docgen-originals/figures/odg-convert-howto.txt b/freemarker-core/src/manual/en_US/docgen-originals/figures/odg-convert-howto.txt
new file mode 100644
index 0000000..e55acec
--- /dev/null
+++ b/freemarker-core/src/manual/en_US/docgen-originals/figures/odg-convert-howto.txt
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+ 
+Converting to SVG:
+1. Open the ODG file with Libeoffice/OpenOffice Draw
+2. Ctrl+A to select all objects
+3. File/Export..., chose SVG format, and then tick "Selection"
+4. Check the result. If contour lines at the right and bottom edge of the
+   figure are partically clipped (stroke width is halved), set a stroke with
+   other than 0 for all shapes.
+   
+Converting to a decent quality (though non-transparent) PNG:
+1. Open the ODG file with Libeoffice/OpenOffice Draw
+2. Export to PDF
+3. Open PDF in Adobe Acrobat Reader
+4. Go to Adobe Acrobat Reader preferences and set it to not use subpixel
+   anti-aliasing, just normal anti-aliasing. They used to call this LCD vs
+   Monitor mode.
+5. Zoom in/out until you get the desired size in pixels, take a
+   screen shot, crop it in some image editor, save it as PNG.
+   
+Converting to transparent but somewhat ugly PNG:
+1. Convert to SVG as described earlier
+2. Use Apache Batik Rasterizer command line utility like:
+   $BARIK_INSTALLATION\batik-rasterizer-1.8.jar -dpi 72 -m image/png ${FIGURE}.svg
+   If Batik fails (as it doesn't support all SVG features), use Inkscape.
+   Of course avoid supixel anti-aliasing, as it's not device independent.

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/manual/en_US/docgen-originals/figures/overview.odg
----------------------------------------------------------------------
diff --git a/freemarker-core/src/manual/en_US/docgen-originals/figures/overview.odg b/freemarker-core/src/manual/en_US/docgen-originals/figures/overview.odg
new file mode 100644
index 0000000..0533b7c
Binary files /dev/null and b/freemarker-core/src/manual/en_US/docgen-originals/figures/overview.odg differ

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/manual/en_US/docgen-originals/figures/tree_with_alpha.png
----------------------------------------------------------------------
diff --git a/freemarker-core/src/manual/en_US/docgen-originals/figures/tree_with_alpha.png b/freemarker-core/src/manual/en_US/docgen-originals/figures/tree_with_alpha.png
new file mode 100644
index 0000000..dc4fba8
Binary files /dev/null and b/freemarker-core/src/manual/en_US/docgen-originals/figures/tree_with_alpha.png differ

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/manual/en_US/docgen.cjson
----------------------------------------------------------------------
diff --git a/freemarker-core/src/manual/en_US/docgen.cjson b/freemarker-core/src/manual/en_US/docgen.cjson
new file mode 100644
index 0000000..076e8f3
--- /dev/null
+++ b/freemarker-core/src/manual/en_US/docgen.cjson
@@ -0,0 +1,132 @@
+//charset: UTF-8
+
+// 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.
+
+deployUrl: "http://freemarker.org/docs/"
+onlineTrackerHTML: "docgen-misc/googleAnalytics.html"
+searchKey: "003127866208504630097:arjqbv_znfw"
+validation: {
+  programlistingsRequireRole
+  // programlistingsRequireLanguage
+  maximumProgramlistingWidth: 100
+}
+showXXELogo
+generateEclipseTOC
+// eclipse: {
+//  link_to: "freemarker-toc.xml#ManualLink"
+// }
+
+removeNodesWhenOnline: [ "preface" ]
+
+copyrightHolder: "The Apache Software Foundation"
+copyrightHolderSite: "http://apache.org/"
+copyrightSuffix: "Apache FreeMarker, FreeMarker, Apache Incubator, Apache, the Apache FreeMarker logo are trademarks of The Apache Software Foundation."
+copyrightStartYear: 1999
+copyrightCommentFile: "docgen-misc/copyrightComment.txt"
+
+seoMeta: {
+  "dgui_quickstart": {
+    "title": "Getting Started with template writing"
+  }
+  "pgui_quickstart": {
+    "title": "Getting Started with the Java API"
+  }
+}
+
+logo: {
+  href: "http://freemarker.org"
+  src: logo.png,
+  alt: "FreeMarker"
+}
+
+olinks: {
+  homepage: "http://freemarker.org/"
+  api: "api/index.html"
+  
+  // Homepage links:
+  freemarkerdownload: "http://freemarker.org/freemarkerdownload.html"
+  contribute: "http://freemarker.org/contribute.html"
+  history: "http://freemarker.org/history.html"
+  what-is-freemarker: "http://freemarker.org/"
+  mailing-lists: "http://freemarker.org/mailing-lists.html"
+  
+  // External URL-s:
+  onlineTemplateTester: "http://freemarker-online.kenshoo.com/"
+  twitter: "https://twitter.com/freemarker"
+  sourceforgeProject: "https://sourceforge.net/projects/freemarker/"
+  githubProject: "https://github.com/freemarker/freemarker"
+  newBugReport: "https://issues.apache.org/jira/browse/FREEMARKER/"
+  newStackOverflowQuestion: "http://stackoverflow.com/questions/ask?tags=freemarker"
+}
+
+internalBookmarks: {
+  "Alpha. index": alphaidx
+  "Glossary": gloss
+  "Expressions": exp_cheatsheet
+  "?builtins": ref_builtins_alphaidx
+  "#directives": ref_directive_alphaidx
+  ".spec_vars": ref_specvar
+  "FAQ": app_faq
+}
+
+tabs: {
+  "Home": "olink:homepage"
+  "Manual": ""  // Empty => We are here
+  "Java API": "olink:api"
+}
+
+// Available icons:
+// .icon-heart
+// .icon-bug
+// .icon-download
+// .icon-star
+secondaryTabs: {
+  "Contribute": { class: "icon-heart", href: "olink:contribute" }
+  "Report a Bug": { class: "icon-bug", href: "olink:newBugReport" }
+  "Download": { class: "icon-download", href: "olink:freemarkerdownload" }
+}
+
+footerSiteMap: {
+  "Overview": {
+    "What is FreeMarker?": "olink:what-is-freemarker"
+    "Download": "olink:freemarkerdownload"
+    "Version history": "id:app_versions"
+    "About us": "olink:history"
+    "License": "id:app_license"
+  }
+  "Handy stuff": {
+    "Try template online": "olink:onlineTemplateTester"
+    "Expressions cheatsheet": "id:exp_cheatsheet"
+    "#directives": "id:ref_directive_alphaidx"
+    "?built_ins": "id:ref_builtins_alphaidx"
+    ".special_vars": "id:ref_specvar"
+  }
+  "Community": {
+    "FreeMarker on Github": "olink:githubProject"
+    "Follow us on Twitter": "olink:twitter"
+    "Report a bug": "olink:newBugReport"
+    "Ask a question": "olink:newStackOverflowQuestion"
+    "Mailing lists": "olink:mailing-lists"
+  }
+}
+
+socialLinks: {
+  "Github": { class: "github", href: "olink:githubProject" }
+  "Twitter": { class: "twitter", href: "olink:twitter" }
+  "Stack Overflow": { class: "stack-overflow", href: "olink:newStackOverflowQuestion" }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/manual/en_US/favicon.png
----------------------------------------------------------------------
diff --git a/freemarker-core/src/manual/en_US/favicon.png b/freemarker-core/src/manual/en_US/favicon.png
new file mode 100644
index 0000000..ce0de20
Binary files /dev/null and b/freemarker-core/src/manual/en_US/favicon.png differ

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/manual/en_US/figures/model2sketch.png
----------------------------------------------------------------------
diff --git a/freemarker-core/src/manual/en_US/figures/model2sketch.png b/freemarker-core/src/manual/en_US/figures/model2sketch.png
new file mode 100644
index 0000000..93f9a6b
Binary files /dev/null and b/freemarker-core/src/manual/en_US/figures/model2sketch.png differ

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/manual/en_US/figures/overview.png
----------------------------------------------------------------------
diff --git a/freemarker-core/src/manual/en_US/figures/overview.png b/freemarker-core/src/manual/en_US/figures/overview.png
new file mode 100644
index 0000000..b32e0bd
Binary files /dev/null and b/freemarker-core/src/manual/en_US/figures/overview.png differ

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/manual/en_US/figures/tree.png
----------------------------------------------------------------------
diff --git a/freemarker-core/src/manual/en_US/figures/tree.png b/freemarker-core/src/manual/en_US/figures/tree.png
new file mode 100644
index 0000000..dcd9bf3
Binary files /dev/null and b/freemarker-core/src/manual/en_US/figures/tree.png differ

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/manual/en_US/logo.png
----------------------------------------------------------------------
diff --git a/freemarker-core/src/manual/en_US/logo.png b/freemarker-core/src/manual/en_US/logo.png
new file mode 100644
index 0000000..193dc11
Binary files /dev/null and b/freemarker-core/src/manual/en_US/logo.png differ

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/manual/zh_CN/book.xml
----------------------------------------------------------------------
diff --git a/freemarker-core/src/manual/zh_CN/book.xml b/freemarker-core/src/manual/zh_CN/book.xml
new file mode 100644
index 0000000..c26677f
--- /dev/null
+++ b/freemarker-core/src/manual/zh_CN/book.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<book conformance="docgen" version="5.0" xml:lang="en"
+      xmlns="http://docbook.org/ns/docbook"
+      xmlns:xlink="http://www.w3.org/1999/xlink"
+      xmlns:xi="http://www.w3.org/2001/XInclude"
+      xmlns:ns5="http://www.w3.org/1999/xhtml"
+      xmlns:ns4="http://www.w3.org/2000/svg"
+      xmlns:ns3="http://www.w3.org/1998/Math/MathML"
+      xmlns:ns="http://docbook.org/ns/docbook">
+  <info>
+    <title>Apache FreeMarker 手册</title>
+
+    <titleabbrev>手册</titleabbrev>
+
+    <productname>Freemarker 3.0.0</productname>
+  </info>
+
+  <preface role="index.html" xml:id="preface">
+    <title>TODO</title>
+
+    <para>TODO... Eventually, we might copy the FM2 Manual and rework
+    it.</para>
+
+    <para>Anchors to satisfy Docgen:</para>
+
+    <itemizedlist>
+      <listitem>
+        <para xml:id="app_versions">app_versions</para>
+      </listitem>
+
+      <listitem>
+        <para xml:id="app_license">app_license</para>
+      </listitem>
+
+      <listitem>
+        <para xml:id="exp_cheatsheet">exp_cheatsheet</para>
+      </listitem>
+
+      <listitem>
+        <para xml:id="ref_directive_alphaidx">ref_directive_alphaidx</para>
+      </listitem>
+
+      <listitem>
+        <para xml:id="ref_builtins_alphaidx">ref_builtins_alphaidx</para>
+      </listitem>
+
+      <listitem>
+        <para xml:id="ref_specvar">ref_specvar</para>
+      </listitem>
+
+      <listitem>
+        <para xml:id="alphaidx">alphaidx</para>
+      </listitem>
+
+      <listitem>
+        <para xml:id="gloss">gloss</para>
+      </listitem>
+
+      <listitem>
+        <para xml:id="app_faq">app_faq</para>
+      </listitem>
+    </itemizedlist>
+  </preface>
+</book>

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/manual/zh_CN/docgen-help/README
----------------------------------------------------------------------
diff --git a/freemarker-core/src/manual/zh_CN/docgen-help/README b/freemarker-core/src/manual/zh_CN/docgen-help/README
new file mode 100644
index 0000000..6ebc928
--- /dev/null
+++ b/freemarker-core/src/manual/zh_CN/docgen-help/README
@@ -0,0 +1,2 @@
+Put the locale-specific or translated guides to editors here.
+For the non-localized guides see the similar folder of the en_US Manual.
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/manual/zh_CN/docgen-misc/googleAnalytics.html
----------------------------------------------------------------------
diff --git a/freemarker-core/src/manual/zh_CN/docgen-misc/googleAnalytics.html b/freemarker-core/src/manual/zh_CN/docgen-misc/googleAnalytics.html
new file mode 100644
index 0000000..759564e
--- /dev/null
+++ b/freemarker-core/src/manual/zh_CN/docgen-misc/googleAnalytics.html
@@ -0,0 +1,14 @@
+<!--
+  This snippet was generated by Google Analytics.
+  Thus, the standard FreeMarker copyright comment was intentionally omitted.
+  <#DO_NOT_UPDATE_COPYRIGHT>
+-->
+<script>
+  (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
+  (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
+  m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
+  })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
+
+  ga('create', 'UA-55420501-1', 'auto');
+  ga('send', 'pageview');
+</script>

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/manual/zh_CN/docgen-originals/figures/README
----------------------------------------------------------------------
diff --git a/freemarker-core/src/manual/zh_CN/docgen-originals/figures/README b/freemarker-core/src/manual/zh_CN/docgen-originals/figures/README
new file mode 100644
index 0000000..f3a8221
--- /dev/null
+++ b/freemarker-core/src/manual/zh_CN/docgen-originals/figures/README
@@ -0,0 +1,2 @@
+Put the translated originals (sources) of the figures used in the manual here.
+For figures that aren't translated, see the similar folder of the en_US Manual.
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/manual/zh_CN/docgen.cjson
----------------------------------------------------------------------
diff --git a/freemarker-core/src/manual/zh_CN/docgen.cjson b/freemarker-core/src/manual/zh_CN/docgen.cjson
new file mode 100644
index 0000000..ecff859
--- /dev/null
+++ b/freemarker-core/src/manual/zh_CN/docgen.cjson
@@ -0,0 +1,130 @@
+//charset: UTF-8
+
+// 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.
+
+deployUrl: "http://freemarker.org/docs/"
+onlineTrackerHTML: "docgen-misc/googleAnalytics.html"
+searchKey: "014728049242975963158:8awjt03uofm"
+validation: {
+  programlistingsRequireRole
+  // programlistingsRequireLanguage
+  maximumProgramlistingWidth: 100
+}
+showXXELogo
+generateEclipseTOC
+// eclipse: {
+//  link_to: "freemarker-toc.xml#ManualLink"
+// }
+
+removeNodesWhenOnline: [ "preface" ]
+
+copyrightHolder: "The Apache Software Foundation"
+copyrightStartYear: 1999
+copyrightCommentFile: "../en_US/docgen-misc/copyrightComment.txt"
+
+seoMeta: {
+  "dgui_quickstart": {
+    "title": "Getting Started with template writing"
+  }
+  "pgui_quickstart": {
+    "title": "Getting Started with the Java API"
+  }
+}
+
+logo: {
+  href: "http://freemarker.org"
+  src: logo.png,
+  alt: "FreeMarker"
+}
+
+olinks: {
+  homepage: "http://freemarker.org/"
+  api: "api/index.html"
+  
+  // Homepage links:
+  freemarkerdownload: "http://freemarker.org/freemarkerdownload.html"
+  contribute: "http://freemarker.org/contribute.html"
+  history: "http://freemarker.org/history.html"
+  what-is-freemarker: "http://freemarker.org/"
+  mailing-lists: "http://freemarker.org/mailing-lists.html"
+  
+  // External URL-s:
+  onlineTemplateTester: "http://freemarker-online.kenshoo.com/"
+  twitter: "https://twitter.com/freemarker"
+  sourceforgeProject: "https://sourceforge.net/projects/freemarker/"
+  githubProject: "https://github.com/freemarker/freemarker"
+  newBugReport: "https://sourceforge.net/p/freemarker/bugs/new/"
+  newStackOverflowQuestion: "http://stackoverflow.com/questions/ask?tags=freemarker"
+}
+
+internalBookmarks: {
+  "Alpha. index": alphaidx
+  "Glossary": gloss
+  "Expressions": exp_cheatsheet
+  "?builtins": ref_builtins_alphaidx
+  "#directives": ref_directive_alphaidx
+  ".spec_vars": ref_specvar
+  "FAQ": app_faq
+}
+
+tabs: {
+  "Home": "olink:homepage"
+  "Manual": ""  // Empty => We are here
+  "Java API": "olink:api"
+}
+
+// Available icons:
+// .icon-heart
+// .icon-bug
+// .icon-download
+// .icon-star
+secondaryTabs: {
+  "Contribute": { class: "icon-heart", href: "olink:contribute" }
+  "Report a Bug": { class: "icon-bug", href: "olink:newBugReport" }
+  "Download": { class: "icon-download", href: "olink:freemarkerdownload" }
+}
+
+footerSiteMap: {
+  "Overview": {
+    "What is FreeMarker?": "olink:what-is-freemarker"
+    "Download": "olink:freemarkerdownload"
+    "Version history": "id:app_versions"
+    "About us": "olink:history"
+    "License": "id:app_license"
+  }
+  "Handy stuff": {
+    "Try template online": "olink:onlineTemplateTester"
+    "Expressions cheatsheet": "id:exp_cheatsheet"
+    "#directives": "id:ref_directive_alphaidx"
+    "?built_ins": "id:ref_builtins_alphaidx"
+    ".special_vars": "id:ref_specvar"
+  }
+  "Community": {
+    "FreeMarker on Github": "olink:githubProject"
+    "Follow us on Twitter": "olink:twitter"
+    "Report a bug": "olink:newBugReport"
+    "Ask a question": "olink:newStackOverflowQuestion"
+    "Mailing lists": "olink:mailing-lists"
+  }
+}
+
+socialLinks: {
+  "Github": { class: "github", href: "olink:githubProject" }
+  "Twitter": { class: "twitter", href: "olink:twitter" }
+  "Stack Overflow": { class: "stack-overflow", href: "olink:newStackOverflowQuestion" }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/manual/zh_CN/favicon.png
----------------------------------------------------------------------
diff --git a/freemarker-core/src/manual/zh_CN/favicon.png b/freemarker-core/src/manual/zh_CN/favicon.png
new file mode 100644
index 0000000..ce0de20
Binary files /dev/null and b/freemarker-core/src/manual/zh_CN/favicon.png differ

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/manual/zh_CN/figures/model2sketch.png
----------------------------------------------------------------------
diff --git a/freemarker-core/src/manual/zh_CN/figures/model2sketch.png b/freemarker-core/src/manual/zh_CN/figures/model2sketch.png
new file mode 100644
index 0000000..93f9a6b
Binary files /dev/null and b/freemarker-core/src/manual/zh_CN/figures/model2sketch.png differ

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/manual/zh_CN/figures/overview.png
----------------------------------------------------------------------
diff --git a/freemarker-core/src/manual/zh_CN/figures/overview.png b/freemarker-core/src/manual/zh_CN/figures/overview.png
new file mode 100644
index 0000000..b32e0bd
Binary files /dev/null and b/freemarker-core/src/manual/zh_CN/figures/overview.png differ

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/manual/zh_CN/figures/tree.png
----------------------------------------------------------------------
diff --git a/freemarker-core/src/manual/zh_CN/figures/tree.png b/freemarker-core/src/manual/zh_CN/figures/tree.png
new file mode 100644
index 0000000..dcd9bf3
Binary files /dev/null and b/freemarker-core/src/manual/zh_CN/figures/tree.png differ

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/manual/zh_CN/logo.png
----------------------------------------------------------------------
diff --git a/freemarker-core/src/manual/zh_CN/logo.png b/freemarker-core/src/manual/zh_CN/logo.png
new file mode 100644
index 0000000..193dc11
Binary files /dev/null and b/freemarker-core/src/manual/zh_CN/logo.png differ

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/ASTBasedErrorMessagesTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/ASTBasedErrorMessagesTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/ASTBasedErrorMessagesTest.java
new file mode 100644
index 0000000..10d63b3
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/ASTBasedErrorMessagesTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.util.Map;
+
+import org.apache.freemarker.test.TemplateTest;
+import org.junit.Test;
+
+public class ASTBasedErrorMessagesTest extends TemplateTest {
+    
+    @Test
+    public void testInvalidRefBasic() {
+        assertErrorContains("${foo}", "foo", "specify a default");
+        assertErrorContains("${map[foo]}", "foo", "\\!map[", "specify a default");
+    }
+    
+    @Test
+    public void testInvalidRefDollar() {
+        assertErrorContains("${$x}", "$x", "must not start with \"$\"", "specify a default");
+        assertErrorContains("${map.$x}", "map.$x", "must not start with \"$\"", "specify a default");
+    }
+
+    @Test
+    public void testInvalidRefAfterDot() {
+        assertErrorContains("${map.foo.bar}", "map.foo", "\\!foo.bar", "after the last dot", "specify a default");
+    }
+
+    @Test
+    public void testInvalidRefInSquareBrackets() {
+        assertErrorContains("${map['foo']}", "map", "final [] step", "specify a default");
+    }
+
+    @Test
+    public void testInvalidRefSize() {
+        assertErrorContains("${map.size()}", "map.size", "?size", "specify a default");
+        assertErrorContains("${map.length()}", "map.length", "?length", "specify a default");
+    }
+
+    @Override
+    protected Object createDataModel() {
+        Map<String, Object> dataModel = createCommonTestValuesDataModel();
+        dataModel.put("overloads", new Overloads());
+        return dataModel;
+    }
+    
+    public static class Overloads {
+        
+        @SuppressWarnings("unused")
+        public void m(String s) {}
+        
+        @SuppressWarnings("unused")
+        public void m(int i) {}
+        
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/ASTPrinter.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/ASTPrinter.java b/freemarker-core/src/test/java/org/apache/freemarker/core/ASTPrinter.java
new file mode 100644
index 0000000..3518b29
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/ASTPrinter.java
@@ -0,0 +1,438 @@
+/*
+ * 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.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.CodingErrorAction;
+import java.nio.charset.StandardCharsets;
+import java.util.Enumeration;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.util.FTLUtil;
+import org.apache.freemarker.core.util._ClassUtil;
+import org.apache.freemarker.core.util._StringUtil;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+
+/**
+ * Static methods and command-line tool for printing the AST of a template. 
+ */
+public class ASTPrinter {
+
+    private final Configuration cfg;
+    private int successfulCounter;
+    private int failedCounter;
+    
+    static public void main(String[] args) throws IOException {
+        if (args.length == 0) {
+            usage();
+            System.exit(-1);
+        }
+        
+        ASTPrinter astp = new ASTPrinter(); 
+        if (args[0].equalsIgnoreCase("-r")) {
+            astp.mainRecursive(args);
+        } else {
+            astp.mainSingleTemplate(args);
+        }
+    }
+    
+    private ASTPrinter() {
+        cfg = new TestConfigurationBuilder(Configuration.VERSION_3_0_0).build();
+    }
+    
+    private void mainSingleTemplate(String[] args) throws IOException, FileNotFoundException {
+        final String templateFileName;
+        final String templateContent;
+        if (args[0].startsWith("ftl:")) {
+            templateFileName = null;
+            templateContent = args[0];
+        } else {
+            templateFileName = args[0];
+            templateContent = null;
+        }
+        
+        Template t = new Template(
+                templateFileName,
+                templateFileName == null ? new StringReader(templateContent) : new FileReader(templateFileName),
+                cfg);
+        
+        p(getASTAsString(t));
+    }
+
+    private void mainRecursive(String[] args) throws IOException {
+        if (args.length != 4) {
+            p("Number of arguments must be 4, but was: " + args.length);
+            usage();
+            System.exit(-1);
+        }
+        
+        final String srcDirPath = args[1].trim();
+        File srcDir = new File(srcDirPath);
+        if (!srcDir.isDirectory()) {
+            p("This should be an existing directory: " + srcDirPath);
+            System.exit(-1);
+        }
+        
+        Pattern fnPattern;
+        try {
+            fnPattern = Pattern.compile(args[2]);
+        } catch (PatternSyntaxException e) {
+            p(_StringUtil.jQuote(args[2]) + " is not a valid regular expression");
+            System.exit(-1);
+            return;
+        }
+        
+        final String dstDirPath = args[3].trim();
+        File dstDir = new File(dstDirPath);
+        if (!dstDir.isDirectory()) {
+            p("This should be an existing directory: " + dstDirPath);
+            System.exit(-1);
+        }
+        
+        long startTime = System.currentTimeMillis();
+        recurse(srcDir, fnPattern, dstDir);
+        long endTime = System.currentTimeMillis();
+        
+        p("Templates successfully processed " + successfulCounter + ", failed " + failedCounter
+                + ". Time taken: " + (endTime - startTime) / 1000.0 + " s");
+    }
+    
+    private void recurse(File srcDir, Pattern fnPattern, File dstDir) throws IOException {
+        File[] files = srcDir.listFiles();
+        if (files == null) {
+            throw new IOException("Failed to kust directory: " + srcDir);
+        }
+        for (File file : files) {
+            if (file.isDirectory()) {
+                recurse(file, fnPattern, new File(dstDir, file.getName()));
+            } else {
+                if (fnPattern.matcher(file.getName()).matches()) {
+                    File dstFile = new File(dstDir, file.getName());
+                    String res;
+                    try {
+                        Template t = new Template(file.getPath().replace('\\', '/'), loadIntoString(file), cfg);
+                        res = getASTAsString(t);
+                        successfulCounter++;
+                    } catch (ParseException e) {
+                        res = "<<<FAILED>>>\n" + e.getMessage();
+                        failedCounter++;
+                        p("");
+                        p("-------------------------failed-------------------------");
+                        p("Error message was saved into: " + dstFile.getAbsolutePath());
+                        p("");
+                        p(e.getMessage());
+                    }
+                    save(res, dstFile);
+                }
+            }
+        }
+    }
+
+    private String loadIntoString(File file) throws IOException {
+        long ln = file.length();
+        if (ln < 0) {
+            throw new IOException("Failed to get the length of " + file);
+        }
+        byte[] buffer = new byte[(int) ln];
+        InputStream in = new FileInputStream(file);
+        try {
+            int offset = 0;
+            int bytesRead;
+            while (offset < buffer.length) {
+                bytesRead = in.read(buffer, offset, buffer.length - offset);
+                if (bytesRead == -1) {
+                    throw new IOException("Unexpected end of file: " + file);
+                }
+                offset += bytesRead;
+            }
+        } finally {
+            in.close();
+        }
+        
+        try {
+            return decode(buffer, StandardCharsets.UTF_8);
+        } catch (CharacterCodingException e) {
+            return decode(buffer, StandardCharsets.ISO_8859_1);
+        }
+    }
+
+    private String decode(byte[] buffer, Charset charset) throws CharacterCodingException {
+        return charset.newDecoder()
+                .onMalformedInput(CodingErrorAction.REPORT).onUnmappableCharacter(CodingErrorAction.REPORT)
+                .decode(ByteBuffer.wrap(buffer)).toString();
+    }
+
+    private void save(String astStr, File file) throws IOException {
+        File parentDir = file.getParentFile();
+        if (!parentDir.isDirectory() && !parentDir.mkdirs()) {
+            throw new IOException("Failed to invoke parent directory: " + parentDir);
+        }
+        
+        Writer w = new BufferedWriter(new FileWriter(file));
+        try {
+            w.write(astStr);
+        } finally {
+            w.close();
+        }
+    }
+
+    private static void usage() {
+        p("Prints template Abstract Syntax Tree (AST) as plain text.");
+        p("Usage:");
+        p("    java org.apache.freemarker.core.PrintAST <templateFile>");
+        p("    java org.apache.freemarker.core.PrintAST ftl:<templateSource>");
+        p("    java org.apache.freemarker.core.PrintAST -r <src-directory> <regexp> <dst-directory>");
+    }
+
+    private static final String INDENTATION = "    ";
+
+    public static String getASTAsString(String ftl) throws IOException {
+        return getASTAsString(ftl, (Options) null);
+    }
+    
+    public static String getASTAsString(String ftl, Options opts) throws IOException {
+        return getASTAsString(null, ftl, opts);
+    }
+
+    public static String getASTAsString(String templateName, String ftl) throws IOException {
+        return getASTAsString(templateName, ftl, null);
+    }
+    
+    public static String getASTAsString(String templateName, String ftl, Options opts) throws IOException {
+        Template t = new Template(templateName, ftl, new TestConfigurationBuilder().build());
+        return getASTAsString(t, opts);
+    }
+
+    public static String getASTAsString(Template t) throws IOException {
+        return getASTAsString(t, null);
+    }
+
+    public static String getASTAsString(Template t, Options opts) throws IOException {
+        validateAST(t);
+        
+        StringWriter out = new StringWriter();
+        printNode(t.getRootASTNode(), "", null, opts != null ? opts : Options.DEFAULT_INSTANCE, out);
+        return out.toString();
+    }
+    
+    public static void validateAST(Template t) throws InvalidASTException {
+        final ASTElement node = t.getRootASTNode();
+        if (node.getParent() != null) {
+            throw new InvalidASTException("Root node parent must be null."
+                    + "\nRoot node: " + node.dump(false)
+                    + "\nParent"
+                    + ": " + node.getParent().getClass() + ", " + node.getParent().dump(false));
+        }
+        validateAST(node);
+    }
+
+    private static void validateAST(ASTElement te) {
+        int childCount = te.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            ASTElement child = te.getChild(i);
+            ASTElement parentElement = child.getParent();
+            // As ASTImplicitParent.accept does nothing but returns its children, it's optimized out in the final
+            // AST tree. While it will be present as a child, the parent element also will have children
+            // that contains the children of the ASTImplicitParent directly. 
+            if (parentElement instanceof ASTImplicitParent && parentElement.getParent() != null) {
+                parentElement = parentElement.getParent();
+            }
+            if (parentElement != te) {
+                throw new InvalidASTException("Wrong parent node."
+                        + "\nNode: " + child.dump(false)
+                        + "\nExpected parent: " + te.dump(false)
+                        + "\nActual parent: " + parentElement.dump(false));
+            }
+            if (child.getIndex() != i) {
+                throw new InvalidASTException("Wrong node index."
+                        + "\nNode: " + child.dump(false)
+                        + "\nExpected index: " + i
+                        + "\nActual index: " + child.getIndex());
+            }
+        }
+        if (te instanceof ASTImplicitParent && te.getChildCount() < 2) {
+            throw new InvalidASTException("Mixed content with child count less than 2 should removed by optimizatoin, "
+                    + "but found one with " + te.getChildCount() + " child(ren).");
+        }
+        ASTElement[] children = te.getChildBuffer();
+        if (children != null) {
+            if (childCount == 0) {
+                throw new InvalidASTException(
+                        "Children must be null when childCount is 0."
+                        + "\nNode: " + te.dump(false));
+            }
+            for (int i = 0; i < te.getChildCount(); i++) {
+                if (children[i] == null) {
+                    throw new InvalidASTException(
+                            "Child can't be null at index " + i
+                            + "\nNode: " + te.dump(false));
+                }
+            }
+            for (int i = te.getChildCount(); i < children.length; i++) {
+                if (children[i] != null) {
+                    throw new InvalidASTException(
+                            "Children can't be non-null at index " + i
+                            + "\nNode: " + te.dump(false));
+                }
+            }
+        } else {
+            if (childCount != 0) {
+                throw new InvalidASTException(
+                        "Children mustn't be null when child count isn't 0."
+                        + "\nNode: " + te.dump(false));
+            }
+        }
+    }
+
+    private static void printNode(Object node, String ind, ParameterRole paramRole, Options opts, Writer out) throws IOException {
+        if (node instanceof ASTNode) {
+            ASTNode tObj = (ASTNode) node;
+
+            printNodeLineStart(paramRole, ind, out);
+            out.write(tObj.getNodeTypeSymbol());
+            printNodeLineEnd(node, out, opts);
+            
+            if (opts.getShowConstantValue() && node instanceof ASTExpression) {
+                TemplateModel tm = ((ASTExpression) node).constantValue;
+                if (tm != null) {
+                    out.write(INDENTATION);
+                    out.write(ind);
+                    out.write("= const ");
+                    out.write(FTLUtil.getTypeDescription(tm));
+                    out.write(' ');
+                    out.write(tm.toString());
+                    out.write('\n');
+                }
+            }
+            
+            int paramCnt = tObj.getParameterCount();
+            for (int i = 0; i < paramCnt; i++) {
+                ParameterRole role = tObj.getParameterRole(i);
+                if (role == null) throw new NullPointerException("parameter role");
+                Object value = tObj.getParameterValue(i);
+                printNode(value, ind + INDENTATION, role, opts, out);
+            }
+            if (tObj instanceof ASTElement) {
+                Enumeration enu = ((ASTElement) tObj).children();
+                while (enu.hasMoreElements()) {
+                    printNode(enu.nextElement(), INDENTATION + ind, null, opts, out);
+                }
+            }
+        } else {
+            printNodeLineStart(paramRole, ind, out);
+            out.write(_StringUtil.jQuote(node));
+            printNodeLineEnd(node, out, opts);
+        }
+    }
+
+    protected static void printNodeLineEnd(Object node, Writer out, Options opts) throws IOException {
+        boolean commentStared = false;
+        if (opts.getShowJavaClass()) {
+            out.write("  // ");
+            commentStared = true;
+            out.write(_ClassUtil.getShortClassNameOfObject(node, true));
+        }
+        if (opts.getShowLocation() && node instanceof ASTNode) {
+            if (!commentStared) {
+                out.write("  // ");
+                commentStared = true;
+            } else {
+                out.write("; ");
+            }
+            ASTNode tObj = (ASTNode) node;
+            out.write("Location " + tObj.beginLine + ":" + tObj.beginColumn + "-" + tObj.endLine + ":" + tObj.endColumn);
+        }
+        out.write('\n');
+    }
+
+    private static void printNodeLineStart(ParameterRole paramRole, String ind, Writer out) throws IOException {
+        out.write(ind);
+        if (paramRole != null) {
+            out.write("- ");
+            out.write(paramRole.toString());
+            out.write(": ");
+        }
+    }
+    
+    public static class Options {
+        
+        private final static Options DEFAULT_INSTANCE = new Options(); 
+        
+        private boolean showJavaClass = true;
+        private boolean showConstantValue = false;
+        private boolean showLocation = false;
+        
+        public boolean getShowJavaClass() {
+            return showJavaClass;
+        }
+        
+        public void setShowJavaClass(boolean showJavaClass) {
+            this.showJavaClass = showJavaClass;
+        }
+        
+        public boolean getShowConstantValue() {
+            return showConstantValue;
+        }
+        
+        public void setShowConstantValue(boolean showConstantValue) {
+            this.showConstantValue = showConstantValue;
+        }
+
+        public boolean getShowLocation() {
+            return showLocation;
+        }
+
+        public void setShowLocation(boolean showLocation) {
+            this.showLocation = showLocation;
+        }
+        
+    }
+    
+    private static void p(Object obj) {
+        System.out.println(obj);
+    }
+
+    public static class InvalidASTException extends RuntimeException {
+
+        public InvalidASTException(String message, Throwable cause) {
+            super(message, cause);
+        }
+
+        public InvalidASTException(String message) {
+            super(message);
+        }
+        
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/ASTTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/ASTTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/ASTTest.java
new file mode 100644
index 0000000..96f173a
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/ASTTest.java
@@ -0,0 +1,103 @@
+/*
+ * 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.FileNotFoundException;
+import java.io.IOException;
+
+import org.apache.freemarker.core.ASTPrinter.Options;
+import org.apache.freemarker.core.util._StringUtil;
+import org.apache.freemarker.test.util.FileTestCase;
+import org.apache.freemarker.test.TestUtil;
+
+public class ASTTest extends FileTestCase {
+
+    public ASTTest(String name) {
+        super(name);
+    }
+    
+    public void test1() throws Exception {
+        testAST("ast-1");
+    }
+
+    public void testRange() throws Exception {
+        testAST("ast-range");
+    }
+    
+    public void testAssignments() throws Exception {
+        testAST("ast-assignments");
+    }
+    
+    public void testBuiltins() throws Exception {
+        testAST("ast-builtins");
+    }
+    
+    public void testStringLiteralInterpolation() throws Exception {
+        testAST("ast-strlitinterpolation");
+    }
+    
+    public void testWhitespaceStripping() throws Exception {
+        testAST("ast-whitespacestripping");
+    }
+
+    public void testMixedContentSimplifications() throws Exception {
+        testAST("ast-mixedcontentsimplifications");
+    }
+
+    public void testMultipleIgnoredChildren() throws Exception {
+        testAST("ast-multipleignoredchildren");
+    }
+    
+    public void testNestedIgnoredChildren() throws Exception {
+        testAST("ast-nestedignoredchildren");
+    }
+
+    public void testLocations() throws Exception {
+        testASTWithLocations("ast-locations");
+    }
+    
+    private void testAST(String testName) throws FileNotFoundException, IOException {
+        testAST(testName, null);
+    }
+
+    private void testASTWithLocations(String testName) throws FileNotFoundException, IOException {
+        Options options = new Options();
+        options.setShowLocation(true);
+        testAST(testName, options);
+    }
+
+    private void testAST(String testName, Options ops) throws FileNotFoundException, IOException {
+        final String templateName = testName + ".ftl";
+        assertExpectedFileEqualsString(
+                testName + ".ast",
+                ASTPrinter.getASTAsString(templateName,
+                        TestUtil.removeFTLCopyrightComment(
+                                normalizeLineBreaks(
+                                        loadTestTextResource(
+                                                getTestFileURL(
+                                                        getExpectedContentFileDirectoryResourcePath(), templateName)))
+                        ), ops));
+    }
+    
+    private String normalizeLineBreaks(final String s) throws FileNotFoundException, IOException {
+        return _StringUtil.replace(s, "\r\n", "\n").replace('\r', '\n');
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/ActualNamingConvetionTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/ActualNamingConvetionTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/ActualNamingConvetionTest.java
new file mode 100644
index 0000000..57e40fa
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/ActualNamingConvetionTest.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Test;
+
+public class ActualNamingConvetionTest {
+    
+    @Test
+    public void testUndetectable() throws IOException {
+        final String ftl = "<#if true>${x?size}</#if>";
+        assertEquals(getActualNamingConvention(ftl,
+                ParsingConfiguration.AUTO_DETECT_NAMING_CONVENTION), ParsingConfiguration.AUTO_DETECT_NAMING_CONVENTION);
+        assertEquals(getActualNamingConvention(ftl,
+                ParsingConfiguration.LEGACY_NAMING_CONVENTION), ParsingConfiguration.LEGACY_NAMING_CONVENTION);
+        assertEquals(getActualNamingConvention(ftl,
+                ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION), ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION);
+    }
+
+    @Test
+    public void testLegacyDetected() throws IOException {
+        final String ftl = "${x?upper_case}";
+        assertEquals(getActualNamingConvention(ftl,
+                ParsingConfiguration.AUTO_DETECT_NAMING_CONVENTION), ParsingConfiguration.LEGACY_NAMING_CONVENTION);
+        assertEquals(getActualNamingConvention(ftl,
+                ParsingConfiguration.LEGACY_NAMING_CONVENTION), ParsingConfiguration.LEGACY_NAMING_CONVENTION);
+    }
+
+    @Test
+    public void testCamelCaseDetected() throws IOException {
+        final String ftl = "${x?upperCase}";
+        assertEquals(getActualNamingConvention(ftl,
+                ParsingConfiguration.AUTO_DETECT_NAMING_CONVENTION), ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION);
+        assertEquals(getActualNamingConvention(ftl,
+                ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION), ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION);
+    }
+
+    private int getActualNamingConvention(String ftl, int namingConvention) throws IOException {
+        return new Template(null, ftl,
+                new TestConfigurationBuilder().namingConvention(namingConvention).build())
+                .getActualNamingConvention();
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/ActualTagSyntaxTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/ActualTagSyntaxTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/ActualTagSyntaxTest.java
new file mode 100644
index 0000000..88f0646
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/ActualTagSyntaxTest.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import static org.apache.freemarker.core.ParsingConfiguration.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Test;
+
+public class ActualTagSyntaxTest {
+
+    @Test
+    public void testWithFtlHeader() throws IOException {
+        testWithFtlHeader(AUTO_DETECT_TAG_SYNTAX);
+        testWithFtlHeader(ANGLE_BRACKET_TAG_SYNTAX);
+        testWithFtlHeader(SQUARE_BRACKET_TAG_SYNTAX);
+    }
+    
+    private void testWithFtlHeader(int cfgTagSyntax) throws IOException {
+        assertEquals(getActualTagSyntax("[#ftl]foo", cfgTagSyntax), SQUARE_BRACKET_TAG_SYNTAX);
+        assertEquals(getActualTagSyntax("<#ftl>foo", cfgTagSyntax), ANGLE_BRACKET_TAG_SYNTAX);
+    }
+    
+    @Test
+    public void testUndecidable() throws IOException {
+        assertEquals(getActualTagSyntax("foo", AUTO_DETECT_TAG_SYNTAX), ANGLE_BRACKET_TAG_SYNTAX);
+        assertEquals(getActualTagSyntax("foo", ANGLE_BRACKET_TAG_SYNTAX), ANGLE_BRACKET_TAG_SYNTAX);
+        assertEquals(getActualTagSyntax("foo", SQUARE_BRACKET_TAG_SYNTAX), SQUARE_BRACKET_TAG_SYNTAX);
+    }
+
+    @Test
+    public void testDecidableWithoutFtlHeader() throws IOException {
+        assertEquals(getActualTagSyntax("foo<#if true></#if>", AUTO_DETECT_TAG_SYNTAX), ANGLE_BRACKET_TAG_SYNTAX);
+        assertEquals(getActualTagSyntax("foo<#if true></#if>", ANGLE_BRACKET_TAG_SYNTAX), ANGLE_BRACKET_TAG_SYNTAX);
+        assertEquals(getActualTagSyntax("foo<#if true></#if>", SQUARE_BRACKET_TAG_SYNTAX), SQUARE_BRACKET_TAG_SYNTAX);
+        
+        assertEquals(getActualTagSyntax("foo[#if true][/#if]", AUTO_DETECT_TAG_SYNTAX), SQUARE_BRACKET_TAG_SYNTAX);
+        assertEquals(getActualTagSyntax("foo[#if true][/#if]", ANGLE_BRACKET_TAG_SYNTAX), ANGLE_BRACKET_TAG_SYNTAX);
+        assertEquals(getActualTagSyntax("foo[#if true][/#if]", SQUARE_BRACKET_TAG_SYNTAX), SQUARE_BRACKET_TAG_SYNTAX);
+    }
+    
+    private int getActualTagSyntax(String ftl, int cfgTagSyntax) throws IOException {
+        return new Template(
+                null, ftl,
+                new TestConfigurationBuilder().tagSyntax(cfgTagSyntax).build()).getActualTagSyntax();
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/BreakPlacementTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/BreakPlacementTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/BreakPlacementTest.java
new file mode 100644
index 0000000..61ba02b
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/BreakPlacementTest.java
@@ -0,0 +1,56 @@
+/*
+ * 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 BreakPlacementTest extends TemplateTest {
+    
+    private static final String BREAK_NESTING_ERROR_MESSAGE_PART = "<#break> must be nested";
+
+    @Test
+    public void testValidPlacements() throws IOException, TemplateException {
+        assertOutput("<#assign x = 1><#switch x><#case 1>one<#break><#case 2>two</#switch>", "one");
+        assertOutput("<#list 1..2 as x>${x}<#break></#list>", "1");
+        assertOutput("<#list 1..2>[<#items as x>${x}<#break></#items>]</#list>", "[1]");
+        assertOutput("<#list 1..2 as x>${x}<#list 1..3>B<#break>E<#items as y></#items></#list>E</#list>.", "1B.");
+        assertOutput("<#list 1..2 as x>${x}<#list 3..4 as x>${x}<#break></#list>;</#list>", "13;23;");
+        assertOutput("<#list [1..2, 3..4, [], 5..6] as xs>[<#list xs as x>${x}<#else><#break></#list>]</#list>.",
+                "[12][34][.");
+        assertOutput("<#list [1..2, 3..4, [], 5..6] as xs>"
+                + "<#list xs>[<#items as x>${x}</#items>]<#else><#break></#list>"
+                + "</#list>.",
+                "[12][34].");
+    }
+
+    @Test
+    public void testInvalidPlacements() throws IOException, TemplateException {
+        assertErrorContains("<#break>", BREAK_NESTING_ERROR_MESSAGE_PART);
+        assertErrorContains("<#list 1..2 as x>${x}</#list><#break>", BREAK_NESTING_ERROR_MESSAGE_PART);
+        assertErrorContains("<#if false><#break></#if>", BREAK_NESTING_ERROR_MESSAGE_PART);
+        assertErrorContains("<#list xs><#break></#list>", BREAK_NESTING_ERROR_MESSAGE_PART);
+        assertErrorContains("<#list 1..2 as x>${x}<#else><#break></#list>", BREAK_NESTING_ERROR_MESSAGE_PART);
+        assertErrorContains("<#list 1..2 as x>${x}<#macro m><#break></#macro></#list>", BREAK_NESTING_ERROR_MESSAGE_PART);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/CamelCaseTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/CamelCaseTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/CamelCaseTest.java
new file mode 100644
index 0000000..95572ad
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/CamelCaseTest.java
@@ -0,0 +1,486 @@
+/*
+ * 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.StandardCharsets;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+import org.apache.freemarker.core.outputformat.impl.HTMLOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.UndefinedOutputFormat;
+import org.apache.freemarker.core.util._StringUtil;
+import org.apache.freemarker.test.TemplateTest;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Test;
+
+public class CamelCaseTest extends TemplateTest {
+
+    @Test
+    public void camelCaseSpecialVars() throws IOException, TemplateException {
+        setConfiguration(new TestConfigurationBuilder()
+                .outputEncoding(StandardCharsets.UTF_8)
+                .urlEscapingCharset(StandardCharsets.ISO_8859_1)
+                .locale(Locale.GERMANY)
+                .build());
+        assertOutput("${.dataModel?isHash?c}", "true");
+        assertOutput("${.data_model?is_hash?c}", "true");
+        assertOutput("${.localeObject.toString()}", "de_DE");
+        assertOutput("${.locale_object.toString()}", "de_DE");
+        assertOutput("${.templateName!'null'}", "null");
+        assertOutput("${.template_name!'null'}", "null");
+        assertOutput("${.currentTemplateName!'null'}", "null");
+        assertOutput("${.current_template_name!'null'}", "null");
+        assertOutput("${.mainTemplateName!'null'}", "null");
+        assertOutput("${.main_template_name!'null'}", "null");
+        assertOutput("${.outputEncoding}", StandardCharsets.UTF_8.name());
+        assertOutput("${.output_encoding}", StandardCharsets.UTF_8.name());
+        assertOutput("${.outputFormat}", UndefinedOutputFormat.INSTANCE.getName());
+        assertOutput("${.output_format}", UndefinedOutputFormat.INSTANCE.getName());
+        assertOutput("${.urlEscapingCharset}", StandardCharsets.ISO_8859_1.name());
+        assertOutput("${.url_escaping_charset}", StandardCharsets.ISO_8859_1.name());
+        assertOutput("${.currentNode!'-'}", "-");
+        assertOutput("${.current_node!'-'}", "-");
+    }
+
+    @Test
+    public void camelCaseSpecialVarsInErrorMessage() throws IOException, TemplateException {
+        assertErrorContains("${.fooBar}", "dataModel", "\\!data_model");
+        assertErrorContains("${.foo_bar}", "data_model", "\\!dataModel");
+        // [2.4] If camel case will be the recommended style, then this need to be inverted:
+        assertErrorContains("${.foo}", "data_model", "\\!dataModel");
+        
+        assertErrorContains("<#if x><#elseIf y></#if>${.foo}", "dataModel", "\\!data_model");
+        assertErrorContains("<#if x><#elseif y></#if>${.foo}", "data_model", "\\!dataModel");
+
+        setConfigurationToCamelCaseNamingConvention();
+        assertErrorContains("${.foo}", "dataModel", "\\!data_model");
+
+        setConfigurationToLegacyCaseNamingConvention();
+        assertErrorContains("${.foo}", "data_model", "\\!dataModel");
+    }
+    
+    @Test
+    public void camelCaseSettingNames() throws IOException, TemplateException {
+        assertOutput("<#setting booleanFormat='Y,N'>${true} <#setting booleanFormat='+,-'>${true}", "Y +");
+        assertOutput("<#setting boolean_format='Y,N'>${true} <#setting boolean_format='+,-'>${true}", "Y +");
+        
+        // Still works inside ?interpret
+        assertOutput("<@r\"<#setting booleanFormat='Y,N'>${true}\"?interpret />", "Y");
+    }
+    
+    @Test
+    public void camelCaseFtlHeaderParameters() throws IOException, TemplateException {
+        assertOutput(
+                "<#ftl "
+                + "stripWhitespace=false "
+                + "stripText=true "
+                + "outputFormat='" + HTMLOutputFormat.INSTANCE.getName() + "' "
+                + "autoEsc=true "
+                + "nsPrefixes={} "
+                + ">\nx\n<#if true>\n${.outputFormat}\n</#if>\n",
+                "\nHTML\n");
+
+        assertOutput(
+                "<#ftl "
+                + "strip_whitespace=false "
+                + "strip_text=true "
+                + "output_format='" + HTMLOutputFormat.INSTANCE.getName() + "' "
+                + "auto_esc=true "
+                + "ns_prefixes={} "
+                + ">\nx\n<#if true>\n${.output_format}\n</#if>\n",
+                "\nHTML\n");
+
+        assertErrorContains("<#ftl strip_text=true xmlns={}>", "ns_prefixes", "\\!nsPrefixes");
+        assertErrorContains("<#ftl stripText=true xmlns={}>", "nsPrefixes");
+        
+        assertErrorContains("<#ftl stripWhitespace=true strip_text=true>", "naming convention");
+        assertErrorContains("<#ftl strip_whitespace=true stripText=true>", "naming convention");
+        assertErrorContains("<#ftl stripWhitespace=true>${.foo_bar}", "naming convention");
+        assertErrorContains("<#ftl strip_whitespace=true>${.fooBar}", "naming convention");
+
+        setConfiguration(new TestConfigurationBuilder()
+                .namingConvention(ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION)
+                .outputEncoding(StandardCharsets.UTF_8)
+                .build());
+        assertErrorContains("<#ftl strip_whitespace=true>", "naming convention");
+        assertOutput("<#ftl stripWhitespace=true>${.outputEncoding}", StandardCharsets.UTF_8.name());
+        
+        setConfiguration(new TestConfigurationBuilder()
+                .namingConvention(ParsingConfiguration.LEGACY_NAMING_CONVENTION)
+                .outputEncoding(StandardCharsets.UTF_8)
+                .build());
+        assertErrorContains("<#ftl stripWhitespace=true>", "naming convention");
+        assertOutput("<#ftl strip_whitespace=true>${.output_encoding}", StandardCharsets.UTF_8.name());
+        
+        setConfiguration(new TestConfigurationBuilder()
+                .namingConvention(ParsingConfiguration.AUTO_DETECT_NAMING_CONVENTION)
+                .outputEncoding(StandardCharsets.UTF_8)
+                .build());
+        assertOutput("<#ftl stripWhitespace=true>${.outputEncoding}", StandardCharsets.UTF_8.name());
+        assertOutput("<#ftl encoding='iso-8859-1' stripWhitespace=true>${.outputEncoding}", StandardCharsets.UTF_8.name());
+        assertOutput("<#ftl stripWhitespace=true encoding='iso-8859-1'>${.outputEncoding}", StandardCharsets.UTF_8.name());
+        assertOutput("<#ftl encoding='iso-8859-1' strip_whitespace=true>${.output_encoding}", StandardCharsets.UTF_8.name());
+        assertOutput("<#ftl strip_whitespace=true encoding='iso-8859-1'>${.output_encoding}", StandardCharsets.UTF_8.name());
+    }
+    
+    @Test
+    public void camelCaseSettingNamesInErrorMessages() throws IOException, TemplateException {
+        assertErrorContains("<#setting fooBar=1>", "booleanFormat", "\\!boolean_format");
+        assertErrorContains("<#setting foo_bar=1>", "boolean_format", "\\!booleanFormat");
+        // [2.4] If camel case will be the recommended style, then this need to be inverted:
+        assertErrorContains("<#setting foo=1>", "boolean_format", "\\!booleanFormat");
+
+        assertErrorContains("<#if x><#elseIf y></#if><#setting foo=1>", "booleanFormat", "\\!boolean_format");
+        assertErrorContains("<#if x><#elseif y></#if><#setting foo=1>", "boolean_format", "\\!booleanFormat");
+
+        setConfigurationToCamelCaseNamingConvention();
+        assertErrorContains("<#setting foo=1>", "booleanFormat", "\\!boolean_format");
+
+        setConfigurationToLegacyCaseNamingConvention();
+        assertErrorContains("<#setting foo=1>", "boolean_format", "\\!booleanFormat");
+    }
+    
+    @Test
+    public void camelCaseIncludeParameters() throws IOException, TemplateException {
+        assertOutput("<#ftl stripWhitespace=true>[<#include 'noSuchTemplate' ignoreMissing=true>]", "[]");
+        assertOutput("<#ftl strip_whitespace=true>[<#include 'noSuchTemplate' ignore_missing=true>]", "[]");
+        assertErrorContains("<#ftl stripWhitespace=true>[<#include 'noSuchTemplate' ignore_missing=true>]",
+                "naming convention", "ignore_missing");
+        assertErrorContains("<#ftl strip_whitespace=true>[<#include 'noSuchTemplate' ignoreMissing=true>]",
+                "naming convention", "ignoreMissing");
+    }
+    
+    @Test
+    public void specialVarsHasBothNamingStyle() throws IOException, TemplateException {
+        assertContainsBothNamingStyles(
+                new HashSet(Arrays.asList(ASTExpBuiltInVariable.SPEC_VAR_NAMES)),
+                new NamePairAssertion() { @Override
+                public void assertPair(String name1, String name2) { } });
+    }
+    
+    @Test
+    public void camelCaseBuiltIns() throws IOException, TemplateException {
+        assertOutput("${'x'?upperCase}", "X");
+        assertOutput("${'x'?upper_case}", "X");
+    }
+
+    @Test
+    public void stringLiteralInterpolation() throws IOException, TemplateException {
+        assertEquals(ParsingConfiguration.AUTO_DETECT_NAMING_CONVENTION, getConfiguration().getNamingConvention());
+        addToDataModel("x", "x");
+        
+        assertOutput("${'-${x?upperCase}-'} ${x?upperCase}", "-X- X");
+        assertOutput("${x?upperCase} ${'-${x?upperCase}-'}", "X -X-");
+        assertOutput("${'-${x?upper_case}-'} ${x?upper_case}", "-X- X");
+        assertOutput("${x?upper_case} ${'-${x?upper_case}-'}", "X -X-");
+
+        assertErrorContains("${'-${x?upper_case}-'} ${x?upperCase}",
+                "naming convention", "legacy", "upperCase", "detection", "9");
+        assertErrorContains("${x?upper_case} ${'-${x?upperCase}-'}",
+                "naming convention", "legacy", "upperCase", "detection", "5");
+        assertErrorContains("${'-${x?upperCase}-'} ${x?upper_case}",
+                "naming convention", "camel", "upper_case");
+        assertErrorContains("${x?upperCase} ${'-${x?upper_case}-'}",
+                "naming convention", "camel", "upper_case");
+
+        setConfigurationToCamelCaseNamingConvention();
+        assertOutput("${'-${x?upperCase}-'} ${x?upperCase}", "-X- X");
+        assertErrorContains("${'-${x?upper_case}-'}",
+                "naming convention", "camel", "upper_case", "\\!detection");
+
+        setConfigurationToLegacyCaseNamingConvention();
+        assertOutput("${'-${x?upper_case}-'} ${x?upper_case}", "-X- X");
+        assertErrorContains("${'-${x?upperCase}-'}",
+                "naming convention", "legacy", "upperCase", "\\!detection");
+    }
+    
+    @Test
+    public void evalAndInterpret() throws IOException, TemplateException {
+        assertEquals(ParsingConfiguration.AUTO_DETECT_NAMING_CONVENTION, getConfiguration().getNamingConvention());
+        // The naming convention detected doesn't affect the enclosing template's naming convention.
+        // - ?eval:
+        assertOutput("${\"'x'?upperCase\"?eval}${'x'?upper_case}", "XX");
+        assertOutput("${\"'x'?upper_case\"?eval}${'x'?upperCase}", "XX");
+        assertOutput("${'x'?upperCase}${\"'x'?upper_case\"?eval}", "XX");
+        assertErrorContains("${\"'x'\n?upperCase\n?is_string\"?eval}",
+                "naming convention", "camel", "upperCase", "is_string", "line 2", "line 3");
+        // - ?interpret:
+        assertOutput("<@r\"${'x'?upperCase}\"?interpret />${'x'?upper_case}", "XX");
+        assertOutput("<@r\"${'x'?upper_case}\"?interpret />${'x'?upperCase}", "XX");
+        assertOutput("${'x'?upper_case}<@r\"${'x'?upperCase}\"?interpret />", "XX");
+        assertErrorContains("<@r\"${'x'\n?upperCase\n?is_string}\"?interpret />",
+                "naming convention", "camel", "upperCase", "is_string", "line 2", "line 3");
+        
+        // Will be inherited by ?eval-ed/?interpreted fragments:
+        setConfigurationToCamelCaseNamingConvention();
+        // - ?eval:
+        assertErrorContains("${\"'x'?upper_case\"?eval}", "naming convention", "camel", "upper_case");
+        assertOutput("${\"'x'?upperCase\"?eval}", "X");
+        // - ?interpret:
+        assertErrorContains("<@r\"${'x'?upper_case}\"?interpret />", "naming convention", "camel", "upper_case");
+        assertOutput("<@r\"${'x'?upperCase}\"?interpret />", "X");
+        
+        // Again, will be inherited by ?eval-ed/?interpreted fragments:
+        setConfigurationToLegacyCaseNamingConvention();
+        // - ?eval:
+        assertErrorContains("${\"'x'?upperCase\"?eval}", "naming convention", "legacy", "upperCase");
+        assertOutput("${\"'x'?upper_case\"?eval}", "X");
+        // - ?interpret:
+        assertErrorContains("<@r\"${'x'?upperCase}\"?interpret />", "naming convention", "legacy", "upperCase");
+        assertOutput("<@r\"${'x'?upper_case}\"?interpret />", "X");
+    }
+
+    private void setConfigurationToLegacyCaseNamingConvention() {
+        setConfiguration(new TestConfigurationBuilder()
+                .namingConvention(ParsingConfiguration.LEGACY_NAMING_CONVENTION)
+                .build());
+    }
+
+    @Test
+    public void camelCaseBuiltInErrorMessage() throws IOException, TemplateException {
+        assertErrorContains("${'x'?upperCasw}", "upperCase", "\\!upper_case");
+        assertErrorContains("${'x'?upper_casw}", "upper_case", "\\!upperCase");
+        // [2.4] If camel case will be the recommended style, then this need to be inverted:
+        assertErrorContains("${'x'?foo}", "upper_case", "\\!upperCase");
+        
+        assertErrorContains("<#if x><#elseIf y></#if> ${'x'?foo}", "upperCase", "\\!upper_case");
+        assertErrorContains("<#if x><#elseif y></#if>${'x'?foo}", "upper_case", "\\!upperCase");
+
+        setConfigurationToCamelCaseNamingConvention();
+        assertErrorContains("${'x'?foo}", "upperCase", "\\!upper_case");
+        setConfigurationToLegacyCaseNamingConvention();
+        assertErrorContains("${'x'?foo}", "upper_case", "\\!upperCase");
+    }
+
+    private void setConfigurationToCamelCaseNamingConvention() {
+        setConfiguration(new TestConfigurationBuilder()
+                .namingConvention(ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION)
+                .build());
+    }
+
+    @Test
+    public void builtInsHasBothNamingStyle() throws IOException, TemplateException {
+        assertContainsBothNamingStyles(getConfiguration().getSupportedBuiltInNames(), new NamePairAssertion() {
+
+            @Override
+            public void assertPair(String name1, String name2) {
+                ASTExpBuiltIn bi1  = ASTExpBuiltIn.BUILT_INS_BY_NAME.get(name1);
+                ASTExpBuiltIn bi2 = ASTExpBuiltIn.BUILT_INS_BY_NAME.get(name2);
+                assertTrue("\"" + name1 + "\" and \"" + name2 + "\" doesn't belong to the same BI object.",
+                        bi1 == bi2);
+            }
+            
+        });
+    }
+
+    private void assertContainsBothNamingStyles(Set<String> names, NamePairAssertion namePairAssertion) {
+        Set<String> underscoredNamesWithCamelCasePair = new HashSet<>();
+        for (String name : names) {
+            if (_StringUtil.getIdentifierNamingConvention(name) == ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION) {
+                String underscoredName = correctIsoBIExceptions(_StringUtil.camelCaseToUnderscored(name)); 
+                assertTrue(
+                        "Missing underscored variation \"" + underscoredName + "\" for \"" + name + "\".",
+                        names.contains(underscoredName));
+                assertTrue(underscoredNamesWithCamelCasePair.add(underscoredName));
+                
+                namePairAssertion.assertPair(name, underscoredName);
+            }
+        }
+        for (String name : names) {
+            if (_StringUtil.getIdentifierNamingConvention(name) == ParsingConfiguration.LEGACY_NAMING_CONVENTION) {
+                assertTrue("Missing camel case variation for \"" + name + "\".",
+                        underscoredNamesWithCamelCasePair.contains(name));
+            }
+        }
+    }
+    
+    private String correctIsoBIExceptions(String underscoredName) {
+        return underscoredName.replace("_n_z", "_nz").replace("_f_z", "_fz");
+    }
+    
+    @Test
+    public void camelCaseDirectives() throws IOException, TemplateException {
+        camelCaseDirectives(false);
+        setConfiguration(new TestConfigurationBuilder()
+                .tagSyntax(ParsingConfiguration.AUTO_DETECT_TAG_SYNTAX)
+                .build());
+        camelCaseDirectives(true);
+    }
+
+    private void camelCaseDirectives(boolean squared) throws IOException, TemplateException {
+        assertOutput(
+                squared("<#list 1..4 as x><#if x == 1>one <#elseIf x == 2>two <#elseIf x == 3>three "
+                        + "<#else>more</#if></#list>", squared),
+                "one two three more");
+        assertOutput(
+                squared("<#list 1..4 as x><#if x == 1>one <#elseif x == 2>two <#elseif x == 3>three "
+                        + "<#else>more</#if></#list>", squared),
+                "one two three more");
+        
+        assertOutput(
+                squared("<#escape x as x?upperCase>${'a'}<#noEscape>${'b'}</#noEscape></#escape>", squared),
+                "Ab");
+        assertOutput(
+                squared("<#escape x as x?upper_case>${'a'}<#noescape>${'b'}</#noescape></#escape>", squared),
+                "Ab");
+        
+        assertOutput(
+                squared("<#noParse></#noparse></#noParse>", squared),
+                squared("</#noparse>", squared));
+        assertOutput(
+                squared("<#noparse></#noParse></#noparse>", squared),
+                squared("</#noParse>", squared));
+    }
+    
+    private String squared(String ftl, boolean squared) {
+        return squared ? ftl.replace('<', '[').replace('>', ']') : ftl;
+    }
+
+    @Test
+    public void explicitNamingConvention() throws IOException, TemplateException {
+        explicitNamingConvention(false);
+        explicitNamingConvention(true);
+    }
+    
+    private void explicitNamingConvention(boolean squared) throws IOException, TemplateException {
+        int tagSyntax = squared ? ParsingConfiguration.AUTO_DETECT_TAG_SYNTAX
+                : ParsingConfiguration.ANGLE_BRACKET_TAG_SYNTAX;
+        setConfiguration(new TestConfigurationBuilder()
+                .tagSyntax(tagSyntax)
+                .namingConvention(ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION)
+                .build());
+
+        assertErrorContains(
+                squared("<#if true>t<#elseif false>f</#if>", squared),
+                "naming convention", "camel", "#elseif");
+        assertOutput(
+                squared("<#if true>t<#elseIf false>f</#if>", squared),
+                "t");
+        
+        assertErrorContains(
+                squared("<#noparse>${x}</#noparse>", squared),
+                "naming convention", "camel", "#noparse");
+        assertOutput(
+                squared("<#noParse>${x}</#noParse>", squared),
+                "${x}");
+        
+        assertErrorContains(
+                squared("<#escape x as -x><#noescape>${1}</#noescape></#escape>", squared),
+                "naming convention", "camel", "#noescape");
+        assertOutput(
+                squared("<#escape x as -x><#noEscape>${1}</#noEscape></#escape>", squared),
+                "1");
+
+        // ---
+
+        setConfiguration(new TestConfigurationBuilder()
+                .tagSyntax(tagSyntax)
+                .namingConvention(ParsingConfiguration.LEGACY_NAMING_CONVENTION)
+                .build());
+
+        assertErrorContains(
+                squared("<#if true>t<#elseIf false>f</#if>", squared),
+                "naming convention", "legacy", "#elseIf");
+        assertOutput(
+                squared("<#if true>t<#elseif false>f</#if>", squared),
+                "t");
+        
+        assertErrorContains(
+                squared("<#noParse>${x}</#noParse>", squared),
+                "naming convention", "legacy", "#noParse");
+        assertOutput(
+                squared("<#noparse>${x}</#noparse>", squared),
+                "${x}");
+        
+        assertErrorContains(
+                squared("<#escape x as -x><#noEscape>${1}</#noEscape></#escape>", squared),
+                "naming convention", "legacy", "#noEscape");
+        assertOutput(
+                squared("<#escape x as -x><#noescape>${1}</#noescape></#escape>", squared),
+                "1");
+    }
+    
+    @Test
+    public void inconsistentAutoDetectedNamingConvention() {
+        assertErrorContains(
+                "<#if x><#elseIf y><#elseif z></#if>",
+                "naming convention", "camel");
+        assertErrorContains(
+                "<#if x><#elseif y><#elseIf z></#if>",
+                "naming convention", "legacy");
+        assertErrorContains(
+                "<#if x><#elseIf y></#if><#noparse></#noparse>",
+                "naming convention", "camel");
+        assertErrorContains(
+                "<#if x><#elseif y></#if><#noParse></#noParse>",
+                "naming convention", "legacy");
+        assertErrorContains(
+                "<#if x><#elseif y><#elseIf z></#if>",
+                "naming convention", "legacy");
+        assertErrorContains(
+                "<#escape x as x + 1><#noEscape></#noescape></#escape>",
+                "naming convention", "camel");
+        assertErrorContains(
+                "<#escape x as x + 1><#noEscape></#noEscape><#noescape></#noescape></#escape>",
+                "naming convention", "camel");
+        assertErrorContains(
+                "<#escape x as x + 1><#noescape></#noEscape></#escape>",
+                "naming convention", "legacy");
+        assertErrorContains(
+                "<#escape x as x + 1><#noescape></#noescape><#noEscape></#noEscape></#escape>",
+                "naming convention", "legacy");
+
+        assertErrorContains("${x?upperCase?is_string}",
+                "naming convention", "camel", "upperCase", "is_string");
+        assertErrorContains("${x?upper_case?isString}",
+                "naming convention", "legacy", "upper_case", "isString");
+
+        assertErrorContains("<#setting outputEncoding='utf-8'>${x?is_string}",
+                "naming convention", "camel", "outputEncoding", "is_string");
+        assertErrorContains("<#setting output_encoding='utf-8'>${x?isString}",
+                "naming convention", "legacy", "output_encoding", "isString");
+        
+        assertErrorContains("${x?isString}<#setting output_encoding='utf-8'>",
+                "naming convention", "camel", "isString", "output_encoding");
+        assertErrorContains("${x?is_string}<#setting outputEncoding='utf-8'>",
+                "naming convention", "legacy", "is_string", "outputEncoding");
+        
+        assertErrorContains("${.outputEncoding}${x?is_string}",
+                "naming convention", "camel", "outputEncoding", "is_string");
+        assertErrorContains("${.output_encoding}${x?isString}",
+                "naming convention", "legacy", "output_encoding", "isString");
+        
+        assertErrorContains("${x?upperCase}<#noparse></#noparse>",
+                "naming convention", "camel", "upperCase", "noparse");
+        assertErrorContains("${x?upper_case}<#noParse></#noParse>",
+                "naming convention", "legacy", "upper_case", "noParse");
+    }
+    
+    private interface NamePairAssertion {
+        
+        void assertPair(String name1, String name2);
+        
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/CanonicalFormTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/CanonicalFormTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/CanonicalFormTest.java
new file mode 100644
index 0000000..c78c90e
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/CanonicalFormTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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 org.apache.freemarker.core.templateresolver.impl.ClassTemplateLoader;
+import org.apache.freemarker.test.CopyrightCommentRemoverTemplateLoader;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.apache.freemarker.test.util.FileTestCase;
+
+public class CanonicalFormTest extends FileTestCase {
+
+    public CanonicalFormTest(String name) {
+        super(name);
+    }
+
+    public void testMacrosCanonicalForm() throws Exception {
+        assertCanonicalFormOf("cano-macros.ftl");
+    }
+    
+    public void testIdentifierEscapingCanonicalForm() throws Exception {
+        assertCanonicalFormOf("cano-identifier-escaping.ftl");
+    }
+
+    public void testAssignmentCanonicalForm() throws Exception {
+        assertCanonicalFormOf("cano-assignments.ftl");
+    }
+
+    public void testBuiltInCanonicalForm() throws Exception {
+        assertCanonicalFormOf("cano-builtins.ftl");
+    }
+
+    public void testStringLiteralInterpolationCanonicalForm() throws Exception {
+        assertCanonicalFormOf("cano-strlitinterpolation.ftl");
+    }
+    
+    private void assertCanonicalFormOf(String ftlFileName)
+            throws IOException {
+        Configuration cfg = new TestConfigurationBuilder()
+                .templateLoader(
+                        new CopyrightCommentRemoverTemplateLoader(
+                                new ClassTemplateLoader(CanonicalFormTest.class, "")))
+                .build();
+        StringWriter sw = new StringWriter();
+        cfg.getTemplate(ftlFileName).dump(sw);
+        assertExpectedFileEqualsString(ftlFileName + ".out", sw.toString());
+    }
+
+}


[44/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpHashLiteral.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpHashLiteral.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpHashLiteral.java
new file mode 100644
index 0000000..792787a
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpHashLiteral.java
@@ -0,0 +1,220 @@
+/*
+ * 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.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.ListIterator;
+
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx2;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+import org.apache.freemarker.core.model.impl.CollectionAndSequence;
+
+/**
+ * AST expression node: <tt>{ keyExp: valueExp, ... }</tt> 
+ */
+final class ASTExpHashLiteral extends ASTExpression {
+
+    private final ArrayList keys, values;
+    private final int size;
+
+    ASTExpHashLiteral(ArrayList/*<ASTExpression>*/ keys, ArrayList/*<ASTExpression>*/ values) {
+        this.keys = keys;
+        this.values = values;
+        size = keys.size();
+        keys.trimToSize();
+        values.trimToSize();
+    }
+
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        return new SequenceHash(env);
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        StringBuilder buf = new StringBuilder("{");
+        for (int i = 0; i < size; i++) {
+            ASTExpression key = (ASTExpression) keys.get(i);
+            ASTExpression value = (ASTExpression) values.get(i);
+            buf.append(key.getCanonicalForm());
+            buf.append(": ");
+            buf.append(value.getCanonicalForm());
+            if (i != size - 1) {
+                buf.append(", ");
+            }
+        }
+        buf.append("}");
+        return buf.toString();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "{...}";
+    }
+
+    @Override
+    boolean isLiteral() {
+        if (constantValue != null) {
+            return true;
+        }
+        for (int i = 0; i < size; i++) {
+            ASTExpression key = (ASTExpression) keys.get(i);
+            ASTExpression value = (ASTExpression) values.get(i);
+            if (!key.isLiteral() || !value.isLiteral()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+		ArrayList clonedKeys = (ArrayList) keys.clone();
+		for (ListIterator iter = clonedKeys.listIterator(); iter.hasNext(); ) {
+            iter.set(((ASTExpression) iter.next()).deepCloneWithIdentifierReplaced(
+                    replacedIdentifier, replacement, replacementState));
+        }
+		ArrayList clonedValues = (ArrayList) values.clone();
+		for (ListIterator iter = clonedValues.listIterator(); iter.hasNext(); ) {
+            iter.set(((ASTExpression) iter.next()).deepCloneWithIdentifierReplaced(
+                    replacedIdentifier, replacement, replacementState));
+        }
+    	return new ASTExpHashLiteral(clonedKeys, clonedValues);
+    }
+
+    private class SequenceHash implements TemplateHashModelEx2 {
+
+        private HashMap<String, TemplateModel> map;
+        private TemplateCollectionModel keyCollection, valueCollection; // ordered lists of keys and values
+
+        SequenceHash(Environment env) throws TemplateException {
+            map = new LinkedHashMap<>();
+            for (int i = 0; i < size; i++) {
+                ASTExpression keyExp = (ASTExpression) keys.get(i);
+                ASTExpression valExp = (ASTExpression) values.get(i);
+                String key = keyExp.evalAndCoerceToPlainText(env);
+                TemplateModel value = valExp.eval(env);
+                valExp.assertNonNull(value, env);
+                map.put(key, value);
+            }
+        }
+
+        @Override
+        public int size() {
+            return size;
+        }
+
+        @Override
+        public TemplateCollectionModel keys() {
+            if (keyCollection == null) {
+                keyCollection = new CollectionAndSequence(new NativeStringCollectionCollectionEx(map.keySet()));
+            }
+            return keyCollection;
+        }
+
+        @Override
+        public TemplateCollectionModel values() {
+            if (valueCollection == null) {
+                valueCollection = new CollectionAndSequence(new NativeCollectionEx(map.values()));
+            }
+            return valueCollection;
+        }
+
+        @Override
+        public TemplateModel get(String key) {
+            return map.get(key);
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return size == 0;
+        }
+        
+        @Override
+        public String toString() {
+            return getCanonicalForm();
+        }
+
+        @Override
+        public KeyValuePairIterator keyValuePairIterator() throws TemplateModelException {
+            return new KeyValuePairIterator() {
+                private final TemplateModelIterator keyIterator = keys().iterator();
+                private final TemplateModelIterator valueIterator = values().iterator();
+
+                @Override
+                public boolean hasNext() throws TemplateModelException {
+                    return keyIterator.hasNext();
+                }
+
+                @Override
+                public KeyValuePair next() throws TemplateModelException {
+                    return new KeyValuePair() {
+                        private final TemplateModel key = keyIterator.next();
+                        private final TemplateModel value = valueIterator.next();
+
+                        @Override
+                        public TemplateModel getKey() throws TemplateModelException {
+                            return key;
+                        }
+
+                        @Override
+                        public TemplateModel getValue() throws TemplateModelException {
+                            return value;
+                        }
+                        
+                    };
+                }
+                
+            };
+        }
+        
+    }
+
+    @Override
+    int getParameterCount() {
+        return size * 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        checkIndex(idx);
+        return idx % 2 == 0 ? keys.get(idx / 2) : values.get(idx / 2);
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        checkIndex(idx);
+        return idx % 2 == 0 ? ParameterRole.ITEM_KEY : ParameterRole.ITEM_VALUE;
+    }
+
+    private void checkIndex(int idx) {
+        if (idx >= size * 2) {
+            throw new IndexOutOfBoundsException();
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpListLiteral.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpListLiteral.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpListLiteral.java
new file mode 100644
index 0000000..b3fba1f
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpListLiteral.java
@@ -0,0 +1,195 @@
+/*
+ * 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.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.ListIterator;
+
+import org.apache.freemarker.core.model.TemplateMethodModel;
+import org.apache.freemarker.core.model.TemplateMethodModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+
+/**
+ * AST expression node: {@code [ exp, ... ]} 
+ */
+final class ASTExpListLiteral extends ASTExpression {
+
+    final ArrayList/*<ASTExpression>*/ items;
+
+    ASTExpListLiteral(ArrayList items) {
+        this.items = items;
+        items.trimToSize();
+    }
+
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        NativeSequence list = new NativeSequence(items.size());
+        for (Object item : items) {
+            ASTExpression exp = (ASTExpression) item;
+            TemplateModel tm = exp.eval(env);
+            exp.assertNonNull(tm, env);
+            list.add(tm);
+        }
+        return list;
+    }
+
+    /**
+     * For {@link TemplateMethodModel} calls, but not for {@link TemplateMethodModelEx}-es, returns the list of
+     * arguments as {@link String}-s.
+     */
+    List/*<String>*/ getValueList(Environment env) throws TemplateException {
+        int size = items.size();
+        switch(size) {
+            case 0: {
+                return Collections.EMPTY_LIST;
+            }
+            case 1: {
+                return Collections.singletonList(((ASTExpression) items.get(0)).evalAndCoerceToPlainText(env));
+            }
+            default: {
+                List result = new ArrayList(items.size());
+                for (ListIterator iterator = items.listIterator(); iterator.hasNext(); ) {
+                    ASTExpression exp = (ASTExpression) iterator.next();
+                    result.add(exp.evalAndCoerceToPlainText(env));
+                }
+                return result;
+            }
+        }
+    }
+
+    /**
+     * For {@link TemplateMethodModelEx} calls, returns the list of arguments as {@link TemplateModel}-s.
+     */
+    List/*<TemplateModel>*/ getModelList(Environment env) throws TemplateException {
+        int size = items.size();
+        switch(size) {
+            case 0: {
+                return Collections.EMPTY_LIST;
+            }
+            case 1: {
+                return Collections.singletonList(((ASTExpression) items.get(0)).eval(env));
+            }
+            default: {
+                List result = new ArrayList(items.size());
+                for (ListIterator iterator = items.listIterator(); iterator.hasNext(); ) {
+                    ASTExpression exp = (ASTExpression) iterator.next();
+                    result.add(exp.eval(env));
+                }
+                return result;
+            }
+        }
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        StringBuilder buf = new StringBuilder("[");
+        int size = items.size();
+        for (int i = 0; i < size; i++) {
+            ASTExpression value = (ASTExpression) items.get(i);
+            buf.append(value.getCanonicalForm());
+            if (i != size - 1) {
+                buf.append(", ");
+            }
+        }
+        buf.append("]");
+        return buf.toString();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "[...]";
+    }
+    
+    @Override
+    boolean isLiteral() {
+        if (constantValue != null) {
+            return true;
+        }
+        for (int i = 0; i < items.size(); i++) {
+            ASTExpression exp = (ASTExpression) items.get(i);
+            if (!exp.isLiteral()) {
+                return false;
+            }
+        }
+        return true;
+    }
+    
+    // A hacky routine used by ASTDirVisit and ASTDirRecurse
+    TemplateSequenceModel evaluateStringsToNamespaces(Environment env) throws TemplateException {
+        TemplateSequenceModel val = (TemplateSequenceModel) eval(env);
+        NativeSequence result = new NativeSequence(val.size());
+        for (int i = 0; i < items.size(); i++) {
+            Object itemExpr = items.get(i);
+            if (itemExpr instanceof ASTExpStringLiteral) {
+                String s = ((ASTExpStringLiteral) itemExpr).getAsString();
+                try {
+                    Environment.Namespace ns = env.importLib(s, null);
+                    result.add(ns);
+                } catch (IOException ioe) {
+                    throw new _MiscTemplateException(((ASTExpStringLiteral) itemExpr),
+                            "Couldn't import library ", new _DelayedJQuote(s), ": ",
+                            new _DelayedGetMessage(ioe));
+                }
+            } else {
+                result.add(val.get(i));
+            }
+        }
+        return result;
+    }
+    
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+		ArrayList clonedValues = (ArrayList) items.clone();
+		for (ListIterator iter = clonedValues.listIterator(); iter.hasNext(); ) {
+            iter.set(((ASTExpression) iter.next()).deepCloneWithIdentifierReplaced(
+                    replacedIdentifier, replacement, replacementState));
+        }
+        return new ASTExpListLiteral(clonedValues);
+    }
+
+    @Override
+    int getParameterCount() {
+        return items != null ? items.size() : 0;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        checkIndex(idx);
+        return items.get(idx);
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        checkIndex(idx);
+        return ParameterRole.ITEM_VALUE;
+    }
+
+    private void checkIndex(int idx) {
+        if (items == null || idx >= items.size()) {
+            throw new IndexOutOfBoundsException();
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpMethodCall.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpMethodCall.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpMethodCall.java
new file mode 100644
index 0000000..86e376f
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpMethodCall.java
@@ -0,0 +1,147 @@
+/*
+ * 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.
+ */
+
+/*
+ * 22 October 1999: This class added by Holger Arendt.
+ */
+
+package org.apache.freemarker.core;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.freemarker.core.model.TemplateMethodModel;
+import org.apache.freemarker.core.model.TemplateMethodModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.util._NullWriter;
+
+
+/**
+ * AST expression node: {@code exp(args)}.
+ */
+final class ASTExpMethodCall extends ASTExpression {
+
+    private final ASTExpression target;
+    private final ASTExpListLiteral arguments;
+
+    ASTExpMethodCall(ASTExpression target, ArrayList arguments) {
+        this(target, new ASTExpListLiteral(arguments));
+    }
+
+    private ASTExpMethodCall(ASTExpression target, ASTExpListLiteral arguments) {
+        this.target = target;
+        this.arguments = arguments;
+    }
+
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        TemplateModel targetModel = target.eval(env);
+        if (targetModel instanceof TemplateMethodModel) {
+            TemplateMethodModel targetMethod = (TemplateMethodModel) targetModel;
+            List argumentStrings = 
+            targetMethod instanceof TemplateMethodModelEx
+            ? arguments.getModelList(env)
+            : arguments.getValueList(env);
+            Object result = targetMethod.exec(argumentStrings);
+            return env.getObjectWrapper().wrap(result);
+        } else if (targetModel instanceof ASTDirMacro) {
+            ASTDirMacro func = (ASTDirMacro) targetModel;
+            env.setLastReturnValue(null);
+            if (!func.isFunction()) {
+                throw new _MiscTemplateException(env, "A macro cannot be called in an expression. (Functions can be.)");
+            }
+            Writer prevOut = env.getOut();
+            try {
+                env.setOut(_NullWriter.INSTANCE);
+                env.invoke(func, null, arguments.items, null, null);
+            } catch (IOException e) {
+                // Should not occur
+                throw new TemplateException("Unexpected exception during function execution", e, env);
+            } finally {
+                env.setOut(prevOut);
+            }
+            return env.getLastReturnValue();
+        } else {
+            throw new NonMethodException(target, targetModel, env);
+        }
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        StringBuilder buf = new StringBuilder();
+        buf.append(target.getCanonicalForm());
+        buf.append("(");
+        String list = arguments.getCanonicalForm();
+        buf.append(list.substring(1, list.length() - 1));
+        buf.append(")");
+        return buf.toString();
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return "...(...)";
+    }
+    
+    TemplateModel getConstantValue() {
+        return null;
+    }
+
+    @Override
+    boolean isLiteral() {
+        return false;
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+        return new ASTExpMethodCall(
+                target.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
+                (ASTExpListLiteral) arguments.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState));
+    }
+
+    @Override
+    int getParameterCount() {
+        return 1 + arguments.items.size();
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        if (idx == 0) {
+            return target;
+        } else if (idx < getParameterCount()) {
+            return arguments.items.get(idx - 1);
+        } else {
+            throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        if (idx == 0) {
+            return ParameterRole.CALLEE;
+        } else if (idx < getParameterCount()) {
+            return ParameterRole.ARGUMENT_VALUE;
+        } else {
+            throw new IndexOutOfBoundsException();
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNegateOrPlus.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNegateOrPlus.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNegateOrPlus.java
new file mode 100644
index 0000000..a211ef7
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNegateOrPlus.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.arithmetic.impl.ConservativeArithmeticEngine;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+
+/**
+ * AST expression node: {@code -exp} or {@code +exp}.
+ */
+final class ASTExpNegateOrPlus extends ASTExpression {
+    
+    private static final int TYPE_MINUS = 0;
+    private static final int TYPE_PLUS = 1;
+
+    private final ASTExpression target;
+    private final boolean isMinus;
+    private static final Integer MINUS_ONE = Integer.valueOf(-1); 
+
+    ASTExpNegateOrPlus(ASTExpression target, boolean isMinus) {
+        this.target = target;
+        this.isMinus = isMinus;
+    }
+    
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        TemplateNumberModel targetModel = null;
+        TemplateModel tm = target.eval(env);
+        try {
+            targetModel = (TemplateNumberModel) tm;
+        } catch (ClassCastException cce) {
+            throw new NonNumericalException(target, tm, env);
+        }
+        if (!isMinus) {
+            return targetModel;
+        }
+        target.assertNonNull(targetModel, env);
+        Number n = targetModel.getAsNumber();
+        // [FM3] Add ArithmeticEngine.negate, then use the engine from the env
+        n = ConservativeArithmeticEngine.INSTANCE.multiply(MINUS_ONE, n);
+        return new SimpleNumber(n);
+    }
+    
+    @Override
+    public String getCanonicalForm() {
+        String op = isMinus ? "-" : "+";
+        return op + target.getCanonicalForm();
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return isMinus ? "-..." : "+...";
+    }
+    
+    @Override
+    boolean isLiteral() {
+        return target.isLiteral();
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+    	return new ASTExpNegateOrPlus(
+    	        target.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
+    	        isMinus);
+    }
+
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return target;
+        case 1: return Integer.valueOf(isMinus ? TYPE_MINUS : TYPE_PLUS);
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        switch (idx) {
+        case 0: return ParameterRole.RIGHT_HAND_OPERAND;
+        case 1: return ParameterRole.AST_NODE_SUBTYPE;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNot.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNot.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNot.java
new file mode 100644
index 0000000..19dd088
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNot.java
@@ -0,0 +1,76 @@
+/*
+ * 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;
+
+/**
+ * AST expression node: {@code !exp}.
+ */
+final class ASTExpNot extends ASTExpBoolean {
+
+    private final ASTExpression target;
+
+    ASTExpNot(ASTExpression target) {
+        this.target = target;
+    }
+
+    @Override
+    boolean evalToBoolean(Environment env) throws TemplateException {
+        return (!target.evalToBoolean(env));
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return "!" + target.getCanonicalForm();
+    }
+ 
+    @Override
+    String getNodeTypeSymbol() {
+        return "!";
+    }
+    
+    @Override
+    boolean isLiteral() {
+        return target.isLiteral();
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+    	return new ASTExpNot(
+    	        target.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState));
+    }
+
+    @Override
+    int getParameterCount() {
+        return 1;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return target;
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return ParameterRole.RIGHT_HAND_OPERAND;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNumberLiteral.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNumberLiteral.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNumberLiteral.java
new file mode 100644
index 0000000..01847a6
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNumberLiteral.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+
+/**
+ * AST expression node: numerical literal
+ */
+final class ASTExpNumberLiteral extends ASTExpression implements TemplateNumberModel {
+
+    private final Number value;
+
+    public ASTExpNumberLiteral(Number value) {
+        this.value = value;
+    }
+    
+    @Override
+    TemplateModel _eval(Environment env) {
+        return new SimpleNumber(value);
+    }
+
+    @Override
+    public String evalAndCoerceToPlainText(Environment env) throws TemplateException {
+        return env.formatNumberToPlainText(this, this, false);
+    }
+
+    @Override
+    public Number getAsNumber() {
+        return value;
+    }
+    
+    String getName() {
+        return "the number: '" + value + "'";
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return value.toString();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return getCanonicalForm();
+    }
+    
+    @Override
+    boolean isLiteral() {
+        return true;
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+        return new ASTExpNumberLiteral(value);
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 0;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpOr.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpOr.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpOr.java
new file mode 100644
index 0000000..5673ec3
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpOr.java
@@ -0,0 +1,82 @@
+/*
+ * 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;
+
+/**
+ * AST expression node: {@code exp || exp}.
+ */
+final class ASTExpOr extends ASTExpBoolean {
+
+    private final ASTExpression lho;
+    private final ASTExpression rho;
+
+    ASTExpOr(ASTExpression lho, ASTExpression rho) {
+        this.lho = lho;
+        this.rho = rho;
+    }
+
+    @Override
+    boolean evalToBoolean(Environment env) throws TemplateException {
+        return lho.evalToBoolean(env) || rho.evalToBoolean(env);
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return lho.getCanonicalForm() + " || " + rho.getCanonicalForm();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "||";
+    }
+
+    @Override
+    boolean isLiteral() {
+        return constantValue != null || (lho.isLiteral() && rho.isLiteral());
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+    	return new ASTExpOr(
+    	        lho.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
+    	        rho.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState));
+    }
+
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return lho;
+        case 1: return rho;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        return ParameterRole.forBinaryOperatorOperand(idx);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpParenthesis.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpParenthesis.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpParenthesis.java
new file mode 100644
index 0000000..eabccbf
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpParenthesis.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateModel;
+
+/**
+ * AST expression node: {@code (exp)}.
+ */
+final class ASTExpParenthesis extends ASTExpression {
+
+    private final ASTExpression nested;
+
+    ASTExpParenthesis(ASTExpression nested) {
+        this.nested = nested;
+    }
+
+    @Override
+    boolean evalToBoolean(Environment env) throws TemplateException {
+        return nested.evalToBoolean(env);
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return "(" + nested.getCanonicalForm() + ")";
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "(...)";
+    }
+    
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        return nested.eval(env);
+    }
+    
+    @Override
+    public boolean isLiteral() {
+        return nested.isLiteral();
+    }
+    
+    ASTExpression getNestedExpression() {
+        return nested;
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+        return new ASTExpParenthesis(
+                nested.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState));
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 1;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return nested;
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return ParameterRole.ENCLOSED_OPERAND;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpRange.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpRange.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpRange.java
new file mode 100644
index 0000000..194c402
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpRange.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.util.BugException;
+
+/**
+ * AST expression node: {@code exp .. exp}, {@code exp ..< exp} (or {@code exp ..! exp}), {@code exp ..* exp}.
+ */
+final class ASTExpRange extends ASTExpression {
+
+    static final int END_INCLUSIVE = 0; 
+    static final int END_EXCLUSIVE = 1; 
+    static final int END_UNBOUND = 2; 
+    static final int END_SIZE_LIMITED = 3; 
+    
+    final ASTExpression lho;
+    final ASTExpression rho;
+    final int endType;
+
+    ASTExpRange(ASTExpression lho, ASTExpression rho, int endType) {
+        this.lho = lho;
+        this.rho = rho;
+        this.endType = endType;
+    }
+    
+    int getEndType() {
+        return endType;
+    }
+
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        final int begin = lho.evalToNumber(env).intValue();
+        if (endType != END_UNBOUND) {
+            final int lhoValue = rho.evalToNumber(env).intValue();
+            return new BoundedRangeModel(
+                    begin, endType != END_SIZE_LIMITED ? lhoValue : begin + lhoValue,
+                    endType == END_INCLUSIVE, endType == END_SIZE_LIMITED); 
+        } else {
+            return new ListableRightUnboundedRangeModel(begin);
+        }
+    }
+    
+    // Surely this way we can tell that it won't be a boolean without evaluating the range, but why was this important?
+    @Override
+    boolean evalToBoolean(Environment env) throws TemplateException {
+        throw new NonBooleanException(this, new BoundedRangeModel(0, 0, false, false), env);
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        String rhs = rho != null ? rho.getCanonicalForm() : "";
+        return lho.getCanonicalForm() + getNodeTypeSymbol() + rhs;
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        switch (endType) {
+        case END_EXCLUSIVE: return "..<";
+        case END_INCLUSIVE: return "..";
+        case END_UNBOUND: return "..";
+        case END_SIZE_LIMITED: return "..*";
+        default: throw new BugException(endType);
+        }
+    }
+    
+    @Override
+    boolean isLiteral() {
+        boolean rightIsLiteral = rho == null || rho.isLiteral();
+        return constantValue != null || (lho.isLiteral() && rightIsLiteral);
+    }
+    
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+        return new ASTExpRange(
+                lho.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
+                rho.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
+                endType);
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return lho;
+        case 1: return rho;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        return ParameterRole.forBinaryOperatorOperand(idx);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpStringLiteral.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpStringLiteral.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpStringLiteral.java
new file mode 100644
index 0000000..96c15df
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpStringLiteral.java
@@ -0,0 +1,211 @@
+/*
+ * 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.List;
+
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.apache.freemarker.core.outputformat.OutputFormat;
+import org.apache.freemarker.core.util.FTLUtil;
+
+/**
+ * AST expression node: string literal
+ */
+final class ASTExpStringLiteral extends ASTExpression implements TemplateScalarModel {
+    
+    private final String value;
+    
+    /** {@link List} of {@link String}-s and {@link ASTInterpolation}-s. */
+    private List<Object> dynamicValue;
+    
+    ASTExpStringLiteral(String value) {
+        this.value = value;
+    }
+    
+    /**
+     * @param parentTkMan
+     *            The token source of the template that contains this string literal. As of this writing, we only need
+     *            this to share the {@code namingConvetion} with that.
+     */
+    void parseValue(FMParserTokenManager parentTkMan, OutputFormat outputFormat) throws ParseException {
+        // The way this works is incorrect (the literal should be parsed without un-escaping),
+        // but we can't fix this backward compatibly.
+        if (value.length() > 3 && (value.indexOf("${") >= 0 || value.indexOf("#{") >= 0)) {
+            
+            Template parentTemplate = getTemplate();
+            ParsingConfiguration pCfg = parentTemplate.getParsingConfiguration();
+
+            try {
+                SimpleCharStream simpleCharacterStream = new SimpleCharStream(
+                        new StringReader(value),
+                        beginLine, beginColumn + 1,
+                        value.length());
+                simpleCharacterStream.setTabSize(pCfg.getTabSize());
+                
+                FMParserTokenManager tkMan = new FMParserTokenManager(
+                        simpleCharacterStream);
+                
+                FMParser parser = new FMParser(parentTemplate, false,
+                        tkMan, pCfg, null, null,
+                        null);
+                // We continue from the parent parser's current state:
+                parser.setupStringLiteralMode(parentTkMan, outputFormat);
+                try {
+                    dynamicValue = parser.StaticTextAndInterpolations();
+                } finally {
+                    // The parent parser continues from this parser's current state:
+                    parser.tearDownStringLiteralMode(parentTkMan);
+                }
+            } catch (ParseException e) {
+                e.setTemplate(parentTemplate);
+                throw e;
+            }
+            constantValue = null;
+        }
+    }
+    
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        if (dynamicValue == null) {
+            return new SimpleScalar(value);
+        } else {
+            // This should behave like concatenating the values with `+`. Thus, an interpolated expression that
+            // returns markup promotes the result of the whole expression to markup.
+            
+            // Exactly one of these is non-null, depending on if the result will be plain text or markup, which can
+            // change during evaluation, depending on the result of the interpolations:
+            StringBuilder plainTextResult = null;
+            TemplateMarkupOutputModel<?> markupResult = null;
+            
+            for (Object part : dynamicValue) {
+                Object calcedPart =
+                        part instanceof String ? part
+                        : ((ASTInterpolation) part).calculateInterpolatedStringOrMarkup(env);
+                if (markupResult != null) {
+                    TemplateMarkupOutputModel<?> partMO = calcedPart instanceof String
+                            ? markupResult.getOutputFormat().fromPlainTextByEscaping((String) calcedPart)
+                            : (TemplateMarkupOutputModel<?>) calcedPart;
+                    markupResult = _EvalUtil.concatMarkupOutputs(this, markupResult, partMO);
+                } else { // We are using `plainTextOutput` (or nothing yet)
+                    if (calcedPart instanceof String) {
+                        String partStr = (String) calcedPart;
+                        if (plainTextResult == null) {
+                            plainTextResult = new StringBuilder(partStr);
+                        } else {
+                            plainTextResult.append(partStr);
+                        }
+                    } else { // `calcedPart` is TemplateMarkupOutputModel
+                        TemplateMarkupOutputModel<?> moPart = (TemplateMarkupOutputModel<?>) calcedPart;
+                        if (plainTextResult != null) {
+                            TemplateMarkupOutputModel<?> leftHandMO = moPart.getOutputFormat()
+                                    .fromPlainTextByEscaping(plainTextResult.toString());
+                            markupResult = _EvalUtil.concatMarkupOutputs(this, leftHandMO, moPart);
+                            plainTextResult = null;
+                        } else {
+                            markupResult = moPart;
+                        }
+                    }
+                }
+            } // for each part
+            return markupResult != null ? markupResult
+                    : plainTextResult != null ? new SimpleScalar(plainTextResult.toString())
+                    : SimpleScalar.EMPTY_STRING;
+        }
+    }
+
+    @Override
+    public String getAsString() {
+        return value;
+    }
+    
+    /**
+     * Tells if this is something like <tt>"${foo}"</tt>, which is usually a user mistake.
+     */
+    boolean isSingleInterpolationLiteral() {
+        return dynamicValue != null && dynamicValue.size() == 1
+                && dynamicValue.get(0) instanceof ASTInterpolation;
+    }
+    
+    @Override
+    public String getCanonicalForm() {
+        if (dynamicValue == null) {
+            return FTLUtil.toStringLiteral(value);
+        } else {
+            StringBuilder sb = new StringBuilder();
+            sb.append('"');
+            for (Object child : dynamicValue) {
+                if (child instanceof ASTInterpolation) {
+                    sb.append(((ASTInterpolation) child).getCanonicalFormInStringLiteral());
+                } else {
+                    sb.append(FTLUtil.escapeStringLiteralPart((String) child, '"'));
+                }
+            }
+            sb.append('"');
+            return sb.toString();
+        }
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return dynamicValue == null ? getCanonicalForm() : "dynamic \"...\"";
+    }
+    
+    @Override
+    boolean isLiteral() {
+        return dynamicValue == null;
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+        ASTExpStringLiteral cloned = new ASTExpStringLiteral(value);
+        // FIXME: replacedIdentifier should be searched inside interpolatedOutput too:
+        cloned.dynamicValue = dynamicValue;
+        return cloned;
+    }
+
+    @Override
+    int getParameterCount() {
+        return dynamicValue == null ? 0 : dynamicValue.size();
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        checkIndex(idx);
+        return dynamicValue.get(idx);
+    }
+
+    private void checkIndex(int idx) {
+        if (dynamicValue == null || idx >= dynamicValue.size()) {
+            throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        checkIndex(idx);
+        return ParameterRole.VALUE_PART;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpVariable.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpVariable.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpVariable.java
new file mode 100644
index 0000000..59ceddc
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpVariable.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 org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * AST expression node: Reference to a "top-level" (local, current namespace, global, data-model) variable
+ */
+final class ASTExpVariable extends ASTExpression {
+
+    private final String name;
+
+    ASTExpVariable(String name) {
+        this.name = name;
+    }
+
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        try {
+            return env.getVariable(name);
+        } catch (NullPointerException e) {
+            if (env == null) {
+                throw new _MiscTemplateException(
+                        "Variables are not available (certainly you are in a parse-time executed directive). "
+                        + "The name of the variable you tried to read: ", name);
+            } else {
+                throw e;
+            }
+        }
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return _StringUtil.toFTLTopLevelIdentifierReference(name);
+    }
+    
+    /**
+     * The name of the identifier without any escaping or other syntactical distortions. 
+     */
+    String getName() {
+        return name;
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return getCanonicalForm();
+    }
+
+    @Override
+    boolean isLiteral() {
+        return false;
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 0;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+        if (name.equals(replacedIdentifier)) {
+            if (replacementState.replacementAlreadyInUse) {
+                ASTExpression clone = replacement.deepCloneWithIdentifierReplaced(null, null, replacementState);
+                clone.copyLocationFrom(replacement);
+                return clone;
+            } else {
+                replacementState.replacementAlreadyInUse = true;
+                return replacement;
+            }
+        } else {
+            return new ASTExpVariable(name);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpression.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpression.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpression.java
new file mode 100644
index 0000000..be00f66
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpression.java
@@ -0,0 +1,208 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.impl.BeanModel;
+
+/**
+ * AST expression node superclass
+ */
+abstract class ASTExpression extends ASTNode {
+
+    /**
+     * @param env might be {@code null}, if this kind of expression can be evaluated during parsing (as opposed to
+     *     during template execution).
+     */
+    abstract TemplateModel _eval(Environment env) throws TemplateException;
+    
+    abstract boolean isLiteral();
+
+    // Used to store a constant return value for this expression. Only if it
+    // is possible, of course.
+    
+    TemplateModel constantValue;
+
+    // Hook in here to set the constant value if possible.
+    
+    @Override
+    void setLocation(Template template, int beginColumn, int beginLine, int endColumn, int endLine) {
+        super.setLocation(template, beginColumn, beginLine, endColumn, endLine);
+        if (isLiteral()) {
+            try {
+                constantValue = _eval(null);
+            } catch (Exception e) {
+            // deliberately ignore.
+            }
+        }
+    }
+
+    final TemplateModel getAsTemplateModel(Environment env) throws TemplateException {
+        return eval(env);
+    }
+    
+    final TemplateModel eval(Environment env) throws TemplateException {
+        return constantValue != null ? constantValue : _eval(env);
+    }
+    
+    String evalAndCoerceToPlainText(Environment env) throws TemplateException {
+        return _EvalUtil.coerceModelToPlainText(eval(env), this, null, env);
+    }
+
+    /**
+     * @param seqTip Tip to display if the value type is not coercable, but it's sequence or collection.
+     */
+    String evalAndCoerceToPlainText(Environment env, String seqTip) throws TemplateException {
+        return _EvalUtil.coerceModelToPlainText(eval(env), this, seqTip, env);
+    }
+
+    Object evalAndCoerceToStringOrMarkup(Environment env) throws TemplateException {
+        return _EvalUtil.coerceModelToStringOrMarkup(eval(env), this, null, env);
+    }
+
+    /**
+     * @param seqTip Tip to display if the value type is not coercable, but it's sequence or collection.
+     */
+    Object evalAndCoerceToStringOrMarkup(Environment env, String seqTip) throws TemplateException {
+        return _EvalUtil.coerceModelToStringOrMarkup(eval(env), this, seqTip, env);
+    }
+    
+    String evalAndCoerceToStringOrUnsupportedMarkup(Environment env) throws TemplateException {
+        return _EvalUtil.coerceModelToStringOrUnsupportedMarkup(eval(env), this, null, env);
+    }
+
+    /**
+     * @param seqTip Tip to display if the value type is not coercable, but it's sequence or collection.
+     */
+    String evalAndCoerceToStringOrUnsupportedMarkup(Environment env, String seqTip) throws TemplateException {
+        return _EvalUtil.coerceModelToStringOrUnsupportedMarkup(eval(env), this, seqTip, env);
+    }
+    
+    Number evalToNumber(Environment env) throws TemplateException {
+        TemplateModel model = eval(env);
+        return modelToNumber(model, env);
+    }
+
+    Number modelToNumber(TemplateModel model, Environment env) throws TemplateException {
+        if (model instanceof TemplateNumberModel) {
+            return _EvalUtil.modelToNumber((TemplateNumberModel) model, this);
+        } else {
+            throw new NonNumericalException(this, model, env);
+        }
+    }
+    
+    boolean evalToBoolean(Environment env) throws TemplateException {
+        return evalToBoolean(env, null);
+    }
+
+    boolean evalToBoolean(Configuration cfg) throws TemplateException {
+        return evalToBoolean(null, cfg);
+    }
+
+    TemplateModel evalToNonMissing(Environment env) throws TemplateException {
+        TemplateModel result = eval(env);
+        assertNonNull(result, env);
+        return result;
+    }
+    
+    private boolean evalToBoolean(Environment env, Configuration cfg) throws TemplateException {
+        TemplateModel model = eval(env);
+        return modelToBoolean(model, env, cfg);
+    }
+    
+    boolean modelToBoolean(TemplateModel model, Environment env) throws TemplateException {
+        return modelToBoolean(model, env, null);
+    }
+
+    boolean modelToBoolean(TemplateModel model, Configuration cfg) throws TemplateException {
+        return modelToBoolean(model, null, cfg);
+    }
+    
+    private boolean modelToBoolean(TemplateModel model, Environment env, Configuration cfg) throws TemplateException {
+        if (model instanceof TemplateBooleanModel) {
+            return ((TemplateBooleanModel) model).getAsBoolean();
+        } else {
+            throw new NonBooleanException(this, model, env);
+        }
+    }
+    
+    final ASTExpression deepCloneWithIdentifierReplaced(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+        ASTExpression clone = deepCloneWithIdentifierReplaced_inner(replacedIdentifier, replacement, replacementState);
+        if (clone.beginLine == 0) {
+            clone.copyLocationFrom(this);
+        }
+        return clone;
+    }
+    
+    static class ReplacemenetState {
+        /**
+         * If the replacement expression is not in use yet, we don't have to deepClone it.
+         */
+        boolean replacementAlreadyInUse; 
+    }
+
+    /**
+     * This should return an equivalent new expression object (or an identifier replacement expression).
+     * The position need not be filled, unless it will be different from the position of what we were cloning. 
+     */
+    protected abstract ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState);
+
+    static boolean isEmpty(TemplateModel model) throws TemplateModelException {
+        if (model instanceof BeanModel) {
+            return ((BeanModel) model).isEmpty();
+        } else if (model instanceof TemplateSequenceModel) {
+            return ((TemplateSequenceModel) model).size() == 0;
+        } else if (model instanceof TemplateScalarModel) {
+            String s = ((TemplateScalarModel) model).getAsString();
+            return (s == null || s.length() == 0);
+        } else if (model == null) {
+            return true;
+        } else if (model instanceof TemplateMarkupOutputModel) { // Note: happens just after FTL string check
+            TemplateMarkupOutputModel mo = (TemplateMarkupOutputModel) model;
+            return mo.getOutputFormat().isEmpty(mo);
+        } else if (model instanceof TemplateCollectionModel) {
+            return !((TemplateCollectionModel) model).iterator().hasNext();
+        } else if (model instanceof TemplateHashModel) {
+            return ((TemplateHashModel) model).isEmpty();
+        } else if (model instanceof TemplateNumberModel
+                || model instanceof TemplateDateModel
+                || model instanceof TemplateBooleanModel) {
+            return false;
+        } else {
+            return true;
+        }
+    }
+    
+    void assertNonNull(TemplateModel model, Environment env) throws InvalidReferenceException {
+        if (model == null) throw InvalidReferenceException.getInstance(this, env);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTHashInterpolation.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTHashInterpolation.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTHashInterpolation.java
new file mode 100644
index 0000000..8c3f8fa
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTHashInterpolation.java
@@ -0,0 +1,172 @@
+/*
+ * 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.Writer;
+import java.text.NumberFormat;
+import java.util.Locale;
+
+import org.apache.freemarker.core.outputformat.MarkupOutputFormat;
+import org.apache.freemarker.core.util.FTLUtil;
+
+/**
+ * AST interpolation node: <tt>#{exp}</tt>
+ */
+final class ASTHashInterpolation extends ASTInterpolation {
+
+    private final ASTExpression expression;
+    private final boolean hasFormat;
+    private final int minFracDigits;
+    private final int maxFracDigits;
+    /** For OutputFormat-based auto-escaping */
+    private final MarkupOutputFormat autoEscapeOutputFormat;
+    private volatile FormatHolder formatCache; // creating new NumberFormat is slow operation
+
+    ASTHashInterpolation(ASTExpression expression, MarkupOutputFormat autoEscapeOutputFormat) {
+        this.expression = expression;
+        hasFormat = false;
+        minFracDigits = 0;
+        maxFracDigits = 0;
+        this.autoEscapeOutputFormat = autoEscapeOutputFormat;
+    }
+
+    ASTHashInterpolation(ASTExpression expression,
+            int minFracDigits, int maxFracDigits,
+            MarkupOutputFormat autoEscapeOutputFormat) {
+        this.expression = expression;
+        hasFormat = true;
+        this.minFracDigits = minFracDigits;
+        this.maxFracDigits = maxFracDigits;
+        this.autoEscapeOutputFormat = autoEscapeOutputFormat;
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) throws TemplateException, IOException {
+        String s = calculateInterpolatedStringOrMarkup(env);
+        Writer out = env.getOut();
+        if (autoEscapeOutputFormat != null) {
+            autoEscapeOutputFormat.output(s, out);
+        } else {
+            out.write(s);
+        }
+        return null;
+    }
+
+    @Override
+    protected String calculateInterpolatedStringOrMarkup(Environment env) throws TemplateException {
+        Number num = expression.evalToNumber(env);
+        
+        FormatHolder fmth = formatCache;  // atomic sampling
+        if (fmth == null || !fmth.locale.equals(env.getLocale())) {
+            synchronized (this) {
+                fmth = formatCache;
+                if (fmth == null || !fmth.locale.equals(env.getLocale())) {
+                    NumberFormat fmt = NumberFormat.getNumberInstance(env.getLocale());
+                    if (hasFormat) {
+                        fmt.setMinimumFractionDigits(minFracDigits);
+                        fmt.setMaximumFractionDigits(maxFracDigits);
+                    } else {
+                        fmt.setMinimumFractionDigits(0);
+                        fmt.setMaximumFractionDigits(50);
+                    }
+                    fmt.setGroupingUsed(false);
+                    formatCache = new FormatHolder(fmt, env.getLocale());
+                    fmth = formatCache;
+                }
+            }
+        }
+        // We must use Format even if hasFormat == false.
+        // Some locales may use non-Arabic digits, thus replacing the
+        // decimal separator in the result of toString() is not enough.
+        return fmth.format.format(num);
+    }
+
+    @Override
+    protected String dump(boolean canonical, boolean inStringLiteral) {
+        StringBuilder buf = new StringBuilder("#{");
+        final String exprCF = expression.getCanonicalForm();
+        buf.append(inStringLiteral ? FTLUtil.escapeStringLiteralPart(exprCF, '"') : exprCF);
+        if (hasFormat) {
+            buf.append(" ; ");
+            buf.append("m");
+            buf.append(minFracDigits);
+            buf.append("M");
+            buf.append(maxFracDigits);
+        }
+        buf.append("}");
+        return buf.toString();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "#{...}";
+    }
+
+    @Override
+    boolean heedsOpeningWhitespace() {
+        return true;
+    }
+
+    @Override
+    boolean heedsTrailingWhitespace() {
+        return true;
+    }
+    
+    private static class FormatHolder {
+        final NumberFormat format;
+        final Locale locale;
+        
+        FormatHolder(NumberFormat format, Locale locale) {
+            this.format = format;
+            this.locale = locale;
+        }
+    }
+
+    @Override
+    int getParameterCount() {
+        return 3;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return expression;
+        case 1: return Integer.valueOf(minFracDigits);
+        case 2: return Integer.valueOf(maxFracDigits);
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        switch (idx) {
+        case 0: return ParameterRole.CONTENT;
+        case 1: return ParameterRole.MINIMUM_DECIMALS;
+        case 2: return ParameterRole.MAXIMUM_DECIMALS;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTImplicitParent.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTImplicitParent.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTImplicitParent.java
new file mode 100644
index 0000000..4d3c339
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTImplicitParent.java
@@ -0,0 +1,101 @@
+/*
+ * 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;
+
+/**
+ * AST directive-like node, used where there's no other parent for a list of {@link ASTElement}-s. Most often occurs as
+ * the root node of the AST.
+ */
+final class ASTImplicitParent extends ASTElement {
+
+    ASTImplicitParent() { }
+    
+    @Override
+    ASTElement postParseCleanup(boolean stripWhitespace)
+        throws ParseException {
+        super.postParseCleanup(stripWhitespace);
+        return getChildCount() == 1 ? getChild(0) : this;
+    }
+
+    /**
+     * Processes the contents of the internal <tt>ASTElement</tt> list,
+     * and outputs the resulting text.
+     */
+    @Override
+    ASTElement[] accept(Environment env)
+        throws TemplateException, IOException {
+        return getChildBuffer();
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        if (canonical) {
+            return getChildrenCanonicalForm();
+        } else {
+            if (getParent() == null) {
+                return "root";
+            }
+            return getNodeTypeSymbol(); // ASTImplicitParent is uninteresting in a stack trace.
+        }
+    }
+
+    @Override
+    protected boolean isOutputCacheable() {
+        int ln = getChildCount();
+        for (int i = 0; i < ln; i++) {
+            if (!getChild(i).isOutputCacheable()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return "#mixed_content";
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 0;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+    
+    @Override
+    boolean isIgnorable(boolean stripWhitespace) {
+        return getChildCount() == 0;
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTInterpolation.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTInterpolation.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTInterpolation.java
new file mode 100644
index 0000000..028acc2
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTInterpolation.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+
+/**
+ * AST interpolation node superclass.
+ */
+abstract class ASTInterpolation extends ASTElement {
+
+    protected abstract String dump(boolean canonical, boolean inStringLiteral);
+
+    @Override
+    protected final String dump(boolean canonical) {
+        return dump(canonical, false);
+    }
+    
+    final String getCanonicalFormInStringLiteral() {
+        return dump(true, true);
+    }
+
+    /**
+     * Returns the already type-converted value that this interpolation will insert into the output.
+     * 
+     * @return A {@link String} or {@link TemplateMarkupOutputModel}. Not {@code null}.
+     */
+    protected abstract Object calculateInterpolatedStringOrMarkup(Environment env) throws TemplateException;
+
+    @Override
+    boolean isShownInStackTrace() {
+        return true;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTNode.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTNode.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTNode.java
new file mode 100644
index 0000000..18e34c1
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTNode.java
@@ -0,0 +1,233 @@
+/*
+ * 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;
+
+/**
+ * AST node: The superclass of all AST nodes
+ */
+abstract class ASTNode {
+    
+    private Template template;
+    int beginColumn, beginLine, endColumn, endLine;
+    
+    /** This is needed for an ?eval hack; the expression AST nodes will be the descendants of the template, however,
+     *  we can't give their position in the template, only in the dynamic string that's evaluated. That's signaled
+     *  by a negative line numbers, starting from this constant as line 1. */
+    static final int RUNTIME_EVAL_LINE_DISPLACEMENT = -1000000000;  
+
+    final void setLocation(Template template, Token begin, Token end) {
+        setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
+    }
+
+    final void setLocation(Template template, Token tagBegin, Token tagEnd, TemplateElements children) {
+        ASTElement lastChild = children.getLast();
+        if (lastChild != null) {
+            // [<#if exp>children]<#else>
+            setLocation(template, tagBegin, lastChild);
+        } else {
+            // [<#if exp>]<#else>
+            setLocation(template, tagBegin, tagEnd);
+        }
+    }
+    
+    final void setLocation(Template template, Token begin, ASTNode end) {
+        setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
+    }
+    
+    final void setLocation(Template template, ASTNode begin, Token end) {
+        setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
+    }
+
+    final void setLocation(Template template, ASTNode begin, ASTNode end) {
+        setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
+    }
+
+    void setLocation(Template template, int beginColumn, int beginLine, int endColumn, int endLine) {
+        this.template = template;
+        this.beginColumn = beginColumn;
+        this.beginLine = beginLine;
+        this.endColumn = endColumn;
+        this.endLine = endLine;
+    }
+    
+    public final int getBeginColumn() {
+        return beginColumn;
+    }
+
+    public final int getBeginLine() {
+        return beginLine;
+    }
+
+    public final int getEndColumn() {
+        return endColumn;
+    }
+
+    public final int getEndLine() {
+        return endLine;
+    }
+
+    /**
+     * Returns a string that indicates
+     * where in the template source, this object is.
+     */
+    public String getStartLocation() {
+        return MessageUtil.formatLocationForEvaluationError(template, beginLine, beginColumn);
+    }
+
+    /**
+     * As of 2.3.20. the same as {@link #getStartLocation}. Meant to be used where there's a risk of XSS
+     * when viewing error messages.
+     */
+    public String getStartLocationQuoted() {
+        return getStartLocation();
+    }
+
+    public String getEndLocation() {
+        return MessageUtil.formatLocationForEvaluationError(template, endLine, endColumn);
+    }
+
+    /**
+     * As of 2.3.20. the same as {@link #getEndLocation}. Meant to be used where there's a risk of XSS
+     * when viewing error messages.
+     */
+    public String getEndLocationQuoted() {
+        return getEndLocation();
+    }
+    
+    public final String getSource() {
+        String s;
+        if (template != null) {
+            s = template.getSource(beginColumn, beginLine, endColumn, endLine);
+        } else {
+            s = null;
+        }
+
+        // Can't just return null for backward-compatibility... 
+        return s != null ? s : getCanonicalForm();
+    }
+
+    @Override
+    public String toString() {
+        String s;
+    	try {
+    		s = getSource();
+    	} catch (Exception e) { // REVISIT: A bit of a hack? (JR)
+    	    s = null;
+    	}
+    	return s != null ? s : getCanonicalForm();
+    }
+
+    /**
+     * @return whether the point in the template file specified by the 
+     * column and line numbers is contained within this template object.
+     */
+    public boolean contains(int column, int line) {
+        if (line < beginLine || line > endLine) {
+            return false;
+        }
+        if (line == beginLine) {
+            if (column < beginColumn) {
+                return false;
+            }
+        }
+        if (line == endLine) {
+            if (column > endColumn) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public Template getTemplate() {
+        return template;
+    }
+    
+    ASTNode copyLocationFrom(ASTNode from) {
+        template = from.template;
+        beginColumn = from.beginColumn;
+        beginLine = from.beginLine;
+        endColumn = from.endColumn;
+        endLine = from.endLine;
+        return this;
+    }    
+
+    /**
+     * FTL generated from the AST of the node, which must be parseable to an AST that does the same as the original
+     * source, assuming we turn off automatic white-space removal when parsing the canonical form.
+     * 
+     * @see ASTElement#getDescription()
+     * @see #getNodeTypeSymbol()
+     */
+    abstract public String getCanonicalForm();
+    
+    /**
+     * A very sort single-line string that describes what kind of AST node this is, without describing any 
+     * embedded expression or child element. Examples: {@code "#if"}, {@code "+"}, <tt>"${...}</tt>. These values should
+     * be suitable as tree node labels in a tree view. Yet, they should be consistent and complete enough so that an AST
+     * that is equivalent with the original could be reconstructed from the tree view. Thus, for literal values that are
+     * leaf nodes the symbols should be the canonical form of value.
+     * 
+     * Note that {@link ASTElement#getDescription()} has similar role, only it doesn't go under the element level
+     * (i.e. down to the expression level), instead it always prints the embedded expressions itself.
+     * 
+     * @see #getCanonicalForm()
+     * @see ASTElement#getDescription()
+     */
+    abstract String getNodeTypeSymbol();
+    
+    /**
+     * Returns highest valid parameter index + 1. So one should scan indexes with {@link #getParameterValue(int)}
+     * starting from 0 up until but excluding this. For example, for the binary "+" operator this will give 2, so the
+     * legal indexes are 0 and 1. Note that if a parameter is optional in a template-object-type and happens to be
+     * omitted in an instance, this will still return the same value and the value of that parameter will be
+     * {@code null}.
+     */
+    abstract int getParameterCount();
+    
+    /**
+     * Returns the value of the parameter identified by the index. For example, the binary "+" operator will have an
+     * LHO {@link ASTExpression} at index 0, and and RHO {@link ASTExpression} at index 1. Or, the binary "." operator will
+     * have an LHO {@link ASTExpression} at index 0, and an RHO {@link String}(!) at index 1. Or, the {@code #include}
+     * directive will have a path {@link ASTExpression} at index 0, a "parse" {@link ASTExpression} at index 1, etc.
+     * 
+     * <p>The index value doesn't correspond to the source-code location in general. It's an arbitrary identifier
+     * that corresponds to the role of the parameter instead. This also means that when a parameter is omitted, the
+     * index of the other parameters won't shift.
+     *
+     *  @return {@code null} or any kind of {@link Object}, very often an {@link ASTExpression}. However, if there's
+     *      a {@link ASTNode} stored inside the returned value, it must itself be be a {@link ASTNode}
+     *      too, otherwise the AST couldn't be (easily) fully traversed. That is, non-{@link ASTNode} values
+     *      can only be used for leafs. 
+     *  
+     *  @throws IndexOutOfBoundsException if {@code idx} is less than 0 or not less than {@link #getParameterCount()}. 
+     */
+    abstract Object getParameterValue(int idx);
+
+    /**
+     *  Returns the role of the parameter at the given index, like {@link ParameterRole#LEFT_HAND_OPERAND}.
+     *  
+     *  As of this writing (2013-06-17), for directive parameters it will always give {@link ParameterRole#UNKNOWN},
+     *  because there was no need to be more specific so far. This should be improved as need.
+     *  
+     *  @throws IndexOutOfBoundsException if {@code idx} is less than 0 or not less than {@link #getParameterCount()}. 
+     */
+    abstract ParameterRole getParameterRole(int idx);
+    
+}


[27/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/debug/DebugModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/debug/DebugModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/DebugModel.java
new file mode 100644
index 0000000..b5e0a58
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/DebugModel.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.debug;
+
+import java.rmi.Remote;
+import java.rmi.RemoteException;
+import java.util.Date;
+
+import org.apache.freemarker.core.model.TemplateModelException;
+
+/**
+ * Represents the debugger-side mirror of a TemplateModel object, a Template
+ * object, or a Configuration object. The Environment objects are also represented
+ * by instances of this model, although not directly but through a separate
+ * subinterface {@link DebuggedEnvironment}. The interface is a union of
+ * almost all of FreeMarker template models with identical method signatures.
+ * For purposes of optimizing network traffic there are bulk retrieval methods
+ * for sequences and hashes, as well as a {@link #getModelTypes()} method that
+ * returns a bit mask of various <tt>TYPE_xxx</tt> constants flagging which
+ * template models are implemented by the mirrored object.
+ */
+public interface DebugModel extends Remote {
+    public static final int TYPE_SCALAR        =    1;
+    public static final int TYPE_NUMBER        =    2;
+    public static final int TYPE_DATE          =    4;
+    public static final int TYPE_BOOLEAN       =    8;
+    public static final int TYPE_SEQUENCE      =   16;
+    public static final int TYPE_COLLECTION    =   32;
+    public static final int TYPE_HASH          =   64;
+    public static final int TYPE_HASH_EX       =  128;
+    public static final int TYPE_METHOD        =  256;
+    public static final int TYPE_METHOD_EX     =  512;
+    public static final int TYPE_TRANSFORM     = 1024;
+    public static final int TYPE_ENVIRONMENT   = 2048;
+    public static final int TYPE_TEMPLATE      = 4096;
+    public static final int TYPE_CONFIGURATION = 8192;
+    
+    public String getAsString()
+    throws TemplateModelException,
+        RemoteException;
+        
+    public Number getAsNumber()
+    throws TemplateModelException,
+        RemoteException;
+    
+    public boolean getAsBoolean()
+    throws TemplateModelException,
+        RemoteException;
+    
+    public Date getAsDate()
+    throws TemplateModelException,
+        RemoteException;
+    
+    public int getDateType()
+    throws TemplateModelException,
+        RemoteException;
+        
+    public int size()
+    throws TemplateModelException,
+        RemoteException;
+        
+    public DebugModel get(int index)
+    throws TemplateModelException,
+        RemoteException;
+    
+    public DebugModel[] get(int fromIndex, int toIndex)
+    throws TemplateModelException,
+        RemoteException;
+        
+    public DebugModel get(String key)
+    throws TemplateModelException,
+        RemoteException;
+        
+    public DebugModel[] get(String[] keys)
+    throws TemplateModelException,
+        RemoteException;
+    
+    public DebugModel[] getCollection()
+    throws TemplateModelException,
+        RemoteException;
+
+    public String[] keys()
+    throws TemplateModelException,
+        RemoteException;
+    
+    public int getModelTypes()
+    throws RemoteException;
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/debug/DebuggedEnvironment.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/debug/DebuggedEnvironment.java b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/DebuggedEnvironment.java
new file mode 100644
index 0000000..dca312d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/DebuggedEnvironment.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.debug;
+
+import java.rmi.RemoteException;
+
+import org.apache.freemarker.core.MutableProcessingConfiguration;
+
+/**
+ * Represents the debugger-side mirror of a debugged 
+ * {@link org.apache.freemarker.core.Environment} object in the remote VM. This interface
+ * extends {@link DebugModel}, and the properties of the Environment are exposed
+ * as hash keys on it. Specifically, the following keys are supported:
+ * "currentNamespace", "dataModel", "globalNamespace", "knownVariables", 
+ * "mainNamespace", and "template".
+ * <p>The debug model for the template supports keys "configuration" and "name".
+ * <p>The debug model for the configuration supports key "sharedVariables".
+ * <p>Additionally, all of the debug models for environment, template, and 
+ * configuration also support all the setting keys of 
+ * {@link MutableProcessingConfiguration} objects.
+
+ */
+public interface DebuggedEnvironment extends DebugModel {
+    /**
+     * Resumes the processing of the environment in the remote VM after it was 
+     * stopped on a breakpoint.
+     */
+    public void resume() throws RemoteException;
+    
+    /**
+     * Stops the processing of the environment after it was stopped on
+     * a breakpoint. Causes a {@link org.apache.freemarker.core.StopException} to be
+     * thrown in the processing thread in the remote VM. 
+     */
+    public void stop() throws RemoteException;
+    
+    /**
+     * Returns a unique identifier for this environment
+     */
+    public long getId() throws RemoteException;
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/debug/Debugger.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/debug/Debugger.java b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/Debugger.java
new file mode 100644
index 0000000..3e2b8de
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/Debugger.java
@@ -0,0 +1,95 @@
+/*
+ * 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.debug;
+
+import java.rmi.Remote;
+import java.rmi.RemoteException;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * The main debugger interface. Allows management of breakpoints as well as
+ * installation of listeners for debug events.
+ */
+public interface Debugger extends Remote {
+    public static final int DEFAULT_PORT = 7011;
+
+    /**
+     * Adds a breakpoint
+     * @param breakpoint the breakpoint to add
+     */
+    public void addBreakpoint(Breakpoint breakpoint)
+    throws RemoteException;
+    
+    /**
+     * Removes a single breakpoint
+     * @param breakpoint the breakpoint to remove
+     */
+    public void removeBreakpoint(Breakpoint breakpoint)
+    throws RemoteException;
+
+    /**
+     * Removes all breakpoints for a specific template
+     */
+    public void removeBreakpoints(String templateName)
+    throws RemoteException;
+
+    /**
+     * Removes all breakpoints
+     */
+    public void removeBreakpoints()
+    throws RemoteException;
+
+    /**
+     * Retrieves a list of all {@link Breakpoint} objects.
+     */
+    public List getBreakpoints()
+    throws RemoteException;
+        
+    /**
+     * Retrieves a list of all {@link Breakpoint} objects for the specified
+     * template.
+     */
+    public List getBreakpoints(String templateName)
+    throws RemoteException;
+
+    /**
+     * Retrieves a collection of all {@link DebuggedEnvironment} objects that 
+     * are currently suspended.
+     */
+    public Collection getSuspendedEnvironments()
+    throws RemoteException;
+        
+    /**
+     * Adds a listener for debugger events.
+     * @return an identification token that should be passed to 
+     * {@link #removeDebuggerListener(Object)} to remove this listener.
+     */
+    public Object addDebuggerListener(DebuggerListener listener)
+    throws RemoteException;
+        
+    /**
+     * Removes a previously added debugger listener.
+     * @param id the identification token for the listener that was returned
+     * from a prior call to {@link #addDebuggerListener(DebuggerListener)}.
+     */
+    public void removeDebuggerListener(Object id)
+    throws RemoteException;
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/debug/DebuggerClient.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/debug/DebuggerClient.java b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/DebuggerClient.java
new file mode 100644
index 0000000..2af3136
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/DebuggerClient.java
@@ -0,0 +1,149 @@
+/*
+ * 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.debug;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.rmi.RemoteException;
+import java.rmi.server.RemoteObject;
+import java.security.MessageDigest;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.freemarker.core.util.UndeclaredThrowableException;
+
+/**
+ * A utility class that allows you to connect to the FreeMarker debugger service
+ * running on a specific host and port. 
+ */
+public class DebuggerClient {
+    private DebuggerClient() {
+    }
+    
+    /**
+     * Connects to the FreeMarker debugger service running on a specific host
+     * and port. The Java VM to which the connection is made must have defined
+     * the system property <tt>org.apache.freemarker.core.debug.password</tt> in order to enable
+     * the debugger service. Additionally, the <tt>org.apache.freemarker.core.debug.port</tt>
+     * system property can be set to specify the port where the debugger service
+     * is listening. When not specified, it defaults to 
+     * {@link Debugger#DEFAULT_PORT}.
+     * @param host the host address of the machine where the debugger service is
+     * running.
+     * @param port the port of the debugger service
+     * @param password the password required to connect to the debugger service
+     * @return Debugger a debugger object. null is returned in case incorrect
+     * password was supplied.
+     * @throws IOException if an exception occurs.
+     */
+    public static Debugger getDebugger(InetAddress host, int port, String password)
+    throws IOException {
+        try {
+            Socket s = new Socket(host, port);
+            try {
+                ObjectOutputStream out = new ObjectOutputStream(s.getOutputStream());
+                ObjectInputStream in = new ObjectInputStream(s.getInputStream());
+                int protocolVersion = in.readInt();
+                if (protocolVersion > 220) {
+                    throw new IOException(
+                        "Incompatible protocol version " + protocolVersion + 
+                        ". At most 220 was expected.");
+                }
+                byte[] challenge = (byte[]) in.readObject();
+                MessageDigest md = MessageDigest.getInstance("SHA");
+                md.update(password.getBytes(StandardCharsets.UTF_8));
+                md.update(challenge);
+                out.writeObject(md.digest());
+                return new LocalDebuggerProxy((Debugger) in.readObject());
+                //return (Debugger)in.readObject();
+            } finally {
+                s.close();
+            }
+        } catch (IOException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new UndeclaredThrowableException(e); 
+        }
+    }
+    
+    private static class LocalDebuggerProxy implements Debugger {
+        private final Debugger remoteDebugger;
+
+        LocalDebuggerProxy(Debugger remoteDebugger) {
+            this.remoteDebugger = remoteDebugger;
+        }
+
+        @Override
+        public void addBreakpoint(Breakpoint breakpoint) throws RemoteException {
+            remoteDebugger.addBreakpoint(breakpoint);
+        }
+
+        @Override
+        public Object addDebuggerListener(DebuggerListener listener)
+        throws RemoteException {
+            if (listener instanceof RemoteObject) {
+                return remoteDebugger.addDebuggerListener(listener);
+            } else {
+                RmiDebuggerListenerImpl remotableListener = 
+                    new RmiDebuggerListenerImpl(listener);
+                return remoteDebugger.addDebuggerListener(remotableListener);
+            }
+        }
+
+        @Override
+        public List getBreakpoints() throws RemoteException {
+            return remoteDebugger.getBreakpoints();
+        }
+
+        @Override
+        public List getBreakpoints(String templateName) throws RemoteException {
+            return remoteDebugger.getBreakpoints(templateName);
+        }
+
+        @Override
+        public Collection getSuspendedEnvironments() throws RemoteException {
+            return remoteDebugger.getSuspendedEnvironments();
+        }
+
+        @Override
+        public void removeBreakpoint(Breakpoint breakpoint) throws RemoteException {
+            remoteDebugger.removeBreakpoint(breakpoint);
+        }
+
+        @Override
+        public void removeBreakpoints(String templateName) throws RemoteException {
+            remoteDebugger.removeBreakpoints(templateName);
+        }
+
+        @Override
+        public void removeBreakpoints() throws RemoteException {
+            remoteDebugger.removeBreakpoints();
+        }
+
+        @Override
+        public void removeDebuggerListener(Object id) throws RemoteException {
+            remoteDebugger.removeDebuggerListener(id);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/debug/DebuggerListener.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/debug/DebuggerListener.java b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/DebuggerListener.java
new file mode 100644
index 0000000..a332426
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/DebuggerListener.java
@@ -0,0 +1,36 @@
+/*
+ * 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.debug;
+
+import java.rmi.Remote;
+import java.rmi.RemoteException;
+import java.util.EventListener;
+
+/**
+ * An interface for components that wish to receive debugging events.
+ */
+public interface DebuggerListener extends Remote, EventListener {
+    /**
+     * Called whenever an environment gets suspended (ie hits a breakpoint).
+     * @param e the event object
+     */
+    public void environmentSuspended(EnvironmentSuspendedEvent e)
+    throws RemoteException;
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/debug/DebuggerServer.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/debug/DebuggerServer.java b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/DebuggerServer.java
new file mode 100644
index 0000000..29fa199
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/DebuggerServer.java
@@ -0,0 +1,131 @@
+/*
+ * 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.debug;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.nio.charset.UnsupportedCharsetException;
+import java.security.MessageDigest;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.Random;
+
+import org.apache.freemarker.core._CoreLogs;
+import org.apache.freemarker.core.util.UndeclaredThrowableException;
+import org.apache.freemarker.core.util._SecurityUtil;
+import org.slf4j.Logger;
+
+/**
+ */
+class DebuggerServer {
+
+    private static final Logger LOG = _CoreLogs.DEBUG_SERVER;
+    
+    // TODO: Eventually replace with Yarrow    
+    // TODO: Can be extremely slow (on Linux, not enough entropy)
+    private static final Random R = new SecureRandom();
+    
+    private final byte[] password;
+    private final int port;
+    private final Serializable debuggerStub;
+    private boolean stop = false;
+    private ServerSocket serverSocket;
+    
+    public DebuggerServer(Serializable debuggerStub) {
+        port = _SecurityUtil.getSystemProperty("org.apache.freemarker.core.debug.port", Debugger.DEFAULT_PORT).intValue();
+        try {
+            password = _SecurityUtil.getSystemProperty("org.apache.freemarker.core.debug.password", "").getBytes(
+                    StandardCharsets.UTF_8);
+        } catch (UnsupportedCharsetException e) {
+            throw new UndeclaredThrowableException(e);
+        }
+        this.debuggerStub = debuggerStub;
+    }
+    
+    public void start() {
+        new Thread(new Runnable()
+        {
+            @Override
+            public void run() {
+                startInternal();
+            }
+        }, "FreeMarker Debugger Server Acceptor").start();
+    }
+    
+    private void startInternal() {
+        try {
+            serverSocket = new ServerSocket(port);
+            while (!stop) {
+                Socket s = serverSocket.accept();
+                new Thread(new DebuggerAuthProtocol(s)).start();
+            }
+        } catch (IOException e) {
+            LOG.error("Debugger server shut down.", e);
+        }
+    }
+    
+    private class DebuggerAuthProtocol implements Runnable {
+        private final Socket s;
+        
+        DebuggerAuthProtocol(Socket s) {
+            this.s = s;
+        }
+        
+        @Override
+        public void run() {
+            try {
+                ObjectOutputStream out = new ObjectOutputStream(s.getOutputStream());
+                ObjectInputStream in = new ObjectInputStream(s.getInputStream());
+                byte[] challenge = new byte[512];
+                R.nextBytes(challenge);
+                out.writeInt(220); // protocol version
+                out.writeObject(challenge);
+                MessageDigest md = MessageDigest.getInstance("SHA");
+                md.update(password);
+                md.update(challenge);
+                byte[] response = (byte[]) in.readObject();
+                if (Arrays.equals(response, md.digest())) {
+                    out.writeObject(debuggerStub);
+                } else {
+                    out.writeObject(null);
+                }
+            } catch (Exception e) {
+                LOG.warn("Connection to {} abruptly broke", s.getInetAddress().getHostAddress(), e);
+            }
+        }
+
+    }
+
+    public void stop() {
+        stop = true;
+        if (serverSocket != null) {
+            try {
+                serverSocket.close();
+            } catch (IOException e) {
+                LOG.error("Unable to close server socket.", e);
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/debug/EnvironmentSuspendedEvent.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/debug/EnvironmentSuspendedEvent.java b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/EnvironmentSuspendedEvent.java
new file mode 100644
index 0000000..5be10e6
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/EnvironmentSuspendedEvent.java
@@ -0,0 +1,67 @@
+/*
+ * 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.debug;
+
+import java.util.EventObject;
+
+/**
+ * Event describing a suspension of an environment (ie because it hit a
+ * breakpoint).
+ */
+public class EnvironmentSuspendedEvent extends EventObject {
+    private static final long serialVersionUID = 1L;
+
+    private final String name;
+    private final int line;
+    private final DebuggedEnvironment env;
+
+    public EnvironmentSuspendedEvent(Object source, String name, int line, DebuggedEnvironment env) {
+        super(source);
+        this.name = name;
+        this.line = line;
+        this.env = env;
+    }
+
+    /**
+     * The name of the template where the execution of the environment
+     * was suspended
+     * @return String the template name
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * The line number in the template where the execution of the environment
+     * was suspended.
+     * @return int the line number
+     */
+    public int getLine() {
+        return line;
+    }
+
+    /**
+     * The environment that was suspended
+     * @return DebuggedEnvironment
+     */
+    public DebuggedEnvironment getEnvironment() {
+        return env;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/debug/RmiDebugModelImpl.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/debug/RmiDebugModelImpl.java b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/RmiDebugModelImpl.java
new file mode 100644
index 0000000..bb11db3
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/RmiDebugModelImpl.java
@@ -0,0 +1,164 @@
+/*
+ * 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.debug;
+
+import java.rmi.RemoteException;
+import java.rmi.server.UnicastRemoteObject;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateMethodModel;
+import org.apache.freemarker.core.model.TemplateMethodModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.TemplateTransformModel;
+
+/**
+ */
+class RmiDebugModelImpl extends UnicastRemoteObject implements DebugModel {
+    private static final long serialVersionUID = 1L;
+
+    private final TemplateModel model;
+    private final int type;
+    
+    RmiDebugModelImpl(TemplateModel model, int extraTypes) throws RemoteException {
+        super();
+        this.model = model;
+        type = calculateType(model) + extraTypes;
+    }
+
+    private static DebugModel getDebugModel(TemplateModel tm) throws RemoteException {
+        return (DebugModel) RmiDebuggedEnvironmentImpl.getCachedWrapperFor(tm);
+    }
+    @Override
+    public String getAsString() throws TemplateModelException {
+        return ((TemplateScalarModel) model).getAsString();
+    }
+
+    @Override
+    public Number getAsNumber() throws TemplateModelException {
+        return ((TemplateNumberModel) model).getAsNumber();
+    }
+
+    @Override
+    public Date getAsDate() throws TemplateModelException {
+        return ((TemplateDateModel) model).getAsDate();
+    }
+
+    @Override
+    public int getDateType() {
+        return ((TemplateDateModel) model).getDateType();
+    }
+
+    @Override
+    public boolean getAsBoolean() throws TemplateModelException {
+        return ((TemplateBooleanModel) model).getAsBoolean();
+    }
+
+    @Override
+    public int size() throws TemplateModelException {
+        if (model instanceof TemplateSequenceModel) {
+            return ((TemplateSequenceModel) model).size();
+        }
+        return ((TemplateHashModelEx) model).size();
+    }
+
+    @Override
+    public DebugModel get(int index) throws TemplateModelException, RemoteException {
+        return getDebugModel(((TemplateSequenceModel) model).get(index));
+    }
+    
+    @Override
+    public DebugModel[] get(int fromIndex, int toIndex) throws TemplateModelException, RemoteException {
+        DebugModel[] dm = new DebugModel[toIndex - fromIndex];
+        TemplateSequenceModel s = (TemplateSequenceModel) model;
+        for (int i = fromIndex; i < toIndex; i++) {
+            dm[i - fromIndex] = getDebugModel(s.get(i));
+        }
+        return dm;
+    }
+
+    @Override
+    public DebugModel[] getCollection() throws TemplateModelException, RemoteException {
+        List list = new ArrayList();
+        TemplateModelIterator i = ((TemplateCollectionModel) model).iterator();
+        while (i.hasNext()) {
+            list.add(getDebugModel(i.next()));
+        }
+        return (DebugModel[]) list.toArray(new DebugModel[list.size()]);
+    }
+    
+    @Override
+    public DebugModel get(String key) throws TemplateModelException, RemoteException {
+        return getDebugModel(((TemplateHashModel) model).get(key));
+    }
+    
+    @Override
+    public DebugModel[] get(String[] keys) throws TemplateModelException, RemoteException {
+        DebugModel[] dm = new DebugModel[keys.length];
+        TemplateHashModel h = (TemplateHashModel) model;
+        for (int i = 0; i < keys.length; i++) {
+            dm[i] = getDebugModel(h.get(keys[i]));
+        }
+        return dm;
+    }
+
+    @Override
+    public String[] keys() throws TemplateModelException {
+        TemplateHashModelEx h = (TemplateHashModelEx) model;
+        List list = new ArrayList();
+        TemplateModelIterator i = h.keys().iterator();
+        while (i.hasNext()) {
+            list.add(((TemplateScalarModel) i.next()).getAsString());
+        }
+        return (String[]) list.toArray(new String[list.size()]);
+    }
+
+    @Override
+    public int getModelTypes() {
+        return type;
+    }
+    
+    private static int calculateType(TemplateModel model) {
+        int type = 0;
+        if (model instanceof TemplateScalarModel) type += TYPE_SCALAR;
+        if (model instanceof TemplateNumberModel) type += TYPE_NUMBER;
+        if (model instanceof TemplateDateModel) type += TYPE_DATE;
+        if (model instanceof TemplateBooleanModel) type += TYPE_BOOLEAN;
+        if (model instanceof TemplateSequenceModel) type += TYPE_SEQUENCE;
+        if (model instanceof TemplateCollectionModel) type += TYPE_COLLECTION;
+        if (model instanceof TemplateHashModelEx) type += TYPE_HASH_EX;
+        else if (model instanceof TemplateHashModel) type += TYPE_HASH;
+        if (model instanceof TemplateMethodModelEx) type += TYPE_METHOD_EX;
+        else if (model instanceof TemplateMethodModel) type += TYPE_METHOD;
+        if (model instanceof TemplateTransformModel) type += TYPE_TRANSFORM;
+        return type;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/debug/RmiDebuggedEnvironmentImpl.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/debug/RmiDebuggedEnvironmentImpl.java b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/RmiDebuggedEnvironmentImpl.java
new file mode 100644
index 0000000..38b1d0a
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/RmiDebuggedEnvironmentImpl.java
@@ -0,0 +1,340 @@
+/*
+ * 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.debug;
+
+import java.rmi.Remote;
+import java.rmi.RemoteException;
+import java.rmi.server.UnicastRemoteObject;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.MutableProcessingConfiguration;
+import org.apache.freemarker.core.ProcessingConfiguration;
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.impl.DefaultObjectWrapper;
+import org.apache.freemarker.core.model.impl.SimpleCollection;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.apache.freemarker.core.util.UndeclaredThrowableException;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+@SuppressWarnings("serial")
+class RmiDebuggedEnvironmentImpl extends RmiDebugModelImpl implements DebuggedEnvironment {
+
+    private static final SoftCache CACHE = new SoftCache(new IdentityHashMap());
+    private static final Object ID_LOCK = new Object();
+    
+    private static long nextId = 1;
+    private static Set remotes = new HashSet();
+
+    private static final DefaultObjectWrapper OBJECT_WRAPPER = new DefaultObjectWrapper.Builder(Configuration
+            .VERSION_3_0_0)
+            .build();
+    
+    private boolean stopped = false;
+    private final long id;
+    
+    private RmiDebuggedEnvironmentImpl(Environment env) throws RemoteException {
+        super(new DebugEnvironmentModel(env), DebugModel.TYPE_ENVIRONMENT);
+        synchronized (ID_LOCK) {
+            id = nextId++;
+        }
+    }
+
+    static synchronized Object getCachedWrapperFor(Object key)
+    throws RemoteException {
+        Object value = CACHE.get(key);
+        if (value == null) {
+            if (key instanceof TemplateModel) {
+                int extraTypes;
+                if (key instanceof DebugConfigurationModel) {
+                    extraTypes = DebugModel.TYPE_CONFIGURATION;
+                } else if (key instanceof DebugTemplateModel) {
+                    extraTypes = DebugModel.TYPE_TEMPLATE;
+                } else {
+                    extraTypes = 0;
+                }
+                value = new RmiDebugModelImpl((TemplateModel) key, extraTypes);
+            } else if (key instanceof Environment) {
+                value = new RmiDebuggedEnvironmentImpl((Environment) key); 
+            } else if (key instanceof Template) {
+                value = new DebugTemplateModel((Template) key);
+            } else if (key instanceof Configuration) {
+                value = new DebugConfigurationModel((Configuration) key);
+            }
+        }
+        if (value != null) {
+            CACHE.put(key, value);
+        }
+        if (value instanceof Remote) {
+            remotes.add(value);
+        }
+        return value;
+    }
+
+    // TODO See in SuppressFBWarnings
+    @Override
+    @SuppressFBWarnings(value="NN_NAKED_NOTIFY", justification="Will have to be re-desigend; postponed.")
+    public void resume() {
+        synchronized (this) {
+            notify();
+        }
+    }
+
+    @Override
+    public void stop() {
+        stopped = true;
+        resume();
+    }
+
+    @Override
+    public long getId() {
+        return id;
+    }
+    
+    boolean isStopped() {
+        return stopped;
+    }
+    
+    private abstract static class DebugMapModel implements TemplateHashModelEx {
+        @Override
+        public int size() {
+            return keySet().size();
+        }
+
+        @Override
+        public TemplateCollectionModel keys() {
+            return new SimpleCollection(keySet(), OBJECT_WRAPPER);
+        }
+
+        @Override
+        public TemplateCollectionModel values() throws TemplateModelException {
+            Collection keys = keySet();
+            List list = new ArrayList(keys.size());
+            
+            for (Iterator it = keys.iterator(); it.hasNext(); ) {
+                list.add(get((String) it.next()));
+            }
+            return new SimpleCollection(list, OBJECT_WRAPPER);
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return size() == 0;
+        }
+        
+        abstract Collection keySet();
+
+        static List composeList(Collection c1, Collection c2) {
+            List list = new ArrayList(c1);
+            list.addAll(c2);
+            Collections.sort(list);
+            return list;
+        }
+    }
+    
+    private static class DebugConfigurableModel extends DebugMapModel {
+        static final List KEYS = Arrays.asList(
+                MutableProcessingConfiguration.ARITHMETIC_ENGINE_KEY,
+                MutableProcessingConfiguration.BOOLEAN_FORMAT_KEY,
+                MutableProcessingConfiguration.LOCALE_KEY,
+                MutableProcessingConfiguration.NUMBER_FORMAT_KEY,
+                MutableProcessingConfiguration.OBJECT_WRAPPER_KEY,
+                MutableProcessingConfiguration.TEMPLATE_EXCEPTION_HANDLER_KEY);
+
+        final ProcessingConfiguration ProcessingConfiguration;
+        
+        DebugConfigurableModel(ProcessingConfiguration processingConfiguration) {
+            this.ProcessingConfiguration = processingConfiguration;
+        }
+        
+        @Override
+        Collection keySet() {
+            return KEYS;
+        }
+        
+        @Override
+        public TemplateModel get(String key) throws TemplateModelException {
+            return null; // TODO
+        }
+
+    }
+    
+    private static class DebugConfigurationModel extends DebugConfigurableModel {
+        private static final List KEYS = composeList(DebugConfigurableModel.KEYS, Collections.singleton("sharedVariables"));
+
+        private TemplateModel sharedVariables = new DebugMapModel()
+        {
+            @Override
+            Collection keySet() {
+                return ((Configuration) ProcessingConfiguration).getSharedVariables().keySet();
+            }
+        
+            @Override
+            public TemplateModel get(String key) {
+                return ((Configuration) ProcessingConfiguration).getWrappedSharedVariable(key);
+            }
+        };
+        
+        DebugConfigurationModel(Configuration config) {
+            super(config);
+        }
+        
+        @Override
+        Collection keySet() {
+            return KEYS;
+        }
+
+        @Override
+        public TemplateModel get(String key) throws TemplateModelException {
+            if ("sharedVariables".equals(key)) {
+                return sharedVariables; 
+            } else {
+                return super.get(key);
+            }
+        }
+    }
+    
+    private static class DebugTemplateModel extends DebugConfigurableModel {
+        private static final List KEYS = composeList(DebugConfigurableModel.KEYS, 
+            Arrays.asList("configuration", "name"));
+    
+        private final SimpleScalar name;
+
+        DebugTemplateModel(Template template) {
+            super(template);
+            name = new SimpleScalar(template.getLookupName());
+        }
+
+        @Override
+        Collection keySet() {
+            return KEYS;
+        }
+
+        @Override
+        public TemplateModel get(String key) throws TemplateModelException {
+            if ("configuration".equals(key)) {
+                try {
+                    return (TemplateModel) getCachedWrapperFor(((Template) ProcessingConfiguration).getConfiguration());
+                } catch (RemoteException e) {
+                    throw new TemplateModelException(e);
+                }
+            }
+            if ("name".equals(key)) {
+                return name;
+            }
+            return super.get(key);
+        }
+    }
+
+    private static class DebugEnvironmentModel extends DebugConfigurableModel {
+        private static final List KEYS = composeList(DebugConfigurableModel.KEYS, 
+            Arrays.asList(
+                    "currentNamespace",
+                    "dataModel",
+                    "globalNamespace",
+                    "knownVariables",
+                    "mainNamespace",
+                    "template"));
+    
+        private TemplateModel knownVariables = new DebugMapModel()
+        {
+            @Override
+            Collection keySet() {
+                try {
+                    return ((Environment) ProcessingConfiguration).getKnownVariableNames();
+                } catch (TemplateModelException e) {
+                    throw new UndeclaredThrowableException(e);
+                }
+            }
+        
+            @Override
+            public TemplateModel get(String key) throws TemplateModelException {
+                return ((Environment) ProcessingConfiguration).getVariable(key);
+            }
+        };
+         
+        DebugEnvironmentModel(Environment env) {
+            super(env);
+        }
+
+        @Override
+        Collection keySet() {
+            return KEYS;
+        }
+
+        @Override
+        public TemplateModel get(String key) throws TemplateModelException {
+            if ("currentNamespace".equals(key)) {
+                return ((Environment) ProcessingConfiguration).getCurrentNamespace();
+            }
+            if ("dataModel".equals(key)) {
+                return ((Environment) ProcessingConfiguration).getDataModel();
+            }
+            if ("globalNamespace".equals(key)) {
+                return ((Environment) ProcessingConfiguration).getGlobalNamespace();
+            }
+            if ("knownVariables".equals(key)) {
+                return knownVariables;
+            }
+            if ("mainNamespace".equals(key)) {
+                return ((Environment) ProcessingConfiguration).getMainNamespace();
+            }
+            if ("mainTemplate".equals(key)) {
+                try {
+                    return (TemplateModel) getCachedWrapperFor(((Environment) ProcessingConfiguration).getMainTemplate());
+                } catch (RemoteException e) {
+                    throw new TemplateModelException(e);
+                }
+            }
+            if ("currentTemplate".equals(key)) {
+                try {
+                    return (TemplateModel) getCachedWrapperFor(((Environment) ProcessingConfiguration).getCurrentTemplate());
+                } catch (RemoteException e) {
+                    throw new TemplateModelException(e);
+                }
+            }
+            return super.get(key);
+        }
+    }
+
+    public static void cleanup() {
+        for (Iterator i = remotes.iterator(); i.hasNext(); ) {
+            Object remoteObject = i.next();
+            try {
+                UnicastRemoteObject.unexportObject((Remote) remoteObject, true);
+            } catch (Exception e) {
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/debug/RmiDebuggerImpl.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/debug/RmiDebuggerImpl.java b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/RmiDebuggerImpl.java
new file mode 100644
index 0000000..ea54e4e
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/RmiDebuggerImpl.java
@@ -0,0 +1,86 @@
+/*
+ * 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.debug;
+
+import java.rmi.RemoteException;
+import java.rmi.server.UnicastRemoteObject;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ */
+class RmiDebuggerImpl
+extends
+    UnicastRemoteObject
+implements
+    Debugger {
+    private static final long serialVersionUID = 1L;
+
+    private final RmiDebuggerService service;
+    
+    protected RmiDebuggerImpl(RmiDebuggerService service) throws RemoteException {
+        this.service = service;
+    }
+
+    @Override
+    public void addBreakpoint(Breakpoint breakpoint) {
+        service.addBreakpoint(breakpoint);
+    }
+
+    @Override
+    public Object addDebuggerListener(DebuggerListener listener) {
+        return service.addDebuggerListener(listener);
+    }
+
+    @Override
+    public List getBreakpoints() {
+        return service.getBreakpointsSpi();
+    }
+
+    @Override
+    public List getBreakpoints(String templateName) {
+        return service.getBreakpointsSpi(templateName);
+    }
+
+    @Override
+    public Collection getSuspendedEnvironments() {
+        return service.getSuspendedEnvironments();
+    }
+
+    @Override
+    public void removeBreakpoint(Breakpoint breakpoint) {
+        service.removeBreakpoint(breakpoint);
+    }
+
+    @Override
+    public void removeDebuggerListener(Object id) {
+        service.removeDebuggerListener(id);
+    }
+
+    @Override
+    public void removeBreakpoints() {
+        service.removeBreakpoints();
+    }
+
+    @Override
+    public void removeBreakpoints(String templateName) {
+        service.removeBreakpoints(templateName);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/debug/RmiDebuggerListenerImpl.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/debug/RmiDebuggerListenerImpl.java b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/RmiDebuggerListenerImpl.java
new file mode 100644
index 0000000..28985ec
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/RmiDebuggerListenerImpl.java
@@ -0,0 +1,67 @@
+/*
+ * 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.debug;
+
+import java.rmi.NoSuchObjectException;
+import java.rmi.RemoteException;
+import java.rmi.server.UnicastRemoteObject;
+import java.rmi.server.Unreferenced;
+
+import org.apache.freemarker.core._CoreLogs;
+import org.apache.freemarker.core.debug.DebuggerClient;
+import org.apache.freemarker.core.debug.DebuggerListener;
+import org.apache.freemarker.core.debug.EnvironmentSuspendedEvent;
+import org.slf4j.Logger;
+
+/**
+ * Used by the {@link DebuggerClient} to invoke local
+ */
+class RmiDebuggerListenerImpl
+extends
+    UnicastRemoteObject
+implements
+    DebuggerListener, Unreferenced {
+    
+    private static final Logger LOG = _CoreLogs.DEBUG_CLIENT;
+    
+    private static final long serialVersionUID = 1L;
+
+    private final DebuggerListener listener;
+
+    @Override
+    public void unreferenced() {
+        try {
+            UnicastRemoteObject.unexportObject(this, false);
+        } catch (NoSuchObjectException e) {
+            LOG.warn("Failed to unexport RMI debugger listener", e);
+        }
+    }
+    
+    public RmiDebuggerListenerImpl(DebuggerListener listener) 
+    throws RemoteException {
+        this.listener = listener;
+    }
+
+    @Override
+    public void environmentSuspended(EnvironmentSuspendedEvent e) 
+    throws RemoteException {
+        listener.environmentSuspended(e);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/debug/RmiDebuggerService.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/debug/RmiDebuggerService.java b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/RmiDebuggerService.java
new file mode 100644
index 0000000..e44d398
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/RmiDebuggerService.java
@@ -0,0 +1,307 @@
+/*
+ * 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.debug;
+
+import java.io.Serializable;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.rmi.RemoteException;
+import java.rmi.server.RemoteObject;
+import java.rmi.server.UnicastRemoteObject;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core._Debug;
+import org.apache.freemarker.core.util.UndeclaredThrowableException;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+/**
+ * @version $Id
+ */
+class RmiDebuggerService
+extends
+        _DebuggerService {
+    private final Map templateDebugInfos = new HashMap();
+    private final HashSet suspendedEnvironments = new HashSet();
+    private final Map listeners = new HashMap();
+    private final ReferenceQueue refQueue = new ReferenceQueue();
+     
+
+    private final RmiDebuggerImpl debugger;
+    private DebuggerServer server;
+
+    RmiDebuggerService() {
+        try {
+            debugger = new RmiDebuggerImpl(this);
+            server = new DebuggerServer((Serializable) RemoteObject.toStub(debugger));
+            server.start();
+        } catch (RemoteException e) {
+            e.printStackTrace();
+            throw new UndeclaredThrowableException(e);
+        }
+    }
+    
+    @Override
+    List getBreakpointsSpi(String templateName) {
+        synchronized (templateDebugInfos) {
+            TemplateDebugInfo tdi = findTemplateDebugInfo(templateName);
+            return tdi == null ? Collections.EMPTY_LIST : tdi.breakpoints;
+        }
+    }
+
+    List getBreakpointsSpi() {
+        List sumlist = new ArrayList();
+        synchronized (templateDebugInfos) {
+            for (Iterator iter = templateDebugInfos.values().iterator(); iter.hasNext(); ) {
+                sumlist.addAll(((TemplateDebugInfo) iter.next()).breakpoints);
+            }
+        }
+        Collections.sort(sumlist);
+        return sumlist;
+    }
+
+    // TODO See in SuppressFBWarnings
+    @Override
+    @SuppressFBWarnings(value={ "UW_UNCOND_WAIT", "WA_NOT_IN_LOOP" }, justification="Will have to be re-desigend; postponed.")
+    boolean suspendEnvironmentSpi(Environment env, String templateName, int line)
+    throws RemoteException {
+        RmiDebuggedEnvironmentImpl denv = 
+            (RmiDebuggedEnvironmentImpl)
+                RmiDebuggedEnvironmentImpl.getCachedWrapperFor(env);
+                
+        synchronized (suspendedEnvironments) {
+            suspendedEnvironments.add(denv);
+        }
+        try {
+            EnvironmentSuspendedEvent breakpointEvent = 
+                new EnvironmentSuspendedEvent(this, templateName, line, denv);
+    
+            synchronized (listeners) {
+                for (Iterator iter = listeners.values().iterator(); iter.hasNext(); ) {
+                    DebuggerListener listener = (DebuggerListener) iter.next();
+                    listener.environmentSuspended(breakpointEvent);
+                }
+            }
+            synchronized (denv) {
+                try {
+                    denv.wait();
+                } catch (InterruptedException e) {
+                    // Intentionally ignored
+                }
+            }
+            return denv.isStopped();
+        } finally {
+            synchronized (suspendedEnvironments) {
+                suspendedEnvironments.remove(denv);
+            }
+        }
+    }
+    
+    @Override
+    void registerTemplateSpi(Template template) {
+        String templateName = template.getLookupName();
+        synchronized (templateDebugInfos) {
+            TemplateDebugInfo tdi = createTemplateDebugInfo(templateName);
+            tdi.templates.add(new TemplateReference(templateName, template, refQueue));
+            // Inject already defined breakpoints into the template
+            for (Iterator iter = tdi.breakpoints.iterator(); iter.hasNext(); ) {
+                Breakpoint breakpoint = (Breakpoint) iter.next();
+                _Debug.insertDebugBreak(template, breakpoint.getLine());
+            }
+        }
+    }
+    
+    Collection getSuspendedEnvironments() {
+        return (Collection) suspendedEnvironments.clone();
+    }
+
+    Object addDebuggerListener(DebuggerListener listener) {
+        Object id; 
+        synchronized (listeners) {
+            id = Long.valueOf(System.currentTimeMillis());
+            listeners.put(id, listener);
+        }
+        return id;
+    }
+    
+    void removeDebuggerListener(Object id) {
+        synchronized (listeners) {
+            listeners.remove(id);
+        }
+    }
+
+    void addBreakpoint(Breakpoint breakpoint) {
+        String templateName = breakpoint.getTemplateName();
+        synchronized (templateDebugInfos) {
+            TemplateDebugInfo tdi = createTemplateDebugInfo(templateName);
+            List breakpoints = tdi.breakpoints;
+            int pos = Collections.binarySearch(breakpoints, breakpoint);
+            if (pos < 0) {
+                // Add to the list of breakpoints
+                breakpoints.add(-pos - 1, breakpoint);
+                // Inject the breakpoint into all templates with this name
+                for (Iterator iter = tdi.templates.iterator(); iter.hasNext(); ) {
+                    TemplateReference ref = (TemplateReference) iter.next();
+                    Template t = ref.getTemplate();
+                    if (t == null) {
+                        iter.remove();
+                    } else {
+                        _Debug.insertDebugBreak(t, breakpoint.getLine());
+                    }
+                }
+            }
+        }
+    }
+
+    private TemplateDebugInfo findTemplateDebugInfo(String templateName) {
+        processRefQueue();
+        return (TemplateDebugInfo) templateDebugInfos.get(templateName); 
+    }
+    
+    private TemplateDebugInfo createTemplateDebugInfo(String templateName) {
+        TemplateDebugInfo tdi = findTemplateDebugInfo(templateName);
+        if (tdi == null) {
+            tdi = new TemplateDebugInfo();
+            templateDebugInfos.put(templateName, tdi);
+        }
+        return tdi;
+    }
+    
+    void removeBreakpoint(Breakpoint breakpoint) {
+        String templateName = breakpoint.getTemplateName();
+        synchronized (templateDebugInfos) {
+            TemplateDebugInfo tdi = findTemplateDebugInfo(templateName);
+            if (tdi != null) {
+                List breakpoints = tdi.breakpoints;
+                int pos = Collections.binarySearch(breakpoints, breakpoint);
+                if (pos >= 0) { 
+                    breakpoints.remove(pos);
+                    for (Iterator iter = tdi.templates.iterator(); iter.hasNext(); ) {
+                        TemplateReference ref = (TemplateReference) iter.next();
+                        Template t = ref.getTemplate();
+                        if (t == null) {
+                            iter.remove();
+                        } else {
+                            _Debug.removeDebugBreak(t, breakpoint.getLine());
+                        }
+                    }
+                }
+                if (tdi.isEmpty()) {
+                    templateDebugInfos.remove(templateName);
+                }
+            }
+        }
+    }
+
+    void removeBreakpoints(String templateName) {
+        synchronized (templateDebugInfos) {
+            TemplateDebugInfo tdi = findTemplateDebugInfo(templateName);
+            if (tdi != null) {
+                removeBreakpoints(tdi);
+                if (tdi.isEmpty()) {
+                    templateDebugInfos.remove(templateName);
+                }
+            }
+        }
+    }
+
+    void removeBreakpoints() {
+        synchronized (templateDebugInfos) {
+            for (Iterator iter = templateDebugInfos.values().iterator(); iter.hasNext(); ) {
+                TemplateDebugInfo tdi = (TemplateDebugInfo) iter.next(); 
+                removeBreakpoints(tdi);
+                if (tdi.isEmpty()) {
+                    iter.remove();
+                }
+            }
+        }
+    }
+
+    private void removeBreakpoints(TemplateDebugInfo tdi) {
+        tdi.breakpoints.clear();
+        for (Iterator iter = tdi.templates.iterator(); iter.hasNext(); ) {
+            TemplateReference ref = (TemplateReference) iter.next();
+            Template t = ref.getTemplate();
+            if (t == null) {
+                iter.remove();
+            } else {
+                _Debug.removeDebugBreaks(t);
+            }
+        }
+    }
+
+    private static final class TemplateDebugInfo {
+        final List templates = new ArrayList();
+        final List breakpoints = new ArrayList();
+        
+        boolean isEmpty() {
+            return templates.isEmpty() && breakpoints.isEmpty();
+        }
+    }
+    
+    private static final class TemplateReference extends WeakReference {
+        final String templateName;
+         
+        TemplateReference(String templateName, Template template, ReferenceQueue queue) {
+            super(template, queue);
+            this.templateName = templateName;
+        }
+        
+        Template getTemplate() {
+            return (Template) get();
+        }
+    }
+    
+    private void processRefQueue() {
+        for (; ; ) {
+            TemplateReference ref = (TemplateReference) refQueue.poll();
+            if (ref == null) {
+                break;
+            }
+            TemplateDebugInfo tdi = findTemplateDebugInfo(ref.templateName);
+            if (tdi != null) {
+                tdi.templates.remove(ref);
+                if (tdi.isEmpty()) {
+                    templateDebugInfos.remove(ref.templateName);
+                }
+            }
+        }
+    }
+
+    @Override
+    void shutdownSpi() {
+        server.stop();
+        try {
+            UnicastRemoteObject.unexportObject(debugger, true);
+        } catch (Exception e) {
+        }
+
+        RmiDebuggedEnvironmentImpl.cleanup();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/debug/SoftCache.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/debug/SoftCache.java b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/SoftCache.java
new file mode 100644
index 0000000..730574a
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/SoftCache.java
@@ -0,0 +1,89 @@
+/*
+ * 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.debug;
+
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.SoftReference;
+import java.util.Map;
+
+class SoftCache {
+    
+    private final ReferenceQueue queue = new ReferenceQueue();
+    private final Map map;
+    
+    public SoftCache(Map backingMap) {
+        map = backingMap;
+    }
+    
+    public Object get(Object key) {
+        processQueue();
+        Reference ref = (Reference) map.get(key);
+        return ref == null ? null : ref.get();
+    }
+
+    public void put(Object key, Object value) {
+        processQueue();
+        map.put(key, new SoftValueReference(key, value, queue));
+    }
+
+    public void remove(Object key) {
+        processQueue();
+        map.remove(key);
+    }
+
+    public void clear() {
+        map.clear();
+        processQueue();
+    }
+    
+    /**
+     * Returns a close approximation of the number of cache entries.
+     */
+    public int getSize() {
+        processQueue();
+        return map.size();
+    }
+
+    private void processQueue() {
+        for (; ; ) {
+            SoftValueReference ref = (SoftValueReference) queue.poll();
+            if (ref == null) {
+                return;
+            }
+            Object key = ref.getKey();
+            map.remove(key);
+        }
+    }
+
+    private static final class SoftValueReference extends SoftReference {
+        private final Object key;
+
+        SoftValueReference(Object key, Object value, ReferenceQueue queue) {
+            super(value, queue);
+            this.key = key;
+        }
+
+        Object getKey() {
+            return key;
+        }
+    }
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/debug/_DebuggerService.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/debug/_DebuggerService.java b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/_DebuggerService.java
new file mode 100644
index 0000000..37d094c
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/_DebuggerService.java
@@ -0,0 +1,93 @@
+/*
+ * 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.debug;
+
+import java.rmi.RemoteException;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core.util._SecurityUtil;
+
+/**
+ * Don't use this; used internally by FreeMarker, might changes without notice.
+ * This class provides debugging hooks for the core FreeMarker engine. It is
+ * not usable for anyone outside the FreeMarker core classes.
+ */
+public abstract class _DebuggerService {
+    private static final _DebuggerService INSTANCE = createInstance();
+    
+    private static _DebuggerService createInstance() {
+        // Creates the appropriate service class. If the debugging is turned
+        // off, this is a fast no-op service, otherwise it's the real-thing
+        // RMI service.
+        return 
+            _SecurityUtil.getSystemProperty("org.apache.freemarker.core.debug.password", null) == null
+            ? new NoOpDebuggerService()
+            : new RmiDebuggerService();
+    }
+
+    public static List getBreakpoints(String templateName) {
+        return INSTANCE.getBreakpointsSpi(templateName);
+    }
+    
+    abstract List getBreakpointsSpi(String templateName);
+
+    public static void registerTemplate(Template template) {
+        INSTANCE.registerTemplateSpi(template);
+    }
+    
+    abstract void registerTemplateSpi(Template template);
+    
+    public static boolean suspendEnvironment(Environment env, String templateName, int line)
+    throws RemoteException {
+        return INSTANCE.suspendEnvironmentSpi(env, templateName, line);
+    }
+    
+    abstract boolean suspendEnvironmentSpi(Environment env, String templateName, int line)
+    throws RemoteException;
+
+    abstract void shutdownSpi();
+
+    public static void shutdown() {
+        INSTANCE.shutdownSpi();
+    }
+
+    private static class NoOpDebuggerService extends _DebuggerService {
+        @Override
+        List getBreakpointsSpi(String templateName) {
+            return Collections.EMPTY_LIST;
+        }
+        
+        @Override
+        boolean suspendEnvironmentSpi(Environment env, String templateName, int line) {
+            throw new UnsupportedOperationException();
+        }
+        
+        @Override
+        void registerTemplateSpi(Template template) {
+        }
+
+        @Override
+        void shutdownSpi() {
+        }
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/debug/package.html
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/debug/package.html b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/package.html
new file mode 100644
index 0000000..677b842
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/package.html
@@ -0,0 +1,27 @@
+<!--
+  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.
+-->
+<html>
+<head>
+</head>
+<body bgcolor="white">
+<p>Debugging API; experimental status, might change!
+This is to support debugging in IDE-s. If you are working on a client for this,
+don't hesitate to contact us!</p>
+</body>
+</html>

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/AdapterTemplateModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/AdapterTemplateModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/AdapterTemplateModel.java
new file mode 100644
index 0000000..9dc6587
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/AdapterTemplateModel.java
@@ -0,0 +1,49 @@
+/*
+ * 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;
+
+/**
+ * A {@link TemplateModel} that can be unwrapped and then it considers a provided desired (hint) class. This is
+ * useful when multiple languages has to communicate with each other through FreeMarker. For example, if we have a
+ * model that wraps a Jython object, then we have to unwrap that differently when we pass it to plain Java method and
+ * when we pass it to a Jython method.
+ * 
+ * <p>This is rarely implemented by applications. It is typically implemented by the model classes belonging to
+ * {@link ObjectWrapper}-s.
+ */
+public interface AdapterTemplateModel extends TemplateModel {
+    /**
+     * Retrieves the underlying object, or some other object semantically 
+     * equivalent to its value narrowed by the class hint.   
+     * @param hint the desired class of the returned value. An implementation 
+     * should make reasonable effort to retrieve an object of the requested 
+     * class, but if that is impossible, it must at least return the underlying 
+     * object as-is. As a minimal requirement, an implementation must always 
+     * return the exact underlying object when 
+     * <tt>hint.isInstance(underlyingObject)</tt> holds. When called 
+     * with <tt>java.lang.Object.class</tt>, it should return a generic Java 
+     * object (i.e. if the model is wrapping a scripting language object that is
+     * further wrapping a Java object, the deepest underlying Java object should
+     * be returned). 
+     * @return the underlying object, or its value accommodated for the hint
+     * class.
+     */
+    Object getAdaptedObject(Class<?> hint);
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/Constants.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/Constants.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/Constants.java
new file mode 100644
index 0000000..268188d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/Constants.java
@@ -0,0 +1,133 @@
+/*
+ * 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;
+
+import java.io.Serializable;
+
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+
+/**
+ * Frequently used constant {@link TemplateModel} values.
+ * 
+ * <p>These constants should be stored in the {@link TemplateModel}
+ * sub-interfaces, but for bacward compatibility they are stored here instead.
+ * Starting from FreeMarker 2.4 they should be copyed (not moved!) into the
+ * {@link TemplateModel} sub-interfaces, and this class should be marked as
+ * deprecated.</p>
+ */
+public class Constants {
+
+    public static final TemplateBooleanModel TRUE = TemplateBooleanModel.TRUE;
+
+    public static final TemplateBooleanModel FALSE = TemplateBooleanModel.FALSE;
+    
+    public static final TemplateScalarModel EMPTY_STRING = (TemplateScalarModel) TemplateScalarModel.EMPTY_STRING;
+
+    public static final TemplateNumberModel ZERO = new SimpleNumber(0);
+    
+    public static final TemplateNumberModel ONE = new SimpleNumber(1);
+    
+    public static final TemplateNumberModel MINUS_ONE = new SimpleNumber(-1);
+    
+    public static final TemplateModelIterator EMPTY_ITERATOR = new EmptyIteratorModel();
+    
+    private static class EmptyIteratorModel implements TemplateModelIterator, Serializable {
+
+        @Override
+        public TemplateModel next() throws TemplateModelException {
+            throw new TemplateModelException("The collection has no more elements.");
+        }
+
+        @Override
+        public boolean hasNext() throws TemplateModelException {
+            return false;
+        }
+        
+    }
+
+    public static final TemplateCollectionModelEx EMPTY_COLLECTION = new EmptyCollectionExModel();
+    
+    private static class EmptyCollectionExModel implements TemplateCollectionModelEx, Serializable {
+
+        @Override
+        public int size() throws TemplateModelException {
+            return 0;
+        }
+
+        @Override
+        public boolean isEmpty() throws TemplateModelException {
+            return true;
+        }
+
+        @Override
+        public TemplateModelIterator iterator() throws TemplateModelException {
+            return EMPTY_ITERATOR;
+        }
+        
+    }
+    
+    public static final TemplateSequenceModel EMPTY_SEQUENCE = new EmptySequenceModel();
+    
+    private static class EmptySequenceModel implements TemplateSequenceModel, Serializable {
+        
+        @Override
+        public TemplateModel get(int index) throws TemplateModelException {
+            return null;
+        }
+    
+        @Override
+        public int size() throws TemplateModelException {
+            return 0;
+        }
+        
+    }
+    
+    public static final TemplateHashModelEx EMPTY_HASH = new EmptyHashModel();
+    
+    private static class EmptyHashModel implements TemplateHashModelEx, Serializable {
+        
+        @Override
+        public int size() throws TemplateModelException {
+            return 0;
+        }
+
+        @Override
+        public TemplateCollectionModel keys() throws TemplateModelException {
+            return EMPTY_COLLECTION;
+        }
+
+        @Override
+        public TemplateCollectionModel values() throws TemplateModelException {
+            return EMPTY_COLLECTION;
+        }
+
+        @Override
+        public TemplateModel get(String key) throws TemplateModelException {
+            return null;
+        }
+
+        @Override
+        public boolean isEmpty() throws TemplateModelException {
+            return true;
+        }
+        
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/FalseTemplateBooleanModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/FalseTemplateBooleanModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/FalseTemplateBooleanModel.java
new file mode 100644
index 0000000..0fd848d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/FalseTemplateBooleanModel.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+/**
+ * Used for the {@link TemplateBooleanModel#TRUE} singleton. 
+ */
+final class FalseTemplateBooleanModel implements SerializableTemplateBooleanModel {
+    
+    @Override
+    public boolean getAsBoolean() {
+        return false;
+    }
+
+    private Object readResolve() {
+        return FALSE;
+    }
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/GeneralPurposeNothing.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/GeneralPurposeNothing.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/GeneralPurposeNothing.java
new file mode 100644
index 0000000..da1c102
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/GeneralPurposeNothing.java
@@ -0,0 +1,83 @@
+/*
+ * 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;
+
+import java.util.List;
+
+/**
+ * Singleton object representing nothing, used by ?if_exists built-in.
+ * It is meant to be interpreted in the most sensible way possible in various contexts.
+ * This can be returned to avoid exceptions.
+ */
+
+final class GeneralPurposeNothing
+implements TemplateBooleanModel, TemplateScalarModel, TemplateSequenceModel, TemplateHashModelEx, TemplateMethodModelEx {
+
+    public static final TemplateModel INSTANCE = new GeneralPurposeNothing();
+      
+    private GeneralPurposeNothing() {
+    }
+
+    @Override
+    public String getAsString() {
+        return "";
+    }
+
+    @Override
+    public boolean getAsBoolean() {
+        return false;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return true;
+    }
+
+    @Override
+    public int size() {
+        return 0;
+    }
+
+    @Override
+    public TemplateModel get(int i) throws TemplateModelException {
+        throw new TemplateModelException("Empty list");
+    }
+
+    @Override
+    public TemplateModel get(String key) {
+        return null;
+    }
+
+    @Override
+    public Object exec(List args) {
+        return null;
+    }
+    
+    @Override
+    public TemplateCollectionModel keys() {
+        return Constants.EMPTY_COLLECTION;
+    }
+
+    @Override
+    public TemplateCollectionModel values() {
+        return Constants.EMPTY_COLLECTION;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/ObjectWrapper.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/ObjectWrapper.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/ObjectWrapper.java
new file mode 100644
index 0000000..42f09d8
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/ObjectWrapper.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.model;
+
+import java.util.Map;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.model.impl.DefaultObjectWrapper;
+
+/**
+ * Maps Java objects to the type-system of FreeMarker Template Language (see the {@link TemplateModel}
+ * interfaces). Thus this is what decides what parts of the Java objects will be accessible in the templates and how.
+ * 
+ * <p>For example, with a {@link DefaultObjectWrapper} both the items of {@link Map} and the JavaBean properties (the getters)
+ * of an object are accessible in template uniformly with the {@code myObject.foo} syntax, where "foo" is the map key or
+ * the property name. This is because both kind of object is wrapped by {@link DefaultObjectWrapper} into a
+ * {@link TemplateHashModel} implementation that will call {@link Map#get(Object)} or the getter method, transparently
+ * to the template language.
+ * 
+ * @see Configuration#getObjectWrapper()
+ */
+public interface ObjectWrapper {
+    
+    /**
+     * Makes a {@link TemplateModel} out of a non-{@link TemplateModel} object, usually by "wrapping" it into a
+     * {@link TemplateModel} implementation that delegates to the original object.
+     * 
+     * @param obj The object to wrap into a {@link TemplateModel}. If it already implements {@link TemplateModel},
+     *      it should just return the object as is. If it's {@code null}, the method should return {@code null}
+     *      (however, {@link DefaultObjectWrapper}, has a legacy option for returning a null model object instead, but it's not
+     *      a good idea).
+     * 
+     * @return a {@link TemplateModel} wrapper of the object passed in. To support un-wrapping, you may consider the
+     *     return value to implement {@link WrapperTemplateModel} and {@link AdapterTemplateModel}.  
+     *     It's normally expectated that the {@link TemplateModel} isn't less thread safe than the wrapped object.
+     *     If the {@link ObjectWrapper} returns less thread safe objects that should be clearly documented, as it
+     *     restricts how it can be used, like, then it can't be used to wrap
+     *     {@linkplain Configuration#getSharedVariables() shared variables}).
+     */
+    TemplateModel wrap(Object obj) throws TemplateModelException;
+    
+}



[37/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java b/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java
new file mode 100644
index 0000000..6713200
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java
@@ -0,0 +1,3213 @@
+/*
+ * 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.PrintWriter;
+import java.io.Serializable;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.nio.charset.Charset;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.text.Collator;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TimeZone;
+
+import org.apache.freemarker.core.arithmetic.ArithmeticEngine;
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateDirectiveBody;
+import org.apache.freemarker.core.model.TemplateDirectiveModel;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+import org.apache.freemarker.core.model.TemplateNodeModel;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.TemplateTransformModel;
+import org.apache.freemarker.core.model.TransformControl;
+import org.apache.freemarker.core.model.impl.SimpleHash;
+import org.apache.freemarker.core.templateresolver.MalformedTemplateNameException;
+import org.apache.freemarker.core.templateresolver.TemplateNameFormat;
+import org.apache.freemarker.core.templateresolver.TemplateResolver;
+import org.apache.freemarker.core.templateresolver._CacheAPI;
+import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateNameFormat;
+import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateNameFormatFM2;
+import org.apache.freemarker.core.util.UndeclaredThrowableException;
+import org.apache.freemarker.core.util._DateUtil;
+import org.apache.freemarker.core.util._DateUtil.DateToISO8601CalendarFactory;
+import org.apache.freemarker.core.util._NullWriter;
+import org.apache.freemarker.core.util._StringUtil;
+import org.apache.freemarker.core.valueformat.TemplateDateFormat;
+import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormat;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory;
+import org.apache.freemarker.core.valueformat.TemplateValueFormatException;
+import org.apache.freemarker.core.valueformat.UndefinedCustomFormatException;
+import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException;
+import org.apache.freemarker.core.valueformat.impl.ISOTemplateDateFormatFactory;
+import org.apache.freemarker.core.valueformat.impl.JavaTemplateDateFormatFactory;
+import org.apache.freemarker.core.valueformat.impl.JavaTemplateNumberFormatFactory;
+import org.apache.freemarker.core.valueformat.impl.XSTemplateDateFormatFactory;
+import org.slf4j.Logger;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+/**
+ * Object that represents the runtime environment during template processing. For every invocation of a
+ * <tt>Template.process()</tt> method, a new instance of this object is created, and then discarded when
+ * <tt>process()</tt> returns. This object stores the set of temporary variables created by the template, the value of
+ * settings set by the template, the reference to the data model root, etc. Everything that is needed to fulfill the
+ * template processing job.
+ *
+ * <p>
+ * Data models that need to access the <tt>Environment</tt> object that represents the template processing on the
+ * current thread can use the {@link #getCurrentEnvironment()} method.
+ *
+ * <p>
+ * If you need to modify or read this object before or after the <tt>process</tt> call, use
+ * {@link Template#createProcessingEnvironment(Object rootMap, Writer out, ObjectWrapper wrapper)}
+ */
+public final class Environment extends MutableProcessingConfiguration<Environment> implements CustomStateScope {
+    
+    private static final ThreadLocal<Environment> TLS_ENVIRONMENT = new ThreadLocal();
+
+    private static final Logger LOG = _CoreLogs.RUNTIME;
+    private static final Logger LOG_ATTEMPT = _CoreLogs.ATTEMPT;
+
+    // Do not use this object directly; deepClone it first! DecimalFormat isn't
+    // thread-safe.
+    private static final DecimalFormat C_NUMBER_FORMAT = new DecimalFormat(
+            "0.################",
+            new DecimalFormatSymbols(Locale.US));
+
+    static {
+        C_NUMBER_FORMAT.setGroupingUsed(false);
+        C_NUMBER_FORMAT.setDecimalSeparatorAlwaysShown(false);
+    }
+
+    private final Configuration configuration;
+    private final TemplateHashModel rootDataModel;
+    private ASTElement[] instructionStack = new ASTElement[16];
+    private int instructionStackSize = 0;
+    private final ArrayList recoveredErrorStack = new ArrayList();
+
+    private TemplateNumberFormat cachedTemplateNumberFormat;
+    private Map<String, TemplateNumberFormat> cachedTemplateNumberFormats;
+    private Map<CustomStateKey, Object> customStateMap;
+
+    private TemplateBooleanFormat cachedTemplateBooleanFormat;
+
+    /**
+     * Stores the date/time/date-time formatters that are used when no format is explicitly given at the place of
+     * formatting. That is, in situations like ${lastModified} or even ${lastModified?date}, but not in situations like
+     * ${lastModified?string.iso}.
+     * 
+     * <p>
+     * The index of the array is calculated from what kind of formatter we want (see
+     * {@link #getTemplateDateFormatCacheArrayIndex(int, boolean, boolean)}):<br>
+     * Zoned input: 0: U, 1: T, 2: D, 3: DT<br>
+     * Zoneless input: 4: U, 5: T, 6: D, 7: DT<br>
+     * SQL D T TZ + Zoned input: 8: U, 9: T, 10: D, 11: DT<br>
+     * SQL D T TZ + Zoneless input: 12: U, 13: T, 14: D, 15: DT
+     * 
+     * <p>
+     * This is a lazily filled cache. It starts out as {@code null}, then when first needed the array will be created.
+     * The array elements also start out as {@code null}-s, and they are filled as the particular kind of formatter is
+     * first needed.
+     */
+    private TemplateDateFormat[] cachedTempDateFormatArray;
+    /** Similar to {@link #cachedTempDateFormatArray}, but used when a formatting string was specified. */
+    private HashMap<String, TemplateDateFormat>[] cachedTempDateFormatsByFmtStrArray;
+    private static final int CACHED_TDFS_ZONELESS_INPUT_OFFS = 4;
+    private static final int CACHED_TDFS_SQL_D_T_TZ_OFFS = CACHED_TDFS_ZONELESS_INPUT_OFFS * 2;
+    private static final int CACHED_TDFS_LENGTH = CACHED_TDFS_SQL_D_T_TZ_OFFS * 2;
+
+    /** Caches the result of {@link #isSQLDateAndTimeTimeZoneSameAsNormal()}. */
+    private Boolean cachedSQLDateAndTimeTimeZoneSameAsNormal;
+
+    private NumberFormat cNumberFormat;
+
+    /**
+     * Used by the "iso_" built-ins to accelerate formatting.
+     * 
+     * @see #getISOBuiltInCalendarFactory()
+     */
+    private DateToISO8601CalendarFactory isoBuiltInCalendarFactory;
+
+    private Collator cachedCollator;
+
+    private Writer out;
+    private ASTDirMacro.Context currentMacroContext;
+    private LocalContextStack localContextStack;
+    private final Template mainTemplate;
+    private final Namespace mainNamespace;
+    private Namespace currentNamespace, globalNamespace;
+    private HashMap<String, Namespace> loadedLibs;
+
+    private boolean inAttemptBlock;
+    private Throwable lastThrowable;
+
+    private TemplateModel lastReturnValue;
+    private HashMap macroToNamespaceLookup = new HashMap();
+
+    private TemplateNodeModel currentVisitorNode;
+    private TemplateSequenceModel nodeNamespaces;
+    // Things we keep track of for the fallback mechanism.
+    private int nodeNamespaceIndex;
+    private String currentNodeName, currentNodeNS;
+
+    private Charset cachedURLEscapingCharset;
+    private boolean cachedURLEscapingCharsetSet;
+
+    private boolean fastInvalidReferenceExceptions;
+
+    /**
+     * Retrieves the environment object associated with the current thread, or {@code null} if there's no template
+     * processing going on in this thread. Data model implementations that need access to the environment can call this
+     * method to obtain the environment object that represents the template processing that is currently running on the
+     * current thread.
+     */
+    public static Environment getCurrentEnvironment() {
+        return TLS_ENVIRONMENT.get();
+    }
+
+    public static Environment getCurrentEnvironmentNotNull() {
+        Environment currentEnvironment = getCurrentEnvironment();
+        if (currentEnvironment == null) {
+            throw new IllegalStateException("There's no FreeMarker Environemnt in this this thread.");
+        }
+        return currentEnvironment;
+    }
+
+    static void setCurrentEnvironment(Environment env) {
+        TLS_ENVIRONMENT.set(env);
+    }
+
+    public Environment(Template template, final TemplateHashModel rootDataModel, Writer out) {
+        mainTemplate = template;
+        configuration = template.getConfiguration();
+        globalNamespace = new Namespace(null);
+        currentNamespace = mainNamespace = new Namespace(mainTemplate);
+        this.out = out;
+        this.rootDataModel = rootDataModel;
+        importMacros(template);
+    }
+
+    /**
+     * Returns the topmost {@link Template}, with other words, the one for which this {@link Environment} was created.
+     * That template will never change, like {@code #include} or macro calls don't change it.
+     * 
+     * @see #getCurrentNamespace()
+     * 
+     * @since 2.3.22
+     */
+    public Template getMainTemplate() {
+        return mainTemplate;
+    }
+
+    /**
+     * Returns the {@link Template} that we are "lexically" inside at the moment. This template will change when
+     * entering an {@code #include} or calling a macro or function in another template, or returning to yet another
+     * template with {@code #nested}. As such, it's useful in {@link TemplateDirectiveModel} to find out if from where
+     * the directive was called from.
+     * 
+     * @see #getMainTemplate()
+     * @see #getCurrentNamespace()
+     * 
+     * @since 2.3.23
+     */
+    @SuppressFBWarnings(value = "RANGE_ARRAY_INDEX", justification = "False alarm")
+    public Template getCurrentTemplate() {
+        int ln = instructionStackSize;
+        return ln == 0 ? getMainTemplate() : instructionStack[ln - 1].getTemplate();
+    }
+
+    public Template getCurrentTemplateNotNull() {
+        Template currentTemplate = getCurrentTemplate();
+        if (currentTemplate == null) {
+            throw new IllegalStateException("There's no current template at the moment.");
+        }
+        return currentTemplate;
+    }
+
+    /**
+     * Gets the currently executing <em>custom</em> directive's call place information, or {@code null} if there's no
+     * executing custom directive. This currently only works for calls made from templates with the {@code <@...>}
+     * syntax. This should only be called from the {@link TemplateDirectiveModel} that was invoked with {@code <@...>},
+     * otherwise its return value is not defined by this API (it's usually {@code null}).
+     * 
+     * @since 2.3.22
+     */
+    @SuppressFBWarnings(value = "RANGE_ARRAY_INDEX", justification = "False alarm")
+    public DirectiveCallPlace getCurrentDirectiveCallPlace() {
+        int ln = instructionStackSize;
+        if (ln == 0) return null;
+        ASTElement te = instructionStack[ln - 1];
+        if (te instanceof ASTDirUserDefined) return (ASTDirUserDefined) te;
+        if (te instanceof ASTDirMacro && ln > 1 && instructionStack[ln - 2] instanceof ASTDirUserDefined) {
+            return (ASTDirUserDefined) instructionStack[ln - 2];
+        }
+        return null;
+    }
+
+    /**
+     * Deletes cached values that meant to be valid only during a single template execution.
+     */
+    private void clearCachedValues() {
+        cachedTemplateNumberFormats = null;
+        cachedTemplateNumberFormat = null;
+
+        cachedTempDateFormatArray = null;
+        cachedTempDateFormatsByFmtStrArray = null;
+
+        cachedCollator = null;
+        cachedURLEscapingCharset = null;
+        cachedURLEscapingCharsetSet = false;
+    }
+
+    /**
+     * Processes the template to which this environment belongs to.
+     */
+    public void process() throws TemplateException, IOException {
+        Environment savedEnv = TLS_ENVIRONMENT.get();
+        TLS_ENVIRONMENT.set(this);
+        try {
+            // Cached values from a previous execution are possibly outdated.
+            clearCachedValues();
+            try {
+                doAutoImportsAndIncludes(this);
+                visit(getMainTemplate().getRootASTNode());
+                // It's here as we must not flush if there was an exception.
+                if (getAutoFlush()) {
+                    out.flush();
+                }
+            } finally {
+                // It's just to allow the GC to free memory...
+                clearCachedValues();
+            }
+        } finally {
+            TLS_ENVIRONMENT.set(savedEnv);
+        }
+    }
+
+    /**
+     * Executes the auto-imports and auto-includes for the main template of this environment.
+     * This is not meant to be called or overridden by code outside of FreeMarker.
+     */
+    private void doAutoImportsAndIncludes(Environment env) throws TemplateException, IOException {
+        Template t = getMainTemplate();
+        doAutoImports(t);
+        doAutoIncludes(t);
+    }
+
+    private void doAutoImports(Template t) throws IOException, TemplateException {
+        Map<String, String> envAutoImports = isAutoImportsSet() ? getAutoImports() : null;
+        Map<String, String> tAutoImports = t.isAutoImportsSet() ? t.getAutoImports() : null;
+
+        boolean lazyAutoImports = getLazyAutoImports() != null ? getLazyAutoImports() : getLazyImports();
+
+        for (Map.Entry<String, String> autoImport : configuration.getAutoImports().entrySet()) {
+            String nsVarName = autoImport.getKey();
+            if ((tAutoImports == null || !tAutoImports.containsKey(nsVarName))
+                    && (envAutoImports == null || !envAutoImports.containsKey(nsVarName))) {
+                importLib(autoImport.getValue(), nsVarName, lazyAutoImports);
+            }
+        }
+        if (tAutoImports != null) {
+            for (Map.Entry<String, String> autoImport : tAutoImports.entrySet()) {
+                String nsVarName = autoImport.getKey();
+                if (envAutoImports == null || !envAutoImports.containsKey(nsVarName)) {
+                    importLib(autoImport.getValue(), nsVarName, lazyAutoImports);
+                }
+            }
+        }
+        if (envAutoImports != null) {
+            for (Map.Entry<String, String> autoImport : envAutoImports.entrySet()) {
+                String nsVarName = autoImport.getKey();
+                importLib(autoImport.getValue(), nsVarName, lazyAutoImports);
+            }
+        }
+    }
+
+    private void doAutoIncludes(Template t) throws TemplateException, IOException {
+        // We can't store autoIncludes in LinkedHashSet-s because setAutoIncludes(List) allows duplicates,
+        // unfortunately. Yet we have to prevent duplicates among Configuration levels, with the lowest levels having
+        // priority. So we build some Set-s to do that, but we avoid the most common cases where they aren't needed.
+
+        List<String> tAutoIncludes = t.isAutoIncludesSet() ? t.getAutoIncludes() : null;
+        List<String> envAutoIncludes = isAutoIncludesSet() ? getAutoIncludes() : null;
+
+        for (String templateName : configuration.getAutoIncludes()) {
+            if ((tAutoIncludes == null || !tAutoIncludes.contains(templateName))
+                    && (envAutoIncludes == null || !envAutoIncludes.contains(templateName))) {
+                include(configuration.getTemplate(templateName, getLocale()));
+            }
+        }
+
+        if (tAutoIncludes != null) {
+            for (String templateName : tAutoIncludes) {
+                if (envAutoIncludes == null || !envAutoIncludes.contains(templateName)) {
+                    include(configuration.getTemplate(templateName, getLocale()));
+                }
+            }
+        }
+
+        if (envAutoIncludes != null) {
+            for (String templateName : envAutoIncludes) {
+                include(configuration.getTemplate(templateName, getLocale()));
+            }
+        }
+    }
+
+    /**
+     * "Visit" the template element.
+     */
+    void visit(ASTElement element) throws IOException, TemplateException {
+        // ATTENTION: This method body is manually "inlined" into visit(ASTElement[]); keep them in sync!
+        pushElement(element);
+        try {
+            ASTElement[] templateElementsToVisit = element.accept(this);
+            if (templateElementsToVisit != null) {
+                for (ASTElement el : templateElementsToVisit) {
+                    if (el == null) {
+                        break;  // Skip unused trailing buffer capacity 
+                    }
+                    visit(el);
+                }
+            }
+        } catch (TemplateException te) {
+            handleTemplateException(te);
+        } finally {
+            popElement();
+        }
+        // ATTENTION: This method body above is manually "inlined" into visit(ASTElement[]); keep them in sync!
+    }
+    
+    /**
+     * @param elementBuffer
+     *            The elements to visit; might contains trailing {@code null}-s. Can be {@code null}.
+     * 
+     * @since 2.3.24
+     */
+    final void visit(ASTElement[] elementBuffer) throws IOException, TemplateException {
+        if (elementBuffer == null) {
+            return;
+        }
+        for (ASTElement element : elementBuffer) {
+            if (element == null) {
+                break;  // Skip unused trailing buffer capacity 
+            }
+            
+            // ATTENTION: This part is the manually "inlining" of visit(ASTElement[]); keep them in sync!
+            // We don't just let Hotspot to do it, as we want a hard guarantee regarding maximum stack usage. 
+            pushElement(element);
+            try {
+                ASTElement[] templateElementsToVisit = element.accept(this);
+                if (templateElementsToVisit != null) {
+                    for (ASTElement el : templateElementsToVisit) {
+                        if (el == null) {
+                            break;  // Skip unused trailing buffer capacity 
+                        }
+                        visit(el);
+                    }
+                }
+            } catch (TemplateException te) {
+                handleTemplateException(te);
+            } finally {
+                popElement();
+            }
+            // ATTENTION: This part above is the manually "inlining" of visit(ASTElement[]); keep them in sync!
+        }
+    }
+
+    @SuppressFBWarnings(value = "RANGE_ARRAY_INDEX", justification = "Not called when stack is empty")
+    private ASTElement replaceTopElement(ASTElement element) {
+        return instructionStack[instructionStackSize - 1] = element;
+    }
+
+    private static final TemplateModel[] NO_OUT_ARGS = new TemplateModel[0];
+
+    void visit(final ASTElement element,
+            TemplateDirectiveModel directiveModel, Map args,
+            final List bodyParameterNames) throws TemplateException, IOException {
+        visit(new ASTElement[] { element }, directiveModel, args, bodyParameterNames);
+    }
+    
+    void visit(final ASTElement[] childBuffer,
+            TemplateDirectiveModel directiveModel, Map args,
+            final List bodyParameterNames) throws TemplateException, IOException {
+        TemplateDirectiveBody nested;
+        if (childBuffer == null) {
+            nested = null;
+        } else {
+            nested = new NestedElementTemplateDirectiveBody(childBuffer);
+        }
+        final TemplateModel[] outArgs;
+        if (bodyParameterNames == null || bodyParameterNames.isEmpty()) {
+            outArgs = NO_OUT_ARGS;
+        } else {
+            outArgs = new TemplateModel[bodyParameterNames.size()];
+        }
+        if (outArgs.length > 0) {
+            pushLocalContext(new LocalContext() {
+
+                @Override
+                public TemplateModel getLocalVariable(String name) {
+                    int index = bodyParameterNames.indexOf(name);
+                    return index != -1 ? outArgs[index] : null;
+                }
+
+                @Override
+                public Collection getLocalVariableNames() {
+                    return bodyParameterNames;
+                }
+            });
+        }
+        try {
+            directiveModel.execute(this, args, outArgs, nested);
+        } finally {
+            if (outArgs.length > 0) {
+                localContextStack.pop();
+            }
+        }
+    }
+
+    /**
+     * "Visit" the template element, passing the output through a TemplateTransformModel
+     * 
+     * @param elementBuffer
+     *            the element to visit through a transform; might contains trailing {@code null}-s
+     * @param transform
+     *            the transform to pass the element output through
+     * @param args
+     *            optional arguments fed to the transform
+     */
+    void visitAndTransform(ASTElement[] elementBuffer,
+            TemplateTransformModel transform,
+            Map args)
+                    throws TemplateException, IOException {
+        try {
+            Writer tw = transform.getWriter(out, args);
+            if (tw == null) tw = EMPTY_BODY_WRITER;
+            TransformControl tc = tw instanceof TransformControl
+                    ? (TransformControl) tw
+                    : null;
+
+            Writer prevOut = out;
+            out = tw;
+            try {
+                if (tc == null || tc.onStart() != TransformControl.SKIP_BODY) {
+                    do {
+                        visit(elementBuffer);
+                    } while (tc != null && tc.afterBody() == TransformControl.REPEAT_EVALUATION);
+                }
+            } catch (Throwable t) {
+                try {
+                    if (tc != null) {
+                        tc.onError(t);
+                    } else {
+                        throw t;
+                    }
+                } catch (TemplateException e) {
+                    throw e;
+                } catch (IOException e) {
+                    throw e;
+                } catch (RuntimeException e) {
+                    throw e;
+                } catch (Error e) {
+                    throw e;
+                } catch (Throwable e) {
+                    throw new UndeclaredThrowableException(e);
+                }
+            } finally {
+                out = prevOut;
+                tw.close();
+            }
+        } catch (TemplateException te) {
+            handleTemplateException(te);
+        }
+    }
+
+    /**
+     * Visit a block using buffering/recovery
+     */
+     void visitAttemptRecover(
+             ASTDirAttemptRecoverContainer attemptBlock, ASTElement attemptedSection, ASTDirRecover recoverySection)
+             throws TemplateException, IOException {
+        Writer prevOut = out;
+        StringWriter sw = new StringWriter();
+         out = sw;
+        TemplateException thrownException = null;
+        boolean lastFIRE = setFastInvalidReferenceExceptions(false);
+        boolean lastInAttemptBlock = inAttemptBlock;
+        try {
+            inAttemptBlock = true;
+            visit(attemptedSection);
+        } catch (TemplateException te) {
+            thrownException = te;
+        } finally {
+            inAttemptBlock = lastInAttemptBlock;
+            setFastInvalidReferenceExceptions(lastFIRE);
+            out = prevOut;
+        }
+        if (thrownException != null) {
+            if (LOG_ATTEMPT.isDebugEnabled()) {
+                LOG_ATTEMPT.debug("Error in attempt block " +
+                        attemptBlock.getStartLocationQuoted(), thrownException);
+            }
+            try {
+                recoveredErrorStack.add(thrownException);
+                visit(recoverySection);
+            } finally {
+                recoveredErrorStack.remove(recoveredErrorStack.size() - 1);
+            }
+        } else {
+            out.write(sw.toString());
+        }
+    }
+
+    String getCurrentRecoveredErrorMessage() throws TemplateException {
+        if (recoveredErrorStack.isEmpty()) {
+            throw new _MiscTemplateException(this, ".error is not available outside of a #recover block");
+        }
+        return ((Throwable) recoveredErrorStack.get(recoveredErrorStack.size() - 1)).getMessage();
+    }
+
+    /**
+     * Tells if we are inside an <tt>#attempt</tt> block (but before <tt>#recover</tt>). This can be useful for
+     * {@link TemplateExceptionHandler}-s, as then they may don't want to print the error to the output, as
+     * <tt>#attempt</tt> will roll it back anyway.
+     * 
+     * @since 2.3.20
+     */
+    public boolean isInAttemptBlock() {
+        return inAttemptBlock;
+    }
+
+    /**
+     * Used for {@code #nested}.
+     */
+    void invokeNestedContent(ASTDirNested.Context bodyCtx) throws TemplateException, IOException {
+        ASTDirMacro.Context invokingMacroContext = getCurrentMacroContext();
+        LocalContextStack prevLocalContextStack = localContextStack;
+        ASTElement[] nestedContentBuffer = invokingMacroContext.nestedContentBuffer;
+        if (nestedContentBuffer != null) {
+            currentMacroContext = invokingMacroContext.prevMacroContext;
+            currentNamespace = invokingMacroContext.nestedContentNamespace;
+
+            localContextStack = invokingMacroContext.prevLocalContextStack;
+            if (invokingMacroContext.nestedContentParameterNames != null) {
+                pushLocalContext(bodyCtx);
+            }
+            try {
+                visit(nestedContentBuffer);
+            } finally {
+                if (invokingMacroContext.nestedContentParameterNames != null) {
+                    localContextStack.pop();
+                }
+                currentMacroContext = invokingMacroContext;
+                currentNamespace = getMacroNamespace(invokingMacroContext.getMacro());
+                localContextStack = prevLocalContextStack;
+            }
+        }
+    }
+
+    /**
+     * "visit" an ASTDirList
+     */
+    boolean visitIteratorBlock(ASTDirList.IterationContext ictxt)
+            throws TemplateException, IOException {
+        pushLocalContext(ictxt);
+        try {
+            return ictxt.accept(this);
+        } catch (TemplateException te) {
+            handleTemplateException(te);
+            return true;
+        } finally {
+            localContextStack.pop();
+        }
+    }
+
+    /**
+     * Used for {@code #visit} and {@code #recurse}.
+     */
+    void invokeNodeHandlerFor(TemplateNodeModel node, TemplateSequenceModel namespaces)
+            throws TemplateException, IOException {
+        if (nodeNamespaces == null) {
+            NativeSequence seq = new NativeSequence(1);
+            seq.add(currentNamespace);
+            nodeNamespaces = seq;
+        }
+        int prevNodeNamespaceIndex = nodeNamespaceIndex;
+        String prevNodeName = currentNodeName;
+        String prevNodeNS = currentNodeNS;
+        TemplateSequenceModel prevNodeNamespaces = nodeNamespaces;
+        TemplateNodeModel prevVisitorNode = currentVisitorNode;
+        currentVisitorNode = node;
+        if (namespaces != null) {
+            nodeNamespaces = namespaces;
+        }
+        try {
+            TemplateModel macroOrTransform = getNodeProcessor(node);
+            if (macroOrTransform instanceof ASTDirMacro) {
+                invoke((ASTDirMacro) macroOrTransform, null, null, null, null);
+            } else if (macroOrTransform instanceof TemplateTransformModel) {
+                visitAndTransform(null, (TemplateTransformModel) macroOrTransform, null);
+            } else {
+                String nodeType = node.getNodeType();
+                if (nodeType != null) {
+                    // If the node's type is 'text', we just output it.
+                    if ((nodeType.equals("text") && node instanceof TemplateScalarModel)) {
+                        out.write(((TemplateScalarModel) node).getAsString());
+                    } else if (nodeType.equals("document")) {
+                        recurse(node, namespaces);
+                    }
+                    // We complain here, unless the node's type is 'pi', or "comment" or "document_type", in which case
+                    // we just ignore it.
+                    else if (!nodeType.equals("pi")
+                            && !nodeType.equals("comment")
+                            && !nodeType.equals("document_type")) {
+                        throw new _MiscTemplateException(
+                                this, noNodeHandlerDefinedDescription(node, node.getNodeNamespace(), nodeType));
+                    }
+                } else {
+                    throw new _MiscTemplateException(
+                            this, noNodeHandlerDefinedDescription(node, node.getNodeNamespace(), "default"));
+                }
+            }
+        } finally {
+            currentVisitorNode = prevVisitorNode;
+            nodeNamespaceIndex = prevNodeNamespaceIndex;
+            currentNodeName = prevNodeName;
+            currentNodeNS = prevNodeNS;
+            nodeNamespaces = prevNodeNamespaces;
+        }
+    }
+
+    private Object[] noNodeHandlerDefinedDescription(
+            TemplateNodeModel node, String ns, String nodeType)
+                    throws TemplateModelException {
+        String nsPrefix;
+        if (ns != null) {
+            if (ns.length() > 0) {
+                nsPrefix = " and namespace ";
+            } else {
+                nsPrefix = " and no namespace";
+            }
+        } else {
+            nsPrefix = "";
+            ns = "";
+        }
+        return new Object[] { "No macro or directive is defined for node named ",
+                new _DelayedJQuote(node.getNodeName()), nsPrefix, ns,
+                ", and there is no fallback handler called @", nodeType, " either." };
+    }
+
+    void fallback() throws TemplateException, IOException {
+        TemplateModel macroOrTransform = getNodeProcessor(currentNodeName, currentNodeNS, nodeNamespaceIndex);
+        if (macroOrTransform instanceof ASTDirMacro) {
+            invoke((ASTDirMacro) macroOrTransform, null, null, null, null);
+        } else if (macroOrTransform instanceof TemplateTransformModel) {
+            visitAndTransform(null, (TemplateTransformModel) macroOrTransform, null);
+        }
+    }
+
+    /**
+     * Calls the macro or function with the given arguments and nested block.
+     */
+    void invoke(ASTDirMacro macro,
+            Map namedArgs, List positionalArgs,
+            List bodyParameterNames, ASTElement[] childBuffer) throws TemplateException, IOException {
+        if (macro == ASTDirMacro.DO_NOTHING_MACRO) {
+            return;
+        }
+
+        pushElement(macro);
+        try {
+            final ASTDirMacro.Context macroCtx = macro.new Context(this, childBuffer, bodyParameterNames);
+            setMacroContextLocalsFromArguments(macroCtx, macro, namedArgs, positionalArgs);
+
+            final ASTDirMacro.Context prevMacroCtx = currentMacroContext;
+            currentMacroContext = macroCtx;
+
+            final LocalContextStack prevLocalContextStack = localContextStack;
+            localContextStack = null;
+
+            final Namespace prevNamespace = currentNamespace;
+            currentNamespace = (Namespace) macroToNamespaceLookup.get(macro);
+
+            try {
+                macroCtx.sanityCheck(this);
+                visit(macro.getChildBuffer());
+            } catch (ASTDirReturn.Return re) {
+                // Not an error, just a <#return>
+            } catch (TemplateException te) {
+                handleTemplateException(te);
+            } finally {
+                currentMacroContext = prevMacroCtx;
+                localContextStack = prevLocalContextStack;
+                currentNamespace = prevNamespace;
+            }
+        } finally {
+            popElement();
+        }
+    }
+
+    /**
+     * Sets the local variables corresponding to the macro call arguments in the macro context.
+     */
+    private void setMacroContextLocalsFromArguments(
+            final ASTDirMacro.Context macroCtx,
+            final ASTDirMacro macro,
+            final Map namedArgs, final List positionalArgs) throws TemplateException {
+        String catchAllParamName = macro.getCatchAll();
+        if (namedArgs != null) {
+            final NativeHashEx2 catchAllParamValue;
+            if (catchAllParamName != null) {
+                catchAllParamValue = new NativeHashEx2();
+                macroCtx.setLocalVar(catchAllParamName, catchAllParamValue);
+            } else {
+                catchAllParamValue = null;
+            }
+
+             for (Map.Entry argNameAndValExp : (Set<Map.Entry>) namedArgs.entrySet()) {
+                final String argName = (String) argNameAndValExp.getKey();
+                final boolean isArgNameDeclared = macro.hasArgNamed(argName);
+                if (isArgNameDeclared || catchAllParamName != null) {
+                    ASTExpression argValueExp = (ASTExpression) argNameAndValExp.getValue();
+                    TemplateModel argValue = argValueExp.eval(this);
+                    if (isArgNameDeclared) {
+                        macroCtx.setLocalVar(argName, argValue);
+                    } else {
+                        catchAllParamValue.put(argName, argValue);
+                    }
+                } else {
+                    throw new _MiscTemplateException(this,
+                            (macro.isFunction() ? "Function " : "Macro "), new _DelayedJQuote(macro.getName()),
+                            " has no parameter with name ", new _DelayedJQuote(argName), ".");
+                }
+            }
+        } else if (positionalArgs != null) {
+            final NativeSequence catchAllParamValue;
+            if (catchAllParamName != null) {
+                catchAllParamValue = new NativeSequence(8);
+                macroCtx.setLocalVar(catchAllParamName, catchAllParamValue);
+            } else {
+                catchAllParamValue = null;
+            }
+
+            String[] argNames = macro.getArgumentNamesInternal();
+            final int argsCnt = positionalArgs.size();
+            if (argNames.length < argsCnt && catchAllParamName == null) {
+                throw new _MiscTemplateException(this,
+                        (macro.isFunction() ? "Function " : "Macro "), new _DelayedJQuote(macro.getName()),
+                        " only accepts ", new _DelayedToString(argNames.length), " parameters, but got ",
+                        new _DelayedToString(argsCnt), ".");
+            }
+            for (int i = 0; i < argsCnt; i++) {
+                ASTExpression argValueExp = (ASTExpression) positionalArgs.get(i);
+                TemplateModel argValue = argValueExp.eval(this);
+                try {
+                    if (i < argNames.length) {
+                        String argName = argNames[i];
+                        macroCtx.setLocalVar(argName, argValue);
+                    } else {
+                        catchAllParamValue.add(argValue);
+                    }
+                } catch (RuntimeException re) {
+                    throw new _MiscTemplateException(re, this);
+                }
+            }
+        }
+    }
+
+    /**
+     * Defines the given macro in the current namespace (doesn't call it).
+     */
+    void visitMacroDef(ASTDirMacro macro) {
+        macroToNamespaceLookup.put(macro, currentNamespace);
+        currentNamespace.put(macro.getName(), macro);
+    }
+
+    Namespace getMacroNamespace(ASTDirMacro macro) {
+        return (Namespace) macroToNamespaceLookup.get(macro);
+    }
+
+    void recurse(TemplateNodeModel node, TemplateSequenceModel namespaces)
+            throws TemplateException, IOException {
+        if (node == null) {
+            node = getCurrentVisitorNode();
+            if (node == null) {
+                throw new _TemplateModelException(
+                        "The target node of recursion is missing or null.");
+            }
+        }
+        TemplateSequenceModel children = node.getChildNodes();
+        if (children == null) return;
+        for (int i = 0; i < children.size(); i++) {
+            TemplateNodeModel child = (TemplateNodeModel) children.get(i);
+            if (child != null) {
+                invokeNodeHandlerFor(child, namespaces);
+            }
+        }
+    }
+
+    ASTDirMacro.Context getCurrentMacroContext() {
+        return currentMacroContext;
+    }
+
+    private void handleTemplateException(TemplateException templateException)
+            throws TemplateException {
+        // Logic to prevent double-handling of the exception in
+        // nested visit() calls.
+        if (lastThrowable == templateException) {
+            throw templateException;
+        }
+        lastThrowable = templateException;
+
+        // Log the exception, if logTemplateExceptions isn't false. However, even if it's false, if we are inside
+        // an #attempt block, it has to be logged, as it certainly won't bubble up to the caller of FreeMarker.
+        if (LOG.isErrorEnabled() && (isInAttemptBlock() || getLogTemplateExceptions())) {
+            LOG.error("Error executing FreeMarker template", templateException);
+        }
+
+        // Stop exception is not passed to the handler, but
+        // explicitly rethrown.
+        if (templateException instanceof StopException) {
+            throw templateException;
+        }
+
+        // Finally, pass the exception to the handler
+        getTemplateExceptionHandler().handleTemplateException(templateException, this, out);
+    }
+
+    @Override
+    public void setTemplateExceptionHandler(TemplateExceptionHandler templateExceptionHandler) {
+        super.setTemplateExceptionHandler(templateExceptionHandler);
+        lastThrowable = null;
+    }
+
+    @Override
+    protected TemplateExceptionHandler getDefaultTemplateExceptionHandler() {
+        return getMainTemplate().getTemplateExceptionHandler();
+    }
+
+    @Override
+    protected ArithmeticEngine getDefaultArithmeticEngine() {
+        return getMainTemplate().getArithmeticEngine();
+    }
+
+    @Override
+    protected ObjectWrapper getDefaultObjectWrapper() {
+        return getMainTemplate().getObjectWrapper();
+    }
+
+    @Override
+    public void setLocale(Locale locale) {
+        Locale prevLocale = getLocale();
+        super.setLocale(locale);
+        if (!locale.equals(prevLocale)) {
+            cachedTemplateNumberFormats = null;
+            if (cachedTemplateNumberFormat != null && cachedTemplateNumberFormat.isLocaleBound()) {
+                cachedTemplateNumberFormat = null;
+            }
+
+            if (cachedTempDateFormatArray != null) {
+                for (int i = 0; i < CACHED_TDFS_LENGTH; i++) {
+                    final TemplateDateFormat f = cachedTempDateFormatArray[i];
+                    if (f != null && f.isLocaleBound()) {
+                        cachedTempDateFormatArray[i] = null;
+                    }
+                }
+            }
+
+            cachedTempDateFormatsByFmtStrArray = null;
+
+            cachedCollator = null;
+        }
+    }
+
+    @Override
+    protected Locale getDefaultLocale() {
+        return getMainTemplate().getLocale();
+    }
+
+    @Override
+    public void setTimeZone(TimeZone timeZone) {
+        TimeZone prevTimeZone = getTimeZone();
+        super.setTimeZone(timeZone);
+
+        if (!timeZone.equals(prevTimeZone)) {
+            if (cachedTempDateFormatArray != null) {
+                for (int i = 0; i < CACHED_TDFS_SQL_D_T_TZ_OFFS; i++) {
+                    TemplateDateFormat f = cachedTempDateFormatArray[i];
+                    if (f != null && f.isTimeZoneBound()) {
+                        cachedTempDateFormatArray[i] = null;
+                    }
+                }
+            }
+            if (cachedTempDateFormatsByFmtStrArray != null) {
+                for (int i = 0; i < CACHED_TDFS_SQL_D_T_TZ_OFFS; i++) {
+                    cachedTempDateFormatsByFmtStrArray[i] = null;
+                }
+            }
+
+            cachedSQLDateAndTimeTimeZoneSameAsNormal = null;
+        }
+    }
+
+    @Override
+    protected TimeZone getDefaultTimeZone() {
+        return getMainTemplate().getTimeZone();
+    }
+
+    @Override
+    public void setSQLDateAndTimeTimeZone(TimeZone timeZone) {
+        TimeZone prevTimeZone = getSQLDateAndTimeTimeZone();
+        super.setSQLDateAndTimeTimeZone(timeZone);
+
+        if (!nullSafeEquals(timeZone, prevTimeZone)) {
+            if (cachedTempDateFormatArray != null) {
+                for (int i = CACHED_TDFS_SQL_D_T_TZ_OFFS; i < CACHED_TDFS_LENGTH; i++) {
+                    TemplateDateFormat format = cachedTempDateFormatArray[i];
+                    if (format != null && format.isTimeZoneBound()) {
+                        cachedTempDateFormatArray[i] = null;
+                    }
+                }
+            }
+            if (cachedTempDateFormatsByFmtStrArray != null) {
+                for (int i = CACHED_TDFS_SQL_D_T_TZ_OFFS; i < CACHED_TDFS_LENGTH; i++) {
+                    cachedTempDateFormatsByFmtStrArray[i] = null;
+                }
+            }
+
+            cachedSQLDateAndTimeTimeZoneSameAsNormal = null;
+        }
+    }
+
+    @Override
+    protected TimeZone getDefaultSQLDateAndTimeTimeZone() {
+        return getMainTemplate().getSQLDateAndTimeTimeZone();
+    }
+
+    // Replace with Objects.equals in Java 7
+    private static boolean nullSafeEquals(Object o1, Object o2) {
+        if (o1 == o2) return true;
+        if (o1 == null || o2 == null) return false;
+        return o1.equals(o2);
+    }
+
+    /**
+     * Tells if the same concrete time zone is used for SQL date-only and time-only values as for other
+     * date/time/date-time values.
+     */
+    boolean isSQLDateAndTimeTimeZoneSameAsNormal() {
+        if (cachedSQLDateAndTimeTimeZoneSameAsNormal == null) {
+            cachedSQLDateAndTimeTimeZoneSameAsNormal = Boolean.valueOf(
+                    getSQLDateAndTimeTimeZone() == null
+                            || getSQLDateAndTimeTimeZone().equals(getTimeZone()));
+        }
+        return cachedSQLDateAndTimeTimeZoneSameAsNormal.booleanValue();
+    }
+
+    @Override
+    public void setURLEscapingCharset(Charset urlEscapingCharset) {
+        cachedURLEscapingCharsetSet = false;
+        super.setURLEscapingCharset(urlEscapingCharset);
+    }
+
+    @Override
+    protected Charset getDefaultURLEscapingCharset() {
+        return getMainTemplate().getURLEscapingCharset();
+    }
+
+    @Override
+    protected TemplateClassResolver getDefaultNewBuiltinClassResolver() {
+        return getMainTemplate().getNewBuiltinClassResolver();
+    }
+
+    @Override
+    protected boolean getDefaultAutoFlush() {
+        return getMainTemplate().getAutoFlush();
+    }
+
+    @Override
+    protected boolean getDefaultShowErrorTips() {
+        return getMainTemplate().getShowErrorTips();
+    }
+
+    @Override
+    protected boolean getDefaultAPIBuiltinEnabled() {
+        return getMainTemplate().getAPIBuiltinEnabled();
+    }
+
+    @Override
+    protected boolean getDefaultLogTemplateExceptions() {
+        return getMainTemplate().getLogTemplateExceptions();
+    }
+
+    @Override
+    protected boolean getDefaultLazyImports() {
+        return getMainTemplate().getLazyImports();
+    }
+
+    @Override
+    protected Boolean getDefaultLazyAutoImports() {
+        return getMainTemplate().getLazyAutoImports();
+    }
+
+    @Override
+    protected Map<String, String> getDefaultAutoImports() {
+        return getMainTemplate().getAutoImports();
+    }
+
+    @Override
+    protected List<String> getDefaultAutoIncludes() {
+        return getMainTemplate().getAutoIncludes();
+    }
+
+    @Override
+    protected Object getDefaultCustomAttribute(Object name) {
+        return getMainTemplate().getCustomAttribute(name);
+    }
+
+    @Override
+    protected Map<Object, Object> getDefaultCustomAttributes() {
+        return getMainTemplate().getCustomAttributes();
+    }
+
+    /*
+     * Note that altough it's not allowed to set this setting with the <tt>setting</tt> directive, it still must be
+     * allowed to set it from Java code while the template executes, since some frameworks allow templates to actually
+     * change the output encoding on-the-fly.
+     */
+    @Override
+    public void setOutputEncoding(Charset outputEncoding) {
+        cachedURLEscapingCharsetSet = false;
+        super.setOutputEncoding(outputEncoding);
+    }
+
+    @Override
+    protected Charset getDefaultOutputEncoding() {
+        return getMainTemplate().getOutputEncoding();
+    }
+
+    /**
+     * Returns the name of the charset that should be used for URL encoding. This will be <code>null</code> if the
+     * information is not available. The function caches the return value, so it's quick to call it repeatedly.
+     */
+    Charset getEffectiveURLEscapingCharset() {
+        if (!cachedURLEscapingCharsetSet) {
+            cachedURLEscapingCharset = getURLEscapingCharset();
+            if (cachedURLEscapingCharset == null) {
+                cachedURLEscapingCharset = getOutputEncoding();
+            }
+            cachedURLEscapingCharsetSet = true;
+        }
+        return cachedURLEscapingCharset;
+    }
+
+    Collator getCollator() {
+        if (cachedCollator == null) {
+            cachedCollator = Collator.getInstance(getLocale());
+        }
+        return cachedCollator;
+    }
+
+    /**
+     * Compares two {@link TemplateModel}-s according the rules of the FTL "==" operator.
+     * 
+     * @since 2.3.20
+     */
+    public boolean applyEqualsOperator(TemplateModel leftValue, TemplateModel rightValue)
+            throws TemplateException {
+        return _EvalUtil.compare(leftValue, _EvalUtil.CMP_OP_EQUALS, rightValue, this);
+    }
+
+    /**
+     * Compares two {@link TemplateModel}-s according the rules of the FTL "==" operator, except that if the two types
+     * are incompatible, they are treated as non-equal instead of throwing an exception. Comparing dates of different
+     * types (date-only VS time-only VS date-time) will still throw an exception, however.
+     * 
+     * @since 2.3.20
+     */
+    public boolean applyEqualsOperatorLenient(TemplateModel leftValue, TemplateModel rightValue)
+            throws TemplateException {
+        return _EvalUtil.compareLenient(leftValue, _EvalUtil.CMP_OP_EQUALS, rightValue, this);
+    }
+
+    /**
+     * Compares two {@link TemplateModel}-s according the rules of the FTL "&lt;" operator.
+     * 
+     * @since 2.3.20
+     */
+    public boolean applyLessThanOperator(TemplateModel leftValue, TemplateModel rightValue)
+            throws TemplateException {
+        return _EvalUtil.compare(leftValue, _EvalUtil.CMP_OP_LESS_THAN, rightValue, this);
+    }
+
+    /**
+     * Compares two {@link TemplateModel}-s according the rules of the FTL "&lt;" operator.
+     * 
+     * @since 2.3.20
+     */
+    public boolean applyLessThanOrEqualsOperator(TemplateModel leftValue, TemplateModel rightValue)
+            throws TemplateException {
+        return _EvalUtil.compare(leftValue, _EvalUtil.CMP_OP_LESS_THAN_EQUALS, rightValue, this);
+    }
+
+    /**
+     * Compares two {@link TemplateModel}-s according the rules of the FTL "&gt;" operator.
+     * 
+     * @since 2.3.20
+     */
+    public boolean applyGreaterThanOperator(TemplateModel leftValue, TemplateModel rightValue)
+            throws TemplateException {
+        return _EvalUtil.compare(leftValue, _EvalUtil.CMP_OP_GREATER_THAN, rightValue, this);
+    }
+
+    /**
+     * Compares two {@link TemplateModel}-s according the rules of the FTL "&gt;=" operator.
+     * 
+     * @since 2.3.20
+     */
+    public boolean applyWithGreaterThanOrEqualsOperator(TemplateModel leftValue, TemplateModel rightValue)
+            throws TemplateException {
+        return _EvalUtil.compare(leftValue, _EvalUtil.CMP_OP_GREATER_THAN_EQUALS, rightValue, this);
+    }
+
+    public void setOut(Writer out) {
+        this.out = out;
+    }
+
+    public Writer getOut() {
+        return out;
+    }
+
+    @Override
+    public void setNumberFormat(String formatName) {
+        super.setNumberFormat(formatName);
+        cachedTemplateNumberFormat = null;
+    }
+
+    @Override
+    protected String getDefaultNumberFormat() {
+        return getMainTemplate().getNumberFormat();
+    }
+
+    @Override
+    protected Map<String, TemplateNumberFormatFactory> getDefaultCustomNumberFormats() {
+        return getMainTemplate().getCustomNumberFormats();
+    }
+
+    @Override
+    protected TemplateNumberFormatFactory getDefaultCustomNumberFormat(String name) {
+        return getMainTemplate().getCustomNumberFormat(name);
+    }
+
+    @Override
+    protected String getDefaultBooleanFormat() {
+        return getMainTemplate().getBooleanFormat();
+    }
+
+    String formatBoolean(boolean value, boolean fallbackToTrueFalse) throws TemplateException {
+        TemplateBooleanFormat templateBooleanFormat = getTemplateBooleanFormat();
+        if (value) {
+            String s = templateBooleanFormat.getTrueStringValue();
+            if (s == null) {
+                if (fallbackToTrueFalse) {
+                    return MiscUtil.C_TRUE;
+                } else {
+                    throw new _MiscTemplateException(getNullBooleanFormatErrorDescription());
+                }
+            } else {
+                return s;
+            }
+        } else {
+            String s = templateBooleanFormat.getFalseStringValue();
+            if (s == null) {
+                if (fallbackToTrueFalse) {
+                    return MiscUtil.C_FALSE;
+                } else {
+                    throw new _MiscTemplateException(getNullBooleanFormatErrorDescription());
+                }
+            } else {
+                return s;
+            }
+        }
+    }
+
+    TemplateBooleanFormat getTemplateBooleanFormat() {
+        TemplateBooleanFormat format = cachedTemplateBooleanFormat;
+        if (format == null) {
+            format = TemplateBooleanFormat.getInstance(getBooleanFormat());
+            cachedTemplateBooleanFormat = format;
+        }
+        return format;
+    }
+
+    @Override
+    public void setBooleanFormat(String booleanFormat) {
+        String previousFormat = getBooleanFormat();
+        super.setBooleanFormat(booleanFormat);
+        if (!booleanFormat.equals(previousFormat)) {
+            cachedTemplateBooleanFormat = null;
+        }
+    }
+
+    private _ErrorDescriptionBuilder getNullBooleanFormatErrorDescription() {
+        return new _ErrorDescriptionBuilder(
+                "Can't convert boolean to string automatically, because the \"", BOOLEAN_FORMAT_KEY ,"\" setting was ",
+                new _DelayedJQuote(getBooleanFormat()),
+                (getBooleanFormat().equals(TemplateBooleanFormat.C_TRUE_FALSE)
+                        ? ", which is the legacy default computer-language format, and hence isn't accepted."
+                        : ".")
+        ).tips(
+                "If you just want \"true\"/\"false\" result as you are generting computer-language output, "
+                        + "use \"?c\", like ${myBool?c}.",
+                "You can write myBool?string('yes', 'no') and like to specify boolean formatting in place.",
+                new Object[] {
+                        "If you need the same two values on most places, the programmers should set the \"",
+                        BOOLEAN_FORMAT_KEY ,"\" setting to something like \"yes,no\"." }
+        );
+    }
+
+    /**
+     * Format number with the default number format.
+     * 
+     * @param exp
+     *            The blamed expression if an error occurs; it's only needed for better error messages
+     */
+    String formatNumberToPlainText(TemplateNumberModel number, ASTExpression exp, boolean useTempModelExc)
+            throws TemplateException {
+        return formatNumberToPlainText(number, getTemplateNumberFormat(exp, useTempModelExc), exp, useTempModelExc);
+    }
+
+    /**
+     * Format number with the number format specified as the parameter, with the current locale.
+     * 
+     * @param exp
+     *            The blamed expression if an error occurs; it's only needed for better error messages
+     */
+    String formatNumberToPlainText(
+            TemplateNumberModel number, TemplateNumberFormat format, ASTExpression exp,
+            boolean useTempModelExc)
+            throws TemplateException {
+        try {
+            return _EvalUtil.assertFormatResultNotNull(format.formatToPlainText(number));
+        } catch (TemplateValueFormatException e) {
+            throw MessageUtil.newCantFormatNumberException(format, exp, e, useTempModelExc);
+        }
+    }
+
+    /**
+     * Returns the current number format ({@link #getNumberFormat()}) as {@link TemplateNumberFormat}.
+     * 
+     * <p>
+     * Performance notes: The result is stored for reuse, so calling this method frequently is usually not a problem.
+     * However, at least as of this writing (2.3.24), changing the current locale {@link #setLocale(Locale)} or changing
+     * the current number format ({@link #setNumberFormat(String)}) will drop the stored value, so it will have to be
+     * recalculated.
+     * 
+     * @since 2.3.24
+     */
+    public TemplateNumberFormat getTemplateNumberFormat() throws TemplateValueFormatException {
+        TemplateNumberFormat format = cachedTemplateNumberFormat;
+        if (format == null) {
+            format = getTemplateNumberFormat(getNumberFormat(), false);
+            cachedTemplateNumberFormat = format;
+        }
+        return format;
+    }
+
+    /**
+     * Returns the number format as {@link TemplateNumberFormat} for the given format string and the current locale.
+     * (The current locale is the locale returned by {@link #getLocale()}.) Note that the result will be cached in the
+     * {@link Environment} instance (though at least in 2.3.24 the cache will be flushed if the current locale of the
+     * {@link Environment} is changed).
+     * 
+     * @param formatString
+     *            A string that you could also use as the value of the {@code numberFormat} configuration setting. Can't
+     *            be {@code null}.
+     * 
+     * @since 2.3.24
+     */
+    public TemplateNumberFormat getTemplateNumberFormat(String formatString) throws TemplateValueFormatException {
+        return getTemplateNumberFormat(formatString, true);
+    }
+
+    /**
+     * Returns the number format as {@link TemplateNumberFormat}, for the given format string and locale. To get a
+     * number format for the current locale, use {@link #getTemplateNumberFormat(String)} instead.
+     * 
+     * <p>
+     * Note on performance (which was true at least for 2.3.24): Unless the locale happens to be equal to the current
+     * locale, the {@link Environment}-level format cache can't be used, so the format string has to be parsed and the
+     * matching factory has to be get an invoked, which is much more expensive than getting the format from the cache.
+     * Thus the returned format should be stored by the caller for later reuse (but only within the current thread and
+     * in relation to the current {@link Environment}), if it will be needed frequently.
+     * 
+     * @param formatString
+     *            A string that you could also use as the value of the {@code numberFormat} configuration setting.
+     * @param locale
+     *            The locale of the number format; not {@code null}.
+     * 
+     * @since 2.3.24
+     */
+    public TemplateNumberFormat getTemplateNumberFormat(String formatString, Locale locale)
+            throws TemplateValueFormatException {
+        if (locale.equals(getLocale())) {
+            getTemplateNumberFormat(formatString);
+        }
+
+        return getTemplateNumberFormatWithoutCache(formatString, locale);
+    }
+
+    /**
+     * Convenience wrapper around {@link #getTemplateNumberFormat()} to be called during expression evaluation.
+     */
+    TemplateNumberFormat getTemplateNumberFormat(ASTExpression exp, boolean useTempModelExc) throws TemplateException {
+        TemplateNumberFormat format;
+        try {
+            format = getTemplateNumberFormat();
+        } catch (TemplateValueFormatException e) {
+            _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder(
+                    "Failed to get number format object for the current number format string, ",
+                    new _DelayedJQuote(getNumberFormat()), ": ", e.getMessage())
+                    .blame(exp); 
+            throw useTempModelExc
+                    ? new _TemplateModelException(e, this, desc) : new _MiscTemplateException(e, this, desc);
+        }
+        return format;
+    }
+
+    /**
+     * Convenience wrapper around {@link #getTemplateNumberFormat(String)} to be called during expression evaluation.
+     * 
+     * @param exp
+     *            The blamed expression if an error occurs; it's only needed for better error messages
+     */
+    TemplateNumberFormat getTemplateNumberFormat(String formatString, ASTExpression exp, boolean useTempModelExc)
+            throws TemplateException {
+        TemplateNumberFormat format;
+        try {
+            format = getTemplateNumberFormat(formatString);
+        } catch (TemplateValueFormatException e) {
+            _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder(
+                    "Failed to get number format object for the ", new _DelayedJQuote(formatString),
+                    " number format string: ", e.getMessage())
+                    .blame(exp);
+            throw useTempModelExc
+                    ? new _TemplateModelException(e, this, desc) : new _MiscTemplateException(e, this, desc);
+        }
+        return format;
+    }
+
+    /**
+     * Gets the {@link TemplateNumberFormat} <em>for the current locale</em>.
+     * 
+     * @param formatString
+     *            Not {@code null}
+     * @param cacheResult
+     *            If the results should stored in the {@link Environment}-level cache. It will still try to get the
+     *            result from the cache regardless of this parameter.
+     */
+    private TemplateNumberFormat getTemplateNumberFormat(String formatString, boolean cacheResult)
+            throws TemplateValueFormatException {
+        if (cachedTemplateNumberFormats == null) {
+            if (cacheResult) {
+                cachedTemplateNumberFormats = new HashMap<>();
+            }
+        } else {
+            TemplateNumberFormat format = cachedTemplateNumberFormats.get(formatString);
+            if (format != null) {
+                return format;
+            }
+        }
+
+        TemplateNumberFormat format = getTemplateNumberFormatWithoutCache(formatString, getLocale());
+
+        if (cacheResult) {
+            cachedTemplateNumberFormats.put(formatString, format);
+        }
+        return format;
+    }
+
+    /**
+     * Returns the {@link TemplateNumberFormat} for the given parameters without using the {@link Environment}-level
+     * cache. Of course, the {@link TemplateNumberFormatFactory} involved might still uses its own cache.
+     * 
+     * @param formatString
+     *            Not {@code null}
+     * @param locale
+     *            Not {@code null}
+     */
+    private TemplateNumberFormat getTemplateNumberFormatWithoutCache(String formatString, Locale locale)
+            throws TemplateValueFormatException {
+        int formatStringLen = formatString.length();
+        if (formatStringLen > 1
+                && formatString.charAt(0) == '@'
+                && Character.isLetter(formatString.charAt(1))) {
+            final String name;
+            final String params;
+            {
+                int endIdx;
+                findParamsStart: for (endIdx = 1; endIdx < formatStringLen; endIdx++) {
+                    char c = formatString.charAt(endIdx);
+                    if (c == ' ' || c == '_') {
+                        break findParamsStart;
+                    }
+                }
+                name = formatString.substring(1, endIdx);
+                params = endIdx < formatStringLen ? formatString.substring(endIdx + 1) : "";
+            }
+
+            TemplateNumberFormatFactory formatFactory = getCustomNumberFormat(name);
+            if (formatFactory == null) {
+                throw new UndefinedCustomFormatException(
+                        "No custom number format was defined with name " + _StringUtil.jQuote(name));
+            }
+
+            return formatFactory.get(params, locale, this);
+        } else {
+            return JavaTemplateNumberFormatFactory.INSTANCE.get(formatString, locale, this);
+        }
+    }
+
+    /**
+     * Returns the {@link NumberFormat} used for the <tt>c</tt> built-in. This is always US English
+     * <code>"0.################"</code>, without grouping and without superfluous decimal separator.
+     */
+    public NumberFormat getCNumberFormat() {
+        // It can't be cached in a static field, because DecimalFormat-s aren't
+        // thread-safe.
+        if (cNumberFormat == null) {
+            cNumberFormat = (DecimalFormat) C_NUMBER_FORMAT.clone();
+        }
+        return cNumberFormat;
+    }
+
+    @Override
+    public void setTimeFormat(String timeFormat) {
+        String prevTimeFormat = getTimeFormat();
+        super.setTimeFormat(timeFormat);
+        if (!timeFormat.equals(prevTimeFormat)) {
+            if (cachedTempDateFormatArray != null) {
+                for (int i = 0; i < CACHED_TDFS_LENGTH; i += CACHED_TDFS_ZONELESS_INPUT_OFFS) {
+                    cachedTempDateFormatArray[i + TemplateDateModel.TIME] = null;
+                }
+            }
+        }
+    }
+
+    @Override
+    protected String getDefaultTimeFormat() {
+        return getMainTemplate().getTimeFormat();
+    }
+
+    @Override
+    public void setDateFormat(String dateFormat) {
+        String prevDateFormat = getDateFormat();
+        super.setDateFormat(dateFormat);
+        if (!dateFormat.equals(prevDateFormat)) {
+            if (cachedTempDateFormatArray != null) {
+                for (int i = 0; i < CACHED_TDFS_LENGTH; i += CACHED_TDFS_ZONELESS_INPUT_OFFS) {
+                    cachedTempDateFormatArray[i + TemplateDateModel.DATE] = null;
+                }
+            }
+        }
+    }
+
+    @Override
+    protected String getDefaultDateFormat() {
+        return getMainTemplate().getDateFormat();
+    }
+
+    @Override
+    public void setDateTimeFormat(String dateTimeFormat) {
+        String prevDateTimeFormat = getDateTimeFormat();
+        super.setDateTimeFormat(dateTimeFormat);
+        if (!dateTimeFormat.equals(prevDateTimeFormat)) {
+            if (cachedTempDateFormatArray != null) {
+                for (int i = 0; i < CACHED_TDFS_LENGTH; i += CACHED_TDFS_ZONELESS_INPUT_OFFS) {
+                    cachedTempDateFormatArray[i + TemplateDateModel.DATETIME] = null;
+                }
+            }
+        }
+    }
+
+    @Override
+    protected String getDefaultDateTimeFormat() {
+        return getMainTemplate().getDateTimeFormat();
+    }
+
+    @Override
+    protected Map<String, TemplateDateFormatFactory> getDefaultCustomDateFormats() {
+        return getMainTemplate().getCustomDateFormats();
+    }
+
+    @Override
+    protected TemplateDateFormatFactory getDefaultCustomDateFormat(String name) {
+        return getMainTemplate().getCustomDateFormat(name);
+    }
+
+    public Configuration getConfiguration() {
+        return configuration;
+    }
+
+    TemplateModel getLastReturnValue() {
+        return lastReturnValue;
+    }
+
+    void setLastReturnValue(TemplateModel lastReturnValue) {
+        this.lastReturnValue = lastReturnValue;
+    }
+
+    void clearLastReturnValue() {
+        lastReturnValue = null;
+    }
+
+    /**
+     * @param tdmSourceExpr
+     *            The blamed expression if an error occurs; only used for error messages.
+     */
+    String formatDateToPlainText(TemplateDateModel tdm, ASTExpression tdmSourceExpr,
+            boolean useTempModelExc) throws TemplateException {
+        TemplateDateFormat format = getTemplateDateFormat(tdm, tdmSourceExpr, useTempModelExc);
+        
+        try {
+            return _EvalUtil.assertFormatResultNotNull(format.formatToPlainText(tdm));
+        } catch (TemplateValueFormatException e) {
+            throw MessageUtil.newCantFormatDateException(format, tdmSourceExpr, e, useTempModelExc);
+        }
+    }
+
+    /**
+     * @param blamedDateSourceExp
+     *            The blamed expression if an error occurs; only used for error messages.
+     * @param blamedFormatterExp
+     *            The blamed expression if an error occurs; only used for error messages.
+     */
+    String formatDateToPlainText(TemplateDateModel tdm, String formatString,
+            ASTExpression blamedDateSourceExp, ASTExpression blamedFormatterExp,
+            boolean useTempModelExc) throws TemplateException {
+        Date date = _EvalUtil.modelToDate(tdm, blamedDateSourceExp);
+        
+        TemplateDateFormat format = getTemplateDateFormat(
+                formatString, tdm.getDateType(), date.getClass(),
+                blamedDateSourceExp, blamedFormatterExp,
+                useTempModelExc);
+        
+        try {
+            return _EvalUtil.assertFormatResultNotNull(format.formatToPlainText(tdm));
+        } catch (TemplateValueFormatException e) {
+            throw MessageUtil.newCantFormatDateException(format, blamedDateSourceExp, e, useTempModelExc);
+        }
+    }
+
+    /**
+     * Gets a {@link TemplateDateFormat} using the date/time/datetime format settings and the current locale and time
+     * zone. (The current locale is the locale returned by {@link #getLocale()}. The current time zone is
+     * {@link #getTimeZone()} or {@link #getSQLDateAndTimeTimeZone()}).
+     * 
+     * @param dateType
+     *            The FTL date type; see the similar parameter of
+     *            {@link TemplateDateFormatFactory#get(String, int, Locale, TimeZone, boolean, Environment)}
+     * @param dateClass
+     *            The exact {@link Date} class, like {@link java.sql.Date} or {@link java.sql.Time}; this can influences
+     *            time zone selection. See also: {@link #setSQLDateAndTimeTimeZone(TimeZone)}
+     */
+    public TemplateDateFormat getTemplateDateFormat(int dateType, Class<? extends Date> dateClass)
+            throws TemplateValueFormatException {
+        boolean isSQLDateOrTime = isSQLDateOrTimeClass(dateClass);
+        return getTemplateDateFormat(dateType, shouldUseSQLDTTimeZone(isSQLDateOrTime), isSQLDateOrTime);
+    }
+    
+    /**
+     * Gets a {@link TemplateDateFormat} for the specified format string and the current locale and time zone. (The
+     * current locale is the locale returned by {@link #getLocale()}. The current time zone is {@link #getTimeZone()} or
+     * {@link #getSQLDateAndTimeTimeZone()}).
+     * 
+     * <p>
+     * Note on performance: The result will be cached in the {@link Environment} instance. However, at least in 2.3.24
+     * the cached entries that depend on the current locale or the current time zone or the current date/time/datetime
+     * format of the {@link Environment} will be lost when those settings are changed.
+     * 
+     * @param formatString
+     *            Like {@code "iso m"} or {@code "dd.MM.yyyy HH:mm"} or {@code "@somethingCustom"} or
+     *            {@code "@somethingCustom params"}
+     * 
+     * @since 2.3.24
+     */
+    public TemplateDateFormat getTemplateDateFormat(
+            String formatString, int dateType, Class<? extends Date> dateClass)
+                    throws TemplateValueFormatException {
+        boolean isSQLDateOrTime = isSQLDateOrTimeClass(dateClass);
+        return getTemplateDateFormat(
+                formatString, dateType,
+                shouldUseSQLDTTimeZone(isSQLDateOrTime), isSQLDateOrTime, true);
+    }
+
+    /**
+     * Like {@link #getTemplateDateFormat(String, int, Class)}, but allows you to use a different locale than the
+     * current one. If you want to use the current locale, use {@link #getTemplateDateFormat(String, int, Class)}
+     * instead.
+     * 
+     * <p>
+     * Performance notes regarding the locale and time zone parameters of
+     * {@link #getTemplateDateFormat(String, int, Locale, TimeZone, boolean)} apply.
+     * 
+     * @param locale
+     *            Can't be {@code null}; See the similar parameter of
+     *            {@link TemplateDateFormatFactory#get(String, int, Locale, TimeZone, boolean, Environment)}
+     * 
+     * @see #getTemplateDateFormat(String, int, Class)
+     * 
+     * @since 2.4
+     */
+    public TemplateDateFormat getTemplateDateFormat(
+            String formatString,
+            int dateType, Class<? extends Date> dateClass,
+            Locale locale)
+                    throws TemplateValueFormatException {
+        boolean isSQLDateOrTime = isSQLDateOrTimeClass(dateClass);
+        boolean useSQLDTTZ = shouldUseSQLDTTimeZone(isSQLDateOrTime);
+        return getTemplateDateFormat(
+                formatString,
+                dateType, locale, useSQLDTTZ ? getSQLDateAndTimeTimeZone() : getTimeZone(), isSQLDateOrTime);        
+    }
+
+    /**
+     * Like {@link #getTemplateDateFormat(String, int, Class)}, but allows you to use a different locale and time zone
+     * than the current one. If you want to use the current locale and time zone, use
+     * {@link #getTemplateDateFormat(String, int, Class)} instead.
+     * 
+     * <p>
+     * Performance notes regarding the locale and time zone parameters of
+     * {@link #getTemplateDateFormat(String, int, Locale, TimeZone, boolean)} apply.
+     * 
+     * @param timeZone
+     *            The {@link TimeZone} used if {@code dateClass} is not an SQL date-only or time-only type. Can't be
+     *            {@code null}.
+     * @param sqlDateAndTimeTimeZone
+     *            The {@link TimeZone} used if {@code dateClass} is an SQL date-only or time-only type. Can't be
+     *            {@code null}.
+     * 
+     * @see #getTemplateDateFormat(String, int, Class)
+     * 
+     * @since 2.4
+     */
+    public TemplateDateFormat getTemplateDateFormat(
+            String formatString,
+            int dateType, Class<? extends Date> dateClass,
+            Locale locale, TimeZone timeZone, TimeZone sqlDateAndTimeTimeZone)
+                    throws TemplateValueFormatException {
+        boolean isSQLDateOrTime = isSQLDateOrTimeClass(dateClass);
+        boolean useSQLDTTZ = shouldUseSQLDTTimeZone(isSQLDateOrTime);
+        return getTemplateDateFormat(
+                formatString,
+                dateType, locale, useSQLDTTZ ? sqlDateAndTimeTimeZone : timeZone, isSQLDateOrTime);        
+    }
+    
+    /**
+     * Gets a {@link TemplateDateFormat} for the specified parameters. This is mostly meant to be used by
+     * {@link TemplateDateFormatFactory} implementations to delegate to a format based on a specific format string. It
+     * works well for that, as its parameters are the same low level values as the parameters of
+     * {@link TemplateDateFormatFactory#get(String, int, Locale, TimeZone, boolean, Environment)}. For other tasks
+     * consider the other overloads of this method.
+     * 
+     * <p>
+     * Note on performance (which was true at least for 2.3.24): Unless the locale happens to be equal to the current
+     * locale and the time zone with one of the current time zones ({@link #getTimeZone()} or
+     * {@link #getSQLDateAndTimeTimeZone()}), the {@link Environment}-level format cache can't be used, so the format
+     * string has to be parsed and the matching factory has to be get an invoked, which is much more expensive than
+     * getting the format from the cache. Thus the returned format should be stored by the caller for later reuse (but
+     * only within the current thread and in relation to the current {@link Environment}), if it will be needed
+     * frequently.
+     * 
+     * @param formatString
+     *            Like {@code "iso m"} or {@code "dd.MM.yyyy HH:mm"} or {@code "@somethingCustom"} or
+     *            {@code "@somethingCustom params"}
+     * @param dateType
+     *            The FTL date type; see the similar parameter of
+     *            {@link TemplateDateFormatFactory#get(String, int, Locale, TimeZone, boolean, Environment)}
+     * @param timeZone
+     *            Not {@code null}; See the similar parameter of
+     *            {@link TemplateDateFormatFactory#get(String, int, Locale, TimeZone, boolean, Environment)}
+     * @param locale
+     *            Not {@code null}; See the similar parameter of
+     *            {@link TemplateDateFormatFactory#get(String, int, Locale, TimeZone, boolean, Environment)}
+     * @param zonelessInput
+     *            See the similar parameter of
+     *            {@link TemplateDateFormatFactory#get(String, int, Locale, TimeZone, boolean, Environment)}
+     * 
+     * @since 2.3.24
+     */
+    public TemplateDateFormat getTemplateDateFormat(
+            String formatString,
+            int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput)
+                    throws TemplateValueFormatException {
+        Locale currentLocale = getLocale();
+        if (locale.equals(currentLocale)) {
+            int equalCurrentTZ;
+            TimeZone currentTimeZone = getTimeZone();
+            if (timeZone.equals(currentTimeZone)) {
+                equalCurrentTZ = 1;
+            } else {
+                TimeZone currentSQLDTTimeZone = getSQLDateAndTimeTimeZone();
+                if (timeZone.equals(currentSQLDTTimeZone)) {
+                    equalCurrentTZ = 2;
+                } else {
+                    equalCurrentTZ = 0;
+                }
+            }
+            if (equalCurrentTZ != 0) {
+                return getTemplateDateFormat(formatString, dateType, equalCurrentTZ == 2, zonelessInput, true);
+            }
+            // Falls through
+        }
+        return getTemplateDateFormatWithoutCache(formatString, dateType, locale, timeZone, zonelessInput);
+    }
+    
+    TemplateDateFormat getTemplateDateFormat(TemplateDateModel tdm, ASTExpression tdmSourceExpr, boolean useTempModelExc)
+            throws TemplateException {
+        Date date = _EvalUtil.modelToDate(tdm, tdmSourceExpr);
+        
+        return getTemplateDateFormat(
+                tdm.getDateType(), date.getClass(), tdmSourceExpr,
+                useTempModelExc);
+    }
+
+    /**
+     * Same as {@link #getTemplateDateFormat(int, Class)}, but translates the exceptions to {@link TemplateException}-s.
+     */
+    TemplateDateFormat getTemplateDateFormat(
+            int dateType, Class<? extends Date> dateClass, ASTExpression blamedDateSourceExp, boolean useTempModelExc)
+                    throws TemplateException {
+        try {
+            return getTemplateDateFormat(dateType, dateClass);
+        } catch (UnknownDateTypeFormattingUnsupportedException e) {
+            throw MessageUtil.newCantFormatUnknownTypeDateException(blamedDateSourceExp, e);
+        } catch (TemplateValueFormatException e) {
+            String settingName;
+            String settingValue;
+            switch (dateType) {
+            case TemplateDateModel.TIME:
+                settingName = MutableProcessingConfiguration.TIME_FORMAT_KEY;
+                settingValue = getTimeFormat();
+                break;
+            case TemplateDateModel.DATE:
+                settingName = MutableProcessingConfiguration.DATE_FORMAT_KEY;
+                settingValue = getDateFormat();
+                break;
+            case TemplateDateModel.DATETIME:
+                settingName = MutableProcessingConfiguration.DATETIME_FORMAT_KEY;
+                settingValue = getDateTimeFormat();
+                break;
+            default:
+                settingName = "???";
+                settingValue = "???";
+            }
+            
+            _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder(
+                    "The value of the \"", settingName,
+                    "\" FreeMarker configuration setting is a malformed date/time/datetime format string: ",
+                    new _DelayedJQuote(settingValue), ". Reason given: ",
+                    e.getMessage());                    
+            throw useTempModelExc ? new _TemplateModelException(e, desc) : new _MiscTemplateException(e, desc);
+        }
+    }
+
+    /**
+     * Same as {@link #getTemplateDateFormat(String, int, Class)}, but translates the exceptions to
+     * {@link TemplateException}-s.
+     */
+    TemplateDateFormat getTemplateDateFormat(
+            String formatString, int dateType, Class<? extends Date> dateClass,
+            ASTExpression blamedDateSourceExp, ASTExpression blamedFormatterExp,
+            boolean useTempModelExc)
+                    throws TemplateException {
+        try {
+            return getTemplateDateFormat(formatString, dateType, dateClass);
+        } catch (UnknownDateTypeFormattingUnsupportedException e) {
+            throw MessageUtil.newCantFormatUnknownTypeDateException(blamedDateSourceExp, e);
+        } catch (TemplateValueFormatException e) {
+            _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder(
+                    "Can't invoke date/time/datetime format based on format string ",
+                    new _DelayedJQuote(formatString), ". Reason given: ",
+                    e.getMessage())
+                    .blame(blamedFormatterExp);
+            throw useTempModelExc ? new _TemplateModelException(e, desc) : new _MiscTemplateException(e, desc);
+        }
+    }
+
+    /**
+     * Used to get the {@link TemplateDateFormat} according the date/time/datetime format settings, for the current
+     * locale and time zone. See {@link #getTemplateDateFormat(String, int, Locale, TimeZone, boolean)} for the meaning
+     * of some of the parameters.
+     */
+    private TemplateDateFormat getTemplateDateFormat(int dateType, boolean useSQLDTTZ, boolean zonelessInput)
+            throws TemplateValueFormatException {
+        if (dateType == TemplateDateModel.UNKNOWN) {
+            throw new UnknownDateTypeFormattingUnsupportedException();
+        }
+        int cacheIdx = getTemplateDateFormatCacheArrayIndex(dateType, zonelessInput, useSQLDTTZ);
+        TemplateDateFormat[] cachedTempDateFormatArray = this.cachedTempDateFormatArray;
+        if (cachedTempDateFormatArray == null) {
+            cachedTempDateFormatArray = new TemplateDateFormat[CACHED_TDFS_LENGTH];
+            this.cachedTempDateFormatArray = cachedTempDateFormatArray;
+        }
+        TemplateDateFormat format = cachedTempDateFormatArray[cacheIdx];
+        if (format == null) {
+            final String formatString;
+            switch (dateType) {
+            case TemplateDateModel.TIME:
+                formatString = getTimeFormat();
+                break;
+            case TemplateDateModel.DATE:
+                formatString = getDateFormat();
+                break;
+            case TemplateDateModel.DATETIME:
+                formatString = getDateTimeFormat();
+                break;
+            default:
+                throw new IllegalArgumentException("Invalid date type enum: " + Integer.valueOf(dateType));
+            }
+
+            format = getTemplateDateFormat(formatString, dateType, useSQLDTTZ, zonelessInput, false);
+            
+            cachedTempDateFormatArray[cacheIdx] = format;
+        }
+        return format;
+    }
+
+    /**
+     * Used to get the {@link TemplateDateFormat} for the specified parameters, using the {@link Environment}-level
+     * cache. As the {@link Environment}-level cache currently only stores formats for the current locale and time zone,
+     * there's no parameter to specify those.
+     * 
+     * @param cacheResult
+     *            If the results should stored in the {@link Environment}-level cache. It will still try to get the
+     *            result from the cache regardless of this parameter.
+     */
+    private TemplateDateFormat getTemplateDateFormat(
+            String formatString, int dateType, boolean useSQLDTTimeZone, boolean zonelessInput,
+            boolean cacheResult)
+                    throws TemplateValueFormatException {
+        HashMap<String, TemplateDateFormat> cachedFormatsByFormatString;
+        readFromCache: do {
+            HashMap<String, TemplateDateFormat>[] cachedTempDateFormatsByFmtStrArray = this.cachedTempDateFormatsByFmtStrArray;
+            if (cachedTempDateFormatsByFmtStrArray == null) {
+                if (cacheResult) {
+                    cachedTempDateFormatsByFmtStrArray = new HashMap[CACHED_TDFS_LENGTH];
+                    this.cachedTempDateFormatsByFmtStrArray = cachedTempDateFormatsByFmtStrArray;
+                } else {
+                    cachedFormatsByFormatString = null;
+                    break readFromCache;
+                }
+            }
+
+            TemplateDateFormat format;
+            {
+                int cacheArrIdx = getTemplateDateFormatCacheArrayIndex(dateType, zonelessInput, useSQLDTTimeZone);
+                cachedFormatsByFormatString = cachedTempDateFormatsByFmtStrArray[cacheArrIdx];
+                if (cachedFormatsByFormatString == null) {
+                    if (cacheResult) {
+                        cachedFormatsByFormatString = new HashMap<>(4);
+                        cachedTempDateFormatsByFmtStrArray[cacheArrIdx] = cachedFormatsByFormatString;
+                        format = null;
+                    } else {
+                        break readFromCache;
+                    }
+                } else {
+                    format = cachedFormatsByFormatString.get(formatString);
+                }
+            }
+
+            if (format != null) {
+                return format;
+            }
+            // Cache miss; falls through
+        } while (false);
+
+        TemplateDateFormat format = getTemplateDateFormatWithoutCache(
+                formatString,
+                dateType, getLocale(), useSQLDTTimeZone ? getSQLDateAndTimeTimeZone() : getTimeZone(),
+                zonelessInput);
+        if (cacheResult) {
+            // We know here that cachedFormatsByFormatString != null
+            cachedFormatsByFormatString.put(formatString, format);
+        }
+        return format;
+    }
+
+    /**
+     * Returns the {@link TemplateDateFormat} for the given parameters without using the {@link Environment}-level
+     * cache. Of course, the {@link TemplateDateFormatFactory} involved might still uses its own cache, which can be
+     * global (class-loader-level) or {@link Environment}-level.
+     * 
+     * @param formatString
+     *            See the similar parameter of {@link TemplateDateFormatFactory#get}
+     * @param dateType
+     *            See the similar parameter of {@link TemplateDateFormatFactory#get}
+     * @param zonelessInput
+     *            See the similar parameter of {@link TemplateDateFormatFactory#get}
+     */
+    private TemplateDateFormat getTemplateDateFormatWithoutCache(
+            String formatString, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput)
+                    throws TemplateValueFormatException {
+        final int formatStringLen = formatString.length();
+        final String formatParams;
+
+        TemplateDateFormatFactory formatFactory;
+        char firstChar = formatStringLen != 0 ? formatString.charAt(0) : 0;
+
+        // As of Java 8, 'x' and 'i' (lower case) are illegal date format letters, so this is backward-compatible.
+        if (firstChar == 'x'
+                && formatStringLen > 1
+                && formatString.charAt(1) == 's') {
+            formatFactory = XSTemplateDateFormatFactory.INSTANCE;
+            formatParams = formatString; // for speed, we don't remove the prefix
+        } else if (firstChar == 'i'
+                && formatStringLen > 2
+                && formatString.charAt(1) == 's'
+                && formatString.charAt(2) == 'o') {
+            formatFactory = ISOTemplateDateFormatFactory.INSTANCE;
+            formatParams = formatString; // for speed, we don't remove the prefix
+        } else if (firstChar == '@'
+                && formatStringLen > 1
+                && Character.isLetter(formatString.charAt(1))) {
+            final String name;
+            {
+                int endIdx;
+                findParamsStart: for (endIdx = 1; endIdx < formatStringLen; endIdx++) {
+                    char c = formatString.charAt(endIdx);
+                    if (c == ' ' || c == '_') {
+                        break findParamsStart;
+                    }
+                }
+                name = formatString.substring(1, endIdx);
+                formatParams = endIdx < formatStringLen ? formatString.substring(endIdx + 1) : "";
+            }
+
+            formatFactory = getCustomDateFormat(name);
+            if (formatFactory == null) {
+                throw new UndefinedCustomFormatException(
+                        "No custom date format was defined with name " + _StringUtil.jQuote(name));
+            }
+        } else {
+            formatParams = formatString;
+            formatFactory = JavaTemplateDateFormatFactory.INSTANCE;
+        }
+
+        return formatFactory.get(formatParams, dateType, locale, timeZone,
+                zonelessInput, this);
+    }
+
+    boolean shouldUseSQLDTTZ(Class dateClass) {
+        // Attention! If you update this method, update all overloads of it!
+        return dateClass != Date.class // This pre-condition is only for speed
+                && !isSQLDateAndTimeTimeZoneSameAsNormal()
+                && isSQLDateOrTimeClass(dateClass);
+    }
+
+    private boolean shouldUseSQLDTTimeZone(boolean sqlDateOrTime) {
+        // Attention! If you update this method, update all overloads of it!
+        return sqlDateOrTime && !isSQLDateAndTimeTimeZoneSameAsNormal();
+    }
+
+    /**
+     * Tells if the given class is or is subclass of {@link java.sql.Date} or {@link java.sql.Time}.
+     */
+    private static boolean isSQLDateOrTimeClass(Class dateClass) {
+  

<TRUNCATED>


[21/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperTCCLSingletonUtil.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperTCCLSingletonUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperTCCLSingletonUtil.java
new file mode 100644
index 0000000..f5b617d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperTCCLSingletonUtil.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.model.impl;
+
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.apache.freemarker.core.util.CommonBuilder;
+
+/**
+ * Utility method for caching {@link DefaultObjectWrapper} (and subclasses) sigletons per Thread Context Class
+ * Loader.
+ */
+// [FM3] Maybe generalize and publish this functionality
+final class DefaultObjectWrapperTCCLSingletonUtil {
+
+    private DefaultObjectWrapperTCCLSingletonUtil() {
+        // Not meant to be instantiated
+    }
+
+    /**
+     * Contains the common parts of the singleton management for {@link DefaultObjectWrapper} and {@link DefaultObjectWrapper}.
+     *
+     * @param dowConstructorInvoker Creates a <em>new</em> read-only object wrapper of the desired
+     *     {@link DefaultObjectWrapper} subclass.
+     */
+    static <
+            ObjectWrapperT extends DefaultObjectWrapper,
+            BuilderT extends DefaultObjectWrapper.ExtendableBuilder<ObjectWrapperT, BuilderT>>
+    ObjectWrapperT getSingleton(
+            BuilderT builder,
+            Map<ClassLoader, Map<BuilderT, WeakReference<ObjectWrapperT>>> instanceCache,
+            ReferenceQueue<ObjectWrapperT> instanceCacheRefQue,
+            _ConstructorInvoker<ObjectWrapperT, BuilderT> dowConstructorInvoker) {
+        // DefaultObjectWrapper can't be cached across different Thread Context Class Loaders (TCCL), because the result of
+        // a class name (String) to Class mappings depends on it, and the staticModels and enumModels need that.
+        // (The ClassIntrospector doesn't have to consider the TCCL, as it only works with Class-es, not class
+        // names.)
+        ClassLoader tccl = Thread.currentThread().getContextClassLoader();
+
+        Reference<ObjectWrapperT> instanceRef;
+        Map<BuilderT, WeakReference<ObjectWrapperT>> tcclScopedCache;
+        synchronized (instanceCache) {
+            tcclScopedCache = instanceCache.get(tccl);
+            if (tcclScopedCache == null) {
+                tcclScopedCache = new HashMap<>();
+                instanceCache.put(tccl, tcclScopedCache);
+                instanceRef = null;
+            } else {
+                instanceRef = tcclScopedCache.get(builder);
+            }
+        }
+
+        ObjectWrapperT instance = instanceRef != null ? instanceRef.get() : null;
+        if (instance != null) {  // cache hit
+            return instance;
+        }
+        // cache miss
+
+        builder = builder.cloneForCacheKey();  // prevent any aliasing issues
+        instance = dowConstructorInvoker.invoke(builder);
+
+        synchronized (instanceCache) {
+            instanceRef = tcclScopedCache.get(builder);
+            ObjectWrapperT concurrentInstance = instanceRef != null ? instanceRef.get() : null;
+            if (concurrentInstance == null) {
+                tcclScopedCache.put(builder, new WeakReference<>(instance, instanceCacheRefQue));
+            } else {
+                instance = concurrentInstance;
+            }
+        }
+
+        removeClearedReferencesFromCache(instanceCache, instanceCacheRefQue);
+
+        return instance;
+    }
+
+    private static <
+            ObjectWrapperT extends DefaultObjectWrapper, BuilderT extends DefaultObjectWrapper.ExtendableBuilder>
+    void removeClearedReferencesFromCache(
+            Map<ClassLoader, Map<BuilderT, WeakReference<ObjectWrapperT>>> instanceCache,
+            ReferenceQueue<ObjectWrapperT> instanceCacheRefQue) {
+        Reference<? extends ObjectWrapperT> clearedRef;
+        while ((clearedRef = instanceCacheRefQue.poll()) != null) {
+            synchronized (instanceCache) {
+                findClearedRef: for (Map<BuilderT, WeakReference<ObjectWrapperT>> tcclScopedCache : instanceCache.values()) {
+                    for (Iterator<WeakReference<ObjectWrapperT>> it2 = tcclScopedCache.values().iterator(); it2.hasNext(); ) {
+                        if (it2.next() == clearedRef) {
+                            it2.remove();
+                            break findClearedRef;
+                        }
+                    }
+                }
+            } // sync
+        } // while poll
+    }
+
+    /**
+     * For internal use only; don't depend on this, there's no backward compatibility guarantee at all!
+     * Used when the builder delegates the product creation to something else (typically, an instance cache). Calling
+     * {@link CommonBuilder#build()} would be infinite recursion in such cases.
+     */
+    public interface _ConstructorInvoker<ProductT, BuilderT> {
+
+        ProductT invoke(BuilderT builder);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultUnassignableIteratorAdapter.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultUnassignableIteratorAdapter.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultUnassignableIteratorAdapter.java
new file mode 100644
index 0000000..c87c21f
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultUnassignableIteratorAdapter.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.model.impl;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+
+/**
+ * As opposed to {@link DefaultIteratorAdapter}, this simpler {@link Iterator} adapter is used in situations where the
+ * {@link TemplateModelIterator} won't be assigned to FreeMarker template variables, only used internally by
+ * {@code #list} or custom Java code. Because of that, it doesn't have to handle the situation where the user tries to
+ * iterate over the same value twice.
+ */
+class DefaultUnassignableIteratorAdapter implements TemplateModelIterator {
+
+    private final Iterator<?> it;
+    private final ObjectWrapper wrapper;
+
+    DefaultUnassignableIteratorAdapter(Iterator<?> it, ObjectWrapper wrapper) {
+        this.it = it;
+        this.wrapper = wrapper;
+    }
+
+    @Override
+    public TemplateModel next() throws TemplateModelException {
+        try {
+            return wrapper.wrap(it.next());
+        } catch (NoSuchElementException e) {
+            throw new TemplateModelException("The collection has no more items.", e);
+        }
+    }
+
+    @Override
+    public boolean hasNext() throws TemplateModelException {
+        return it.hasNext();
+    }
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/EmptyCallableMemberDescriptor.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/EmptyCallableMemberDescriptor.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/EmptyCallableMemberDescriptor.java
new file mode 100644
index 0000000..424a7f4
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/EmptyCallableMemberDescriptor.java
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+/**
+ * Represents that no member was chosen. Why it wasn't is represented by the two singleton instances,
+ * {@link #NO_SUCH_METHOD} and {@link #AMBIGUOUS_METHOD}. (Note that instances of these are cached associated with the
+ * argument types, thus it shouldn't store details that are specific to the actual argument values. In fact, it better
+ * remains a set of singletons.)     
+ */
+final class EmptyCallableMemberDescriptor extends MaybeEmptyCallableMemberDescriptor {
+    
+    static final EmptyCallableMemberDescriptor NO_SUCH_METHOD = new EmptyCallableMemberDescriptor();
+    static final EmptyCallableMemberDescriptor AMBIGUOUS_METHOD = new EmptyCallableMemberDescriptor();
+    
+    private EmptyCallableMemberDescriptor() { }
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/EmptyMemberAndArguments.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/EmptyMemberAndArguments.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/EmptyMemberAndArguments.java
new file mode 100644
index 0000000..2b68125
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/EmptyMemberAndArguments.java
@@ -0,0 +1,93 @@
+/*
+ * 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._DelayedOrdinal;
+
+/**
+ * Describes a failed member lookup. Instances of this must not be cached as instances may store the actual argument
+ * values.
+ */
+final class EmptyMemberAndArguments extends MaybeEmptyMemberAndArguments {
+    
+    static final EmptyMemberAndArguments WRONG_NUMBER_OF_ARGUMENTS
+            = new EmptyMemberAndArguments(
+                    "No compatible overloaded variation was found; wrong number of arguments.", true, null);
+    
+    private final Object errorDescription;
+    private final boolean numberOfArgumentsWrong;
+    private final Object[] unwrappedArguments;
+    
+    private EmptyMemberAndArguments(
+            Object errorDescription, boolean numberOfArgumentsWrong, Object[] unwrappedArguments) {
+        this.errorDescription = errorDescription;
+        this.numberOfArgumentsWrong = numberOfArgumentsWrong;
+        this.unwrappedArguments = unwrappedArguments;
+    }
+
+    static EmptyMemberAndArguments noCompatibleOverload(int unwrappableIndex) {
+        return new EmptyMemberAndArguments(
+                new Object[] { "No compatible overloaded variation was found; can't convert (unwrap) the ",
+                new _DelayedOrdinal(Integer.valueOf(unwrappableIndex)), " argument to the desired Java type." },
+                false,
+                null);
+    }
+    
+    static EmptyMemberAndArguments noCompatibleOverload(Object[] unwrappedArgs) {
+        return new EmptyMemberAndArguments(
+                "No compatible overloaded variation was found; declared parameter types and argument value types mismatch.",
+                false,
+                unwrappedArgs);
+    }
+
+    static EmptyMemberAndArguments ambiguous(Object[] unwrappedArgs) {
+        return new EmptyMemberAndArguments(
+                "Multiple compatible overloaded variations were found with the same priority.",
+                false,
+                unwrappedArgs);
+    }
+
+    static MaybeEmptyMemberAndArguments from(
+            EmptyCallableMemberDescriptor emtpyMemberDesc, Object[] unwrappedArgs) {
+        if (emtpyMemberDesc == EmptyCallableMemberDescriptor.NO_SUCH_METHOD) {
+            return noCompatibleOverload(unwrappedArgs);
+        } else if (emtpyMemberDesc == EmptyCallableMemberDescriptor.AMBIGUOUS_METHOD) {
+            return ambiguous(unwrappedArgs);
+        } else {
+            throw new IllegalArgumentException("Unrecognized constant: " + emtpyMemberDesc);
+        }
+    }
+
+    Object getErrorDescription() {
+        return errorDescription;
+    }
+
+    /**
+     * @return {@code null} if the error has occurred earlier than the full argument list was unwrapped.
+     */
+    Object[] getUnwrappedArguments() {
+        return unwrappedArguments;
+    }
+
+    public boolean isNumberOfArgumentsWrong() {
+        return numberOfArgumentsWrong;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/EnumModels.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/EnumModels.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/EnumModels.java
new file mode 100644
index 0000000..e4c96c8
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/EnumModels.java
@@ -0,0 +1,50 @@
+/*
+ * 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.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.freemarker.core.model.TemplateModel;
+
+class EnumModels extends ClassBasedModelFactory {
+
+    public EnumModels(DefaultObjectWrapper wrapper) {
+        super(wrapper);
+    }
+    
+    @Override
+    protected TemplateModel createModel(Class clazz) {
+        Object[] obj = clazz.getEnumConstants();
+        if (obj == null) {
+            // Return null - it'll manifest itself as undefined in the template.
+            // We're doing this rather than throw an exception as this way 
+            // people can use someEnumModel?default({}) to gracefully fall back 
+            // to an empty hash if they want to.
+            return null;
+        }
+        Map map = new LinkedHashMap();
+        for (Object anObj : obj) {
+            Enum value = (Enum) anObj;
+            map.put(value.name(), value);
+        }
+        return new SimpleHash(map, getWrapper());
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/HashAdapter.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/HashAdapter.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/HashAdapter.java
new file mode 100644
index 0000000..add437a
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/HashAdapter.java
@@ -0,0 +1,181 @@
+/*
+ * 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.util.AbstractMap;
+import java.util.AbstractSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelAdapter;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+import org.apache.freemarker.core.util.UndeclaredThrowableException;
+
+/**
+ * Adapts a {@link TemplateHashModel} to a {@link Map}.
+ */
+class HashAdapter extends AbstractMap implements TemplateModelAdapter {
+    private final DefaultObjectWrapper wrapper;
+    private final TemplateHashModel model;
+    private Set entrySet;
+    
+    HashAdapter(TemplateHashModel model, DefaultObjectWrapper wrapper) {
+        this.model = model;
+        this.wrapper = wrapper;
+    }
+    
+    @Override
+    public TemplateModel getTemplateModel() {
+        return model;
+    }
+    
+    @Override
+    public boolean isEmpty() {
+        try {
+            return model.isEmpty();
+        } catch (TemplateModelException e) {
+            throw new UndeclaredThrowableException(e);
+        }
+    }
+    
+    @Override
+    public Object get(Object key) {
+        try {
+            return wrapper.unwrap(model.get(String.valueOf(key)));
+        } catch (TemplateModelException e) {
+            throw new UndeclaredThrowableException(e);
+        }
+    }
+
+    @Override
+    public boolean containsKey(Object key) {
+        // A quick check that doesn't require TemplateHashModelEx 
+        if (get(key) != null) {
+            return true;
+        }
+        return super.containsKey(key);
+    }
+    
+    @Override
+    public Set entrySet() {
+        if (entrySet != null) {
+            return entrySet;
+        }
+        return entrySet = new AbstractSet() {
+            @Override
+            public Iterator iterator() {
+                final TemplateModelIterator i;
+                try {
+                     i = getModelEx().keys().iterator();
+                } catch (TemplateModelException e) {
+                    throw new UndeclaredThrowableException(e);
+                }
+                return new Iterator() {
+                    @Override
+                    public boolean hasNext() {
+                        try {
+                            return i.hasNext();
+                        } catch (TemplateModelException e) {
+                            throw new UndeclaredThrowableException(e);
+                        }
+                    }
+                    
+                    @Override
+                    public Object next() {
+                        final Object key;
+                        try {
+                            key = wrapper.unwrap(i.next());
+                        } catch (TemplateModelException e) {
+                            throw new UndeclaredThrowableException(e);
+                        }
+                        return new Map.Entry() {
+                            @Override
+                            public Object getKey() {
+                                return key;
+                            }
+                            
+                            @Override
+                            public Object getValue() {
+                                return get(key);
+                            }
+                            
+                            @Override
+                            public Object setValue(Object value) {
+                                throw new UnsupportedOperationException();
+                            }
+                            
+                            @Override
+                            public boolean equals(Object o) {
+                                if (!(o instanceof Map.Entry))
+                                    return false;
+                                Map.Entry e = (Map.Entry) o;
+                                Object k1 = getKey();
+                                Object k2 = e.getKey();
+                                if (k1 == k2 || (k1 != null && k1.equals(k2))) {
+                                    Object v1 = getValue();
+                                    Object v2 = e.getValue();
+                                    if (v1 == v2 || (v1 != null && v1.equals(v2))) 
+                                        return true;
+                                }
+                                return false;
+                            }
+                        
+                            @Override
+                            public int hashCode() {
+                                Object value = getValue();
+                                return (key == null ? 0 : key.hashCode()) ^
+                                       (value == null ? 0 : value.hashCode());
+                            }
+                        };
+                    }
+                    
+                    @Override
+                    public void remove() {
+                        throw new UnsupportedOperationException();
+                    }
+                };
+            }
+            
+            @Override
+            public int size() {
+                try {
+                    return getModelEx().size();
+                } catch (TemplateModelException e) {
+                    throw new UndeclaredThrowableException(e);
+                }
+            }
+        };
+    }
+    
+    private TemplateHashModelEx getModelEx() {
+        if (model instanceof TemplateHashModelEx) {
+            return ((TemplateHashModelEx) model);
+        }
+        throw new UnsupportedOperationException(
+                "Operation supported only on TemplateHashModelEx. " + 
+                model.getClass().getName() + " does not implement it though.");
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/InvalidPropertyException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/InvalidPropertyException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/InvalidPropertyException.java
new file mode 100644
index 0000000..ff73a05
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/InvalidPropertyException.java
@@ -0,0 +1,34 @@
+/*
+ * 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.model.TemplateModelException;
+
+/**
+ * An exception thrown when there is an attempt to access
+ * an invalid bean property when we are in a "strict bean" mode
+ */
+
+public class InvalidPropertyException extends TemplateModelException {
+	
+    public InvalidPropertyException(String description) {
+        super(description);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/JRebelClassChangeNotifier.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/JRebelClassChangeNotifier.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/JRebelClassChangeNotifier.java
new file mode 100644
index 0000000..15bc8d7
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/JRebelClassChangeNotifier.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.model.impl;
+
+import java.lang.ref.WeakReference;
+
+import org.zeroturnaround.javarebel.ClassEventListener;
+import org.zeroturnaround.javarebel.ReloaderFactory;
+
+class JRebelClassChangeNotifier implements ClassChangeNotifier {
+
+    static void testAvailability() {
+        ReloaderFactory.getInstance();
+    }
+
+    @Override
+    public void subscribe(ClassIntrospector classIntrospector) {
+        ReloaderFactory.getInstance().addClassReloadListener(
+                new ClassIntrospectorCacheInvalidator(classIntrospector));
+    }
+
+    private static class ClassIntrospectorCacheInvalidator
+            implements ClassEventListener {
+        private final WeakReference ref;
+
+        ClassIntrospectorCacheInvalidator(ClassIntrospector w) {
+            ref = new WeakReference(w);
+        }
+
+        @Override
+        public void onClassEvent(int eventType, Class pClass) {
+            ClassIntrospector ci = (ClassIntrospector) ref.get();
+            if (ci == null) {
+                ReloaderFactory.getInstance().removeClassReloadListener(this);
+            } else if (eventType == ClassEventListener.EVENT_RELOADED) {
+                ci.remove(pClass);
+            }
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/JavaMethodModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/JavaMethodModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/JavaMethodModel.java
new file mode 100644
index 0000000..6408117
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/JavaMethodModel.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.model.impl;
+
+import java.lang.reflect.Member;
+import java.lang.reflect.Method;
+import java.util.List;
+
+import org.apache.freemarker.core._UnexpectedTypeErrorExplainerTemplateModel;
+import org.apache.freemarker.core.model.TemplateMethodModelEx;
+import org.apache.freemarker.core.model.TemplateModelException;
+
+/**
+ * Wraps a {@link Method} into the {@link TemplateMethodModelEx} interface. It is used by {@link BeanModel} to wrap
+ * non-overloaded methods.
+ */
+public final class JavaMethodModel extends SimpleMethod implements TemplateMethodModelEx,
+        _UnexpectedTypeErrorExplainerTemplateModel {
+    private final Object object;
+    private final DefaultObjectWrapper wrapper;
+
+    /**
+     * Creates a model for a specific method on a specific object.
+     * @param object the object to call the method on, or {@code null} for a static method.
+     * @param method the method that will be invoked.
+     * @param argTypes Either pass in {@code Method#getParameterTypes() method.getParameterTypes()} here,
+     *          or reuse an earlier result of that call (for speed). Not {@code null}.
+     */
+    JavaMethodModel(Object object, Method method, Class[] argTypes, DefaultObjectWrapper wrapper) {
+        super(method, argTypes);
+        this.object = object;
+        this.wrapper = wrapper;
+    }
+
+    /**
+     * Invokes the method, passing it the arguments from the list.
+     */
+    @Override
+    public Object exec(List arguments) throws TemplateModelException {
+        try {
+            return wrapper.invokeMethod(object, (Method) getMember(), 
+                    unwrapArguments(arguments, wrapper));
+        } catch (TemplateModelException e) {
+            throw e;
+        } catch (Exception e) {
+            throw _MethodUtil.newInvocationTemplateModelException(object, getMember(), e);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return getMember().toString();
+    }
+
+    /**
+     * Implementation of experimental interface; don't use it, no backward compatibility guarantee!
+     */
+    @Override
+    public Object[] explainTypeError(Class[] expectedClasses) {
+        final Member member = getMember();
+        if (!(member instanceof Method)) {
+            return null;  // This shouldn't occur
+        }
+        Method m = (Method) member;
+        
+        final Class returnType = m.getReturnType();
+        if (returnType == null || returnType == void.class || returnType == Void.class) {
+            return null;  // Calling it won't help
+        }
+        
+        String mName = m.getName();
+        if (mName.startsWith("get") && mName.length() > 3 && Character.isUpperCase(mName.charAt(3))
+                && (m.getParameterTypes().length == 0)) {
+            return new Object[] {
+                    "Maybe using obj.something instead of obj.getSomething will yield the desired value." };
+        } else if (mName.startsWith("is") && mName.length() > 2 && Character.isUpperCase(mName.charAt(2))
+                && (m.getParameterTypes().length == 0)) {
+            return new Object[] {
+                    "Maybe using obj.something instead of obj.isSomething will yield the desired value." };
+        } else {
+            return new Object[] {
+                    "Maybe using obj.something(",
+                    (m.getParameterTypes().length != 0 ? "params" : ""),
+                    ") instead of obj.something will yield the desired value" };
+        }
+    }
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MapKeyValuePairIterator.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MapKeyValuePairIterator.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MapKeyValuePairIterator.java
new file mode 100644
index 0000000..85be491
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MapKeyValuePairIterator.java
@@ -0,0 +1,77 @@
+/*
+ * 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.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateHashModelEx2;
+import org.apache.freemarker.core.model.TemplateHashModelEx2.KeyValuePair;
+import org.apache.freemarker.core.model.TemplateHashModelEx2.KeyValuePairIterator;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+
+/**
+ *  Implementation of {@link KeyValuePairIterator} for a {@link TemplateHashModelEx2} that wraps or otherwise uses a
+ *  {@link Map} internally.
+ *
+ *  @since 2.3.25
+ */
+public class MapKeyValuePairIterator implements KeyValuePairIterator {
+
+    private final Iterator<Entry<?, ?>> entrySetIterator;
+    
+    private final ObjectWrapper objectWrapper;
+    
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    public <K, V> MapKeyValuePairIterator(Map<?, ?> map, ObjectWrapper objectWrapper) {
+        entrySetIterator = ((Map) map).entrySet().iterator();
+        this.objectWrapper = objectWrapper;
+    }
+
+    @Override
+    public boolean hasNext() {
+        return entrySetIterator.hasNext();
+    }
+
+    @Override
+    public KeyValuePair next() {
+        final Entry<?, ?> entry = entrySetIterator.next();
+        return new KeyValuePair() {
+
+            @Override
+            public TemplateModel getKey() throws TemplateModelException {
+                return wrap(entry.getKey());
+            }
+
+            @Override
+            public TemplateModel getValue() throws TemplateModelException {
+                return wrap(entry.getValue());
+            }
+            
+        };
+    }
+    
+    private TemplateModel wrap(Object obj) throws TemplateModelException {
+        return (obj instanceof TemplateModel) ? (TemplateModel) obj : objectWrapper.wrap(obj);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MaybeEmptyCallableMemberDescriptor.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MaybeEmptyCallableMemberDescriptor.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MaybeEmptyCallableMemberDescriptor.java
new file mode 100644
index 0000000..c713ed9
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MaybeEmptyCallableMemberDescriptor.java
@@ -0,0 +1,25 @@
+/*
+ * 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;
+
+/**
+ * Superclass of the {@link EmptyCallableMemberDescriptor} and {@link CallableMemberDescriptor} "case classes".
+ */
+abstract class MaybeEmptyCallableMemberDescriptor { }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MaybeEmptyMemberAndArguments.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MaybeEmptyMemberAndArguments.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MaybeEmptyMemberAndArguments.java
new file mode 100644
index 0000000..d14a343
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MaybeEmptyMemberAndArguments.java
@@ -0,0 +1,22 @@
+/*
+ * 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;
+
+abstract class MaybeEmptyMemberAndArguments { }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MemberAndArguments.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MemberAndArguments.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MemberAndArguments.java
new file mode 100644
index 0000000..3a37d9d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MemberAndArguments.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.model.impl;
+
+import java.lang.reflect.InvocationTargetException;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+
+/**
+ */
+class MemberAndArguments extends MaybeEmptyMemberAndArguments {
+    
+    private final CallableMemberDescriptor callableMemberDesc;
+    private final Object[] args;
+    
+    /**
+     * @param args The already unwrapped arguments
+     */
+    MemberAndArguments(CallableMemberDescriptor callableMemberDesc, Object[] args) {
+        this.callableMemberDesc = callableMemberDesc;
+        this.args = args;
+    }
+    
+    /**
+     * The already unwrapped arguments.
+     */
+    Object[] getArgs() {
+        return args;
+    }
+    
+    TemplateModel invokeMethod(DefaultObjectWrapper ow, Object obj)
+            throws TemplateModelException, InvocationTargetException, IllegalAccessException {
+        return callableMemberDesc.invokeMethod(ow, obj, args);
+    }
+
+    Object invokeConstructor(DefaultObjectWrapper ow)
+            throws IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException,
+            TemplateModelException {
+        return callableMemberDesc.invokeConstructor(ow, args);
+    }
+    
+    CallableMemberDescriptor getCallableMemberDescriptor() {
+        return callableMemberDesc;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MethodAppearanceFineTuner.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MethodAppearanceFineTuner.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MethodAppearanceFineTuner.java
new file mode 100644
index 0000000..c60599d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MethodAppearanceFineTuner.java
@@ -0,0 +1,156 @@
+/*
+ * 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.IndexedPropertyDescriptor;
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.Method;
+
+/**
+ * Used for customizing how the methods are visible from templates, via
+ * {@link DefaultObjectWrapper.ExtendableBuilder#setMethodAppearanceFineTuner(MethodAppearanceFineTuner)}.
+ * The object that implements this should also implement {@link SingletonCustomizer} whenever possible.
+ * 
+ * @since 2.3.21
+ */
+public interface MethodAppearanceFineTuner {
+
+    /**
+     * Implement this to tweak certain aspects of how methods appear in the
+     * data-model. {@link DefaultObjectWrapper} will pass in all Java methods here that
+     * it intends to expose in the data-model as methods (so you can do
+     * <tt>obj.foo()</tt> in the template).
+     * With this method you can do the following tweaks:
+     * <ul>
+     *   <li>Hide a method that would be otherwise shown by calling
+     *     {@link org.apache.freemarker.core.model.impl.MethodAppearanceFineTuner.Decision#setExposeMethodAs(String)}
+     *     with <tt>null</tt> parameter. Note that you can't un-hide methods
+     *     that are not public or are considered to by unsafe
+     *     (like {@link Object#wait()}) because
+     *     {@link #process} is not called for those.</li>
+     *   <li>Show the method with a different name in the data-model than its
+     *     real name by calling
+     *     {@link org.apache.freemarker.core.model.impl.MethodAppearanceFineTuner.Decision#setExposeMethodAs(String)}
+     *     with non-<tt>null</tt> parameter.
+     *   <li>Create a fake JavaBean property for this method by calling
+     *     {@link org.apache.freemarker.core.model.impl.MethodAppearanceFineTuner.Decision#setExposeAsProperty(PropertyDescriptor)}.
+     *     For example, if you have <tt>int size()</tt> in a class, but you
+     *     want it to be accessed from the templates as <tt>obj.size</tt>,
+     *     rather than as <tt>obj.size()</tt>, you can do that with this.
+     *     The default is {@code null}, which means that no fake property is
+     *     created for the method. You need not and shouldn't set this
+     *     to non-<tt>null</tt> for the getter methods of real JavaBean
+     *     properties, as those are automatically shown as properties anyway.
+     *     The property name in the {@link PropertyDescriptor} can be anything,
+     *     but the method (or methods) in it must belong to the class that
+     *     is given as the <tt>clazz</tt> parameter or it must be inherited from
+     *     that class, or else whatever errors can occur later.
+     *     {@link IndexedPropertyDescriptor}-s are supported.
+     *     If a real JavaBean property of the same name exists, it won't be
+     *     replaced by the fake one. Also if a fake property of the same name
+     *     was assigned earlier, it won't be replaced.
+     *   <li>Prevent the method to hide a JavaBean property (fake or real) of
+     *     the same name by calling
+     *     {@link org.apache.freemarker.core.model.impl.MethodAppearanceFineTuner.Decision#setMethodShadowsProperty(boolean)}
+     *     with <tt>false</tt>. The default is <tt>true</tt>, so if you have
+     *     both a property and a method called "foo", then in the template
+     *     <tt>myObject.foo</tt> will return the method itself instead
+     *     of the property value, which is often undesirable.
+     * </ul>
+     * 
+     * <p>Note that you can expose a Java method both as a method and as a
+     * JavaBean property on the same time, however you have to chose different
+     * names for them to prevent shadowing. 
+     * 
+     * @param in Describes the method about which the decision will have to be made.
+     *  
+     * @param out Stores how the method will be exposed in the
+     *   data-model after {@link #process} returns.
+     *   This is initialized so that it reflects the default
+     *   behavior of {@link DefaultObjectWrapper}, so you don't have to do anything with this
+     *   when you don't want to change the default behavior.
+     */
+    void process(DecisionInput in, Decision out);
+
+    /**
+     * Used for {@link MethodAppearanceFineTuner#process} to store the results.
+     */
+    final class Decision {
+        private PropertyDescriptor exposeAsProperty;
+        private String exposeMethodAs;
+        private boolean methodShadowsProperty;
+
+        void setDefaults(Method m) {
+            exposeAsProperty = null;
+            exposeMethodAs = m.getName();
+            methodShadowsProperty = true;
+        }
+
+        public PropertyDescriptor getExposeAsProperty() {
+            return exposeAsProperty;
+        }
+
+        public void setExposeAsProperty(PropertyDescriptor exposeAsProperty) {
+            this.exposeAsProperty = exposeAsProperty;
+        }
+
+        public String getExposeMethodAs() {
+            return exposeMethodAs;
+        }
+
+        public void setExposeMethodAs(String exposeMethodAs) {
+            this.exposeMethodAs = exposeMethodAs;
+        }
+
+        public boolean getMethodShadowsProperty() {
+            return methodShadowsProperty;
+        }
+
+        public void setMethodShadowsProperty(boolean methodShadowsProperty) {
+            this.methodShadowsProperty = methodShadowsProperty;
+        }
+
+    }
+
+    /**
+     * Used for {@link org.apache.freemarker.core.model.impl.MethodAppearanceFineTuner#process} as input parameter.
+     */
+    final class DecisionInput {
+        private Method method;
+        private Class<?> containingClass;
+
+        void setMethod(Method method) {
+            this.method = method;
+        }
+
+        void setContainingClass(Class<?> containingClass) {
+            this.containingClass = containingClass;
+        }
+
+        public Method getMethod() {
+            return method;
+        }
+
+        public Class/*<?>*/ getContainingClass() {
+            return containingClass;
+        }
+
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MethodSorter.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MethodSorter.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MethodSorter.java
new file mode 100644
index 0000000..9218bdf
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MethodSorter.java
@@ -0,0 +1,36 @@
+/*
+ * 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.List;
+
+/**
+ * Used for JUnit testing method-order dependence bugs via
+ * {@link DefaultObjectWrapper.Builder#setMethodSorter(MethodSorter)}.
+ */
+interface MethodSorter {
+
+    /**
+     * Sorts the methods in place (that is, by modifying the parameter list).
+     */
+    void sortMethodDescriptors(List<MethodDescriptor> methodDescriptors);
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/NonPrimitiveArrayBackedReadOnlyList.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/NonPrimitiveArrayBackedReadOnlyList.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/NonPrimitiveArrayBackedReadOnlyList.java
new file mode 100644
index 0000000..1d5bae8
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/NonPrimitiveArrayBackedReadOnlyList.java
@@ -0,0 +1,42 @@
+/*
+ * 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.util.AbstractList;
+
+class NonPrimitiveArrayBackedReadOnlyList extends AbstractList {
+    
+    private final Object[] array;
+    
+    NonPrimitiveArrayBackedReadOnlyList(Object[] array) {
+        this.array = array;
+    }
+
+    @Override
+    public Object get(int index) {
+        return array[index];
+    }
+
+    @Override
+    public int size() {
+        return array.length;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/OverloadedFixArgsMethods.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/OverloadedFixArgsMethods.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/OverloadedFixArgsMethods.java
new file mode 100644
index 0000000..bff717d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/OverloadedFixArgsMethods.java
@@ -0,0 +1,99 @@
+/*
+ * 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.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.freemarker.core.model.ObjectWrapperAndUnwrapper;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+
+/**
+ * Stores the non-varargs methods for a {@link OverloadedMethods} object.
+ */
+class OverloadedFixArgsMethods extends OverloadedMethodsSubset {
+    
+    OverloadedFixArgsMethods() {
+        super();
+    }
+
+    @Override
+    Class[] preprocessParameterTypes(CallableMemberDescriptor memberDesc) {
+        return memberDesc.getParamTypes();
+    }
+    
+    @Override
+    void afterWideningUnwrappingHints(Class[] paramTypes, int[] paramNumericalTypes) {
+        // Do nothing
+    }
+
+    @Override
+    MaybeEmptyMemberAndArguments getMemberAndArguments(List tmArgs, DefaultObjectWrapper unwrapper)
+    throws TemplateModelException {
+        if (tmArgs == null) {
+            // null is treated as empty args
+            tmArgs = Collections.EMPTY_LIST;
+        }
+        final int argCount = tmArgs.size();
+        final Class[][] unwrappingHintsByParamCount = getUnwrappingHintsByParamCount();
+        if (unwrappingHintsByParamCount.length <= argCount) {
+            return EmptyMemberAndArguments.WRONG_NUMBER_OF_ARGUMENTS;
+        }
+        Class[] unwarppingHints = unwrappingHintsByParamCount[argCount];
+        if (unwarppingHints == null) {
+            return EmptyMemberAndArguments.WRONG_NUMBER_OF_ARGUMENTS;
+        }
+        
+        Object[] pojoArgs = new Object[argCount];
+        
+        int[] typeFlags = getTypeFlags(argCount);
+        if (typeFlags == ALL_ZEROS_ARRAY) {
+            typeFlags = null;
+        }
+
+        Iterator it = tmArgs.iterator();
+        for (int i = 0; i < argCount; ++i) {
+            Object pojo = unwrapper.tryUnwrapTo(
+                    (TemplateModel) it.next(),
+                    unwarppingHints[i],
+                    typeFlags != null ? typeFlags[i] : 0);
+            if (pojo == ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS) {
+                return EmptyMemberAndArguments.noCompatibleOverload(i + 1);
+            }
+            pojoArgs[i] = pojo;
+        }
+        
+        MaybeEmptyCallableMemberDescriptor maybeEmtpyMemberDesc = getMemberDescriptorForArgs(pojoArgs, false);
+        if (maybeEmtpyMemberDesc instanceof CallableMemberDescriptor) {
+            CallableMemberDescriptor memberDesc = (CallableMemberDescriptor) maybeEmtpyMemberDesc;
+            if (typeFlags != null) {
+                // Note that overloaded method selection has already accounted for overflow errors when the method
+                // was selected. So this forced conversion shouldn't cause such corruption. Except, conversion from
+                // BigDecimal is allowed to overflow for backward-compatibility.
+                forceNumberArgumentsToParameterTypes(pojoArgs, memberDesc.getParamTypes(), typeFlags);
+            }
+            return new MemberAndArguments(memberDesc, pojoArgs);
+        } else {
+            return EmptyMemberAndArguments.from((EmptyCallableMemberDescriptor) maybeEmtpyMemberDesc, pojoArgs);
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/OverloadedMethods.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/OverloadedMethods.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/OverloadedMethods.java
new file mode 100644
index 0000000..1ba1a56
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/OverloadedMethods.java
@@ -0,0 +1,271 @@
+/*
+ * 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.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.freemarker.core._DelayedConversionToString;
+import org.apache.freemarker.core._ErrorDescriptionBuilder;
+import org.apache.freemarker.core._TemplateModelException;
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.util.FTLUtil;
+import org.apache.freemarker.core.util._ClassUtil;
+
+/**
+ * Used instead of {@link java.lang.reflect.Method} or {@link java.lang.reflect.Constructor} for overloaded methods and
+ * constructors.
+ * 
+ * <p>After the initialization with the {@link #addMethod(Method)} and {@link #addConstructor(Constructor)} calls are
+ * done, the instance must be thread-safe. Before that, it's the responsibility of the caller of those methods to
+ * ensure that the object is properly publishing to other threads.
+ */
+final class OverloadedMethods {
+
+    private final OverloadedMethodsSubset fixArgMethods;
+    private OverloadedMethodsSubset varargMethods;
+    
+    OverloadedMethods() {
+        fixArgMethods = new OverloadedFixArgsMethods();
+    }
+    
+    void addMethod(Method method) {
+        final Class[] paramTypes = method.getParameterTypes();
+        addCallableMemberDescriptor(new ReflectionCallableMemberDescriptor(method, paramTypes));
+    }
+
+    void addConstructor(Constructor constr) {
+        final Class[] paramTypes = constr.getParameterTypes();
+        addCallableMemberDescriptor(new ReflectionCallableMemberDescriptor(constr, paramTypes));
+    }
+    
+    private void addCallableMemberDescriptor(ReflectionCallableMemberDescriptor memberDesc) {
+        // Note: "varargs" methods are always callable as oms args, with a sequence (array) as the last parameter.
+        fixArgMethods.addCallableMemberDescriptor(memberDesc);
+        if (memberDesc.isVarargs()) {
+            if (varargMethods == null) {
+                varargMethods = new OverloadedVarArgsMethods();
+            }
+            varargMethods.addCallableMemberDescriptor(memberDesc);
+        }
+    }
+    
+    MemberAndArguments getMemberAndArguments(List/*<TemplateModel>*/ tmArgs, DefaultObjectWrapper unwrapper)
+    throws TemplateModelException {
+        // Try to find a oms args match:
+        MaybeEmptyMemberAndArguments fixArgsRes = fixArgMethods.getMemberAndArguments(tmArgs, unwrapper);
+        if (fixArgsRes instanceof MemberAndArguments) {
+            return (MemberAndArguments) fixArgsRes;
+        }
+
+        // Try to find a varargs match:
+        MaybeEmptyMemberAndArguments varargsRes;
+        if (varargMethods != null) {
+            varargsRes = varargMethods.getMemberAndArguments(tmArgs, unwrapper);
+            if (varargsRes instanceof MemberAndArguments) {
+                return (MemberAndArguments) varargsRes;
+            }
+        } else {
+            varargsRes = null;
+        }
+        
+        _ErrorDescriptionBuilder edb = new _ErrorDescriptionBuilder(
+                toCompositeErrorMessage(
+                        (EmptyMemberAndArguments) fixArgsRes,
+                        (EmptyMemberAndArguments) varargsRes,
+                        tmArgs),
+                "\nThe matching overload was searched among these members:\n",
+                memberListToString());
+        addMarkupBITipAfterNoNoMarchIfApplicable(edb, tmArgs);
+        throw new _TemplateModelException(edb);
+    }
+
+    private Object[] toCompositeErrorMessage(
+            final EmptyMemberAndArguments fixArgsEmptyRes, final EmptyMemberAndArguments varargsEmptyRes,
+            List tmArgs) {
+        final Object[] argsErrorMsg;
+        if (varargsEmptyRes != null) {
+            if (fixArgsEmptyRes == null || fixArgsEmptyRes.isNumberOfArgumentsWrong()) {
+                argsErrorMsg = toErrorMessage(varargsEmptyRes, tmArgs);
+            } else {
+                argsErrorMsg = new Object[] {
+                        "When trying to call the non-varargs overloads:\n",
+                        toErrorMessage(fixArgsEmptyRes, tmArgs),
+                        "\nWhen trying to call the varargs overloads:\n",
+                        toErrorMessage(varargsEmptyRes, null)
+                };
+            }
+        } else {
+            argsErrorMsg = toErrorMessage(fixArgsEmptyRes, tmArgs);
+        }
+        return argsErrorMsg;
+    }
+
+    private Object[] toErrorMessage(EmptyMemberAndArguments res, List/*<TemplateModel>*/ tmArgs) {
+        final Object[] unwrappedArgs = res.getUnwrappedArguments();
+        return new Object[] {
+                res.getErrorDescription(),
+                tmArgs != null
+                        ? new Object[] {
+                                "\nThe FTL type of the argument values were: ", getTMActualParameterTypes(tmArgs), "." }
+                        : "",
+                unwrappedArgs != null
+                        ? new Object[] {
+                                "\nThe Java type of the argument values were: ",
+                                getUnwrappedActualParameterTypes(unwrappedArgs) + "." }
+                        : ""};
+    }
+
+    private _DelayedConversionToString memberListToString() {
+        return new _DelayedConversionToString(null) {
+            
+            @Override
+            protected String doConversion(Object obj) {
+                final Iterator fixArgMethodsIter = fixArgMethods.getMemberDescriptors();
+                final Iterator varargMethodsIter = varargMethods != null ? varargMethods.getMemberDescriptors() : null;
+                
+                boolean hasMethods = fixArgMethodsIter.hasNext() || (varargMethodsIter != null && varargMethodsIter.hasNext()); 
+                if (hasMethods) {
+                    StringBuilder sb = new StringBuilder();
+                    HashSet fixArgMethods = new HashSet();
+                    while (fixArgMethodsIter.hasNext()) {
+                        if (sb.length() != 0) sb.append(",\n");
+                        sb.append("    ");
+                        CallableMemberDescriptor callableMemberDesc = (CallableMemberDescriptor) fixArgMethodsIter.next();
+                        fixArgMethods.add(callableMemberDesc);
+                        sb.append(callableMemberDesc.getDeclaration());
+                    }
+                    if (varargMethodsIter != null) {
+                        while (varargMethodsIter.hasNext()) {
+                            CallableMemberDescriptor callableMemberDesc = (CallableMemberDescriptor) varargMethodsIter.next();
+                            if (!fixArgMethods.contains(callableMemberDesc)) {
+                                if (sb.length() != 0) sb.append(",\n");
+                                sb.append("    ");
+                                sb.append(callableMemberDesc.getDeclaration());
+                            }
+                        }
+                    }
+                    return sb.toString();
+                } else {
+                    return "No members";
+                }
+            }
+            
+        };
+    }
+    
+    /**
+     * Adds tip to the error message if converting a {@link TemplateMarkupOutputModel} argument to {@link String} might
+     * allows finding a matching overload. 
+     */
+    private void addMarkupBITipAfterNoNoMarchIfApplicable(_ErrorDescriptionBuilder edb,
+            List tmArgs) {
+        for (int argIdx = 0; argIdx < tmArgs.size(); argIdx++) {
+            Object tmArg = tmArgs.get(argIdx);
+            if (tmArg instanceof TemplateMarkupOutputModel) {
+                for (Iterator membDescs = fixArgMethods.getMemberDescriptors(); membDescs.hasNext();) {
+                    CallableMemberDescriptor membDesc = (CallableMemberDescriptor) membDescs.next();
+                    Class[] paramTypes = membDesc.getParamTypes();
+                    
+                    Class paramType = null;
+                    if (membDesc.isVarargs() && argIdx >= paramTypes.length - 1) {
+                        paramType = paramTypes[paramTypes.length - 1];
+                        if (paramType.isArray()) {
+                            paramType = paramType.getComponentType();
+                        }
+                    }
+                    if (paramType == null && argIdx < paramTypes.length) {
+                        paramType = paramTypes[argIdx];
+                    }
+                    if (paramType != null) {
+                        if (paramType.isAssignableFrom(String.class) && !paramType.isAssignableFrom(tmArg.getClass())) {
+                            edb.tip(JavaMethodModel.MARKUP_OUTPUT_TO_STRING_TIP);
+                            return;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private _DelayedConversionToString getTMActualParameterTypes(List arguments) {
+        final String[] argumentTypeDescs = new String[arguments.size()];
+        for (int i = 0; i < arguments.size(); i++) {
+            argumentTypeDescs[i] = FTLUtil.getTypeDescription((TemplateModel) arguments.get(i));
+        }
+        
+        return new DelayedCallSignatureToString(argumentTypeDescs) {
+
+            @Override
+            String argumentToString(Object argType) {
+                return (String) argType;
+            }
+            
+        };
+    }
+    
+    private Object getUnwrappedActualParameterTypes(Object[] unwrappedArgs) {
+        final Class[] argumentTypes = new Class[unwrappedArgs.length];
+        for (int i = 0; i < unwrappedArgs.length; i++) {
+            Object unwrappedArg = unwrappedArgs[i];
+            argumentTypes[i] = unwrappedArg != null ? unwrappedArg.getClass() : null;
+        }
+        
+        return new DelayedCallSignatureToString(argumentTypes) {
+
+            @Override
+            String argumentToString(Object argType) {
+                return argType != null
+                        ? _ClassUtil.getShortClassName((Class) argType)
+                        : _ClassUtil.getShortClassNameOfObject(null);
+            }
+            
+        };
+    }
+    
+    private abstract class DelayedCallSignatureToString extends _DelayedConversionToString {
+
+        public DelayedCallSignatureToString(Object[] argTypeArray) {
+            super(argTypeArray);
+        }
+
+        @Override
+        protected String doConversion(Object obj) {
+            Object[] argTypes = (Object[]) obj;
+            
+            StringBuilder sb = new StringBuilder();
+            for (int i = 0; i < argTypes.length; i++) {
+                if (i != 0) sb.append(", ");
+                sb.append(argumentToString(argTypes[i]));
+            }
+            
+            return sb.toString();
+        }
+        
+        abstract String argumentToString(Object argType);
+        
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/OverloadedMethodsModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/OverloadedMethodsModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/OverloadedMethodsModel.java
new file mode 100644
index 0000000..9a66a6d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/OverloadedMethodsModel.java
@@ -0,0 +1,65 @@
+/*
+ * 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.util.List;
+
+import org.apache.freemarker.core.model.TemplateMethodModel;
+import org.apache.freemarker.core.model.TemplateMethodModelEx;
+import org.apache.freemarker.core.model.TemplateModelException;
+
+/**
+ * Wraps a set of same-name overloaded methods behind {@link TemplateMethodModel} interface,
+ * like if it was a single method, chooses among them behind the scenes on call-time based on the argument values.
+ */
+class OverloadedMethodsModel implements TemplateMethodModelEx {
+    private final Object object;
+    private final OverloadedMethods overloadedMethods;
+    private final DefaultObjectWrapper wrapper;
+    
+    OverloadedMethodsModel(Object object, OverloadedMethods overloadedMethods, DefaultObjectWrapper wrapper) {
+        this.object = object;
+        this.overloadedMethods = overloadedMethods;
+        this.wrapper = wrapper;
+    }
+
+    /**
+     * Invokes the method, passing it the arguments from the list. The actual
+     * method to call from several overloaded methods will be chosen based
+     * on the classes of the arguments.
+     * @throws TemplateModelException if the method cannot be chosen
+     * unambiguously.
+     */
+    @Override
+    public Object exec(List arguments) throws TemplateModelException {
+        MemberAndArguments maa = overloadedMethods.getMemberAndArguments(arguments, wrapper);
+        try {
+            return maa.invokeMethod(wrapper, object);
+        } catch (Exception e) {
+            if (e instanceof TemplateModelException) throw (TemplateModelException) e;
+            
+            throw _MethodUtil.newInvocationTemplateModelException(
+                    object,
+                    maa.getCallableMemberDescriptor(),
+                    e);
+        }
+    }
+}


[04/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/CoercionToTextualTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/CoercionToTextualTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/CoercionToTextualTest.java
new file mode 100644
index 0000000..91d3749
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/CoercionToTextualTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.util.Collections;
+import java.util.Date;
+
+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.outputformat.impl.HTMLOutputFormat;
+import org.apache.freemarker.core.userpkg.HTMLISOTemplateDateFormatFactory;
+import org.apache.freemarker.core.userpkg.PrintfGTemplateNumberFormatFactory;
+import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory;
+import org.apache.freemarker.test.TemplateTest;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Before;
+import org.junit.Test;
+
+@SuppressWarnings("boxing")
+public class CoercionToTextualTest extends TemplateTest {
+    
+    /** 2015-09-06T12:00:00Z */
+    private static long T = 1441540800000L;
+    private static TemplateDateModel TM = new SimpleDate(new Date(T), TemplateDateModel.DATETIME);
+    
+    @Test
+    public void testBasicStringBuiltins() throws IOException, TemplateException {
+        assertOutput("${s?upperCase}", "ABC");
+        assertOutput("${n?string?lowerCase}", "1.50e+03");
+        assertErrorContains("${n?lowerCase}", "convert", "string", "markup", "text/html");
+        assertOutput("${dt?string?lowerCase}", "2015-09-06t12:00:00z");
+        assertErrorContains("${dt?lowerCase}", "convert", "string", "markup", "text/html");
+        assertOutput("${b?upperCase}", "Y");
+        assertErrorContains("${m?upperCase}", "convertible to string", "HTMLOutputModel");
+    }
+
+    @Test
+    public void testEscBuiltin() throws IOException, TemplateException {
+        setConfiguration(createDefaultConfigurationBuilder()
+                .outputFormat(HTMLOutputFormat.INSTANCE)
+                .autoEscapingPolicy(ParsingConfiguration.DISABLE_AUTO_ESCAPING_POLICY)
+                .booleanFormat("<y>,<n>")
+                .build());
+        assertOutput("${'a<b'?esc}", "a&lt;b");
+        assertOutput("${n?string?esc}", "1.50E+03");
+        assertOutput("${n?esc}", "1.50*10<sup>3</sup>");
+        assertOutput("${dt?string?esc}", "2015-09-06T12:00:00Z");
+        assertOutput("${dt?esc}", "2015-09-06<span class='T'>T</span>12:00:00Z");
+        assertOutput("${b?esc}", "&lt;y&gt;");
+        assertOutput("${m?esc}", "<p>M</p>");
+    }
+    
+    @Test
+    public void testStringOverloadedBuiltIns() throws IOException, TemplateException {
+        assertOutput("${s?contains('b')}", "y");
+        assertOutput("${n?string?contains('E')}", "y");
+        assertErrorContains("${n?contains('E')}", "convert", "string", "markup", "text/html");
+        assertErrorContains("${n?indexOf('E')}", "convert", "string", "markup", "text/html");
+        assertOutput("${dt?string?contains('0')}", "y");
+        assertErrorContains("${dt?contains('0')}", "convert", "string", "markup", "text/html");
+        assertErrorContains("${m?contains('0')}", "convertible to string", "HTMLOutputModel");
+        assertErrorContains("${m?indexOf('0')}", "convertible to string", "HTMLOutputModel");
+    }
+    
+    @Test
+    public void testMarkupStringBuiltIns() throws IOException, TemplateException {
+        assertErrorContains("${n?string?markupString}", "Expected", "markup", "string");
+        assertErrorContains("${n?markupString}", "Expected", "markup", "number");
+        assertErrorContains("${dt?markupString}", "Expected", "markup", "date");
+    }
+    
+    @Test
+    public void testSimpleInterpolation() throws IOException, TemplateException {
+        assertOutput("${s}", "abc");
+        assertOutput("${n?string}", "1.50E+03");
+        assertOutput("${n}", "1.50*10<sup>3</sup>");
+        assertOutput("${dt?string}", "2015-09-06T12:00:00Z");
+        assertOutput("${dt}", "2015-09-06<span class='T'>T</span>12:00:00Z");
+        assertOutput("${b}", "y");
+        assertOutput("${m}", "<p>M</p>");
+    }
+    
+    @Test
+    public void testConcatenation() throws IOException, TemplateException {
+        assertOutput("${s + '&'}", "abc&");
+        assertOutput("${n?string + '&'}", "1.50E+03&");
+        assertOutput("${n + '&'}", "1.50*10<sup>3</sup>&amp;");
+        assertOutput("${dt?string + '&'}", "2015-09-06T12:00:00Z&");
+        assertOutput("${dt + '&'}", "2015-09-06<span class='T'>T</span>12:00:00Z&amp;");
+        assertOutput("${b + '&'}", "y&");
+        assertOutput("${m + '&'}", "<p>M</p>&amp;");
+    }
+
+    @Test
+    public void testConcatenation2() throws IOException, TemplateException {
+        assertOutput("${'&' + s}", "&abc");
+        assertOutput("${'&' + n?string}", "&1.50E+03");
+        assertOutput("${'&' + n}", "&amp;1.50*10<sup>3</sup>");
+        assertOutput("${'&' + dt?string}", "&2015-09-06T12:00:00Z");
+        assertOutput("${'&' + dt}", "&amp;2015-09-06<span class='T'>T</span>12:00:00Z");
+        assertOutput("${'&' + b}", "&y");
+        assertOutput("${'&' + m}", "&amp;<p>M</p>");
+    }
+
+    @Override
+    protected Configuration createDefaultConfiguration() throws Exception {
+        return createDefaultConfigurationBuilder().build();
+    }
+
+    private TestConfigurationBuilder createDefaultConfigurationBuilder() {
+        return new TestConfigurationBuilder()
+                .customNumberFormats(Collections.<String, TemplateNumberFormatFactory>singletonMap(
+                        "G", PrintfGTemplateNumberFormatFactory.INSTANCE))
+                .customDateFormats(Collections.<String, TemplateDateFormatFactory>singletonMap(
+                        "HI", HTMLISOTemplateDateFormatFactory.INSTANCE))
+                .numberFormat("@G 3")
+                .dateTimeFormat("@HI")
+                .booleanFormat("y,n");
+    }
+
+    @Before
+    public void setup() throws TemplateModelException {
+        addToDataModel("s", "abc");
+        addToDataModel("n", 1500);
+        addToDataModel("dt", TM);
+        addToDataModel("b", Boolean.TRUE);
+        addToDataModel("m", HTMLOutputFormat.INSTANCE.fromMarkup("<p>M</p>"));
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/ConfigurableTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/ConfigurableTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/ConfigurableTest.java
new file mode 100644
index 0000000..ebcc465
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/ConfigurableTest.java
@@ -0,0 +1,176 @@
+/*
+ * 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.Field;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.freemarker.core.util._StringUtil;
+import org.junit.Test;
+
+public class ConfigurableTest {
+
+    @Test
+    public void testGetSettingNamesAreSorted() throws Exception {
+        MutableProcessingConfiguration cfgable = createConfigurable();
+        for (boolean camelCase : new boolean[] { false, true }) {
+            Collection<String> names = cfgable.getSettingNames(camelCase);
+            String prevName = null;
+            for (String name : names) {
+                if (prevName != null) {
+                    assertThat(name, greaterThan(prevName));
+                }
+                prevName = name;
+            }
+        }
+    }
+
+    @Test
+    public void testStaticFieldKeysCoverAllGetSettingNames() throws Exception {
+        MutableProcessingConfiguration cfgable = createConfigurable();
+        Collection<String> names = cfgable.getSettingNames(false);
+        for (String name : names) {
+                assertTrue("No field was found for " + name, keyFieldExists(name));
+        }
+    }
+    
+    @Test
+    public void testGetSettingNamesCoversAllStaticKeyFields() throws Exception {
+        MutableProcessingConfiguration cfgable = createConfigurable();
+        Collection<String> names = cfgable.getSettingNames(false);
+        
+        for (Field f : MutableProcessingConfiguration.class.getFields()) {
+            if (f.getName().endsWith("_KEY")) {
+                final Object name = f.get(null);
+                assertTrue("Missing setting name: " + name, names.contains(name));
+            }
+        }
+    }
+
+    @Test
+    public void testKeyStaticFieldsHasAllVariationsAndCorrectFormat() throws IllegalArgumentException, IllegalAccessException {
+        ConfigurableTest.testKeyStaticFieldsHasAllVariationsAndCorrectFormat(MutableProcessingConfiguration.class);
+    }
+    
+    @Test
+    public void testGetSettingNamesNameConventionsContainTheSame() throws Exception {
+        MutableProcessingConfiguration cfgable = createConfigurable();
+        ConfigurableTest.testGetSettingNamesNameConventionsContainTheSame(
+                new ArrayList<>(cfgable.getSettingNames(false)),
+                new ArrayList<>(cfgable.getSettingNames(true)));
+    }
+
+    public static void testKeyStaticFieldsHasAllVariationsAndCorrectFormat(
+            Class<? extends MutableProcessingConfiguration> confClass) throws IllegalArgumentException, IllegalAccessException {
+        // For all _KEY fields there must be a _KEY_CAMEL_CASE and a _KEY_SNAKE_CASE field.
+        // Their content must not contradict the expected naming convention.
+        // They _KEY filed value must be deducable from the field name
+        // The _KEY value must be the same as _KEY_SNAKE_CASE field.
+        // The _KEY_CAMEL_CASE converted to snake case must give the value of the _KEY_SNAKE_CASE.
+        for (Field field : confClass.getFields()) {
+            String fieldName = field.getName();
+            if (fieldName.endsWith("_KEY")) {
+                String keyFieldValue = (String) field.get(null);
+                assertNotEquals(ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION,
+                        _StringUtil.getIdentifierNamingConvention(keyFieldValue));
+                assertEquals(fieldName.substring(0, fieldName.length() - 4).toLowerCase(), keyFieldValue);
+                
+                try {
+                    String keySCFieldValue = (String) confClass.getField(fieldName + "_SNAKE_CASE").get(null);
+                    assertEquals(keyFieldValue, keySCFieldValue);
+                } catch (NoSuchFieldException e) {
+                    fail("Missing ..._SNAKE_CASE field for " + fieldName);
+                }
+                
+                try {
+                    String keyCCFieldValue = (String) confClass.getField(fieldName + "_CAMEL_CASE").get(null);
+                    assertNotEquals(ParsingConfiguration.LEGACY_NAMING_CONVENTION,
+                            _StringUtil.getIdentifierNamingConvention(keyCCFieldValue));
+                    assertEquals(keyFieldValue, _StringUtil.camelCaseToUnderscored(keyCCFieldValue));
+                } catch (NoSuchFieldException e) {
+                    fail("Missing ..._CAMEL_CASE field for " + fieldName);
+                }
+            }
+        }
+        
+        // For each _KEY_SNAKE_CASE field there must be a _KEY field.
+        for (Field field : confClass.getFields()) {
+            String fieldName = field.getName();
+            if (fieldName.endsWith("_KEY_SNAKE_CASE")) {
+                try {
+                    confClass.getField(fieldName.substring(0, fieldName.length() - 11)).get(null);
+                } catch (NoSuchFieldException e) {
+                    fail("Missing ..._KEY field for " + fieldName);
+                }
+            }
+        }
+        
+        // For each _KEY_CAMEL_CASE field there must be a _KEY field.
+        for (Field field : confClass.getFields()) {
+            String fieldName = field.getName();
+            if (fieldName.endsWith("_KEY_CAMEL_CASE")) {
+                try {
+                    confClass.getField(fieldName.substring(0, fieldName.length() - 11)).get(null);
+                } catch (NoSuchFieldException e) {
+                    fail("Missing ..._KEY field for " + fieldName);
+                }
+            }
+        }
+    }
+    
+    public static void testGetSettingNamesNameConventionsContainTheSame(List<String> namesSCList, List<String> namesCCList) {
+        Set<String> namesSC = new HashSet<>(namesSCList);
+        assertEquals(namesSCList.size(), namesSC.size());
+        
+        Set<String> namesCC = new HashSet<>(namesCCList);
+        assertEquals(namesCCList.size(), namesCC.size());
+
+        assertEquals(namesSC.size(), namesCC.size());
+        
+        for (String nameCC : namesCC) {
+            final String nameSC = _StringUtil.camelCaseToUnderscored(nameCC);
+            if (!namesSC.contains(nameSC)) {
+                fail("\"" + nameCC + "\" misses corresponding snake case name, \"" + nameSC + "\".");
+            }
+        }
+    }
+    
+    private MutableProcessingConfiguration createConfigurable() throws IOException {
+        return new TemplateConfiguration.Builder();
+    }
+
+    private boolean keyFieldExists(String name) throws Exception {
+        try {
+            MutableProcessingConfiguration.class.getField(name.toUpperCase() + "_KEY");
+        } catch (NoSuchFieldException e) {
+            return false;
+        }
+        return true;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/ConfigurationTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/ConfigurationTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/ConfigurationTest.java
new file mode 100644
index 0000000..dcefa3f
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/ConfigurationTest.java
@@ -0,0 +1,1486 @@
+/*
+ * 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.IOException;
+import java.io.Serializable;
+import java.io.StringWriter;
+import java.lang.reflect.Field;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TimeZone;
+
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.impl.DefaultObjectWrapper;
+import org.apache.freemarker.core.model.impl.RestrictedObjectWrapper;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.apache.freemarker.core.outputformat.MarkupOutputFormat;
+import org.apache.freemarker.core.outputformat.OutputFormat;
+import org.apache.freemarker.core.outputformat.UnregisteredOutputFormatException;
+import org.apache.freemarker.core.outputformat.impl.CombinedMarkupOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.HTMLOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.RTFOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.UndefinedOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.XMLOutputFormat;
+import org.apache.freemarker.core.templateresolver.CacheStorageWithGetSize;
+import org.apache.freemarker.core.templateresolver.ConditionalTemplateConfigurationFactory;
+import org.apache.freemarker.core.templateresolver.FileNameGlobMatcher;
+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.ByteArrayTemplateLoader;
+import org.apache.freemarker.core.templateresolver.impl.ClassTemplateLoader;
+import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateLookupStrategy;
+import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateNameFormat;
+import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateNameFormatFM2;
+import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateResolver;
+import org.apache.freemarker.core.templateresolver.impl.NullCacheStorage;
+import org.apache.freemarker.core.templateresolver.impl.SoftCacheStorage;
+import org.apache.freemarker.core.templateresolver.impl.StringTemplateLoader;
+import org.apache.freemarker.core.templateresolver.impl.StrongCacheStorage;
+import org.apache.freemarker.core.userpkg.BaseNTemplateNumberFormatFactory;
+import org.apache.freemarker.core.userpkg.CustomHTMLOutputFormat;
+import org.apache.freemarker.core.userpkg.DummyOutputFormat;
+import org.apache.freemarker.core.userpkg.EpochMillisDivTemplateDateFormatFactory;
+import org.apache.freemarker.core.userpkg.EpochMillisTemplateDateFormatFactory;
+import org.apache.freemarker.core.userpkg.HexTemplateNumberFormatFactory;
+import org.apache.freemarker.core.util._DateUtil;
+import org.apache.freemarker.core.util._NullArgumentException;
+import org.apache.freemarker.core.util._NullWriter;
+import org.apache.freemarker.core.util._StringUtil;
+import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import junit.framework.TestCase;
+
+public class ConfigurationTest extends TestCase {
+
+    private static final Charset ISO_8859_2 = Charset.forName("ISO-8859-2");
+
+    public ConfigurationTest(String name) {
+        super(name);
+    }
+
+    public void testUnsetAndIsSet() throws Exception {
+        Configuration.ExtendableBuilder<?> cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0);
+        
+        assertFalse(cfgB.isLogTemplateExceptionsSet());
+        assertFalse(cfgB.getLogTemplateExceptions());
+        //
+        cfgB.setLogTemplateExceptions(true);
+        {
+            Configuration cfg = cfgB.build();
+            assertTrue(cfgB.isLogTemplateExceptionsSet());
+            assertTrue(cfg.isLogTemplateExceptionsSet());
+            assertTrue(cfgB.getLogTemplateExceptions());
+            assertTrue(cfg.getLogTemplateExceptions());
+        }
+        //
+        for (int i = 0; i < 2; i++) {
+            cfgB.unsetLogTemplateExceptions();
+            Configuration cfg = cfgB.build();
+            assertFalse(cfgB.isLogTemplateExceptionsSet());
+            assertTrue(cfg.isLogTemplateExceptionsSet());
+            assertFalse(cfgB.getLogTemplateExceptions());
+            assertFalse(cfg.getLogTemplateExceptions());
+        }
+
+        DefaultObjectWrapper dow = new DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
+        assertFalse(cfgB.isObjectWrapperSet());
+        assertSame(dow, cfgB.getObjectWrapper());
+        //
+        RestrictedObjectWrapper ow = new RestrictedObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
+        cfgB.setObjectWrapper(ow);
+        assertTrue(cfgB.isObjectWrapperSet());
+        assertSame(ow, cfgB.getObjectWrapper());
+        //
+        for (int i = 0; i < 2; i++) {
+            cfgB.unsetObjectWrapper();
+            assertFalse(cfgB.isObjectWrapperSet());
+            assertSame(dow, cfgB.getObjectWrapper());
+        }
+        
+        assertFalse(cfgB.isTemplateExceptionHandlerSet());
+        assertSame(TemplateExceptionHandler.DEBUG_HANDLER, cfgB.getTemplateExceptionHandler());
+        //
+        cfgB.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
+        assertTrue(cfgB.isTemplateExceptionHandlerSet());
+        assertSame(TemplateExceptionHandler.RETHROW_HANDLER, cfgB.getTemplateExceptionHandler());
+        //
+        for (int i = 0; i < 2; i++) {
+            cfgB.unsetTemplateExceptionHandler();
+            assertFalse(cfgB.isTemplateExceptionHandlerSet());
+            assertSame(TemplateExceptionHandler.DEBUG_HANDLER, cfgB.getTemplateExceptionHandler());
+        }
+        
+        assertFalse(cfgB.isTemplateLoaderSet());
+        assertNull(cfgB.getTemplateLoader());
+        //
+        cfgB.setTemplateLoader(null);
+        assertTrue(cfgB.isTemplateLoaderSet());
+        assertNull(cfgB.getTemplateLoader());
+        //
+        for (int i = 0; i < 3; i++) {
+            if (i == 2) {
+                cfgB.setTemplateLoader(new StringTemplateLoader());
+            }
+            cfgB.unsetTemplateLoader();
+            assertFalse(cfgB.isTemplateLoaderSet());
+            assertNull(cfgB.getTemplateLoader());
+        }
+        
+        assertFalse(cfgB.isTemplateLookupStrategySet());
+        assertSame(DefaultTemplateLookupStrategy.INSTANCE, cfgB.getTemplateLookupStrategy());
+        //
+        cfgB.setTemplateLookupStrategy(DefaultTemplateLookupStrategy.INSTANCE);
+        assertTrue(cfgB.isTemplateLookupStrategySet());
+        //
+        for (int i = 0; i < 2; i++) {
+            cfgB.unsetTemplateLookupStrategy();
+            assertFalse(cfgB.isTemplateLookupStrategySet());
+        }
+        
+        assertFalse(cfgB.isTemplateNameFormatSet());
+        assertSame(DefaultTemplateNameFormatFM2.INSTANCE, cfgB.getTemplateNameFormat());
+        //
+        cfgB.setTemplateNameFormat(DefaultTemplateNameFormat.INSTANCE);
+        assertTrue(cfgB.isTemplateNameFormatSet());
+        assertSame(DefaultTemplateNameFormat.INSTANCE, cfgB.getTemplateNameFormat());
+        //
+        for (int i = 0; i < 2; i++) {
+            cfgB.unsetTemplateNameFormat();
+            assertFalse(cfgB.isTemplateNameFormatSet());
+            assertSame(DefaultTemplateNameFormatFM2.INSTANCE, cfgB.getTemplateNameFormat());
+        }
+        
+        assertFalse(cfgB.isCacheStorageSet());
+        assertTrue(cfgB.getCacheStorage() instanceof SoftCacheStorage);
+        //
+        cfgB.setCacheStorage(NullCacheStorage.INSTANCE);
+        assertTrue(cfgB.isCacheStorageSet());
+        assertSame(NullCacheStorage.INSTANCE, cfgB.getCacheStorage());
+        //
+        for (int i = 0; i < 3; i++) {
+            if (i == 2) {
+                cfgB.setCacheStorage(cfgB.getCacheStorage());
+            }
+            cfgB.unsetCacheStorage();
+            assertFalse(cfgB.isCacheStorageSet());
+            assertTrue(cfgB.getCacheStorage() instanceof SoftCacheStorage);
+        }
+    }
+    
+    public void testTemplateLoadingErrors() throws Exception {
+        Configuration cfg = new Configuration.Builder(Configuration.VERSION_3_0_0)
+                .templateLoader(new ClassTemplateLoader(getClass(), "nosuchpackage"))
+                .build();
+        try {
+            cfg.getTemplate("missing.ftl");
+            fail();
+        } catch (TemplateNotFoundException e) {
+            assertThat(e.getMessage(), not(containsString("wasn't set")));
+        }
+    }
+    
+    public void testVersion() {
+        Version v = Configuration.getVersion();
+        assertTrue(v.intValue() >= _CoreAPI.VERSION_INT_3_0_0);
+        
+        try {
+            new Configuration.Builder(new Version(999, 1, 2));
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), containsString("upgrade"));
+        }
+        
+        try {
+            new Configuration.Builder(new Version(2, 3, 0));
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), containsString("3.0.0"));
+        }
+    }
+    
+    public void testShowErrorTips() throws Exception {
+        try {
+            Configuration cfg = new Configuration.Builder(Configuration.VERSION_3_0_0).build();
+            new Template(null, "${x}", cfg).process(null, _NullWriter.INSTANCE);
+            fail();
+        } catch (TemplateException e) {
+            assertThat(e.getMessage(), containsString("Tip:"));
+        }
+
+        try {
+            Configuration cfg = new Configuration.Builder(Configuration.VERSION_3_0_0).showErrorTips(false).build();
+            new Template(null, "${x}", cfg).process(null, _NullWriter.INSTANCE);
+            fail();
+        } catch (TemplateException e) {
+            assertThat(e.getMessage(), not(containsString("Tip:")));
+        }
+    }
+    
+    @Test
+    @SuppressWarnings("boxing")
+    public void testGetTemplateOverloads() throws Exception {
+        final Locale hu = new Locale("hu", "HU");
+        final String tFtl = "t.ftl";
+        final String tHuFtl = "t_hu.ftl";
+        final String tEnFtl = "t_en.ftl";
+        final String tUtf8Ftl = "utf8.ftl";
+        final Serializable custLookupCond = 12345;
+
+        ByteArrayTemplateLoader tl = new ByteArrayTemplateLoader();
+        tl.putTemplate(tFtl, "${1}".getBytes(StandardCharsets.UTF_8));
+        tl.putTemplate(tEnFtl, "${1}".getBytes(StandardCharsets.UTF_8));
+        tl.putTemplate(tHuFtl, "${1}".getBytes(StandardCharsets.UTF_8));
+        tl.putTemplate(tUtf8Ftl, "<#ftl encoding='utf-8'>".getBytes(StandardCharsets.UTF_8));
+
+        Configuration cfg = new Configuration.Builder(Configuration.VERSION_3_0_0)
+                .locale(Locale.GERMAN)
+                .sourceEncoding(StandardCharsets.ISO_8859_1)
+                .templateLoader(tl)
+                .templateConfigurations(
+                        new ConditionalTemplateConfigurationFactory(
+                                new FileNameGlobMatcher("*_hu.*"),
+                                new TemplateConfiguration.Builder().sourceEncoding(ISO_8859_2).build()))
+                .build();
+
+        // 1 args:
+        {
+            Template t = cfg.getTemplate(tFtl);
+            assertEquals(tFtl, t.getLookupName());
+            assertEquals(tFtl, t.getSourceName());
+            assertEquals(Locale.GERMAN, t.getLocale());
+            assertNull(t.getCustomLookupCondition());
+            assertEquals(StandardCharsets.ISO_8859_1, t.getActualSourceEncoding());
+        }
+        {
+            Template t = cfg.getTemplate(tUtf8Ftl);
+            assertEquals(tUtf8Ftl, t.getLookupName());
+            assertEquals(tUtf8Ftl, t.getSourceName());
+            assertEquals(Locale.GERMAN, t.getLocale());
+            assertNull(t.getCustomLookupCondition());
+            assertEquals(StandardCharsets.UTF_8, t.getActualSourceEncoding());
+        }
+        
+        // 2 args:
+        {
+            Template t = cfg.getTemplate(tFtl, Locale.GERMAN);
+            assertEquals(tFtl, t.getLookupName());
+            assertEquals(tFtl, t.getSourceName());
+            assertEquals(Locale.GERMAN, t.getLocale());
+            assertNull(t.getCustomLookupCondition());
+            assertEquals(StandardCharsets.ISO_8859_1, t.getActualSourceEncoding());
+        }
+        {
+            Template t = cfg.getTemplate(tFtl, (Locale) null);
+            assertEquals(tFtl, t.getLookupName());
+            assertEquals(tFtl, t.getSourceName());
+            assertEquals(Locale.GERMAN, t.getLocale());
+            assertNull(t.getCustomLookupCondition());
+            assertEquals(StandardCharsets.ISO_8859_1, t.getActualSourceEncoding());
+        }
+        {
+            Template t = cfg.getTemplate(tFtl, Locale.US);
+            assertEquals(tFtl, t.getLookupName());
+            assertEquals(tEnFtl, t.getSourceName());
+            assertEquals(Locale.US, t.getLocale());
+            assertNull(t.getCustomLookupCondition());
+            assertEquals(StandardCharsets.ISO_8859_1, t.getActualSourceEncoding());
+        }
+        {
+            Template t = cfg.getTemplate(tUtf8Ftl, Locale.US);
+            assertEquals(tUtf8Ftl, t.getLookupName());
+            assertEquals(tUtf8Ftl, t.getSourceName());
+            assertEquals(Locale.US, t.getLocale());
+            assertNull(t.getCustomLookupCondition());
+            assertEquals(StandardCharsets.UTF_8, t.getActualSourceEncoding());
+        }
+        {
+            Template t = cfg.getTemplate(tFtl, hu);
+            assertEquals(tFtl, t.getLookupName());
+            assertEquals(tHuFtl, t.getSourceName());
+            assertEquals(hu, t.getLocale());
+            assertNull(t.getCustomLookupCondition());
+            assertEquals(ISO_8859_2, t.getActualSourceEncoding());
+        }
+        {
+            Template t = cfg.getTemplate(tUtf8Ftl, hu);
+            assertEquals(tUtf8Ftl, t.getLookupName());
+            assertEquals(tUtf8Ftl, t.getSourceName());
+            assertEquals(hu, t.getLocale());
+            assertNull(t.getCustomLookupCondition());
+            assertEquals(StandardCharsets.UTF_8, t.getActualSourceEncoding());
+        }
+
+        // 4 args:
+        try {
+            cfg.getTemplate("missing.ftl", hu, custLookupCond, false);
+            fail();
+        } catch (TemplateNotFoundException e) {
+            // Expected
+        }
+        assertNull(cfg.getTemplate("missing.ftl", hu, custLookupCond, true));
+        {
+            Template t = cfg.getTemplate(tFtl, hu, custLookupCond, false);
+            assertEquals(tFtl, t.getLookupName());
+            assertEquals(tHuFtl, t.getSourceName());
+            assertEquals(hu, t.getLocale());
+            assertEquals(custLookupCond, t.getCustomLookupCondition());
+            assertEquals(ISO_8859_2, t.getActualSourceEncoding());
+            assertOutputEquals("1", t);
+        }
+        {
+            Template t = cfg.getTemplate(tFtl, null, custLookupCond, false);
+            assertEquals(tFtl, t.getLookupName());
+            assertEquals(tFtl, t.getSourceName());
+            assertEquals(Locale.GERMAN, t.getLocale());
+            assertEquals(custLookupCond, t.getCustomLookupCondition());
+            assertEquals(StandardCharsets.ISO_8859_1, t.getActualSourceEncoding());
+            assertOutputEquals("1", t);
+        }
+    }
+
+    private void assertOutputEquals(final String expectedContent, final Template t) throws ConfigurationException,
+            IOException, TemplateException {
+        StringWriter sw = new StringWriter();
+        t.process(null, sw);
+        assertEquals(expectedContent, sw.toString());
+    }
+    
+    public void testTemplateResolverCache() throws Exception {
+        Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0);
+        
+        CacheStorageWithGetSize cache = (CacheStorageWithGetSize) cfgB.getCacheStorage();
+        assertEquals(0, cache.getSize());
+        cfgB.setCacheStorage(new StrongCacheStorage());
+        cache = (CacheStorageWithGetSize) cfgB.getCacheStorage();
+        assertEquals(0, cache.getSize());
+        cfgB.setTemplateLoader(new ClassTemplateLoader(ConfigurationTest.class, ""));
+        Configuration cfg = cfgB.build();
+        assertEquals(0, cache.getSize());
+        cfg.getTemplate("toCache1.ftl");
+        assertEquals(1, cache.getSize());
+        cfg.getTemplate("toCache2.ftl");
+        assertEquals(2, cache.getSize());
+        cfg.clearTemplateCache();
+        assertEquals(0, cache.getSize());
+        cfg.getTemplate("toCache1.ftl");
+        assertEquals(1, cache.getSize());
+        cfgB.setTemplateLoader(cfgB.getTemplateLoader());
+        assertEquals(1, cache.getSize());
+    }
+
+    public void testTemplateNameFormat() throws Exception {
+        StringTemplateLoader tl = new StringTemplateLoader();
+        tl.putTemplate("a/b.ftl", "In a/b.ftl");
+        tl.putTemplate("b.ftl", "In b.ftl");
+
+        Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0)
+                .templateLoader(tl);
+
+        {
+            cfgB.setTemplateNameFormat(DefaultTemplateNameFormatFM2.INSTANCE);
+            final Template template = cfgB.build().getTemplate("a/./../b.ftl");
+            assertEquals("a/b.ftl", template.getLookupName());
+            assertEquals("a/b.ftl", template.getSourceName());
+            assertEquals("In a/b.ftl", template.toString());
+        }
+        
+        {
+            cfgB.setTemplateNameFormat(DefaultTemplateNameFormat.INSTANCE);
+            final Template template = cfgB.build().getTemplate("a/./../b.ftl");
+            assertEquals("b.ftl", template.getLookupName());
+            assertEquals("b.ftl", template.getSourceName());
+            assertEquals("In b.ftl", template.toString());
+        }
+    }
+
+    public void testTemplateNameFormatSetSetting() throws Exception {
+        Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0);
+        assertSame(DefaultTemplateNameFormatFM2.INSTANCE, cfgB.getTemplateNameFormat());
+        cfgB.setSetting(Configuration.ExtendableBuilder.TEMPLATE_NAME_FORMAT_KEY, "defAult_2_4_0");
+        assertSame(DefaultTemplateNameFormat.INSTANCE, cfgB.getTemplateNameFormat());
+        cfgB.setSetting(Configuration.ExtendableBuilder.TEMPLATE_NAME_FORMAT_KEY, "defaUlt_2_3_0");
+        assertSame(DefaultTemplateNameFormatFM2.INSTANCE, cfgB.getTemplateNameFormat());
+        assertTrue(cfgB.isTemplateNameFormatSet());
+        cfgB.setSetting(Configuration.ExtendableBuilder.TEMPLATE_NAME_FORMAT_KEY, "defauLt");
+        assertFalse(cfgB.isTemplateNameFormatSet());
+    }
+
+    public void testObjectWrapperSetSetting() throws Exception {
+        Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0);
+        {
+            cfgB.setSetting(MutableProcessingConfiguration.OBJECT_WRAPPER_KEY, "defAult");
+            DefaultObjectWrapper dow = new DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
+            assertSame(dow, cfgB.getObjectWrapper());
+            assertEquals(Configuration.VERSION_3_0_0, dow.getIncompatibleImprovements());
+        }
+        
+        {
+            cfgB.setSetting(MutableProcessingConfiguration.OBJECT_WRAPPER_KEY, "restricted");
+            assertThat(cfgB.getObjectWrapper(), instanceOf(RestrictedObjectWrapper.class));
+        }
+    }
+    
+    public void testTemplateLookupStrategyDefaultAndSet() throws Exception {
+        Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0);
+        assertSame(DefaultTemplateLookupStrategy.INSTANCE, cfgB.getTemplateLookupStrategy());
+        assertSame(DefaultTemplateLookupStrategy.INSTANCE, cfgB.build().getTemplateLookupStrategy());
+
+        cfgB.setTemplateLoader(new ClassTemplateLoader(ConfigurationTest.class, ""));
+        assertSame(DefaultTemplateLookupStrategy.INSTANCE, cfgB.getTemplateLookupStrategy());
+        Configuration cfg = cfgB.build();
+        assertSame(DefaultTemplateLookupStrategy.INSTANCE, cfg.getTemplateLookupStrategy());
+        cfg.getTemplate("toCache1.ftl");
+
+        final TemplateLookupStrategy myStrategy = new TemplateLookupStrategy() {
+            @Override
+            public TemplateLookupResult lookup(TemplateLookupContext ctx) throws IOException {
+                return ctx.lookupWithAcquisitionStrategy("toCache2.ftl");
+            }
+        };
+        cfgB.setTemplateLookupStrategy(myStrategy);
+        assertSame(myStrategy, cfgB.getTemplateLookupStrategy());
+        cfg = cfgB.build();
+        cfg.clearTemplateCache();
+        assertSame(myStrategy, cfg.getTemplateLookupStrategy());
+        Template template = cfg.getTemplate("toCache1.ftl");
+        assertEquals("toCache2.ftl", template.getSourceName());
+    }
+    
+    public void testSetTemplateConfigurations() throws Exception {
+        Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0);
+        assertNull(cfgB.getTemplateConfigurations());
+
+        StringTemplateLoader tl = new StringTemplateLoader();
+        tl.putTemplate("t.de.ftlh", "");
+        tl.putTemplate("t.fr.ftlx", "");
+        tl.putTemplate("t.ftlx", "");
+        tl.putTemplate("Stat/t.de.ftlx", "");
+        cfgB.setTemplateLoader(tl);
+        
+        cfgB.setTimeZone(TimeZone.getTimeZone("GMT+09"));
+        
+        cfgB.setSetting(Configuration.ExtendableBuilder.TEMPLATE_CONFIGURATIONS_KEY,
+                "MergingTemplateConfigurationFactory("
+                    + "FirstMatchTemplateConfigurationFactory("
+                        + "ConditionalTemplateConfigurationFactory("
+                            + "FileNameGlobMatcher('*.de.*'), TemplateConfiguration(timeZone=TimeZone('GMT+01'))), "
+                        + "ConditionalTemplateConfigurationFactory("
+                            + "FileNameGlobMatcher('*.fr.*'), TemplateConfiguration(timeZone=TimeZone('GMT'))), "
+                        + "allowNoMatch=true"
+                    + "), "
+                    + "FirstMatchTemplateConfigurationFactory("
+                        + "ConditionalTemplateConfigurationFactory("
+                            + "FileExtensionMatcher('ftlh'), TemplateConfiguration(booleanFormat='TODO,HTML')), "
+                        + "ConditionalTemplateConfigurationFactory("
+                            + "FileExtensionMatcher('ftlx'), TemplateConfiguration(booleanFormat='TODO,XML')), "
+                        + "noMatchErrorDetails='Unrecognized template file extension'"
+                    + "), "
+                    + "ConditionalTemplateConfigurationFactory("
+                        + "PathGlobMatcher('stat/**', caseInsensitive=true), "
+                        + "TemplateConfiguration(timeZone=TimeZone('UTC'))"
+                    + ")"
+                + ")");
+
+        Configuration cfg = cfgB.build();
+        {
+            Template t = cfg.getTemplate("t.de.ftlh");
+            assertEquals("TODO,HTML", t.getBooleanFormat());
+            assertEquals(TimeZone.getTimeZone("GMT+01"), t.getTimeZone());
+        }
+        {
+            Template t = cfg.getTemplate("t.fr.ftlx");
+            assertEquals("TODO,XML", t.getBooleanFormat());
+            assertEquals(TimeZone.getTimeZone("GMT"), t.getTimeZone());
+        }
+        {
+            Template t = cfg.getTemplate("t.ftlx");
+            assertEquals("TODO,XML", t.getBooleanFormat());
+            assertEquals(TimeZone.getTimeZone("GMT+09"), t.getTimeZone());
+        }
+        {
+            Template t = cfg.getTemplate("Stat/t.de.ftlx");
+            assertEquals("TODO,XML", t.getBooleanFormat());
+            assertEquals(_DateUtil.UTC, t.getTimeZone());
+        }
+        
+        assertNotNull(cfgB.getTemplateConfigurations());
+        cfgB.setSetting(Configuration.ExtendableBuilder.TEMPLATE_CONFIGURATIONS_KEY, "null");
+        assertNull(cfgB.getTemplateConfigurations());
+    }
+
+    public void testSetAutoEscaping() throws Exception {
+       Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0);
+    
+       assertEquals(ParsingConfiguration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY, cfgB.getAutoEscapingPolicy());
+
+       cfgB.setAutoEscapingPolicy(ParsingConfiguration.ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY);
+       assertEquals(ParsingConfiguration.ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY, cfgB.getAutoEscapingPolicy());
+
+       cfgB.setAutoEscapingPolicy(ParsingConfiguration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY);
+       assertEquals(ParsingConfiguration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY, cfgB.getAutoEscapingPolicy());
+
+       cfgB.setAutoEscapingPolicy(ParsingConfiguration.DISABLE_AUTO_ESCAPING_POLICY);
+       assertEquals(ParsingConfiguration.DISABLE_AUTO_ESCAPING_POLICY, cfgB.getAutoEscapingPolicy());
+       
+       cfgB.setSetting(Configuration.ExtendableBuilder.AUTO_ESCAPING_POLICY_KEY_CAMEL_CASE, "enableIfSupported");
+       assertEquals(ParsingConfiguration.ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY, cfgB.getAutoEscapingPolicy());
+
+       cfgB.setSetting(Configuration.ExtendableBuilder.AUTO_ESCAPING_POLICY_KEY_CAMEL_CASE, "enable_if_supported");
+       assertEquals(ParsingConfiguration.ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY, cfgB.getAutoEscapingPolicy());
+       
+       cfgB.setSetting(Configuration.ExtendableBuilder.AUTO_ESCAPING_POLICY_KEY_CAMEL_CASE, "enableIfDefault");
+       assertEquals(ParsingConfiguration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY, cfgB.getAutoEscapingPolicy());
+
+       cfgB.setSetting(Configuration.ExtendableBuilder.AUTO_ESCAPING_POLICY_KEY_CAMEL_CASE, "enable_if_default");
+       assertEquals(ParsingConfiguration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY, cfgB.getAutoEscapingPolicy());
+       
+       cfgB.setSetting(Configuration.ExtendableBuilder.AUTO_ESCAPING_POLICY_KEY_CAMEL_CASE, "disable");
+       assertEquals(ParsingConfiguration.DISABLE_AUTO_ESCAPING_POLICY, cfgB.getAutoEscapingPolicy());
+       
+       try {
+           cfgB.setAutoEscapingPolicy(ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION);
+           fail();
+       } catch (IllegalArgumentException e) {
+           // Expected
+       }
+    }
+
+    public void testSetOutputFormat() throws Exception {
+       Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0);
+       
+       assertEquals(UndefinedOutputFormat.INSTANCE, cfgB.getOutputFormat());
+       assertFalse(cfgB.isOutputFormatSet());
+       
+       try {
+           cfgB.setOutputFormat(null);
+           fail();
+       } catch (_NullArgumentException e) {
+           // Expected
+       }
+       
+       assertFalse(cfgB.isOutputFormatSet());
+       
+       cfgB.setSetting(Configuration.ExtendableBuilder.OUTPUT_FORMAT_KEY_CAMEL_CASE, XMLOutputFormat.class.getSimpleName());
+       assertEquals(XMLOutputFormat.INSTANCE, cfgB.getOutputFormat());
+       
+       cfgB.setSetting(Configuration.ExtendableBuilder.OUTPUT_FORMAT_KEY_SNAKE_CASE, HTMLOutputFormat.class.getSimpleName());
+       assertEquals(HTMLOutputFormat.INSTANCE, cfgB.getOutputFormat());
+       
+       cfgB.unsetOutputFormat();
+       assertEquals(UndefinedOutputFormat.INSTANCE, cfgB.getOutputFormat());
+       assertFalse(cfgB.isOutputFormatSet());
+       
+       cfgB.setOutputFormat(UndefinedOutputFormat.INSTANCE);
+       assertTrue(cfgB.isOutputFormatSet());
+       cfgB.setSetting(Configuration.ExtendableBuilder.OUTPUT_FORMAT_KEY_CAMEL_CASE, "default");
+       assertFalse(cfgB.isOutputFormatSet());
+       
+       try {
+           cfgB.setSetting(Configuration.ExtendableBuilder.OUTPUT_FORMAT_KEY, "null");
+       } catch (ConfigurationSettingValueException e) {
+           assertThat(e.getCause().getMessage(), containsString(UndefinedOutputFormat.class.getSimpleName()));
+       }
+    }
+    
+    @Test
+    public void testGetOutputFormatByName() throws Exception {
+        Configuration cfg = new Configuration.Builder(Configuration.VERSION_3_0_0).build();
+        
+        assertSame(HTMLOutputFormat.INSTANCE, cfg.getOutputFormat(HTMLOutputFormat.INSTANCE.getName()));
+        
+        try {
+            cfg.getOutputFormat("noSuchFormat");
+            fail();
+        } catch (UnregisteredOutputFormatException e) {
+            assertThat(e.getMessage(), containsString("noSuchFormat"));
+        }
+        
+        try {
+            cfg.getOutputFormat("HTML}");
+            fail();
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), containsString("'{'"));
+        }
+        
+        {
+            OutputFormat of = cfg.getOutputFormat("HTML{RTF}");
+            assertThat(of, instanceOf(CombinedMarkupOutputFormat.class));
+            CombinedMarkupOutputFormat combinedOF = (CombinedMarkupOutputFormat) of;
+            assertSame(HTMLOutputFormat.INSTANCE, combinedOF.getOuterOutputFormat());
+            assertSame(RTFOutputFormat.INSTANCE, combinedOF.getInnerOutputFormat());
+        }
+
+        {
+            OutputFormat of = cfg.getOutputFormat("XML{HTML{RTF}}");
+            assertThat(of, instanceOf(CombinedMarkupOutputFormat.class));
+            CombinedMarkupOutputFormat combinedOF = (CombinedMarkupOutputFormat) of;
+            assertSame(XMLOutputFormat.INSTANCE, combinedOF.getOuterOutputFormat());
+            MarkupOutputFormat innerOF = combinedOF.getInnerOutputFormat();
+            assertThat(innerOF, instanceOf(CombinedMarkupOutputFormat.class));
+            CombinedMarkupOutputFormat innerCombinedOF = (CombinedMarkupOutputFormat) innerOF; 
+            assertSame(HTMLOutputFormat.INSTANCE, innerCombinedOF.getOuterOutputFormat());
+            assertSame(RTFOutputFormat.INSTANCE, innerCombinedOF.getInnerOutputFormat());
+        }
+        
+        try {
+            cfg.getOutputFormat("plainText{HTML}");
+            fail();
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), allOf(containsString("plainText"), containsString("markup")));
+        }
+        try {
+            cfg.getOutputFormat("HTML{plainText}");
+            fail();
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), allOf(containsString("plainText"), containsString("markup")));
+        }
+    }
+
+    public void testSetRegisteredCustomOutputFormats() throws Exception {
+        Configuration.Builder cfg = new Configuration.Builder(Configuration.VERSION_3_0_0);
+        
+        assertTrue(cfg.getRegisteredCustomOutputFormats().isEmpty());
+        
+        cfg.setSetting(Configuration.ExtendableBuilder.REGISTERED_CUSTOM_OUTPUT_FORMATS_KEY_CAMEL_CASE,
+                "[org.apache.freemarker.core.userpkg.CustomHTMLOutputFormat(), "
+                + "org.apache.freemarker.core.userpkg.DummyOutputFormat()]");
+        assertEquals(
+                ImmutableList.of(CustomHTMLOutputFormat.INSTANCE, DummyOutputFormat.INSTANCE),
+                new ArrayList(cfg.getRegisteredCustomOutputFormats()));
+        
+        try {
+            cfg.setSetting(Configuration.ExtendableBuilder.REGISTERED_CUSTOM_OUTPUT_FORMATS_KEY_SNAKE_CASE, "[TemplateConfiguration()]");
+            fail();
+        } catch (ConfigurationSettingValueException e) {
+            assertThat(e.getMessage(), containsString(OutputFormat.class.getSimpleName()));
+        }
+    }
+
+    public void testSetRecognizeStandardFileExtensions() throws Exception {
+        Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0);
+     
+        assertTrue(cfgB.getRecognizeStandardFileExtensions());
+        assertFalse(cfgB.isRecognizeStandardFileExtensionsSet());
+
+        cfgB.setRecognizeStandardFileExtensions(false);
+        assertFalse(cfgB.getRecognizeStandardFileExtensions());
+        assertTrue(cfgB.isRecognizeStandardFileExtensionsSet());
+     
+        cfgB.unsetRecognizeStandardFileExtensions();
+        assertTrue(cfgB.getRecognizeStandardFileExtensions());
+        assertFalse(cfgB.isRecognizeStandardFileExtensionsSet());
+        
+        cfgB.setRecognizeStandardFileExtensions(true);
+        assertTrue(cfgB.getRecognizeStandardFileExtensions());
+        assertTrue(cfgB.isRecognizeStandardFileExtensionsSet());
+     
+        cfgB.setSetting(Configuration.ExtendableBuilder.RECOGNIZE_STANDARD_FILE_EXTENSIONS_KEY_CAMEL_CASE, "false");
+        assertFalse(cfgB.getRecognizeStandardFileExtensions());
+        assertTrue(cfgB.isRecognizeStandardFileExtensionsSet());
+        
+        cfgB.setSetting(Configuration.ExtendableBuilder.RECOGNIZE_STANDARD_FILE_EXTENSIONS_KEY_SNAKE_CASE, "default");
+        assertTrue(cfgB.getRecognizeStandardFileExtensions());
+        assertFalse(cfgB.isRecognizeStandardFileExtensionsSet());
+     }
+    
+    public void testSetTimeZone() throws ConfigurationException {
+        TimeZone origSysDefTZ = TimeZone.getDefault();
+        try {
+            TimeZone sysDefTZ = TimeZone.getTimeZone("GMT-01");
+            TimeZone.setDefault(sysDefTZ);
+            
+            Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0);
+            assertEquals(sysDefTZ, cfgB.getTimeZone());
+            cfgB.setSetting(MutableProcessingConfiguration.TIME_ZONE_KEY, "JVM default");
+            assertEquals(sysDefTZ, cfgB.getTimeZone());
+            
+            TimeZone newSysDefTZ = TimeZone.getTimeZone("GMT+09");
+            TimeZone.setDefault(newSysDefTZ);
+            assertEquals(sysDefTZ, cfgB.getTimeZone());
+            cfgB.setSetting(MutableProcessingConfiguration.TIME_ZONE_KEY, "JVM default");
+            assertEquals(newSysDefTZ, cfgB.getTimeZone());
+        } finally {
+            TimeZone.setDefault(origSysDefTZ);
+        }
+    }
+    
+    public void testSetSQLDateAndTimeTimeZone() throws ConfigurationException {
+        TimeZone origSysDefTZ = TimeZone.getDefault();
+        try {
+            TimeZone sysDefTZ = TimeZone.getTimeZone("GMT-01");
+            TimeZone.setDefault(sysDefTZ);
+            
+            Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0);
+            assertNull(cfgB.getSQLDateAndTimeTimeZone());
+            
+            cfgB.setSQLDateAndTimeTimeZone(null);
+            assertNull(cfgB.getSQLDateAndTimeTimeZone());
+            
+            cfgB.setSetting(MutableProcessingConfiguration.SQL_DATE_AND_TIME_TIME_ZONE_KEY, "JVM default");
+            assertEquals(sysDefTZ, cfgB.getSQLDateAndTimeTimeZone());
+            
+            cfgB.setSetting(MutableProcessingConfiguration.SQL_DATE_AND_TIME_TIME_ZONE_KEY, "null");
+            assertNull(cfgB.getSQLDateAndTimeTimeZone());
+        } finally {
+            TimeZone.setDefault(origSysDefTZ);
+        }
+    }
+
+    public void testTimeZoneLayers() throws Exception {
+        TimeZone localTZ = TimeZone.getTimeZone("Europe/Brussels");
+
+        {
+            Configuration cfg = new Configuration.Builder(Configuration.VERSION_3_0_0).build();
+            Template t = new Template(null, "", cfg);
+            Environment env1 = t.createProcessingEnvironment(null, new StringWriter());
+            Environment env2 = t.createProcessingEnvironment(null, new StringWriter());
+
+            // cfg:
+            assertEquals(TimeZone.getDefault(), cfg.getTimeZone());
+            assertNull(cfg.getSQLDateAndTimeTimeZone());
+            // env:
+            assertEquals(TimeZone.getDefault(), env1.getTimeZone());
+            assertNull(env1.getSQLDateAndTimeTimeZone());
+            // env 2:
+            assertEquals(TimeZone.getDefault(), env2.getTimeZone());
+            assertNull(env2.getSQLDateAndTimeTimeZone());
+
+            env1.setSQLDateAndTimeTimeZone(_DateUtil.UTC);
+            // cfg:
+            assertEquals(TimeZone.getDefault(), cfg.getTimeZone());
+            assertNull(cfg.getSQLDateAndTimeTimeZone());
+            // env:
+            assertEquals(TimeZone.getDefault(), env1.getTimeZone());
+            assertEquals(_DateUtil.UTC, env1.getSQLDateAndTimeTimeZone());
+
+            env1.setTimeZone(localTZ);
+            // cfg:
+            assertEquals(TimeZone.getDefault(), cfg.getTimeZone());
+            assertNull(cfg.getSQLDateAndTimeTimeZone());
+            // env:
+            assertEquals(localTZ, env1.getTimeZone());
+            assertEquals(_DateUtil.UTC, env1.getSQLDateAndTimeTimeZone());
+            // env 2:
+            assertEquals(TimeZone.getDefault(), env2.getTimeZone());
+            assertNull(env2.getSQLDateAndTimeTimeZone());
+        }
+
+        {
+            TimeZone otherTZ1 = TimeZone.getTimeZone("GMT+05");
+            TimeZone otherTZ2 = TimeZone.getTimeZone("GMT+06");
+            Configuration cfg = new Configuration.Builder(Configuration.VERSION_3_0_0)
+                    .timeZone(otherTZ1)
+                    .sqlDateAndTimeTimeZone(otherTZ2)
+                    .build();
+
+            Template t = new Template(null, "", cfg);
+            Environment env1 = t.createProcessingEnvironment(null, new StringWriter());
+            Environment env2 = t.createProcessingEnvironment(null, new StringWriter());
+
+            env1.setTimeZone(localTZ);
+            env1.setSQLDateAndTimeTimeZone(_DateUtil.UTC);
+
+            // cfg:
+            assertEquals(otherTZ1, cfg.getTimeZone());
+            assertEquals(otherTZ2, cfg.getSQLDateAndTimeTimeZone());
+            // env:
+            assertEquals(localTZ, env1.getTimeZone());
+            assertEquals(_DateUtil.UTC, env1.getSQLDateAndTimeTimeZone());
+            // env 2:
+            assertEquals(otherTZ1, env2.getTimeZone());
+            assertEquals(otherTZ2, env2.getSQLDateAndTimeTimeZone());
+
+            try {
+                setTimeZoneToNull(env2);
+                fail();
+            } catch (IllegalArgumentException e) {
+                // expected
+            }
+            env2.setSQLDateAndTimeTimeZone(null);
+            assertEquals(otherTZ1, env2.getTimeZone());
+            assertNull(env2.getSQLDateAndTimeTimeZone());
+        }
+    }
+
+    @SuppressFBWarnings(value="NP_NULL_PARAM_DEREF_ALL_TARGETS_DANGEROUS", justification="Expected to fail")
+    private void setTimeZoneToNull(Environment env2) {
+        env2.setTimeZone(null);
+    }
+    
+    public void testSetICIViaSetSettingAPI() throws ConfigurationException {
+        Configuration.Builder cfg = new Configuration.Builder(Configuration.VERSION_3_0_0);
+        assertEquals(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS, cfg.getIncompatibleImprovements());
+        // This is the only valid value ATM:
+        cfg.setSetting(Configuration.ExtendableBuilder.INCOMPATIBLE_IMPROVEMENTS_KEY, "3.0.0");
+        assertEquals(Configuration.VERSION_3_0_0, cfg.getIncompatibleImprovements());
+    }
+
+    public void testSetLogTemplateExceptionsViaSetSettingAPI() throws ConfigurationException {
+        Configuration.Builder cfg = new Configuration.Builder(Configuration.VERSION_3_0_0);
+        assertFalse(cfg.getLogTemplateExceptions());
+        cfg.setSetting(MutableProcessingConfiguration.LOG_TEMPLATE_EXCEPTIONS_KEY, "true");
+        assertTrue(cfg.getLogTemplateExceptions());
+    }
+    
+    public void testSharedVariables() throws TemplateException, IOException {
+        Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0);
+
+        Map<String, Object> vars = new HashMap<>();
+        vars.put("a", "aa");
+        vars.put("b", "bb");
+        vars.put("c", new MyScalarModel());
+        cfgB.setSharedVariables(vars);
+
+        assertNull(cfgB.getSharedVariable("erased"));
+        
+        {
+            Configuration cfg = cfgB.build();
+
+            TemplateScalarModel aVal = (TemplateScalarModel) cfg.getWrappedSharedVariable("a");
+            assertEquals("aa", aVal.getAsString());
+            assertEquals(SimpleScalar.class, aVal.getClass());
+
+            TemplateScalarModel bVal = (TemplateScalarModel) cfg.getWrappedSharedVariable("b");
+            assertEquals("bb", bVal.getAsString());
+            assertEquals(SimpleScalar.class, bVal.getClass());
+            
+            TemplateScalarModel cVal = (TemplateScalarModel) cfg.getWrappedSharedVariable("c");
+            assertEquals("my", cVal.getAsString());
+            assertEquals(MyScalarModel.class, cfg.getWrappedSharedVariable("c").getClass());
+
+            // See if it actually works in templates:
+            StringWriter sw = new StringWriter();
+            new Template(null, "${a} ${b}", cfg)
+                    .process(ImmutableMap.of("a", "aaDM"), sw);
+            assertEquals("aaDM bb", sw.toString());
+        }
+        
+        cfgB.setSharedVariable("b", "bbLegacy");
+        
+        {
+            Configuration cfg = cfgB.build();
+
+            TemplateScalarModel aVal = (TemplateScalarModel) cfg.getWrappedSharedVariable("a");
+            assertEquals("aa", aVal.getAsString());
+            assertEquals(SimpleScalar.class, aVal.getClass());
+            
+            TemplateScalarModel bVal = (TemplateScalarModel) cfg.getWrappedSharedVariable("b");
+            assertEquals("bbLegacy", bVal.getAsString());
+            assertEquals(SimpleScalar.class, bVal.getClass());
+        }
+    }
+
+    @Test
+    public void testApiBuiltinEnabled() throws Exception {
+        try {
+            new Template(
+                    null, "${1?api}",
+                    new Configuration.Builder(Configuration.VERSION_3_0_0).build())
+                    .process(null, _NullWriter.INSTANCE);
+            fail();
+        } catch (TemplateException e) {
+            assertThat(e.getMessage(), containsString(MutableProcessingConfiguration.API_BUILTIN_ENABLED_KEY));
+        }
+            
+        new Template(
+                null, "${m?api.hashCode()}",
+                new Configuration.Builder(Configuration.VERSION_3_0_0).apiBuiltinEnabled(true).build())
+                .process(Collections.singletonMap("m", new HashMap()), _NullWriter.INSTANCE);
+    }
+
+    @Test
+    public void testTemplateUpdateDelay() throws Exception {
+        Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0);
+
+        assertEquals(DefaultTemplateResolver.DEFAULT_TEMPLATE_UPDATE_DELAY_MILLIS, cfgB.getTemplateUpdateDelayMilliseconds());
+        
+        cfgB.setTemplateUpdateDelayMilliseconds(4000);
+        assertEquals(4000L, cfgB.getTemplateUpdateDelayMilliseconds());
+        
+        cfgB.setTemplateUpdateDelayMilliseconds(100);
+        assertEquals(100L, cfgB.getTemplateUpdateDelayMilliseconds());
+        
+        try {
+            cfgB.setSetting(Configuration.ExtendableBuilder.TEMPLATE_UPDATE_DELAY_KEY, "5");
+            assertEquals(5000L, cfgB.getTemplateUpdateDelayMilliseconds());
+        } catch (ConfigurationSettingValueException e) {
+            assertThat(e.getMessage(), containsStringIgnoringCase("unit must be specified"));
+        }
+        cfgB.setSetting(Configuration.ExtendableBuilder.TEMPLATE_UPDATE_DELAY_KEY, "0");
+        assertEquals(0L, cfgB.getTemplateUpdateDelayMilliseconds());
+        try {
+            cfgB.setSetting(Configuration.ExtendableBuilder.TEMPLATE_UPDATE_DELAY_KEY, "5 foo");
+            assertEquals(5000L, cfgB.getTemplateUpdateDelayMilliseconds());
+        } catch (ConfigurationSettingValueException e) {
+            assertThat(e.getMessage(), containsStringIgnoringCase("\"foo\""));
+        }
+        
+        cfgB.setSetting(Configuration.ExtendableBuilder.TEMPLATE_UPDATE_DELAY_KEY, "3 ms");
+        assertEquals(3L, cfgB.getTemplateUpdateDelayMilliseconds());
+        cfgB.setSetting(Configuration.ExtendableBuilder.TEMPLATE_UPDATE_DELAY_KEY, "4ms");
+        assertEquals(4L, cfgB.getTemplateUpdateDelayMilliseconds());
+        
+        cfgB.setSetting(Configuration.ExtendableBuilder.TEMPLATE_UPDATE_DELAY_KEY, "3 s");
+        assertEquals(3000L, cfgB.getTemplateUpdateDelayMilliseconds());
+        cfgB.setSetting(Configuration.ExtendableBuilder.TEMPLATE_UPDATE_DELAY_KEY, "4s");
+        assertEquals(4000L, cfgB.getTemplateUpdateDelayMilliseconds());
+        
+        cfgB.setSetting(Configuration.ExtendableBuilder.TEMPLATE_UPDATE_DELAY_KEY, "3 m");
+        assertEquals(1000L * 60 * 3, cfgB.getTemplateUpdateDelayMilliseconds());
+        cfgB.setSetting(Configuration.ExtendableBuilder.TEMPLATE_UPDATE_DELAY_KEY, "4m");
+        assertEquals(1000L * 60 * 4, cfgB.getTemplateUpdateDelayMilliseconds());
+
+        cfgB.setSetting(Configuration.ExtendableBuilder.TEMPLATE_UPDATE_DELAY_KEY, "1 h");
+        assertEquals(1000L * 60 * 60, cfgB.getTemplateUpdateDelayMilliseconds());
+        cfgB.setSetting(Configuration.ExtendableBuilder.TEMPLATE_UPDATE_DELAY_KEY, "2h");
+        assertEquals(1000L * 60 * 60 * 2, cfgB.getTemplateUpdateDelayMilliseconds());
+    }
+    
+    @Test
+    @SuppressFBWarnings(value = "NP_NULL_PARAM_DEREF_ALL_TARGETS_DANGEROUS ", justification = "Testing wrong args")
+    public void testSetCustomNumberFormat() throws Exception {
+        Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0);
+        
+        try {
+            cfgB.setCustomNumberFormats(null);
+            fail();
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), containsString("null"));
+        }
+
+        try {
+            cfgB.setCustomNumberFormats(Collections.<String, TemplateNumberFormatFactory>singletonMap(
+                    "", HexTemplateNumberFormatFactory.INSTANCE));
+            fail();
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), containsString("0 length"));
+        }
+
+        try {
+            cfgB.setCustomNumberFormats(Collections.<String, TemplateNumberFormatFactory>singletonMap(
+                    "a_b", HexTemplateNumberFormatFactory.INSTANCE));
+            fail();
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), containsString("a_b"));
+        }
+
+        try {
+            cfgB.setCustomNumberFormats(Collections.<String, TemplateNumberFormatFactory>singletonMap(
+                    "a b", HexTemplateNumberFormatFactory.INSTANCE));
+            fail();
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), containsString("a b"));
+        }
+        
+        try {
+            cfgB.setCustomNumberFormats(ImmutableMap.<String, TemplateNumberFormatFactory>of(
+                    "a", HexTemplateNumberFormatFactory.INSTANCE,
+                    "@wrong", HexTemplateNumberFormatFactory.INSTANCE));
+            fail();
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), containsString("@wrong"));
+        }
+        
+        cfgB.setSetting(MutableProcessingConfiguration.CUSTOM_NUMBER_FORMATS_KEY_CAMEL_CASE,
+                "{ 'base': " + BaseNTemplateNumberFormatFactory.class.getName() + "() }");
+        assertEquals(
+                Collections.singletonMap("base", BaseNTemplateNumberFormatFactory.INSTANCE),
+                cfgB.getCustomNumberFormats());
+        
+        cfgB.setSetting(MutableProcessingConfiguration.CUSTOM_NUMBER_FORMATS_KEY_SNAKE_CASE,
+                "{ "
+                + "'base': " + BaseNTemplateNumberFormatFactory.class.getName() + "(), "
+                + "'hex': " + HexTemplateNumberFormatFactory.class.getName() + "()"
+                + " }");
+        assertEquals(
+                ImmutableMap.of(
+                        "base", BaseNTemplateNumberFormatFactory.INSTANCE,
+                        "hex", HexTemplateNumberFormatFactory.INSTANCE),
+                cfgB.getCustomNumberFormats());
+        
+        cfgB.setSetting(MutableProcessingConfiguration.CUSTOM_NUMBER_FORMATS_KEY, "{}");
+        assertEquals(Collections.emptyMap(), cfgB.getCustomNumberFormats());
+        
+        try {
+            cfgB.setSetting(MutableProcessingConfiguration.CUSTOM_NUMBER_FORMATS_KEY_CAMEL_CASE,
+                    "{ 'x': " + EpochMillisTemplateDateFormatFactory.class.getName() + "() }");
+            fail();
+        } catch (ConfigurationException e) {
+            assertThat(e.getCause().getMessage(), allOf(
+                    containsString(EpochMillisTemplateDateFormatFactory.class.getName()),
+                    containsString(TemplateNumberFormatFactory.class.getName())));
+        }
+    }
+
+    @Test
+    public void testSetTabSize() throws Exception {
+        Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0);
+        
+        String ftl = "${\t}";
+        
+        try {
+            new Template(null, ftl, cfgB.build());
+            fail();
+        } catch (ParseException e) {
+            assertEquals(9, e.getColumnNumber());
+        }
+        
+        cfgB.setTabSize(1);
+        try {
+            new Template(null, ftl, cfgB.build());
+            fail();
+        } catch (ParseException e) {
+            assertEquals(4, e.getColumnNumber());
+        }
+        
+        try {
+            cfgB.setTabSize(0);
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+        
+        try {
+            cfgB.setTabSize(257);
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+    }
+
+    @Test
+    public void testTabSizeSetting() throws Exception {
+        Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0);
+        assertEquals(8, cfgB.getTabSize());
+        cfgB.setSetting(Configuration.ExtendableBuilder.TAB_SIZE_KEY_CAMEL_CASE, "4");
+        assertEquals(4, cfgB.getTabSize());
+        cfgB.setSetting(Configuration.ExtendableBuilder.TAB_SIZE_KEY_SNAKE_CASE, "1");
+        assertEquals(1, cfgB.getTabSize());
+        
+        try {
+            cfgB.setSetting(Configuration.ExtendableBuilder.TAB_SIZE_KEY_SNAKE_CASE, "x");
+            fail();
+        } catch (ConfigurationException e) {
+            assertThat(e.getCause(), instanceOf(NumberFormatException.class));
+        }
+    }
+    
+    @SuppressFBWarnings(value="NP_NULL_PARAM_DEREF_ALL_TARGETS_DANGEROUS", justification="We test failures")
+    @Test
+    public void testSetCustomDateFormat() throws Exception {
+        Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0);
+        
+        try {
+            cfgB.setCustomDateFormats(null);
+            fail();
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), containsString("null"));
+        }
+        
+        try {
+            cfgB.setCustomDateFormats(Collections.<String, TemplateDateFormatFactory>singletonMap(
+                    "", EpochMillisTemplateDateFormatFactory.INSTANCE));
+            fail();
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), containsString("0 length"));
+        }
+
+        try {
+            cfgB.setCustomDateFormats(Collections.<String, TemplateDateFormatFactory>singletonMap(
+                    "a_b", EpochMillisTemplateDateFormatFactory.INSTANCE));
+            fail();
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), containsString("a_b"));
+        }
+
+        try {
+            cfgB.setCustomDateFormats(Collections.<String, TemplateDateFormatFactory>singletonMap(
+                    "a b", EpochMillisTemplateDateFormatFactory.INSTANCE));
+            fail();
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), containsString("a b"));
+        }
+        
+        try {
+            cfgB.setCustomDateFormats(ImmutableMap.<String, TemplateDateFormatFactory>of(
+                    "a", EpochMillisTemplateDateFormatFactory.INSTANCE,
+                    "@wrong", EpochMillisTemplateDateFormatFactory.INSTANCE));
+            fail();
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), containsString("@wrong"));
+        }
+        
+        cfgB.setSetting(MutableProcessingConfiguration.CUSTOM_DATE_FORMATS_KEY_CAMEL_CASE,
+                "{ 'epoch': " + EpochMillisTemplateDateFormatFactory.class.getName() + "() }");
+        assertEquals(
+                Collections.singletonMap("epoch", EpochMillisTemplateDateFormatFactory.INSTANCE),
+                cfgB.getCustomDateFormats());
+        
+        cfgB.setSetting(MutableProcessingConfiguration.CUSTOM_DATE_FORMATS_KEY_SNAKE_CASE,
+                "{ "
+                + "'epoch': " + EpochMillisTemplateDateFormatFactory.class.getName() + "(), "
+                + "'epochDiv': " + EpochMillisDivTemplateDateFormatFactory.class.getName() + "()"
+                + " }");
+        assertEquals(
+                ImmutableMap.of(
+                        "epoch", EpochMillisTemplateDateFormatFactory.INSTANCE,
+                        "epochDiv", EpochMillisDivTemplateDateFormatFactory.INSTANCE),
+                cfgB.getCustomDateFormats());
+        
+        cfgB.setSetting(MutableProcessingConfiguration.CUSTOM_DATE_FORMATS_KEY, "{}");
+        assertEquals(Collections.emptyMap(), cfgB.getCustomDateFormats());
+        
+        try {
+            cfgB.setSetting(MutableProcessingConfiguration.CUSTOM_DATE_FORMATS_KEY_CAMEL_CASE,
+                    "{ 'x': " + HexTemplateNumberFormatFactory.class.getName() + "() }");
+            fail();
+        } catch (ConfigurationException e) {
+            assertThat(e.getCause().getMessage(), allOf(
+                    containsString(HexTemplateNumberFormatFactory.class.getName()),
+                    containsString(TemplateDateFormatFactory.class.getName())));
+        }
+    }
+
+    public void testNamingConventionSetSetting() throws ConfigurationException {
+        Configuration.Builder cfg = new Configuration.Builder(Configuration.VERSION_3_0_0);
+
+        assertEquals(ParsingConfiguration.AUTO_DETECT_NAMING_CONVENTION, cfg.getNamingConvention());
+        
+        cfg.setSetting("naming_convention", "legacy");
+        assertEquals(ParsingConfiguration.LEGACY_NAMING_CONVENTION, cfg.getNamingConvention());
+        
+        cfg.setSetting("naming_convention", "camel_case");
+        assertEquals(ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION, cfg.getNamingConvention());
+        
+        cfg.setSetting("naming_convention", "auto_detect");
+        assertEquals(ParsingConfiguration.AUTO_DETECT_NAMING_CONVENTION, cfg.getNamingConvention());
+    }
+
+    public void testLazyImportsSetSetting() throws ConfigurationException {
+        Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0);
+
+        assertFalse(cfgB.getLazyImports());
+        assertFalse(cfgB.isLazyImportsSet());
+        cfgB.setSetting("lazy_imports", "true");
+        assertTrue(cfgB.getLazyImports());
+        cfgB.setSetting("lazyImports", "false");
+        assertFalse(cfgB.getLazyImports());
+        assertTrue(cfgB.isLazyImportsSet());
+    }
+    
+    public void testLazyAutoImportsSetSetting() throws ConfigurationException {
+        Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0);
+
+        assertNull(cfgB.getLazyAutoImports());
+        assertFalse(cfgB.isLazyAutoImportsSet());
+        cfgB.setSetting("lazy_auto_imports", "true");
+        assertEquals(Boolean.TRUE, cfgB.getLazyAutoImports());
+        assertTrue(cfgB.isLazyAutoImportsSet());
+        cfgB.setSetting("lazyAutoImports", "false");
+        assertEquals(Boolean.FALSE, cfgB.getLazyAutoImports());
+        cfgB.setSetting("lazyAutoImports", "null");
+        assertNull(cfgB.getLazyAutoImports());
+        assertTrue(cfgB.isLazyAutoImportsSet());
+        cfgB.unsetLazyAutoImports();
+        assertNull(cfgB.getLazyAutoImports());
+        assertFalse(cfgB.isLazyAutoImportsSet());
+    }
+
+    public void testLocaleSetting() throws TemplateException, ConfigurationException {
+        Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0);
+
+        assertEquals(Locale.getDefault(), cfgB.getLocale());
+        assertFalse(cfgB.isLocaleSet());
+
+        Locale nonDefault = Locale.getDefault().equals(Locale.GERMANY) ? Locale.FRANCE : Locale.GERMANY;
+        cfgB.setLocale(nonDefault);
+        assertTrue(cfgB.isLocaleSet());
+        assertEquals(nonDefault, cfgB.getLocale());
+
+        cfgB.unsetLocale();
+        assertEquals(Locale.getDefault(), cfgB.getLocale());
+        assertFalse(cfgB.isLocaleSet());
+
+        cfgB.setSetting(Configuration.ExtendableBuilder.LOCALE_KEY, "JVM default");
+        assertEquals(Locale.getDefault(), cfgB.getLocale());
+        assertTrue(cfgB.isLocaleSet());
+    }
+
+    public void testDefaultEncodingSetting() throws TemplateException, ConfigurationException {
+        Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0);
+
+        assertEquals(Charset.defaultCharset(), cfgB.getSourceEncoding());
+        assertFalse(cfgB.isSourceEncodingSet());
+
+        Charset nonDefault = Charset.defaultCharset().equals(StandardCharsets.UTF_8) ? StandardCharsets.ISO_8859_1
+                : StandardCharsets.UTF_8;
+        cfgB.setSourceEncoding(nonDefault);
+        assertTrue(cfgB.isSourceEncodingSet());
+        assertEquals(nonDefault, cfgB.getSourceEncoding());
+
+        cfgB.unsetSourceEncoding();
+        assertEquals(Charset.defaultCharset(), cfgB.getSourceEncoding());
+        assertFalse(cfgB.isSourceEncodingSet());
+
+        cfgB.setSetting(Configuration.ExtendableBuilder.SOURCE_ENCODING_KEY, "JVM default");
+        assertEquals(Charset.defaultCharset(), cfgB.getSourceEncoding());
+        assertTrue(cfgB.isSourceEncodingSet());
+    }
+
+    public void testTimeZoneSetting() throws TemplateException, ConfigurationException {
+        Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0);
+
+        assertEquals(TimeZone.getDefault(), cfgB.getTimeZone());
+        assertFalse(cfgB.isTimeZoneSet());
+
+        TimeZone nonDefault = TimeZone.getDefault().equals(_DateUtil.UTC) ? TimeZone.getTimeZone("PST") : _DateUtil.UTC;
+        cfgB.setTimeZone(nonDefault);
+        assertTrue(cfgB.isTimeZoneSet());
+        assertEquals(nonDefault, cfgB.getTimeZone());
+
+        cfgB.unsetTimeZone();
+        assertEquals(TimeZone.getDefault(), cfgB.getTimeZone());
+        assertFalse(cfgB.isTimeZoneSet());
+
+        cfgB.setSetting(Configuration.ExtendableBuilder.TIME_ZONE_KEY, "JVM default");
+        assertEquals(TimeZone.getDefault(), cfgB.getTimeZone());
+        assertTrue(cfgB.isTimeZoneSet());
+    }
+
+    @Test
+    public void testGetSettingNamesAreSorted() throws Exception {
+        Configuration cfg = new Configuration.Builder(Configuration.VERSION_3_0_0).build();
+        for (boolean camelCase : new boolean[] { false, true }) {
+            List<String> names = new ArrayList<>(Configuration.Builder.getSettingNames(camelCase));
+            List<String> procCfgNames = new ArrayList<>(new Template(null, "", cfg)
+                    .createProcessingEnvironment(null, _NullWriter.INSTANCE)
+                    .getSettingNames(camelCase));
+            assertStartsWith(names, procCfgNames);
+            
+            String prevName = null;
+            for (int i = procCfgNames.size(); i < names.size(); i++) {
+                String name = names.get(i);
+                if (prevName != null) {
+                    assertThat(name, greaterThan(prevName));
+                }
+                prevName = name;
+            }
+        }
+    }
+
+    @Test
+    @SuppressFBWarnings("DLS_DEAD_LOCAL_STORE")
+    public void testGetSettingNamesNameConventionsContainTheSame() throws Exception {
+        Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0);
+        ConfigurableTest.testGetSettingNamesNameConventionsContainTheSame(
+                new ArrayList<>(cfgB.getSettingNames(false)),
+                new ArrayList<>(cfgB.getSettingNames(true)));
+    }
+
+    @Test
+    @SuppressFBWarnings("DLS_DEAD_LOCAL_STORE")
+    public void testStaticFieldKeysCoverAllGetSettingNames() throws Exception {
+        Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0);
+        List<String> names = new ArrayList<>(cfgB.getSettingNames(false));
+        List<String> cfgableNames = new ArrayList<>(cfgB.getSettingNames(false));
+        assertStartsWith(names, cfgableNames);
+        
+        for (int i = cfgableNames.size(); i < names.size(); i++) {
+            String name = names.get(i);
+            assertTrue("No field was found for " + name, keyFieldExists(name));
+        }
+    }
+    
+    @Test
+    public void testGetSettingNamesCoversAllStaticKeyFields() throws Exception {
+        Collection<String> names = new Configuration.Builder(Configuration.VERSION_3_0_0).getSettingNames(false);
+        
+        for (Class<? extends MutableProcessingConfiguration> cfgableClass : new Class[] { Configuration.class, MutableProcessingConfiguration.class }) {
+            for (Field f : cfgableClass.getFields()) {
+                if (f.getName().endsWith("_KEY")) {
+                    final Object name = f.get(null);
+                    assertTrue("Missing setting name: " + name, names.contains(name));
+                }
+            }
+        }
+    }
+    
+    @Test
+    public void testKeyStaticFieldsHasAllVariationsAndCorrectFormat() throws IllegalArgumentException, IllegalAccessException {
+        ConfigurableTest.testKeyStaticFieldsHasAllVariationsAndCorrectFormat(Configuration.ExtendableBuilder.class);
+    }
+
+    @Test
+    public void testGetSettingNamesCoversAllSettingNames() throws Exception {
+        Collection<String> names = new Configuration.Builder(Configuration.VERSION_3_0_0).getSettingNames(false);
+        
+        for (Field f : MutableProcessingConfiguration.class.getFields()) {
+            if (f.getName().endsWith("_KEY")) {
+                final Object name = f.get(null);
+                assertTrue("Missing setting name: " + name, names.contains(name));
+            }
+        }
+    }
+
+    @Test
+    public void testSetSettingSupportsBothNamingConventions() throws Exception {
+        Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0);
+        
+        cfgB.setSetting(Configuration.ExtendableBuilder.SOURCE_ENCODING_KEY_CAMEL_CASE, StandardCharsets.UTF_16LE.name());
+        assertEquals(StandardCharsets.UTF_16LE, cfgB.getSourceEncoding());
+        cfgB.setSetting(Configuration.ExtendableBuilder.SOURCE_ENCODING_KEY_SNAKE_CASE, StandardCharsets.UTF_8.name());
+        assertEquals(StandardCharsets.UTF_8, cfgB.getSourceEncoding());
+        
+        for (String nameCC : cfgB.getSettingNames(true)) {
+            for (String value : new String[] { "1", "default", "true" }) {
+                Exception resultCC = null;
+                try {
+                    cfgB.setSetting(nameCC, value);
+                } catch (Exception e) {
+                    assertThat(e, not(instanceOf(UnknownConfigurationSettingException.class)));
+                    resultCC = e;
+                }
+                
+                String nameSC = _StringUtil.camelCaseToUnderscored(nameCC);
+                Exception resultSC = null;
+                try {
+                    cfgB.setSetting(nameSC, value);
+                } catch (Exception e) {
+                    assertThat(e, not(instanceOf(UnknownConfigurationSettingException.class)));
+                    resultSC = e;
+                }
+                
+                if (resultCC == null) {
+                    assertNull(resultSC);
+                } else {
+                    assertNotNull(resultSC);
+                    assertEquals(resultCC.getClass(), resultSC.getClass());
+                }
+            }
+        }
+    }
+    
+    @Test
+    public void testGetSupportedBuiltInDirectiveNames() {
+        Configuration cfg = new Configuration.Builder(Configuration.VERSION_3_0_0).build();
+        
+        Set<String> allNames = cfg.getSupportedBuiltInDirectiveNames(ParsingConfiguration.AUTO_DETECT_NAMING_CONVENTION);
+        Set<String> lNames = cfg.getSupportedBuiltInDirectiveNames(ParsingConfiguration.LEGACY_NAMING_CONVENTION);
+        Set<String> cNames = cfg.getSupportedBuiltInDirectiveNames(ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION);
+        
+        checkNamingConventionNameSets(allNames, lNames, cNames);
+        
+        for (String name : cNames) {
+            assertThat(name.toLowerCase(), isIn(lNames));
+        }
+    }
+
+    @Test
+    public void testGetSupportedBuiltInNames() {
+        Configuration cfg = new Configuration.Builder(Configuration.VERSION_3_0_0).build();
+        
+        Set<String> allNames = cfg.getSupportedBuiltInNames(ParsingConfiguration.AUTO_DETECT_NAMING_CONVENTION);
+        Set<String> lNames = cfg.getSupportedBuiltInNames(ParsingConfiguration.LEGACY_NAMING_CONVENTION);
+        Set<String> cNames = cfg.getSupportedBuiltInNames(ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION);
+        
+        checkNamingConventionNameSets(allNames, lNames, cNames);
+    }
+
+    private void checkNamingConventionNameSets(Set<String> allNames, Set<String> lNames, Set<String> cNames) {
+        for (String name : lNames) {
+            assertThat(allNames, hasItem(name));
+            assertTrue("Should be all-lowercase: " + name, name.equals(name.toLowerCase()));
+        }
+        for (String name : cNames) {
+            assertThat(allNames, hasItem(name));
+        }
+        for (String name : allNames) {
+            assertThat(name, anyOf(isIn(lNames), isIn(cNames)));
+        }
+        assertEquals(lNames.size(), cNames.size());
+    }
+    
+    @Test
+    public void testRemovedSettings() {
+        Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0);
+        try {
+            cfgB.setSetting("classic_compatible", "true");
+            fail();
+        } catch (ConfigurationException e) {
+            assertThat(e.getMessage(), allOf(containsString("removed"), containsString("3.0.0")));
+        }
+        try {
+            cfgB.setSetting("strict_syntax", "true");
+            fail();
+        } catch (ConfigurationException e) {
+            assertThat(e.getMessage(), allOf(containsString("removed"), containsString("3.0.0")));
+        }
+    }
+    
+    @SuppressWarnings("boxing")
+    private void assertStartsWith(List<String> list, List<String> headList) {
+        int index = 0;
+        for (String name : headList) {
+            assertThat(index, lessThan(list.size()));
+            assertEquals(name, list.get(index));
+            index++;
+        }
+    }
+
+    private boolean keyFieldExists(String name) throws Exception {
+        Field field;
+        try {
+            field = Configuration.class.getField(name.toUpperCase() + "_KEY");
+        } catch (NoSuchFieldException e) {
+            return false;
+        }
+        assertEquals(name, field.get(null));
+        return true;
+    }
+    
+    private static class MyScalarModel implements TemplateScalarModel {
+
+        @Override
+        public String getAsString() throws TemplateModelException {
+            return "my";
+        }
+        
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/CoreLocaleUtilsTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/CoreLocaleUtilsTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/CoreLocaleUtilsTest.java
new file mode 100644
index 0000000..6714fc3
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/CoreLocaleUtilsTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.util.Locale;
+
+import org.apache.freemarker.core.util._LocaleUtil;
+import org.junit.Test;
+
+public class CoreLocaleUtilsTest {
+
+    @Test
+    public void testGetLessSpecificLocale() {
+        Locale locale;
+        
+        locale = new Locale("ru", "RU", "Linux");
+        assertEquals("ru_RU_Linux", locale.toString());
+        locale = _LocaleUtil.getLessSpecificLocale(locale);
+        assertEquals("ru_RU", locale.toString());
+        locale = _LocaleUtil.getLessSpecificLocale(locale);
+        assertEquals("ru", locale.toString());
+        locale = _LocaleUtil.getLessSpecificLocale(locale);
+        assertNull(locale);
+        
+        locale = new Locale("ch", "CH");
+        assertEquals("ch_CH", locale.toString());
+        locale = _LocaleUtil.getLessSpecificLocale(locale);
+        assertEquals("ch", locale.toString());
+        locale = _LocaleUtil.getLessSpecificLocale(locale);
+        assertNull(locale);
+        
+        locale = new Locale("ja");
+        assertEquals("ja", locale.toString());
+        locale = _LocaleUtil.getLessSpecificLocale(locale);
+        assertNull(locale);
+
+        locale = new Locale("ja", "", "");
+        assertEquals("ja", locale.toString());
+        locale = _LocaleUtil.getLessSpecificLocale(locale);
+        assertNull(locale);
+        
+        locale = new Locale("");
+        assertEquals("", locale.toString());
+        locale = _LocaleUtil.getLessSpecificLocale(locale);
+        assertNull(locale);
+        
+        locale = new Locale("hu", "", "Linux");
+        assertEquals("hu__Linux", locale.toString());
+        locale = _LocaleUtil.getLessSpecificLocale(locale);
+        assertEquals("hu", locale.toString());
+        locale = _LocaleUtil.getLessSpecificLocale(locale);
+        assertNull(locale);
+    }
+    
+}



[48/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/ide-settings/Eclipse/Formatter-profile-FreeMarker.xml
----------------------------------------------------------------------
diff --git a/freemarker-core/src/ide-settings/Eclipse/Formatter-profile-FreeMarker.xml b/freemarker-core/src/ide-settings/Eclipse/Formatter-profile-FreeMarker.xml
new file mode 100644
index 0000000..2070004
--- /dev/null
+++ b/freemarker-core/src/ide-settings/Eclipse/Formatter-profile-FreeMarker.xml
@@ -0,0 +1,313 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!--
+  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.
+-->
+<profiles version="12">
+<profile kind="CodeFormatterProfile" name="FreeMarker" version="12">
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_ellipsis" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_imports" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_javadoc_comments" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.indentation.size" value="4"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.disabling_tag" value="@formatter:off"/>
+<setting id="org.eclipse.jdt.core.formatter.continuation_indentation" value="2"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_enum_constants" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_imports" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_package" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_binary_operator" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.indent_root_tags" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.enabling_tag" value="@formatter:on"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column" value="false"/>
+<setting id="org.eclipse.jdt.core.compiler.problem.enumIdentifier" value="error"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_block" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.line_length" value="120"/>
+<setting id="org.eclipse.jdt.core.formatter.use_on_off_tags" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_method_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_binary_expression" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_lambda_body" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.compact_else_if" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.compiler.problem.assertIdentifier" value="error"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_binary_operator" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_unary_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_ellipsis" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_line_comments" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.align_type_members_on_columns" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_assignment" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_conditional_expression" value="80"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block_in_case" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_header" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while" value="insert"/>
+<setting id="org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode" value="enabled"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_method_declaration" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.join_wrapped_lines" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_resources_in_try" value="80"/>
+<setting id="org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column" value="false"/>
+<setting id="org.eclipse.jdt.core.compiler.source" value="1.8"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.tabulation.size" value="4"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_source_code" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_field" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer" value="2"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_method" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.compiler.codegen.targetPlatform" value="1.8"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_switch" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_html" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_compact_if" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_empty_lines" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_unary_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_label" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_member_type" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_block_comments" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_body" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_multiple_fields" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_array_initializer" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.wrap_before_binary_operator" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.compiler.compliance" value="1.8"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_constant" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_type_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_package" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.join_lines_in_comments" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.indent_parameter_description" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.tabulation.char" value="space"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_import_groups" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.lineSplit" value="120"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch" value="insert"/>
+</profile>
+</profiles>

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/ide-settings/IntelliJ-IDEA/Editor-Inspections-FreeMarker.xml
----------------------------------------------------------------------
diff --git a/freemarker-core/src/ide-settings/IntelliJ-IDEA/Editor-Inspections-FreeMarker.xml b/freemarker-core/src/ide-settings/IntelliJ-IDEA/Editor-Inspections-FreeMarker.xml
new file mode 100644
index 0000000..4b40e57
--- /dev/null
+++ b/freemarker-core/src/ide-settings/IntelliJ-IDEA/Editor-Inspections-FreeMarker.xml
@@ -0,0 +1,33 @@
+<!--
+  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.
+-->
+<component name="InspectionProjectProfileManager">
+  <profile version="1.0">
+    <option name="myName" value="FreeMarker" />
+    <inspection_tool class="LoggerInitializedWithForeignClass" enabled="false" level="WARNING" enabled_by_default="false">
+      <option name="loggerClassName" value="org.apache.log4j.Logger,org.slf4j.LoggerFactory,org.apache.commons.logging.LogFactory,java.util.logging.Logger" />
+      <option name="loggerFactoryMethodName" value="getLogger,getLogger,getLog,getLogger" />
+    </inspection_tool>
+    <inspection_tool class="MissingOverrideAnnotation" enabled="true" level="WARNING" enabled_by_default="true">
+      <option name="ignoreObjectMethods" value="true" />
+      <option name="ignoreAnonymousClassMethods" value="false" />
+    </inspection_tool>
+    <inspection_tool class="RawTypeCanBeGeneric" enabled="true" level="WARNING" enabled_by_default="true" />
+    <inspection_tool class="RawUseOfParameterizedType" enabled="true" level="WARNING" enabled_by_default="true" />
+  </profile>
+</component>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/ide-settings/IntelliJ-IDEA/Java-code-style-FreeMarker.xml
----------------------------------------------------------------------
diff --git a/freemarker-core/src/ide-settings/IntelliJ-IDEA/Java-code-style-FreeMarker.xml b/freemarker-core/src/ide-settings/IntelliJ-IDEA/Java-code-style-FreeMarker.xml
new file mode 100644
index 0000000..983f742
--- /dev/null
+++ b/freemarker-core/src/ide-settings/IntelliJ-IDEA/Java-code-style-FreeMarker.xml
@@ -0,0 +1,66 @@
+<!--
+  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.
+-->
+<code_scheme name="FreeMarker">
+  <option name="LINE_SEPARATOR" value="&#xA;" />
+  <option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
+  <option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="1" />
+  <option name="IMPORT_LAYOUT_TABLE">
+    <value>
+      <package name="" withSubpackages="true" static="true" />
+      <emptyLine />
+      <package name="java" withSubpackages="true" static="false" />
+      <emptyLine />
+      <package name="javax" withSubpackages="true" static="false" />
+      <emptyLine />
+      <package name="org" withSubpackages="true" static="false" />
+      <emptyLine />
+      <package name="com" withSubpackages="true" static="false" />
+      <emptyLine />
+      <package name="" withSubpackages="true" static="false" />
+      <emptyLine />
+    </value>
+  </option>
+  <option name="WRAP_WHEN_TYPING_REACHES_RIGHT_MARGIN" value="true" />
+  <option name="JD_ADD_BLANK_AFTER_PARM_COMMENTS" value="true" />
+  <option name="JD_ADD_BLANK_AFTER_RETURN" value="true" />
+  <option name="JD_KEEP_EMPTY_PARAMETER" value="false" />
+  <option name="JD_KEEP_EMPTY_EXCEPTION" value="false" />
+  <option name="JD_KEEP_EMPTY_RETURN" value="false" />
+  <option name="JD_PARAM_DESCRIPTION_ON_NEW_LINE" value="true" />
+  <option name="WRAP_COMMENTS" value="true" />
+  <JavaCodeStyleSettings>
+    <option name="ANNOTATION_PARAMETER_WRAP" value="1" />
+    <option name="CLASS_NAMES_IN_JAVADOC" value="3" />
+  </JavaCodeStyleSettings>
+  <codeStyleSettings language="JAVA">
+    <option name="RIGHT_MARGIN" value="120" />
+    <option name="CALL_PARAMETERS_WRAP" value="1" />
+    <option name="EXTENDS_LIST_WRAP" value="1" />
+    <option name="THROWS_LIST_WRAP" value="1" />
+    <option name="EXTENDS_KEYWORD_WRAP" value="1" />
+    <option name="BINARY_OPERATION_WRAP" value="1" />
+    <option name="TERNARY_OPERATION_WRAP" value="1" />
+    <option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
+    <option name="ARRAY_INITIALIZER_WRAP" value="1" />
+    <option name="ASSIGNMENT_WRAP" value="1" />
+    <option name="WRAP_LONG_LINES" value="true" />
+    <option name="PARAMETER_ANNOTATION_WRAP" value="1" />
+    <option name="VARIABLE_ANNOTATION_WRAP" value="1" />
+  </codeStyleSettings>
+</code_scheme>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/APINotSupportedTemplateException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/APINotSupportedTemplateException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/APINotSupportedTemplateException.java
new file mode 100644
index 0000000..732f5e2
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/APINotSupportedTemplateException.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateModel;
+
+/**
+ * Thrown when {@code ?api} is not supported by a value.
+ */
+class APINotSupportedTemplateException extends TemplateException {
+
+    APINotSupportedTemplateException(Environment env, ASTExpression blamedExpr, TemplateModel model) {
+        super(null, env, blamedExpr, buildDescription(env, blamedExpr, model));
+    }
+
+    protected static _ErrorDescriptionBuilder buildDescription(Environment env, ASTExpression blamedExpr,
+            TemplateModel tm) {
+        final _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder(
+                "The value doesn't support ?api. See requirements in the FreeMarker Manual. ("
+                + "FTL type: ", new _DelayedFTLTypeDescription(tm),
+                ", TemplateModel class: ", new _DelayedShortClassName(tm.getClass()),
+                ", ObjectWapper: ", new _DelayedToString(env.getObjectWrapper()), ")"
+        ).blame(blamedExpr);
+
+        if (blamedExpr.isLiteral()) {
+            desc.tip("Only adapted Java objects can possibly have API, not values created inside templates.");
+        }
+
+        return desc;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTComment.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTComment.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTComment.java
new file mode 100644
index 0000000..7ee2695
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTComment.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * AST comment node
+ */
+final class ASTComment extends ASTElement {
+
+    private final String text;
+
+    ASTComment(String text) {
+        this.text = text;
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) {
+        // do nothing, skip the body
+        return null;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        if (canonical) {
+            return "<#--" + text + "-->";
+        } else {
+            return "comment " + _StringUtil.jQuote(text.trim());
+        }
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "#--...--";
+    }
+    
+
+    @Override
+    int getParameterCount() {
+        return 1;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return text;
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return ParameterRole.CONTENT;
+    }
+
+    public String getText() {
+        return text;
+    }
+
+    @Override
+    boolean isOutputCacheable() {
+        return true;
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDebugBreak.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDebugBreak.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDebugBreak.java
new file mode 100644
index 0000000..fe42f41
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDebugBreak.java
@@ -0,0 +1,89 @@
+/*
+ * 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.core.debug._DebuggerService;
+
+/**
+ * AST node: A debug breakpoint
+ */
+class ASTDebugBreak extends ASTElement {
+    public ASTDebugBreak(ASTElement nestedBlock) {
+        addChild(nestedBlock);
+        copyLocationFrom(nestedBlock);
+    }
+    
+    @Override
+    protected ASTElement[] accept(Environment env) throws TemplateException, IOException {
+        if (!_DebuggerService.suspendEnvironment(
+                env, getTemplate().getSourceName(), getChild(0).getBeginLine())) {
+            return getChild(0).accept(env);
+        } else {
+            throw new StopException(env, "Stopped by debugger");
+        }
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        if (canonical) {
+            StringBuilder sb = new StringBuilder();
+            sb.append("<#-- ");
+            sb.append("debug break");
+            if (getChildCount() == 0) {
+                sb.append(" /-->");
+            } else {
+                sb.append(" -->");
+                sb.append(getChild(0).getCanonicalForm());                
+                sb.append("<#--/ debug break -->");
+            }
+            return sb.toString();
+        } else {
+            return "debug break";
+        }
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "#debug_break";
+    }
+
+    @Override
+    int getParameterCount() {
+        return 0;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+        
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirAssignment.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirAssignment.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirAssignment.java
new file mode 100644
index 0000000..4961f7f
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirAssignment.java
@@ -0,0 +1,279 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * AST directive node: An instruction that makes a single assignment, like {@code <#local x=1>}, {@code <#global x=1>},
+ * {@code <#assign x=1>}.
+ * This is also used as the child of {@link ASTDirAssignmentsContainer}, if there are multiple assignments in the same
+ * tag, like in {@code <#local x=1 y=2>}.
+ */
+final class ASTDirAssignment extends ASTDirective {
+
+    // These must not clash with ArithmeticExpression.TYPE_... constants: 
+    private static final int OPERATOR_TYPE_EQUALS = 0x10000;
+    private static final int OPERATOR_TYPE_PLUS_EQUALS = 0x10001;
+    private static final int OPERATOR_TYPE_PLUS_PLUS = 0x10002;
+    private static final int OPERATOR_TYPE_MINUS_MINUS = 0x10003;
+    
+    private final int/*enum*/ scope;
+    private final String variableName;
+    private final int operatorType;
+    private final ASTExpression valueExp;
+    private ASTExpression namespaceExp;
+
+    static final int NAMESPACE = 1;
+    static final int LOCAL = 2;
+    static final int GLOBAL = 3;
+    
+    private static final Number ONE = Integer.valueOf(1);
+
+    /**
+     * @param variableName the variable name to assign to.
+     * @param valueExp the expression to assign.
+     * @param scope the scope of the assignment, one of NAMESPACE, LOCAL, or GLOBAL
+     */
+    ASTDirAssignment(String variableName,
+            int operator,
+            ASTExpression valueExp,
+            int scope) {
+        this.scope = scope;
+        
+        this.variableName = variableName;
+        
+        if (operator == FMParserConstants.EQUALS) {
+            operatorType = OPERATOR_TYPE_EQUALS;
+        } else {
+            switch (operator) {
+            case FMParserConstants.PLUS_PLUS:
+                operatorType = OPERATOR_TYPE_PLUS_PLUS;
+                break;
+            case FMParserConstants.MINUS_MINUS:
+                operatorType = OPERATOR_TYPE_MINUS_MINUS;
+                break;
+            case FMParserConstants.PLUS_EQUALS:
+                operatorType = OPERATOR_TYPE_PLUS_EQUALS;
+                break;
+            case FMParserConstants.MINUS_EQUALS:
+                operatorType = ArithmeticExpression.TYPE_SUBSTRACTION;
+                break;
+            case FMParserConstants.TIMES_EQUALS:
+                operatorType = ArithmeticExpression.TYPE_MULTIPLICATION;
+                break;
+            case FMParserConstants.DIV_EQUALS:
+                operatorType = ArithmeticExpression.TYPE_DIVISION;
+                break;
+            case FMParserConstants.MOD_EQUALS:
+                operatorType = ArithmeticExpression.TYPE_MODULO;
+                break;
+            default:
+                throw new BugException();
+            }
+        }
+        
+        this.valueExp = valueExp;
+    }
+    
+    void setNamespaceExp(ASTExpression namespaceExp) {
+        if (scope != NAMESPACE && namespaceExp != null) throw new BugException();
+        this.namespaceExp =  namespaceExp;
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) throws TemplateException {
+        final Environment.Namespace namespace;
+        if (namespaceExp == null) {
+            switch (scope) {
+            case LOCAL:
+                namespace = null;
+                break;
+            case GLOBAL:
+                namespace = env.getGlobalNamespace();
+                break;
+            case NAMESPACE:
+                namespace = env.getCurrentNamespace();
+                break;
+            default:
+                throw new BugException("Unexpected scope type: " + scope);
+            }
+        } else {
+            TemplateModel namespaceTM = namespaceExp.eval(env);
+            try {
+                namespace = (Environment.Namespace) namespaceTM;
+            } catch (ClassCastException e) {
+                throw new NonNamespaceException(namespaceExp, namespaceTM, env);
+            }
+            if (namespace == null) {
+                throw InvalidReferenceException.getInstance(namespaceExp, env);
+            }
+        }
+        
+        TemplateModel value;
+        if (operatorType == OPERATOR_TYPE_EQUALS) {
+            value = valueExp.eval(env);
+            valueExp.assertNonNull(value, env);
+        } else {
+            TemplateModel lhoValue;
+            if (namespace == null) {
+                lhoValue = env.getLocalVariable(variableName);
+            } else {
+                lhoValue = namespace.get(variableName);
+            }
+            
+            if (operatorType == OPERATOR_TYPE_PLUS_EQUALS) {  // Add or concat operation
+                if (lhoValue == null) {
+                    throw InvalidReferenceException.getInstance(
+                            variableName, getOperatorTypeAsString(), env);
+                }
+                
+                value = valueExp.eval(env);
+                valueExp.assertNonNull(value, env);
+                value = ASTExpAddOrConcat._eval(env, namespaceExp, null, lhoValue, valueExp, value);
+            } else {  // Numerical operation
+                Number lhoNumber;
+                if (lhoValue instanceof TemplateNumberModel) {
+                    lhoNumber = _EvalUtil.modelToNumber((TemplateNumberModel) lhoValue, null);
+                } else if (lhoValue == null) {
+                    throw InvalidReferenceException.getInstance(variableName, getOperatorTypeAsString(), env);
+                } else {
+                    throw new NonNumericalException(variableName, lhoValue, null, env);
+                }
+
+                if (operatorType == OPERATOR_TYPE_PLUS_PLUS) {
+                    value  = ASTExpAddOrConcat._evalOnNumbers(env, getParent(), lhoNumber, ONE);
+                } else if (operatorType == OPERATOR_TYPE_MINUS_MINUS) {
+                    value = ArithmeticExpression._eval(
+                            env, getParent(), lhoNumber, ArithmeticExpression.TYPE_SUBSTRACTION, ONE);
+                } else { // operatorType == ArithmeticExpression.TYPE_...
+                    Number rhoNumber = valueExp.evalToNumber(env);
+                    value = ArithmeticExpression._eval(env, this, lhoNumber, operatorType, rhoNumber);
+                }
+            }
+        }
+        
+        if (namespace == null) {
+            env.setLocalVariable(variableName, value);
+        } else {
+            namespace.put(variableName, value);
+        }
+        return null;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder buf = new StringBuilder();
+        String dn = getParent() instanceof ASTDirAssignmentsContainer ? null : getNodeTypeSymbol();
+        if (dn != null) {
+            if (canonical) buf.append("<");
+            buf.append(dn);
+            buf.append(' ');
+        }
+        
+        buf.append(_StringUtil.toFTLTopLevelTragetIdentifier(variableName));
+        
+        if (valueExp != null) {
+            buf.append(' ');
+        }
+        buf.append(getOperatorTypeAsString());
+        if (valueExp != null) {
+            buf.append(' ');
+            buf.append(valueExp.getCanonicalForm());
+        }
+        if (dn != null) {
+            if (namespaceExp != null) {
+                buf.append(" in ");
+                buf.append(namespaceExp.getCanonicalForm());
+            }
+            if (canonical) buf.append(">");
+        }
+        return buf.toString();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return getDirectiveName(scope);
+    }
+    
+    static String getDirectiveName(int scope) {
+        if (scope == ASTDirAssignment.LOCAL) {
+            return "#local";
+        } else if (scope == ASTDirAssignment.GLOBAL) {
+            return "#global";
+        } else if (scope == ASTDirAssignment.NAMESPACE) {
+            return "#assign";
+        } else {
+            return "#{unknown_assignment_type}";
+        }
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 5;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return variableName;
+        case 1: return getOperatorTypeAsString();
+        case 2: return valueExp;
+        case 3: return Integer.valueOf(scope);
+        case 4: return namespaceExp;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        switch (idx) {
+        case 0: return ParameterRole.ASSIGNMENT_TARGET;
+        case 1: return ParameterRole.ASSIGNMENT_OPERATOR;
+        case 2: return ParameterRole.ASSIGNMENT_SOURCE;
+        case 3: return ParameterRole.VARIABLE_SCOPE;
+        case 4: return ParameterRole.NAMESPACE;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+    private String getOperatorTypeAsString() {
+        if (operatorType == OPERATOR_TYPE_EQUALS) {
+            return "=";
+        } else if (operatorType == OPERATOR_TYPE_PLUS_EQUALS) {
+            return "+=";
+        } else if (operatorType == OPERATOR_TYPE_PLUS_PLUS) {
+            return "++";
+        } else if (operatorType == OPERATOR_TYPE_MINUS_MINUS) {
+            return "--";
+        } else {
+            return ArithmeticExpression.getOperatorSymbol(operatorType) + "=";
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirAssignmentsContainer.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirAssignmentsContainer.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirAssignmentsContainer.java
new file mode 100644
index 0000000..fd2873d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirAssignmentsContainer.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 java.io.IOException;
+
+/**
+ * AST directive node: An instruction that does multiple assignments, like [#local x=1 x=2].
+ * Each assignment is represented by a {@link ASTDirAssignment} child element.
+ * If there's only one assignment, its usually just a {@link ASTDirAssignment} without parent {@link ASTDirAssignmentsContainer}.
+ */
+final class ASTDirAssignmentsContainer extends ASTDirective {
+
+    private int scope;
+    private ASTExpression namespaceExp;
+
+    ASTDirAssignmentsContainer(int scope) {
+        this.scope = scope;
+        setChildBufferCapacity(1);
+    }
+
+    void addAssignment(ASTDirAssignment assignment) {
+        addChild(assignment);
+    }
+    
+    void setNamespaceExp(ASTExpression namespaceExp) {
+        this.namespaceExp = namespaceExp;
+        int ln = getChildCount();
+        for (int i = 0; i < ln; i++) {
+            ((ASTDirAssignment) getChild(i)).setNamespaceExp(namespaceExp);
+        }
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) throws TemplateException, IOException {
+        return getChildBuffer();
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder buf = new StringBuilder();
+        if (canonical) buf.append('<');
+        buf.append(ASTDirAssignment.getDirectiveName(scope));
+        if (canonical) {
+            buf.append(' ');
+            int ln = getChildCount();
+            for (int i = 0; i < ln; i++) {
+                if (i != 0) {
+                    buf.append(", ");
+                }
+                ASTDirAssignment assignment = (ASTDirAssignment) getChild(i);
+                buf.append(assignment.getCanonicalForm());
+            }
+        } else {
+            buf.append("-container");
+        }
+        if (namespaceExp != null) {
+            buf.append(" in ");
+            buf.append(namespaceExp.getCanonicalForm());
+        }
+        if (canonical) buf.append(">");
+        return buf.toString();
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return Integer.valueOf(scope);
+        case 1: return namespaceExp;
+        default: return null;
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        switch (idx) {
+        case 0: return ParameterRole.VARIABLE_SCOPE;
+        case 1: return ParameterRole.NAMESPACE;
+        default: return null;
+        }
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return ASTDirAssignment.getDirectiveName(scope);
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirAttemptRecoverContainer.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirAttemptRecoverContainer.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirAttemptRecoverContainer.java
new file mode 100644
index 0000000..c01c453
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirAttemptRecoverContainer.java
@@ -0,0 +1,88 @@
+/*
+ * 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;
+
+/**
+ * AST directive node: Holder for the attempted section of the {@code #attempt} element and of the nested
+ * {@code #recover} element ({@link ASTDirRecover}).
+ */
+final class ASTDirAttemptRecoverContainer extends ASTDirective {
+    
+    private ASTElement attemptedSection;
+    private ASTDirRecover recoverySection;
+    
+    ASTDirAttemptRecoverContainer(TemplateElements attemptedSectionChildren, ASTDirRecover recoverySection) {
+        ASTElement attemptedSection = attemptedSectionChildren.asSingleElement();
+        this.attemptedSection = attemptedSection;
+        this.recoverySection = recoverySection;
+        setChildBufferCapacity(2);
+        addChild(attemptedSection); // for backward compatibility
+        addChild(recoverySection);
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) throws TemplateException, IOException {
+        env.visitAttemptRecover(this, attemptedSection, recoverySection);
+        return null;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        if (!canonical) {
+            return getNodeTypeSymbol();
+        } else {
+            StringBuilder buf = new StringBuilder();
+            buf.append("<").append(getNodeTypeSymbol()).append(">");
+            buf.append(getChildrenCanonicalForm());            
+            buf.append("</").append(getNodeTypeSymbol()).append(">");
+            return buf.toString();
+        }
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 1;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return recoverySection;
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return ParameterRole.ERROR_HANDLER;
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "#attempt";
+    }
+    
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirAutoEsc.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirAutoEsc.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirAutoEsc.java
new file mode 100644
index 0000000..b27dc0a
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirAutoEsc.java
@@ -0,0 +1,77 @@
+/*
+ * 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;
+
+/**
+ * AST directive node: {@code #autoEsc}
+ */
+final class ASTDirAutoEsc extends ASTDirective {
+    
+    ASTDirAutoEsc(TemplateElements children) { 
+        setChildren(children);
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) throws TemplateException, IOException {
+        return getChildBuffer();
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        if (canonical) {
+            return "<" + getNodeTypeSymbol() + "\">" + getChildrenCanonicalForm() + "</" + getNodeTypeSymbol() + ">";
+        } else {
+            return getNodeTypeSymbol();
+        }
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "#autoesc";
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 0;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    boolean isIgnorable(boolean stripWhitespace) {
+        return getChildCount() == 0;
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirBreak.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirBreak.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirBreak.java
new file mode 100644
index 0000000..19a0c25
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirBreak.java
@@ -0,0 +1,70 @@
+/*
+ * 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;
+
+/**
+ * AST directive node: {@code #break}
+ */
+final class ASTDirBreak extends ASTDirective {
+
+    @Override
+    ASTElement[] accept(Environment env) {
+        throw Break.INSTANCE;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        return canonical ? "<" + getNodeTypeSymbol() + "/>" : getNodeTypeSymbol();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "#break";
+    }
+
+    @Override
+    int getParameterCount() {
+        return 0;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+    
+    static class Break extends RuntimeException {
+        static final Break INSTANCE = new Break();
+        private Break() {
+        }
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+}
+
+

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirCapturingAssignment.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirCapturingAssignment.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirCapturingAssignment.java
new file mode 100644
index 0000000..9aa5eab
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirCapturingAssignment.java
@@ -0,0 +1,184 @@
+/*
+ * 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.Map;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateTransformModel;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.apache.freemarker.core.outputformat.MarkupOutputFormat;
+
+/**
+ * AST directive node: Like {@code <#local x>...</#local>}.
+ */
+final class ASTDirCapturingAssignment extends ASTDirective {
+
+    private final String varName;
+    private final ASTExpression namespaceExp;
+    private final int scope;
+    private final MarkupOutputFormat<?> markupOutputFormat;
+
+    ASTDirCapturingAssignment(TemplateElements children, String varName, int scope, ASTExpression namespaceExp, MarkupOutputFormat<?> markupOutputFormat) {
+        setChildren(children);
+        this.varName = varName;
+        this.namespaceExp = namespaceExp;
+        this.scope = scope;
+        this.markupOutputFormat = markupOutputFormat;
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) throws TemplateException, IOException {
+        ASTElement[] children = getChildBuffer();
+        if (children != null) {
+            env.visitAndTransform(children, new CaptureOutput(env), null);
+        } else {
+            TemplateModel value = capturedStringToModel("");
+            if (namespaceExp != null) {
+                Environment.Namespace ns = (Environment.Namespace) namespaceExp.eval(env);
+                ns.put(varName, value);
+            } else if (scope == ASTDirAssignment.NAMESPACE) {
+                env.setVariable(varName, value);
+            } else if (scope == ASTDirAssignment.GLOBAL) {
+                env.setGlobalVariable(varName, value);
+            } else if (scope == ASTDirAssignment.LOCAL) {
+                env.setLocalVariable(varName, value);
+            }
+        }
+        return null;
+    }
+
+    private TemplateModel capturedStringToModel(String s) throws TemplateModelException {
+        return markupOutputFormat == null ? new SimpleScalar(s) : markupOutputFormat.fromMarkup(s);
+    }
+
+    private class CaptureOutput implements TemplateTransformModel {
+        private final Environment env;
+        private final Environment.Namespace fnsModel;
+        
+        CaptureOutput(Environment env) throws TemplateException {
+            this.env = env;
+            TemplateModel nsModel = null;
+            if (namespaceExp != null) {
+                nsModel = namespaceExp.eval(env);
+                if (!(nsModel instanceof Environment.Namespace)) {
+                    throw new NonNamespaceException(namespaceExp, nsModel, env);
+                }
+            }
+            fnsModel = (Environment.Namespace ) nsModel; 
+        }
+        
+        @Override
+        public Writer getWriter(Writer out, Map args) {
+            return new StringWriter() {
+                @Override
+                public void close() throws IOException {
+                    TemplateModel result;
+                    try {
+                        result = capturedStringToModel(toString());
+                    } catch (TemplateModelException e) {
+                        // [Java 1.6] e to cause
+                        throw new IOException("Failed to invoke FTL value from captured string: " + e);
+                    }
+                    switch(scope) {
+                        case ASTDirAssignment.NAMESPACE: {
+                            if (fnsModel != null) {
+                                fnsModel.put(varName, result);
+                            } else {
+                                env.setVariable(varName, result);
+                            }
+                            break;
+                        }
+                        case ASTDirAssignment.LOCAL: {
+                            env.setLocalVariable(varName, result);
+                            break;
+                        }
+                        case ASTDirAssignment.GLOBAL: {
+                            env.setGlobalVariable(varName, result);
+                            break;
+                        }
+                    }
+                }
+            };
+        }
+    }
+    
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder sb = new StringBuilder();
+        if (canonical) sb.append("<");
+        sb.append(getNodeTypeSymbol());
+        sb.append(' ');
+        sb.append(varName);
+        if (namespaceExp != null) {
+            sb.append(" in ");
+            sb.append(namespaceExp.getCanonicalForm());
+        }
+        if (canonical) {
+            sb.append('>');
+            sb.append(getChildrenCanonicalForm());
+            sb.append("</");
+            sb.append(getNodeTypeSymbol());
+            sb.append('>');
+        } else {
+            sb.append(" = .nested_output");
+        }
+        return sb.toString();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return ASTDirAssignment.getDirectiveName(scope);
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 3;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return varName;
+        case 1: return Integer.valueOf(scope);
+        case 2: return namespaceExp;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        switch (idx) {
+        case 0: return ParameterRole.ASSIGNMENT_TARGET;
+        case 1: return ParameterRole.VARIABLE_SCOPE;
+        case 2: return ParameterRole.NAMESPACE;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirCase.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirCase.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirCase.java
new file mode 100644
index 0000000..0f87778
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirCase.java
@@ -0,0 +1,91 @@
+/*
+ * 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;
+
+/**
+ * AST directive node: {@code #case} (inside a {@code #switch}) 
+ */
+final class ASTDirCase extends ASTDirective {
+
+    final int TYPE_CASE = 0;
+    final int TYPE_DEFAULT = 1;
+    
+    ASTExpression condition;
+
+    ASTDirCase(ASTExpression matchingValue, TemplateElements children) {
+        condition = matchingValue;
+        setChildren(children);
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) {
+        return getChildBuffer();
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder sb = new StringBuilder();
+        if (canonical) sb.append('<');
+        sb.append(getNodeTypeSymbol());
+        if (condition != null) {
+            sb.append(' ');
+            sb.append(condition.getCanonicalForm());
+        }
+        if (canonical) {
+            sb.append('>');
+            sb.append(getChildrenCanonicalForm());
+        }
+        return sb.toString();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return condition != null ? "#case" : "#default";
+    }
+
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return condition;
+        case 1: return Integer.valueOf(condition != null ? TYPE_CASE : TYPE_DEFAULT);
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        switch (idx) {
+        case 0: return ParameterRole.CONDITION;
+        case 1: return ParameterRole.AST_NODE_SUBTYPE;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+        
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirCompress.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirCompress.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirCompress.java
new file mode 100644
index 0000000..8810381
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirCompress.java
@@ -0,0 +1,87 @@
+/*
+ * 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.core.util.StandardCompress;
+
+/**
+ * AST directive node: {@code #compress}.
+ * An instruction that reduces all sequences of whitespace to a single
+ * space or newline. In addition, leading and trailing whitespace is removed.
+ * 
+ * @see org.apache.freemarker.core.util.StandardCompress
+ */
+final class ASTDirCompress extends ASTDirective {
+
+    ASTDirCompress(TemplateElements children) { 
+        setChildren(children);
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) throws TemplateException, IOException {
+        ASTElement[] childBuffer = getChildBuffer();
+        if (childBuffer != null) {
+            env.visitAndTransform(childBuffer, StandardCompress.INSTANCE, null);
+        }
+        return null;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        if (canonical) {
+            return "<" + getNodeTypeSymbol() + ">" + getChildrenCanonicalForm() + "</" + getNodeTypeSymbol() + ">";
+        } else {
+            return getNodeTypeSymbol();
+        }
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "#compress";
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 0;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    boolean isIgnorable(boolean stripWhitespace) {
+        return getChildCount() == 0 && getParameterCount() == 0;
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirElseOfList.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirElseOfList.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirElseOfList.java
new file mode 100644
index 0000000..7aafd2f
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirElseOfList.java
@@ -0,0 +1,75 @@
+/*
+ * 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;
+
+/**
+ * AST directive node: {@code #else} inside a {@code  #list}.
+ */
+final class ASTDirElseOfList extends ASTDirective {
+    
+    ASTDirElseOfList(TemplateElements children) {
+        setChildren(children);
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) throws TemplateException, IOException {
+        return getChildBuffer();
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        if (canonical) {
+            StringBuilder buf = new StringBuilder();
+            buf.append('<').append(getNodeTypeSymbol()).append('>');
+            buf.append(getChildrenCanonicalForm());            
+            return buf.toString();
+        } else {
+            return getNodeTypeSymbol();
+        }
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return "#else";
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 0;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+}


[51/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building the jar-s (with OSGi support, legal files, etc.), generating and installing Maven artifacts, running the tests, generating JavaDoc.


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

Branch: refs/heads/3
Commit: 3fd5606295396ce2bea03bf2a11772f690e3cc6f
Parents: d373a34
Author: ddekany <dd...@apache.org>
Authored: Fri May 12 20:09:46 2017 +0200
Committer: ddekany <dd...@apache.org>
Committed: Sun May 14 12:50:43 2017 +0200

----------------------------------------------------------------------
 .gitignore                                      |   16 +-
 .travis.yml                                     |    5 -
 LICENSE                                         |   13 +-
 README-gradle.md                                |   13 -
 README.md                                       |   77 +-
 build.gradle                                    |  348 +-
 build.properties.sample                         |   23 -
 build.xml                                       | 1093 -----
 freemarker-core-java8-test/build.gradle         |   19 +
 .../src/main/resources/META-INF/DISCLAIMER      |    8 +
 .../src/main/resources/META-INF/LICENSE         |  202 +
 .../core/model/impl/BridgeMethodsBean.java      |   30 +
 .../core/model/impl/BridgeMethodsBeanBase.java  |   29 +
 ...Java8BridgeMethodsWithDefaultMethodBean.java |   29 +
 ...ava8BridgeMethodsWithDefaultMethodBean2.java |   23 +
 ...8BridgeMethodsWithDefaultMethodBeanBase.java |   31 +
 ...BridgeMethodsWithDefaultMethodBeanBase2.java |   28 +
 .../model/impl/Java8DefaultMethodsBean.java     |   84 +
 .../model/impl/Java8DefaultMethodsBeanBase.java |   97 +
 ...a8DefaultObjectWrapperBridgeMethodsTest.java |   65 +
 .../impl/Java8DefaultObjectWrapperTest.java     |  160 +
 freemarker-core/build.gradle                    |  155 +
 freemarker-core/src/dist/bin/LICENSE            |  232 +
 .../src/dist/bin/documentation/index.html       |   67 +
 .../src/dist/javadoc/META-INF/LICENSE           |  202 +
 .../Eclipse/Formatter-profile-FreeMarker.xml    |  313 ++
 .../Editor-Inspections-FreeMarker.xml           |   33 +
 .../Java-code-style-FreeMarker.xml              |   66 +
 .../core/APINotSupportedTemplateException.java  |   49 +
 .../org/apache/freemarker/core/ASTComment.java  |   87 +
 .../apache/freemarker/core/ASTDebugBreak.java   |   89 +
 .../freemarker/core/ASTDirAssignment.java       |  279 ++
 .../core/ASTDirAssignmentsContainer.java        |  115 +
 .../core/ASTDirAttemptRecoverContainer.java     |   88 +
 .../apache/freemarker/core/ASTDirAutoEsc.java   |   77 +
 .../org/apache/freemarker/core/ASTDirBreak.java |   70 +
 .../core/ASTDirCapturingAssignment.java         |  184 +
 .../org/apache/freemarker/core/ASTDirCase.java  |   91 +
 .../apache/freemarker/core/ASTDirCompress.java  |   87 +
 .../freemarker/core/ASTDirElseOfList.java       |   75 +
 .../apache/freemarker/core/ASTDirEscape.java    |  111 +
 .../apache/freemarker/core/ASTDirFallback.java  |   70 +
 .../org/apache/freemarker/core/ASTDirFlush.java |   65 +
 .../core/ASTDirIfElseIfElseContainer.java       |  107 +
 .../freemarker/core/ASTDirIfOrElseOrElseIf.java |  114 +
 .../apache/freemarker/core/ASTDirImport.java    |  125 +
 .../apache/freemarker/core/ASTDirInclude.java   |  174 +
 .../org/apache/freemarker/core/ASTDirItems.java |  120 +
 .../org/apache/freemarker/core/ASTDirList.java  |  462 ++
 .../core/ASTDirListElseContainer.java           |   88 +
 .../org/apache/freemarker/core/ASTDirMacro.java |  325 ++
 .../apache/freemarker/core/ASTDirNested.java    |  159 +
 .../apache/freemarker/core/ASTDirNoAutoEsc.java |   77 +
 .../apache/freemarker/core/ASTDirNoEscape.java  |   78 +
 .../freemarker/core/ASTDirOutputFormat.java     |   85 +
 .../apache/freemarker/core/ASTDirRecover.java   |   75 +
 .../apache/freemarker/core/ASTDirRecurse.java   |  130 +
 .../apache/freemarker/core/ASTDirReturn.java    |   91 +
 .../org/apache/freemarker/core/ASTDirSep.java   |   89 +
 .../apache/freemarker/core/ASTDirSetting.java   |  172 +
 .../org/apache/freemarker/core/ASTDirStop.java  |   81 +
 .../apache/freemarker/core/ASTDirSwitch.java    |  129 +
 .../apache/freemarker/core/ASTDirTOrTrOrTl.java |  109 +
 .../freemarker/core/ASTDirUserDefined.java      |  343 ++
 .../org/apache/freemarker/core/ASTDirVisit.java |  126 +
 .../apache/freemarker/core/ASTDirective.java    |   98 +
 .../freemarker/core/ASTDollarInterpolation.java |  151 +
 .../org/apache/freemarker/core/ASTElement.java  |  445 ++
 .../freemarker/core/ASTExpAddOrConcat.java      |  313 ++
 .../org/apache/freemarker/core/ASTExpAnd.java   |   82 +
 .../apache/freemarker/core/ASTExpBoolean.java   |   34 +
 .../freemarker/core/ASTExpBooleanLiteral.java   |   91 +
 .../apache/freemarker/core/ASTExpBuiltIn.java   |  485 ++
 .../freemarker/core/ASTExpBuiltInVariable.java  |  298 ++
 .../freemarker/core/ASTExpComparison.java       |  104 +
 .../apache/freemarker/core/ASTExpDefault.java   |  142 +
 .../org/apache/freemarker/core/ASTExpDot.java   |   92 +
 .../freemarker/core/ASTExpDynamicKeyName.java   |  284 ++
 .../apache/freemarker/core/ASTExpExists.java    |   91 +
 .../freemarker/core/ASTExpHashLiteral.java      |  220 +
 .../freemarker/core/ASTExpListLiteral.java      |  195 +
 .../freemarker/core/ASTExpMethodCall.java       |  147 +
 .../freemarker/core/ASTExpNegateOrPlus.java     |  110 +
 .../org/apache/freemarker/core/ASTExpNot.java   |   76 +
 .../freemarker/core/ASTExpNumberLiteral.java    |   92 +
 .../org/apache/freemarker/core/ASTExpOr.java    |   82 +
 .../freemarker/core/ASTExpParenthesis.java      |   88 +
 .../org/apache/freemarker/core/ASTExpRange.java |  119 +
 .../freemarker/core/ASTExpStringLiteral.java    |  211 +
 .../apache/freemarker/core/ASTExpVariable.java  |  105 +
 .../apache/freemarker/core/ASTExpression.java   |  208 +
 .../freemarker/core/ASTHashInterpolation.java   |  172 +
 .../freemarker/core/ASTImplicitParent.java      |  101 +
 .../freemarker/core/ASTInterpolation.java       |   51 +
 .../org/apache/freemarker/core/ASTNode.java     |  233 +
 .../apache/freemarker/core/ASTStaticText.java   |  408 ++
 .../freemarker/core/ArithmeticExpression.java   |  129 +
 .../freemarker/core/BoundedRangeModel.java      |   70 +
 .../core/BuiltInBannedWhenAutoEscaping.java     |   27 +
 .../apache/freemarker/core/BuiltInForDate.java  |   56 +
 .../freemarker/core/BuiltInForHashEx.java       |   55 +
 .../core/BuiltInForLegacyEscaping.java          |   48 +
 .../freemarker/core/BuiltInForLoopVariable.java |   48 +
 .../freemarker/core/BuiltInForMarkupOutput.java |   40 +
 .../apache/freemarker/core/BuiltInForNode.java  |   39 +
 .../freemarker/core/BuiltInForNodeEx.java       |   37 +
 .../freemarker/core/BuiltInForNumber.java       |   35 +
 .../freemarker/core/BuiltInForSequence.java     |   38 +
 .../freemarker/core/BuiltInForString.java       |   36 +
 .../core/BuiltInWithParseTimeParameters.java    |  109 +
 .../freemarker/core/BuiltInsForDates.java       |  212 +
 .../core/BuiltInsForExistenceHandling.java      |  133 +
 .../freemarker/core/BuiltInsForHashes.java      |   59 +
 .../core/BuiltInsForLoopVariables.java          |  156 +
 .../core/BuiltInsForMarkupOutputs.java          |   41 +
 .../core/BuiltInsForMultipleTypes.java          |  717 +++
 .../freemarker/core/BuiltInsForNodes.java       |  154 +
 .../freemarker/core/BuiltInsForNumbers.java     |  319 ++
 .../core/BuiltInsForOutputFormatRelated.java    |   84 +
 .../freemarker/core/BuiltInsForSequences.java   |  871 ++++
 .../core/BuiltInsForStringsBasic.java           |  697 +++
 .../core/BuiltInsForStringsEncoding.java        |  195 +
 .../freemarker/core/BuiltInsForStringsMisc.java |  305 ++
 .../core/BuiltInsForStringsRegexp.java          |  322 ++
 .../core/BuiltInsWithParseTimeParameters.java   |  157 +
 ...lPlaceCustomDataInitializationException.java |   33 +
 .../apache/freemarker/core/Configuration.java   | 2616 +++++++++++
 .../freemarker/core/ConfigurationException.java |   37 +
 .../ConfigurationSettingValueException.java     |   86 +
 .../apache/freemarker/core/CustomStateKey.java  |   60 +
 .../freemarker/core/CustomStateScope.java       |   34 +
 .../freemarker/core/DirectiveCallPlace.java     |  137 +
 .../org/apache/freemarker/core/Environment.java | 3213 ++++++++++++++
 .../core/InvalidReferenceException.java         |  167 +
 .../core/ListableRightUnboundedRangeModel.java  |   97 +
 .../apache/freemarker/core/LocalContext.java    |   36 +
 .../freemarker/core/LocalContextStack.java      |   57 +
 .../core/MarkupOutputFormatBoundBuiltIn.java    |   46 +
 .../org/apache/freemarker/core/MessageUtil.java |  341 ++
 .../org/apache/freemarker/core/MiscUtil.java    |   69 +
 ...utableParsingAndProcessingConfiguration.java |  475 ++
 .../core/MutableProcessingConfiguration.java    | 2418 ++++++++++
 .../freemarker/core/NativeCollectionEx.java     |   73 +
 .../apache/freemarker/core/NativeHashEx2.java   |  106 +
 .../apache/freemarker/core/NativeSequence.java  |   74 +
 .../core/NativeStringArraySequence.java         |   53 +
 .../NativeStringCollectionCollectionEx.java     |   79 +
 .../core/NativeStringListSequence.java          |   56 +
 .../NestedContentNotSupportedException.java     |   67 +
 .../freemarker/core/NonBooleanException.java    |   62 +
 .../freemarker/core/NonDateException.java       |   58 +
 .../core/NonExtendedHashException.java          |   62 +
 .../core/NonExtendedNodeException.java          |   64 +
 .../freemarker/core/NonHashException.java       |   64 +
 .../core/NonMarkupOutputException.java          |   64 +
 .../freemarker/core/NonMethodException.java     |   64 +
 .../freemarker/core/NonNamespaceException.java  |   63 +
 .../freemarker/core/NonNodeException.java       |   64 +
 .../freemarker/core/NonNumericalException.java  |   74 +
 .../freemarker/core/NonSequenceException.java   |   64 +
 .../core/NonSequenceOrCollectionException.java  |   92 +
 .../freemarker/core/NonStringException.java     |   74 +
 .../NonStringOrTemplateOutputException.java     |   78 +
 .../NonUserDefinedDirectiveLikeException.java   |   67 +
 .../core/OutputFormatBoundBuiltIn.java          |   48 +
 .../apache/freemarker/core/ParameterRole.java   |   91 +
 .../apache/freemarker/core/ParseException.java  |  518 +++
 .../core/ParsingAndProcessingConfiguration.java |   29 +
 .../freemarker/core/ParsingConfiguration.java   |  299 ++
 .../core/ProcessingConfiguration.java           |  704 +++
 .../org/apache/freemarker/core/RangeModel.java  |   59 +
 .../apache/freemarker/core/RegexpHelper.java    |  207 +
 .../core/RightUnboundedRangeModel.java          |   48 +
 .../core/SettingValueNotSetException.java       |   33 +
 .../apache/freemarker/core/SpecialBuiltIn.java  |   27 +
 .../apache/freemarker/core/StopException.java   |   64 +
 .../org/apache/freemarker/core/Template.java    | 1341 ++++++
 .../freemarker/core/TemplateBooleanFormat.java  |   91 +
 .../freemarker/core/TemplateClassResolver.java  |   82 +
 .../freemarker/core/TemplateConfiguration.java  |  991 +++++
 .../core/TemplateElementArrayBuilder.java       |  102 +
 .../core/TemplateElementsToVisit.java           |   48 +
 .../freemarker/core/TemplateException.java      |  655 +++
 .../core/TemplateExceptionHandler.java          |  156 +
 .../freemarker/core/TemplateLanguage.java       |  111 +
 .../core/TemplateNotFoundException.java         |   64 +
 ...emplateParsingConfigurationWithFallback.java |  146 +
 .../freemarker/core/TemplatePostProcessor.java  |   31 +
 .../core/TemplatePostProcessorException.java    |   35 +
 ...nterruptionSupportTemplatePostProcessor.java |  140 +
 .../apache/freemarker/core/TokenMgrError.java   |  249 ++
 .../freemarker/core/TopLevelConfiguration.java  |  194 +
 .../core/UnexpectedTypeException.java           |  109 +
 .../UnknownConfigurationSettingException.java   |   40 +
 .../org/apache/freemarker/core/Version.java     |  297 ++
 .../core/WrongTemplateCharsetException.java     |   63 +
 .../apache/freemarker/core/_CharsetBuilder.java |   41 +
 .../org/apache/freemarker/core/_CoreAPI.java    |   88 +
 .../org/apache/freemarker/core/_CoreLogs.java   |   46 +
 .../java/org/apache/freemarker/core/_Debug.java |  122 +
 .../apache/freemarker/core/_DelayedAOrAn.java   |   35 +
 .../core/_DelayedConversionToString.java        |   52 +
 .../core/_DelayedFTLTypeDescription.java        |   37 +
 .../core/_DelayedGetCanonicalForm.java          |   39 +
 .../freemarker/core/_DelayedGetMessage.java     |   35 +
 .../core/_DelayedGetMessageWithoutStackTop.java |   34 +
 .../apache/freemarker/core/_DelayedJQuote.java  |   36 +
 .../freemarker/core/_DelayedJoinWithComma.java  |   48 +
 .../apache/freemarker/core/_DelayedOrdinal.java |   47 +
 .../freemarker/core/_DelayedShortClassName.java |   35 +
 .../freemarker/core/_DelayedToString.java       |   37 +
 .../core/_ErrorDescriptionBuilder.java          |  356 ++
 .../org/apache/freemarker/core/_EvalUtil.java   |  545 +++
 .../java/org/apache/freemarker/core/_Java8.java |   34 +
 .../org/apache/freemarker/core/_Java8Impl.java  |   54 +
 .../freemarker/core/_MiscTemplateException.java |  124 +
 ...ObjectBuilderSettingEvaluationException.java |   46 +
 .../core/_ObjectBuilderSettingEvaluator.java    | 1068 +++++
 .../core/_SettingEvaluationEnvironment.java     |   61 +
 .../core/_TemplateModelException.java           |  133 +
 .../freemarker/core/_TimeZoneBuilder.java       |   43 +
 ...expectedTypeErrorExplainerTemplateModel.java |   36 +
 .../core/arithmetic/ArithmeticEngine.java       |   92 +
 .../impl/BigDecimalArithmeticEngine.java        |  107 +
 .../impl/ConservativeArithmeticEngine.java      |  381 ++
 .../core/arithmetic/impl/package.html           |   26 +
 .../freemarker/core/arithmetic/package.html     |   25 +
 .../freemarker/core/debug/Breakpoint.java       |   83 +
 .../freemarker/core/debug/DebugModel.java       |  105 +
 .../core/debug/DebuggedEnvironment.java         |   58 +
 .../apache/freemarker/core/debug/Debugger.java  |   95 +
 .../freemarker/core/debug/DebuggerClient.java   |  149 +
 .../freemarker/core/debug/DebuggerListener.java |   36 +
 .../freemarker/core/debug/DebuggerServer.java   |  131 +
 .../core/debug/EnvironmentSuspendedEvent.java   |   67 +
 .../core/debug/RmiDebugModelImpl.java           |  164 +
 .../core/debug/RmiDebuggedEnvironmentImpl.java  |  340 ++
 .../freemarker/core/debug/RmiDebuggerImpl.java  |   86 +
 .../core/debug/RmiDebuggerListenerImpl.java     |   67 +
 .../core/debug/RmiDebuggerService.java          |  307 ++
 .../apache/freemarker/core/debug/SoftCache.java |   89 +
 .../freemarker/core/debug/_DebuggerService.java |   93 +
 .../apache/freemarker/core/debug/package.html   |   27 +
 .../core/model/AdapterTemplateModel.java        |   49 +
 .../apache/freemarker/core/model/Constants.java |  133 +
 .../core/model/FalseTemplateBooleanModel.java   |   36 +
 .../core/model/GeneralPurposeNothing.java       |   83 +
 .../freemarker/core/model/ObjectWrapper.java    |   59 +
 .../core/model/ObjectWrapperAndUnwrapper.java   |   90 +
 .../core/model/ObjectWrapperWithAPISupport.java |   46 +
 .../core/model/RichObjectWrapper.java           |   34 +
 .../model/SerializableTemplateBooleanModel.java |   24 +
 .../core/model/TemplateBooleanModel.java        |   48 +
 .../core/model/TemplateCollectionModel.java     |   48 +
 .../core/model/TemplateCollectionModelEx.java   |   45 +
 .../core/model/TemplateDateModel.java           |   73 +
 .../core/model/TemplateDirectiveBody.java       |   45 +
 .../core/model/TemplateDirectiveModel.java      |   69 +
 .../core/model/TemplateHashModel.java           |   41 +
 .../core/model/TemplateHashModelEx.java         |   51 +
 .../core/model/TemplateHashModelEx2.java        |   80 +
 .../core/model/TemplateMarkupOutputModel.java   |   52 +
 .../core/model/TemplateMethodModel.java         |   60 +
 .../core/model/TemplateMethodModelEx.java       |   54 +
 .../freemarker/core/model/TemplateModel.java    |   55 +
 .../core/model/TemplateModelAdapter.java        |   34 +
 .../core/model/TemplateModelException.java      |  111 +
 .../core/model/TemplateModelIterator.java       |   39 +
 .../core/model/TemplateModelWithAPISupport.java |   39 +
 .../core/model/TemplateNodeModel.java           |   78 +
 .../core/model/TemplateNodeModelEx.java         |   40 +
 .../core/model/TemplateNumberModel.java         |   42 +
 .../core/model/TemplateScalarModel.java         |   45 +
 .../core/model/TemplateSequenceModel.java       |   48 +
 .../core/model/TemplateTransformModel.java      |   54 +
 .../freemarker/core/model/TransformControl.java |  101 +
 .../core/model/TrueTemplateBooleanModel.java    |   36 +
 .../core/model/WrapperTemplateModel.java        |   33 +
 .../core/model/WrappingTemplateModel.java       |   62 +
 .../freemarker/core/model/impl/APIModel.java    |   45 +
 .../core/model/impl/ArgumentTypes.java          |  647 +++
 .../core/model/impl/BeanAndStringModel.java     |   53 +
 .../freemarker/core/model/impl/BeanModel.java   |  339 ++
 .../model/impl/CallableMemberDescriptor.java    |   56 +
 .../core/model/impl/CharacterOrString.java      |   45 +
 .../core/model/impl/ClassBasedModelFactory.java |  148 +
 .../core/model/impl/ClassChangeNotifier.java    |   32 +
 .../core/model/impl/ClassIntrospector.java      | 1263 ++++++
 .../core/model/impl/CollectionAdapter.java      |   88 +
 .../core/model/impl/CollectionAndSequence.java  |  111 +
 .../core/model/impl/DefaultArrayAdapter.java    |  378 ++
 .../model/impl/DefaultEnumerationAdapter.java   |  128 +
 .../core/model/impl/DefaultIterableAdapter.java |   94 +
 .../core/model/impl/DefaultIteratorAdapter.java |  138 +
 .../core/model/impl/DefaultListAdapter.java     |  123 +
 .../core/model/impl/DefaultMapAdapter.java      |  171 +
 .../impl/DefaultNonListCollectionAdapter.java   |  103 +
 .../core/model/impl/DefaultObjectWrapper.java   | 1773 ++++++++
 .../DefaultObjectWrapperTCCLSingletonUtil.java  |  129 +
 .../DefaultUnassignableIteratorAdapter.java     |   59 +
 .../impl/EmptyCallableMemberDescriptor.java     |   35 +
 .../model/impl/EmptyMemberAndArguments.java     |   93 +
 .../freemarker/core/model/impl/EnumModels.java  |   50 +
 .../freemarker/core/model/impl/HashAdapter.java |  181 +
 .../model/impl/InvalidPropertyException.java    |   34 +
 .../model/impl/JRebelClassChangeNotifier.java   |   58 +
 .../core/model/impl/JavaMethodModel.java        |  105 +
 .../model/impl/MapKeyValuePairIterator.java     |   77 +
 .../MaybeEmptyCallableMemberDescriptor.java     |   25 +
 .../impl/MaybeEmptyMemberAndArguments.java      |   22 +
 .../core/model/impl/MemberAndArguments.java     |   64 +
 .../model/impl/MethodAppearanceFineTuner.java   |  156 +
 .../core/model/impl/MethodSorter.java           |   36 +
 .../NonPrimitiveArrayBackedReadOnlyList.java    |   42 +
 .../model/impl/OverloadedFixArgsMethods.java    |   99 +
 .../core/model/impl/OverloadedMethods.java      |  271 ++
 .../core/model/impl/OverloadedMethodsModel.java |   65 +
 .../model/impl/OverloadedMethodsSubset.java     |  402 ++
 .../core/model/impl/OverloadedNumberUtil.java   | 1289 ++++++
 .../model/impl/OverloadedVarArgsMethods.java    |  245 ++
 .../impl/PrimtiveArrayBackedReadOnlyList.java   |   47 +
 .../ReflectionCallableMemberDescriptor.java     |   95 +
 .../core/model/impl/ResourceBundleModel.java    |  181 +
 .../model/impl/RestrictedObjectWrapper.java     |   98 +
 .../core/model/impl/SequenceAdapter.java        |   68 +
 .../freemarker/core/model/impl/SetAdapter.java  |   32 +
 .../core/model/impl/SimpleCollection.java       |  138 +
 .../freemarker/core/model/impl/SimpleDate.java  |   85 +
 .../freemarker/core/model/impl/SimpleHash.java  |  296 ++
 .../core/model/impl/SimpleMethod.java           |  174 +
 .../core/model/impl/SimpleNumber.java           |   77 +
 .../core/model/impl/SimpleScalar.java           |   73 +
 .../core/model/impl/SimpleSequence.java         |  162 +
 .../core/model/impl/SingletonCustomizer.java    |   51 +
 .../freemarker/core/model/impl/StaticModel.java |  177 +
 .../core/model/impl/StaticModels.java           |   43 +
 .../model/impl/TemplateModelListSequence.java   |   58 +
 .../freemarker/core/model/impl/TypeFlags.java   |  130 +
 .../core/model/impl/UnsafeMethods.java          |  112 +
 .../freemarker/core/model/impl/_MethodUtil.java |  319 ++
 .../freemarker/core/model/impl/_ModelAPI.java   |  122 +
 .../freemarker/core/model/impl/package.html     |   26 +
 .../apache/freemarker/core/model/package.html   |   25 +
 .../outputformat/CommonMarkupOutputFormat.java  |  124 +
 .../CommonTemplateMarkupOutputModel.java        |   69 +
 .../core/outputformat/MarkupOutputFormat.java   |  135 +
 .../core/outputformat/OutputFormat.java         |   86 +
 .../UnregisteredOutputFormatException.java      |   39 +
 .../core/outputformat/impl/CSSOutputFormat.java |   54 +
 .../impl/CombinedMarkupOutputFormat.java        |  108 +
 .../outputformat/impl/HTMLOutputFormat.java     |   77 +
 .../outputformat/impl/JSONOutputFormat.java     |   54 +
 .../impl/JavaScriptOutputFormat.java            |   55 +
 .../impl/PlainTextOutputFormat.java             |   58 +
 .../core/outputformat/impl/RTFOutputFormat.java |   77 +
 .../impl/TemplateCombinedMarkupOutputModel.java |   52 +
 .../impl/TemplateHTMLOutputModel.java           |   42 +
 .../impl/TemplateRTFOutputModel.java            |   42 +
 .../impl/TemplateXHTMLOutputModel.java          |   42 +
 .../impl/TemplateXMLOutputModel.java            |   42 +
 .../impl/UndefinedOutputFormat.java             |   58 +
 .../outputformat/impl/XHTMLOutputFormat.java    |   77 +
 .../core/outputformat/impl/XMLOutputFormat.java |   77 +
 .../core/outputformat/impl/package.html         |   26 +
 .../freemarker/core/outputformat/package.html   |   25 +
 .../org/apache/freemarker/core/package.html     |   27 +
 .../core/templateresolver/AndMatcher.java       |   45 +
 .../core/templateresolver/CacheStorage.java     |   37 +
 .../CacheStorageWithGetSize.java                |   36 +
 ...ConditionalTemplateConfigurationFactory.java |   65 +
 .../templateresolver/FileExtensionMatcher.java  |   85 +
 .../templateresolver/FileNameGlobMatcher.java   |   86 +
 .../FirstMatchTemplateConfigurationFactory.java |  110 +
 .../templateresolver/GetTemplateResult.java     |   89 +
 .../MalformedTemplateNameException.java         |   60 +
 .../MergingTemplateConfigurationFactory.java    |   63 +
 .../core/templateresolver/NotMatcher.java       |   41 +
 .../core/templateresolver/OrMatcher.java        |   45 +
 .../core/templateresolver/PathGlobMatcher.java  |  100 +
 .../core/templateresolver/PathRegexMatcher.java |   54 +
 .../TemplateConfigurationFactory.java           |   54 +
 .../TemplateConfigurationFactoryException.java  |   36 +
 .../core/templateresolver/TemplateLoader.java   |  104 +
 .../templateresolver/TemplateLoaderSession.java |   76 +
 .../templateresolver/TemplateLoadingResult.java |  208 +
 .../TemplateLoadingResultStatus.java            |   49 +
 .../templateresolver/TemplateLoadingSource.java |   69 +
 .../templateresolver/TemplateLookupContext.java |  112 +
 .../templateresolver/TemplateLookupResult.java  |   54 +
 .../TemplateLookupStrategy.java                 |   78 +
 .../templateresolver/TemplateNameFormat.java    |   53 +
 .../core/templateresolver/TemplateResolver.java |  166 +
 .../templateresolver/TemplateSourceMatcher.java |   30 +
 .../core/templateresolver/_CacheAPI.java        |   43 +
 .../impl/ByteArrayTemplateLoader.java           |  199 +
 .../impl/ClassTemplateLoader.java               |  184 +
 .../impl/DefaultTemplateLookupStrategy.java     |   61 +
 .../impl/DefaultTemplateNameFormat.java         |  309 ++
 .../impl/DefaultTemplateNameFormatFM2.java      |  105 +
 .../impl/DefaultTemplateResolver.java           |  904 ++++
 .../impl/FileTemplateLoader.java                |  383 ++
 .../templateresolver/impl/MruCacheStorage.java  |  330 ++
 .../impl/MultiTemplateLoader.java               |  172 +
 .../templateresolver/impl/NullCacheStorage.java |   71 +
 .../templateresolver/impl/SoftCacheStorage.java |  112 +
 .../impl/StringTemplateLoader.java              |  199 +
 .../impl/StrongCacheStorage.java                |   70 +
 ...emplateLoaderBasedTemplateLookupContext.java |   66 +
 ...TemplateLoaderBasedTemplateLookupResult.java |  124 +
 .../impl/URLTemplateLoader.java                 |  229 +
 .../impl/URLTemplateLoadingSource.java          |   58 +
 .../impl/_TemplateLoaderUtils.java              |   43 +
 .../core/templateresolver/impl/package.html     |   26 +
 .../core/templateresolver/package.html          |   25 +
 .../freemarker/core/util/BugException.java      |   52 +
 .../freemarker/core/util/CaptureOutput.java     |  147 +
 .../freemarker/core/util/CommonBuilder.java     |   35 +
 .../apache/freemarker/core/util/DeepUnwrap.java |  153 +
 .../apache/freemarker/core/util/FTLUtil.java    |  805 ++++
 .../core/util/GenericParseException.java        |   40 +
 .../apache/freemarker/core/util/HtmlEscape.java |  109 +
 .../freemarker/core/util/NormalizeNewlines.java |  115 +
 .../freemarker/core/util/ObjectFactory.java     |   31 +
 .../core/util/OptInTemplateClassResolver.java   |  160 +
 .../core/util/ProductWrappingBuilder.java       |   38 +
 .../freemarker/core/util/StandardCompress.java  |  239 +
 .../core/util/UndeclaredThrowableException.java |   43 +
 .../util/UnrecognizedTimeZoneException.java     |   38 +
 .../util/UnsupportedNumberClassException.java   |   38 +
 .../apache/freemarker/core/util/XmlEscape.java  |   92 +
 .../freemarker/core/util/_ArrayEnumeration.java |   51 +
 .../freemarker/core/util/_ArrayIterator.java    |   54 +
 .../apache/freemarker/core/util/_ClassUtil.java |  182 +
 .../freemarker/core/util/_CollectionUtil.java   |   36 +
 .../apache/freemarker/core/util/_DateUtil.java  |  914 ++++
 .../freemarker/core/util/_JavaVersions.java     |   80 +
 .../freemarker/core/util/_KeyValuePair.java     |   61 +
 .../freemarker/core/util/_LocaleUtil.java       |   43 +
 .../core/util/_NullArgumentException.java       |   59 +
 .../freemarker/core/util/_NullWriter.java       |   90 +
 .../freemarker/core/util/_NumberUtil.java       |  228 +
 .../freemarker/core/util/_ObjectHolder.java     |   55 +
 .../freemarker/core/util/_SecurityUtil.java     |   87 +
 .../freemarker/core/util/_SortedArraySet.java   |   80 +
 .../freemarker/core/util/_StringUtil.java       | 1675 +++++++
 .../core/util/_UnmodifiableCompositeSet.java    |   98 +
 .../freemarker/core/util/_UnmodifiableSet.java  |   47 +
 .../apache/freemarker/core/util/package.html    |   25 +
 .../InvalidFormatParametersException.java       |   37 +
 .../InvalidFormatStringException.java           |   37 +
 .../ParsingNotSupportedException.java           |   37 +
 .../core/valueformat/TemplateDateFormat.java    |  110 +
 .../valueformat/TemplateDateFormatFactory.java  |   95 +
 .../core/valueformat/TemplateFormatUtil.java    |   77 +
 .../core/valueformat/TemplateNumberFormat.java  |   93 +
 .../TemplateNumberFormatFactory.java            |   67 +
 .../core/valueformat/TemplateValueFormat.java   |   42 +
 .../TemplateValueFormatException.java           |   37 +
 .../valueformat/TemplateValueFormatFactory.java |   28 +
 .../UndefinedCustomFormatException.java         |   34 +
 .../UnformattableValueException.java            |   41 +
 ...nDateTypeFormattingUnsupportedException.java |   36 +
 ...nownDateTypeParsingUnsupportedException.java |   37 +
 .../valueformat/UnparsableValueException.java   |   38 +
 ...AliasTargetTemplateValueFormatException.java |   38 +
 .../impl/AliasTemplateDateFormatFactory.java    |   97 +
 .../impl/AliasTemplateNumberFormatFactory.java  |   96 +
 .../impl/ExtendedDecimalFormatParser.java       |  530 +++
 .../impl/ISOLikeTemplateDateFormat.java         |  270 ++
 .../impl/ISOLikeTemplateDateFormatFactory.java  |   57 +
 .../valueformat/impl/ISOTemplateDateFormat.java |   90 +
 .../impl/ISOTemplateDateFormatFactory.java      |   56 +
 .../impl/JavaTemplateDateFormat.java            |   75 +
 .../impl/JavaTemplateDateFormatFactory.java     |  187 +
 .../impl/JavaTemplateNumberFormat.java          |   64 +
 .../impl/JavaTemplateNumberFormatFactory.java   |  133 +
 .../valueformat/impl/XSTemplateDateFormat.java  |   94 +
 .../impl/XSTemplateDateFormatFactory.java       |   51 +
 .../core/valueformat/impl/package.html          |   26 +
 .../freemarker/core/valueformat/package.html    |   25 +
 .../java/org/apache/freemarker/dom/AtAtKey.java |   58 +
 .../freemarker/dom/AttributeNodeModel.java      |   69 +
 .../freemarker/dom/CharacterDataNodeModel.java  |   46 +
 .../apache/freemarker/dom/DocumentModel.java    |   76 +
 .../freemarker/dom/DocumentTypeModel.java       |   56 +
 .../java/org/apache/freemarker/dom/DomLog.java  |   32 +
 .../apache/freemarker/dom/DomStringUtil.java    |   67 +
 .../org/apache/freemarker/dom/ElementModel.java |  234 +
 .../freemarker/dom/JaxenXPathSupport.java       |  243 +
 .../apache/freemarker/dom/NodeListModel.java    |  219 +
 .../org/apache/freemarker/dom/NodeModel.java    |  613 +++
 .../apache/freemarker/dom/NodeOutputter.java    |  258 ++
 .../dom/NodeQueryResultItemObjectWrapper.java   |   92 +
 .../org/apache/freemarker/dom/PINodeModel.java  |   45 +
 .../dom/SunInternalXalanXPathSupport.java       |  163 +
 .../org/apache/freemarker/dom/XPathSupport.java |   30 +
 .../freemarker/dom/XalanXPathSupport.java       |  163 +
 .../java/org/apache/freemarker/dom/package.html |   30 +
 freemarker-core/src/main/javacc/FTL.jj          | 4132 ++++++++++++++++++
 .../adhoc/IdentifierCharGenerator.java          |  546 +++
 .../main/misc/overloadedNumberRules/README.txt  |   34 +
 .../main/misc/overloadedNumberRules/config.fmpp |   73 +
 .../misc/overloadedNumberRules/generator.ftl    |   80 +
 .../main/misc/overloadedNumberRules/prices.ods  |  Bin 0 -> 17855 bytes
 .../src/main/resources/META-INF/DISCLAIMER      |    8 +
 .../src/main/resources/META-INF/LICENSE         |  202 +
 .../core/model/impl/unsafeMethods.properties    |   98 +
 .../apache/freemarker/core/version.properties   |  100 +
 .../src/manual/en_US/FM3-CHANGE-LOG.txt         |  226 +
 freemarker-core/src/manual/en_US/book.xml       |   82 +
 .../manual/en_US/docgen-help/editors-readme.txt |  130 +
 .../en_US/docgen-misc/copyrightComment.txt      |   16 +
 .../en_US/docgen-misc/googleAnalytics.html      |   14 +
 .../figures/model2sketch_with_alpha.png         |  Bin 0 -> 61463 bytes
 .../figures/odg-convert-howto.txt               |   43 +
 .../en_US/docgen-originals/figures/overview.odg |  Bin 0 -> 11939 bytes
 .../figures/tree_with_alpha.png                 |  Bin 0 -> 10304 bytes
 freemarker-core/src/manual/en_US/docgen.cjson   |  132 +
 freemarker-core/src/manual/en_US/favicon.png    |  Bin 0 -> 1291 bytes
 .../src/manual/en_US/figures/model2sketch.png   |  Bin 0 -> 21425 bytes
 .../src/manual/en_US/figures/overview.png       |  Bin 0 -> 11837 bytes
 .../src/manual/en_US/figures/tree.png           |  Bin 0 -> 4699 bytes
 freemarker-core/src/manual/en_US/logo.png       |  Bin 0 -> 10134 bytes
 freemarker-core/src/manual/zh_CN/book.xml       |   82 +
 .../src/manual/zh_CN/docgen-help/README         |    2 +
 .../zh_CN/docgen-misc/googleAnalytics.html      |   14 +
 .../zh_CN/docgen-originals/figures/README       |    2 +
 freemarker-core/src/manual/zh_CN/docgen.cjson   |  130 +
 freemarker-core/src/manual/zh_CN/favicon.png    |  Bin 0 -> 1291 bytes
 .../src/manual/zh_CN/figures/model2sketch.png   |  Bin 0 -> 21425 bytes
 .../src/manual/zh_CN/figures/overview.png       |  Bin 0 -> 11837 bytes
 .../src/manual/zh_CN/figures/tree.png           |  Bin 0 -> 4699 bytes
 freemarker-core/src/manual/zh_CN/logo.png       |  Bin 0 -> 10134 bytes
 .../core/ASTBasedErrorMessagesTest.java         |   74 +
 .../org/apache/freemarker/core/ASTPrinter.java  |  438 ++
 .../org/apache/freemarker/core/ASTTest.java     |  103 +
 .../core/ActualNamingConvetionTest.java         |   66 +
 .../freemarker/core/ActualTagSyntaxTest.java    |   68 +
 .../freemarker/core/BreakPlacementTest.java     |   56 +
 .../apache/freemarker/core/CamelCaseTest.java   |  486 ++
 .../freemarker/core/CanonicalFormTest.java      |   68 +
 .../freemarker/core/CoercionToTextualTest.java  |  149 +
 .../freemarker/core/ConfigurableTest.java       |  176 +
 .../freemarker/core/ConfigurationTest.java      | 1486 +++++++
 .../freemarker/core/CoreLocaleUtilsTest.java    |   73 +
 .../freemarker/core/CustomAttributeTest.java    |  163 +
 .../apache/freemarker/core/DateFormatTest.java  |  464 ++
 .../freemarker/core/DirectiveCallPlaceTest.java |  249 ++
 .../freemarker/core/EncodingOverrideTest.java   |   62 +
 .../EnvironmentGetTemplateVariantsTest.java     |  214 +
 .../apache/freemarker/core/ExceptionTest.java   |  115 +
 .../apache/freemarker/core/GetSourceTest.java   |   52 +
 .../freemarker/core/HeaderParsingTest.java      |   60 +
 .../IncludeAndImportConfigurableLayersTest.java |  354 ++
 .../freemarker/core/IncludeAndImportTest.java   |  270 ++
 .../freemarker/core/IncudeFromNamelessTest.java |   58 +
 .../core/InterpretAndEvalTemplateNameTest.java  |   70 +
 .../core/InterpretSettingInheritanceTest.java   |  104 +
 .../freemarker/core/IteratorIssuesTest.java     |   64 +
 .../core/JavaCCExceptionAsEOFFixTest.java       |  126 +
 .../apache/freemarker/core/ListErrorsTest.java  |  130 +
 .../freemarker/core/MiscErrorMessagesTest.java  |   48 +
 .../core/MistakenlyPublicImportAPIsTest.java    |  104 +
 .../core/MistakenlyPublicMacroAPIsTest.java     |   88 +
 .../core/NewBiObjectWrapperRestrictionTest.java |   50 +
 .../core/ObjectBuilderSettingsTest.java         | 1499 +++++++
 .../core/OptInTemplateClassResolverTest.java    |  230 +
 .../freemarker/core/OutputFormatTest.java       | 1068 +++++
 .../ParseTimeParameterBIErrorMessagesTest.java  |   46 +
 .../core/ParsingErrorMessagesTest.java          |  116 +
 .../core/RestrictedObjectWrapperTest.java       |   72 +
 .../core/RestrictedObjetWrapperTest.java        |  112 +
 .../apache/freemarker/core/SQLTimeZoneTest.java |  371 ++
 .../freemarker/core/SettingDirectiveTest.java   |   40 +
 .../freemarker/core/SpecialVariableTest.java    |  114 +
 .../core/StringLiteralInterpolationTest.java    |  135 +
 .../org/apache/freemarker/core/TabSizeTest.java |   91 +
 .../core/TagSyntaxVariationsTest.java           |  186 +
 .../core/TemplateConfigurationTest.java         |  909 ++++
 ...gurationWithDefaultTemplateResolverTest.java |  267 ++
 .../core/TemplateConstructorsTest.java          |  113 +
 .../core/TemplateGetEncodingTest.java           |   64 +
 .../core/TemplateLookupStrategyTest.java        |  669 +++
 .../core/TemplateNameSpecialVariablesTest.java  |  159 +
 .../core/TemplateNotFoundMessageTest.java       |  207 +
 .../core/TheadInterruptingSupportTest.java      |  163 +
 .../freemarker/core/TypeErrorMessagesTest.java  |  105 +
 .../freemarker/core/UnclosedCommentTest.java    |   41 +
 .../org/apache/freemarker/core/VersionTest.java |  227 +
 .../core/WhitespaceStrippingTest.java           |   63 +
 .../freemarker/core/XHTMLOutputFormatTest.java  |   59 +
 .../freemarker/core/XMLOutputFormatTest.java    |   59 +
 .../impl/AbstractParallelIntrospectionTest.java |  126 +
 .../model/impl/AlphabeticalMethodSorter.java    |   45 +
 .../CommonSupertypeForUnwrappingHintTest.java   |  129 +
 .../model/impl/DefaultObjectWrapperDesc.java    |   31 +
 .../model/impl/DefaultObjectWrapperInc.java     |   31 +
 ...jectWrapperModelFactoryRegistrationTest.java |   63 +
 .../DefaultObjectWrapperSingletonsTest.java     |  675 +++
 .../model/impl/DefaultObjectWrapperTest.java    |  901 ++++
 .../core/model/impl/EnumModelsTest.java         |   85 +
 .../core/model/impl/ErrorMessagesTest.java      |  170 +
 .../impl/FineTuneMethodAppearanceTest.java      |   65 +
 .../GetlessMethodsAsPropertyGettersRule.java    |   67 +
 .../core/model/impl/IsApplicableTest.java       |  171 +
 .../impl/IsMoreSpecificParameterTypeTest.java   |   98 +
 .../Java7MembersOnlyDefaultObjectWrapper.java   |  101 +
 .../impl/ManyObjectsOfDifferentClasses.java     |  249 ++
 .../impl/ManyStaticsOfDifferentClasses.java     |  236 +
 .../model/impl/MiscNumericalOperationsTest.java |  111 +
 .../model/impl/ModelAPINewInstanceTest.java     |  134 +
 .../core/model/impl/ModelCacheTest.java         |   71 +
 .../model/impl/OverloadedNumberUtilTest.java    |  585 +++
 .../impl/ParameterListPreferabilityTest.java    |  445 ++
 .../impl/PrallelObjectIntrospectionTest.java    |   43 +
 .../impl/PrallelStaticIntrospectionTest.java    |   47 +
 .../core/model/impl/RationalNumber.java         |   90 +
 .../core/model/impl/StaticModelsTest.java       |   91 +
 .../core/model/impl/TypeFlagsTest.java          |  671 +++
 .../core/outputformat/_OutputFormatTestAPI.java |   35 +
 .../impl/CombinedMarkupOutputFormatTest.java    |  194 +
 .../outputformat/impl/HTMLOutputFormatTest.java |  187 +
 .../outputformat/impl/RTFOutputFormatTest.java  |  129 +
 .../DefaultTemplateResolverTest.java            |  365 ++
 .../FileTemplateLoaderTest.java                 |  122 +
 .../MultiTemplateLoaderTest.java                |   99 +
 .../TemplateConfigurationFactoryTest.java       |  203 +
 .../TemplateNameFormatTest.java                 |  330 ++
 .../TemplateSourceMatcherTest.java              |  188 +
 .../AppMetaTemplateDateFormatFactory.java       |  129 +
 .../BaseNTemplateNumberFormatFactory.java       |  128 +
 .../core/userpkg/CustomHTMLOutputFormat.java    |   72 +
 .../core/userpkg/CustomTemplateHTMLModel.java   |   34 +
 .../core/userpkg/DummyOutputFormat.java         |   65 +
 ...EpochMillisDivTemplateDateFormatFactory.java |  102 +
 .../EpochMillisTemplateDateFormatFactory.java   |   92 +
 .../HTMLISOTemplateDateFormatFactory.java       |  114 +
 .../userpkg/HexTemplateNumberFormatFactory.java |   77 +
 ...AndTZSensitiveTemplateDateFormatFactory.java |   97 +
 ...aleSensitiveTemplateNumberFormatFactory.java |   78 +
 .../core/userpkg/PackageVisibleAll.java         |   26 +
 .../userpkg/PackageVisibleAllWithBuilder.java   |   26 +
 .../PackageVisibleAllWithBuilderBuilder.java    |   28 +
 .../PackageVisibleWithPublicConstructor.java    |   27 +
 .../PrintfGTemplateNumberFormatFactory.java     |  138 +
 .../freemarker/core/userpkg/PublicAll.java      |   24 +
 .../userpkg/PublicWithMixedConstructors.java    |   38 +
 .../PublicWithPackageVisibleConstructor.java    |   26 +
 .../core/userpkg/SeldomEscapedOutputFormat.java |   71 +
 .../core/userpkg/TemplateDummyOutputModel.java  |   34 +
 .../TemplateSeldomEscapedOutputModel.java       |   34 +
 .../freemarker/core/util/DateUtilTest.java      | 1085 +++++
 .../freemarker/core/util/FTLUtilTest.java       |  117 +
 .../freemarker/core/util/NumberUtilTest.java    |  215 +
 .../freemarker/core/util/StringUtilTest.java    |  403 ++
 .../core/valueformat/NumberFormatTest.java      |  365 ++
 .../impl/ExtendedDecimalFormatTest.java         |  343 ++
 .../apache/freemarker/dom/DOMSiblingTest.java   |   99 +
 .../freemarker/dom/DOMSimplifiersTest.java      |  201 +
 .../java/org/apache/freemarker/dom/DOMTest.java |  159 +
 .../manualtest/AutoEscapingExample.java         |   72 +
 .../ConfigureOutputFormatExamples.java          |  105 +
 .../manualtest/CustomFormatsExample.java        |   84 +
 .../manualtest/GettingStartedExample.java       |   69 +
 .../apache/freemarker/manualtest/Product.java   |   49 +
 .../TemplateConfigurationExamples.java          |  191 +
 .../UnitAwareTemplateNumberFormatFactory.java   |   80 +
 .../UnitAwareTemplateNumberModel.java           |   43 +
 .../CopyrightCommentRemoverTemplateLoader.java  |  104 +
 .../test/MonitoredTemplateLoader.java           |  325 ++
 .../apache/freemarker/test/TemplateTest.java    |  341 ++
 .../test/TestConfigurationBuilder.java          |   92 +
 .../freemarker/test/hamcerst/Matchers.java      |   34 +
 .../hamcerst/StringContainsIgnoringCase.java    |   47 +
 .../org/apache/freemarker/test/package.html     |   28 +
 .../test/templatesuite/TemplateTestCase.java    |  515 +++
 .../test/templatesuite/TemplateTestSuite.java   |  298 ++
 .../templatesuite/models/AllTemplateModels.java |  128 +
 .../templatesuite/models/BeanTestClass.java     |   93 +
 .../templatesuite/models/BeanTestInterface.java |   25 +
 .../models/BeanTestSuperclass.java              |   30 +
 .../models/BooleanAndScalarModel.java           |   40 +
 .../models/BooleanAndStringTemplateModel.java   |   38 +
 .../test/templatesuite/models/BooleanHash1.java |   58 +
 .../test/templatesuite/models/BooleanHash2.java |   50 +
 .../test/templatesuite/models/BooleanList1.java |   62 +
 .../test/templatesuite/models/BooleanList2.java |   53 +
 .../models/BooleanVsStringMethods.java          |   40 +
 .../templatesuite/models/EnumTestClass.java     |   34 +
 .../templatesuite/models/ExceptionModel.java    |   39 +
 .../models/HashAndScalarModel.java              |   84 +
 .../templatesuite/models/JavaObjectInfo.java    |   35 +
 .../test/templatesuite/models/Listables.java    |  185 +
 .../test/templatesuite/models/MultiModel1.java  |  116 +
 .../test/templatesuite/models/MultiModel2.java  |   63 +
 .../test/templatesuite/models/MultiModel3.java  |   69 +
 .../test/templatesuite/models/MultiModel4.java  |   77 +
 .../test/templatesuite/models/MultiModel5.java  |   81 +
 .../test/templatesuite/models/NewTestModel.java |   52 +
 .../templatesuite/models/NewTestModel2.java     |   52 +
 .../models/NumberAndStringModel.java            |   47 +
 .../models/OverloadedConstructor.java           |   46 +
 .../templatesuite/models/OverloadedMethods.java |  191 +
 .../models/OverloadedMethods2.java              | 1110 +++++
 .../templatesuite/models/SimpleTestMethod.java  |   49 +
 .../models/TransformHashWrapper.java            |   79 +
 .../models/TransformMethodWrapper1.java         |   49 +
 .../models/TransformMethodWrapper2.java         |   64 +
 .../templatesuite/models/TransformModel1.java   |  175 +
 .../templatesuite/models/VarArgTestModel.java   |   63 +
 .../freemarker/test/templatesuite/package.html  |   42 +
 .../freemarker/test/util/AssertDirective.java   |   73 +
 .../test/util/AssertEqualsDirective.java        |   91 +
 .../test/util/AssertFailsDirective.java         |  152 +
 .../AssertationFailedInTemplateException.java   |   46 +
 .../test/util/BadParameterTypeException.java    |   60 +
 .../freemarker/test/util/CoreTestUtil.java      |   19 +
 .../test/util/EntirelyCustomObjectWrapper.java  |   91 +
 .../freemarker/test/util/FileTestCase.java      |  217 +
 .../util/MissingRequiredParameterException.java |   51 +
 .../freemarker/test/util/NoOutputDirective.java |   50 +
 .../test/util/ParameterException.java           |   54 +
 .../SimpleMapAndCollectionObjectWrapper.java    |   60 +
 .../util/UnsupportedParameterException.java     |   50 +
 .../apache/freemarker/test/util/XMLLoader.java  |  138 +
 .../org/apache/freemarker/core/ast-1.ast        |  187 +
 .../org/apache/freemarker/core/ast-1.ftl        |   29 +
 .../apache/freemarker/core/ast-assignments.ast  |  172 +
 .../apache/freemarker/core/ast-assignments.ftl  |   29 +
 .../org/apache/freemarker/core/ast-builtins.ast |   59 +
 .../org/apache/freemarker/core/ast-builtins.ftl |   23 +
 .../apache/freemarker/core/ast-locations.ast    |  155 +
 .../apache/freemarker/core/ast-locations.ftl    |   36 +
 .../core/ast-mixedcontentsimplifications.ast    |   38 +
 .../core/ast-mixedcontentsimplifications.ftl    |   26 +
 .../core/ast-multipleignoredchildren.ast        |   30 +
 .../core/ast-multipleignoredchildren.ftl        |   33 +
 .../core/ast-nestedignoredchildren.ast          |   20 +
 .../core/ast-nestedignoredchildren.ftl          |   19 +
 .../org/apache/freemarker/core/ast-range.ast    |  281 ++
 .../org/apache/freemarker/core/ast-range.ftl    |   47 +
 .../freemarker/core/ast-strlitinterpolation.ast |   82 +
 .../freemarker/core/ast-strlitinterpolation.ftl |   25 +
 .../freemarker/core/ast-whitespacestripping.ast |   70 +
 .../freemarker/core/ast-whitespacestripping.ftl |   40 +
 .../apache/freemarker/core/cano-assignments.ftl |   35 +
 .../freemarker/core/cano-assignments.ftl.out    |   34 +
 .../apache/freemarker/core/cano-builtins.ftl    |   23 +
 .../freemarker/core/cano-builtins.ftl.out       |   23 +
 .../core/cano-identifier-escaping.ftl           |   76 +
 .../core/cano-identifier-escaping.ftl.out       |   44 +
 .../org/apache/freemarker/core/cano-macros.ftl  |   29 +
 .../apache/freemarker/core/cano-macros.ftl.out  |   28 +
 .../core/cano-strlitinterpolation.ftl           |   19 +
 .../core/cano-strlitinterpolation.ftl.out       |   19 +
 .../core/encodingOverride-ISO-8859-1.ftl        |   20 +
 .../freemarker/core/encodingOverride-UTF-8.ftl  |   20 +
 .../freemarker/core/templateresolver/test.ftl   |   19 +
 .../org/apache/freemarker/core/toCache1.ftl     |   19 +
 .../org/apache/freemarker/core/toCache2.ftl     |   19 +
 .../apache/freemarker/dom/DOMSiblingTest.xml    |   31 +
 .../manualtest/AutoEscapingExample-capture.ftlh |   21 +
 .../AutoEscapingExample-capture.ftlh.out        |   20 +
 .../manualtest/AutoEscapingExample-convert.ftlh |   27 +
 .../AutoEscapingExample-convert.ftlh.out        |   25 +
 .../manualtest/AutoEscapingExample-convert2.ftl |   25 +
 .../AutoEscapingExample-convert2.ftl.out        |   21 +
 .../manualtest/AutoEscapingExample-infoBox.ftlh |   26 +
 .../AutoEscapingExample-infoBox.ftlh.out        |   25 +
 .../manualtest/AutoEscapingExample-markup.ftlh  |   28 +
 .../AutoEscapingExample-markup.ftlh.out         |   26 +
 .../AutoEscapingExample-stringConcat.ftlh       |   19 +
 .../AutoEscapingExample-stringConcat.ftlh.out   |   19 +
 .../AutoEscapingExample-stringLiteral.ftlh      |   21 +
 .../AutoEscapingExample-stringLiteral.ftlh.out  |   20 +
 .../AutoEscapingExample-stringLiteral2.ftlh     |   25 +
 .../AutoEscapingExample-stringLiteral2.ftlh.out |   21 +
 .../ConfigureOutputFormatExamples1.properties   |   21 +
 .../ConfigureOutputFormatExamples2.properties   |   31 +
 .../manualtest/CustomFormatsExample-alias1.ftlh |   22 +
 .../CustomFormatsExample-alias1.ftlh.out        |   22 +
 .../manualtest/CustomFormatsExample-alias2.ftlh |   19 +
 .../CustomFormatsExample-alias2.ftlh.out        |   19 +
 .../CustomFormatsExample-modelAware.ftlh        |   20 +
 .../CustomFormatsExample-modelAware.ftlh.out    |   20 +
 .../TemplateConfigurationExamples1.properties   |   25 +
 .../TemplateConfigurationExamples2.properties   |   32 +
 .../TemplateConfigurationExamples3.properties   |   47 +
 .../org/apache/freemarker/manualtest/test.ftlh  |   28 +
 .../org/apache/freemarker/test/servlet/web.xml  |  101 +
 .../test/templatesuite/expected/arithmetic.txt  |   46 +
 .../expected/boolean-formatting.txt             |   31 +
 .../test/templatesuite/expected/boolean.txt     |  102 +
 .../expected/charset-in-header.txt              |   26 +
 .../test/templatesuite/expected/comment.txt     |   34 +
 .../test/templatesuite/expected/comparisons.txt |   93 +
 .../test/templatesuite/expected/compress.txt    |   40 +
 .../templatesuite/expected/dateformat-java.txt  |   55 +
 .../expected/default-object-wrapper.txt         |   55 +
 .../templatesuite/expected/default-xmlns.txt    |   25 +
 .../test/templatesuite/expected/default.txt     |   26 +
 .../expected/encoding-builtins.txt              |   44 +
 .../test/templatesuite/expected/escapes.txt     |   49 +
 .../test/templatesuite/expected/exception.txt   |   43 +
 .../test/templatesuite/expected/exception2.txt  |   47 +
 .../test/templatesuite/expected/exception3.txt  |   21 +
 .../test/templatesuite/expected/exthash.txt     |   76 +
 .../test/templatesuite/expected/hashconcat.txt  |  138 +
 .../test/templatesuite/expected/hashliteral.txt |   74 +
 .../test/templatesuite/expected/helloworld.txt  |   31 +
 .../expected/identifier-escaping.txt            |   57 +
 .../expected/identifier-non-ascii.txt           |   19 +
 .../test/templatesuite/expected/if.txt          |  104 +
 .../test/templatesuite/expected/import.txt      |   40 +
 .../test/templatesuite/expected/include.txt     |   67 +
 .../test/templatesuite/expected/include2.txt    |   28 +
 .../test/templatesuite/expected/interpret.txt   |   23 +
 .../test/templatesuite/expected/iterators.txt   |   84 +
 .../templatesuite/expected/lastcharacter.txt    |   31 +
 .../test/templatesuite/expected/list-bis.txt    |   51 +
 .../test/templatesuite/expected/list.txt        |   51 +
 .../test/templatesuite/expected/list2.txt       |  211 +
 .../test/templatesuite/expected/list3.txt       |   57 +
 .../test/templatesuite/expected/listhash.txt    |  157 +
 .../templatesuite/expected/listhashliteral.txt  |   36 +
 .../test/templatesuite/expected/listliteral.txt |   75 +
 .../templatesuite/expected/localization.txt     |   32 +
 .../test/templatesuite/expected/logging.txt     |   27 +
 .../templatesuite/expected/loopvariable.txt     |   54 +
 .../templatesuite/expected/macros-return.txt    |   23 +
 .../test/templatesuite/expected/macros.txt      |   67 +
 .../test/templatesuite/expected/macros2.txt     |   22 +
 .../test/templatesuite/expected/multimodels.txt |   93 +
 .../test/templatesuite/expected/nested.txt      |   25 +
 .../expected/new-allowsnothing.txt              |   19 +
 .../expected/new-defaultresolver.txt            |   19 +
 .../test/templatesuite/expected/new-optin.txt   |   32 +
 .../test/templatesuite/expected/newlines1.txt   |   29 +
 .../test/templatesuite/expected/newlines2.txt   |   30 +
 .../test/templatesuite/expected/noparse.txt     |   54 +
 .../templatesuite/expected/number-format.txt    |   33 +
 .../templatesuite/expected/number-literal.txt   |   79 +
 .../templatesuite/expected/number-to-date.txt   |   31 +
 .../templatesuite/expected/numerical-cast.txt   |  462 ++
 .../templatesuite/expected/output-encoding1.txt |   27 +
 .../templatesuite/expected/output-encoding2.txt |  Bin 0 -> 1972 bytes
 .../templatesuite/expected/output-encoding3.txt |   26 +
 .../test/templatesuite/expected/precedence.txt  |   48 +
 .../test/templatesuite/expected/recover.txt     |   26 +
 .../test/templatesuite/expected/root.txt        |   44 +
 .../expected/sequence-builtins.txt              |  404 ++
 .../test/templatesuite/expected/specialvars.txt |   25 +
 .../string-builtins-regexps-matches.txt         |   99 +
 .../expected/string-builtins-regexps.txt        |  112 +
 .../templatesuite/expected/string-builtins1.txt |  112 +
 .../templatesuite/expected/string-builtins2.txt |  135 +
 .../templatesuite/expected/stringbimethods.txt  |   29 +
 .../templatesuite/expected/stringliteral.txt    |  Bin 0 -> 1550 bytes
 .../test/templatesuite/expected/switch.txt      |   80 +
 .../test/templatesuite/expected/transforms.txt  |   68 +
 .../templatesuite/expected/type-builtins.txt    |   33 +
 .../test/templatesuite/expected/var-layers.txt  |   37 +
 .../test/templatesuite/expected/varargs.txt     |   44 +
 .../test/templatesuite/expected/variables.txt   |   62 +
 .../templatesuite/expected/whitespace-trim.txt  |   60 +
 .../templatesuite/expected/wstrip-in-header.txt |   23 +
 .../test/templatesuite/expected/wstripping.txt  |   39 +
 .../templatesuite/expected/xml-fragment.txt     |   25 +
 .../expected/xml-ns_prefix-scope.txt            |   29 +
 .../test/templatesuite/expected/xml.txt         |   65 +
 .../test/templatesuite/expected/xmlns1.txt      |   63 +
 .../test/templatesuite/expected/xmlns3.txt      |   47 +
 .../test/templatesuite/expected/xmlns4.txt      |   47 +
 .../test/templatesuite/expected/xmlns5.txt      |   26 +
 .../models/BeansTestResources.properties        |   19 +
 .../test/templatesuite/models/defaultxmlns1.xml |   24 +
 .../models/xml-ns_prefix-scope.xml              |   26 +
 .../test/templatesuite/models/xml.xml           |   31 +
 .../test/templatesuite/models/xmlfragment.xml   |   19 +
 .../test/templatesuite/models/xmlns.xml         |   32 +
 .../test/templatesuite/models/xmlns2.xml        |   32 +
 .../test/templatesuite/models/xmlns3.xml        |   32 +
 .../templatesuite/templates/api-builtins.ftl    |   40 +
 .../test/templatesuite/templates/arithmetic.ftl |   50 +
 .../templatesuite/templates/assignments.ftl     |  108 +
 .../templates/boolean-formatting.ftl            |   82 +
 .../test/templatesuite/templates/boolean.ftl    |  142 +
 .../templates/charset-in-header.ftl             |   27 +
 .../templates/charset-in-header_inc1.ftl        |   20 +
 .../templates/charset-in-header_inc2.ftl        |   19 +
 .../test/templatesuite/templates/comment.ftl    |   50 +
 .../templatesuite/templates/comparisons.ftl     |  218 +
 .../test/templatesuite/templates/compress.ftl   |   59 +
 .../templates/date-type-builtins.ftl            |   47 +
 .../templates/dateformat-iso-bi.ftl             |  163 +
 .../templates/dateformat-iso-like.ftl           |  155 +
 .../templatesuite/templates/dateformat-java.ftl |   71 +
 .../templatesuite/templates/dateparsing.ftl     |   84 +
 .../templates/default-object-wrapper.ftl        |   59 +
 .../templatesuite/templates/default-xmlns.ftl   |   28 +
 .../test/templatesuite/templates/default.ftl    |   34 +
 .../templates/encoding-builtins.ftl             |   52 +
 .../test/templatesuite/templates/escapes.ftl    |   79 +
 .../test/templatesuite/templates/exception.ftl  |   31 +
 .../test/templatesuite/templates/exception2.ftl |   31 +
 .../test/templatesuite/templates/exception3.ftl |   31 +
 .../templates/existence-operators.ftl           |  141 +
 .../test/templatesuite/templates/hashconcat.ftl |   60 +
 .../templatesuite/templates/hashliteral.ftl     |  100 +
 .../test/templatesuite/templates/helloworld.ftl |   30 +
 .../templates/identifier-escaping.ftl           |   81 +
 .../templates/identifier-non-ascii.ftl          |   21 +
 .../test/templatesuite/templates/if.ftl         |  109 +
 .../test/templatesuite/templates/import.ftl     |   45 +
 .../test/templatesuite/templates/import_lib.ftl |   31 +
 .../test/templatesuite/templates/include.ftl    |   47 +
 .../templates/include2-included.ftl             |   19 +
 .../test/templatesuite/templates/include2.ftl   |   32 +
 .../test/templatesuite/templates/included.ftl   |   30 +
 .../test/templatesuite/templates/interpret.ftl  |   25 +
 .../test/templatesuite/templates/iterators.ftl  |   71 +
 .../templatesuite/templates/lastcharacter.ftl   |   31 +
 .../test/templatesuite/templates/list-bis.ftl   |   48 +
 .../test/templatesuite/templates/list.ftl       |   44 +
 .../test/templatesuite/templates/list2.ftl      |   90 +
 .../test/templatesuite/templates/list3.ftl      |   70 +
 .../test/templatesuite/templates/listhash.ftl   |   70 +
 .../templatesuite/templates/listhashliteral.ftl |   35 +
 .../templatesuite/templates/listliteral.ftl     |   84 +
 .../templatesuite/templates/localization.ftl    |   32 +
 .../templatesuite/templates/localization_en.ftl |   32 +
 .../templates/localization_en_AU.ftl            |   32 +
 .../test/templatesuite/templates/logging.ftl    |   42 +
 .../templatesuite/templates/loopvariable.ftl    |   49 +
 .../templatesuite/templates/macros-return.ftl   |   34 +
 .../test/templatesuite/templates/macros.ftl     |  101 +
 .../test/templatesuite/templates/macros2.ftl    |   35 +
 .../templatesuite/templates/multimodels.ftl     |   84 +
 .../test/templatesuite/templates/nested.ftl     |   29 +
 .../templatesuite/templates/nestedinclude.ftl   |   21 +
 .../templates/new-defaultresolver.ftl           |   23 +
 .../test/templatesuite/templates/new-optin.ftl  |   30 +
 .../test/templatesuite/templates/newlines1.ftl  |   29 +
 .../test/templatesuite/templates/newlines2.ftl  |   33 +
 .../test/templatesuite/templates/noparse.ftl    |   62 +
 .../templatesuite/templates/number-format.ftl   |   42 +
 .../templatesuite/templates/number-literal.ftl  |  133 +
 .../templates/number-math-builtins.ftl          |   78 +
 .../templatesuite/templates/number-to-date.ftl  |   35 +
 .../templatesuite/templates/numerical-cast.ftl  |   82 +
 .../templates/output-encoding1.ftl              |   30 +
 .../templates/output-encoding2.ftl              |   28 +
 .../templates/output-encoding3.ftl              |   28 +
 .../templates/overloaded-methods.ftl            |  411 ++
 .../test/templatesuite/templates/precedence.ftl |   61 +
 .../templatesuite/templates/range-common.ftl    |  314 ++
 .../test/templatesuite/templates/range.ftl      |   50 +
 .../test/templatesuite/templates/recover.ftl    |   47 +
 .../test/templatesuite/templates/root.ftl       |   47 +
 .../templates/sequence-builtins.ftl             |  360 ++
 .../test/templatesuite/templates/setting.ftl    |   53 +
 .../templates/simplehash-char-key.ftl           |   44 +
 .../templatesuite/templates/specialvars.ftl     |   38 +
 .../templates/string-builtin-coercion.ftl       |   34 +
 .../string-builtins-regexps-matches.ftl         |  118 +
 .../templates/string-builtins-regexps.ftl       |  136 +
 .../templates/string-builtins1.ftl              |  129 +
 .../templates/string-builtins2.ftl              |  135 +
 .../templates/string-builtins3.ftl              |  225 +
 .../templatesuite/templates/stringbimethods.ftl |   36 +
 .../templatesuite/templates/stringliteral.ftl   |   69 +
 .../templates/subdir/include-subdir.ftl         |   27 +
 .../templates/subdir/include-subdir2.ftl        |   19 +
 .../templates/subdir/new-optin-2.ftl            |   24 +
 .../templates/subdir/new-optin.ftl              |   26 +
 .../templates/subdir/subsub/new-optin.ftl       |   24 +
 .../templatesuite/templates/switch-builtin.ftl  |   54 +
 .../test/templatesuite/templates/switch.ftl     |  139 +
 .../templatesuite/templates/then-builtin.ftl    |   53 +
 .../test/templatesuite/templates/transforms.ftl |  100 +
 .../templatesuite/templates/type-builtins.ftl   |   44 +
 .../test/templatesuite/templates/undefined.ftl  |   19 +
 .../test/templatesuite/templates/url.ftl        |   24 +
 .../test/templatesuite/templates/var-layers.ftl |   39 +
 .../test/templatesuite/templates/varargs.ftl    |   45 +
 .../test/templatesuite/templates/variables.ftl  |   70 +
 .../templatesuite/templates/varlayers_lib.ftl   |   28 +
 .../templatesuite/templates/whitespace-trim.ftl |  102 +
 .../templates/wsstripinheader_inc.ftl           |   22 +
 .../templates/wstrip-in-header.ftl              |   26 +
 .../templatesuite/templates/xml-fragment.ftl    |   26 +
 .../templates/xml-ns_prefix-scope-lib.ftl       |   23 +
 .../templates/xml-ns_prefix-scope-main.ftl      |   36 +
 .../test/templatesuite/templates/xml.ftl        |   47 +
 .../test/templatesuite/templates/xmlns1.ftl     |   53 +
 .../test/templatesuite/templates/xmlns3.ftl     |   70 +
 .../test/templatesuite/templates/xmlns4.ftl     |   70 +
 .../test/templatesuite/templates/xmlns5.ftl     |   28 +
 .../freemarker/test/templatesuite/testcases.xml |  211 +
 freemarker-servlet/build.gradle                 |   80 +
 .../servlet/AllHttpScopesHashModel.java         |  114 +
 .../freemarker/servlet/FreemarkerServlet.java   | 1611 +++++++
 .../FreemarkerServletConfigurationBuilder.java  |   79 +
 .../servlet/HttpRequestHashModel.java           |  108 +
 .../servlet/HttpRequestParametersHashModel.java |  104 +
 .../servlet/HttpSessionHashModel.java           |  113 +
 .../apache/freemarker/servlet/IncludePage.java  |  254 ++
 .../freemarker/servlet/InitParamParser.java     |  264 ++
 .../servlet/ServletContextHashModel.java        |   62 +
 .../servlet/WebAppTemplateLoader.java           |  301 ++
 .../apache/freemarker/servlet/_ServletLogs.java |   34 +
 .../jsp/CustomTagAndELFunctionCombiner.java     |  202 +
 .../freemarker/servlet/jsp/EventForwarding.java |  200 +
 .../jsp/FreeMarkerJspApplicationContext.java    |  165 +
 .../servlet/jsp/FreeMarkerJspFactory.java       |   63 +
 .../servlet/jsp/FreeMarkerJspFactory21.java     |   51 +
 .../servlet/jsp/FreeMarkerPageContext.java      |  459 ++
 .../freemarker/servlet/jsp/JspTagModelBase.java |  162 +
 .../servlet/jsp/JspWriterAdapter.java           |  188 +
 .../servlet/jsp/PageContextFactory.java         |   66 +
 .../servlet/jsp/SimpleTagDirectiveModel.java    |  111 +
 .../servlet/jsp/TagTransformModel.java          |  419 ++
 .../freemarker/servlet/jsp/TaglibFactory.java   | 2015 +++++++++
 .../servlet/jsp/TaglibMethodUtil.java           |  117 +
 .../servlet/jsp/_FreeMarkerPageContext21.java   |  122 +
 .../apache/freemarker/servlet/jsp/package.html  |   26 +
 .../org/apache/freemarker/servlet/package.html  |   26 +
 .../src/main/resources/META-INF/DISCLAIMER      |    8 +
 .../src/main/resources/META-INF/LICENSE         |  202 +
 .../servlet/DummyMockServletContext.java        |  157 +
 .../servlet/FreemarkerServletTest.java          |  628 +++
 .../freemarker/servlet/InitParamParserTest.java |  163 +
 .../servlet/WebAppTemplateLoaderTest.java       |   48 +
 .../servlet/jsp/JspTestFreemarkerServlet.java   |   51 +
 ...estFreemarkerServletWithDefaultOverride.java |   47 +
 .../servlet/jsp/RealServletContainertTest.java  |  506 +++
 .../freemarker/servlet/jsp/TLDParsingTest.java  |  137 +
 .../servlet/jsp/TaglibMethodUtilTest.java       |  108 +
 .../jsp/taglibmembers/AttributeAccessorTag.java |   68 +
 .../jsp/taglibmembers/AttributeInfoTag.java     |   59 +
 .../jsp/taglibmembers/EnclosingClass.java       |   32 +
 .../servlet/jsp/taglibmembers/GetAndSetTag.java |   66 +
 .../jsp/taglibmembers/TestFunctions.java        |   79 +
 .../jsp/taglibmembers/TestSimpleTag.java        |   54 +
 .../jsp/taglibmembers/TestSimpleTag2.java       |   32 +
 .../jsp/taglibmembers/TestSimpleTag3.java       |   32 +
 .../servlet/jsp/taglibmembers/TestTag.java      |  100 +
 .../servlet/jsp/taglibmembers/TestTag2.java     |   50 +
 .../servlet/jsp/taglibmembers/TestTag3.java     |   50 +
 .../config/WebappLocalFreemarkerServlet.java    |   25 +
 .../servlet/test/DefaultModel2TesterAction.java |   92 +
 .../freemarker/servlet/test/Model2Action.java   |   37 +
 .../servlet/test/Model2TesterServlet.java       |  139 +
 .../freemarker/servlet/test/WebAppTestCase.java |  360 ++
 .../src/test/resources/META-INF/malformed.tld   |   31 +
 .../tldDiscovery MetaInfTldSources-1.tld        |   31 +
 .../freemarker/servlet/jsp/TLDParsingTest.tld   |   89 +
 .../servlet/jsp/templates/classpath-test.ftl    |   19 +
 .../jsp/tldDiscovery-ClassPathTlds-1.tld        |   31 +
 .../jsp/tldDiscovery-ClassPathTlds-2.tld        |   31 +
 .../servlet/jsp/webapps/basic/CONTENTS.txt      |   36 +
 .../WEB-INF/el-function-tag-name-clash.tld      |   50 +
 .../jsp/webapps/basic/WEB-INF/el-functions.tld  |   84 +
 .../expected/attributes-modernModels.txt        |   73 +
 .../basic/WEB-INF/expected/attributes.txt       |   73 +
 .../basic/WEB-INF/expected/customTags1.txt      |  106 +
 .../servlet/jsp/webapps/basic/WEB-INF/test.tld  |   75 +
 .../servlet/jsp/webapps/basic/WEB-INF/web.xml   |  142 +
 .../servlet/jsp/webapps/basic/attributes.ftl    |   90 +
 .../jsp/webapps/basic/customELFunctions1.ftl    |   30 +
 .../jsp/webapps/basic/customELFunctions1.jsp    |   31 +
 .../servlet/jsp/webapps/basic/customTags1.ftl   |   59 +
 .../webapps/basic/elFunctionsTagNameClash.ftl   |   25 +
 .../webapps/basic/elFunctionsTagNameClash.jsp   |   26 +
 .../jsp/webapps/basic/trivial-jstl-@Ignore.ftl  |   48 +
 .../servlet/jsp/webapps/basic/trivial.ftl       |   37 +
 .../servlet/jsp/webapps/basic/trivial.jsp       |   45 +
 .../servlet/jsp/webapps/config/CONTENTS.txt     |   33 +
 .../webapps/config/WEB-INF/classes/sub/test.ftl |   19 +
 .../jsp/webapps/config/WEB-INF/classes/test.ftl |   19 +
 .../WEB-INF/lib/templates.jar/sub/test2.ftl     |   19 +
 .../webapps/config/WEB-INF/templates/test.ftl   |   19 +
 .../servlet/jsp/webapps/config/WEB-INF/web.xml  |  109 +
 .../servlet/jsp/webapps/config/test.ftl         |   19 +
 .../servlet/jsp/webapps/errors/CONTENTS.txt     |   28 +
 .../servlet/jsp/webapps/errors/WEB-INF/web.xml  |   92 +
 .../jsp/webapps/errors/failing-parsetime.ftlnv  |   20 +
 .../jsp/webapps/errors/failing-parsetime.jsp    |   19 +
 .../jsp/webapps/errors/failing-runtime.ftl      |   26 +
 .../jsp/webapps/errors/failing-runtime.jsp      |   23 +
 .../servlet/jsp/webapps/errors/not-failing.ftl  |   19 +
 .../jsp/webapps/multipleLoaders/CONTENTS.txt    |   24 +
 .../multipleLoaders/WEB-INF/templates/test.ftl  |   19 +
 .../jsp/webapps/multipleLoaders/WEB-INF/web.xml |   83 +
 .../jsp/webapps/tldDiscovery/CONTENTS.txt       |   37 +
 .../WEB-INF/expected/subdir/test-rel.txt        |   20 +
 .../WEB-INF/expected/test-noClasspath.txt       |   32 +
 .../tldDiscovery/WEB-INF/expected/test1.txt     |   73 +
 .../tldDiscovery/WEB-INF/fmtesttag 2.tld        |   32 +
 .../webapps/tldDiscovery/WEB-INF/fmtesttag4.tld |   32 +
 .../lib/taglib-foo.jar/META-INF/foo bar.tld     |   32 +
 .../WEB-INF/subdir-with-tld/fmtesttag3.tld      |   32 +
 .../WEB-INF/taglib 2.jar/META-INF/taglib.tld    |   31 +
 .../jsp/webapps/tldDiscovery/WEB-INF/web.xml    |  179 +
 .../tldDiscovery/not-auto-scanned/fmtesttag.tld |   40 +
 .../webapps/tldDiscovery/subdir/test-rel.ftl    |   20 +
 .../webapps/tldDiscovery/test-noClasspath.ftl   |   32 +
 .../servlet/jsp/webapps/tldDiscovery/test1.ftl  |   55 +
 .../WEB-INF/templates/test.ftl                  |    1 +
 freemarker-test-utils/build.gradle              |   53 +
 .../freemarker/test/ResourcesExtractor.java     |  294 ++
 .../org/apache/freemarker/test/TestUtil.java    |  255 ++
 .../apache/freemarker/test/_TStringUtil.java    |   65 +
 .../src/main/resources/logback-test.xml         |   34 +
 gradle.properties.sample                        |    2 +
 gradle/wrapper/gradle-wrapper.properties        |    4 +-
 ivy.xml                                         |  152 -
 ivysettings.xml                                 |   54 -
 old-ant-build/.travis.yml                       |    5 +
 old-ant-build/build.properties                  |   23 +
 old-ant-build/build.properties.sample           |   23 +
 old-ant-build/build.xml                         | 1093 +++++
 old-ant-build/ivy.xml                           |  152 +
 old-ant-build/ivysettings.xml                   |   54 +
 old-ant-build/osgi.bnd                          |   64 +
 osgi.bnd                                        |   64 -
 settings.gradle                                 |    6 +
 src/dist/bin/LICENSE                            |  232 -
 src/dist/bin/documentation/index.html           |   67 -
 src/dist/jar/META-INF/LICENSE                   |  202 -
 src/dist/javadoc/META-INF/LICENSE               |  202 -
 .../Eclipse/Formatter-profile-FreeMarker.xml    |  313 --
 .../Editor-Inspections-FreeMarker.xml           |   33 -
 .../Java-code-style-FreeMarker.xml              |   66 -
 .../core/APINotSupportedTemplateException.java  |   49 -
 .../org/apache/freemarker/core/ASTComment.java  |   87 -
 .../apache/freemarker/core/ASTDebugBreak.java   |   89 -
 .../freemarker/core/ASTDirAssignment.java       |  279 --
 .../core/ASTDirAssignmentsContainer.java        |  115 -
 .../core/ASTDirAttemptRecoverContainer.java     |   88 -
 .../apache/freemarker/core/ASTDirAutoEsc.java   |   77 -
 .../org/apache/freemarker/core/ASTDirBreak.java |   70 -
 .../core/ASTDirCapturingAssignment.java         |  184 -
 .../org/apache/freemarker/core/ASTDirCase.java  |   91 -
 .../apache/freemarker/core/ASTDirCompress.java  |   87 -
 .../freemarker/core/ASTDirElseOfList.java       |   75 -
 .../apache/freemarker/core/ASTDirEscape.java    |  111 -
 .../apache/freemarker/core/ASTDirFallback.java  |   70 -
 .../org/apache/freemarker/core/ASTDirFlush.java |   65 -
 .../core/ASTDirIfElseIfElseContainer.java       |  107 -
 .../freemarker/core/ASTDirIfOrElseOrElseIf.java |  114 -
 .../apache/freemarker/core/ASTDirImport.java    |  125 -
 .../apache/freemarker/core/ASTDirInclude.java   |  174 -
 .../org/apache/freemarker/core/ASTDirItems.java |  120 -
 .../org/apache/freemarker/core/ASTDirList.java  |  462 --
 .../core/ASTDirListElseContainer.java           |   88 -
 .../org/apache/freemarker/core/ASTDirMacro.java |  325 --
 .../apache/freemarker/core/ASTDirNested.java    |  159 -
 .../apache/freemarker/core/ASTDirNoAutoEsc.java |   77 -
 .../apache/freemarker/core/ASTDirNoEscape.java  |   78 -
 .../freemarker/core/ASTDirOutputFormat.java     |   85 -
 .../apache/freemarker/core/ASTDirRecover.java   |   75 -
 .../apache/freemarker/core/ASTDirRecurse.java   |  130 -
 .../apache/freemarker/core/ASTDirReturn.java    |   91 -
 .../org/apache/freemarker/core/ASTDirSep.java   |   89 -
 .../apache/freemarker/core/ASTDirSetting.java   |  172 -
 .../org/apache/freemarker/core/ASTDirStop.java  |   81 -
 .../apache/freemarker/core/ASTDirSwitch.java    |  129 -
 .../apache/freemarker/core/ASTDirTOrTrOrTl.java |  109 -
 .../freemarker/core/ASTDirUserDefined.java      |  343 --
 .../org/apache/freemarker/core/ASTDirVisit.java |  126 -
 .../apache/freemarker/core/ASTDirective.java    |   98 -
 .../freemarker/core/ASTDollarInterpolation.java |  151 -
 .../org/apache/freemarker/core/ASTElement.java  |  445 --
 .../freemarker/core/ASTExpAddOrConcat.java      |  313 --
 .../org/apache/freemarker/core/ASTExpAnd.java   |   82 -
 .../apache/freemarker/core/ASTExpBoolean.java   |   34 -
 .../freemarker/core/ASTExpBooleanLiteral.java   |   91 -
 .../apache/freemarker/core/ASTExpBuiltIn.java   |  485 --
 .../freemarker/core/ASTExpBuiltInVariable.java  |  298 --
 .../freemarker/core/ASTExpComparison.java       |  104 -
 .../apache/freemarker/core/ASTExpDefault.java   |  142 -
 .../org/apache/freemarker/core/ASTExpDot.java   |   92 -
 .../freemarker/core/ASTExpDynamicKeyName.java   |  284 --
 .../apache/freemarker/core/ASTExpExists.java    |   91 -
 .../freemarker/core/ASTExpHashLiteral.java      |  220 -
 .../freemarker/core/ASTExpListLiteral.java      |  195 -
 .../freemarker/core/ASTExpMethodCall.java       |  147 -
 .../freemarker/core/ASTExpNegateOrPlus.java     |  110 -
 .../org/apache/freemarker/core/ASTExpNot.java   |   76 -
 .../freemarker/core/ASTExpNumberLiteral.java    |   92 -
 .../org/apache/freemarker/core/ASTExpOr.java    |   82 -
 .../freemarker/core/ASTExpParenthesis.java      |   88 -
 .../org/apache/freemarker/core/ASTExpRange.java |  119 -
 .../freemarker/core/ASTExpStringLiteral.java    |  211 -
 .../apache/freemarker/core/ASTExpVariable.java  |  105 -
 .../apache/freemarker/core/ASTExpression.java   |  208 -
 .../freemarker/core/ASTHashInterpolation.java   |  172 -
 .../freemarker/core/ASTImplicitParent.java      |  101 -
 .../freemarker/core/ASTInterpolation.java       |   51 -
 .../org/apache/freemarker/core/ASTNode.java     |  233 -
 .../apache/freemarker/core/ASTStaticText.java   |  408 --
 .../freemarker/core/ArithmeticExpression.java   |  129 -
 .../freemarker/core/BoundedRangeModel.java      |   70 -
 .../core/BuiltInBannedWhenAutoEscaping.java     |   27 -
 .../apache/freemarker/core/BuiltInForDate.java  |   56 -
 .../freemarker/core/BuiltInForHashEx.java       |   55 -
 .../core/BuiltInForLegacyEscaping.java          |   48 -
 .../freemarker/core/BuiltInForLoopVariable.java |   48 -
 .../freemarker/core/BuiltInForMarkupOutput.java |   40 -
 .../apache/freemarker/core/BuiltInForNode.java  |   39 -
 .../freemarker/core/BuiltInForNodeEx.java       |   37 -
 .../freemarker/core/BuiltInForNumber.java       |   35 -
 .../freemarker/core/BuiltInForSequence.java     |   38 -
 .../freemarker/core/BuiltInForString.java       |   36 -
 .../core/BuiltInWithParseTimeParameters.java    |  109 -
 .../freemarker/core/BuiltInsForDates.java       |  212 -
 .../core/BuiltInsForExistenceHandling.java      |  133 -
 .../freemarker/core/BuiltInsForHashes.java      |   59 -
 .../core/BuiltInsForLoopVariables.java          |  156 -
 .../core/BuiltInsForMarkupOutputs.java          |   41 -
 .../core/BuiltInsForMultipleTypes.java          |  717 ---
 .../freemarker/core/BuiltInsForNodes.java       |  154 -
 .../freemarker/core/BuiltInsForNumbers.java     |  319 --
 .../core/BuiltInsForOutputFormatRelated.java    |   84 -
 .../freemarker/core/BuiltInsForSequences.java   |  871 ----
 .../core/BuiltInsForStringsBasic.java           |  697 ---
 .../core/BuiltInsForStringsEncoding.java        |  195 -
 .../freemarker/core/BuiltInsForStringsMisc.java |  305 --
 .../core/BuiltInsForStringsRegexp.java          |  322 --
 .../core/BuiltInsWithParseTimeParameters.java   |  157 -
 ...lPlaceCustomDataInitializationException.java |   33 -
 .../apache/freemarker/core/Configuration.java   | 2631 -----------
 .../freemarker/core/ConfigurationException.java |   37 -
 .../ConfigurationSettingValueException.java     |   86 -
 .../apache/freemarker/core/CustomStateKey.java  |   60 -
 .../freemarker/core/CustomStateScope.java       |   34 -
 .../freemarker/core/DirectiveCallPlace.java     |  137 -
 .../org/apache/freemarker/core/Environment.java | 3213 --------------
 .../core/InvalidReferenceException.java         |  167 -
 .../core/ListableRightUnboundedRangeModel.java  |   97 -
 .../apache/freemarker/core/LocalContext.java    |   36 -
 .../freemarker/core/LocalContextStack.java      |   57 -
 .../core/MarkupOutputFormatBoundBuiltIn.java    |   46 -
 .../org/apache/freemarker/core/MessageUtil.java |  341 --
 .../org/apache/freemarker/core/MiscUtil.java    |   69 -
 ...utableParsingAndProcessingConfiguration.java |  475 --
 .../core/MutableProcessingConfiguration.java    | 2418 ----------
 .../freemarker/core/NativeCollectionEx.java     |   73 -
 .../apache/freemarker/core/NativeHashEx2.java   |  106 -
 .../apache/freemarker/core/NativeSequence.java  |   74 -
 .../core/NativeStringArraySequence.java         |   53 -
 .../NativeStringCollectionCollectionEx.java     |   79 -
 .../core/NativeStringListSequence.java          |   56 -
 .../NestedContentNotSupportedException.java     |   67 -
 .../freemarker/core/NonBooleanException.java    |   62 -
 .../freemarker/core/NonDateException.java       |   58 -
 .../core/NonExtendedHashException.java          |   62 -
 .../core/NonExtendedNodeException.java          |   64 -
 .../freemarker/core/NonHashException.java       |   64 -
 .../core/NonMarkupOutputException.java          |   64 -
 .../freemarker/core/NonMethodException.java     |   64 -
 .../freemarker/core/NonNamespaceException.java  |   63 -
 .../freemarker/core/NonNodeException.java       |   64 -
 .../freemarker/core/NonNumericalException.java  |   74 -
 .../freemarker/core/NonSequenceException.java   |   64 -
 .../core/NonSequenceOrCollectionException.java  |   92 -
 .../freemarker/core/NonStringException.java     |   74 -
 .../NonStringOrTemplateOutputException.java     |   78 -
 .../NonUserDefinedDirectiveLikeException.java   |   67 -
 .../core/OutputFormatBoundBuiltIn.java          |   48 -
 .../apache/freemarker/core/ParameterRole.java   |   91 -
 .../apache/freemarker/core/ParseException.java  |  518 ---
 .../core/ParsingAndProcessingConfiguration.java |   29 -
 .../freemarker/core/ParsingConfiguration.java   |  299 --
 .../core/ProcessingConfiguration.java           |  704 ---
 .../org/apache/freemarker/core/RangeModel.java  |   59 -
 .../apache/freemarker/core/RegexpHelper.java    |  207 -
 .../core/RightUnboundedRangeModel.java          |   48 -
 .../core/SettingValueNotSetException.java       |   33 -
 .../apache/freemarker/core/SpecialBuiltIn.java  |   27 -
 .../apache/freemarker/core/StopException.java   |   64 -
 .../org/apache/freemarker/core/Template.java    | 1341 ------
 .../freemarker/core/TemplateBooleanFormat.java  |   91 -
 .../freemarker/core/TemplateClassResolver.java  |   82 -
 .../freemarker/core/TemplateConfiguration.java  |  991 -----
 .../core/TemplateElementArrayBuilder.java       |  102 -
 .../core/TemplateElementsToVisit.java           |   48 -
 .../freemarker/core/TemplateException.java      |  655 ---
 .../core/TemplateExceptionHandler.java          |  156 -
 .../freemarker/core/TemplateLanguage.java       |  111 -
 .../core/TemplateNotFoundException.java         |   64 -
 ...emplateParsingConfigurationWithFallback.java |  146 -
 .../freemarker/core/TemplatePostProcessor.java  |   31 -
 .../core/TemplatePostProcessorException.java    |   35 -
 ...nterruptionSupportTemplatePostProcessor.java |  140 -
 .../apache/freemarker/core/TokenMgrError.java   |  249 --
 .../freemarker/core/TopLevelConfiguration.java  |  194 -
 .../core/UnexpectedTypeException.java           |  109 -
 .../UnknownConfigurationSettingException.java   |   40 -
 .../org/apache/freemarker/core/Version.java     |  297 --
 .../core/WrongTemplateCharsetException.java     |   63 -
 .../apache/freemarker/core/_CharsetBuilder.java |   41 -
 .../org/apache/freemarker/core/_CoreAPI.java    |   88 -
 .../org/apache/freemarker/core/_CoreLogs.java   |   46 -
 .../java/org/apache/freemarker/core/_Debug.java |  122 -
 .../apache/freemarker/core/_DelayedAOrAn.java   |   35 -
 .../core/_DelayedConversionToString.java        |   52 -
 .../core/_DelayedFTLTypeDescription.java        |   37 -
 .../core/_DelayedGetCanonicalForm.java          |   39 -
 .../freemarker/core/_DelayedGetMessage.java     |   35 -
 .../core/_DelayedGetMessageWithoutStackTop.java |   34 -
 .../apache/freemarker/core/_DelayedJQuote.java  |   36 -
 .../freemarker/core/_DelayedJoinWithComma.java  |   48 -
 .../apache/freemarker/core/_DelayedOrdinal.java |   47 -
 .../freemarker/core/_DelayedShortClassName.java |   35 -
 .../freemarker/core/_DelayedToString.java       |   37 -
 .../core/_ErrorDescriptionBuilder.java          |  356 --
 .../org/apache/freemarker/core/_EvalUtil.java   |  545 ---
 .../java/org/apache/freemarker/core/_Java8.java |   34 -
 .../org/apache/freemarker/core/_Java8Impl.java  |   43 -
 .../freemarker/core/_MiscTemplateException.java |  124 -
 ...ObjectBuilderSettingEvaluationException.java |   46 -
 .../core/_ObjectBuilderSettingEvaluator.java    | 1068 -----
 .../core/_SettingEvaluationEnvironment.java     |   61 -
 .../core/_TemplateModelException.java           |  133 -
 .../freemarker/core/_TimeZoneBuilder.java       |   43 -
 ...expectedTypeErrorExplainerTemplateModel.java |   36 -
 .../core/arithmetic/ArithmeticEngine.java       |   92 -
 .../impl/BigDecimalArithmeticEngine.java        |  107 -
 .../impl/ConservativeArithmeticEngine.java      |  381 --
 .../core/arithmetic/impl/package.html           |   26 -
 .../freemarker/core/arithmetic/package.html     |   25 -
 .../freemarker/core/debug/Breakpoint.java       |   83 -
 .../freemarker/core/debug/DebugModel.java       |  105 -
 .../core/debug/DebuggedEnvironment.java         |   58 -
 .../apache/freemarker/core/debug/Debugger.java  |   95 -
 .../freemarker/core/debug/DebuggerClient.java   |  149 -
 .../freemarker/core/debug/DebuggerListener.java |   36 -
 .../freemarker/core/debug/DebuggerServer.java   |  131 -
 .../core/debug/EnvironmentSuspendedEvent.java   |   67 -
 .../core/debug/RmiDebugModelImpl.java           |  164 -
 .../core/debug/RmiDebuggedEnvironmentImpl.java  |  340 --
 .../freemarker/core/debug/RmiDebuggerImpl.java  |   86 -
 .../core/debug/RmiDebuggerListenerImpl.java     |   67 -
 .../core/debug/RmiDebuggerService.java          |  307 --
 .../apache/freemarker/core/debug/SoftCache.java |   89 -
 .../freemarker/core/debug/_DebuggerService.java |   93 -
 .../apache/freemarker/core/debug/package.html   |   27 -
 .../core/model/AdapterTemplateModel.java        |   49 -
 .../apache/freemarker/core/model/Constants.java |  133 -
 .../core/model/FalseTemplateBooleanModel.java   |   36 -
 .../core/model/GeneralPurposeNothing.java       |   83 -
 .../freemarker/core/model/ObjectWrapper.java    |   59 -
 .../core/model/ObjectWrapperAndUnwrapper.java   |   90 -
 .../core/model/ObjectWrapperWithAPISupport.java |   46 -
 .../core/model/RichObjectWrapper.java           |   34 -
 .../model/SerializableTemplateBooleanModel.java |   24 -
 .../core/model/TemplateBooleanModel.java        |   48 -
 .../core/model/TemplateCollectionModel.java     |   48 -
 .../core/model/TemplateCollectionModelEx.java   |   45 -
 .../core/model/TemplateDateModel.java           |   73 -
 .../core/model/TemplateDirectiveBody.java       |   45 -
 .../core/model/TemplateDirectiveModel.java      |   69 -
 .../core/model/TemplateHashModel.java           |   41 -
 .../core/model/TemplateHashModelEx.java         |   51 -
 .../core/model/TemplateHashModelEx2.java        |   80 -
 .../core/model/TemplateMarkupOutputModel.java   |   52 -
 .../core/model/TemplateMethodModel.java         |   60 -
 .../core/model/TemplateMethodModelEx.java       |   54 -
 .../freemarker/core/model/TemplateModel.java    |   55 -
 .../core/model/TemplateModelAdapter.java        |   34 -
 .../core/model/TemplateModelException.java      |  111 -
 .../core/model/TemplateModelIterator.java       |   39 -
 .../core/model/TemplateModelWithAPISupport.java |   39 -
 .../core/model/TemplateNodeModel.java           |   78 -
 .../core/model/TemplateNodeModelEx.java         |   40 -
 .../core/model/TemplateNumberModel.java         |   42 -
 .../core/model/TemplateScalarModel.java         |   45 -
 .../core/model/TemplateSequenceModel.java       |   48 -
 .../core/model/TemplateTransformModel.java      |   54 -
 .../freemarker/core/model/TransformControl.java |  101 -
 .../core/model/TrueTemplateBooleanModel.java    |   36 -
 .../core/model/WrapperTemplateModel.java        |   33 -
 .../core/model/WrappingTemplateModel.java       |   62 -
 .../freemarker/core/model/impl/APIModel.java    |   45 -
 .../core/model/impl/ArgumentTypes.java          |  647 ---
 .../core/model/impl/BeanAndStringModel.java     |   53 -
 .../freemarker/core/model/impl/BeanModel.java   |  339 --
 .../model/impl/CallableMemberDescriptor.java    |   56 -
 .../core/model/impl/CharacterOrString.java      |   45 -
 .../core/model/impl/ClassBasedModelFactory.java |  148 -
 .../core/model/impl/ClassChangeNotifier.java    |   32 -
 .../core/model/impl/ClassIntrospector.java      | 1263 ------
 .../core/model/impl/CollectionAdapter.java      |   88 -
 .../core/model/impl/CollectionAndSequence.java  |  111 -
 .../core/model/impl/DefaultArrayAdapter.java    |  378 --
 .../model/impl/DefaultEnumerationAdapter.java   |  128 -
 .../core/model/impl/DefaultIterableAdapter.java |   94 -
 .../core/model/impl/DefaultIteratorAdapter.java |  138 -
 .../core/model/impl/DefaultListAdapter.java     |  123 -
 .../core/model/impl/DefaultMapAdapter.java      |  171 -
 .../impl/DefaultNonListCollectionAdapter.java   |  103 -
 .../core/model/impl/DefaultObjectWrapper.java   | 1773 --------
 .../DefaultObjectWrapperTCCLSingletonUtil.java  |  129 -
 .../DefaultUnassignableIteratorAdapter.java     |   59 -
 .../impl/EmptyCallableMemberDescriptor.java     |   35 -
 .../model/impl/EmptyMemberAndArguments.java     |   93 -
 .../freemarker/core/model/impl/EnumModels.java  |   50 -
 .../freemarker/core/model/impl/HashAdapter.java |  181 -
 .../model/impl/InvalidPropertyException.java    |   34 -
 .../model/impl/JRebelClassChangeNotifier.java   |   58 -
 .../core/model/impl/JavaMethodModel.java        |  105 -
 .../model/impl/MapKeyValuePairIterator.java     |   77 -
 .../MaybeEmptyCallableMemberDescriptor.java     |   25 -
 .../impl/MaybeEmptyMemberAndArguments.java      |   22 -
 .../core/model/impl/MemberAndArguments.java     |   64 -
 .../model/impl/MethodAppearanceFineTuner.java   |  156 -
 .../core/model/impl/MethodSorter.java           |   36 -
 .../NonPrimitiveArrayBackedReadOnlyList.java    |   42 -
 .../model/impl/OverloadedFixArgsMethods.java    |   99 -
 .../core/model/impl/OverloadedMethods.java      |  271 --
 .../core/model/impl/OverloadedMethodsModel.java |   65 -
 .../model/impl/OverloadedMethodsSubset.java     |  402 --
 .../core/model/impl/OverloadedNumberUtil.java   | 1289 ------
 .../model/impl/OverloadedVarArgsMethods.java    |  245 --
 .../impl/PrimtiveArrayBackedReadOnlyList.java   |   47 -
 .../ReflectionCallableMemberDescriptor.java     |   95 -
 .../core/model/impl/ResourceBundleModel.java    |  181 -
 .../model/impl/RestrictedObjectWrapper.java     |   98 -
 .../core/model/impl/SequenceAdapter.java        |   68 -
 .../freemarker/core/model/impl/SetAdapter.java  |   32 -
 .../core/model/impl/SimpleCollection.java       |  138 -
 .../freemarker/core/model/impl/SimpleDate.java  |   85 -
 .../freemarker/core/model/impl/SimpleHash.java  |  296 --
 .../core/model/impl/SimpleMethod.java           |  174 -
 .../core/model/impl/SimpleNumber.java           |   77 -
 .../core/model/impl/SimpleScalar.java           |   73 -
 .../core/model/impl/SimpleSequence.java         |  162 -
 .../core/model/impl/SingletonCustomizer.java    |   51 -
 .../freemarker/core/model/impl/StaticModel.java |  177 -
 .../core/model/impl/StaticModels.java           |   43 -
 .../model/impl/TemplateModelListSequence.java   |   58 -
 .../freemarker/core/model/impl/TypeFlags.java   |  130 -
 .../core/model/impl/UnsafeMethods.java          |  112 -
 .../freemarker/core/model/impl/_MethodUtil.java |  319 --
 .../freemarker/core/model/impl/_ModelAPI.java   |  122 -
 .../freemarker/core/model/impl/package.html     |   26 -
 .../apache/freemarker/core/model/package.html   |   25 -
 .../outputformat/CommonMarkupOutputFormat.java  |  124 -
 .../CommonTemplateMarkupOutputModel.java        |   69 -
 .../core/outputformat/MarkupOutputFormat.java   |  135 -
 .../core/outputformat/OutputFormat.java         |   86 -
 .../UnregisteredOutputFormatException.java      |   39 -
 .../core/outputformat/impl/CSSOutputFormat.java |   54 -
 .../impl/CombinedMarkupOutputFormat.java        |  108 -
 .../outputformat/impl/HTMLOutputFormat.java     |   77 -
 .../outputformat/impl/JSONOutputFormat.java     |   54 -
 .../impl/JavaScriptOutputFormat.java            |   55 -
 .../impl/PlainTextOutputFormat.java             |   58 -
 .../core/outputformat/impl/RTFOutputFormat.java |   77 -
 .../impl/TemplateCombinedMarkupOutputModel.java |   52 -
 .../impl/TemplateHTMLOutputModel.java           |   42 -
 .../impl/TemplateRTFOutputModel.java            |   42 -
 .../impl/TemplateXHTMLOutputModel.java          |   42 -
 .../impl/TemplateXMLOutputModel.java            |   42 -
 .../impl/UndefinedOutputFormat.java             |   58 -
 .../outputformat/impl/XHTMLOutputFormat.java    |   77 -
 .../core/outputformat/impl/XMLOutputFormat.java |   77 -
 .../core/outputformat/impl/package.html         |   26 -
 .../freemarker/core/outputformat/package.html   |   25 -
 .../org/apache/freemarker/core/package.html     |   27 -
 .../core/templateresolver/AndMatcher.java       |   45 -
 .../core/templateresolver/CacheStorage.java     |   37 -
 .../CacheStorageWithGetSize.java                |   36 -
 ...ConditionalTemplateConfigurationFactory.java |   65 -
 .../templateresolver/FileExtensionMatcher.java  |   85 -
 .../templateresolver/FileNameGlobMatcher.java   |   86 -
 .../FirstMatchTemplateConfigurationFactory.java |  110 -
 .../templateresolver/GetTemplateResult.java     |   89 -
 .../MalformedTemplateNameException.java         |   60 -
 .../MergingTemplateConfigurationFactory.java    |   63 -
 .../core/templateresolver/NotMatcher.java       |   41 -
 .../core/templateresolver/OrMatcher.java        |   45 -
 .../core/templateresolver/PathGlobMatcher.java  |  100 -
 .../core/templateresolver/PathRegexMatcher.java |   54 -
 .../TemplateConfigurationFactory.java           |   54 -
 .../TemplateConfigurationFactoryException.java  |   36 -
 .../core/templateresolver/TemplateLoader.java   |  104 -
 .../templateresolver/TemplateLoaderSession.java |   76 -
 .../templateresolver/TemplateLoadingResult.java |  208 -
 .../TemplateLoadingResultStatus.java            |   49 -
 .../templateresolver/TemplateLoadingSource.java |   69 -
 .../templateresolver/TemplateLookupContext.java |  112 -
 .../templateresolver/TemplateLookupResult.java  |   54 -
 .../TemplateLookupStrategy.java                 |   78 -
 .../templateresolver/TemplateNameFormat.java    |   53 -
 .../core/templateresolver/TemplateResolver.java |  166 -
 .../templateresolver/TemplateSourceMatcher.java |   30 -
 .../core/templateresolver/_CacheAPI.java        |   43 -
 .../impl/ByteArrayTemplateLoader.java           |  199 -
 .../impl/ClassTemplateLoader.java               |  184 -
 .../impl/DefaultTemplateLookupStrategy.java     |   61 -
 .../impl/DefaultTemplateNameFormat.java         |  309 --
 .../impl/DefaultTemplateNameFormatFM2.java      |  105 -
 .../impl/DefaultTemplateResolver.java           |  904 ----
 .../impl/FileTemplateLoader.java                |  383 --
 .../templateresolver/impl/MruCacheStorage.java  |  330 --
 .../impl/MultiTemplateLoader.java               |  172 -
 .../templateresolver/impl/NullCacheStorage.java |   71 -
 .../templateresolver/impl/SoftCacheStorage.java |  112 -
 .../impl/StringTemplateLoader.java              |  199 -
 .../impl/StrongCacheStorage.java                |   70 -
 ...emplateLoaderBasedTemplateLookupContext.java |   66 -
 ...TemplateLoaderBasedTemplateLookupResult.java |  124 -
 .../impl/URLTemplateLoader.java                 |  229 -
 .../impl/URLTemplateLoadingSource.java          |   58 -
 .../impl/_TemplateLoaderUtils.java              |   43 -
 .../core/templateresolver/impl/package.html     |   26 -
 .../core/templateresolver/package.html          |   25 -
 .../freemarker/core/util/BugException.java      |   52 -
 .../freemarker/core/util/CaptureOutput.java     |  147 -
 .../freemarker/core/util/CommonBuilder.java     |   35 -
 .../apache/freemarker/core/util/DeepUnwrap.java |  153 -
 .../apache/freemarker/core/util/FTLUtil.java    |  805 ----
 .../core/util/GenericParseException.java        |   40 -
 .../apache/freemarker/core/util/HtmlEscape.java |  109 -
 .../freemarker/core/util/NormalizeNewlines.java |  115 -
 .../freemarker/core/util/ObjectFactory.java     |   31 -
 .../core/util/OptInTemplateClassResolver.java   |  160 -
 .../core/util/ProductWrappingBuilder.java       |   38 -
 .../freemarker/core/util/StandardCompress.java  |  239 -
 .../core/util/UndeclaredThrowableException.java |   43 -
 .../util/UnrecognizedTimeZoneException.java     |   38 -
 .../util/UnsupportedNumberClassException.java   |   38 -
 .../apache/freemarker/core/util/XmlEscape.java  |   92 -
 .../freemarker/core/util/_ArrayEnumeration.java |   51 -
 .../freemarker/core/util/_ArrayIterator.java    |   54 -
 .../apache/freemarker/core/util/_ClassUtil.java |  182 -
 .../freemarker/core/util/_CollectionUtil.java   |   36 -
 .../apache/freemarker/core/util/_DateUtil.java  |  914 ----
 .../freemarker/core/util/_JavaVersions.java     |   80 -
 .../freemarker/core/util/_KeyValuePair.java     |   61 -
 .../freemarker/core/util/_LocaleUtil.java       |   43 -
 .../core/util/_NullArgumentException.java       |   59 -
 .../freemarker/core/util/_NullWriter.java       |   90 -
 .../freemarker/core/util/_NumberUtil.java       |  228 -
 .../freemarker/core/util/_ObjectHolder.java     |   55 -
 .../freemarker/core/util/_SecurityUtil.java     |   87 -
 .../freemarker/core/util/_SortedArraySet.java   |   80 -
 .../freemarker/core/util/_StringUtil.java       | 1675 -------
 .../core/util/_UnmodifiableCompositeSet.java    |   98 -
 .../freemarker/core/util/_UnmodifiableSet.java  |   47 -
 .../apache/freemarker/core/util/package.html    |   25 -
 .../InvalidFormatParametersException.java       |   37 -
 .../InvalidFormatStringException.java           |   37 -
 .../ParsingNotSupportedException.java           |   37 -
 .../core/valueformat/TemplateDateFormat.java    |  110 -
 .../valueformat/TemplateDateFormatFactory.java  |   95 -
 .../core/valueformat/TemplateFormatUtil.java    |   77 -
 .../core/valueformat/TemplateNumberFormat.java  |   93 -
 .../TemplateNumberFormatFactory.java            |   67 -
 .../core/valueformat/TemplateValueFormat.java   |   42 -
 .../TemplateValueFormatException.java           |   37 -
 .../valueformat/TemplateValueFormatFactory.java |   28 -
 .../UndefinedCustomFormatException.java         |   34 -
 .../UnformattableValueException.java            |   41 -
 ...nDateTypeFormattingUnsupportedException.java |   36 -
 ...nownDateTypeParsingUnsupportedException.java |   37 -
 .../valueformat/UnparsableValueException.java   |   38 -
 ...AliasTargetTemplateValueFormatException.java |   38 -
 .../impl/AliasTemplateDateFormatFactory.java    |   97 -
 .../impl/AliasTemplateNumberFormatFactory.java  |   96 -
 .../impl/ExtendedDecimalFormatParser.java       |  530 ---
 .../impl/ISOLikeTemplateDateFormat.java         |  270 --
 .../impl/ISOLikeTemplateDateFormatFactory.java  |   57 -
 .../valueformat/impl/ISOTemplateDateFormat.java |   90 -
 .../impl/ISOTemplateDateFormatFactory.java      |   56 -
 .../impl/JavaTemplateDateFormat.java            |   75 -
 .../impl/JavaTemplateDateFormatFactory.java     |  187 -
 .../impl/JavaTemplateNumberFormat.java          |   64 -
 .../impl/JavaTemplateNumberFormatFactory.java   |  133 -
 .../valueformat/impl/XSTemplateDateFormat.java  |   94 -
 .../impl/XSTemplateDateFormatFactory.java       |   51 -
 .../core/valueformat/impl/package.html          |   26 -
 .../freemarker/core/valueformat/package.html    |   25 -
 .../java/org/apache/freemarker/dom/AtAtKey.java |   58 -
 .../freemarker/dom/AttributeNodeModel.java      |   69 -
 .../freemarker/dom/CharacterDataNodeModel.java  |   46 -
 .../apache/freemarker/dom/DocumentModel.java    |   76 -
 .../freemarker/dom/DocumentTypeModel.java       |   56 -
 .../java/org/apache/freemarker/dom/DomLog.java  |   32 -
 .../apache/freemarker/dom/DomStringUtil.java    |   67 -
 .../org/apache/freemarker/dom/ElementModel.java |  234 -
 .../freemarker/dom/JaxenXPathSupport.java       |  243 -
 .../apache/freemarker/dom/NodeListModel.java    |  219 -
 .../org/apache/freemarker/dom/NodeModel.java    |  613 ---
 .../apache/freemarker/dom/NodeOutputter.java    |  258 --
 .../dom/NodeQueryResultItemObjectWrapper.java   |   92 -
 .../org/apache/freemarker/dom/PINodeModel.java  |   45 -
 .../dom/SunInternalXalanXPathSupport.java       |  163 -
 .../org/apache/freemarker/dom/XPathSupport.java |   30 -
 .../freemarker/dom/XalanXPathSupport.java       |  163 -
 .../java/org/apache/freemarker/dom/package.html |   30 -
 .../servlet/AllHttpScopesHashModel.java         |  114 -
 .../freemarker/servlet/FreemarkerServlet.java   | 1611 -------
 .../FreemarkerServletConfigurationBuilder.java  |   79 -
 .../servlet/HttpRequestHashModel.java           |  108 -
 .../servlet/HttpRequestParametersHashModel.java |  104 -
 .../servlet/HttpSessionHashModel.java           |  113 -
 .../apache/freemarker/servlet/IncludePage.java  |  254 --
 .../freemarker/servlet/InitParamParser.java     |  264 --
 .../servlet/ServletContextHashModel.java        |   62 -
 .../servlet/WebAppTemplateLoader.java           |  301 --
 .../apache/freemarker/servlet/_ServletLogs.java |   34 -
 .../jsp/CustomTagAndELFunctionCombiner.java     |  202 -
 .../freemarker/servlet/jsp/EventForwarding.java |  200 -
 .../jsp/FreeMarkerJspApplicationContext.java    |  165 -
 .../servlet/jsp/FreeMarkerJspFactory.java       |   63 -
 .../servlet/jsp/FreeMarkerJspFactory21.java     |   51 -
 .../servlet/jsp/FreeMarkerPageContext.java      |  460 --
 .../freemarker/servlet/jsp/JspTagModelBase.java |  162 -
 .../servlet/jsp/JspWriterAdapter.java           |  188 -
 .../servlet/jsp/PageContextFactory.java         |   66 -
 .../servlet/jsp/SimpleTagDirectiveModel.java    |  111 -
 .../servlet/jsp/TagTransformModel.java          |  419 --
 .../freemarker/servlet/jsp/TaglibFactory.java   | 2015 ---------
 .../servlet/jsp/TaglibMethodUtil.java           |  117 -
 .../servlet/jsp/_FreeMarkerPageContext21.java   |  122 -
 .../apache/freemarker/servlet/jsp/package.html  |   26 -
 .../org/apache/freemarker/servlet/package.html  |   26 -
 src/main/javacc/FTL.jj                          | 4132 ------------------
 .../adhoc/IdentifierCharGenerator.java          |  546 ---
 src/main/misc/overloadedNumberRules/README.txt  |   34 -
 src/main/misc/overloadedNumberRules/config.fmpp |   73 -
 .../misc/overloadedNumberRules/generator.ftl    |   80 -
 src/main/misc/overloadedNumberRules/prices.ods  |  Bin 17855 -> 0 bytes
 .../core/model/impl/unsafeMethods.properties    |   98 -
 .../apache/freemarker/core/version.properties   |  104 -
 src/manual/en_US/FM3-CHANGE-LOG.txt             |  226 -
 src/manual/en_US/book.xml                       |   82 -
 src/manual/en_US/docgen-help/editors-readme.txt |  130 -
 .../en_US/docgen-misc/copyrightComment.txt      |   16 -
 .../en_US/docgen-misc/googleAnalytics.html      |   14 -
 .../figures/model2sketch_with_alpha.png         |  Bin 61463 -> 0 bytes
 .../figures/odg-convert-howto.txt               |   43 -
 .../en_US/docgen-originals/figures/overview.odg |  Bin 11939 -> 0 bytes
 .../figures/tree_with_alpha.png                 |  Bin 10304 -> 0 bytes
 src/manual/en_US/docgen.cjson                   |  132 -
 src/manual/en_US/favicon.png                    |  Bin 1291 -> 0 bytes
 src/manual/en_US/figures/model2sketch.png       |  Bin 21425 -> 0 bytes
 src/manual/en_US/figures/overview.png           |  Bin 11837 -> 0 bytes
 src/manual/en_US/figures/tree.png               |  Bin 4699 -> 0 bytes
 src/manual/en_US/logo.png                       |  Bin 10134 -> 0 bytes
 src/manual/zh_CN/book.xml                       |   82 -
 src/manual/zh_CN/docgen-help/README             |    2 -
 .../zh_CN/docgen-misc/googleAnalytics.html      |   14 -
 .../zh_CN/docgen-originals/figures/README       |    2 -
 src/manual/zh_CN/docgen.cjson                   |  130 -
 src/manual/zh_CN/favicon.png                    |  Bin 1291 -> 0 bytes
 src/manual/zh_CN/figures/model2sketch.png       |  Bin 21425 -> 0 bytes
 src/manual/zh_CN/figures/overview.png           |  Bin 11837 -> 0 bytes
 src/manual/zh_CN/figures/tree.png               |  Bin 4699 -> 0 bytes
 src/manual/zh_CN/logo.png                       |  Bin 10134 -> 0 bytes
 .../core/ASTBasedErrorMessagesTest.java         |   74 -
 .../org/apache/freemarker/core/ASTPrinter.java  |  438 --
 .../org/apache/freemarker/core/ASTTest.java     |  103 -
 .../core/ActualNamingConvetionTest.java         |   66 -
 .../freemarker/core/ActualTagSyntaxTest.java    |   68 -
 .../freemarker/core/BreakPlacementTest.java     |   56 -
 .../apache/freemarker/core/CamelCaseTest.java   |  486 --
 .../freemarker/core/CanonicalFormTest.java      |   68 -
 .../freemarker/core/CoercionToTextualTest.java  |  145 -
 .../freemarker/core/ConfigurableTest.java       |  176 -
 .../freemarker/core/ConfigurationTest.java      | 1480 -------
 .../freemarker/core/CoreLocaleUtilsTest.java    |   73 -
 .../freemarker/core/CustomAttributeTest.java    |  163 -
 .../apache/freemarker/core/DateFormatTest.java  |  464 --
 .../freemarker/core/DirectiveCallPlaceTest.java |  249 --
 .../freemarker/core/EncodingOverrideTest.java   |   62 -
 .../EnvironmentGetTemplateVariantsTest.java     |  214 -
 .../apache/freemarker/core/ExceptionTest.java   |  115 -
 .../apache/freemarker/core/GetSourceTest.java   |   52 -
 .../freemarker/core/HeaderParsingTest.java      |   60 -
 .../IncludeAndImportConfigurableLayersTest.java |  354 --
 .../freemarker/core/IncludeAndImportTest.java   |  270 --
 .../freemarker/core/IncudeFromNamelessTest.java |   58 -
 .../core/InterpretAndEvalTemplateNameTest.java  |   70 -
 .../core/InterpretSettingInheritanceTest.java   |  104 -
 .../freemarker/core/IteratorIssuesTest.java     |   64 -
 .../core/JavaCCExceptionAsEOFFixTest.java       |  126 -
 .../apache/freemarker/core/ListErrorsTest.java  |  130 -
 .../freemarker/core/MiscErrorMessagesTest.java  |   48 -
 .../core/MistakenlyPublicImportAPIsTest.java    |  104 -
 .../core/MistakenlyPublicMacroAPIsTest.java     |   88 -
 .../freemarker/core/MockServletContext.java     |  157 -
 .../core/NewBiObjectWrapperRestrictionTest.java |   50 -
 .../core/ObjectBuilderSettingsTest.java         | 1499 -------
 .../core/OptInTemplateClassResolverTest.java    |  230 -
 .../freemarker/core/OutputFormatTest.java       | 1068 -----
 .../ParseTimeParameterBIErrorMessagesTest.java  |   46 -
 .../core/ParsingErrorMessagesTest.java          |  116 -
 .../core/RestrictedObjectWrapperTest.java       |   72 -
 .../core/RestrictedObjetWrapperTest.java        |  112 -
 .../apache/freemarker/core/SQLTimeZoneTest.java |  371 --
 .../freemarker/core/SettingDirectiveTest.java   |   40 -
 .../freemarker/core/SpecialVariableTest.java    |  114 -
 .../core/StringLiteralInterpolationTest.java    |  133 -
 .../org/apache/freemarker/core/TabSizeTest.java |   91 -
 .../core/TagSyntaxVariationsTest.java           |  186 -
 .../core/TemplateConfigurationTest.java         |  909 ----
 ...gurationWithDefaultTemplateResolverTest.java |  267 --
 .../core/TemplateConstructorsTest.java          |  113 -
 .../core/TemplateGetEncodingTest.java           |   64 -
 .../core/TemplateLookupStrategyTest.java        |  669 ---
 .../core/TemplateNameSpecialVariablesTest.java  |  159 -
 .../core/TemplateNotFoundMessageTest.java       |  219 -
 .../core/TheadInterruptingSupportTest.java      |  163 -
 .../freemarker/core/TypeErrorMessagesTest.java  |  105 -
 .../freemarker/core/UnclosedCommentTest.java    |   41 -
 .../org/apache/freemarker/core/VersionTest.java |  227 -
 .../core/WhitespaceStrippingTest.java           |   63 -
 .../freemarker/core/XHTMLOutputFormatTest.java  |   59 -
 .../freemarker/core/XMLOutputFormatTest.java    |   59 -
 .../impl/AbstractParallelIntrospectionTest.java |  126 -
 .../model/impl/AlphabeticalMethodSorter.java    |   45 -
 .../core/model/impl/BridgeMethodsBean.java      |   30 -
 .../core/model/impl/BridgeMethodsBeanBase.java  |   29 -
 .../CommonSupertypeForUnwrappingHintTest.java   |  129 -
 .../model/impl/DefaultObjectWrapperDesc.java    |   31 -
 .../model/impl/DefaultObjectWrapperInc.java     |   31 -
 ...jectWrapperModelFactoryRegistrationTest.java |   63 -
 .../DefaultObjectWrapperSingletonsTest.java     |  675 ---
 .../model/impl/DefaultObjectWrapperTest.java    |  901 ----
 .../core/model/impl/EnumModelsTest.java         |   85 -
 .../core/model/impl/ErrorMessagesTest.java      |  170 -
 .../impl/FineTuneMethodAppearanceTest.java      |   65 -
 .../GetlessMethodsAsPropertyGettersRule.java    |   67 -
 .../core/model/impl/IsApplicableTest.java       |  171 -
 .../impl/IsMoreSpecificParameterTypeTest.java   |   98 -
 .../Java7MembersOnlyDefaultObjectWrapper.java   |  101 -
 ...Java8BridgeMethodsWithDefaultMethodBean.java |   29 -
 ...ava8BridgeMethodsWithDefaultMethodBean2.java |   23 -
 ...8BridgeMethodsWithDefaultMethodBeanBase.java |   31 -
 ...BridgeMethodsWithDefaultMethodBeanBase2.java |   28 -
 .../model/impl/Java8DefaultMethodsBean.java     |   84 -
 .../model/impl/Java8DefaultMethodsBeanBase.java |   97 -
 ...a8DefaultObjectWrapperBridgeMethodsTest.java |   65 -
 .../impl/Java8DefaultObjectWrapperTest.java     |  160 -
 .../impl/ManyObjectsOfDifferentClasses.java     |  249 --
 .../impl/ManyStaticsOfDifferentClasses.java     |  236 -
 .../model/impl/MiscNumericalOperationsTest.java |  111 -
 .../model/impl/ModelAPINewInstanceTest.java     |  134 -
 .../core/model/impl/ModelCacheTest.java         |   71 -
 .../model/impl/OverloadedNumberUtilTest.java    |  585 ---
 .../impl/ParameterListPreferabilityTest.java    |  445 --
 .../impl/PrallelObjectIntrospectionTest.java    |   43 -
 .../impl/PrallelStaticIntrospectionTest.java    |   47 -
 .../core/model/impl/RationalNumber.java         |   90 -
 .../core/model/impl/StaticModelsTest.java       |   91 -
 .../core/model/impl/TypeFlagsTest.java          |  671 ---
 .../core/outputformat/_OutputFormatTestAPI.java |   35 -
 .../impl/CombinedMarkupOutputFormatTest.java    |  194 -
 .../outputformat/impl/HTMLOutputFormatTest.java |  187 -
 .../outputformat/impl/RTFOutputFormatTest.java  |  129 -
 .../DefaultTemplateResolverTest.java            |  365 --
 .../FileTemplateLoaderTest.java                 |  122 -
 .../MultiTemplateLoaderTest.java                |   99 -
 .../TemplateConfigurationFactoryTest.java       |  203 -
 .../TemplateNameFormatTest.java                 |  330 --
 .../TemplateSourceMatcherTest.java              |  188 -
 .../AppMetaTemplateDateFormatFactory.java       |  129 -
 .../BaseNTemplateNumberFormatFactory.java       |  128 -
 .../core/userpkg/CustomHTMLOutputFormat.java    |   72 -
 .../core/userpkg/CustomTemplateHTMLModel.java   |   34 -
 .../core/userpkg/DummyOutputFormat.java         |   65 -
 ...EpochMillisDivTemplateDateFormatFactory.java |  102 -
 .../EpochMillisTemplateDateFormatFactory.java   |   92 -
 .../HTMLISOTemplateDateFormatFactory.java       |  114 -
 .../userpkg/HexTemplateNumberFormatFactory.java |   77 -
 ...AndTZSensitiveTemplateDateFormatFactory.java |   97 -
 ...aleSensitiveTemplateNumberFormatFactory.java |   78 -
 .../core/userpkg/PackageVisibleAll.java         |   26 -
 .../userpkg/PackageVisibleAllWithBuilder.java   |   26 -
 .../PackageVisibleAllWithBuilderBuilder.java    |   28 -
 .../PackageVisibleWithPublicConstructor.java    |   27 -
 .../PrintfGTemplateNumberFormatFactory.java     |  138 -
 .../freemarker/core/userpkg/PublicAll.java      |   24 -
 .../userpkg/PublicWithMixedConstructors.java    |   38 -
 .../PublicWithPackageVisibleConstructor.java    |   26 -
 .../core/userpkg/SeldomEscapedOutputFormat.java |   71 -
 .../core/userpkg/TemplateDummyOutputModel.java  |   34 -
 .../TemplateSeldomEscapedOutputModel.java       |   34 -
 .../freemarker/core/util/DateUtilTest.java      | 1085 -----
 .../freemarker/core/util/FTLUtilTest.java       |  117 -
 .../freemarker/core/util/NumberUtilTest.java    |  215 -
 .../freemarker/core/util/StringUtilTest.java    |  403 --
 .../core/valueformat/NumberFormatTest.java      |  365 --
 .../impl/ExtendedDecimalFormatTest.java         |  343 --
 .../apache/freemarker/dom/DOMSiblingTest.java   |   99 -
 .../freemarker/dom/DOMSimplifiersTest.java      |  201 -
 .../java/org/apache/freemarker/dom/DOMTest.java |  159 -
 .../manualtest/AutoEscapingExample.java         |   72 -
 .../ConfigureOutputFormatExamples.java          |  105 -
 .../manualtest/CustomFormatsExample.java        |   82 -
 .../manualtest/GettingStartedExample.java       |   69 -
 .../apache/freemarker/manualtest/Product.java   |   49 -
 .../TemplateConfigurationExamples.java          |  191 -
 .../UnitAwareTemplateNumberFormatFactory.java   |   80 -
 .../UnitAwareTemplateNumberModel.java           |   43 -
 .../servlet/FreemarkerServletTest.java          |  626 ---
 .../freemarker/servlet/InitParamParserTest.java |  164 -
 .../servlet/jsp/JspTestFreemarkerServlet.java   |   50 -
 ...estFreemarkerServletWithDefaultOverride.java |   47 -
 .../servlet/jsp/RealServletContainertTest.java  |  505 ---
 .../freemarker/servlet/jsp/TLDParsingTest.java  |  135 -
 .../servlet/jsp/TaglibMethodUtilTest.java       |  107 -
 .../jsp/taglibmembers/AttributeAccessorTag.java |   68 -
 .../jsp/taglibmembers/AttributeInfoTag.java     |   59 -
 .../jsp/taglibmembers/EnclosingClass.java       |   32 -
 .../servlet/jsp/taglibmembers/GetAndSetTag.java |   66 -
 .../jsp/taglibmembers/TestFunctions.java        |   79 -
 .../jsp/taglibmembers/TestSimpleTag.java        |   54 -
 .../jsp/taglibmembers/TestSimpleTag2.java       |   32 -
 .../jsp/taglibmembers/TestSimpleTag3.java       |   32 -
 .../servlet/jsp/taglibmembers/TestTag.java      |  100 -
 .../servlet/jsp/taglibmembers/TestTag2.java     |   50 -
 .../servlet/jsp/taglibmembers/TestTag3.java     |   50 -
 .../config/WebappLocalFreemarkerServlet.java    |   25 -
 .../CopyrightCommentRemoverTemplateLoader.java  |  105 -
 .../test/MonitoredTemplateLoader.java           |  325 --
 .../freemarker/test/ResourcesExtractor.java     |  295 --
 .../apache/freemarker/test/TemplateTest.java    |  342 --
 .../test/TestConfigurationBuilder.java          |   92 -
 .../freemarker/test/hamcerst/Matchers.java      |   34 -
 .../hamcerst/StringContainsIgnoringCase.java    |   47 -
 .../org/apache/freemarker/test/package.html     |   28 -
 .../test/servlet/DefaultModel2TesterAction.java |   92 -
 .../freemarker/test/servlet/Model2Action.java   |   37 -
 .../test/servlet/Model2TesterServlet.java       |  142 -
 .../freemarker/test/servlet/WebAppTestCase.java |  360 --
 .../test/templatesuite/TemplateTestCase.java    |  515 ---
 .../test/templatesuite/TemplateTestSuite.java   |  298 --
 .../templatesuite/models/AllTemplateModels.java |  128 -
 .../templatesuite/models/BeanTestClass.java     |   93 -
 .../templatesuite/models/BeanTestInterface.java |   25 -
 .../models/BeanTestSuperclass.java              |   30 -
 .../models/BooleanAndScalarModel.java           |   40 -
 .../models/BooleanAndStringTemplateModel.java   |   38 -
 .../test/templatesuite/models/BooleanHash1.java |   58 -
 .../test/templatesuite/models/BooleanHash2.java |   50 -
 .../test/templatesuite/models/BooleanList1.java |   62 -
 .../test/templatesuite/models/BooleanList2.java |   53 -
 .../models/BooleanVsStringMethods.java          |   40 -
 .../templatesuite/models/EnumTestClass.java     |   34 -
 .../templatesuite/models/ExceptionModel.java    |   39 -
 .../models/HashAndScalarModel.java              |   84 -
 .../templatesuite/models/JavaObjectInfo.java    |   35 -
 .../test/templatesuite/models/Listables.java    |  185 -
 .../test/templatesuite/models/MultiModel1.java  |  116 -
 .../test/templatesuite/models/MultiModel2.java  |   63 -
 .../test/templatesuite/models/MultiModel3.java  |   69 -
 .../test/templatesuite/models/MultiModel4.java  |   77 -
 .../test/templatesuite/models/MultiModel5.java  |   81 -
 .../test/templatesuite/models/NewTestModel.java |   52 -
 .../templatesuite/models/NewTestModel2.java     |   52 -
 .../models/NumberAndStringModel.java            |   47 -
 .../models/OverloadedConstructor.java           |   46 -
 .../templatesuite/models/OverloadedMethods.java |  191 -
 .../models/OverloadedMethods2.java              | 1110 -----
 .../templatesuite/models/SimpleTestMethod.java  |   49 -
 .../models/TransformHashWrapper.java            |   79 -
 .../models/TransformMethodWrapper1.java         |   49 -
 .../models/TransformMethodWrapper2.java         |   64 -
 .../templatesuite/models/TransformModel1.java   |  175 -
 .../templatesuite/models/VarArgTestModel.java   |   63 -
 .../freemarker/test/templatesuite/package.html  |   42 -
 .../freemarker/test/util/AssertDirective.java   |   73 -
 .../test/util/AssertEqualsDirective.java        |   91 -
 .../test/util/AssertFailsDirective.java         |  152 -
 .../AssertationFailedInTemplateException.java   |   46 -
 .../test/util/BadParameterTypeException.java    |   60 -
 .../test/util/EntirelyCustomObjectWrapper.java  |   91 -
 .../freemarker/test/util/FileTestCase.java      |  216 -
 .../util/MissingRequiredParameterException.java |   51 -
 .../freemarker/test/util/NoOutputDirective.java |   50 -
 .../test/util/ParameterException.java           |   54 -
 .../SimpleMapAndCollectionObjectWrapper.java    |   60 -
 .../apache/freemarker/test/util/TestUtil.java   |  266 --
 .../util/UnsupportedParameterException.java     |   50 -
 .../apache/freemarker/test/util/XMLLoader.java  |  138 -
 src/test/resources/META-INF/malformed.tld       |   31 -
 .../tldDiscovery MetaInfTldSources-1.tld        |   31 -
 src/test/resources/logback-test.xml             |   34 -
 .../org/apache/freemarker/core/ast-1.ast        |  187 -
 .../org/apache/freemarker/core/ast-1.ftl        |   29 -
 .../apache/freemarker/core/ast-assignments.ast  |  172 -
 .../apache/freemarker/core/ast-assignments.ftl  |   29 -
 .../org/apache/freemarker/core/ast-builtins.ast |   59 -
 .../org/apache/freemarker/core/ast-builtins.ftl |   23 -
 .../apache/freemarker/core/ast-locations.ast    |  155 -
 .../apache/freemarker/core/ast-locations.ftl    |   36 -
 .../core/ast-mixedcontentsimplifications.ast    |   38 -
 .../core/ast-mixedcontentsimplifications.ftl    |   26 -
 .../core/ast-multipleignoredchildren.ast        |   30 -
 .../core/ast-multipleignoredchildren.ftl        |   33 -
 .../core/ast-nestedignoredchildren.ast          |   20 -
 .../core/ast-nestedignoredchildren.ftl          |   19 -
 .../org/apache/freemarker/core/ast-range.ast    |  281 --
 .../org/apache/freemarker/core/ast-range.ftl    |   47 -
 .../freemarker/core/ast-strlitinterpolation.ast |   82 -
 .../freemarker/core/ast-strlitinterpolation.ftl |   25 -
 .../freemarker/core/ast-whitespacestripping.ast |   70 -
 .../freemarker/core/ast-whitespacestripping.ftl |   40 -
 .../apache/freemarker/core/cano-assignments.ftl |   35 -
 .../freemarker/core/cano-assignments.ftl.out    |   34 -
 .../apache/freemarker/core/cano-builtins.ftl    |   23 -
 .../freemarker/core/cano-builtins.ftl.out       |   23 -
 .../core/cano-identifier-escaping.ftl           |   76 -
 .../core/cano-identifier-escaping.ftl.out       |   44 -
 .../org/apache/freemarker/core/cano-macros.ftl  |   29 -
 .../apache/freemarker/core/cano-macros.ftl.out  |   28 -
 .../core/cano-strlitinterpolation.ftl           |   19 -
 .../core/cano-strlitinterpolation.ftl.out       |   19 -
 .../core/encodingOverride-ISO-8859-1.ftl        |   20 -
 .../freemarker/core/encodingOverride-UTF-8.ftl  |   20 -
 .../freemarker/core/templateresolver/test.ftl   |   19 -
 .../org/apache/freemarker/core/toCache1.ftl     |   19 -
 .../org/apache/freemarker/core/toCache2.ftl     |   19 -
 .../apache/freemarker/dom/DOMSiblingTest.xml    |   31 -
 .../manualtest/AutoEscapingExample-capture.ftlh |   21 -
 .../AutoEscapingExample-capture.ftlh.out        |   20 -
 .../manualtest/AutoEscapingExample-convert.ftlh |   27 -
 .../AutoEscapingExample-convert.ftlh.out        |   25 -
 .../manualtest/AutoEscapingExample-convert2.ftl |   25 -
 .../AutoEscapingExample-convert2.ftl.out        |   21 -
 .../manualtest/AutoEscapingExample-infoBox.ftlh |   26 -
 .../AutoEscapingExample-infoBox.ftlh.out        |   25 -
 .../manualtest/AutoEscapingExample-markup.ftlh  |   28 -
 .../AutoEscapingExample-markup.ftlh.out         |   26 -
 .../AutoEscapingExample-stringConcat.ftlh       |   19 -
 .../AutoEscapingExample-stringConcat.ftlh.out   |   19 -
 .../AutoEscapingExample-stringLiteral.ftlh      |   21 -
 .../AutoEscapingExample-stringLiteral.ftlh.out  |   20 -
 .../AutoEscapingExample-stringLiteral2.ftlh     |   25 -
 .../AutoEscapingExample-stringLiteral2.ftlh.out |   21 -
 .../ConfigureOutputFormatExamples1.properties   |   21 -
 .../ConfigureOutputFormatExamples2.properties   |   31 -
 .../manualtest/CustomFormatsExample-alias1.ftlh |   22 -
 .../CustomFormatsExample-alias1.ftlh.out        |   22 -
 .../manualtest/CustomFormatsExample-alias2.ftlh |   19 -
 .../CustomFormatsExample-alias2.ftlh.out        |   19 -
 .../CustomFormatsExample-modelAware.ftlh        |   20 -
 .../CustomFormatsExample-modelAware.ftlh.out    |   20 -
 .../TemplateConfigurationExamples1.properties   |   25 -
 .../TemplateConfigurationExamples2.properties   |   32 -
 .../TemplateConfigurationExamples3.properties   |   47 -
 .../org/apache/freemarker/manualtest/test.ftlh  |   28 -
 .../freemarker/servlet/jsp/TLDParsingTest.tld   |   89 -
 .../servlet/jsp/templates/classpath-test.ftl    |   19 -
 .../jsp/tldDiscovery-ClassPathTlds-1.tld        |   31 -
 .../jsp/tldDiscovery-ClassPathTlds-2.tld        |   31 -
 .../servlet/jsp/webapps/basic/CONTENTS.txt      |   36 -
 .../WEB-INF/el-function-tag-name-clash.tld      |   50 -
 .../jsp/webapps/basic/WEB-INF/el-functions.tld  |   84 -
 .../expected/attributes-modernModels.txt        |   73 -
 .../basic/WEB-INF/expected/attributes.txt       |   73 -
 .../basic/WEB-INF/expected/customTags1.txt      |  106 -
 .../servlet/jsp/webapps/basic/WEB-INF/test.tld  |   75 -
 .../servlet/jsp/webapps/basic/WEB-INF/web.xml   |  142 -
 .../servlet/jsp/webapps/basic/attributes.ftl    |   90 -
 .../jsp/webapps/basic/customELFunctions1.ftl    |   30 -
 .../jsp/webapps/basic/customELFunctions1.jsp    |   31 -
 .../servlet/jsp/webapps/basic/customTags1.ftl   |   59 -
 .../webapps/basic/elFunctionsTagNameClash.ftl   |   25 -
 .../webapps/basic/elFunctionsTagNameClash.jsp   |   26 -
 .../jsp/webapps/basic/trivial-jstl-@Ignore.ftl  |   48 -
 .../servlet/jsp/webapps/basic/trivial.ftl       |   37 -
 .../servlet/jsp/webapps/basic/trivial.jsp       |   45 -
 .../servlet/jsp/webapps/config/CONTENTS.txt     |   33 -
 .../webapps/config/WEB-INF/classes/sub/test.ftl |   19 -
 .../jsp/webapps/config/WEB-INF/classes/test.ftl |   19 -
 .../WEB-INF/lib/templates.jar/sub/test2.ftl     |   19 -
 .../webapps/config/WEB-INF/templates/test.ftl   |   19 -
 .../servlet/jsp/webapps/config/WEB-INF/web.xml  |  109 -
 .../servlet/jsp/webapps/config/test.ftl         |   19 -
 .../servlet/jsp/webapps/errors/CONTENTS.txt     |   28 -
 .../servlet/jsp/webapps/errors/WEB-INF/web.xml  |   92 -
 .../jsp/webapps/errors/failing-parsetime.ftlnv  |   20 -
 .../jsp/webapps/errors/failing-parsetime.jsp    |   19 -
 .../jsp/webapps/errors/failing-runtime.ftl      |   26 -
 .../jsp/webapps/errors/failing-runtime.jsp      |   23 -
 .../servlet/jsp/webapps/errors/not-failing.ftl  |   19 -
 .../jsp/webapps/multipleLoaders/CONTENTS.txt    |   24 -
 .../multipleLoaders/WEB-INF/templates/test.ftl  |   19 -
 .../jsp/webapps/multipleLoaders/WEB-INF/web.xml |   83 -
 .../jsp/webapps/tldDiscovery/CONTENTS.txt       |   37 -
 .../WEB-INF/expected/subdir/test-rel.txt        |   20 -
 .../WEB-INF/expected/test-noClasspath.txt       |   32 -
 .../tldDiscovery/WEB-INF/expected/test1.txt     |   73 -
 .../tldDiscovery/WEB-INF/fmtesttag 2.tld        |   32 -
 .../webapps/tldDiscovery/WEB-INF/fmtesttag4.tld |   32 -
 .../lib/taglib-foo.jar/META-INF/foo bar.tld     |   32 -
 .../WEB-INF/subdir-with-tld/fmtesttag3.tld      |   32 -
 .../WEB-INF/taglib 2.jar/META-INF/taglib.tld    |   31 -
 .../jsp/webapps/tldDiscovery/WEB-INF/web.xml    |  179 -
 .../tldDiscovery/not-auto-scanned/fmtesttag.tld |   40 -
 .../webapps/tldDiscovery/subdir/test-rel.ftl    |   20 -
 .../webapps/tldDiscovery/test-noClasspath.ftl   |   32 -
 .../servlet/jsp/webapps/tldDiscovery/test1.ftl  |   55 -
 .../org/apache/freemarker/test/servlet/web.xml  |  101 -
 .../test/templatesuite/expected/arithmetic.txt  |   46 -
 .../expected/boolean-formatting.txt             |   31 -
 .../test/templatesuite/expected/boolean.txt     |  102 -
 .../expected/charset-in-header.txt              |   26 -
 .../test/templatesuite/expected/comment.txt     |   34 -
 .../test/templatesuite/expected/comparisons.txt |   93 -
 .../test/templatesuite/expected/compress.txt    |   40 -
 .../templatesuite/expected/dateformat-java.txt  |   55 -
 .../expected/default-object-wrapper.txt         |   55 -
 .../templatesuite/expected/default-xmlns.txt    |   25 -
 .../test/templatesuite/expected/default.txt     |   26 -
 .../expected/encoding-builtins.txt              |   44 -
 .../test/templatesuite/expected/escapes.txt     |   49 -
 .../test/templatesuite/expected/exception.txt   |   43 -
 .../test/templatesuite/expected/exception2.txt  |   47 -
 .../test/templatesuite/expected/exception3.txt  |   21 -
 .../test/templatesuite/expected/exthash.txt     |   76 -
 .../test/templatesuite/expected/hashconcat.txt  |  138 -
 .../test/templatesuite/expected/hashliteral.txt |   74 -
 .../test/templatesuite/expected/helloworld.txt  |   31 -
 .../expected/identifier-escaping.txt            |   57 -
 .../expected/identifier-non-ascii.txt           |   19 -
 .../test/templatesuite/expected/if.txt          |  104 -
 .../test/templatesuite/expected/import.txt      |   40 -
 .../test/templatesuite/expected/include.txt     |   67 -
 .../test/templatesuite/expected/include2.txt    |   28 -
 .../test/templatesuite/expected/interpret.txt   |   23 -
 .../test/templatesuite/expected/iterators.txt   |   84 -
 .../templatesuite/expected/lastcharacter.txt    |   31 -
 .../test/templatesuite/expected/list-bis.txt    |   51 -
 .../test/templatesuite/expected/list.txt        |   51 -
 .../test/templatesuite/expected/list2.txt       |  211 -
 .../test/templatesuite/expected/list3.txt       |   57 -
 .../test/templatesuite/expected/listhash.txt    |  157 -
 .../templatesuite/expected/listhashliteral.txt  |   36 -
 .../test/templatesuite/expected/listliteral.txt |   75 -
 .../templatesuite/expected/localization.txt     |   32 -
 .../test/templatesuite/expected/logging.txt     |   27 -
 .../templatesuite/expected/loopvariable.txt     |   54 -
 .../templatesuite/expected/macros-return.txt    |   23 -
 .../test/templatesuite/expected/macros.txt      |   67 -
 .../test/templatesuite/expected/macros2.txt     |   22 -
 .../test/templatesuite/expected/multimodels.txt |   93 -
 .../test/templatesuite/expected/nested.txt      |   25 -
 .../expected/new-allowsnothing.txt              |   19 -
 .../expected/new-defaultresolver.txt            |   19 -
 .../test/templatesuite/expected/new-optin.txt   |   32 -
 .../test/templatesuite/expected/newlines1.txt   |   29 -
 .../test/templatesuite/expected/newlines2.txt   |   30 -
 .../test/templatesuite/expected/noparse.txt     |   54 -
 .../templatesuite/expected/number-format.txt    |   33 -
 .../templatesuite/expected/number-literal.txt   |   79 -
 .../templatesuite/expected/number-to-date.txt   |   31 -
 .../templatesuite/expected/numerical-cast.txt   |  462 --
 .../templatesuite/expected/output-encoding1.txt |   27 -
 .../templatesuite/expected/output-encoding2.txt |  Bin 1972 -> 0 bytes
 .../templatesuite/expected/output-encoding3.txt |   26 -
 .../test/templatesuite/expected/precedence.txt  |   48 -
 .../test/templatesuite/expected/recover.txt     |   26 -
 .../test/templatesuite/expected/root.txt        |   44 -
 .../expected/sequence-builtins.txt              |  404 --
 .../test/templatesuite/expected/specialvars.txt |   25 -
 .../string-builtins-regexps-matches.txt         |   99 -
 .../expected/string-builtins-regexps.txt        |  112 -
 .../templatesuite/expected/string-builtins1.txt |  112 -
 .../templatesuite/expected/string-builtins2.txt |  135 -
 .../templatesuite/expected/stringbimethods.txt  |   29 -
 .../templatesuite/expected/stringliteral.txt    |  Bin 1550 -> 0 bytes
 .../test/templatesuite/expected/switch.txt      |   80 -
 .../test/templatesuite/expected/transforms.txt  |   68 -
 .../templatesuite/expected/type-builtins.txt    |   33 -
 .../test/templatesuite/expected/var-layers.txt  |   37 -
 .../test/templatesuite/expected/varargs.txt     |   44 -
 .../test/templatesuite/expected/variables.txt   |   62 -
 .../templatesuite/expected/whitespace-trim.txt  |   60 -
 .../templatesuite/expected/wstrip-in-header.txt |   23 -
 .../test/templatesuite/expected/wstripping.txt  |   39 -
 .../templatesuite/expected/xml-fragment.txt     |   25 -
 .../expected/xml-ns_prefix-scope.txt            |   29 -
 .../test/templatesuite/expected/xml.txt         |   65 -
 .../test/templatesuite/expected/xmlns1.txt      |   63 -
 .../test/templatesuite/expected/xmlns3.txt      |   47 -
 .../test/templatesuite/expected/xmlns4.txt      |   47 -
 .../test/templatesuite/expected/xmlns5.txt      |   26 -
 .../models/BeansTestResources.properties        |   19 -
 .../test/templatesuite/models/defaultxmlns1.xml |   24 -
 .../models/xml-ns_prefix-scope.xml              |   26 -
 .../test/templatesuite/models/xml.xml           |   31 -
 .../test/templatesuite/models/xmlfragment.xml   |   19 -
 .../test/templatesuite/models/xmlns.xml         |   32 -
 .../test/templatesuite/models/xmlns2.xml        |   32 -
 .../test/templatesuite/models/xmlns3.xml        |   32 -
 .../templatesuite/templates/api-builtins.ftl    |   40 -
 .../test/templatesuite/templates/arithmetic.ftl |   50 -
 .../templatesuite/templates/assignments.ftl     |  108 -
 .../templates/boolean-formatting.ftl            |   82 -
 .../test/templatesuite/templates/boolean.ftl    |  142 -
 .../templates/charset-in-header.ftl             |   27 -
 .../templates/charset-in-header_inc1.ftl        |   20 -
 .../templates/charset-in-header_inc2.ftl        |   19 -
 .../test/templatesuite/templates/comment.ftl    |   50 -
 .../templatesuite/templates/comparisons.ftl     |  218 -
 .../test/templatesuite/templates/compress.ftl   |   59 -
 .../templates/date-type-builtins.ftl            |   47 -
 .../templates/dateformat-iso-bi.ftl             |  163 -
 .../templates/dateformat-iso-like.ftl           |  155 -
 .../templatesuite/templates/dateformat-java.ftl |   71 -
 .../templatesuite/templates/dateparsing.ftl     |   84 -
 .../templates/default-object-wrapper.ftl        |   59 -
 .../templatesuite/templates/default-xmlns.ftl   |   28 -
 .../test/templatesuite/templates/default.ftl    |   34 -
 .../templates/encoding-builtins.ftl             |   52 -
 .../test/templatesuite/templates/escapes.ftl    |   79 -
 .../test/templatesuite/templates/exception.ftl  |   31 -
 .../test/templatesuite/templates/exception2.ftl |   31 -
 .../test/templatesuite/templates/exception3.ftl |   31 -
 .../templates/existence-operators.ftl           |  141 -
 .../test/templatesuite/templates/hashconcat.ftl |   60 -
 .../templatesuite/templates/hashliteral.ftl     |  100 -
 .../test/templatesuite/templates/helloworld.ftl |   30 -
 .../templates/identifier-escaping.ftl           |   81 -
 .../templates/identifier-non-ascii.ftl          |   21 -
 .../test/templatesuite/templates/if.ftl         |  109 -
 .../test/templatesuite/templates/import.ftl     |   45 -
 .../test/templatesuite/templates/import_lib.ftl |   31 -
 .../test/templatesuite/templates/include.ftl    |   47 -
 .../templates/include2-included.ftl             |   19 -
 .../test/templatesuite/templates/include2.ftl   |   32 -
 .../test/templatesuite/templates/included.ftl   |   30 -
 .../test/templatesuite/templates/interpret.ftl  |   25 -
 .../test/templatesuite/templates/iterators.ftl  |   71 -
 .../templatesuite/templates/lastcharacter.ftl   |   31 -
 .../test/templatesuite/templates/list-bis.ftl   |   48 -
 .../test/templatesuite/templates/list.ftl       |   44 -
 .../test/templatesuite/templates/list2.ftl      |   90 -
 .../test/templatesuite/templates/list3.ftl      |   70 -
 .../test/templatesuite/templates/listhash.ftl   |   70 -
 .../templatesuite/templates/listhashliteral.ftl |   35 -
 .../templatesuite/templates/listliteral.ftl     |   84 -
 .../templatesuite/templates/localization.ftl    |   32 -
 .../templatesuite/templates/localization_en.ftl |   32 -
 .../templates/localization_en_AU.ftl            |   32 -
 .../test/templatesuite/templates/logging.ftl    |   42 -
 .../templatesuite/templates/loopvariable.ftl    |   49 -
 .../templatesuite/templates/macros-return.ftl   |   34 -
 .../test/templatesuite/templates/macros.ftl     |  101 -
 .../test/templatesuite/templates/macros2.ftl    |   35 -
 .../templatesuite/templates/multimodels.ftl     |   84 -
 .../test/templatesuite/templates/nested.ftl     |   29 -
 .../templatesuite/templates/nestedinclude.ftl   |   21 -
 .../templates/new-defaultresolver.ftl           |   23 -
 .../test/templatesuite/templates/new-optin.ftl  |   30 -
 .../test/templatesuite/templates/newlines1.ftl  |   29 -
 .../test/templatesuite/templates/newlines2.ftl  |   33 -
 .../test/templatesuite/templates/noparse.ftl    |   62 -
 .../templatesuite/templates/number-format.ftl   |   42 -
 .../templatesuite/templates/number-literal.ftl  |  133 -
 .../templates/number-math-builtins.ftl          |   78 -
 .../templatesuite/templates/number-to-date.ftl  |   35 -
 .../templatesuite/templates/numerical-cast.ftl  |   82 -
 .../templates/output-encoding1.ftl              |   30 -
 .../templates/output-encoding2.ftl              |   28 -
 .../templates/output-encoding3.ftl              |   28 -
 .../templates/overloaded-methods.ftl            |  411 --
 .../test/templatesuite/templates/precedence.ftl |   61 -
 .../templatesuite/templates/range-common.ftl    |  314 --
 .../test/templatesuite/templates/range.ftl      |   50 -
 .../test/templatesuite/templates/recover.ftl    |   47 -
 .../test/templatesuite/templates/root.ftl       |   47 -
 .../templates/sequence-builtins.ftl             |  360 --
 .../test/templatesuite/templates/setting.ftl    |   53 -
 .../templates/simplehash-char-key.ftl           |   44 -
 .../templatesuite/templates/specialvars.ftl     |   38 -
 .../templates/string-builtin-coercion.ftl       |   34 -
 .../string-builtins-regexps-matches.ftl         |  118 -
 .../templates/string-builtins-regexps.ftl       |  136 -
 .../templates/string-builtins1.ftl              |  129 -
 .../templates/string-builtins2.ftl              |  135 -
 .../templates/string-builtins3.ftl              |  225 -
 .../templatesuite/templates/stringbimethods.ftl |   36 -
 .../templatesuite/templates/stringliteral.ftl   |   69 -
 .../templates/subdir/include-subdir.ftl         |   27 -
 .../templates/subdir/include-subdir2.ftl        |   19 -
 .../templates/subdir/new-optin-2.ftl            |   24 -
 .../templates/subdir/new-optin.ftl              |   26 -
 .../templates/subdir/subsub/new-optin.ftl       |   24 -
 .../templatesuite/templates/switch-builtin.ftl  |   54 -
 .../test/templatesuite/templates/switch.ftl     |  139 -
 .../templatesuite/templates/then-builtin.ftl    |   53 -
 .../test/templatesuite/templates/transforms.ftl |  100 -
 .../templatesuite/templates/type-builtins.ftl   |   44 -
 .../test/templatesuite/templates/undefined.ftl  |   19 -
 .../test/templatesuite/templates/url.ftl        |   24 -
 .../test/templatesuite/templates/var-layers.ftl |   39 -
 .../test/templatesuite/templates/varargs.ftl    |   45 -
 .../test/templatesuite/templates/variables.ftl  |   70 -
 .../templatesuite/templates/varlayers_lib.ftl   |   28 -
 .../templatesuite/templates/whitespace-trim.ftl |  102 -
 .../templates/wsstripinheader_inc.ftl           |   22 -
 .../templates/wstrip-in-header.ftl              |   26 -
 .../templatesuite/templates/xml-fragment.ftl    |   26 -
 .../templates/xml-ns_prefix-scope-lib.ftl       |   23 -
 .../templates/xml-ns_prefix-scope-main.ftl      |   36 -
 .../test/templatesuite/templates/xml.ftl        |   47 -
 .../test/templatesuite/templates/xmlns1.ftl     |   53 -
 .../test/templatesuite/templates/xmlns3.ftl     |   70 -
 .../test/templatesuite/templates/xmlns4.ftl     |   70 -
 .../test/templatesuite/templates/xmlns5.ftl     |   28 -
 .../freemarker/test/templatesuite/testcases.xml |  211 -
 2220 files changed, 143651 insertions(+), 142650 deletions(-)
----------------------------------------------------------------------



[25/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/APIModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/APIModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/APIModel.java
new file mode 100644
index 0000000..7ae5a71
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/APIModel.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;
+
+/**
+ * Exposes the Java API (and properties) of an object.
+ * 
+ * <p>
+ * Notes:
+ * <ul>
+ * <li>The exposion level is inherited from the {@link DefaultObjectWrapper}</li>
+ * <li>But methods will always shadow properties and fields with identical name, regardless of {@link DefaultObjectWrapper}
+ * settings</li>
+ * </ul>
+ * 
+ * @since 2.3.22
+ */
+final class APIModel extends BeanModel {
+
+    APIModel(Object object, DefaultObjectWrapper wrapper) {
+        super(object, wrapper, false);
+    }
+
+    protected boolean isMethodsShadowItems() {
+        return true;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ArgumentTypes.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ArgumentTypes.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ArgumentTypes.java
new file mode 100644
index 0000000..3b346e6
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ArgumentTypes.java
@@ -0,0 +1,647 @@
+/*
+ * 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.lang.reflect.InvocationTargetException;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.util._ClassUtil;
+
+/**
+ * The argument types of a method call; usable as cache key.
+ */
+final class ArgumentTypes {
+    
+    /**
+     * Conversion difficulty: Lowest; Java Reflection will do it automatically.
+     */
+    private static final int CONVERSION_DIFFICULTY_REFLECTION = 0;
+
+    /**
+     * Conversion difficulty: Medium: Java reflection API won't convert it, FreeMarker has to do it.
+     */
+    private static final int CONVERSION_DIFFICULTY_FREEMARKER = 1;
+    
+    /**
+     * Conversion difficulty: Highest; conversion is not possible.
+     */
+    private static final int CONVERSION_DIFFICULTY_IMPOSSIBLE = 2;
+
+    /**
+     * The types of the arguments; for varags this contains the exploded list (not the array). 
+     */
+    private final Class<?>[] types;
+    
+    /**
+     * @param args The actual arguments. A varargs argument should be present exploded, no as an array.
+     */
+    ArgumentTypes(Object[] args) {
+        int ln = args.length;
+        Class<?>[] typesTmp = new Class[ln];
+        for (int i = 0; i < ln; ++i) {
+            Object arg = args[i];
+            typesTmp[i] = arg == null
+                    ? Null.class
+                    : arg.getClass();
+        }
+        
+        // `typesTmp` is used so the array is only modified before it's stored in the final `types` field (see JSR-133)
+        types = typesTmp;  
+    }
+    
+    @Override
+    public int hashCode() {
+        int hash = 0;
+        for (Class<?> type : types) {
+            hash ^= type.hashCode();
+        }
+        return hash;
+    }
+    
+    @Override
+    public boolean equals(Object o) {
+        if (o instanceof ArgumentTypes) {
+            ArgumentTypes cs = (ArgumentTypes) o;
+            if (cs.types.length != types.length) {
+                return false;
+            }
+            for (int i = 0; i < types.length; ++i) {
+                if (cs.types[i] != types[i]) {
+                    return false;
+                }
+            }
+            return true;
+        }
+        return false;
+    }
+    
+    /**
+     * @return Possibly {@link EmptyCallableMemberDescriptor#NO_SUCH_METHOD} or
+     *         {@link EmptyCallableMemberDescriptor#AMBIGUOUS_METHOD}. 
+     */
+    MaybeEmptyCallableMemberDescriptor getMostSpecific(
+            List<ReflectionCallableMemberDescriptor> memberDescs, boolean varArg) {
+        LinkedList<CallableMemberDescriptor> applicables = getApplicables(memberDescs, varArg);
+        if (applicables.isEmpty()) {
+            return EmptyCallableMemberDescriptor.NO_SUCH_METHOD;
+        }
+        if (applicables.size() == 1) {
+            return applicables.getFirst();
+        }
+        
+        LinkedList<CallableMemberDescriptor> maximals = new LinkedList<>();
+        for (CallableMemberDescriptor applicable : applicables) {
+            boolean lessSpecific = false;
+            for (Iterator<CallableMemberDescriptor> maximalsIter = maximals.iterator(); 
+                maximalsIter.hasNext(); ) {
+                CallableMemberDescriptor maximal = maximalsIter.next();
+                final int cmpRes = compareParameterListPreferability(
+                        applicable.getParamTypes(), maximal.getParamTypes(), varArg); 
+                if (cmpRes > 0) {
+                    maximalsIter.remove();
+                } else if (cmpRes < 0) {
+                    lessSpecific = true;
+                }
+            }
+            if (!lessSpecific) {
+                maximals.addLast(applicable);
+            }
+        }
+        if (maximals.size() > 1) {
+            return EmptyCallableMemberDescriptor.AMBIGUOUS_METHOD;
+        }
+        return maximals.getFirst();
+    }
+
+    /**
+     * Tells if among the parameter list of two methods, which one fits this argument list better.
+     * This method assumes that the parameter lists are applicable to this argument lists; if that's not ensured,
+     * what the result will be is undefined.
+     * 
+     * <p>The decision is made by comparing the preferability of each parameter types of the same position in a loop.
+     * At the end, the parameter list with the more preferred parameters will be the preferred one. If both parameter
+     * lists has the same amount of preferred parameters, the one that has the first (lower index) preferred parameter
+     * is the preferred one. Otherwise the two parameter list are considered to be equal in terms of preferability.
+     * 
+     * <p>If there's no numerical conversion involved, the preferability of two parameter types is decided on how
+     * specific their types are. For example, {@code String} is more specific than {@link Object} (because
+     * {@code Object.class.isAssignableFrom(String.class)}-s), and so {@code String} is preferred. Primitive
+     * types are considered to be more specific than the corresponding boxing class (like {@code boolean} is more
+     * specific than {@code Boolean}, because the former can't store {@code null}). The preferability decision gets
+     * trickier when there's a possibility of numerical conversion from the actual argument type to the type of some of
+     * the parameters. If such conversion is only possible for one of the competing parameter types, that parameter
+     * automatically wins. If it's possible for both, {@link OverloadedNumberUtil#getArgumentConversionPrice} will
+     * be used to calculate the conversion "price", and the parameter type with lowest price wins. There are also
+     * a twist with array-to-list and list-to-array conversions; we try to avoid those, so the parameter where such
+     * conversion isn't needed will always win.
+     * 
+     * @param paramTypes1 The parameter types of one of the competing methods
+     * @param paramTypes2 The parameter types of the other competing method
+     * @param varArg Whether these competing methods are varargs methods. 
+     * @return More than 0 if the first parameter list is preferred, less then 0 if the other is preferred,
+     *        0 if there's no decision 
+     */
+    int compareParameterListPreferability(Class<?>[] paramTypes1, Class<?>[] paramTypes2, boolean varArg) {
+        final int argTypesLen = types.length; 
+        final int paramTypes1Len = paramTypes1.length;
+        final int paramTypes2Len = paramTypes2.length;
+        //assert varArg || paramTypes1Len == paramTypes2Len;
+        
+        int paramList1WeakWinCnt = 0;
+        int paramList2WeakWinCnt = 0;
+        int paramList1WinCnt = 0;
+        int paramList2WinCnt = 0;
+        int paramList1StrongWinCnt = 0;
+        int paramList2StrongWinCnt = 0;
+        int paramList1VeryStrongWinCnt = 0;
+        int paramList2VeryStrongWinCnt = 0;
+        int firstWinerParamList = 0;
+        for (int i = 0; i < argTypesLen; i++) {
+            final Class<?> paramType1 = getParamType(paramTypes1, paramTypes1Len, i, varArg);
+            final Class<?> paramType2 = getParamType(paramTypes2, paramTypes2Len, i, varArg);
+            
+            final int winerParam;  // 1 => paramType1; -1 => paramType2; 0 => draw
+            if (paramType1 == paramType2) {
+                winerParam = 0;
+            } else {
+                final Class<?> argType = types[i];
+                final boolean argIsNum = Number.class.isAssignableFrom(argType);
+                
+                final int numConvPrice1;
+                if (argIsNum && _ClassUtil.isNumerical(paramType1)) {
+                    final Class<?> nonPrimParamType1 = paramType1.isPrimitive()
+                            ? _ClassUtil.primitiveClassToBoxingClass(paramType1) : paramType1;
+                    numConvPrice1 = OverloadedNumberUtil.getArgumentConversionPrice(argType, nonPrimParamType1);
+                } else {
+                    numConvPrice1 = Integer.MAX_VALUE;
+                }
+                // numConvPrice1 is Integer.MAX_VALUE if either:
+                // - argType and paramType1 aren't both numerical
+                // - FM doesn't know some of the numerical types, or the conversion between them is not allowed    
+                
+                final int numConvPrice2;
+                if (argIsNum && _ClassUtil.isNumerical(paramType2)) {
+                    final Class<?> nonPrimParamType2 = paramType2.isPrimitive()
+                            ? _ClassUtil.primitiveClassToBoxingClass(paramType2) : paramType2;
+                    numConvPrice2 = OverloadedNumberUtil.getArgumentConversionPrice(argType, nonPrimParamType2);
+                } else {
+                    numConvPrice2 = Integer.MAX_VALUE;
+                }
+                
+                if (numConvPrice1 == Integer.MAX_VALUE) {
+                    if (numConvPrice2 == Integer.MAX_VALUE) {  // No numerical conversions anywhere
+                        // List to array conversions (unwrapping sometimes makes a List instead of an array)
+                        if (List.class.isAssignableFrom(argType)
+                                && (paramType1.isArray() || paramType2.isArray())) {
+                            if (paramType1.isArray()) {
+                                if (paramType2.isArray()) {  // both paramType1 and paramType2 are arrays
+                                    int r = compareParameterListPreferability_cmpTypeSpecificty(
+                                            paramType1.getComponentType(), paramType2.getComponentType());
+                                    // Because we don't know if the List items are instances of the component
+                                    // type or not, we prefer the safer choice, which is the more generic array:
+                                    if (r > 0) {
+                                        winerParam = 2;
+                                        paramList2StrongWinCnt++;
+                                    } else if (r < 0) {
+                                        winerParam = 1;
+                                        paramList1StrongWinCnt++;
+                                    } else {
+                                        winerParam = 0;
+                                    }
+                                } else {  // paramType1 is array, paramType2 isn't
+                                    // Avoid List to array conversion if the other way makes any sense:
+                                    if (Collection.class.isAssignableFrom(paramType2)) {
+                                        winerParam = 2;
+                                        paramList2StrongWinCnt++;
+                                    } else {
+                                        winerParam = 1;
+                                        paramList1WeakWinCnt++;
+                                    }
+                                }
+                            } else {  // paramType2 is array, paramType1 isn't
+                                // Avoid List to array conversion if the other way makes any sense:
+                                if (Collection.class.isAssignableFrom(paramType1)) {
+                                    winerParam = 1;
+                                    paramList1StrongWinCnt++;
+                                } else {
+                                    winerParam = 2;
+                                    paramList2WeakWinCnt++;
+                                }
+                            }
+                        } else if (argType.isArray()
+                                && (List.class.isAssignableFrom(paramType1)
+                                        || List.class.isAssignableFrom(paramType2))) {
+                            // Array to List conversions (unwrapping sometimes makes an array instead of a List)
+                            if (List.class.isAssignableFrom(paramType1)) {
+                                if (List.class.isAssignableFrom(paramType2)) {
+                                    // Both paramType1 and paramType2 extends List
+                                    winerParam = 0;
+                                } else {
+                                    // Only paramType1 extends List
+                                    winerParam = 2;
+                                    paramList2VeryStrongWinCnt++;
+                                }
+                            } else {
+                                // Only paramType2 extends List
+                                winerParam = 1;
+                                paramList1VeryStrongWinCnt++;
+                            }
+                        } else {  // No list to/from array conversion
+                            final int r = compareParameterListPreferability_cmpTypeSpecificty(
+                                    paramType1, paramType2);
+                            if (r > 0) {
+                                winerParam = 1;
+                                if (r > 1) {
+                                    paramList1WinCnt++;
+                                } else {
+                                    paramList1WeakWinCnt++;
+                                }
+                            } else if (r < 0) {
+                                winerParam = -1;
+                                if (r < -1) {
+                                    paramList2WinCnt++;
+                                } else {
+                                    paramList2WeakWinCnt++;
+                                }
+                            } else {
+                                winerParam = 0;
+                            }
+                        }
+                    } else {  // No num. conv. of param1, num. conv. of param2
+                        winerParam = -1;
+                        paramList2WinCnt++;
+                    }
+                } else if (numConvPrice2 == Integer.MAX_VALUE) {  // Num. conv. of param1, not of param2
+                    winerParam = 1;
+                    paramList1WinCnt++;
+                } else {  // Num. conv. of both param1 and param2
+                    if (numConvPrice1 != numConvPrice2) {
+                        if (numConvPrice1 < numConvPrice2) {
+                            winerParam = 1;
+                            if (numConvPrice1 < OverloadedNumberUtil.BIG_MANTISSA_LOSS_PRICE
+                                    && numConvPrice2 > OverloadedNumberUtil.BIG_MANTISSA_LOSS_PRICE) {
+                                paramList1StrongWinCnt++;
+                            } else {
+                                paramList1WinCnt++;
+                            }
+                        } else {
+                            winerParam = -1;
+                            if (numConvPrice2 < OverloadedNumberUtil.BIG_MANTISSA_LOSS_PRICE
+                                    && numConvPrice1 > OverloadedNumberUtil.BIG_MANTISSA_LOSS_PRICE) {
+                                paramList2StrongWinCnt++;
+                            } else {
+                                paramList2WinCnt++;
+                            }
+                        }
+                    } else {
+                        winerParam = (paramType1.isPrimitive() ? 1 : 0) - (paramType2.isPrimitive() ? 1 : 0);
+                        if (winerParam == 1) paramList1WeakWinCnt++;
+                        else if (winerParam == -1) paramList2WeakWinCnt++;
+                    }
+                }
+            }  // when paramType1 != paramType2
+            
+            if (firstWinerParamList == 0 && winerParam != 0) {
+                firstWinerParamList = winerParam; 
+            }
+        }  // for each parameter types
+        
+        if (paramList1VeryStrongWinCnt != paramList2VeryStrongWinCnt) {
+            return paramList1VeryStrongWinCnt - paramList2VeryStrongWinCnt;
+        } else if (paramList1StrongWinCnt != paramList2StrongWinCnt) {
+            return paramList1StrongWinCnt - paramList2StrongWinCnt;
+        } else if (paramList1WinCnt != paramList2WinCnt) {
+            return paramList1WinCnt - paramList2WinCnt;
+        } else if (paramList1WeakWinCnt != paramList2WeakWinCnt) {
+            return paramList1WeakWinCnt - paramList2WeakWinCnt;
+        } else if (firstWinerParamList != 0) {  // paramList1WinCnt == paramList2WinCnt
+            return firstWinerParamList;
+        } else { // still undecided
+            if (varArg) {
+                if (paramTypes1Len == paramTypes2Len) {
+                    // If we had a 0-length varargs array in both methods, we also compare the types at the
+                    // index of the varargs parameter, like if we had a single varargs argument. However, this
+                    // time we don't have an argument type, so we can only decide based on type specificity:
+                    if (argTypesLen == paramTypes1Len - 1) {
+                        Class<?> paramType1 = getParamType(paramTypes1, paramTypes1Len, argTypesLen, true);
+                        Class<?> paramType2 = getParamType(paramTypes2, paramTypes2Len, argTypesLen, true);
+                        if (_ClassUtil.isNumerical(paramType1) && _ClassUtil.isNumerical(paramType2)) {
+                            int r = OverloadedNumberUtil.compareNumberTypeSpecificity(paramType1, paramType2);
+                            if (r != 0) return r;
+                            // falls through
+                        }
+                        return compareParameterListPreferability_cmpTypeSpecificty(paramType1, paramType2);
+                    } else {
+                        return 0;
+                    }
+                } else {
+                    // The method with more oms parameters wins:
+                    return paramTypes1Len - paramTypes2Len;
+                }
+            } else {
+                return 0;
+            }
+        }
+    }
+    
+    /**
+     * Trivial comparison of type specificities; unaware of numerical conversions. 
+     * 
+     * @return Less-than-0, 0, or more-than-0 depending on which side is more specific. The absolute value is 1 if
+     *     the difference is only in primitive VS non-primitive, more otherwise.
+     */
+    private int compareParameterListPreferability_cmpTypeSpecificty(
+            final Class<?> paramType1, final Class<?> paramType2) {
+        // The more specific (smaller) type wins.
+        
+        final Class<?> nonPrimParamType1 = paramType1.isPrimitive()
+                ? _ClassUtil.primitiveClassToBoxingClass(paramType1) : paramType1;
+        final Class<?> nonPrimParamType2 = paramType2.isPrimitive()
+                ? _ClassUtil.primitiveClassToBoxingClass(paramType2) : paramType2;
+                
+        if (nonPrimParamType1 == nonPrimParamType2) {
+            if (nonPrimParamType1 != paramType1) {
+                if (nonPrimParamType2 != paramType2) {
+                    return 0;  // identical prim. types; shouldn't ever be reached
+                } else {
+                    return 1;  // param1 is prim., param2 is non prim.
+                }
+            } else if (nonPrimParamType2 != paramType2) {
+                return -1;  // param1 is non-prim., param2 is prim.
+            } else {
+                return 0;  // identical non-prim. types
+            }
+        } else if (nonPrimParamType2.isAssignableFrom(nonPrimParamType1)) {
+            return 2;
+        } else if (nonPrimParamType1.isAssignableFrom(nonPrimParamType2)) {
+            return -2;
+        } if (nonPrimParamType1 == Character.class && nonPrimParamType2.isAssignableFrom(String.class)) {
+            return 2;  // A character is a 1 long string in FTL, so we pretend that it's a String subtype.
+        } if (nonPrimParamType2 == Character.class && nonPrimParamType1.isAssignableFrom(String.class)) {
+            return -2;
+        } else {
+            return 0;  // unrelated types
+        }
+    }
+
+    private static Class<?> getParamType(Class<?>[] paramTypes, int paramTypesLen, int i, boolean varArg) {
+        return varArg && i >= paramTypesLen - 1
+                ? paramTypes[paramTypesLen - 1].getComponentType()
+                : paramTypes[i];
+    }
+    
+    /**
+     * Returns all methods that are applicable to actual
+     * parameter types represented by this ArgumentTypes object.
+     */
+    LinkedList<CallableMemberDescriptor> getApplicables(
+            List<ReflectionCallableMemberDescriptor> memberDescs, boolean varArg) {
+        LinkedList<CallableMemberDescriptor> applicables = new LinkedList<>();
+        for (ReflectionCallableMemberDescriptor memberDesc : memberDescs) {
+            int difficulty = isApplicable(memberDesc, varArg);
+            if (difficulty != CONVERSION_DIFFICULTY_IMPOSSIBLE) {
+                if (difficulty == CONVERSION_DIFFICULTY_REFLECTION) {
+                    applicables.add(memberDesc);
+                } else if (difficulty == CONVERSION_DIFFICULTY_FREEMARKER) {
+                    applicables.add(new SpecialConversionCallableMemberDescriptor(memberDesc));
+                } else {
+                    throw new BugException();
+                }
+            }
+        }
+        return applicables;
+    }
+    
+    /**
+     * Returns if the supplied method is applicable to actual
+     * parameter types represented by this ArgumentTypes object, also tells
+     * how difficult that conversion is.
+     * 
+     * @return One of the <tt>CONVERSION_DIFFICULTY_...</tt> constants.
+     */
+    private int isApplicable(ReflectionCallableMemberDescriptor memberDesc, boolean varArg) {
+        final Class<?>[] paramTypes = memberDesc.getParamTypes(); 
+        final int cl = types.length;
+        final int fl = paramTypes.length - (varArg ? 1 : 0);
+        if (varArg) {
+            if (cl < fl) {
+                return CONVERSION_DIFFICULTY_IMPOSSIBLE;
+            }
+        } else {
+            if (cl != fl) {
+                return CONVERSION_DIFFICULTY_IMPOSSIBLE;
+            }
+        }
+        
+        int maxDifficulty = 0;
+        for (int i = 0; i < fl; ++i) {
+            int difficulty = isMethodInvocationConvertible(paramTypes[i], types[i]);
+            if (difficulty == CONVERSION_DIFFICULTY_IMPOSSIBLE) {
+                return CONVERSION_DIFFICULTY_IMPOSSIBLE;
+            }
+            if (maxDifficulty < difficulty) {
+                maxDifficulty = difficulty;
+            }
+        }
+        if (varArg) {
+            Class<?> varArgParamType = paramTypes[fl].getComponentType();
+            for (int i = fl; i < cl; ++i) {
+                int difficulty = isMethodInvocationConvertible(varArgParamType, types[i]); 
+                if (difficulty == CONVERSION_DIFFICULTY_IMPOSSIBLE) {
+                    return CONVERSION_DIFFICULTY_IMPOSSIBLE;
+                }
+                if (maxDifficulty < difficulty) {
+                    maxDifficulty = difficulty;
+                }
+            }
+        }
+        return maxDifficulty;
+    }
+
+    /**
+     * Determines whether a type is convertible to another type via 
+     * method invocation conversion, and if so, what kind of conversion is needed.
+     * It treates the object type counterpart of primitive types as if they were the primitive types
+     * (that is, a Boolean actual parameter type matches boolean primitive formal type). This behavior
+     * is because this method is used to determine applicable methods for 
+     * an actual parameter list, and primitive types are represented by 
+     * their object duals in reflective method calls.
+     * @param formal the parameter type to which the actual 
+     * parameter type should be convertible; possibly a primitive type
+     * @param actual the argument type; not a primitive type, maybe {@link Null}.
+     * 
+     * @return One of the <tt>CONVERSION_DIFFICULTY_...</tt> constants.
+     */
+    private int isMethodInvocationConvertible(final Class<?> formal, final Class<?> actual) {
+        // Check for identity or widening reference conversion
+        if (formal.isAssignableFrom(actual) && actual != CharacterOrString.class) {
+            return CONVERSION_DIFFICULTY_REFLECTION;
+        } else {
+            final Class<?> formalNP;
+            if (formal.isPrimitive()) {
+                if (actual == Null.class) {
+                    return CONVERSION_DIFFICULTY_IMPOSSIBLE;
+                }
+                
+                formalNP = _ClassUtil.primitiveClassToBoxingClass(formal);
+                if (actual == formalNP) {
+                    // Character and char, etc.
+                    return CONVERSION_DIFFICULTY_REFLECTION;
+                }
+            } else {  // formal is non-primitive
+                if (actual == Null.class) {
+                    return CONVERSION_DIFFICULTY_REFLECTION;
+                }
+                
+                formalNP = formal;
+            }
+            if (Number.class.isAssignableFrom(actual) && Number.class.isAssignableFrom(formalNP)) {
+                return OverloadedNumberUtil.getArgumentConversionPrice(actual, formalNP) == Integer.MAX_VALUE
+                        ? CONVERSION_DIFFICULTY_IMPOSSIBLE : CONVERSION_DIFFICULTY_REFLECTION;
+            } else if (formal.isArray()) {
+                // DefaultObjectWrapper method/constructor calls convert from List to array automatically
+                return List.class.isAssignableFrom(actual)
+                        ? CONVERSION_DIFFICULTY_FREEMARKER : CONVERSION_DIFFICULTY_IMPOSSIBLE;
+            } else if (actual.isArray() && formal.isAssignableFrom(List.class)) {
+                // DefaultObjectWrapper method/constructor calls convert from array to List automatically
+                return CONVERSION_DIFFICULTY_FREEMARKER;
+            } else if (actual == CharacterOrString.class
+                    && (formal.isAssignableFrom(String.class)
+                            || formal.isAssignableFrom(Character.class) || formal == char.class)) {
+                return CONVERSION_DIFFICULTY_FREEMARKER;
+            } else {
+                return CONVERSION_DIFFICULTY_IMPOSSIBLE;
+            }
+        }
+    }
+    
+    /**
+     * Symbolizes the class of null (it's missing from Java).
+     */
+    private static class Null {
+        
+        // Can't be instantiated
+        private Null() { }
+        
+    }
+    
+    /**
+     * Used instead of {@link ReflectionCallableMemberDescriptor} when the method is only applicable
+     * ({@link #isApplicable}) with conversion that Java reflection won't do. It delegates to a
+     * {@link ReflectionCallableMemberDescriptor}, but it adds the necessary conversions to the invocation methods. 
+     */
+    private static final class SpecialConversionCallableMemberDescriptor extends CallableMemberDescriptor {
+        
+        private final ReflectionCallableMemberDescriptor callableMemberDesc;
+
+        SpecialConversionCallableMemberDescriptor(ReflectionCallableMemberDescriptor callableMemberDesc) {
+            this.callableMemberDesc = callableMemberDesc;
+        }
+
+        @Override
+        TemplateModel invokeMethod(DefaultObjectWrapper ow, Object obj, Object[] args) throws TemplateModelException,
+                InvocationTargetException, IllegalAccessException {
+            convertArgsToReflectionCompatible(ow, args);
+            return callableMemberDesc.invokeMethod(ow, obj, args);
+        }
+
+        @Override
+        Object invokeConstructor(DefaultObjectWrapper ow, Object[] args) throws IllegalArgumentException,
+                InstantiationException, IllegalAccessException, InvocationTargetException, TemplateModelException {
+            convertArgsToReflectionCompatible(ow, args);
+            return callableMemberDesc.invokeConstructor(ow, args);
+        }
+
+        @Override
+        String getDeclaration() {
+            return callableMemberDesc.getDeclaration();
+        }
+
+        @Override
+        boolean isConstructor() {
+            return callableMemberDesc.isConstructor();
+        }
+
+        @Override
+        boolean isStatic() {
+            return callableMemberDesc.isStatic();
+        }
+
+        @Override
+        boolean isVarargs() {
+            return callableMemberDesc.isVarargs();
+        }
+
+        @Override
+        Class<?>[] getParamTypes() {
+            return callableMemberDesc.getParamTypes();
+        }
+        
+        @Override
+        String getName() {
+            return callableMemberDesc.getName();
+        }
+
+        private void convertArgsToReflectionCompatible(DefaultObjectWrapper ow, Object[] args) throws TemplateModelException {
+            Class<?>[] paramTypes = callableMemberDesc.getParamTypes();
+            int ln = paramTypes.length;
+            for (int i = 0; i < ln; i++) {
+                Class<?> paramType = paramTypes[i];
+                final Object arg = args[i];
+                if (arg == null) continue;
+                
+                // Handle conversion between List and array types, in both directions. Java reflection won't do such
+                // conversion, so we have to.
+                // Most reflection-incompatible conversions were already addressed by the unwrapping. The reason
+                // this one isn't is that for overloaded methods the hint of a given parameter position is often vague,
+                // so we may end up with a List even if some parameter types at that position are arrays (remember, we
+                // have to chose one unwrapping target type, despite that we have many possible overloaded methods), or
+                // the other way around (that happens when AdapterTemplateMoldel returns an array).
+                // Later, the overloaded method selection will assume that a List argument is applicable to an array
+                // parameter, and that an array argument is applicable to a List parameter, so we end up with this
+                // situation.
+                if (paramType.isArray() && arg instanceof List) {
+                   args[i] = ow.listToArray((List<?>) arg, paramType, null);
+                }
+                if (arg.getClass().isArray() && paramType.isAssignableFrom(List.class)) {
+                    args[i] = ow.arrayToList(arg);
+                }
+                
+                // Handle the conversion from CharacterOrString to Character or String:
+                if (arg instanceof CharacterOrString) {
+                    if (paramType == Character.class || paramType == char.class
+                            || (!paramType.isAssignableFrom(String.class)
+                                    && paramType.isAssignableFrom(Character.class))) {
+                        args[i] = Character.valueOf(((CharacterOrString) arg).getAsChar());
+                    } else {
+                        args[i] = ((CharacterOrString) arg).getAsString();
+                    }
+                }
+            }
+        }
+
+    }
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanAndStringModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanAndStringModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanAndStringModel.java
new file mode 100644
index 0000000..c154bba
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanAndStringModel.java
@@ -0,0 +1,53 @@
+/*
+ * 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.model.TemplateScalarModel;
+
+/**
+ * Subclass of {@link BeanModel} that exposes the return value of the {@link
+ * java.lang.Object#toString()} method through the {@link TemplateScalarModel}
+ * interface.
+ */
+// [FM3] Treating all beans as FTL strings was certainly a bad idea in FM2.
+public class BeanAndStringModel extends BeanModel implements TemplateScalarModel {
+
+    /**
+     * Creates a new model that wraps the specified object with BeanModel + scalar
+     * functionality.
+     * @param object the object to wrap into a model.
+     * @param wrapper the {@link DefaultObjectWrapper} associated with this model.
+     * Every model has to have an associated {@link DefaultObjectWrapper} instance. The
+     * model gains many attributes from its wrapper, including the caching 
+     * behavior, method exposure level, method-over-item shadowing policy etc.
+     */
+    public BeanAndStringModel(Object object, DefaultObjectWrapper wrapper) {
+        super(object, wrapper);
+    }
+
+    /**
+     * Returns the result of calling {@link Object#toString()} on the wrapped
+     * object.
+     */
+    @Override
+    public String getAsString() {
+        return object.toString();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanModel.java
new file mode 100644
index 0000000..91fe9dc
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanModel.java
@@ -0,0 +1,339 @@
+/*
+ * 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.IndexedPropertyDescriptor;
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.freemarker.core._CoreLogs;
+import org.apache.freemarker.core._DelayedFTLTypeDescription;
+import org.apache.freemarker.core._DelayedJQuote;
+import org.apache.freemarker.core._TemplateModelException;
+import org.apache.freemarker.core.model.AdapterTemplateModel;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+import org.apache.freemarker.core.model.TemplateModelWithAPISupport;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.WrapperTemplateModel;
+import org.apache.freemarker.core.util._StringUtil;
+import org.slf4j.Logger;
+
+/**
+ * A class that will wrap an arbitrary object into {@link org.apache.freemarker.core.model.TemplateHashModel}
+ * interface allowing calls to arbitrary property getters and invocation of
+ * accessible methods on the object from a template using the
+ * <tt>object.foo</tt> to access properties and <tt>object.bar(arg1, arg2)</tt> to
+ * invoke methods on it. You can also use the <tt>object.foo[index]</tt> syntax to
+ * access indexed properties. It uses Beans {@link java.beans.Introspector}
+ * to dynamically discover the properties and methods. 
+ */
+
+public class BeanModel
+        implements TemplateHashModelEx, AdapterTemplateModel, WrapperTemplateModel, TemplateModelWithAPISupport {
+    
+    private static final Logger LOG = _CoreLogs.OBJECT_WRAPPER;
+    
+    protected final Object object;
+    protected final DefaultObjectWrapper wrapper;
+    
+    // We use this to represent an unknown value as opposed to known value of null (JR)
+    static final TemplateModel UNKNOWN = new SimpleScalar("UNKNOWN");
+
+    // I've tried to use a volatile ConcurrentHashMap field instead of HashMap + synchronized(this), but oddly it was
+    // a bit slower, at least on Java 8 u66. 
+    private HashMap<Object, TemplateModel> memberCache;
+
+    /**
+     * Creates a new model that wraps the specified object. Note that there are
+     * specialized subclasses of this class for wrapping arrays, collections,
+     * enumeration, iterators, and maps. Note also that the superclass can be
+     * used to wrap String objects if only scalar functionality is needed. You
+     * can also choose to delegate the choice over which model class is used for
+     * wrapping to {@link DefaultObjectWrapper#wrap(Object)}.
+     * @param object the object to wrap into a model.
+     * @param wrapper the {@link DefaultObjectWrapper} associated with this model.
+     * Every model has to have an associated {@link DefaultObjectWrapper} instance. The
+     * model gains many attributes from its wrapper, including the caching 
+     * behavior, method exposure level, method-over-item shadowing policy etc.
+     */
+    public BeanModel(Object object, DefaultObjectWrapper wrapper) {
+        // [2.4]: All models were introspected here, then the results was discareded, and get() will just do the
+        // introspection again. So is this necessary? (The inrospectNow parameter was added in 2.3.21 to allow
+        // lazy-introspecting DefaultObjectWrapper.trueModel|falseModel.)
+        this(object, wrapper, true);
+    }
+
+    /** @since 2.3.21 */
+    BeanModel(Object object, DefaultObjectWrapper wrapper, boolean inrospectNow) {
+        this.object = object;
+        this.wrapper = wrapper;
+        if (inrospectNow && object != null) {
+            // [2.4]: Could this be removed?
+            wrapper.getClassIntrospector().get(object.getClass());
+        }
+    }
+    
+    /**
+     * Uses Beans introspection to locate a property or method with name
+     * matching the key name. If a method or property is found, it's wrapped
+     * into {@link org.apache.freemarker.core.model.TemplateMethodModelEx} (for a method or
+     * indexed property), or evaluated on-the-fly and the return value wrapped
+     * into appropriate model (for a simple property) Models for various
+     * properties and methods are cached on a per-class basis, so the costly
+     * introspection is performed only once per property or method of a class.
+     * (Side-note: this also implies that any class whose method has been called
+     * will be strongly referred to by the framework and will not become
+     * unloadable until this class has been unloaded first. Normally this is not
+     * an issue, but can be in a rare scenario where you invoke many classes on-
+     * the-fly. Also, as the cache grows with new classes and methods introduced
+     * to the framework, it may appear as if it were leaking memory. The
+     * framework does, however detect class reloads (if you happen to be in an
+     * environment that does this kind of things--servlet containers do it when
+     * they reload a web application) and flushes the cache. If no method or
+     * property matching the key is found, the framework will try to invoke
+     * methods with signature
+     * <tt>non-void-return-type get(java.lang.String)</tt>,
+     * then <tt>non-void-return-type get(java.lang.Object)</tt>, or 
+     * alternatively (if the wrapped object is a resource bundle) 
+     * <tt>Object get(java.lang.String)</tt>.
+     * @throws TemplateModelException if there was no property nor method nor
+     * a generic <tt>get</tt> method to invoke.
+     */
+    @Override
+    public TemplateModel get(String key)
+        throws TemplateModelException {
+        Class<?> clazz = object.getClass();
+        Map<Object, Object> classInfo = wrapper.getClassIntrospector().get(clazz);
+        TemplateModel retval = null;
+        
+        try {
+            Object fd = classInfo.get(key);
+            if (fd != null) {
+                retval = invokeThroughDescriptor(fd, classInfo);
+            } else {
+                retval = invokeGenericGet(classInfo, clazz, key);
+            }
+            if (retval == UNKNOWN) {
+                if (wrapper.isStrict()) {
+                    throw new InvalidPropertyException("No such bean property: " + key);
+                } else {
+                    logNoSuchKey(key, classInfo);
+                }
+                retval = wrapper.wrap(null);
+            }
+            return retval;
+        } catch (TemplateModelException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new _TemplateModelException(e,
+                    "An error has occurred when reading existing sub-variable ", new _DelayedJQuote(key),
+                    "; see cause exception! The type of the containing value was: ",
+                    new _DelayedFTLTypeDescription(this)
+            );
+        }
+    }
+
+    private void logNoSuchKey(String key, Map<?, ?> keyMap) {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("Key " + _StringUtil.jQuoteNoXSS(key) + " was not found on instance of " + 
+                object.getClass().getName() + ". Introspection information for " +
+                "the class is: " + keyMap);
+        }
+    }
+    
+    /**
+     * Whether the model has a plain get(String) or get(Object) method
+     */
+    
+    protected boolean hasPlainGetMethod() {
+        return wrapper.getClassIntrospector().get(object.getClass()).get(ClassIntrospector.GENERIC_GET_KEY) != null;
+    }
+    
+    private TemplateModel invokeThroughDescriptor(Object desc, Map<Object, Object> classInfo)
+            throws IllegalAccessException, InvocationTargetException, TemplateModelException {
+        // See if this particular instance has a cached implementation for the requested feature descriptor
+        TemplateModel cachedModel;
+        synchronized (this) {
+            cachedModel = memberCache != null ? memberCache.get(desc) : null;
+        }
+
+        if (cachedModel != null) {
+            return cachedModel;
+        }
+
+        TemplateModel resultModel = UNKNOWN;
+        if (desc instanceof PropertyDescriptor) {
+            PropertyDescriptor pd = (PropertyDescriptor) desc;
+            Method readMethod = pd.getReadMethod();
+            if (readMethod != null) {
+                // Unlike in FreeMarker 2, we prefer the normal read method even if there's an indexed read method.
+                resultModel = wrapper.invokeMethod(object, readMethod, null);
+                // cachedModel remains null, as we don't cache these
+            } else if (desc instanceof IndexedPropertyDescriptor) {
+                // In FreeMarker 2 we have exposed such indexed properties as sequences, but they can't support
+                // the size() method, so we have discontinued that. People has to call the indexed read method like
+                // any other method.
+                resultModel = UNKNOWN;
+            } else {
+                throw new IllegalStateException("PropertyDescriptor.readMethod shouldn't be null");
+            }
+        } else if (desc instanceof Field) {
+            resultModel = wrapper.wrap(((Field) desc).get(object));
+            // cachedModel remains null, as we don't cache these
+        } else if (desc instanceof Method) {
+            Method method = (Method) desc;
+            resultModel = cachedModel = new JavaMethodModel(
+                    object, method, ClassIntrospector.getArgTypes(classInfo, method), wrapper);
+        } else if (desc instanceof OverloadedMethods) {
+            resultModel = cachedModel = new OverloadedMethodsModel(
+                    object, (OverloadedMethods) desc, wrapper);
+        }
+        
+        // If new cachedModel was created, cache it
+        if (cachedModel != null) {
+            synchronized (this) {
+                if (memberCache == null) {
+                    memberCache = new HashMap<>();
+                }
+                memberCache.put(desc, cachedModel);
+            }
+        }
+        return resultModel;
+    }
+    
+    void clearMemberCache() {
+        synchronized (this) {
+            memberCache = null;
+        }
+    }
+
+    protected TemplateModel invokeGenericGet(Map/*<Object, Object>*/ classInfo, Class<?> clazz, String key)
+            throws IllegalAccessException, InvocationTargetException,
+        TemplateModelException {
+        Method genericGet = (Method) classInfo.get(ClassIntrospector.GENERIC_GET_KEY);
+        if (genericGet == null) {
+            return UNKNOWN;
+        }
+
+        return wrapper.invokeMethod(object, genericGet, new Object[] { key });
+    }
+
+    protected TemplateModel wrap(Object obj)
+    throws TemplateModelException {
+        return wrapper.getOuterIdentity().wrap(obj);
+    }
+    
+    protected Object unwrap(TemplateModel model)
+    throws TemplateModelException {
+        return wrapper.unwrap(model);
+    }
+
+    /**
+     * Tells whether the model is considered to be empty.
+     * It is empty if the wrapped object is a 0 length {@link String}, or an empty {@link Collection} or and empty
+     * {@link Map}, or an {@link Iterator} that has no more items, or a {@link Boolean#FALSE}, or {@code null}. 
+     */
+    @Override
+    public boolean isEmpty() {
+        if (object instanceof String) {
+            return ((String) object).length() == 0;
+        }
+        if (object instanceof Collection) {
+            return ((Collection<?>) object).isEmpty();
+        }
+        if (object instanceof Iterator) {
+            return !((Iterator<?>) object).hasNext();
+        }
+        if (object instanceof Map) {
+            return ((Map<?,?>) object).isEmpty();
+        }
+        // [FM3] Why's FALSE empty? 
+        return object == null || Boolean.FALSE.equals(object);
+    }
+    
+    /**
+     * Returns the same as {@link #getWrappedObject()}; to ensure that, this method will be final starting from 2.4.
+     * This behavior of {@link BeanModel} is assumed by some FreeMarker code. 
+     */
+    @Override
+    public Object getAdaptedObject(Class<?> hint) {
+        return object;  // return getWrappedObject(); starting from 2.4
+    }
+
+    @Override
+    public Object getWrappedObject() {
+        return object;
+    }
+    
+    @Override
+    public int size() {
+        return wrapper.getClassIntrospector().keyCount(object.getClass());
+    }
+
+    @Override
+    public TemplateCollectionModel keys() {
+        return new CollectionAndSequence(new SimpleSequence(keySet(), wrapper));
+    }
+
+    @Override
+    public TemplateCollectionModel values() throws TemplateModelException {
+        List<Object> values = new ArrayList<>(size());
+        TemplateModelIterator it = keys().iterator();
+        while (it.hasNext()) {
+            String key = ((TemplateScalarModel) it.next()).getAsString();
+            values.add(get(key));
+        }
+        return new CollectionAndSequence(new SimpleSequence(values, wrapper));
+    }
+    
+    @Override
+    public String toString() {
+        return object.toString();
+    }
+
+    /**
+     * Helper method to support TemplateHashModelEx. Returns the Set of
+     * Strings which are available via the TemplateHashModel
+     * interface. Subclasses that override <tt>invokeGenericGet</tt> to
+     * provide additional hash keys should also override this method.
+     */
+    protected Set/*<Object>*/ keySet() {
+        return wrapper.getClassIntrospector().keySet(object.getClass());
+    }
+
+    @Override
+    public TemplateModel getAPI() throws TemplateModelException {
+        return wrapper.wrapAsAPI(object);
+    }
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CallableMemberDescriptor.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CallableMemberDescriptor.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CallableMemberDescriptor.java
new file mode 100644
index 0000000..bbaf6bd
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CallableMemberDescriptor.java
@@ -0,0 +1,56 @@
+/*
+ * 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.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+
+/**
+ * Packs a {@link Method} or {@link Constructor} together with its parameter types. The actual
+ * {@link Method} or {@link Constructor} is not exposed by the API, because in rare cases calling them require
+ * type conversion that the Java reflection API can't do, hence the developer shouldn't be tempted to call them
+ * directly. 
+ */
+abstract class CallableMemberDescriptor extends MaybeEmptyCallableMemberDescriptor {
+
+    abstract TemplateModel invokeMethod(DefaultObjectWrapper ow, Object obj, Object[] args)
+            throws TemplateModelException, InvocationTargetException, IllegalAccessException;
+
+    abstract Object invokeConstructor(DefaultObjectWrapper ow, Object[] args)
+            throws IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException,
+            TemplateModelException;
+    
+    abstract String getDeclaration();
+    
+    abstract boolean isConstructor();
+    
+    abstract boolean isStatic();
+
+    abstract boolean isVarargs();
+
+    abstract Class[] getParamTypes();
+
+    abstract String getName();
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CharacterOrString.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CharacterOrString.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CharacterOrString.java
new file mode 100644
index 0000000..6026011
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CharacterOrString.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 org.apache.freemarker.core.model.TemplateScalarModel;
+
+/**
+ * Represents value unwrapped both to {@link Character} and {@link String}. This is needed for unwrapped overloaded
+ * method parameters where both {@link Character} and {@link String} occurs on the same parameter position when the
+ * {@link TemplateScalarModel} to unwrapp contains a {@link String} of length 1.
+ */
+final class CharacterOrString {
+
+    private final String stringValue;
+
+    CharacterOrString(String stringValue) {
+        this.stringValue = stringValue;
+    }
+    
+    String getAsString() {
+        return stringValue;
+    }
+
+    char getAsChar() {
+        return stringValue.charAt(0);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassBasedModelFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassBasedModelFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassBasedModelFactory.java
new file mode 100644
index 0000000..3fd3a2d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassBasedModelFactory.java
@@ -0,0 +1,148 @@
+/*
+ * 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.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.util._ClassUtil;
+
+/**
+ * Base class for hash models keyed by Java class names. 
+ */
+abstract class ClassBasedModelFactory implements TemplateHashModel {
+    private final DefaultObjectWrapper wrapper;
+    
+    private final Map/*<String,TemplateModel>*/ cache = new ConcurrentHashMap();
+    private final Set classIntrospectionsInProgress = new HashSet();
+    
+    protected ClassBasedModelFactory(DefaultObjectWrapper wrapper) {
+        this.wrapper = wrapper;
+    }
+
+    @Override
+    public TemplateModel get(String key) throws TemplateModelException {
+        try {
+            return getInternal(key);
+        } catch (Exception e) {
+            if (e instanceof TemplateModelException) {
+                throw (TemplateModelException) e;
+            } else {
+                throw new TemplateModelException(e);
+            }
+        }
+    }
+
+    private TemplateModel getInternal(String key) throws TemplateModelException, ClassNotFoundException {
+        {
+            TemplateModel model = (TemplateModel) cache.get(key);
+            if (model != null) return model;
+        }
+
+        final ClassIntrospector classIntrospector;
+        int classIntrospectorClearingCounter;
+        final Object sharedLock = wrapper.getSharedIntrospectionLock();
+        synchronized (sharedLock) {
+            TemplateModel model = (TemplateModel) cache.get(key);
+            if (model != null) return model;
+            
+            while (model == null
+                    && classIntrospectionsInProgress.contains(key)) {
+                // Another thread is already introspecting this class;
+                // waiting for its result.
+                try {
+                    sharedLock.wait();
+                    model = (TemplateModel) cache.get(key);
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(
+                            "Class inrospection data lookup aborded: " + e);
+                }
+            }
+            if (model != null) return model;
+            
+            // This will be the thread that introspects this class.
+            classIntrospectionsInProgress.add(key);
+
+            // While the classIntrospector should not be changed from another thread, badly written apps can do that,
+            // and it's cheap to get the classIntrospector from inside the lock here:   
+            classIntrospector = wrapper.getClassIntrospector();
+            classIntrospectorClearingCounter = classIntrospector.getClearingCounter();
+        }
+        try {
+            final Class clazz = _ClassUtil.forName(key);
+            
+            // This is called so that we trigger the
+            // class-reloading detector. If clazz is a reloaded class,
+            // the wrapper will in turn call our clearCache method.
+            // TODO: Why do we check it now and only now?
+            classIntrospector.get(clazz);
+            
+            TemplateModel model = createModel(clazz);
+            // Warning: model will be null if the class is not good for the subclass.
+            // For example, EnumModels#createModel returns null if clazz is not an enum.
+            
+            if (model != null) {
+                synchronized (sharedLock) {
+                    // Save it into the cache, but only if nothing relevant has changed while we were outside the lock: 
+                    if (classIntrospector == wrapper.getClassIntrospector()
+                            && classIntrospectorClearingCounter == classIntrospector.getClearingCounter()) {  
+                        cache.put(key, model);
+                    }
+                }
+            }
+            return model;
+        } finally {
+            synchronized (sharedLock) {
+                classIntrospectionsInProgress.remove(key);
+                sharedLock.notifyAll();
+            }
+        }
+    }
+    
+    void clearCache() {
+        synchronized (wrapper.getSharedIntrospectionLock()) {
+            cache.clear();
+        }
+    }
+    
+    void removeFromCache(Class clazz) {
+        synchronized (wrapper.getSharedIntrospectionLock()) {
+            cache.remove(clazz.getName());
+        }
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return false;
+    }
+    
+    protected abstract TemplateModel createModel(Class clazz) 
+    throws TemplateModelException;
+    
+    protected DefaultObjectWrapper getWrapper() {
+        return wrapper;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassChangeNotifier.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassChangeNotifier.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassChangeNotifier.java
new file mode 100644
index 0000000..52321f0
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassChangeNotifier.java
@@ -0,0 +1,32 @@
+/*
+ * 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;
+
+/**
+ * Reports when the non-private interface of a class was changed to the subscribers.   
+ */
+interface ClassChangeNotifier {
+    
+    /**
+     * @param classIntrospector Should only be weak-referenced from the monitor object.
+     */
+    void subscribe(ClassIntrospector classIntrospector);
+
+}


[47/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirEscape.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirEscape.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirEscape.java
new file mode 100644
index 0000000..c3db14e
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirEscape.java
@@ -0,0 +1,111 @@
+/*
+ * 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.core.ASTExpression.ReplacemenetState;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * AST directive node: {@code #escape}.
+ */
+class ASTDirEscape extends ASTDirective {
+
+    private final String variable;
+    private final ASTExpression expr;
+    private ASTExpression escapedExpr;
+
+
+    ASTDirEscape(String variable, ASTExpression expr, ASTExpression escapedExpr) {
+        this.variable = variable;
+        this.expr = expr;
+        this.escapedExpr = escapedExpr;
+    }
+
+    void setContent(TemplateElements children) {
+        setChildren(children);
+        // We don't need it anymore at this point
+        escapedExpr = null;
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) throws TemplateException, IOException {
+        return getChildBuffer();
+    }
+
+    ASTExpression doEscape(ASTExpression expression) {
+        return escapedExpr.deepCloneWithIdentifierReplaced(variable, expression, new ReplacemenetState());
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder sb = new StringBuilder();
+        if (canonical) sb.append('<');
+        sb.append(getNodeTypeSymbol())
+                .append(' ').append(_StringUtil.toFTLTopLevelIdentifierReference(variable))
+                .append(" as ").append(expr.getCanonicalForm());
+        if (canonical) {
+            sb.append('>');
+            sb.append(getChildrenCanonicalForm());
+            sb.append("</").append(getNodeTypeSymbol()).append('>');
+        }
+        return sb.toString();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "#escape";
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return variable;
+        case 1: return expr;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        switch (idx) {
+        case 0: return ParameterRole.PLACEHOLDER_VARIABLE;
+        case 1: return ParameterRole.EXPRESSION_TEMPLATE;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }    
+
+    @Override
+    boolean isOutputCacheable() {
+        return true;
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirFallback.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirFallback.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirFallback.java
new file mode 100644
index 0000000..08b5c42
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirFallback.java
@@ -0,0 +1,70 @@
+/*
+ * 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;
+
+/**
+ * AST directive node: {@code #fallback}.
+ */
+final class ASTDirFallback extends ASTDirective {
+
+    @Override
+    ASTElement[] accept(Environment env) throws IOException, TemplateException {
+        env.fallback();
+        return null;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        return canonical ? "<" + getNodeTypeSymbol() + "/>" : getNodeTypeSymbol();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "#fallback";
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 0;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+    @Override
+    boolean isShownInStackTrace() {
+        return true;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirFlush.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirFlush.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirFlush.java
new file mode 100644
index 0000000..ad7aff4
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirFlush.java
@@ -0,0 +1,65 @@
+/*
+ * 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;
+
+/**
+ * AST directive node: {@code #flush} 
+ */
+final class ASTDirFlush extends ASTDirective {
+
+    @Override
+    ASTElement[] accept(Environment env) throws IOException {
+        env.getOut().flush();
+        return null;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        return canonical ? "<" + getNodeTypeSymbol() + "/>" : getNodeTypeSymbol();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "#flush";
+    }
+ 
+    @Override
+    int getParameterCount() {
+        return 0;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirIfElseIfElseContainer.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirIfElseIfElseContainer.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirIfElseIfElseContainer.java
new file mode 100644
index 0000000..d04b0a0
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirIfElseIfElseContainer.java
@@ -0,0 +1,107 @@
+/*
+ * 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;
+
+/**
+ * AST directive node: Container for a group of related {@code #if}, {@code #elseif} and {@code #else} directives.
+ * Each such block is a nested {@link ASTDirIfOrElseOrElseIf}. Note that if an {@code #if} stands alone,
+ * {@link ASTDirIfOrElseOrElseIf} doesn't need this parent element.
+ */
+final class ASTDirIfElseIfElseContainer extends ASTDirective {
+
+    ASTDirIfElseIfElseContainer(ASTDirIfOrElseOrElseIf block) {
+        setChildBufferCapacity(1);
+        addBlock(block);
+    }
+
+    void addBlock(ASTDirIfOrElseOrElseIf block) {
+        addChild(block);
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) throws TemplateException, IOException {
+        int ln  = getChildCount();
+        for (int i = 0; i < ln; i++) {
+            ASTDirIfOrElseOrElseIf cblock = (ASTDirIfOrElseOrElseIf) getChild(i);
+            ASTExpression condition = cblock.condition;
+            env.replaceElementStackTop(cblock);
+            if (condition == null || condition.evalToBoolean(env)) {
+                return cblock.getChildBuffer();
+            }
+        }
+        return null;
+    }
+
+    @Override
+    ASTElement postParseCleanup(boolean stripWhitespace)
+        throws ParseException {
+        if (getChildCount() == 1) {
+            ASTDirIfOrElseOrElseIf cblock = (ASTDirIfOrElseOrElseIf) getChild(0);
+            cblock.setLocation(getTemplate(), cblock, this);
+            return cblock.postParseCleanup(stripWhitespace);
+        } else {
+            return super.postParseCleanup(stripWhitespace);
+        }
+    }
+    
+    @Override
+    protected String dump(boolean canonical) {
+        if (canonical) {
+            StringBuilder buf = new StringBuilder();
+            int ln = getChildCount();
+            for (int i = 0; i < ln; i++) {
+                ASTDirIfOrElseOrElseIf cblock = (ASTDirIfOrElseOrElseIf) getChild(i);
+                buf.append(cblock.dump(canonical));
+            }
+            buf.append("</#if>");
+            return buf.toString();
+        } else {
+            return getNodeTypeSymbol();
+        }
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "#if-#elseif-#else-container";
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 0;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirIfOrElseOrElseIf.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirIfOrElseOrElseIf.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirIfOrElseOrElseIf.java
new file mode 100644
index 0000000..136b5b7
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirIfOrElseOrElseIf.java
@@ -0,0 +1,114 @@
+/*
+ * 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.core.util.BugException;
+
+/**
+ * AST directive node: An element that represents a conditionally executed block: {@code #if}, {@code #elseif} or
+ * {@code #elseif}. Note that when an {@code #if} has related {@code #elseif}-s or {@code #else}, an
+ * {@link ASTDirIfElseIfElseContainer} parent must be used. For a lonely {@code #if}, no such parent is needed. 
+ */
+final class ASTDirIfOrElseOrElseIf extends ASTDirective {
+
+    static final int TYPE_IF = 0;
+    static final int TYPE_ELSE = 1;
+    static final int TYPE_ELSE_IF = 2;
+    
+    final ASTExpression condition;
+    private final int type;
+
+    ASTDirIfOrElseOrElseIf(ASTExpression condition, TemplateElements children, int type) {
+        this.condition = condition;
+        setChildren(children);
+        this.type = type;
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) throws TemplateException, IOException {
+        if (condition == null || condition.evalToBoolean(env)) {
+            return getChildBuffer();
+        }
+        return null;
+    }
+    
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder buf = new StringBuilder();
+        if (canonical) buf.append('<');
+        buf.append(getNodeTypeSymbol());
+        if (condition != null) {
+            buf.append(' ');
+            buf.append(condition.getCanonicalForm());
+        }
+        if (canonical) {
+            buf.append(">");
+            buf.append(getChildrenCanonicalForm());
+            if (!(getParent() instanceof ASTDirIfElseIfElseContainer)) {
+                buf.append("</#if>");
+            }
+        }
+        return buf.toString();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        if (type == TYPE_ELSE) {
+            return "#else";
+        } else if (type == TYPE_IF) {
+            return "#if";
+        } else if (type == TYPE_ELSE_IF) {
+            return "#elseif";
+        } else {
+            throw new BugException("Unknown type");
+        }
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return condition;
+        case 1: return Integer.valueOf(type);
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        switch (idx) {
+        case 0: return ParameterRole.CONDITION;
+        case 1: return ParameterRole.AST_NODE_SUBTYPE;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirImport.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirImport.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirImport.java
new file mode 100644
index 0000000..38e88bf
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirImport.java
@@ -0,0 +1,125 @@
+/*
+ * 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.core.templateresolver.MalformedTemplateNameException;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * AST directive node: {@code #import}
+ */
+final class ASTDirImport extends ASTDirective {
+
+    private ASTExpression importedTemplateNameExp;
+    private String targetNsVarName;
+
+    /**
+     * @param template the template that this directive is a part of.
+     * @param importedTemplateNameExp the name of the template to be included.
+     * @param targetNsVarName the name of the  variable to assign this library's namespace to
+     */
+    ASTDirImport(Template template,
+            ASTExpression importedTemplateNameExp,
+            String targetNsVarName) {
+        this.targetNsVarName = targetNsVarName;
+        this.importedTemplateNameExp = importedTemplateNameExp;
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) throws TemplateException, IOException {
+        final String importedTemplateName = importedTemplateNameExp.evalAndCoerceToPlainText(env);
+        final String fullImportedTemplateName;
+        try {
+            fullImportedTemplateName = env.toFullTemplateName(getTemplate().getLookupName(), importedTemplateName);
+        } catch (MalformedTemplateNameException e) {
+            throw new _MiscTemplateException(e, env,
+                    "Malformed template name ", new _DelayedJQuote(e.getTemplateName()), ":\n",
+                    e.getMalformednessDescription());
+        }
+        
+        try {
+            env.importLib(fullImportedTemplateName, targetNsVarName);
+        } catch (IOException e) {
+            throw new _MiscTemplateException(e, env,
+                    "Template importing failed (for parameter value ",
+                    new _DelayedJQuote(importedTemplateName),
+                    "):\n", new _DelayedGetMessage(e));
+        }
+        return null;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder buf = new StringBuilder();
+        if (canonical) buf.append('<');
+        buf.append(getNodeTypeSymbol());
+        buf.append(' ');
+        buf.append(importedTemplateNameExp.getCanonicalForm());
+        buf.append(" as ");
+        buf.append(_StringUtil.toFTLTopLevelTragetIdentifier(targetNsVarName));
+        if (canonical) buf.append("/>");
+        return buf.toString();
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return "#import";
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return importedTemplateNameExp;
+        case 1: return targetNsVarName;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        switch (idx) {
+        case 0: return ParameterRole.TEMPLATE_NAME;
+        case 1: return ParameterRole.NAMESPACE;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }    
+    
+    public String getTemplateName() {
+        return importedTemplateNameExp.toString();
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+    @Override
+    boolean isShownInStackTrace() {
+        return true;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirInclude.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirInclude.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirInclude.java
new file mode 100644
index 0000000..2088d62
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirInclude.java
@@ -0,0 +1,174 @@
+/*
+ * 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.core.templateresolver.MalformedTemplateNameException;
+import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * AST directive node: {@code #include} 
+ */
+final class ASTDirInclude extends ASTDirective {
+
+    private final ASTExpression includedTemplateNameExp, ignoreMissingExp;
+    private final Boolean ignoreMissingExpPrecalcedValue;
+
+    /**
+     * @param template the template that this <tt>#include</tt> is a part of.
+     * @param includedTemplateNameExp the path of the template to be included.
+     */
+    ASTDirInclude(Template template,
+            ASTExpression includedTemplateNameExp,
+            ASTExpression ignoreMissingExp) throws ParseException {
+        this.includedTemplateNameExp = includedTemplateNameExp;
+
+        this.ignoreMissingExp = ignoreMissingExp;
+        if (ignoreMissingExp != null && ignoreMissingExp.isLiteral()) {
+            try {
+                try {
+                    ignoreMissingExpPrecalcedValue = Boolean.valueOf(
+                            ignoreMissingExp.evalToBoolean(template.getConfiguration()));
+                } catch (NonBooleanException e) {
+                    throw new ParseException("Expected a boolean as the value of the \"ignore_missing\" attribute",
+                            ignoreMissingExp, e);
+                }
+            } catch (TemplateException e) {
+                // evaluation of literals must not throw a TemplateException
+                throw new BugException(e);
+            }
+        } else {
+            ignoreMissingExpPrecalcedValue = null;
+        }
+    }
+    
+    @Override
+    ASTElement[] accept(Environment env) throws TemplateException, IOException {
+        final String includedTemplateName = includedTemplateNameExp.evalAndCoerceToPlainText(env);
+        final String fullIncludedTemplateName;
+        try {
+            fullIncludedTemplateName = env.toFullTemplateName(getTemplate().getLookupName(), includedTemplateName);
+        } catch (MalformedTemplateNameException e) {
+            throw new _MiscTemplateException(e, env,
+                    "Malformed template name ", new _DelayedJQuote(e.getTemplateName()), ":\n",
+                    e.getMalformednessDescription());
+        }
+        
+        final boolean ignoreMissing;
+        if (ignoreMissingExpPrecalcedValue != null) {
+            ignoreMissing = ignoreMissingExpPrecalcedValue.booleanValue();
+        } else if (ignoreMissingExp != null) {
+            ignoreMissing = ignoreMissingExp.evalToBoolean(env);
+        } else {
+            ignoreMissing = false;
+        }
+        
+        final Template includedTemplate;
+        try {
+            includedTemplate = env.getTemplateForInclusion(fullIncludedTemplateName, ignoreMissing);
+        } catch (IOException e) {
+            throw new _MiscTemplateException(e, env,
+                    "Template inclusion failed (for parameter value ",
+                    new _DelayedJQuote(includedTemplateName),
+                    "):\n", new _DelayedGetMessage(e));
+        }
+        
+        if (includedTemplate != null) {
+            env.include(includedTemplate);
+        }
+        return null;
+    }
+    
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder buf = new StringBuilder();
+        if (canonical) buf.append('<');
+        buf.append(getNodeTypeSymbol());
+        buf.append(' ');
+        buf.append(includedTemplateNameExp.getCanonicalForm());
+        if (ignoreMissingExp != null) {
+            buf.append(" ignore_missing=").append(ignoreMissingExp.getCanonicalForm());
+        }
+        if (canonical) buf.append("/>");
+        return buf.toString();
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return "#include";
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return includedTemplateNameExp;
+        case 1: return ignoreMissingExp;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        switch (idx) {
+        case 0: return ParameterRole.TEMPLATE_NAME;
+        case 1: return ParameterRole.IGNORE_MISSING_PARAMETER;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+
+    private boolean getYesNo(ASTExpression exp, String s) throws TemplateException {
+        try {
+           return _StringUtil.getYesNo(s);
+        } catch (IllegalArgumentException iae) {
+            throw new _MiscTemplateException(exp,
+                     "Value must be boolean (or one of these strings: "
+                     + "\"n\", \"no\", \"f\", \"false\", \"y\", \"yes\", \"t\", \"true\"), but it was ",
+                     new _DelayedJQuote(s), ".");
+        }
+    }
+
+/*
+    boolean heedsOpeningWhitespace() {
+        return true;
+    }
+
+    boolean heedsTrailingWhitespace() {
+        return true;
+    }
+*/
+    
+    @Override
+    boolean isShownInStackTrace() {
+        return true;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirItems.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirItems.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirItems.java
new file mode 100644
index 0000000..292d767
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirItems.java
@@ -0,0 +1,120 @@
+/*
+ * 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.core.ASTDirList.IterationContext;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * AST directive node: {@code #items}
+ */
+class ASTDirItems extends ASTDirective {
+
+    private final String loopVarName;
+    private final String loopVar2Name;
+
+    /**
+     * @param loopVar2Name
+     *            For non-hash listings always {@code null}, for hash listings {@code loopVarName} and
+     *            {@code loopVarName2} holds the key- and value loop variable names.
+     */
+    ASTDirItems(String loopVarName, String loopVar2Name, TemplateElements children) {
+        this.loopVarName = loopVarName;
+        this.loopVar2Name = loopVar2Name;
+        setChildren(children);
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) throws TemplateException, IOException {
+        final IterationContext iterCtx = ASTDirList.findEnclosingIterationContext(env, null);
+        if (iterCtx == null) {
+            // The parser should prevent this situation
+            throw new _MiscTemplateException(env,
+                    getNodeTypeSymbol(), " without iteration in context");
+        }
+        
+        iterCtx.loopForItemsElement(env, getChildBuffer(), loopVarName, loopVar2Name);
+        return null;
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return true;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder sb = new StringBuilder();
+        if (canonical) sb.append('<');
+        sb.append(getNodeTypeSymbol());
+        sb.append(" as ");
+        sb.append(_StringUtil.toFTLTopLevelIdentifierReference(loopVarName));
+        if (loopVar2Name != null) {
+            sb.append(", ");
+            sb.append(_StringUtil.toFTLTopLevelIdentifierReference(loopVar2Name));
+        }
+        if (canonical) {
+            sb.append('>');
+            sb.append(getChildrenCanonicalForm());
+            sb.append("</");
+            sb.append(getNodeTypeSymbol());
+            sb.append('>');
+        }
+        return sb.toString();
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return "#items";
+    }
+
+    @Override
+    int getParameterCount() {
+        return loopVar2Name != null ? 2 : 1;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0:
+            if (loopVarName == null) throw new IndexOutOfBoundsException();
+            return loopVarName;
+        case 1:
+            if (loopVar2Name == null) throw new IndexOutOfBoundsException();
+            return loopVar2Name;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        switch (idx) {
+        case 0:
+            if (loopVarName == null) throw new IndexOutOfBoundsException();
+            return ParameterRole.TARGET_LOOP_VARIABLE;
+        case 1:
+            if (loopVar2Name == null) throw new IndexOutOfBoundsException();
+            return ParameterRole.TARGET_LOOP_VARIABLE;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirList.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirList.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirList.java
new file mode 100644
index 0000000..0675882
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirList.java
@@ -0,0 +1,462 @@
+/*
+ * 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.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateHashModelEx2;
+import org.apache.freemarker.core.model.TemplateHashModelEx2.KeyValuePair;
+import org.apache.freemarker.core.model.TemplateHashModelEx2.KeyValuePairIterator;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * AST directive node: {@code #list} element, or pre-{@code #else} section of it inside a
+ * {@link ASTDirListElseContainer}.
+ */
+final class ASTDirList extends ASTDirective {
+
+    private final ASTExpression listedExp;
+    private final String loopVarName;
+    private final String loopVar2Name;
+    private final boolean hashListing;
+
+    /**
+     * @param listedExp
+     *            a variable referring to a sequence or collection or extended hash to list
+     * @param loopVarName
+     *            The name of the variable that will hold the value of the current item when looping through listed value,
+     *            or {@code null} if we have a nested {@code #items}. If this is a hash listing then this variable will holds the value
+     *            of the hash key.
+     * @param loopVar2Name
+     *            The name of the variable that will hold the value of the current item when looping through the list,
+     *            or {@code null} if we have a nested {@code #items}. If this is a hash listing then it variable will hold the value
+     *            from the key-value pair.
+     * @param childrenBeforeElse
+     *            The nested content to execute if the listed value wasn't empty; can't be {@code null}. If the loop variable
+     *            was specified in the start tag, this is also what we will iterate over.
+     * @param hashListing
+     *            Whether this is a key-value pair listing, or a usual listing. This is properly set even if we have
+     *            a nested {@code #items}.
+     */
+    ASTDirList(ASTExpression listedExp,
+                  String loopVarName,
+                  String loopVar2Name,
+                  TemplateElements childrenBeforeElse,
+                  boolean hashListing) {
+        this.listedExp = listedExp;
+        this.loopVarName = loopVarName;
+        this.loopVar2Name = loopVar2Name;
+        setChildren(childrenBeforeElse);
+        this.hashListing = hashListing;
+    }
+    
+    boolean isHashListing() {
+        return hashListing;
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) throws TemplateException, IOException {
+        acceptWithResult(env);
+        return null;
+    }
+    
+    boolean acceptWithResult(Environment env) throws TemplateException, IOException {
+        TemplateModel listedValue = listedExp.eval(env);
+        if (listedValue == null) {
+            listedExp.assertNonNull(null, env);
+        }
+
+        return env.visitIteratorBlock(new IterationContext(listedValue, loopVarName, loopVar2Name));
+    }
+
+    /**
+     * @param loopVariableName
+     *            Then name of the loop variable whose context we are looking for, or {@code null} if we simply look for
+     *            the innermost context.
+     * @return The matching context or {@code null} if no such context exists.
+     */
+    static IterationContext findEnclosingIterationContext(Environment env, String loopVariableName)
+            throws _MiscTemplateException {
+        LocalContextStack ctxStack = env.getLocalContextStack();
+        if (ctxStack != null) {
+            for (int i = ctxStack.size() - 1; i >= 0; i--) {
+                Object ctx = ctxStack.get(i);
+                if (ctx instanceof IterationContext
+                        && (loopVariableName == null
+                            || loopVariableName.equals(((IterationContext) ctx).getLoopVariableName())
+                            || loopVariableName.equals(((IterationContext) ctx).getLoopVariable2Name())
+                            )) {
+                    return (IterationContext) ctx;
+                }
+            }
+        }
+        return null;
+    }
+    
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder buf = new StringBuilder();
+        if (canonical) buf.append('<');
+        buf.append(getNodeTypeSymbol());
+        buf.append(' ');
+        buf.append(listedExp.getCanonicalForm());
+        if (loopVarName != null) {
+            buf.append(" as ");
+            buf.append(_StringUtil.toFTLTopLevelIdentifierReference(loopVarName));
+            if (loopVar2Name != null) {
+                buf.append(", ");
+                buf.append(_StringUtil.toFTLTopLevelIdentifierReference(loopVar2Name));
+            }
+        }
+        if (canonical) {
+            buf.append(">");
+            buf.append(getChildrenCanonicalForm());
+            if (!(getParent() instanceof ASTDirListElseContainer)) {
+                buf.append("</");
+                buf.append(getNodeTypeSymbol());
+                buf.append('>');
+            }
+        }
+        return buf.toString();
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 1 + (loopVarName != null ? 1 : 0) + (loopVar2Name != null ? 1 : 0);
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0:
+            return listedExp;
+        case 1:
+            if (loopVarName == null) throw new IndexOutOfBoundsException();
+            return loopVarName;
+        case 2:
+            if (loopVar2Name == null) throw new IndexOutOfBoundsException();
+            return loopVar2Name;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        switch (idx) {
+        case 0:
+            return ParameterRole.LIST_SOURCE;
+        case 1:
+            if (loopVarName == null) throw new IndexOutOfBoundsException();
+            return ParameterRole.TARGET_LOOP_VARIABLE;
+        case 2:
+            if (loopVar2Name == null) throw new IndexOutOfBoundsException();
+            return ParameterRole.TARGET_LOOP_VARIABLE;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }    
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "#list";
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return loopVarName != null;
+    }
+
+    /**
+     * Holds the context of a #list directive.
+     */
+    class IterationContext implements LocalContext {
+        
+        private static final String LOOP_STATE_HAS_NEXT = "_has_next"; // lenght: 9
+        private static final String LOOP_STATE_INDEX = "_index"; // length 6
+        
+        private Object openedIterator;
+        private boolean hasNext;
+        private TemplateModel loopVar;
+        private TemplateModel loopVar2;
+        private int index;
+        private boolean alreadyEntered;
+        private Collection localVarNames = null;
+        
+        /** If the {@code #list} has nested {@code #items}, it's {@code null} outside the {@code #items}. */
+        private String loopVarName;
+        /** Used if we list key-value pairs */
+        private String loopVar2Name;
+        
+        private final TemplateModel listedValue;
+        
+        public IterationContext(TemplateModel listedValue, String loopVarName, String loopVar2Name) {
+            this.listedValue = listedValue;
+            this.loopVarName = loopVarName;
+            this.loopVar2Name = loopVar2Name;
+        }
+        
+        boolean accept(Environment env) throws TemplateException, IOException {
+            return executeNestedContent(env, getChildBuffer());
+        }
+
+        void loopForItemsElement(Environment env, ASTElement[] childBuffer, String loopVarName, String loopVar2Name)
+                    throws
+                TemplateException, IOException {
+            try {
+                if (alreadyEntered) {
+                    throw new _MiscTemplateException(env,
+                            "The #items directive was already entered earlier for this listing.");
+                }
+                alreadyEntered = true;
+                this.loopVarName = loopVarName;
+                this.loopVar2Name = loopVar2Name;
+                executeNestedContent(env, childBuffer);
+            } finally {
+                this.loopVarName = null;
+                this.loopVar2Name = null;
+            }
+        }
+
+        /**
+         * Executes the given block for the {@link #listedValue}: if {@link #loopVarName} is non-{@code null}, then for
+         * each list item once, otherwise once if {@link #listedValue} isn't empty.
+         */
+        private boolean executeNestedContent(Environment env, ASTElement[] childBuffer)
+                throws TemplateException, IOException {
+            return !hashListing
+                    ? executedNestedContentForCollOrSeqListing(env, childBuffer)
+                    : executedNestedContentForHashListing(env, childBuffer);
+        }
+
+        private boolean executedNestedContentForCollOrSeqListing(Environment env, ASTElement[] childBuffer)
+                throws IOException, TemplateException {
+            final boolean listNotEmpty;
+            if (listedValue instanceof TemplateCollectionModel) {
+                final TemplateCollectionModel collModel = (TemplateCollectionModel) listedValue;
+                final TemplateModelIterator iterModel
+                        = openedIterator == null ? collModel.iterator()
+                                : ((TemplateModelIterator) openedIterator);
+                listNotEmpty = iterModel.hasNext();
+                if (listNotEmpty) {
+                    if (loopVarName != null) {
+                        try {
+                            do {
+                                loopVar = iterModel.next();
+                                hasNext = iterModel.hasNext();
+                                env.visit(childBuffer);
+                                index++;
+                            } while (hasNext);
+                        } catch (ASTDirBreak.Break br) {
+                            // Silently exit loop
+                        }
+                        openedIterator = null;
+                    } else {
+                        // We must reuse this later, because TemplateCollectionModel-s that wrap an Iterator only
+                        // allow one iterator() call.
+                        openedIterator = iterModel;
+                        env.visit(childBuffer);
+                    }
+                }
+            } else if (listedValue instanceof TemplateSequenceModel) {
+                final TemplateSequenceModel seqModel = (TemplateSequenceModel) listedValue;
+                final int size = seqModel.size();
+                listNotEmpty = size != 0;
+                if (listNotEmpty) {
+                    if (loopVarName != null) {
+                        try {
+                            for (index = 0; index < size; index++) {
+                                loopVar = seqModel.get(index);
+                                hasNext = (size > index + 1);
+                                env.visit(childBuffer);
+                            }
+                        } catch (ASTDirBreak.Break br) {
+                            // Silently exit loop
+                        }
+                    } else {
+                        env.visit(childBuffer);
+                    }
+                }
+            } else if (listedValue instanceof TemplateHashModelEx
+                    && !NonSequenceOrCollectionException.isWrappedIterable(listedValue)) {
+                throw new NonSequenceOrCollectionException(env,
+                        new _ErrorDescriptionBuilder("The value you try to list is ",
+                                new _DelayedAOrAn(new _DelayedFTLTypeDescription(listedValue)),
+                                ", thus you must specify two loop variables after the \"as\"; one for the key, and "
+                                + "another for the value, like ", "<#... as k, v>", ")."
+                                ));
+            } else {
+                throw new NonSequenceOrCollectionException(
+                        listedExp, listedValue, env);
+            }
+            return listNotEmpty;
+        }
+
+        private boolean executedNestedContentForHashListing(Environment env, ASTElement[] childBuffer)
+                throws IOException, TemplateException {
+            final boolean hashNotEmpty;
+            if (listedValue instanceof TemplateHashModelEx) {
+                TemplateHashModelEx listedHash = (TemplateHashModelEx) listedValue; 
+                if (listedHash instanceof TemplateHashModelEx2) {
+                    KeyValuePairIterator kvpIter
+                            = openedIterator == null ? ((TemplateHashModelEx2) listedHash).keyValuePairIterator()
+                                    : (KeyValuePairIterator) openedIterator;
+                    hashNotEmpty = kvpIter.hasNext();
+                    if (hashNotEmpty) {
+                        if (loopVarName != null) {
+                            try {
+                                do {
+                                    KeyValuePair kvp = kvpIter.next();
+                                    loopVar = kvp.getKey();
+                                    loopVar2 = kvp.getValue();
+                                    hasNext = kvpIter.hasNext();
+                                    env.visit(childBuffer);
+                                    index++;
+                                } while (hasNext);
+                            } catch (ASTDirBreak.Break br) {
+                                // Silently exit loop
+                            }
+                            openedIterator = null;
+                        } else {
+                            // We will reuse this at the #iterms
+                            openedIterator = kvpIter;
+                            env.visit(childBuffer);
+                        }
+                    }
+                } else { //  not a TemplateHashModelEx2, but still a TemplateHashModelEx
+                    TemplateModelIterator keysIter = listedHash.keys().iterator();
+                    hashNotEmpty = keysIter.hasNext();
+                    if (hashNotEmpty) {
+                        if (loopVarName != null) {
+                            try {
+                                do {
+                                    loopVar = keysIter.next();
+                                    if (!(loopVar instanceof TemplateScalarModel)) {
+                                        throw new NonStringException(env,
+                                                new _ErrorDescriptionBuilder(
+                                                        "When listing key-value pairs of traditional hash "
+                                                        + "implementations, all keys must be strings, but one of them "
+                                                        + "was ",
+                                                        new _DelayedAOrAn(new _DelayedFTLTypeDescription(loopVar)), "."
+                                                        ).tip("The listed value's TemplateModel class was ",
+                                                                new _DelayedShortClassName(listedValue.getClass()),
+                                                                ", which doesn't implement ",
+                                                                new _DelayedShortClassName(TemplateHashModelEx2.class),
+                                                                ", which leads to this restriction."));
+                                    }
+                                    loopVar2 = listedHash.get(((TemplateScalarModel) loopVar).getAsString());
+                                    hasNext = keysIter.hasNext();
+                                    env.visit(childBuffer);
+                                    index++;
+                                } while (hasNext);
+                            } catch (ASTDirBreak.Break br) {
+                                // Silently exit loop
+                            }
+                        } else {
+                            env.visit(childBuffer);
+                        }
+                    }
+                }
+            } else if (listedValue instanceof TemplateCollectionModel
+                    || listedValue instanceof TemplateSequenceModel) {
+                throw new NonSequenceOrCollectionException(env,
+                        new _ErrorDescriptionBuilder("The value you try to list is ",
+                                new _DelayedAOrAn(new _DelayedFTLTypeDescription(listedValue)),
+                                ", thus you must specify only one loop variable after the \"as\" (there's no separate "
+                                + "key and value)."
+                                ));
+            } else {
+                throw new NonExtendedHashException(
+                        listedExp, listedValue, env);
+            }
+            return hashNotEmpty;
+        }
+
+        String getLoopVariableName() {
+            return loopVarName;
+        }
+
+        String getLoopVariable2Name() {
+            return loopVar2Name;
+        }
+        
+        @Override
+        public TemplateModel getLocalVariable(String name) {
+            String loopVarName = this.loopVarName;
+            if (loopVarName != null && name.startsWith(loopVarName)) {
+                switch(name.length() - loopVarName.length()) {
+                    case 0: 
+                        return loopVar;
+                    case 6: 
+                        if (name.endsWith(LOOP_STATE_INDEX)) {
+                            return new SimpleNumber(index);
+                        }
+                        break;
+                    case 9: 
+                        if (name.endsWith(LOOP_STATE_HAS_NEXT)) {
+                            return hasNext ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+                        }
+                        break;
+                }
+            }
+            
+            if (name.equals(loopVar2Name)) {
+                return loopVar2;
+            }
+            
+            return null;
+        }
+        
+        @Override
+        public Collection getLocalVariableNames() {
+            String loopVarName = this.loopVarName;
+            if (loopVarName != null) {
+                if (localVarNames == null) {
+                    localVarNames = new ArrayList(3);
+                    localVarNames.add(loopVarName);
+                    localVarNames.add(loopVarName + LOOP_STATE_INDEX);
+                    localVarNames.add(loopVarName + LOOP_STATE_HAS_NEXT);
+                }
+                return localVarNames;
+            } else {
+                return Collections.EMPTY_LIST;
+            }
+        }
+
+        boolean hasNext() {
+            return hasNext;
+        }
+        
+        int getIndex() {
+            return index;
+        }
+        
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirListElseContainer.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirListElseContainer.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirListElseContainer.java
new file mode 100644
index 0000000..a85b81c
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirListElseContainer.java
@@ -0,0 +1,88 @@
+/*
+ * 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;
+
+/**
+ * AST directive node: When a {@code #list} has an {@code #else}, this is the parent of the two nodes.
+ */
+class ASTDirListElseContainer extends ASTDirective {
+
+    private final ASTDirList listPart;
+    private final ASTDirElseOfList elsePart;
+
+    public ASTDirListElseContainer(ASTDirList listPart, ASTDirElseOfList elsePart) {
+        setChildBufferCapacity(2);
+        addChild(listPart);
+        addChild(elsePart);
+        this.listPart = listPart;
+        this.elsePart = elsePart;
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) throws TemplateException, IOException {
+        if (!listPart.acceptWithResult(env)) {
+            return elsePart.accept(env);
+        }
+        return null;
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        if (canonical) {
+            StringBuilder buf = new StringBuilder();
+            int ln = getChildCount();
+            for (int i = 0; i < ln; i++) {
+                ASTElement element = getChild(i);
+                buf.append(element.dump(canonical));
+            }
+            buf.append("</#list>");
+            return buf.toString();
+        } else {
+            return getNodeTypeSymbol();
+        }
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return "#list-#else-container";
+    }
+
+    @Override
+    int getParameterCount() {
+        return 0;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirMacro.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirMacro.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirMacro.java
new file mode 100644
index 0000000..5bb2712
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirMacro.java
@@ -0,0 +1,325 @@
+/*
+ * 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.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * AST directive node: {@code #macro}
+ */
+final class ASTDirMacro extends ASTDirective implements TemplateModel {
+
+    static final ASTDirMacro DO_NOTHING_MACRO = new ASTDirMacro(".pass", 
+            Collections.EMPTY_LIST, 
+            Collections.EMPTY_MAP,
+            null, false,
+            TemplateElements.EMPTY);
+    
+    final static int TYPE_MACRO = 0;
+    final static int TYPE_FUNCTION = 1;
+    
+    private final String name;
+    private final String[] paramNames;
+    private final Map paramDefaults;
+    private final String catchAllParamName;
+    private final boolean function;
+
+    ASTDirMacro(String name, List argumentNames, Map args, 
+            String catchAllParamName, boolean function,
+            TemplateElements children) {
+        this.name = name;
+        paramNames = (String[]) argumentNames.toArray(new String[argumentNames.size()]);
+        paramDefaults = args;
+        
+        this.function = function;
+        this.catchAllParamName = catchAllParamName;
+
+        setChildren(children);
+    }
+
+    public String getCatchAll() {
+        return catchAllParamName;
+    }
+    
+    public String[] getArgumentNames() {
+        return paramNames.clone();
+    }
+
+    String[] getArgumentNamesInternal() {
+        return paramNames;
+    }
+
+    boolean hasArgNamed(String name) {
+        return paramDefaults.containsKey(name);
+    }
+    
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) {
+        env.visitMacroDef(this);
+        return null;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder sb = new StringBuilder();
+        if (canonical) sb.append('<');
+        sb.append(getNodeTypeSymbol());
+        sb.append(' ');
+        sb.append(_StringUtil.toFTLTopLevelTragetIdentifier(name));
+        if (function) sb.append('(');
+        int argCnt = paramNames.length;
+        for (int i = 0; i < argCnt; i++) {
+            if (function) {
+                if (i != 0) {
+                    sb.append(", ");
+                }
+            } else {
+                sb.append(' ');
+            }
+            String argName = paramNames[i];
+            sb.append(_StringUtil.toFTLTopLevelIdentifierReference(argName));
+            if (paramDefaults != null && paramDefaults.get(argName) != null) {
+                sb.append('=');
+                ASTExpression defaultExpr = (ASTExpression) paramDefaults.get(argName);
+                if (function) {
+                    sb.append(defaultExpr.getCanonicalForm());
+                } else {
+                    MessageUtil.appendExpressionAsUntearable(sb, defaultExpr);
+                }
+            }
+        }
+        if (catchAllParamName != null) {
+            if (function) {
+                if (argCnt != 0) {
+                    sb.append(", ");
+                }
+            } else {
+                sb.append(' ');
+            }
+            sb.append(catchAllParamName);
+            sb.append("...");
+        }
+        if (function) sb.append(')');
+        if (canonical) {
+            sb.append('>');
+            sb.append(getChildrenCanonicalForm());
+            sb.append("</").append(getNodeTypeSymbol()).append('>');
+        }
+        return sb.toString();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return function ? "#function" : "#macro";
+    }
+    
+    public boolean isFunction() {
+        return function;
+    }
+
+    class Context implements LocalContext {
+        final Environment.Namespace localVars; 
+        final ASTElement[] nestedContentBuffer;
+        final Environment.Namespace nestedContentNamespace;
+        final List nestedContentParameterNames;
+        final LocalContextStack prevLocalContextStack;
+        final Context prevMacroContext;
+        
+        Context(Environment env, 
+                ASTElement[] nestedContentBuffer,
+                List nestedContentParameterNames) {
+            localVars = env.new Namespace();
+            this.nestedContentBuffer = nestedContentBuffer;
+            nestedContentNamespace = env.getCurrentNamespace();
+            this.nestedContentParameterNames = nestedContentParameterNames;
+            prevLocalContextStack = env.getLocalContextStack();
+            prevMacroContext = env.getCurrentMacroContext();
+        }
+                
+        
+        ASTDirMacro getMacro() {
+            return ASTDirMacro.this;
+        }
+
+        // Set default parameters, check if all the required parameters are defined.
+        void sanityCheck(Environment env) throws TemplateException {
+            boolean resolvedAnArg, hasUnresolvedArg;
+            ASTExpression firstUnresolvedExpression;
+            InvalidReferenceException firstReferenceException;
+            do {
+                firstUnresolvedExpression = null;
+                firstReferenceException = null;
+                resolvedAnArg = hasUnresolvedArg = false;
+                for (int i = 0; i < paramNames.length; ++i) {
+                    String argName = paramNames[i];
+                    if (localVars.get(argName) == null) {
+                        ASTExpression valueExp = (ASTExpression) paramDefaults.get(argName);
+                        if (valueExp != null) {
+                            try {
+                                TemplateModel tm = valueExp.eval(env);
+                                if (tm == null) {
+                                    if (!hasUnresolvedArg) {
+                                        firstUnresolvedExpression = valueExp;
+                                        hasUnresolvedArg = true;
+                                    }
+                                } else {
+                                    localVars.put(argName, tm);
+                                    resolvedAnArg = true;
+                                }
+                            } catch (InvalidReferenceException e) {
+                                if (!hasUnresolvedArg) {
+                                    hasUnresolvedArg = true;
+                                    firstReferenceException = e;
+                                }
+                            }
+                        } else {
+                            boolean argWasSpecified = localVars.containsKey(argName);
+                            throw new _MiscTemplateException(env,
+                                    new _ErrorDescriptionBuilder(
+                                            "When calling macro ", new _DelayedJQuote(name), 
+                                            ", required parameter ", new _DelayedJQuote(argName),
+                                            " (parameter #", Integer.valueOf(i + 1), ") was ", 
+                                            (argWasSpecified
+                                                    ? "specified, but had null/missing value."
+                                                    : "not specified.") 
+                                    ).tip(argWasSpecified
+                                            ? new Object[] {
+                                                    "If the parameter value expression on the caller side is known to "
+                                                    + "be legally null/missing, you may want to specify a default "
+                                                    + "value for it with the \"!\" operator, like "
+                                                    + "paramValue!defaultValue." }
+                                            : new Object[] { 
+                                                    "If the omission was deliberate, you may consider making the "
+                                                    + "parameter optional in the macro by specifying a default value "
+                                                    + "for it, like ", "<#macro macroName paramName=defaultExpr>", ")" }
+                                            ));
+                        }
+                    }
+                }
+            } while (resolvedAnArg && hasUnresolvedArg);
+            if (hasUnresolvedArg) {
+                if (firstReferenceException != null) {
+                    throw firstReferenceException;
+                } else {
+                    throw InvalidReferenceException.getInstance(firstUnresolvedExpression, env);
+                }
+            }
+        }
+
+        /**
+         * @return the local variable of the given name
+         * or null if it doesn't exist.
+         */ 
+        @Override
+        public TemplateModel getLocalVariable(String name) throws TemplateModelException {
+             return localVars.get(name);
+        }
+
+        Environment.Namespace getLocals() {
+            return localVars;
+        }
+        
+        /**
+         * Set a local variable in this macro 
+         */
+        void setLocalVar(String name, TemplateModel var) {
+            localVars.put(name, var);
+        }
+
+        @Override
+        public Collection getLocalVariableNames() throws TemplateModelException {
+            HashSet result = new HashSet();
+            for (TemplateModelIterator it = localVars.keys().iterator(); it.hasNext(); ) {
+                result.add(it.next().toString());
+            }
+            return result;
+        }
+    }
+
+    @Override
+    int getParameterCount() {
+        return 1/*name*/ + paramNames.length * 2/*name=default*/ + 1/*catchAll*/ + 1/*type*/;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        if (idx == 0) {
+            return name;
+        } else {
+            final int argDescsEnd = paramNames.length * 2 + 1;
+            if (idx < argDescsEnd) {
+                String paramName = paramNames[(idx - 1) / 2];
+                if (idx % 2 != 0) {
+                    return paramName;
+                } else {
+                    return paramDefaults.get(paramName);
+                }
+            } else if (idx == argDescsEnd) {
+                return catchAllParamName;
+            } else if (idx == argDescsEnd + 1) {
+                return Integer.valueOf(function ? TYPE_FUNCTION : TYPE_MACRO);
+            } else {
+                throw new IndexOutOfBoundsException();
+            }
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        if (idx == 0) {
+            return ParameterRole.ASSIGNMENT_TARGET;
+        } else {
+            final int argDescsEnd = paramNames.length * 2 + 1;
+            if (idx < argDescsEnd) {
+                if (idx % 2 != 0) {
+                    return ParameterRole.PARAMETER_NAME;
+                } else {
+                    return ParameterRole.PARAMETER_DEFAULT;
+                }
+            } else if (idx == argDescsEnd) {
+                return ParameterRole.CATCH_ALL_PARAMETER_NAME;
+            } else if (idx == argDescsEnd + 1) {
+                return ParameterRole.AST_NODE_SUBTYPE;
+            } else {
+                throw new IndexOutOfBoundsException();
+            }
+        }
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        // Because of recursive calls
+        return true;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirNested.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirNested.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirNested.java
new file mode 100644
index 0000000..f08d3b2
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirNested.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 java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+
+/**
+ * AST directive node: {@code #nested}.
+ */
+final class ASTDirNested extends ASTDirective {
+    
+    
+    private List bodyParameters;
+    
+    
+    ASTDirNested(List bodyParameters) {
+        this.bodyParameters = bodyParameters;
+    }
+    
+    List getBodyParameters() {
+        return bodyParameters;
+    }
+
+    /**
+     * There is actually a subtle but essential point in the code below.
+     * A macro operates in the context in which it's defined. However, 
+     * a nested block within a macro instruction is defined in the 
+     * context in which the macro was invoked. So, we actually need to
+     * temporarily switch the namespace and macro context back to
+     * what it was before macro invocation to implement this properly.
+     * I (JR) realized this thanks to some incisive comments from Daniel Dekany.
+     */
+    @Override
+    ASTElement[] accept(Environment env) throws IOException, TemplateException {
+        Context bodyContext = new Context(env);
+        env.invokeNestedContent(bodyContext);
+        return null;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder sb = new StringBuilder();
+        if (canonical) sb.append('<');
+        sb.append(getNodeTypeSymbol());
+        if (bodyParameters != null) {
+            for (int i = 0; i < bodyParameters.size(); i++) {
+                sb.append(' ');
+                sb.append(((ASTExpression) bodyParameters.get(i)).getCanonicalForm());
+            }
+        }
+        if (canonical) sb.append('>');
+        return sb.toString();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "#nested";
+    }
+    
+    @Override
+    int getParameterCount() {
+        return bodyParameters != null ? bodyParameters.size() : 0;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        checkIndex(idx);
+        return bodyParameters.get(idx);
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        checkIndex(idx);
+        return ParameterRole.PASSED_VALUE;
+    }
+
+    private void checkIndex(int idx) {
+        if (bodyParameters == null || idx >= bodyParameters.size()) {
+            throw new IndexOutOfBoundsException();
+        }
+    }
+    
+    /*
+    boolean heedsOpeningWhitespace() {
+        return true;
+    }
+
+    boolean heedsTrailingWhitespace() {
+        return true;
+    }
+    */
+    
+    @Override
+    boolean isShownInStackTrace() {
+        return true;
+    }
+
+    class Context implements LocalContext {
+        ASTDirMacro.Context invokingMacroContext;
+        Environment.Namespace bodyVars;
+        
+        Context(Environment env) throws TemplateException {
+            invokingMacroContext = env.getCurrentMacroContext();
+            List bodyParameterNames = invokingMacroContext.nestedContentParameterNames;
+            if (bodyParameters != null) {
+                for (int i = 0; i < bodyParameters.size(); i++) {
+                    ASTExpression exp = (ASTExpression) bodyParameters.get(i);
+                    TemplateModel tm = exp.eval(env);
+                    if (bodyParameterNames != null && i < bodyParameterNames.size()) {
+                        String bodyParameterName = (String) bodyParameterNames.get(i);
+                        if (bodyVars == null) {
+                            bodyVars = env.new Namespace();
+                        }
+                        bodyVars.put(bodyParameterName, tm);
+                    }
+                }
+            }
+        }
+        
+        @Override
+        public TemplateModel getLocalVariable(String name) throws TemplateModelException {
+            return bodyVars == null ? null : bodyVars.get(name);
+        }
+        
+        @Override
+        public Collection getLocalVariableNames() {
+            List bodyParameterNames = invokingMacroContext.nestedContentParameterNames;
+            return bodyParameterNames == null ? Collections.EMPTY_LIST : bodyParameterNames;
+        }
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirNoAutoEsc.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirNoAutoEsc.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirNoAutoEsc.java
new file mode 100644
index 0000000..f1d1f43
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirNoAutoEsc.java
@@ -0,0 +1,77 @@
+/*
+ * 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;
+
+/**
+ * AST directive node: {@code #noautoesc}.
+ */
+final class ASTDirNoAutoEsc extends ASTDirective {
+    
+    ASTDirNoAutoEsc(TemplateElements children) { 
+        setChildren(children);
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) throws TemplateException, IOException {
+        return getChildBuffer();
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        if (canonical) {
+            return "<" + getNodeTypeSymbol() + "\">" + getChildrenCanonicalForm() + "</" + getNodeTypeSymbol() + ">";
+        } else {
+            return getNodeTypeSymbol();
+        }
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "#noautoesc";
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 0;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    boolean isIgnorable(boolean stripWhitespace) {
+        return getChildCount() == 0;
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirNoEscape.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirNoEscape.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirNoEscape.java
new file mode 100644
index 0000000..e2f3648
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirNoEscape.java
@@ -0,0 +1,78 @@
+/*
+ * 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;
+
+/**
+ * AST directive node: {@code #noescape}.
+ */
+class ASTDirNoEscape extends ASTDirective {
+
+    ASTDirNoEscape(TemplateElements children) {
+        setChildren(children);
+    }
+    
+    @Override
+    ASTElement[] accept(Environment env) throws TemplateException, IOException {
+        return getChildBuffer();
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        if (canonical) {
+            return "<" + getNodeTypeSymbol() + '>' + getChildrenCanonicalForm()
+                    + "</" + getNodeTypeSymbol() + '>';
+        } else {
+            return getNodeTypeSymbol();
+        }
+    }
+
+    @Override
+    int getParameterCount() {
+        return 0;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "#noescape";
+    }
+
+    @Override
+    boolean isOutputCacheable() {
+        return true;
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirOutputFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirOutputFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirOutputFormat.java
new file mode 100644
index 0000000..ee59a0c
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirOutputFormat.java
@@ -0,0 +1,85 @@
+/*
+ * 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;
+
+/**
+ * AST directive node: {@code #outputformat}.
+ */
+final class ASTDirOutputFormat extends ASTDirective {
+    
+    private final ASTExpression paramExp;
+
+    ASTDirOutputFormat(TemplateElements children, ASTExpression paramExp) { 
+        this.paramExp = paramExp; 
+        setChildren(children);
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) throws TemplateException, IOException {
+        return getChildBuffer();
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        if (canonical) {
+            return "<" + getNodeTypeSymbol() + " \"" + paramExp.getCanonicalForm() + "\">"
+                    + getChildrenCanonicalForm() + "</" + getNodeTypeSymbol() + ">";
+        } else {
+            return getNodeTypeSymbol();
+        }
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "#outputformat";
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 1;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        if (idx == 0) return paramExp;
+        else
+            throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        if (idx == 0) return ParameterRole.VALUE;
+        else
+            throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    boolean isIgnorable(boolean stripWhitespace) {
+        return getChildCount() == 0;
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirRecover.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirRecover.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirRecover.java
new file mode 100644
index 0000000..f19e9b2
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirRecover.java
@@ -0,0 +1,75 @@
+/*
+ * 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;
+
+/**
+ * AST directive node: {@code #recover}.
+ */
+final class ASTDirRecover extends ASTDirective {
+    
+    ASTDirRecover(TemplateElements children) {
+        setChildren(children);
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) throws TemplateException, IOException {
+        return getChildBuffer();
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        if (canonical) {
+            StringBuilder buf = new StringBuilder();
+            buf.append('<').append(getNodeTypeSymbol()).append('>');
+            buf.append(getChildrenCanonicalForm());            
+            return buf.toString();
+        } else {
+            return getNodeTypeSymbol();
+        }
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return "#recover";
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 0;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+}


[31/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateConfiguration.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateConfiguration.java b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateConfiguration.java
new file mode 100644
index 0000000..a8fc5ae
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateConfiguration.java
@@ -0,0 +1,991 @@
+/*
+ * 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.Reader;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+
+import org.apache.freemarker.core.arithmetic.ArithmeticEngine;
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.outputformat.OutputFormat;
+import org.apache.freemarker.core.util.CommonBuilder;
+import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory;
+
+/**
+ * A partial set of configuration settings used for customizing the {@link Configuration}-level settings for individual
+ * {@link Template}-s (or rather, for a group of templates). That it's partial means that you should call the
+ * corresponding {@code isXxxSet()} before getting a settings, or else you may cause
+ * {@link SettingValueNotSetException}. (The fallback to the {@link Configuration} setting isn't automatic to keep
+ * the dependency graph of configuration related beans non-cyclic. As user code seldom reads settings from here anyway,
+ * this compromise was chosen.)
+ * <p>
+ * Note on the {@code locale} setting: When used with the standard template loading/caching mechanism ({@link
+ * Configuration#getTemplate(String)} and its overloads), localized lookup happens before the {@code locale} specified
+ * here could have effect. The {@code locale} will be only set in the template that the localized lookup has already
+ * found.
+ * <p>
+ * This class is immutable. Use {@link TemplateConfiguration.Builder} to create a new instance.
+ *
+ * @see Template#Template(String, String, Reader, Configuration, TemplateConfiguration, Charset)
+ */
+public final class TemplateConfiguration implements ParsingAndProcessingConfiguration {
+
+    private final Locale locale;
+    private final String numberFormat;
+    private final String timeFormat;
+    private final String dateFormat;
+    private final String dateTimeFormat;
+    private final TimeZone timeZone;
+    private final TimeZone sqlDateAndTimeTimeZone;
+    private final boolean sqlDateAndTimeTimeZoneSet;
+    private final String booleanFormat;
+    private final TemplateExceptionHandler templateExceptionHandler;
+    private final ArithmeticEngine arithmeticEngine;
+    private final ObjectWrapper objectWrapper;
+    private final Charset outputEncoding;
+    private final boolean outputEncodingSet;
+    private final Charset urlEscapingCharset;
+    private final boolean urlEscapingCharsetSet;
+    private final Boolean autoFlush;
+    private final TemplateClassResolver newBuiltinClassResolver;
+    private final Boolean showErrorTips;
+    private final Boolean apiBuiltinEnabled;
+    private final Boolean logTemplateExceptions;
+    private final Map<String, TemplateDateFormatFactory> customDateFormats;
+    private final Map<String, TemplateNumberFormatFactory> customNumberFormats;
+    private final Map<String, String> autoImports;
+    private final List<String> autoIncludes;
+    private final Boolean lazyImports;
+    private final Boolean lazyAutoImports;
+    private final boolean lazyAutoImportsSet;
+    private final Map<Object, Object> customAttributes;
+    
+    private final TemplateLanguage templateLanguage;
+    private final Integer tagSyntax;
+    private final Integer namingConvention;
+    private final Boolean whitespaceStripping;
+    private final Integer autoEscapingPolicy;
+    private final Boolean recognizeStandardFileExtensions;
+    private final OutputFormat outputFormat;
+    private final Charset sourceEncoding;
+    private final Integer tabSize;
+
+    private TemplateConfiguration(Builder builder) {
+        locale = builder.isLocaleSet() ? builder.getLocale() : null;
+        numberFormat = builder.isNumberFormatSet() ? builder.getNumberFormat() : null;
+        timeFormat = builder.isTimeFormatSet() ? builder.getTimeFormat() : null;
+        dateFormat = builder.isDateFormatSet() ? builder.getDateFormat() : null;
+        dateTimeFormat = builder.isDateTimeFormatSet() ? builder.getDateTimeFormat() : null;
+        timeZone = builder.isTimeZoneSet() ? builder.getTimeZone() : null;
+        sqlDateAndTimeTimeZoneSet = builder.isSQLDateAndTimeTimeZoneSet();
+        sqlDateAndTimeTimeZone = sqlDateAndTimeTimeZoneSet ? builder.getSQLDateAndTimeTimeZone() : null;
+        booleanFormat = builder.isBooleanFormatSet() ? builder.getBooleanFormat() : null;
+        templateExceptionHandler = builder.isTemplateExceptionHandlerSet() ? builder.getTemplateExceptionHandler() : null;
+        arithmeticEngine = builder.isArithmeticEngineSet() ? builder.getArithmeticEngine() : null;
+        objectWrapper = builder.isObjectWrapperSet() ? builder.getObjectWrapper() : null;
+        outputEncodingSet = builder.isOutputEncodingSet();
+        outputEncoding = outputEncodingSet ? builder.getOutputEncoding() : null;
+        urlEscapingCharsetSet = builder.isURLEscapingCharsetSet();
+        urlEscapingCharset = urlEscapingCharsetSet ? builder.getURLEscapingCharset() : null;
+        autoFlush = builder.isAutoFlushSet() ? builder.getAutoFlush() : null;
+        newBuiltinClassResolver = builder.isNewBuiltinClassResolverSet() ? builder.getNewBuiltinClassResolver() : null;
+        showErrorTips = builder.isShowErrorTipsSet() ? builder.getShowErrorTips() : null;
+        apiBuiltinEnabled = builder.isAPIBuiltinEnabledSet() ? builder.getAPIBuiltinEnabled() : null;
+        logTemplateExceptions = builder.isLogTemplateExceptionsSet() ? builder.getLogTemplateExceptions() : null;
+        customDateFormats = builder.isCustomDateFormatsSet() ? builder.getCustomDateFormats() : null;
+        customNumberFormats = builder.isCustomNumberFormatsSet() ? builder.getCustomNumberFormats() : null;
+        autoImports = builder.isAutoImportsSet() ? builder.getAutoImports() : null;
+        autoIncludes = builder.isAutoIncludesSet() ? builder.getAutoIncludes() : null;
+        lazyImports = builder.isLazyImportsSet() ? builder.getLazyImports() : null;
+        lazyAutoImportsSet = builder.isLazyAutoImportsSet();
+        lazyAutoImports = lazyAutoImportsSet ? builder.getLazyAutoImports() : null;
+        customAttributes = builder.isCustomAttributesSet() ? builder.getCustomAttributes() : null;
+
+        templateLanguage = builder.isTemplateLanguageSet() ? builder.getTemplateLanguage() : null;
+        tagSyntax = builder.isTagSyntaxSet() ? builder.getTagSyntax() : null;
+        namingConvention = builder.isNamingConventionSet() ? builder.getNamingConvention() : null;
+        whitespaceStripping = builder.isWhitespaceStrippingSet() ? builder.getWhitespaceStripping() : null;
+        autoEscapingPolicy = builder.isAutoEscapingPolicySet() ? builder.getAutoEscapingPolicy() : null;
+        recognizeStandardFileExtensions = builder.isRecognizeStandardFileExtensionsSet() ? builder.getRecognizeStandardFileExtensions() : null;
+        outputFormat = builder.isOutputFormatSet() ? builder.getOutputFormat() : null;
+        sourceEncoding = builder.isSourceEncodingSet() ? builder.getSourceEncoding() : null;
+        tabSize = builder.isTabSizeSet() ? builder.getTabSize() : null;
+    }
+
+    private static <K,V> Map<K,V> mergeMaps(Map<K,V> m1, Map<K,V> m2, boolean overwriteUpdatesOrder) {
+        if (m1 == null) return m2;
+        if (m2 == null) return m1;
+        if (m1.isEmpty()) return m2;
+        if (m2.isEmpty()) return m1;
+
+        LinkedHashMap<K, V> mergedM = new LinkedHashMap<>((m1.size() + m2.size()) * 4 / 3 + 1, 0.75f);
+        mergedM.putAll(m1);
+        if (overwriteUpdatesOrder) {
+            for (K m2Key : m2.keySet()) {
+                mergedM.remove(m2Key); // So that duplicate keys are moved after m1 keys
+            }
+        }
+        mergedM.putAll(m2);
+        return mergedM;
+    }
+
+    private static List<String> mergeLists(List<String> list1, List<String> list2) {
+        if (list1 == null) return list2;
+        if (list2 == null) return list1;
+        if (list1.isEmpty()) return list2;
+        if (list2.isEmpty()) return list1;
+
+        ArrayList<String> mergedList = new ArrayList<>(list1.size() + list2.size());
+        mergedList.addAll(list1);
+        mergedList.addAll(list2);
+        return mergedList;
+    }
+
+    /**
+     * For internal usage only, copies the custom attributes set directly on this objects into another
+     * {@link MutableProcessingConfiguration}. The target {@link MutableProcessingConfiguration} is assumed to be not seen be other thread than the current
+     * one yet. (That is, the operation is not synchronized on the target {@link MutableProcessingConfiguration}, only on the source
+     * {@link MutableProcessingConfiguration})
+     *
+     * @since 2.3.24
+     */
+    private void copyDirectCustomAttributes(MutableProcessingConfiguration<?> target, boolean overwriteExisting) {
+        if (customAttributes == null) {
+            return;
+        }
+        for (Map.Entry<?, ?> custAttrEnt : customAttributes.entrySet()) {
+            Object custAttrKey = custAttrEnt.getKey();
+            if (overwriteExisting || !target.isCustomAttributeSet(custAttrKey)) {
+                target.setCustomAttribute(custAttrKey, custAttrEnt.getValue());
+            }
+        }
+    }
+
+    @Override
+    public int getTagSyntax() {
+        if (!isTagSyntaxSet()) {
+            throw new SettingValueNotSetException("tagSyntax");
+        }
+        return tagSyntax;
+    }
+
+    @Override
+    public boolean isTagSyntaxSet() {
+        return tagSyntax != null;
+    }
+
+    @Override
+    public TemplateLanguage getTemplateLanguage() {
+        if (!isTemplateLanguageSet()) {
+            throw new SettingValueNotSetException("templateLanguage");
+        }
+        return templateLanguage;
+    }
+
+    @Override
+    public boolean isTemplateLanguageSet() {
+        return templateLanguage != null;
+    }
+
+    @Override
+    public int getNamingConvention() {
+        if (!isNamingConventionSet()) {
+            throw new SettingValueNotSetException("namingConvention");
+        }
+        return namingConvention;
+    }
+
+    @Override
+    public boolean isNamingConventionSet() {
+        return namingConvention != null;
+    }
+
+    @Override
+    public boolean getWhitespaceStripping() {
+        if (!isWhitespaceStrippingSet()) {
+            throw new SettingValueNotSetException("whitespaceStripping");
+        }
+        return whitespaceStripping;
+    }
+
+    @Override
+    public boolean isWhitespaceStrippingSet() {
+        return whitespaceStripping != null;
+    }
+
+    @Override
+    public int getAutoEscapingPolicy() {
+        if (!isAutoEscapingPolicySet()) {
+            throw new SettingValueNotSetException("autoEscapingPolicy");
+        }
+        return autoEscapingPolicy;
+    }
+
+    @Override
+    public boolean isAutoEscapingPolicySet() {
+        return autoEscapingPolicy != null;
+    }
+
+    @Override
+    public OutputFormat getOutputFormat() {
+        if (!isOutputFormatSet()) {
+            throw new SettingValueNotSetException("outputFormat");
+        }
+        return outputFormat;
+    }
+
+    @Override
+    public ArithmeticEngine getArithmeticEngine() {
+        if (!isArithmeticEngineSet()) {
+            throw new SettingValueNotSetException("arithmeticEngine");
+        }
+        return arithmeticEngine;
+    }
+
+    @Override
+    public boolean isArithmeticEngineSet() {
+        return arithmeticEngine != null;
+    }
+
+    @Override
+    public boolean isOutputFormatSet() {
+        return outputFormat != null;
+    }
+    
+    @Override
+    public boolean getRecognizeStandardFileExtensions() {
+        if (!isRecognizeStandardFileExtensionsSet()) {
+            throw new SettingValueNotSetException("recognizeStandardFileExtensions");
+        }
+        return recognizeStandardFileExtensions;
+    }
+    
+    @Override
+    public boolean isRecognizeStandardFileExtensionsSet() {
+        return recognizeStandardFileExtensions != null;
+    }
+
+    @Override
+    public Charset getSourceEncoding() {
+        if (!isSourceEncodingSet()) {
+            throw new SettingValueNotSetException("sourceEncoding");
+        }
+        return sourceEncoding;
+    }
+
+    @Override
+    public boolean isSourceEncodingSet() {
+        return sourceEncoding != null;
+    }
+    
+    @Override
+    public int getTabSize() {
+        if (!isTabSizeSet()) {
+            throw new SettingValueNotSetException("tabSize");
+        }
+        return tabSize;
+    }
+    
+    @Override
+    public boolean isTabSizeSet() {
+        return tabSize != null;
+    }
+    
+    /**
+     * Always throws {@link SettingValueNotSetException}, as this can't be set on the {@link TemplateConfiguration}
+     * level.
+     */
+    @Override
+    public Version getIncompatibleImprovements() {
+        throw new SettingValueNotSetException("incompatibleImprovements");
+    }
+
+    @Override
+    public Locale getLocale() {
+        if (!isLocaleSet()) {
+            throw new SettingValueNotSetException("locale");
+        }
+        return locale;
+    }
+
+    @Override
+    public boolean isLocaleSet() {
+        return locale != null;
+    }
+
+    @Override
+    public TimeZone getTimeZone() {
+        if (!isTimeZoneSet()) {
+            throw new SettingValueNotSetException("timeZone");
+        }
+        return timeZone;
+    }
+
+    @Override
+    public boolean isTimeZoneSet() {
+        return timeZone != null;
+    }
+
+    @Override
+    public TimeZone getSQLDateAndTimeTimeZone() {
+        if (!isSQLDateAndTimeTimeZoneSet()) {
+            throw new SettingValueNotSetException("sqlDateAndTimeTimeZone");
+        }
+        return sqlDateAndTimeTimeZone;
+    }
+
+    @Override
+    public boolean isSQLDateAndTimeTimeZoneSet() {
+        return sqlDateAndTimeTimeZoneSet;
+    }
+
+    @Override
+    public String getNumberFormat() {
+        if (!isNumberFormatSet()) {
+            throw new SettingValueNotSetException("numberFormat");
+        }
+        return numberFormat;
+    }
+
+    @Override
+    public boolean isNumberFormatSet() {
+        return numberFormat != null;
+    }
+
+    @Override
+    public Map<String, TemplateNumberFormatFactory> getCustomNumberFormats() {
+        if (!isCustomNumberFormatsSet()) {
+            throw new SettingValueNotSetException("customNumberFormats");
+        }
+        return customNumberFormats;
+    }
+
+    @Override
+    public TemplateNumberFormatFactory getCustomNumberFormat(String name) {
+        return getCustomNumberFormats().get(name);
+    }
+
+    @Override
+    public boolean isCustomNumberFormatsSet() {
+        return customNumberFormats != null;
+    }
+
+    @Override
+    public String getBooleanFormat() {
+        if (!isBooleanFormatSet()) {
+            throw new SettingValueNotSetException("booleanFormat");
+        }
+        return booleanFormat;
+    }
+
+    @Override
+    public boolean isBooleanFormatSet() {
+        return booleanFormat != null;
+    }
+
+    @Override
+    public String getTimeFormat() {
+        if (!isTimeFormatSet()) {
+            throw new SettingValueNotSetException("timeFormat");
+        }
+        return timeFormat;
+    }
+
+    @Override
+    public boolean isTimeFormatSet() {
+        return timeFormat != null;
+    }
+
+    @Override
+    public String getDateFormat() {
+        if (!isDateFormatSet()) {
+            throw new SettingValueNotSetException("dateFormat");
+        }
+        return dateFormat;
+    }
+
+    @Override
+    public boolean isDateFormatSet() {
+        return dateFormat != null;
+    }
+
+    @Override
+    public String getDateTimeFormat() {
+        if (!isDateTimeFormatSet()) {
+            throw new SettingValueNotSetException("dateTimeFormat");
+        }
+        return dateTimeFormat;
+    }
+
+    @Override
+    public boolean isDateTimeFormatSet() {
+        return dateTimeFormat != null;
+    }
+
+    @Override
+    public Map<String, TemplateDateFormatFactory> getCustomDateFormats() {
+        if (!isCustomDateFormatsSet()) {
+            throw new SettingValueNotSetException("customDateFormats");
+        }
+        return customDateFormats;
+    }
+
+    @Override
+    public TemplateDateFormatFactory getCustomDateFormat(String name) {
+        if (isCustomDateFormatsSet()) {
+            TemplateDateFormatFactory format = customDateFormats.get(name);
+            if (format != null) {
+                return  format;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public boolean isCustomDateFormatsSet() {
+        return customDateFormats != null;
+    }
+
+    @Override
+    public TemplateExceptionHandler getTemplateExceptionHandler() {
+        if (!isTemplateExceptionHandlerSet()) {
+            throw new SettingValueNotSetException("templateExceptionHandler");
+        }
+        return templateExceptionHandler;
+    }
+
+    @Override
+    public boolean isTemplateExceptionHandlerSet() {
+        return templateExceptionHandler != null;
+    }
+
+    @Override
+    public ObjectWrapper getObjectWrapper() {
+        if (!isObjectWrapperSet()) {
+            throw new SettingValueNotSetException("objectWrapper");
+        }
+        return objectWrapper;
+    }
+
+    @Override
+    public boolean isObjectWrapperSet() {
+        return objectWrapper != null;
+    }
+
+    @Override
+    public Charset getOutputEncoding() {
+        if (!isOutputEncodingSet()) {
+            throw new SettingValueNotSetException("");
+        }
+        return outputEncoding;
+    }
+
+    @Override
+    public boolean isOutputEncodingSet() {
+        return outputEncodingSet;
+    }
+
+    @Override
+    public Charset getURLEscapingCharset() {
+        if (!isURLEscapingCharsetSet()) {
+            throw new SettingValueNotSetException("urlEscapingCharset");
+        }
+        return urlEscapingCharset;
+    }
+
+    @Override
+    public boolean isURLEscapingCharsetSet() {
+        return urlEscapingCharsetSet;
+    }
+
+    @Override
+    public TemplateClassResolver getNewBuiltinClassResolver() {
+        if (!isNewBuiltinClassResolverSet()) {
+            throw new SettingValueNotSetException("newBuiltinClassResolver");
+        }
+        return newBuiltinClassResolver;
+    }
+
+    @Override
+    public boolean isNewBuiltinClassResolverSet() {
+        return newBuiltinClassResolver != null;
+    }
+
+    @Override
+    public boolean getAPIBuiltinEnabled() {
+        if (!isAPIBuiltinEnabledSet()) {
+            throw new SettingValueNotSetException("apiBuiltinEnabled");
+        }
+        return apiBuiltinEnabled;
+    }
+
+    @Override
+    public boolean isAPIBuiltinEnabledSet() {
+        return apiBuiltinEnabled != null;
+    }
+
+    @Override
+    public boolean getAutoFlush() {
+        if (!isAutoFlushSet()) {
+            throw new SettingValueNotSetException("autoFlush");
+        }
+        return autoFlush;
+    }
+
+    @Override
+    public boolean isAutoFlushSet() {
+        return autoFlush != null;
+    }
+
+    @Override
+    public boolean getShowErrorTips() {
+        if (!isShowErrorTipsSet()) {
+            throw new SettingValueNotSetException("showErrorTips");
+        }
+        return showErrorTips;
+    }
+
+    @Override
+    public boolean isShowErrorTipsSet() {
+        return showErrorTips != null;
+    }
+
+    @Override
+    public boolean getLogTemplateExceptions() {
+        if (!isLogTemplateExceptionsSet()) {
+            throw new SettingValueNotSetException("logTemplateExceptions");
+        }
+        return logTemplateExceptions;
+    }
+
+    @Override
+    public boolean isLogTemplateExceptionsSet() {
+        return logTemplateExceptions != null;
+    }
+
+    @Override
+    public boolean getLazyImports() {
+        if (!isLazyImportsSet()) {
+            throw new SettingValueNotSetException("lazyImports");
+        }
+        return lazyImports;
+    }
+
+    @Override
+    public boolean isLazyImportsSet() {
+        return lazyImports != null;
+    }
+
+    @Override
+    public Boolean getLazyAutoImports() {
+        if (!isLazyAutoImportsSet()) {
+            throw new SettingValueNotSetException("lazyAutoImports");
+        }
+        return lazyAutoImports;
+    }
+
+    @Override
+    public boolean isLazyAutoImportsSet() {
+        return lazyAutoImportsSet;
+    }
+
+    @Override
+    public Map<String, String> getAutoImports() {
+        if (!isAutoImportsSet()) {
+            throw new SettingValueNotSetException("");
+        }
+        return autoImports;
+    }
+
+    @Override
+    public boolean isAutoImportsSet() {
+        return autoImports != null;
+    }
+
+    @Override
+    public List<String> getAutoIncludes() {
+        if (!isAutoIncludesSet()) {
+            throw new SettingValueNotSetException("autoIncludes");
+        }
+        return autoIncludes;
+    }
+
+    @Override
+    public boolean isAutoIncludesSet() {
+        return autoIncludes != null;
+    }
+
+    @Override
+    public Map<Object, Object> getCustomAttributes() {
+        if (!isCustomAttributesSet()) {
+            throw new SettingValueNotSetException("customAttributes");
+        }
+        return customAttributes;
+    }
+
+    @Override
+    public boolean isCustomAttributesSet() {
+        return customAttributes != null;
+    }
+
+    @Override
+    public Object getCustomAttribute(Object name) {
+        Object attValue;
+        if (isCustomAttributesSet()) {
+            attValue = customAttributes.get(name);
+            if (attValue != null || customAttributes.containsKey(name)) {
+                return attValue;
+            }
+        }
+        return null;
+    }
+
+    public static final class Builder extends MutableParsingAndProcessingConfiguration<Builder>
+            implements CommonBuilder<TemplateConfiguration> {
+
+        public Builder() {
+            super();
+        }
+
+        @Override
+        public TemplateConfiguration build() {
+            return new TemplateConfiguration(this);
+        }
+
+        @Override
+        protected Locale getDefaultLocale() {
+            throw new SettingValueNotSetException("locale");
+        }
+
+        @Override
+        protected TimeZone getDefaultTimeZone() {
+            throw new SettingValueNotSetException("timeZone");
+        }
+
+        @Override
+        protected TimeZone getDefaultSQLDateAndTimeTimeZone() {
+            throw new SettingValueNotSetException("SQLDateAndTimeTimeZone");
+        }
+
+        @Override
+        protected String getDefaultNumberFormat() {
+            throw new SettingValueNotSetException("numberFormat");
+        }
+
+        @Override
+        protected Map<String, TemplateNumberFormatFactory> getDefaultCustomNumberFormats() {
+            throw new SettingValueNotSetException("customNumberFormats");
+        }
+
+        @Override
+        protected TemplateNumberFormatFactory getDefaultCustomNumberFormat(String name) {
+            return null;
+        }
+
+        @Override
+        protected String getDefaultBooleanFormat() {
+            throw new SettingValueNotSetException("booleanFormat");
+        }
+
+        @Override
+        protected String getDefaultTimeFormat() {
+            throw new SettingValueNotSetException("timeFormat");
+        }
+
+        @Override
+        protected String getDefaultDateFormat() {
+            throw new SettingValueNotSetException("dateFormat");
+        }
+
+        @Override
+        protected String getDefaultDateTimeFormat() {
+            throw new SettingValueNotSetException("dateTimeFormat");
+        }
+
+        @Override
+        protected Map<String, TemplateDateFormatFactory> getDefaultCustomDateFormats() {
+            throw new SettingValueNotSetException("customDateFormats");
+        }
+
+        @Override
+        protected TemplateDateFormatFactory getDefaultCustomDateFormat(String name) {
+            throw new SettingValueNotSetException("customDateFormat");
+        }
+
+        @Override
+        protected TemplateExceptionHandler getDefaultTemplateExceptionHandler() {
+            throw new SettingValueNotSetException("templateExceptionHandler");
+        }
+
+        @Override
+        protected ArithmeticEngine getDefaultArithmeticEngine() {
+            throw new SettingValueNotSetException("arithmeticEngine");
+        }
+
+        @Override
+        protected ObjectWrapper getDefaultObjectWrapper() {
+            throw new SettingValueNotSetException("objectWrapper");
+        }
+
+        @Override
+        protected Charset getDefaultOutputEncoding() {
+            throw new SettingValueNotSetException("outputEncoding");
+        }
+
+        @Override
+        protected Charset getDefaultURLEscapingCharset() {
+            throw new SettingValueNotSetException("URLEscapingCharset");
+        }
+
+        @Override
+        protected TemplateClassResolver getDefaultNewBuiltinClassResolver() {
+            throw new SettingValueNotSetException("newBuiltinClassResolver");
+        }
+
+        @Override
+        protected boolean getDefaultAutoFlush() {
+            throw new SettingValueNotSetException("autoFlush");
+        }
+
+        @Override
+        protected boolean getDefaultShowErrorTips() {
+            throw new SettingValueNotSetException("showErrorTips");
+        }
+
+        @Override
+        protected boolean getDefaultAPIBuiltinEnabled() {
+            throw new SettingValueNotSetException("APIBuiltinEnabled");
+        }
+
+        @Override
+        protected boolean getDefaultLogTemplateExceptions() {
+            throw new SettingValueNotSetException("logTemplateExceptions");
+        }
+
+        @Override
+        protected boolean getDefaultLazyImports() {
+            throw new SettingValueNotSetException("lazyImports");
+        }
+
+        @Override
+        protected Boolean getDefaultLazyAutoImports() {
+            throw new SettingValueNotSetException("lazyAutoImports");
+        }
+
+        @Override
+        protected Map<String, String> getDefaultAutoImports() {
+            throw new SettingValueNotSetException("autoImports");
+        }
+
+        @Override
+        protected List<String> getDefaultAutoIncludes() {
+            throw new SettingValueNotSetException("autoIncludes");
+        }
+
+        @Override
+        protected Object getDefaultCustomAttribute(Object name) {
+            return null;
+        }
+
+        @Override
+        protected Map<Object, Object> getDefaultCustomAttributes() {
+            throw new SettingValueNotSetException("customAttributes");
+        }
+
+        /**
+         * Set all settings in this {@link Builder} that were set in the parameter
+         * {@link TemplateConfiguration}, possibly overwriting the earlier value in this object. (A setting is said to be
+         * set in a {@link TemplateConfiguration} if it was explicitly set via a setter method, as opposed to be inherited.)
+         */
+        public void merge(ParsingAndProcessingConfiguration tc) {
+            if (tc.isAPIBuiltinEnabledSet()) {
+                setAPIBuiltinEnabled(tc.getAPIBuiltinEnabled());
+            }
+            if (tc.isArithmeticEngineSet()) {
+                setArithmeticEngine(tc.getArithmeticEngine());
+            }
+            if (tc.isAutoEscapingPolicySet()) {
+                setAutoEscapingPolicy(tc.getAutoEscapingPolicy());
+            }
+            if (tc.isAutoFlushSet()) {
+                setAutoFlush(tc.getAutoFlush());
+            }
+            if (tc.isBooleanFormatSet()) {
+                setBooleanFormat(tc.getBooleanFormat());
+            }
+            if (tc.isCustomDateFormatsSet()) {
+                setCustomDateFormats(mergeMaps(
+                        isCustomDateFormatsSet() ? getCustomDateFormats() : null, tc.getCustomDateFormats(), false));
+            }
+            if (tc.isCustomNumberFormatsSet()) {
+                setCustomNumberFormats(mergeMaps(
+                        isCustomNumberFormatsSet() ? getCustomNumberFormats() : null, tc.getCustomNumberFormats(), false));
+            }
+            if (tc.isDateFormatSet()) {
+                setDateFormat(tc.getDateFormat());
+            }
+            if (tc.isDateTimeFormatSet()) {
+                setDateTimeFormat(tc.getDateTimeFormat());
+            }
+            if (tc.isSourceEncodingSet()) {
+                setSourceEncoding(tc.getSourceEncoding());
+            }
+            if (tc.isLocaleSet()) {
+                setLocale(tc.getLocale());
+            }
+            if (tc.isLogTemplateExceptionsSet()) {
+                setLogTemplateExceptions(tc.getLogTemplateExceptions());
+            }
+            if (tc.isNamingConventionSet()) {
+                setNamingConvention(tc.getNamingConvention());
+            }
+            if (tc.isNewBuiltinClassResolverSet()) {
+                setNewBuiltinClassResolver(tc.getNewBuiltinClassResolver());
+            }
+            if (tc.isNumberFormatSet()) {
+                setNumberFormat(tc.getNumberFormat());
+            }
+            if (tc.isObjectWrapperSet()) {
+                setObjectWrapper(tc.getObjectWrapper());
+            }
+            if (tc.isOutputEncodingSet()) {
+                setOutputEncoding(tc.getOutputEncoding());
+            }
+            if (tc.isOutputFormatSet()) {
+                setOutputFormat(tc.getOutputFormat());
+            }
+            if (tc.isRecognizeStandardFileExtensionsSet()) {
+                setRecognizeStandardFileExtensions(tc.getRecognizeStandardFileExtensions());
+            }
+            if (tc.isShowErrorTipsSet()) {
+                setShowErrorTips(tc.getShowErrorTips());
+            }
+            if (tc.isSQLDateAndTimeTimeZoneSet()) {
+                setSQLDateAndTimeTimeZone(tc.getSQLDateAndTimeTimeZone());
+            }
+            if (tc.isTagSyntaxSet()) {
+                setTagSyntax(tc.getTagSyntax());
+            }
+            if (tc.isTemplateLanguageSet()) {
+                setTemplateLanguage(tc.getTemplateLanguage());
+            }
+            if (tc.isTemplateExceptionHandlerSet()) {
+                setTemplateExceptionHandler(tc.getTemplateExceptionHandler());
+            }
+            if (tc.isTimeFormatSet()) {
+                setTimeFormat(tc.getTimeFormat());
+            }
+            if (tc.isTimeZoneSet()) {
+                setTimeZone(tc.getTimeZone());
+            }
+            if (tc.isURLEscapingCharsetSet()) {
+                setURLEscapingCharset(tc.getURLEscapingCharset());
+            }
+            if (tc.isWhitespaceStrippingSet()) {
+                setWhitespaceStripping(tc.getWhitespaceStripping());
+            }
+            if (tc.isTabSizeSet()) {
+                setTabSize(tc.getTabSize());
+            }
+            if (tc.isLazyImportsSet()) {
+                setLazyImports(tc.getLazyImports());
+            }
+            if (tc.isLazyAutoImportsSet()) {
+                setLazyAutoImports(tc.getLazyAutoImports());
+            }
+            if (tc.isAutoImportsSet()) {
+                setAutoImports(mergeMaps(
+                        isAutoImportsSet() ? getAutoImports() : null,
+                        tc.isAutoImportsSet() ? tc.getAutoImports() : null,
+                        true));
+            }
+            if (tc.isAutoIncludesSet()) {
+                setAutoIncludes(mergeLists(
+                        isAutoIncludesSet() ? getAutoIncludes() : null,
+                        tc.isAutoIncludesSet() ? tc.getAutoIncludes() : null));
+            }
+
+            if (tc.isCustomAttributesSet()) {
+                setCustomAttributesWithoutCopying(mergeMaps(
+                        isCustomAttributesSet() ? getCustomAttributes() : null,
+                        tc.isCustomAttributesSet() ? tc.getCustomAttributes() : null,
+                        true));
+            }
+        }
+
+        @Override
+        public Version getIncompatibleImprovements() {
+            throw new SettingValueNotSetException("incompatibleImprovements");
+        }
+
+        @Override
+        protected int getDefaultTagSyntax() {
+            throw new SettingValueNotSetException("tagSyntax");
+        }
+
+        @Override
+        protected TemplateLanguage getDefaultTemplateLanguage() {
+            throw new SettingValueNotSetException("templateLanguage");
+        }
+
+        @Override
+        protected int getDefaultNamingConvention() {
+            throw new SettingValueNotSetException("namingConvention");
+        }
+
+        @Override
+        protected boolean getDefaultWhitespaceStripping() {
+            throw new SettingValueNotSetException("whitespaceStripping");
+        }
+
+        @Override
+        protected int getDefaultAutoEscapingPolicy() {
+            throw new SettingValueNotSetException("autoEscapingPolicy");
+        }
+
+        @Override
+        protected OutputFormat getDefaultOutputFormat() {
+            throw new SettingValueNotSetException("outputFormat");
+        }
+
+        @Override
+        protected boolean getDefaultRecognizeStandardFileExtensions() {
+            throw new SettingValueNotSetException("recognizeStandardFileExtensions");
+        }
+
+        @Override
+        protected Charset getDefaultSourceEncoding() {
+            throw new SettingValueNotSetException("sourceEncoding");
+        }
+
+        @Override
+        protected int getDefaultTabSize() {
+            throw new SettingValueNotSetException("tabSize");
+        }
+
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateElementArrayBuilder.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateElementArrayBuilder.java b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateElementArrayBuilder.java
new file mode 100644
index 0000000..f8fe66b
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateElementArrayBuilder.java
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.util._CollectionUtil;
+
+/**
+ * Holds an buffer (array) of {@link ASTElement}-s with the count of the utilized items in it. The un-utilized tail
+ * of the array must only contain {@code null}-s.
+ * 
+ * @since 2.3.24
+ */
+class TemplateElements {
+    
+    static final TemplateElements EMPTY = new TemplateElements(null, 0);
+
+    private final ASTElement[] buffer;
+    private final int count;
+
+    /**
+     * @param buffer
+     *            The buffer; {@code null} exactly if {@code count} is 0.
+     * @param count
+     *            The number of utilized buffer elements; if 0, then {@code null} must be {@code null}.
+     */
+    TemplateElements(ASTElement[] buffer, int count) {
+        /*
+        // Assertion:
+        if (count == 0 && buffer != null) {
+            throw new IllegalArgumentException(); 
+        }
+        */
+        
+        this.buffer = buffer;
+        this.count = count;
+    }
+
+    ASTElement[] getBuffer() {
+        return buffer;
+    }
+
+    int getCount() {
+        return count;
+    }
+
+    ASTElement getFirst() {
+        return buffer != null ? buffer[0] : null;
+    }
+    
+    ASTElement getLast() {
+        return buffer != null ? buffer[count - 1] : null;
+    }
+    
+    /**
+     * Used for some backward compatibility hacks.
+     */
+    ASTElement asSingleElement() {
+        if (count == 0) {
+            return new ASTStaticText(_CollectionUtil.EMPTY_CHAR_ARRAY, false);
+        } else {
+            ASTElement first = buffer[0];
+            if (count == 1) {
+                return first;
+            } else {
+                ASTImplicitParent mixedContent = new ASTImplicitParent();
+                mixedContent.setChildren(this);
+                mixedContent.setLocation(first.getTemplate(), first, getLast());
+                return mixedContent;
+            }
+        }
+    }
+    
+    /**
+     * Used for some backward compatibility hacks.
+     */
+    ASTImplicitParent asMixedContent() {
+        ASTImplicitParent mixedContent = new ASTImplicitParent();
+        if (count != 0) {
+            ASTElement first = buffer[0];
+            mixedContent.setChildren(this);
+            mixedContent.setLocation(first.getTemplate(), first, getLast());
+        }
+        return mixedContent;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateElementsToVisit.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateElementsToVisit.java b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateElementsToVisit.java
new file mode 100644
index 0000000..9aaf0c7
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateElementsToVisit.java
@@ -0,0 +1,48 @@
+/*
+ * 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.util.Collection;
+import java.util.Collections;
+
+/**
+ * Used as the return value of {@link ASTElement#accept(Environment)} when the invoked element has nested elements
+ * to invoke. It would be more natural to invoke child elements before returning from
+ * {@link ASTElement#accept(Environment)}, however, if there's nothing to do after the child elements were invoked,
+ * that would mean wasting stack space.
+ * 
+ * @since 2.3.24
+ */
+class TemplateElementsToVisit {
+
+    private final Collection<ASTElement> templateElements;
+
+    TemplateElementsToVisit(Collection<ASTElement> templateElements) {
+        this.templateElements = null != templateElements ? templateElements : Collections.<ASTElement> emptyList();
+    }
+
+    TemplateElementsToVisit(ASTElement nestedBlock) {
+        this(Collections.singleton(nestedBlock));
+    }
+
+    Collection<ASTElement> getTemplateElements() {
+        return templateElements;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateException.java
new file mode 100644
index 0000000..3ca9914
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateException.java
@@ -0,0 +1,655 @@
+/*
+ * 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.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.reflect.Method;
+
+import org.apache.freemarker.core.util._CollectionUtil;
+
+/**
+ * Runtime exception in a template (as opposed to a parsing-time exception: {@link ParseException}).
+ * It prints a special stack trace that contains the template-language stack trace along the usual Java stack trace.
+ */
+public class TemplateException extends Exception {
+
+    private static final String FTL_INSTRUCTION_STACK_TRACE_TITLE
+            = "FTL stack trace (\"~\" means nesting-related):";
+
+    // Set in constructor:
+    private transient _ErrorDescriptionBuilder descriptionBuilder;
+    private final transient Environment env;
+    private final transient ASTExpression blamedExpression;
+    private transient ASTElement[] ftlInstructionStackSnapshot;
+    
+    // Calculated on demand:
+    private String renderedFtlInstructionStackSnapshot;  // clalc. from ftlInstructionStackSnapshot 
+    private String renderedFtlInstructionStackSnapshotTop; // clalc. from ftlInstructionStackSnapshot
+    private String description;  // calc. from descriptionBuilder, or set by the construcor
+    private transient String messageWithoutStackTop;
+    private transient String message;
+    private boolean blamedExpressionStringCalculated;
+    private String blamedExpressionString;
+    private boolean positionsCalculated;
+    private String templateLookupName;
+    private String templateSourceName;
+    private Integer lineNumber; 
+    private Integer columnNumber; 
+    private Integer endLineNumber; 
+    private Integer endColumnNumber; 
+
+    // Concurrency:
+    private transient Object lock = new Object();
+    private transient ThreadLocal messageWasAlreadyPrintedForThisTrace;
+    
+    /**
+     * Constructs a TemplateException with no specified detail message
+     * or underlying cause.
+     */
+    public TemplateException(Environment env) {
+        this(null, null, env);
+    }
+
+    /**
+     * Constructs a TemplateException with the given detail message,
+     * but no underlying cause exception.
+     *
+     * @param description the description of the error that occurred
+     */
+    public TemplateException(String description, Environment env) {
+        this(description, null, env);
+    }
+
+    /**
+     * The same as {@link #TemplateException(Throwable, Environment)}; it's exists only for binary
+     * backward-compatibility.
+     */
+    public TemplateException(Exception cause, Environment env) {
+        this(null, cause, env);
+    }
+
+    /**
+     * Constructs a TemplateException with the given underlying Exception,
+     * but no detail message.
+     *
+     * @param cause the underlying {@link Exception} that caused this
+     * exception to be raised
+     * 
+     * @since 2.3.20
+     */
+    public TemplateException(Throwable cause, Environment env) {
+        this(null, cause, env);
+    }
+    
+    /**
+     * The same as {@link #TemplateException(String, Throwable, Environment)}; it's exists only for binary
+     * backward-compatibility.
+     */
+    public TemplateException(String description, Exception cause, Environment env) {
+        this(description, cause, env, null, null);
+    }
+
+    /**
+     * Constructs a TemplateException with both a description of the error
+     * that occurred and the underlying Exception that caused this exception
+     * to be raised.
+     *
+     * @param description the description of the error that occurred
+     * @param cause the underlying {@link Exception} that caused this exception to be raised
+     * 
+     * @since 2.3.20
+     */
+    public TemplateException(String description, Throwable cause, Environment env) {
+        this(description, cause, env, null, null);
+    }
+    
+    /**
+     * Don't use this; this is to be used internally by FreeMarker. No backward compatibility guarantees.
+     * 
+     * @param blamedExpr Maybe {@code null}. The FTL stack in the {@link Environment} only specifies the error location
+     *          with "template element" granularity, and this can be used to point to the expression inside the
+     *          template element.    
+     */
+    protected TemplateException(Throwable cause, Environment env, ASTExpression blamedExpr,
+            _ErrorDescriptionBuilder descriptionBuilder) {
+        this(null, cause, env, blamedExpr, descriptionBuilder);
+    }
+    
+    private TemplateException(
+            String renderedDescription,
+            Throwable cause,            
+            Environment env, ASTExpression blamedExpression,
+            _ErrorDescriptionBuilder descriptionBuilder) {
+        // Note: Keep this constructor lightweight.
+        
+        super(cause);  // Message managed locally.
+        
+        if (env == null) env = Environment.getCurrentEnvironment();
+        this.env = env;
+        
+        this.blamedExpression = blamedExpression;
+        
+        this.descriptionBuilder = descriptionBuilder;
+        description = renderedDescription;
+        
+        if (env != null) {
+            ftlInstructionStackSnapshot = env.getInstructionStackSnapshot();
+        }
+    }
+    
+    private void renderMessages() {
+        String description = getDescription();
+        
+        if (description != null && description.length() != 0) {
+            messageWithoutStackTop = description;
+        } else if (getCause() != null) {
+            messageWithoutStackTop = "No error description was specified for this error; low-level message: "
+                    + getCause().getClass().getName() + ": " + getCause().getMessage();
+        } else {
+            messageWithoutStackTop = "[No error description was available.]";
+        }
+        
+        String stackTopFew = getFTLInstructionStackTopFew();
+        if (stackTopFew != null) {
+            message = messageWithoutStackTop + "\n\n"
+                    + MessageUtil.ERROR_MESSAGE_HR + "\n"
+                    + FTL_INSTRUCTION_STACK_TRACE_TITLE + "\n"
+                    + stackTopFew
+                    + MessageUtil.ERROR_MESSAGE_HR;
+            messageWithoutStackTop = message.substring(0, messageWithoutStackTop.length());  // to reuse backing char[]
+        } else {
+            message = messageWithoutStackTop;
+        }
+    }
+    
+    private void calculatePosition() {
+        synchronized (lock) {
+            if (!positionsCalculated) {
+                // The expressions is the argument of the template element, so we prefer it as it's more specific. 
+                ASTNode templateObject = blamedExpression != null
+                        ? blamedExpression
+                        : (
+                                ftlInstructionStackSnapshot != null && ftlInstructionStackSnapshot.length != 0
+                                ? ftlInstructionStackSnapshot[0] : null);
+                // Line number blow 0 means no info, negative means position in ?eval-ed value that we won't use here.
+                if (templateObject != null && templateObject.getBeginLine() > 0) {
+                    final Template template = templateObject.getTemplate();
+                    templateLookupName = template.getLookupName();
+                    templateSourceName = template.getSourceName();
+                    lineNumber = Integer.valueOf(templateObject.getBeginLine());
+                    columnNumber = Integer.valueOf(templateObject.getBeginColumn());
+                    endLineNumber = Integer.valueOf(templateObject.getEndLine());
+                    endColumnNumber = Integer.valueOf(templateObject.getEndColumn());
+                }
+                positionsCalculated = true;
+                deleteFTLInstructionStackSnapshotIfNotNeeded();
+            }
+        }
+    }
+
+    /**
+     * Returns the snapshot of the FTL stack trace at the time this exception was created.
+     */
+    public String getFTLInstructionStack() {
+        synchronized (lock) {
+            if (ftlInstructionStackSnapshot != null || renderedFtlInstructionStackSnapshot != null) {
+                if (renderedFtlInstructionStackSnapshot == null) {
+                    StringWriter sw = new StringWriter();
+                    PrintWriter pw = new PrintWriter(sw);
+                    Environment.outputInstructionStack(ftlInstructionStackSnapshot, false, pw);
+                    pw.close();
+                    if (renderedFtlInstructionStackSnapshot == null) {
+                        renderedFtlInstructionStackSnapshot = sw.toString();
+                        deleteFTLInstructionStackSnapshotIfNotNeeded();
+                    }
+                }
+                return renderedFtlInstructionStackSnapshot;
+            } else {
+                return null;
+            }
+        }
+    }
+    
+    private String getFTLInstructionStackTopFew() {
+        synchronized (lock) {
+            if (ftlInstructionStackSnapshot != null || renderedFtlInstructionStackSnapshotTop != null) {
+                if (renderedFtlInstructionStackSnapshotTop == null) {
+                    int stackSize = ftlInstructionStackSnapshot.length;
+                    String s;
+                    if (stackSize == 0) {
+                        s = "";
+                    } else {
+                        StringWriter sw = new StringWriter();
+                        Environment.outputInstructionStack(ftlInstructionStackSnapshot, true, sw);
+                        s = sw.toString();
+                    }
+                    if (renderedFtlInstructionStackSnapshotTop == null) {
+                        renderedFtlInstructionStackSnapshotTop = s;
+                        deleteFTLInstructionStackSnapshotIfNotNeeded();
+                    }
+                }
+                return renderedFtlInstructionStackSnapshotTop.length() != 0
+                        ? renderedFtlInstructionStackSnapshotTop : null;
+            } else {
+                return null;
+            }
+        }
+    }
+    
+    private void deleteFTLInstructionStackSnapshotIfNotNeeded() {
+        if (renderedFtlInstructionStackSnapshot != null && renderedFtlInstructionStackSnapshotTop != null
+                && (positionsCalculated || blamedExpression != null)) {
+            ftlInstructionStackSnapshot = null;
+        }
+        
+    }
+    
+    private String getDescription() {
+        synchronized (lock) {
+            if (description == null && descriptionBuilder != null) {
+                description = descriptionBuilder.toString(
+                        getFailingInstruction(),
+                        env != null ? env.getShowErrorTips() : true);
+                descriptionBuilder = null;
+            }
+            return description;
+        }
+    }
+
+    private ASTElement getFailingInstruction() {
+        if (ftlInstructionStackSnapshot != null && ftlInstructionStackSnapshot.length > 0) {
+            return ftlInstructionStackSnapshot[0];
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * @return the execution environment in which the exception occurred.
+     *    {@code null} if the exception was deserialized. 
+     */
+    public Environment getEnvironment() {
+        return env;
+    }
+
+    /**
+     * Overrides {@link Throwable#printStackTrace(PrintStream)} so that it will include the FTL stack trace.
+     */
+    @Override
+    public void printStackTrace(PrintStream out) {
+        printStackTrace(out, true, true, true);
+    }
+
+    /**
+     * Overrides {@link Throwable#printStackTrace(PrintWriter)} so that it will include the FTL stack trace.
+     */
+    @Override
+    public void printStackTrace(PrintWriter out) {
+        printStackTrace(out, true, true, true);
+    }
+    
+    /**
+     * @param heading should the heading at the top be printed 
+     * @param ftlStackTrace should the FTL stack trace be printed
+     * @param javaStackTrace should the Java stack trace be printed
+     *  
+     * @since 2.3.20
+     */
+    public void printStackTrace(PrintWriter out, boolean heading, boolean ftlStackTrace, boolean javaStackTrace) {
+        synchronized (out) {
+            printStackTrace(new PrintWriterStackTraceWriter(out), heading, ftlStackTrace, javaStackTrace);
+        }
+    }
+
+    /**
+     * @param heading should the heading at the top be printed 
+     * @param ftlStackTrace should the FTL stack trace be printed
+     * @param javaStackTrace should the Java stack trace be printed
+     *  
+     * @since 2.3.20
+     */
+    public void printStackTrace(PrintStream out, boolean heading, boolean ftlStackTrace, boolean javaStackTrace) {
+        synchronized (out) {
+            printStackTrace(new PrintStreamStackTraceWriter(out), heading, ftlStackTrace, javaStackTrace);
+        }
+    }
+    
+    private void printStackTrace(StackTraceWriter out, boolean heading, boolean ftlStackTrace, boolean javaStackTrace) {
+        synchronized (out) {
+            if (heading) { 
+                out.println("FreeMarker template error:");
+            }
+            
+            if (ftlStackTrace) {
+                String stackTrace = getFTLInstructionStack();
+                if (stackTrace != null) {
+                    out.println(getMessageWithoutStackTop());  // Not getMessage()!
+                    out.println();
+                    out.println(MessageUtil.ERROR_MESSAGE_HR);
+                    out.println(FTL_INSTRUCTION_STACK_TRACE_TITLE);
+                    out.print(stackTrace);
+                    out.println(MessageUtil.ERROR_MESSAGE_HR);
+                } else {
+                    ftlStackTrace = false;
+                    javaStackTrace = true;
+                }
+            }
+            
+            if (javaStackTrace) {
+                if (ftlStackTrace) {  // We are after an FTL stack trace
+                    out.println();
+                    out.println("Java stack trace (for programmers):");
+                    out.println(MessageUtil.ERROR_MESSAGE_HR);
+                    synchronized (lock) {
+                        if (messageWasAlreadyPrintedForThisTrace == null) {
+                            messageWasAlreadyPrintedForThisTrace = new ThreadLocal();
+                        }
+                        messageWasAlreadyPrintedForThisTrace.set(Boolean.TRUE);
+                    }
+                    
+                    try {
+                        out.printStandardStackTrace(this);
+                    } finally {
+                        messageWasAlreadyPrintedForThisTrace.set(Boolean.FALSE);
+                    }
+                } else {  // javaStackTrace only
+                    out.printStandardStackTrace(this);
+                }
+                
+                if (getCause() != null) {
+                    // Dirty hack to fight with ServletException class whose getCause() method doesn't work properly:
+                    Throwable causeCause = getCause().getCause();
+                    if (causeCause == null) {
+                        try {
+                            // Reflection is used to prevent dependency on Servlet classes.
+                            Method m = getCause().getClass().getMethod("getRootCause", _CollectionUtil.EMPTY_CLASS_ARRAY);
+                            Throwable rootCause = (Throwable) m.invoke(getCause(), _CollectionUtil.EMPTY_OBJECT_ARRAY);
+                            if (rootCause != null) {
+                                out.println("ServletException root cause: ");
+                                out.printStandardStackTrace(rootCause);
+                            }
+                        } catch (Throwable exc) {
+                            // ignore
+                        }
+                    }
+                }
+            }  // if (javaStackTrace)
+        }
+    }
+    
+    /**
+     * Prints the stack trace as if wasn't overridden by {@link TemplateException}. 
+     * @since 2.3.20
+     */
+    public void printStandardStackTrace(PrintStream ps) {
+        super.printStackTrace(ps);
+    }
+
+    /**
+     * Prints the stack trace as if wasn't overridden by {@link TemplateException}. 
+     * @since 2.3.20
+     */
+    public void printStandardStackTrace(PrintWriter pw) {
+        super.printStackTrace(pw);
+    }
+
+    @Override
+    public String getMessage() {
+        if (messageWasAlreadyPrintedForThisTrace != null
+                && messageWasAlreadyPrintedForThisTrace.get() == Boolean.TRUE) {
+            return "[... Exception message was already printed; see it above ...]";
+        } else {
+            synchronized (lock) {
+                if (message == null) renderMessages();
+                return message;
+            }
+        }
+    }
+    
+    /**
+     * Similar to {@link #getMessage()}, but it doesn't contain the position of the failing instruction at then end
+     * of the text. It might contains the position of the failing <em>expression</em> though as part of the expression
+     * quotation, as that's the part of the description. 
+     */
+    public String getMessageWithoutStackTop() {
+        synchronized (lock) {
+            if (messageWithoutStackTop == null) renderMessages();
+            return messageWithoutStackTop;
+        }
+    }
+    
+    /**
+     * 1-based line number of the failing section, or {@code null} if the information is not available.
+     * 
+     * @since 2.3.21
+     */
+    public Integer getLineNumber() {
+        synchronized (lock) {
+            if (!positionsCalculated) {
+                calculatePosition();
+            }
+            return lineNumber;
+        }
+    }
+
+    /**
+     * Returns the {@linkplain Template#getSourceName() source name} of the template where the error has occurred, or
+     * {@code null} if the information isn't available. This is what should be used for showing the error position.
+     *
+     * @since 2.3.22
+     */
+    public String getTemplateSourceName() {
+        synchronized (lock) {
+            if (!positionsCalculated) {
+                calculatePosition();
+            }
+            return templateSourceName;
+        }
+    }
+
+    /**
+     * Returns the {@linkplain Template#getLookupName()} () lookup name} of the template where the error has
+     * occurred, or {@code null} if the information isn't available. Do not use this for showing the error position;
+     * use {@link #getTemplateSourceName()}.
+     */
+    public String getTemplateLookupName() {
+        synchronized (lock) {
+            if (!positionsCalculated) {
+                calculatePosition();
+            }
+            return templateLookupName;
+        }
+    }
+
+    /**
+     * Returns the {@linkplain #getTemplateSourceName() template source name}, or if that's {@code null} then the
+     * {@linkplain #getTemplateLookupName() template lookup name}. This name is primarily meant to be used in error
+     * messages.
+     */
+    public String getTemplateSourceOrLookupName() {
+        return getTemplateSourceName() != null ? getTemplateSourceName() : getTemplateLookupName();
+    }
+
+    /**
+     * 1-based column number of the failing section, or {@code null} if the information is not available.
+     * 
+     * @since 2.3.21
+     */
+    public Integer getColumnNumber() {
+        synchronized (lock) {
+            if (!positionsCalculated) {
+                calculatePosition();
+            }
+            return columnNumber;
+        }
+    }
+
+    /**
+     * 1-based line number of the last line that contains the failing section, or {@code null} if the information is not
+     * available.
+     * 
+     * @since 2.3.21
+     */
+    public Integer getEndLineNumber() {
+        synchronized (lock) {
+            if (!positionsCalculated) {
+                calculatePosition();
+            }
+            return endLineNumber;
+        }
+    }
+
+    /**
+     * 1-based column number of the last character of the failing template section, or {@code null} if the information
+     * is not available. Note that unlike with Java string API-s, this column number is inclusive.
+     * 
+     * @since 2.3.21
+     */
+    public Integer getEndColumnNumber() {
+        synchronized (lock) {
+            if (!positionsCalculated) {
+                calculatePosition();
+            }
+            return endColumnNumber;
+        }
+    }
+    
+    /**
+     * If there was a blamed expression attached to this exception, it returns its canonical form, otherwise it returns
+     * {@code null}. This expression should always be inside the failing FTL instruction.
+     *  
+     * <p>The typical application of this is getting the undefined expression from {@link InvalidReferenceException}-s.
+     * 
+     * @since 2.3.21
+     */
+    public String getBlamedExpressionString() {
+        synchronized (lock) {
+            if (!blamedExpressionStringCalculated) {
+                if (blamedExpression != null) {
+                    blamedExpressionString = blamedExpression.getCanonicalForm();
+                }
+                blamedExpressionStringCalculated = true;
+            }
+            return blamedExpressionString;
+        }
+    }
+    
+    ASTExpression getBlamedExpression() {
+        return blamedExpression;
+    }
+
+    private void writeObject(ObjectOutputStream out) throws IOException, ClassNotFoundException {
+        // These are calculated from transient fields, so this is the last chance to calculate them: 
+        getFTLInstructionStack();
+        getFTLInstructionStackTopFew();
+        getDescription();
+        calculatePosition();
+        getBlamedExpressionString();
+        
+        out.defaultWriteObject();
+    }
+    
+    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        lock = new Object();
+        in.defaultReadObject();
+    }
+    
+    /** Delegate to a {@link PrintWriter} or to a {@link PrintStream}. */
+    private interface StackTraceWriter {
+        void print(Object obj);
+        void println(Object obj);
+        void println();
+        void printStandardStackTrace(Throwable exception);
+    }
+    
+    private static class PrintStreamStackTraceWriter implements StackTraceWriter {
+        
+        private final PrintStream out;
+
+        PrintStreamStackTraceWriter(PrintStream out) {
+            this.out = out;
+        }
+
+        @Override
+        public void print(Object obj) {
+            out.print(obj);
+        }
+
+        @Override
+        public void println(Object obj) {
+            out.println(obj);
+        }
+
+        @Override
+        public void println() {
+            out.println();
+        }
+
+        @Override
+        public void printStandardStackTrace(Throwable exception) {
+            if (exception instanceof TemplateException) {
+                ((TemplateException) exception).printStandardStackTrace(out);
+            } else {
+                exception.printStackTrace(out);
+            }
+        }
+        
+    }
+
+    private static class PrintWriterStackTraceWriter implements StackTraceWriter {
+        
+        private final PrintWriter out;
+
+        PrintWriterStackTraceWriter(PrintWriter out) {
+            this.out = out;
+        }
+
+        @Override
+        public void print(Object obj) {
+            out.print(obj);
+        }
+
+        @Override
+        public void println(Object obj) {
+            out.println(obj);
+        }
+
+        @Override
+        public void println() {
+            out.println();
+        }
+
+        @Override
+        public void printStandardStackTrace(Throwable exception) {
+            if (exception instanceof TemplateException) {
+                ((TemplateException) exception).printStandardStackTrace(out);
+            } else {
+                exception.printStackTrace(out);
+            }
+        }
+        
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateExceptionHandler.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateExceptionHandler.java b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateExceptionHandler.java
new file mode 100644
index 0000000..8270740
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateExceptionHandler.java
@@ -0,0 +1,156 @@
+/*
+ * 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.PrintWriter;
+import java.io.StringWriter;
+import java.io.Writer;
+
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * Used for the {@code template_exception_handler} configuration setting;
+ * see {@link MutableProcessingConfiguration#setTemplateExceptionHandler(TemplateExceptionHandler)} for more.
+ */
+public interface TemplateExceptionHandler {
+    
+    /** 
+     * Method called after a {@link TemplateException} was raised inside a template. The exception should be re-thrown
+     * unless you want to suppress the exception.
+     * 
+     * <p>Note that you can check with {@link Environment#isInAttemptBlock()} if you are inside a {@code #attempt}
+     * block, which then will handle handle this exception and roll back the output generated inside it.
+     * 
+     * <p>Note that {@link StopException}-s (raised by {@code #stop}) won't be captured.
+     * 
+     * <p>Note that you shouldn't log the exception in this method unless you suppress it. If there's a concern that the
+     * exception might won't be logged after it bubbles up from {@link Template#process(Object, Writer)}, simply
+     * ensure that {@link Configuration#getLogTemplateExceptions()} is {@code true}. 
+     * 
+     * @param te The exception that occurred; don't forget to re-throw it unless you want to suppress it
+     * @param env The runtime environment of the template
+     * @param out This is where the output of the template is written
+     */
+    void handleTemplateException(TemplateException te, Environment env, Writer out) throws TemplateException;
+            
+   /**
+    * {@link TemplateExceptionHandler} that simply skips the failing instructions, letting the template continue
+    * executing. It does nothing to handle the event. Note that the exception is still logged, as with all
+    * other {@link TemplateExceptionHandler}-s.
+    */
+    TemplateExceptionHandler IGNORE_HANDLER = new TemplateExceptionHandler() {
+        @Override
+        public void handleTemplateException(TemplateException te, Environment env, Writer out) {
+            // Do nothing
+        }
+    };
+        
+    /**
+     * {@link TemplateExceptionHandler} that simply re-throws the exception; this should be used in most production
+     * systems.
+     */
+    TemplateExceptionHandler RETHROW_HANDLER = new TemplateExceptionHandler() {
+        @Override
+        public void handleTemplateException(TemplateException te, Environment env, Writer out)
+                throws TemplateException {
+            throw te;
+        }
+    };
+        
+    /**
+     * {@link TemplateExceptionHandler} useful when you developing non-HTML templates. This handler
+     * outputs the stack trace information to the client and then re-throws the exception.
+     */
+    TemplateExceptionHandler DEBUG_HANDLER = new TemplateExceptionHandler() {
+        @Override
+        public void handleTemplateException(TemplateException te, Environment env, Writer out)
+                throws TemplateException {
+            if (!env.isInAttemptBlock()) {
+                PrintWriter pw = (out instanceof PrintWriter) ? (PrintWriter) out : new PrintWriter(out);
+                pw.print("FreeMarker template error (DEBUG mode; use RETHROW in production!):\n");
+                te.printStackTrace(pw, false, true, true);
+                
+                pw.flush();  // To commit the HTTP response
+            }
+            throw te;
+        }
+    }; 
+    
+    /**
+     * {@link TemplateExceptionHandler} useful when you developing HTML templates. This handler
+     * outputs the stack trace information to the client, formatting it so that it will be usually well readable
+     * in the browser, and then re-throws the exception.
+     */
+    TemplateExceptionHandler HTML_DEBUG_HANDLER = new TemplateExceptionHandler() {
+        @Override
+        public void handleTemplateException(TemplateException te, Environment env, Writer out)
+                throws TemplateException {
+            if (!env.isInAttemptBlock()) {
+                boolean externalPw = out instanceof PrintWriter;
+                PrintWriter pw = externalPw ? (PrintWriter) out : new PrintWriter(out);
+                try {
+                    pw.print("<!-- FREEMARKER ERROR MESSAGE STARTS HERE -->"
+                            + "<!-- ]]> -->"
+                            + "<script language=javascript>//\"></script>"
+                            + "<script language=javascript>//'></script>"
+                            + "<script language=javascript>//\"></script>"
+                            + "<script language=javascript>//'></script>"
+                            + "</title></xmp></script></noscript></style></object>"
+                            + "</head></pre></table>"
+                            + "</form></table></table></table></a></u></i></b>"
+                            + "<div align='left' "
+                            + "style='background-color:#FFFF7C; "
+                            + "display:block; border-top:double; padding:4px; margin:0; "
+                            + "font-family:Arial,sans-serif; ");
+                    pw.print(FONT_RESET_CSS);
+                    pw.print("'>"
+                            + "<b style='font-size:12px; font-style:normal; font-weight:bold; "
+                            + "text-decoration:none; text-transform: none;'>FreeMarker template error "
+                            + " (HTML_DEBUG mode; use RETHROW in production!)</b>"
+                            + "<pre style='display:block; background: none; border: 0; margin:0; padding: 0;"
+                            + "font-family:monospace; ");
+                    pw.print(FONT_RESET_CSS);
+                    pw.println("; white-space: pre-wrap; white-space: -moz-pre-wrap; white-space: -pre-wrap; "
+                            + "white-space: -o-pre-wrap; word-wrap: break-word;'>");
+                    
+                    StringWriter stackTraceSW = new StringWriter();
+                    PrintWriter stackPW = new PrintWriter(stackTraceSW);
+                    te.printStackTrace(stackPW, false, true, true);
+                    stackPW.close();
+                    pw.println();
+                    pw.println(_StringUtil.XMLEncNQG(stackTraceSW.toString()));
+                    
+                    pw.println("</pre></div></html>");
+                    pw.flush();  // To commit the HTTP response
+                } finally {
+                    if (!externalPw) pw.close();
+                }
+            }  // if (!env.isInAttemptBlock())
+            
+            throw te;
+        }
+        
+        private static final String FONT_RESET_CSS =
+                "color:#A80000; font-size:12px; font-style:normal; font-variant:normal; "
+                + "font-weight:normal; text-decoration:none; text-transform: none";
+        
+    };
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateLanguage.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateLanguage.java b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateLanguage.java
new file mode 100644
index 0000000..205fa8c
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateLanguage.java
@@ -0,0 +1,111 @@
+/*
+ * 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.InputStream;
+import java.io.Reader;
+import java.nio.charset.Charset;
+
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * Represents a template language. Currently this class is not mature, so it can't be implemented outside FreeMarker,
+ * also its methods shouldn't be called from outside FreeMarker.
+ */
+// [FM3] Make this mature, or hide its somehow. Actually, parse can't be hidden because custom TemplateResolver-s need
+// to call it.
+public abstract class TemplateLanguage {
+
+    // FIXME [FM3] If we leave this here, FTL will be a required dependency of core (which is not nice if
+    // template languages will be pluggable).
+    public static final TemplateLanguage FTL = new TemplateLanguage("FreeMarker Template Language") {
+        @Override
+        public boolean getCanSpecifyCharsetInContent() {
+            return true;
+        }
+
+        @Override
+        public Template parse(String name, String sourceName, Reader reader, Configuration cfg,
+                TemplateConfiguration templateConfiguration, Charset encoding,
+                InputStream streamToUnmarkWhenEncEstabd) throws
+                IOException, ParseException {
+            return new Template(name, sourceName, reader, cfg, templateConfiguration,
+                    encoding, streamToUnmarkWhenEncEstabd);
+        }
+    };
+
+    public static final TemplateLanguage STATIC_TEXT = new TemplateLanguage("Static text") {
+        @Override
+        public boolean getCanSpecifyCharsetInContent() {
+            return false;
+        }
+
+        @Override
+        public Template parse(String name, String sourceName, Reader reader, Configuration cfg,
+                TemplateConfiguration templateConfiguration, Charset sourceEncoding,
+                InputStream streamToUnmarkWhenEncEstabd)
+                throws IOException, ParseException {
+            // Read the contents into a StringWriter, then construct a single-text-block template from it.
+            final StringBuilder sb = new StringBuilder();
+            final char[] buf = new char[4096];
+            int charsRead;
+            while ((charsRead = reader.read(buf)) > 0) {
+                sb.append(buf, 0, charsRead);
+            }
+            return Template.createPlainTextTemplate(name, sourceName, sb.toString(), cfg,
+                    sourceEncoding);
+        }
+    };
+
+    private final String name;
+
+    // Package visibility to prevent user implementations until this API is mature.
+    TemplateLanguage(String name) {
+        this.name = name;
+    }
+
+    /**
+     * Returns if the template can specify its own charset inside the template. If so, {@link #parse(String, String,
+     * Reader, Configuration, TemplateConfiguration, Charset, InputStream)} can throw
+     * {@link WrongTemplateCharsetException}, and it might gets a non-{@code null} for the {@link InputStream}
+     * parameter.
+     */
+    public abstract boolean getCanSpecifyCharsetInContent();
+
+    /**
+     * See {@link Template#Template(String, String, Reader, Configuration, TemplateConfiguration, Charset,
+     * InputStream)}.
+     */
+    public abstract Template parse(String name, String sourceName, Reader reader,
+                                   Configuration cfg, TemplateConfiguration templateConfiguration,
+                                   Charset encoding, InputStream streamToUnmarkWhenEncEstabd)
+            throws IOException, ParseException;
+
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public final String toString() {
+        return "TemplateLanguage(" + _StringUtil.jQuote(name) + ")";
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateNotFoundException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateNotFoundException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateNotFoundException.java
new file mode 100644
index 0000000..37ba911
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateNotFoundException.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 java.io.FileNotFoundException;
+import java.io.Serializable;
+
+import org.apache.freemarker.core.templateresolver.MalformedTemplateNameException;
+
+/**
+ * Thrown when {@link Configuration#getTemplate(String)} (or similar) doesn't find a template.
+ * This extends {@link FileNotFoundException} for backward compatibility, but in fact has nothing to do with files, as
+ * FreeMarker can load templates from many other sources.
+ *
+ * @since 2.3.22
+ * 
+ * @see MalformedTemplateNameException
+ * @see Configuration#getTemplate(String)
+ */
+public final class TemplateNotFoundException extends FileNotFoundException {
+    
+    private final String templateName;
+    private final Object customLookupCondition;
+
+    public TemplateNotFoundException(String templateName, Object customLookupCondition, String message) {
+        super(message);
+        this.templateName = templateName;
+        this.customLookupCondition = customLookupCondition;
+    }
+
+    /**
+     * The name (path) of the template that wasn't found.
+     */
+    public String getTemplateName() {
+        return templateName;
+    }
+
+    /**
+     * The custom lookup condition with which the template was requested, or {@code null} if there's no such condition.
+     * See the {@code customLookupCondition} parameter of
+     * {@link Configuration#getTemplate(String, java.util.Locale, Serializable, boolean)}.
+     */
+    public Object getCustomLookupCondition() {
+        return customLookupCondition;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateParsingConfigurationWithFallback.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateParsingConfigurationWithFallback.java b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateParsingConfigurationWithFallback.java
new file mode 100644
index 0000000..93a5840
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateParsingConfigurationWithFallback.java
@@ -0,0 +1,146 @@
+/*
+ * 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.nio.charset.Charset;
+
+import org.apache.freemarker.core.arithmetic.ArithmeticEngine;
+import org.apache.freemarker.core.outputformat.OutputFormat;
+
+/**
+ * Adds {@link Configuration} fallback to the {@link ParsingConfiguration} part of a {@link TemplateConfiguration}.
+ */
+final class TemplateParsingConfigurationWithFallback implements ParsingConfiguration {
+
+    private final Configuration cfg;
+    private final TemplateConfiguration tCfg;
+
+    TemplateParsingConfigurationWithFallback(Configuration cfg, TemplateConfiguration tCfg) {
+        this.cfg = cfg;
+        this.tCfg = tCfg;
+    }
+
+    @Override
+    public TemplateLanguage getTemplateLanguage() {
+        return tCfg.isTemplateLanguageSet() ? tCfg.getTemplateLanguage() : cfg.getTemplateLanguage();
+    }
+
+    @Override
+    public boolean isTemplateLanguageSet() {
+        return true;
+    }
+
+    @Override
+    public int getTagSyntax() {
+        return tCfg.isTagSyntaxSet() ? tCfg.getTagSyntax() : cfg.getTagSyntax();
+    }
+
+    @Override
+    public boolean isTagSyntaxSet() {
+        return true;
+    }
+
+    @Override
+    public int getNamingConvention() {
+        return tCfg.isNamingConventionSet() ? tCfg.getNamingConvention() : cfg.getNamingConvention();
+    }
+
+    @Override
+    public boolean isNamingConventionSet() {
+        return true;
+    }
+
+    @Override
+    public boolean getWhitespaceStripping() {
+        return tCfg.isWhitespaceStrippingSet() ? tCfg.getWhitespaceStripping() : cfg.getWhitespaceStripping();
+    }
+
+    @Override
+    public boolean isWhitespaceStrippingSet() {
+        return true;
+    }
+
+    @Override
+    public ArithmeticEngine getArithmeticEngine() {
+        return tCfg.isArithmeticEngineSet() ? tCfg.getArithmeticEngine() : cfg.getArithmeticEngine();
+    }
+
+    @Override
+    public boolean isArithmeticEngineSet() {
+        return true;
+    }
+
+    @Override
+    public int getAutoEscapingPolicy() {
+        return tCfg.isAutoEscapingPolicySet() ? tCfg.getAutoEscapingPolicy() : cfg.getAutoEscapingPolicy();
+    }
+
+    @Override
+    public boolean isAutoEscapingPolicySet() {
+        return true;
+    }
+
+    @Override
+    public OutputFormat getOutputFormat() {
+        return tCfg.isOutputFormatSet() ? tCfg.getOutputFormat() : cfg.getOutputFormat();
+    }
+
+    @Override
+    public boolean isOutputFormatSet() {
+        return true;
+    }
+
+    @Override
+    public boolean getRecognizeStandardFileExtensions() {
+        return tCfg.isRecognizeStandardFileExtensionsSet() ? tCfg.getRecognizeStandardFileExtensions()
+                : cfg.getRecognizeStandardFileExtensions();
+    }
+
+    @Override
+    public boolean isRecognizeStandardFileExtensionsSet() {
+        return true;
+    }
+
+    @Override
+    public Version getIncompatibleImprovements() {
+        // This can be only set on the Configuration-level
+        return cfg.getIncompatibleImprovements();
+    }
+
+    @Override
+    public int getTabSize() {
+        return tCfg.isTabSizeSet() ? tCfg.getTabSize() : cfg.getTabSize();
+    }
+
+    @Override
+    public boolean isTabSizeSet() {
+        return true;
+    }
+
+    @Override
+    public Charset getSourceEncoding() {
+        return tCfg.isSourceEncodingSet() ? tCfg.getSourceEncoding() : cfg.getSourceEncoding();
+    }
+
+    @Override
+    public boolean isSourceEncodingSet() {
+        return true;
+    }
+}



[28/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_ObjectBuilderSettingEvaluator.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_ObjectBuilderSettingEvaluator.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_ObjectBuilderSettingEvaluator.java
new file mode 100644
index 0000000..cc96d81
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_ObjectBuilderSettingEvaluator.java
@@ -0,0 +1,1068 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateMethodModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.impl.DefaultObjectWrapper;
+import org.apache.freemarker.core.model.impl.RestrictedObjectWrapper;
+import org.apache.freemarker.core.outputformat.impl.HTMLOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.PlainTextOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.RTFOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.UndefinedOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.XMLOutputFormat;
+import org.apache.freemarker.core.templateresolver.AndMatcher;
+import org.apache.freemarker.core.templateresolver.ConditionalTemplateConfigurationFactory;
+import org.apache.freemarker.core.templateresolver.FileExtensionMatcher;
+import org.apache.freemarker.core.templateresolver.FileNameGlobMatcher;
+import org.apache.freemarker.core.templateresolver.FirstMatchTemplateConfigurationFactory;
+import org.apache.freemarker.core.templateresolver.MergingTemplateConfigurationFactory;
+import org.apache.freemarker.core.templateresolver.NotMatcher;
+import org.apache.freemarker.core.templateresolver.OrMatcher;
+import org.apache.freemarker.core.templateresolver.PathGlobMatcher;
+import org.apache.freemarker.core.templateresolver.PathRegexMatcher;
+import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.util.FTLUtil;
+import org.apache.freemarker.core.util.GenericParseException;
+import org.apache.freemarker.core.util._ClassUtil;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * Don't use this; used internally by FreeMarker, might changes without notice.
+ * 
+ * Evaluates object builder expressions used in configuration {@link Properties}.
+ * It should be replaced with FTL later (when it was improved to be practical for this), so the syntax should be
+ * a subset of the future FTL syntax. This is also why this syntax is restrictive; it shouldn't accept anything that
+ * FTL will not.
+ */
+// Java 5: use generics for expectedClass
+// Java 5: Introduce ObjectBuilder interface
+public class _ObjectBuilderSettingEvaluator {
+    
+    private static final String INSTANCE_FIELD_NAME = "INSTANCE";
+
+    private static final String BUILD_METHOD_NAME = "build";
+
+    private static final String BUILDER_CLASS_POSTFIX_1 = "$Builder";
+    private static final String BUILDER_CLASS_POSTFIX_2 = "Builder";
+
+    private static Map<String,String> SHORTHANDS;
+    
+    private static final Object VOID = new Object();
+
+    private final String src;
+    private final Class expectedClass;
+    private final boolean allowNull;
+    private final _SettingEvaluationEnvironment env;
+
+    // Parser state:
+    private int pos;
+    
+    private _ObjectBuilderSettingEvaluator(
+            String src, int pos, Class expectedClass, boolean allowNull, _SettingEvaluationEnvironment env) {
+        this.src = src;
+        this.pos = pos;
+        this.expectedClass = expectedClass;
+        this.allowNull = allowNull;
+        this.env = env;
+    }
+
+    public static Object eval(String src, Class expectedClass, boolean allowNull, _SettingEvaluationEnvironment env)
+            throws _ObjectBuilderSettingEvaluationException,
+            ClassNotFoundException, InstantiationException, IllegalAccessException {
+        return new _ObjectBuilderSettingEvaluator(src, 0, expectedClass, allowNull, env).eval();
+    }
+
+    /**
+     * Used for getting a list of setting assignments (like {@code (x=1, y=2)}) from an existing string, and apply it on
+     * an existing bean.
+     * 
+     * @return The location of the next character to process.
+     */
+    public static int configureBean(
+            String argumentListSrc, int posAfterOpenParen, Object bean, _SettingEvaluationEnvironment env)
+            throws _ObjectBuilderSettingEvaluationException,
+            ClassNotFoundException, InstantiationException, IllegalAccessException {
+        return new _ObjectBuilderSettingEvaluator(
+                argumentListSrc, posAfterOpenParen, bean.getClass(), true, env).configureBean(bean);
+    }
+    
+    private Object eval() throws _ObjectBuilderSettingEvaluationException,
+            ClassNotFoundException, InstantiationException, IllegalAccessException {
+        Object value;
+        
+        skipWS();
+        value = ensureEvaled(fetchValue(false, true, false, true));
+        skipWS();
+        
+        if (pos != src.length()) {
+            throw new _ObjectBuilderSettingEvaluationException("end-of-expression", src, pos);
+        }
+        
+        if (value == null && !allowNull) {
+            throw new _ObjectBuilderSettingEvaluationException("Value can't be null.");
+        }
+        if (value != null && !expectedClass.isInstance(value)) {
+            throw new _ObjectBuilderSettingEvaluationException("The resulting object (of class "
+                    + value.getClass() + ") is not a(n) " + expectedClass.getName() + ".");
+        }
+        
+        return value;
+    }
+    
+    private int configureBean(Object bean) throws _ObjectBuilderSettingEvaluationException,
+            ClassNotFoundException, InstantiationException, IllegalAccessException {
+        final PropertyAssignmentsExpression propAssignments = new PropertyAssignmentsExpression(bean);
+        fetchParameterListInto(propAssignments);
+        skipWS();
+        propAssignments.eval();
+        return pos;
+    }
+
+    private Object ensureEvaled(Object value) throws _ObjectBuilderSettingEvaluationException {
+        return value instanceof SettingExpression ? ((SettingExpression) value).eval() : value;
+    }
+
+    private Object fetchBuilderCall(boolean optional, boolean topLevel)
+            throws _ObjectBuilderSettingEvaluationException {
+        int startPos = pos;
+        
+        BuilderCallExpression exp = new BuilderCallExpression();
+        // We need the canBeStaticField/mustBeStaticFiled complication to deal with legacy syntax where parentheses
+        // weren't required for constructor calls.
+        exp.canBeStaticField = true;
+        
+        final String fetchedClassName = fetchClassName(optional);
+        {
+            if (fetchedClassName == null) {
+                if (!optional) {
+                    throw new _ObjectBuilderSettingEvaluationException("class name", src, pos);
+                }
+                return VOID;
+            }
+            exp.className = shorthandToFullQualified(fetchedClassName);
+            if (!fetchedClassName.equals(exp.className)) {
+                exp.canBeStaticField = false;
+            }
+        }
+        
+        skipWS();
+        
+        char openParen = fetchOptionalChar("(");
+        // Only the top-level expression can omit the "(...)"
+        if (openParen == 0 && !topLevel) {
+            if (fetchedClassName.indexOf('.') != -1) {
+                exp.mustBeStaticField = true;
+            } else {
+                pos = startPos;
+                return VOID;
+            }
+        }
+    
+        if (openParen != 0) {
+            fetchParameterListInto(exp);
+            exp.canBeStaticField = false;
+        }
+        
+        return exp;
+    }
+
+    private void fetchParameterListInto(ExpressionWithParameters exp) throws _ObjectBuilderSettingEvaluationException {
+        skipWS();
+        if (fetchOptionalChar(")") != ')') { 
+            do {
+                skipWS();
+                
+                Object paramNameOrValue = fetchValue(false, false, true, false);
+                if (paramNameOrValue != VOID) {
+                    skipWS();
+                    if (paramNameOrValue instanceof Name) {
+                        exp.namedParamNames.add(((Name) paramNameOrValue).name);
+                        
+                        skipWS();
+                        fetchRequiredChar("=");
+                        skipWS();
+                        
+                        Object paramValue = fetchValue(false, false, true, true);
+                        exp.namedParamValues.add(ensureEvaled(paramValue));
+                    } else {
+                        if (!exp.namedParamNames.isEmpty()) {
+                            throw new _ObjectBuilderSettingEvaluationException(
+                                    "Positional parameters must precede named parameters");
+                        }
+                        if (!exp.getAllowPositionalParameters()) {
+                            throw new _ObjectBuilderSettingEvaluationException(
+                                    "Positional parameters not supported here");
+                        }
+                        exp.positionalParamValues.add(ensureEvaled(paramNameOrValue));
+                    }
+                    
+                    skipWS();
+                }
+            } while (fetchRequiredChar(",)") == ',');
+        }
+    }
+
+    private Object fetchValue(boolean optional, boolean topLevel, boolean resultCoerced, boolean resolveVariables)
+            throws _ObjectBuilderSettingEvaluationException {
+        if (pos < src.length()) {
+            Object val = fetchNumberLike(true, resultCoerced);
+            if (val != VOID) {
+                return val;
+            }
+    
+            val = fetchStringLiteral(true);
+            if (val != VOID) {
+                return val;
+            }
+
+            val = fetchListLiteral(true);
+            if (val != VOID) {
+                return val;
+            }
+
+            val = fetchMapLiteral(true);
+            if (val != VOID) {
+                return val;
+            }
+            
+            val = fetchBuilderCall(true, topLevel);
+            if (val != VOID) {
+                return val;
+            }
+            
+            String name = fetchSimpleName(true);
+            if (name != null) {
+                val = keywordToValueOrVoid(name);
+                if (val != VOID) {
+                    return val;
+                }
+                
+                if (resolveVariables) {
+                    // Not supported currently...
+                    throw new _ObjectBuilderSettingEvaluationException("Can't resolve variable reference: " + name);
+                } else {
+                    return new Name(name);
+                }
+            }
+        }
+        
+        if (optional) {
+            return VOID;
+        } else {
+            throw new _ObjectBuilderSettingEvaluationException("value or name", src, pos);
+        }
+    }
+
+    private boolean isKeyword(String name) {
+        return keywordToValueOrVoid(name) != VOID;
+    }
+    
+    private Object keywordToValueOrVoid(String name) {
+        if (name.equals("true")) return Boolean.TRUE;
+        if (name.equals("false")) return Boolean.FALSE;
+        if (name.equals("null")) return null;
+        return VOID;
+    }
+
+    private String fetchSimpleName(boolean optional) throws _ObjectBuilderSettingEvaluationException {
+        char c = pos < src.length() ? src.charAt(pos) : 0;
+        if (!isIdentifierStart(c)) {
+            if (optional) {
+                return null;
+            } else {
+                throw new _ObjectBuilderSettingEvaluationException("class name", src, pos);
+            }
+        }
+        int startPos = pos;
+        pos++;
+        
+        seekClassNameEnd: while (true) {
+            if (pos == src.length()) {
+                break seekClassNameEnd;
+            }
+            c = src.charAt(pos);
+            if (!isIdentifierMiddle(c)) {
+                break seekClassNameEnd;
+            }
+            pos++;
+        }
+        
+        return src.substring(startPos, pos);
+    }
+
+    private String fetchClassName(boolean optional) throws _ObjectBuilderSettingEvaluationException {
+        int startPos = pos;
+        StringBuilder sb = new StringBuilder();
+        do {
+            String name = fetchSimpleName(true);
+            if (name == null) {
+                if (!optional) {
+                    throw new _ObjectBuilderSettingEvaluationException("name", src, pos);
+                } else {
+                    pos = startPos;
+                    return null;
+                }
+            }
+            sb.append(name);
+            
+            skipWS();
+            
+            if (pos >= src.length() || src.charAt(pos) != '.') {
+                break;
+            }
+            sb.append('.');
+            pos++;
+            
+            skipWS();
+        } while (true);
+        
+        String className = sb.toString();
+        if (isKeyword(className)) {
+            pos = startPos;
+            return null;
+        }
+        return className;
+    }
+
+    private Object fetchNumberLike(boolean optional, boolean resultCoerced)
+            throws _ObjectBuilderSettingEvaluationException {
+        int startPos = pos;
+        boolean isVersion = false;
+        boolean hasDot = false;
+        seekTokenEnd: while (true) {
+            if (pos == src.length()) {
+                break seekTokenEnd;
+            }
+            char c = src.charAt(pos);
+            if (c == '.') {
+                if (hasDot) {
+                    // More than one dot
+                    isVersion = true;
+                } else {
+                    hasDot = true;
+                }
+            } else if (!(isASCIIDigit(c) || c == '-')) {
+                break seekTokenEnd;
+            }
+            pos++;
+        }
+        
+        if (startPos == pos) {
+            if (optional) {
+                return VOID;
+            } else {
+                throw new _ObjectBuilderSettingEvaluationException("number-like", src, pos);
+            }
+        }
+        
+        String numStr = src.substring(startPos, pos);
+        if (isVersion) {
+            try {
+                return new Version(numStr);
+            } catch (IllegalArgumentException e) {
+                throw new _ObjectBuilderSettingEvaluationException("Malformed version number: " + numStr, e);
+            }
+        } else {
+            // For example, in 1.0f, numStr is "1.0", and typePostfix is "f".
+            String typePostfix = null;
+            seekTypePostfixEnd: while (true) {
+                if (pos == src.length()) {
+                    break seekTypePostfixEnd;
+                }
+                char c = src.charAt(pos);
+                if (Character.isLetter(c)) {
+                    if (typePostfix == null) {
+                        typePostfix = String.valueOf(c);
+                    } else {
+                        typePostfix += c; 
+                    }
+                } else {
+                    break seekTypePostfixEnd;
+                }
+                pos++;
+            }
+            
+            try {
+                if (numStr.endsWith(".")) {
+                    throw new NumberFormatException("A number can't end with a dot");
+                }
+                if (numStr.startsWith(".") || numStr.startsWith("-.")  || numStr.startsWith("+.")) {
+                    throw new NumberFormatException("A number can't start with a dot");
+                }
+
+                if (typePostfix == null) {
+                    // Auto-detect type
+                    if (numStr.indexOf('.') == -1) {
+                        BigInteger biNum = new BigInteger(numStr);
+                        final int bitLength = biNum.bitLength();  // Doesn't include sign bit
+                        if (bitLength <= 31) {
+                            return Integer.valueOf(biNum.intValue());
+                        } else if (bitLength <= 63) {
+                            return Long.valueOf(biNum.longValue());
+                        } else {
+                            return biNum;
+                        }
+                    } else {
+                        if (resultCoerced) {
+                            // The FTL way (BigDecimal is loseless, and it will be coerced to the target type later):
+                            return new BigDecimal(numStr);
+                        } else {
+                            // The Java way (lossy but familiar):
+                            return Double.valueOf(numStr);
+                        }
+                    }
+                } else { // Has explicitly specified type
+                    if (typePostfix.equalsIgnoreCase("l")) {
+                        return Long.valueOf(numStr);
+                    } else if (typePostfix.equalsIgnoreCase("bi")) {
+                        return new BigInteger(numStr);
+                    } else if (typePostfix.equalsIgnoreCase("bd")) {
+                        return new BigDecimal(numStr);
+                    } else if (typePostfix.equalsIgnoreCase("d")) {
+                        return Double.valueOf(numStr);
+                    } else if (typePostfix.equalsIgnoreCase("f")) {
+                        return Float.valueOf(numStr);
+                    } else {
+                        throw new _ObjectBuilderSettingEvaluationException(
+                                "Unrecognized number type postfix: " + typePostfix);
+                    }
+                }
+                
+            } catch (NumberFormatException e) {
+                throw new _ObjectBuilderSettingEvaluationException("Malformed number: " + numStr, e);
+            }
+        }
+    }
+
+    private Object fetchStringLiteral(boolean optional) throws _ObjectBuilderSettingEvaluationException {
+        int startPos = pos;
+        char q = 0;
+        boolean afterEscape = false;
+        boolean raw = false;
+        seekTokenEnd: while (true) {
+            if (pos == src.length()) {
+                if (q != 0) {
+                    // We had an open quotation
+                    throw new _ObjectBuilderSettingEvaluationException(String.valueOf(q), src, pos);
+                }
+                break seekTokenEnd;
+            }
+            char c = src.charAt(pos);
+            if (q == 0) {
+                if (c == 'r' && (pos + 1 < src.length())) {
+                    // Maybe it's like r"foo\bar"
+                    raw = true;
+                    c = src.charAt(pos + 1);
+                }
+                if (c == '\'') {
+                    q = '\'';
+                } else if (c == '"') {
+                    q = '"';
+                } else {
+                    break seekTokenEnd;
+                }
+                if (raw) {
+                    // because of the preceding "r"
+                    pos++;
+                }
+            } else {
+                if (!afterEscape) {
+                    if (c == '\\' && !raw) {
+                        afterEscape = true;
+                    } else if (c == q) {
+                        break seekTokenEnd;
+                    } else if (c == '{') {
+                        char prevC = src.charAt(pos - 1);
+                        if (prevC == '$' || prevC == '#') {
+                            throw new _ObjectBuilderSettingEvaluationException(
+                                    "${...} and #{...} aren't allowed here.");
+                        }
+                    }
+                } else {
+                    afterEscape = false;
+                }
+            }
+            pos++;
+        }
+        if (startPos == pos) {
+            if (optional) {
+                return VOID;
+            } else {
+                throw new _ObjectBuilderSettingEvaluationException("string literal", src, pos);
+            }
+        }
+            
+        final String sInside = src.substring(startPos + (raw ? 2 : 1), pos);
+        try {
+            pos++; // skip closing quotation mark
+            return raw ? sInside : FTLUtil.unescapeStringLiteralPart(sInside);
+        } catch (GenericParseException e) {
+            throw new _ObjectBuilderSettingEvaluationException("Malformed string literal: " + sInside, e);
+        }
+    }
+
+    private Object fetchListLiteral(boolean optional) throws _ObjectBuilderSettingEvaluationException {
+        if (pos == src.length() || src.charAt(pos) != '[') {
+            if (!optional) {
+                throw new _ObjectBuilderSettingEvaluationException("[", src, pos);
+            }
+            return VOID;
+        }
+        pos++;
+        
+        ListExpression listExp = new ListExpression();
+        
+        while (true) {
+            skipWS();
+            
+            if (fetchOptionalChar("]") != 0) {
+                return listExp;
+            }
+            if (listExp.itemCount() != 0) {
+                fetchRequiredChar(",");
+                skipWS();
+            }
+            
+            listExp.addItem(fetchValue(false, false, false, true));
+            
+            skipWS();
+        }
+    }
+
+    private Object fetchMapLiteral(boolean optional) throws _ObjectBuilderSettingEvaluationException {
+        if (pos == src.length() || src.charAt(pos) != '{') {
+            if (!optional) {
+                throw new _ObjectBuilderSettingEvaluationException("{", src, pos);
+            }
+            return VOID;
+        }
+        pos++;
+        
+        MapExpression mapExp = new MapExpression();
+        
+        while (true) {
+            skipWS();
+            
+            if (fetchOptionalChar("}") != 0) {
+                return mapExp;
+            }
+            if (mapExp.itemCount() != 0) {
+                fetchRequiredChar(",");
+                skipWS();
+            }
+            
+            Object key = fetchValue(false, false, false, true);
+            skipWS();
+            fetchRequiredChar(":");
+            skipWS();
+            Object value = fetchValue(false, false, false, true);
+            mapExp.addItem(new KeyValuePair(key, value));
+            
+            skipWS();
+        }
+    }
+    
+    private void skipWS() {
+        while (true) {
+            if (pos == src.length()) {
+                return;
+            }
+            char c = src.charAt(pos);
+            if (!Character.isWhitespace(c)) {
+                return;
+            }
+            pos++;
+        }
+    }
+
+    private char fetchOptionalChar(String expectedChars) throws _ObjectBuilderSettingEvaluationException {
+        return fetchChar(expectedChars, true);
+    }
+    
+    private char fetchRequiredChar(String expectedChars) throws _ObjectBuilderSettingEvaluationException {
+        return fetchChar(expectedChars, false);
+    }
+    
+    private char fetchChar(String expectedChars, boolean optional) throws _ObjectBuilderSettingEvaluationException {
+        char c = pos < src.length() ? src.charAt(pos) : 0;
+        if (expectedChars.indexOf(c) != -1) {
+            pos++;
+            return c;
+        } else if (optional) {
+            return 0;
+        } else {
+            StringBuilder sb = new StringBuilder();
+            for (int i = 0; i < expectedChars.length(); i++) {
+                if (i != 0) {
+                    sb.append(" or ");
+                }
+                sb.append(_StringUtil.jQuote(expectedChars.substring(i, i + 1)));
+            }
+            throw new _ObjectBuilderSettingEvaluationException(
+                    sb.toString(),
+                    src, pos);
+        }
+    }
+    
+    private boolean isASCIIDigit(char c) {
+        return c >= '0' && c <= '9';
+    }
+
+    private boolean isIdentifierStart(char c) {
+        return Character.isLetter(c) || c == '_' || c == '$';
+    }
+
+    private boolean isIdentifierMiddle(char c) {
+        return isIdentifierStart(c) || isASCIIDigit(c);
+    }
+
+    private static synchronized String shorthandToFullQualified(String className) {
+        if (SHORTHANDS == null) {
+            SHORTHANDS = new HashMap/*<String,String>*/();
+            
+            addWithSimpleName(SHORTHANDS, DefaultObjectWrapper.class);
+            addWithSimpleName(SHORTHANDS, DefaultObjectWrapper.class);
+            addWithSimpleName(SHORTHANDS, RestrictedObjectWrapper.class);
+
+            addWithSimpleName(SHORTHANDS, TemplateConfiguration.class);
+            
+            addWithSimpleName(SHORTHANDS, PathGlobMatcher.class);
+            addWithSimpleName(SHORTHANDS, FileNameGlobMatcher.class);
+            addWithSimpleName(SHORTHANDS, FileExtensionMatcher.class);
+            addWithSimpleName(SHORTHANDS, PathRegexMatcher.class);
+            addWithSimpleName(SHORTHANDS, AndMatcher.class);
+            addWithSimpleName(SHORTHANDS, OrMatcher.class);
+            addWithSimpleName(SHORTHANDS, NotMatcher.class);
+            
+            addWithSimpleName(SHORTHANDS, ConditionalTemplateConfigurationFactory.class);
+            addWithSimpleName(SHORTHANDS, MergingTemplateConfigurationFactory.class);
+            addWithSimpleName(SHORTHANDS, FirstMatchTemplateConfigurationFactory.class);
+
+            addWithSimpleName(SHORTHANDS, HTMLOutputFormat.class);
+            addWithSimpleName(SHORTHANDS, XMLOutputFormat.class);
+            addWithSimpleName(SHORTHANDS, RTFOutputFormat.class);
+            addWithSimpleName(SHORTHANDS, PlainTextOutputFormat.class);
+            addWithSimpleName(SHORTHANDS, UndefinedOutputFormat.class);
+
+            addWithSimpleName(SHORTHANDS, TemplateLanguage.class);
+
+            addWithSimpleName(SHORTHANDS, Locale.class);
+
+            {
+                String tzbClassName = _TimeZoneBuilder.class.getName();
+                SHORTHANDS.put("TimeZone",
+                        tzbClassName.substring(0, tzbClassName.length() - BUILDER_CLASS_POSTFIX_2.length()));
+            }
+
+            {
+                String csClassName = _CharsetBuilder.class.getName();
+                SHORTHANDS.put("Charset",
+                        csClassName.substring(0, csClassName.length() - BUILDER_CLASS_POSTFIX_2.length()));
+            }
+
+            // For accessing static fields:
+            addWithSimpleName(SHORTHANDS, Configuration.class);
+        }
+        String fullClassName = SHORTHANDS.get(className);
+        return fullClassName == null ? className : fullClassName;
+    }
+    
+    private static void addWithSimpleName(Map map, Class<?> pClass) {
+        map.put(pClass.getSimpleName(), pClass.getName());
+    }
+
+    private void setJavaBeanProperties(Object bean,
+            List/*<String>*/ namedParamNames, List/*<Object>*/ namedParamValues)
+            throws _ObjectBuilderSettingEvaluationException {
+        if (namedParamNames.isEmpty()) {
+            return;
+        }
+        
+        final Class cl = bean.getClass();
+        Map/*<String,Method>*/ beanPropSetters;
+        try {
+            PropertyDescriptor[] propDescs = Introspector.getBeanInfo(cl).getPropertyDescriptors();
+            beanPropSetters = new HashMap(propDescs.length * 4 / 3, 1.0f);
+            for (PropertyDescriptor propDesc : propDescs) {
+                final Method writeMethod = propDesc.getWriteMethod();
+                if (writeMethod != null) {
+                    beanPropSetters.put(propDesc.getName(), writeMethod);
+                }
+            }
+        } catch (Exception e) {
+            throw new _ObjectBuilderSettingEvaluationException("Failed to inspect " + cl.getName() + " class", e);
+        }
+
+        TemplateHashModel beanTM = null;
+        for (int i = 0; i < namedParamNames.size(); i++) {
+            String name = (String) namedParamNames.get(i);
+            if (!beanPropSetters.containsKey(name)) {
+                throw new _ObjectBuilderSettingEvaluationException(
+                        "The " + cl.getName() + " class has no writeable JavaBeans property called "
+                        + _StringUtil.jQuote(name) + ".");
+            }
+            
+            Method beanPropSetter = (Method) beanPropSetters.put(name, null);
+            if (beanPropSetter == null) {
+                    throw new _ObjectBuilderSettingEvaluationException(
+                            "JavaBeans property " + _StringUtil.jQuote(name) + " is set twice.");
+            }
+            
+            try {
+                if (beanTM == null) {
+                    TemplateModel wrappedObj = env.getObjectWrapper().wrap(bean);
+                    if (!(wrappedObj instanceof TemplateHashModel)) {
+                        throw new _ObjectBuilderSettingEvaluationException(
+                                "The " + cl.getName() + " class is not a wrapped as TemplateHashModel.");
+                    }
+                    beanTM = (TemplateHashModel) wrappedObj;
+                }
+                
+                TemplateModel m = beanTM.get(beanPropSetter.getName());
+                if (m == null) {
+                    throw new _ObjectBuilderSettingEvaluationException(
+                            "Can't find " + beanPropSetter + " as FreeMarker method.");
+                }
+                if (!(m instanceof TemplateMethodModelEx)) {
+                    throw new _ObjectBuilderSettingEvaluationException(
+                            _StringUtil.jQuote(beanPropSetter.getName()) + " wasn't a TemplateMethodModelEx.");
+                }
+                List/*TemplateModel*/ args = new ArrayList();
+                args.add(env.getObjectWrapper().wrap(namedParamValues.get(i)));
+                ((TemplateMethodModelEx) m).exec(args);
+            } catch (Exception e) {
+                throw new _ObjectBuilderSettingEvaluationException(
+                        "Failed to set " + _StringUtil.jQuote(name), e);
+            }
+        }
+    }
+
+    private static class Name {
+        
+        public Name(String name) {
+            this.name = name;
+        }
+
+        private final String name;
+    }
+    
+    private abstract static class SettingExpression {
+        abstract Object eval() throws _ObjectBuilderSettingEvaluationException;
+    }
+    
+    private abstract class ExpressionWithParameters extends SettingExpression {
+        protected List positionalParamValues = new ArrayList();
+        protected List/*<String>*/ namedParamNames = new ArrayList();
+        protected List/*<Object>*/ namedParamValues = new ArrayList();
+        
+        protected abstract boolean getAllowPositionalParameters();
+    }
+    
+    private class ListExpression extends SettingExpression {
+        
+        private List<Object> items = new ArrayList();
+        
+        void addItem(Object item) {
+            items.add(item);
+        }
+
+        public int itemCount() {
+            return items.size();
+        }
+
+        @Override
+        Object eval() throws _ObjectBuilderSettingEvaluationException {
+            ArrayList res = new ArrayList(items.size());
+            for (Object item : items) {
+                res.add(ensureEvaled(item));
+            }
+            return res;
+        }
+        
+    }
+    
+    private class MapExpression extends SettingExpression {
+        
+        private List<KeyValuePair> items = new ArrayList();
+        
+        void addItem(KeyValuePair item) {
+            items.add(item);
+        }
+
+        public int itemCount() {
+            return items.size();
+        }
+
+        @Override
+        Object eval() throws _ObjectBuilderSettingEvaluationException {
+            LinkedHashMap res = new LinkedHashMap(items.size() * 4 / 3, 1f);
+            for (KeyValuePair item : items) {
+                Object key = ensureEvaled(item.key);
+                if (key == null) {
+                    throw new _ObjectBuilderSettingEvaluationException("Map can't use null as key.");
+                }
+                res.put(key, ensureEvaled(item.value));
+            }
+            return res;
+        }
+        
+    }
+    
+    private static class KeyValuePair {
+        private final Object key;
+        private final Object value;
+        
+        public KeyValuePair(Object key, Object value) {
+            this.key = key;
+            this.value = value;
+        }
+    }
+    
+    private class BuilderCallExpression extends ExpressionWithParameters {
+        private String className;
+        private boolean canBeStaticField;
+        private boolean mustBeStaticField;
+        
+        @Override
+        Object eval() throws _ObjectBuilderSettingEvaluationException {
+            if (mustBeStaticField) {
+                if (!canBeStaticField) {
+                    throw new BugException();
+                }
+                return getStaticFieldValue(className);
+            }
+            
+            Class cl;
+            
+            boolean clIsBuilderClass;
+            try {
+                cl = _ClassUtil.forName(className + BUILDER_CLASS_POSTFIX_1);
+                clIsBuilderClass = true;
+            } catch (ClassNotFoundException eIgnored) {
+                try {
+                    cl = _ClassUtil.forName(className + BUILDER_CLASS_POSTFIX_2);
+                    clIsBuilderClass = true;
+                } catch (ClassNotFoundException e) {
+                    clIsBuilderClass = false;
+                    try {
+                        cl = _ClassUtil.forName(className);
+                    } catch (Exception e2) {
+                        boolean failedToGetAsStaticField;
+                        if (canBeStaticField) {
+                            // Try to interpret className as static filed:
+                            try {
+                                return getStaticFieldValue(className);
+                            } catch (_ObjectBuilderSettingEvaluationException e3) {
+                                // Suppress it
+                                failedToGetAsStaticField = true;
+                            }
+                        } else {
+                            failedToGetAsStaticField = false;
+                        }
+                        throw new _ObjectBuilderSettingEvaluationException(
+                                "Failed to get class " + _StringUtil.jQuote(className)
+                                        + (failedToGetAsStaticField ? " (also failed to resolve name as static field)" : "")
+                                        + ".",
+                                e2);
+                    }
+                }
+            }
+            
+            if (!clIsBuilderClass && hasNoParameters()) {
+                try {
+                    Field f = cl.getField(INSTANCE_FIELD_NAME);
+                    if ((f.getModifiers() & (Modifier.PUBLIC | Modifier.STATIC))
+                            == (Modifier.PUBLIC | Modifier.STATIC)) {
+                        return f.get(null);
+                    }
+                } catch (NoSuchFieldException e) {
+                    // Expected
+                } catch (Exception e) {
+                    throw new _ObjectBuilderSettingEvaluationException(
+                            "Error when trying to access " + _StringUtil.jQuote(className) + "."
+                            + INSTANCE_FIELD_NAME, e);
+                }
+            }
+            
+            // Create the object to return or its builder:
+            Object constructorResult = callConstructor(cl);
+            
+            // Named parameters will set JavaBeans properties:
+            setJavaBeanProperties(constructorResult, namedParamNames, namedParamValues);
+
+            return clIsBuilderClass ? callBuild(constructorResult) : constructorResult;
+        }
+        
+        private Object getStaticFieldValue(String dottedName) throws _ObjectBuilderSettingEvaluationException {
+            int lastDotIdx = dottedName.lastIndexOf('.');
+            if (lastDotIdx == -1) {
+                throw new IllegalArgumentException();
+            }
+            String className = shorthandToFullQualified(dottedName.substring(0, lastDotIdx));
+            String fieldName = dottedName.substring(lastDotIdx + 1);
+
+            Class<?> cl;
+            try {
+                cl = _ClassUtil.forName(className);
+            } catch (Exception e) {
+                throw new _ObjectBuilderSettingEvaluationException(
+                        "Failed to get field's parent class, " + _StringUtil.jQuote(className) + ".",
+                        e);
+            }
+            
+            Field field;
+            try {
+                field = cl.getField(fieldName);
+            } catch (Exception e) {
+                throw new _ObjectBuilderSettingEvaluationException(
+                        "Failed to get field " + _StringUtil.jQuote(fieldName) + " from class "
+                        + _StringUtil.jQuote(className) + ".",
+                        e);
+            }
+            
+            if ((field.getModifiers() & Modifier.STATIC) == 0) {
+                throw new _ObjectBuilderSettingEvaluationException("Referred field isn't static: " + field);
+            }
+            if ((field.getModifiers() & Modifier.PUBLIC) == 0) {
+                throw new _ObjectBuilderSettingEvaluationException("Referred field isn't public: " + field);
+            }
+
+            if (field.getName().equals(INSTANCE_FIELD_NAME)) {
+                throw new _ObjectBuilderSettingEvaluationException(
+                        "The " + INSTANCE_FIELD_NAME + " field is only accessible through pseudo-constructor call: "
+                        + className + "()");
+            }
+            
+            try {
+                return field.get(null);
+            } catch (Exception e) {
+                throw new _ObjectBuilderSettingEvaluationException("Failed to get field value: " + field, e);
+            }
+        }
+
+        private Object callConstructor(Class cl)
+                throws _ObjectBuilderSettingEvaluationException {
+            if (hasNoParameters()) {
+                // No need to invoke ObjectWrapper
+                try {
+                    return cl.newInstance();
+                } catch (Exception e) {
+                    throw new _ObjectBuilderSettingEvaluationException(
+                            "Failed to call " + cl.getName() + " 0-argument constructor", e);
+                }
+            } else {
+                DefaultObjectWrapper ow = env.getObjectWrapper();
+                List/*<TemplateModel>*/ tmArgs = new ArrayList(positionalParamValues.size());
+                for (int i = 0; i < positionalParamValues.size(); i++) {
+                    try {
+                        tmArgs.add(ow.wrap(positionalParamValues.get(i)));
+                    } catch (TemplateModelException e) {
+                        throw new _ObjectBuilderSettingEvaluationException("Failed to wrap arg #" + (i + 1), e);
+                    }
+                }
+                try {
+                    return ow.newInstance(cl, tmArgs);
+                } catch (Exception e) {
+                    throw new _ObjectBuilderSettingEvaluationException(
+                            "Failed to call " + cl.getName() + " constructor", e);
+                }
+            }
+        }
+
+        private Object callBuild(Object constructorResult)
+                throws _ObjectBuilderSettingEvaluationException {
+            final Class cl = constructorResult.getClass();
+            Method buildMethod; 
+            try {
+                buildMethod = constructorResult.getClass().getMethod(BUILD_METHOD_NAME, (Class[]) null);
+            } catch (NoSuchMethodException e) {
+                throw new _ObjectBuilderSettingEvaluationException("The " + cl.getName()
+                        + " builder class must have a public " + BUILD_METHOD_NAME + "() method", e);
+            } catch (Exception e) {
+                throw new _ObjectBuilderSettingEvaluationException("Failed to get the " + BUILD_METHOD_NAME
+                        + "() method of the " + cl.getName() + " builder class", e);
+            }
+            
+            try {
+                return buildMethod.invoke(constructorResult, (Object[]) null);
+            } catch (Exception e) {
+                Throwable cause;
+                if (e instanceof InvocationTargetException) {
+                    cause = ((InvocationTargetException) e).getTargetException();
+                } else {
+                    cause = e;
+                }
+                throw new _ObjectBuilderSettingEvaluationException("Failed to call " + BUILD_METHOD_NAME
+                        + "() method on " + cl.getName() + " instance", cause);
+            }
+        }
+
+        private boolean hasNoParameters() {
+            return positionalParamValues.isEmpty() && namedParamValues.isEmpty();
+        }
+
+        @Override
+        protected boolean getAllowPositionalParameters() {
+            return true;
+        }
+        
+    }
+    
+    private class PropertyAssignmentsExpression extends ExpressionWithParameters {
+        
+        private final Object bean;
+        
+        public PropertyAssignmentsExpression(Object bean) {
+            this.bean = bean;
+        }
+
+        @Override
+        Object eval() throws _ObjectBuilderSettingEvaluationException {
+            setJavaBeanProperties(bean, namedParamNames, namedParamValues);
+            return bean;
+        }
+
+        @Override
+        protected boolean getAllowPositionalParameters() {
+            return false;
+        }
+        
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_SettingEvaluationEnvironment.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_SettingEvaluationEnvironment.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_SettingEvaluationEnvironment.java
new file mode 100644
index 0000000..9501185
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_SettingEvaluationEnvironment.java
@@ -0,0 +1,61 @@
+/*
+ * 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.util.Properties;
+
+import org.apache.freemarker.core.model.impl.DefaultObjectWrapper;
+
+/**
+ * Don't use this; used internally by FreeMarker, might changes without notice.
+ * The runtime environment used during the evaluation of configuration {@link Properties}.
+ */
+public class _SettingEvaluationEnvironment {
+    
+    private static final ThreadLocal CURRENT = new ThreadLocal();
+
+    private DefaultObjectWrapper objectWrapper;
+    
+    public static _SettingEvaluationEnvironment getCurrent() {
+        Object r = CURRENT.get();
+        if (r != null) {
+            return (_SettingEvaluationEnvironment) r;
+        }
+        return new _SettingEvaluationEnvironment();
+    }
+    
+    public static _SettingEvaluationEnvironment startScope() {
+        Object previous = CURRENT.get();
+        CURRENT.set(new _SettingEvaluationEnvironment());
+        return (_SettingEvaluationEnvironment) previous;
+    }
+    
+    public static void endScope(_SettingEvaluationEnvironment previous) {
+        CURRENT.set(previous);
+    }
+
+    public DefaultObjectWrapper getObjectWrapper() {
+        if (objectWrapper == null) {
+            objectWrapper = new DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
+        }
+        return objectWrapper;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_TemplateModelException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_TemplateModelException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_TemplateModelException.java
new file mode 100644
index 0000000..76e9d2b
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_TemplateModelException.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.util._ClassUtil;
+
+public class _TemplateModelException extends TemplateModelException {
+
+    // Note: On Java 5 we will use `String descPart1, Object... furtherDescParts` instead of `Object[] descriptionParts`
+    //       and `String description`. That's why these are at the end of the parameter list.
+    
+    // -----------------------------------------------------------------------------------------------------------------
+    // Permutation group:
+    
+    public _TemplateModelException(String description) {
+        super(description);
+    }
+
+    // -----------------------------------------------------------------------------------------------------------------
+    // Permutation group:
+
+    public _TemplateModelException(Throwable cause, String description) {
+        this(cause, null, description);
+    }
+
+    public _TemplateModelException(Environment env, String description) {
+        this((Throwable) null, env, description);
+    }
+    
+    public _TemplateModelException(Throwable cause, Environment env) {
+        this(cause, env, (String) null);
+    }
+
+    public _TemplateModelException(Throwable cause) {
+        this(cause, null, (String) null);
+    }
+    
+    public _TemplateModelException(Throwable cause, Environment env, String description) {
+        super(cause, env, description, true);
+    }
+
+    // -----------------------------------------------------------------------------------------------------------------
+    // Permutation group:
+    
+    public _TemplateModelException(_ErrorDescriptionBuilder description) {
+        this(null, description);
+    }
+
+    public _TemplateModelException(Environment env, _ErrorDescriptionBuilder description) {
+        this(null, env, description);
+    }
+
+    public _TemplateModelException(Throwable cause, Environment env, _ErrorDescriptionBuilder description) {
+        super(cause, env, description, true);
+    }
+
+    // -----------------------------------------------------------------------------------------------------------------
+    // Permutation group:
+    
+    public _TemplateModelException(Object... descriptionParts) {
+        this((Environment) null, descriptionParts);
+    }
+
+    public _TemplateModelException(Environment env, Object... descriptionParts) {
+        this((Throwable) null, env, descriptionParts);
+    }
+
+    public _TemplateModelException(Throwable cause, Object... descriptionParts) {
+        this(cause, null, descriptionParts);
+    }
+
+    public _TemplateModelException(Throwable cause, Environment env, Object... descriptionParts) {
+        super(cause, env, new _ErrorDescriptionBuilder(descriptionParts), true);
+    }
+
+    // -----------------------------------------------------------------------------------------------------------------
+    // Permutation group:
+    
+    public _TemplateModelException(ASTExpression blamed, Object... descriptionParts) {
+        this(blamed, null, descriptionParts);
+    }
+
+    public _TemplateModelException(ASTExpression blamed, Environment env, Object... descriptionParts) {
+        this(blamed, null, env, descriptionParts);
+    }
+
+    public _TemplateModelException(ASTExpression blamed, Throwable cause, Environment env, Object... descriptionParts) {
+        super(cause, env, new _ErrorDescriptionBuilder(descriptionParts).blame(blamed), true);
+    }
+
+    // -----------------------------------------------------------------------------------------------------------------
+    // Permutation group:
+    
+    public _TemplateModelException(ASTExpression blamed, String description) {
+        this(blamed, null, description);
+    }
+
+    public _TemplateModelException(ASTExpression blamed, Environment env, String description) {
+        this(blamed, null, env, description);
+    }
+
+    public _TemplateModelException(ASTExpression blamed, Throwable cause, Environment env, String description) {
+        super(cause, env, new _ErrorDescriptionBuilder(description).blame(blamed), true);
+    }
+
+    static Object[] modelHasStoredNullDescription(Class expected, TemplateModel model) {
+        return new Object[] {
+                "The FreeMarker value exists, but has nothing inside it; the TemplateModel object (class: ",
+                model.getClass().getName(), ") has returned a null",
+                (expected != null ? new Object[] { " instead of a ", _ClassUtil.getShortClassName(expected) } : ""),
+                ". This is possibly a bug in the non-FreeMarker code that builds the data-model." };
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_TimeZoneBuilder.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_TimeZoneBuilder.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_TimeZoneBuilder.java
new file mode 100644
index 0000000..b923b3c
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_TimeZoneBuilder.java
@@ -0,0 +1,43 @@
+/*
+ * 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.util.TimeZone;
+
+/**
+ * For internal use only; don't depend on this, there's no backward compatibility guarantee at all!
+ */
+public class _TimeZoneBuilder {
+
+    private final String timeZoneId;
+
+    public _TimeZoneBuilder(String timeZoneId) {
+        this.timeZoneId = timeZoneId;
+    }
+
+    public TimeZone build() {
+        TimeZone timeZone = TimeZone.getTimeZone(timeZoneId);
+        if (timeZone.getID().equals("GMT") && !timeZoneId.equals("GMT") && !timeZoneId.equals("UTC")
+                && !timeZoneId.equals("GMT+00") && !timeZoneId.equals("GMT+00:00") && !timeZoneId.equals("GMT+0000")) {
+            throw new IllegalArgumentException("Unrecognized time zone: " + timeZoneId);
+        }
+        return timeZone;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_UnexpectedTypeErrorExplainerTemplateModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_UnexpectedTypeErrorExplainerTemplateModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_UnexpectedTypeErrorExplainerTemplateModel.java
new file mode 100644
index 0000000..56481b8
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_UnexpectedTypeErrorExplainerTemplateModel.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateModel;
+
+/**
+ * Don't use this; used internally by FreeMarker, might changes without notice.
+ * 
+ * <p>Implemented by {@link TemplateModel}-s that can explain why they don't implement a certain type. 
+ * */
+public interface _UnexpectedTypeErrorExplainerTemplateModel extends TemplateModel {
+
+    /**
+     * @return A single {@link _ErrorDescriptionBuilder} tip, or {@code null}.
+     */
+    Object[] explainTypeError(Class[]/*<? extends TemplateModel>*/ expectedClasses);
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/ArithmeticEngine.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/ArithmeticEngine.java b/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/ArithmeticEngine.java
new file mode 100644
index 0000000..afe22be
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/ArithmeticEngine.java
@@ -0,0 +1,92 @@
+/*
+ * 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.arithmetic;
+
+import java.math.BigDecimal;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.TemplateException;
+
+/**
+ * Implements the arithmetic operations executed by the template language; see
+ * {@link Configuration#getArithmeticEngine()}.
+ */
+public abstract class ArithmeticEngine {
+
+    public abstract int compareNumbers(Number first, Number second) throws TemplateException;
+    public abstract Number add(Number first, Number second) throws TemplateException;
+    public abstract Number subtract(Number first, Number second) throws TemplateException;
+    public abstract Number multiply(Number first, Number second) throws TemplateException;
+    public abstract Number divide(Number first, Number second) throws TemplateException;
+    public abstract Number modulus(Number first, Number second) throws TemplateException;
+    // [FM3] Add negate (should keep the Number type even for BigDecimalArithmeticEngine, unlike multiply). Then fix
+    // the negate operation in the template language.
+
+    /**
+     * Should be able to parse all FTL numerical literals, Java Double toString results, and XML Schema numbers.
+     * This means these should be parsed successfully, except if the arithmetical engine
+     * couldn't support the resulting value anyway (such as NaN, infinite, even non-integers):
+     * {@code -123.45}, {@code 1.5e3}, {@code 1.5E3}, {@code 0005}, {@code +0}, {@code -0}, {@code NaN},
+     * {@code INF}, {@code -INF}, {@code Infinity}, {@code -Infinity}. 
+     */    
+    public abstract Number toNumber(String s);
+
+    protected int minScale = 12;
+    protected int maxScale = 12;
+    protected int roundingPolicy = BigDecimal.ROUND_HALF_UP;
+
+    /**
+     * Sets the minimal scale to use when dividing BigDecimal numbers. Default
+     * value is 12.
+     */
+    public void setMinScale(int minScale) {
+        if (minScale < 0) {
+            throw new IllegalArgumentException("minScale < 0");
+        }
+        this.minScale = minScale;
+    }
+    
+    /**
+     * Sets the maximal scale to use when multiplying BigDecimal numbers. 
+     * Default value is 100.
+     */
+    public void setMaxScale(int maxScale) {
+        if (maxScale < minScale) {
+            throw new IllegalArgumentException("maxScale < minScale");
+        }
+        this.maxScale = maxScale;
+    }
+
+    public void setRoundingPolicy(int roundingPolicy) {
+        if (roundingPolicy != BigDecimal.ROUND_CEILING
+            && roundingPolicy != BigDecimal.ROUND_DOWN
+            && roundingPolicy != BigDecimal.ROUND_FLOOR
+            && roundingPolicy != BigDecimal.ROUND_HALF_DOWN
+            && roundingPolicy != BigDecimal.ROUND_HALF_EVEN
+            && roundingPolicy != BigDecimal.ROUND_HALF_UP
+            && roundingPolicy != BigDecimal.ROUND_UNNECESSARY
+            && roundingPolicy != BigDecimal.ROUND_UP) {
+            throw new IllegalArgumentException("invalid rounding policy");        
+        }
+        
+        this.roundingPolicy = roundingPolicy;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/impl/BigDecimalArithmeticEngine.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/impl/BigDecimalArithmeticEngine.java b/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/impl/BigDecimalArithmeticEngine.java
new file mode 100644
index 0000000..b022f74
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/impl/BigDecimalArithmeticEngine.java
@@ -0,0 +1,107 @@
+/*
+ * 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.arithmetic.impl;
+
+import java.math.BigDecimal;
+
+import org.apache.freemarker.core.arithmetic.ArithmeticEngine;
+import org.apache.freemarker.core.util._NumberUtil;
+
+/**
+ * Arithmetic engine that converts all numbers to {@link BigDecimal} and
+ * then operates on them. This is FreeMarker's default arithmetic engine.
+ */
+public class BigDecimalArithmeticEngine extends ArithmeticEngine {
+
+    public static final BigDecimalArithmeticEngine INSTANCE = new BigDecimalArithmeticEngine();
+
+    protected BigDecimalArithmeticEngine() {
+        //
+    }
+
+    @Override
+    public int compareNumbers(Number first, Number second) {
+        // We try to find the result based on the sign (+/-/0) first, because:
+        // - It's much faster than converting to BigDecial, and comparing to 0 is the most common comparison.
+        // - It doesn't require any type conversions, and thus things like "Infinity > 0" won't fail.
+        int firstSignum = _NumberUtil.getSignum(first);
+        int secondSignum = _NumberUtil.getSignum(second);
+        if (firstSignum != secondSignum) {
+            return firstSignum < secondSignum ? -1 : (firstSignum > secondSignum ? 1 : 0);
+        } else if (firstSignum == 0 && secondSignum == 0) {
+            return 0;
+        } else {
+            BigDecimal left = _NumberUtil.toBigDecimal(first);
+            BigDecimal right = _NumberUtil.toBigDecimal(second);
+            return left.compareTo(right);
+        }
+    }
+
+    @Override
+    public Number add(Number first, Number second) {
+        BigDecimal left = _NumberUtil.toBigDecimal(first);
+        BigDecimal right = _NumberUtil.toBigDecimal(second);
+        return left.add(right);
+    }
+
+    @Override
+    public Number subtract(Number first, Number second) {
+        BigDecimal left = _NumberUtil.toBigDecimal(first);
+        BigDecimal right = _NumberUtil.toBigDecimal(second);
+        return left.subtract(right);
+    }
+
+    @Override
+    public Number multiply(Number first, Number second) {
+        BigDecimal left = _NumberUtil.toBigDecimal(first);
+        BigDecimal right = _NumberUtil.toBigDecimal(second);
+        BigDecimal result = left.multiply(right);
+        if (result.scale() > maxScale) {
+            result = result.setScale(maxScale, roundingPolicy);
+        }
+        return result;
+    }
+
+    @Override
+    public Number divide(Number first, Number second) {
+        BigDecimal left = _NumberUtil.toBigDecimal(first);
+        BigDecimal right = _NumberUtil.toBigDecimal(second);
+        return divide(left, right);
+    }
+
+    @Override
+    public Number modulus(Number first, Number second) {
+        long left = first.longValue();
+        long right = second.longValue();
+        return Long.valueOf(left % right);
+    }
+
+    @Override
+    public Number toNumber(String s) {
+        return _NumberUtil.toBigDecimalOrDouble(s);
+    }
+
+    private BigDecimal divide(BigDecimal left, BigDecimal right) {
+        int scale1 = left.scale();
+        int scale2 = right.scale();
+        int scale = Math.max(scale1, scale2);
+        scale = Math.max(minScale, scale);
+        return left.divide(right, scale, roundingPolicy);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/impl/ConservativeArithmeticEngine.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/impl/ConservativeArithmeticEngine.java b/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/impl/ConservativeArithmeticEngine.java
new file mode 100644
index 0000000..12c27d9
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/impl/ConservativeArithmeticEngine.java
@@ -0,0 +1,381 @@
+/*
+ * 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.arithmetic.impl;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core._MiscTemplateException;
+import org.apache.freemarker.core.arithmetic.ArithmeticEngine;
+import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.util._NumberUtil;
+
+/**
+ * Arithmetic engine that uses (more-or-less) the widening conversions of
+ * Java language to determine the type of result of operation, instead of
+ * converting everything to BigDecimal up front.
+ * <p>
+ * Widening conversions occur in following situations:
+ * <ul>
+ * <li>byte and short are always widened to int (alike to Java language).</li>
+ * <li>To preserve magnitude: when operands are of different types, the
+ * result type is the type of the wider operand.</li>
+ * <li>to avoid overflows: if add, subtract, or multiply would overflow on
+ * integer types, the result is widened from int to long, or from long to
+ * BigInteger.</li>
+ * <li>to preserve fractional part: if a division of integer types would
+ * have a fractional part, int and long are converted to double, and
+ * BigInteger is converted to BigDecimal. An operation on a float and a
+ * long results in a double. An operation on a float or double and a
+ * BigInteger results in a BigDecimal.</li>
+ * </ul>
+ */
+// [FM3] Review
+public class ConservativeArithmeticEngine extends ArithmeticEngine {
+
+    public static final ConservativeArithmeticEngine INSTANCE = new ConservativeArithmeticEngine();
+
+    private static final int INTEGER = 0;
+    private static final int LONG = 1;
+    private static final int FLOAT = 2;
+    private static final int DOUBLE = 3;
+    private static final int BIG_INTEGER = 4;
+    private static final int BIG_DECIMAL = 5;
+
+    private static final Map classCodes = createClassCodesMap();
+
+    protected ConservativeArithmeticEngine() {
+        //
+    }
+
+    @Override
+    public int compareNumbers(Number first, Number second) throws TemplateException {
+        switch (getCommonClassCode(first, second)) {
+            case INTEGER: {
+                int n1 = first.intValue();
+                int n2 = second.intValue();
+                return  n1 < n2 ? -1 : (n1 == n2 ? 0 : 1);
+            }
+            case LONG: {
+                long n1 = first.longValue();
+                long n2 = second.longValue();
+                return  n1 < n2 ? -1 : (n1 == n2 ? 0 : 1);
+            }
+            case FLOAT: {
+                float n1 = first.floatValue();
+                float n2 = second.floatValue();
+                return  n1 < n2 ? -1 : (n1 == n2 ? 0 : 1);
+            }
+            case DOUBLE: {
+                double n1 = first.doubleValue();
+                double n2 = second.doubleValue();
+                return  n1 < n2 ? -1 : (n1 == n2 ? 0 : 1);
+            }
+            case BIG_INTEGER: {
+                BigInteger n1 = toBigInteger(first);
+                BigInteger n2 = toBigInteger(second);
+                return n1.compareTo(n2);
+            }
+            case BIG_DECIMAL: {
+                BigDecimal n1 = _NumberUtil.toBigDecimal(first);
+                BigDecimal n2 = _NumberUtil.toBigDecimal(second);
+                return n1.compareTo(n2);
+            }
+        }
+        // Make the compiler happy. getCommonClassCode() is guaranteed to
+        // return only above codes, or throw an exception.
+        throw new Error();
+    }
+
+    @Override
+    public Number add(Number first, Number second) throws TemplateException {
+        switch(getCommonClassCode(first, second)) {
+            case INTEGER: {
+                int n1 = first.intValue();
+                int n2 = second.intValue();
+                int n = n1 + n2;
+                return
+                    ((n ^ n1) < 0 && (n ^ n2) < 0) // overflow check
+                    ? Long.valueOf(((long) n1) + n2)
+                    : Integer.valueOf(n);
+            }
+            case LONG: {
+                long n1 = first.longValue();
+                long n2 = second.longValue();
+                long n = n1 + n2;
+                return
+                    ((n ^ n1) < 0 && (n ^ n2) < 0) // overflow check
+                    ? toBigInteger(first).add(toBigInteger(second))
+                    : Long.valueOf(n);
+            }
+            case FLOAT: {
+                return Float.valueOf(first.floatValue() + second.floatValue());
+            }
+            case DOUBLE: {
+                return Double.valueOf(first.doubleValue() + second.doubleValue());
+            }
+            case BIG_INTEGER: {
+                BigInteger n1 = toBigInteger(first);
+                BigInteger n2 = toBigInteger(second);
+                return n1.add(n2);
+            }
+            case BIG_DECIMAL: {
+                BigDecimal n1 = _NumberUtil.toBigDecimal(first);
+                BigDecimal n2 = _NumberUtil.toBigDecimal(second);
+                return n1.add(n2);
+            }
+        }
+        // Make the compiler happy. getCommonClassCode() is guaranteed to
+        // return only above codes, or throw an exception.
+        throw new Error();
+    }
+
+    @Override
+    public Number subtract(Number first, Number second) throws TemplateException {
+        switch(getCommonClassCode(first, second)) {
+            case INTEGER: {
+                int n1 = first.intValue();
+                int n2 = second.intValue();
+                int n = n1 - n2;
+                return
+                    ((n ^ n1) < 0 && (n ^ ~n2) < 0) // overflow check
+                    ? Long.valueOf(((long) n1) - n2)
+                    : Integer.valueOf(n);
+            }
+            case LONG: {
+                long n1 = first.longValue();
+                long n2 = second.longValue();
+                long n = n1 - n2;
+                return
+                    ((n ^ n1) < 0 && (n ^ ~n2) < 0) // overflow check
+                    ? toBigInteger(first).subtract(toBigInteger(second))
+                    : Long.valueOf(n);
+            }
+            case FLOAT: {
+                return Float.valueOf(first.floatValue() - second.floatValue());
+            }
+            case DOUBLE: {
+                return Double.valueOf(first.doubleValue() - second.doubleValue());
+            }
+            case BIG_INTEGER: {
+                BigInteger n1 = toBigInteger(first);
+                BigInteger n2 = toBigInteger(second);
+                return n1.subtract(n2);
+            }
+            case BIG_DECIMAL: {
+                BigDecimal n1 = _NumberUtil.toBigDecimal(first);
+                BigDecimal n2 = _NumberUtil.toBigDecimal(second);
+                return n1.subtract(n2);
+            }
+        }
+        // Make the compiler happy. getCommonClassCode() is guaranteed to
+        // return only above codes, or throw an exception.
+        throw new Error();
+    }
+
+    @Override
+    public Number multiply(Number first, Number second) throws TemplateException {
+        switch(getCommonClassCode(first, second)) {
+            case INTEGER: {
+                int n1 = first.intValue();
+                int n2 = second.intValue();
+                int n = n1 * n2;
+                return
+                    n1 == 0 || n / n1 == n2 // overflow check
+                    ? Integer.valueOf(n)
+                    : Long.valueOf(((long) n1) * n2);
+            }
+            case LONG: {
+                long n1 = first.longValue();
+                long n2 = second.longValue();
+                long n = n1 * n2;
+                return
+                    n1 == 0L || n / n1 == n2 // overflow check
+                    ? Long.valueOf(n)
+                    : toBigInteger(first).multiply(toBigInteger(second));
+            }
+            case FLOAT: {
+                return Float.valueOf(first.floatValue() * second.floatValue());
+            }
+            case DOUBLE: {
+                return Double.valueOf(first.doubleValue() * second.doubleValue());
+            }
+            case BIG_INTEGER: {
+                BigInteger n1 = toBigInteger(first);
+                BigInteger n2 = toBigInteger(second);
+                return n1.multiply(n2);
+            }
+            case BIG_DECIMAL: {
+                BigDecimal n1 = _NumberUtil.toBigDecimal(first);
+                BigDecimal n2 = _NumberUtil.toBigDecimal(second);
+                BigDecimal r = n1.multiply(n2);
+                return r.scale() > maxScale ? r.setScale(maxScale, roundingPolicy) : r;
+            }
+        }
+        // Make the compiler happy. getCommonClassCode() is guaranteed to
+        // return only above codes, or throw an exception.
+        throw new Error();
+    }
+
+    @Override
+    public Number divide(Number first, Number second) throws TemplateException {
+        switch(getCommonClassCode(first, second)) {
+            case INTEGER: {
+                int n1 = first.intValue();
+                int n2 = second.intValue();
+                if (n1 % n2 == 0) {
+                    return Integer.valueOf(n1 / n2);
+                }
+                return Double.valueOf(((double) n1) / n2);
+            }
+            case LONG: {
+                long n1 = first.longValue();
+                long n2 = second.longValue();
+                if (n1 % n2 == 0) {
+                    return Long.valueOf(n1 / n2);
+                }
+                return Double.valueOf(((double) n1) / n2);
+            }
+            case FLOAT: {
+                return Float.valueOf(first.floatValue() / second.floatValue());
+            }
+            case DOUBLE: {
+                return Double.valueOf(first.doubleValue() / second.doubleValue());
+            }
+            case BIG_INTEGER: {
+                BigInteger n1 = toBigInteger(first);
+                BigInteger n2 = toBigInteger(second);
+                BigInteger[] divmod = n1.divideAndRemainder(n2);
+                if (divmod[1].equals(BigInteger.ZERO)) {
+                    return divmod[0];
+                } else {
+                    BigDecimal bd1 = new BigDecimal(n1);
+                    BigDecimal bd2 = new BigDecimal(n2);
+                    return bd1.divide(bd2, minScale, roundingPolicy);
+                }
+            }
+            case BIG_DECIMAL: {
+                BigDecimal n1 = _NumberUtil.toBigDecimal(first);
+                BigDecimal n2 = _NumberUtil.toBigDecimal(second);
+                int scale1 = n1.scale();
+                int scale2 = n2.scale();
+                int scale = Math.max(scale1, scale2);
+                scale = Math.max(minScale, scale);
+                return n1.divide(n2, scale, roundingPolicy);
+            }
+        }
+        // Make the compiler happy. getCommonClassCode() is guaranteed to
+        // return only above codes, or throw an exception.
+        throw new Error();
+    }
+
+    @Override
+    public Number modulus(Number first, Number second) throws TemplateException {
+        switch(getCommonClassCode(first, second)) {
+            case INTEGER: {
+                return Integer.valueOf(first.intValue() % second.intValue());
+            }
+            case LONG: {
+                return Long.valueOf(first.longValue() % second.longValue());
+            }
+            case FLOAT: {
+                return Float.valueOf(first.floatValue() % second.floatValue());
+            }
+            case DOUBLE: {
+                return Double.valueOf(first.doubleValue() % second.doubleValue());
+            }
+            case BIG_INTEGER: {
+                BigInteger n1 = toBigInteger(first);
+                BigInteger n2 = toBigInteger(second);
+                return n1.mod(n2);
+            }
+            case BIG_DECIMAL: {
+                throw new _MiscTemplateException("Can't calculate remainder on BigDecimals");
+            }
+        }
+        // Make the compiler happy. getCommonClassCode() is guaranteed to
+        // return only above codes, or throw an exception.
+        throw new BugException();
+    }
+
+    @Override
+    public Number toNumber(String s) {
+        Number n = _NumberUtil.toBigDecimalOrDouble(s);
+        return n instanceof BigDecimal ? _NumberUtil.optimizeNumberRepresentation(n) : n;
+    }
+
+    private static Map createClassCodesMap() {
+        Map map = new HashMap(17);
+        Integer intcode = Integer.valueOf(INTEGER);
+        map.put(Byte.class, intcode);
+        map.put(Short.class, intcode);
+        map.put(Integer.class, intcode);
+        map.put(Long.class, Integer.valueOf(LONG));
+        map.put(Float.class, Integer.valueOf(FLOAT));
+        map.put(Double.class, Integer.valueOf(DOUBLE));
+        map.put(BigInteger.class, Integer.valueOf(BIG_INTEGER));
+        map.put(BigDecimal.class, Integer.valueOf(BIG_DECIMAL));
+        return map;
+    }
+
+    private static int getClassCode(Number num) throws TemplateException {
+        try {
+            return ((Integer) classCodes.get(num.getClass())).intValue();
+        } catch (NullPointerException e) {
+            if (num == null) {
+                throw new _MiscTemplateException("The Number object was null.");
+            } else {
+                throw new _MiscTemplateException("Unknown number type ", num.getClass().getName());
+            }
+        }
+    }
+
+    private static int getCommonClassCode(Number num1, Number num2) throws TemplateException {
+        int c1 = getClassCode(num1);
+        int c2 = getClassCode(num2);
+        int c = c1 > c2 ? c1 : c2;
+        // If BigInteger is combined with a Float or Double, the result is a
+        // BigDecimal instead of BigInteger in order not to lose the
+        // fractional parts. If Float is combined with Long, the result is a
+        // Double instead of Float to preserve the bigger bit width.
+        switch (c) {
+            case FLOAT: {
+                if ((c1 < c2 ? c1 : c2) == LONG) {
+                    return DOUBLE;
+                }
+                break;
+            }
+            case BIG_INTEGER: {
+                int min = c1 < c2 ? c1 : c2;
+                if (min == DOUBLE || min == FLOAT) {
+                    return BIG_DECIMAL;
+                }
+                break;
+            }
+        }
+        return c;
+    }
+
+    private static BigInteger toBigInteger(Number num) {
+        return num instanceof BigInteger ? (BigInteger) num : new BigInteger(num.toString());
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/impl/package.html
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/impl/package.html b/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/impl/package.html
new file mode 100644
index 0000000..65688e2
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/impl/package.html
@@ -0,0 +1,26 @@
+<!--
+  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.
+-->
+<html>
+<head>
+</head>
+<body>
+<p>Arithmetic used in templates: Standard implementations. This package is part of the
+published API, that is, user code can safely depend on it.</p>
+</body>
+</html>

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/package.html
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/package.html b/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/package.html
new file mode 100644
index 0000000..62566ea
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/package.html
@@ -0,0 +1,25 @@
+<!--
+  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.
+-->
+<html>
+<head>
+</head>
+<body>
+<p>Arithmetic used in templates: Base classes/interfaces.</p>
+</body>
+</html>

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/debug/Breakpoint.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/debug/Breakpoint.java b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/Breakpoint.java
new file mode 100644
index 0000000..363d4d8
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/Breakpoint.java
@@ -0,0 +1,83 @@
+/*
+ * 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.debug;
+
+import java.io.Serializable;
+
+/**
+ * Represents a breakpoint location consisting of a template name and a line number.
+ */
+public class Breakpoint implements Serializable, Comparable {
+    private static final long serialVersionUID = 1L;
+
+    private final String templateName;
+    private final int line;
+    
+    /**
+     * Creates a new breakpoint.
+     * @param templateName the name of the template
+     * @param line the line number in the template where to put the breakpoint
+     */
+    public Breakpoint(String templateName, int line) {
+        this.templateName = templateName;
+        this.line = line;
+    }
+
+    /**
+     * Returns the line number of the breakpoint
+     */
+    public int getLine() {
+        return line;
+    }
+    /**
+     * Returns the template name of the breakpoint
+     */
+    public String getTemplateName() {
+        return templateName;
+    }
+
+    @Override
+    public int hashCode() {
+        return templateName.hashCode() + 31 * line;
+    }
+    
+    @Override
+    public boolean equals(Object o) {
+        if (o instanceof Breakpoint) {
+            Breakpoint b = (Breakpoint) o;
+            return b.templateName.equals(templateName) && b.line == line;
+        }
+        return false;
+    }
+    
+    @Override
+    public int compareTo(Object o) {
+        Breakpoint b = (Breakpoint) o;
+        int r = templateName.compareTo(b.templateName);
+        return r == 0 ? line - b.line : r;
+    }
+    
+    /**
+     * Returns the template name and the line number separated with a colon
+     */
+    public String getLocationString() {
+        return templateName + ":" + line;
+    }
+}



[40/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsRegexp.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsRegexp.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsRegexp.java
new file mode 100644
index 0000000..0420d64
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsRegexp.java
@@ -0,0 +1,322 @@
+/*
+ * 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.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateMethodModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.apache.freemarker.core.util._StringUtil;
+
+
+/**
+ * Contains the string built-ins that correspond to basic regular expressions operations.
+ */
+class BuiltInsForStringsRegexp {
+
+    static class groupsBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel targetModel = target.eval(env);
+            assertNonNull(targetModel, env);
+            if (targetModel instanceof RegexMatchModel) {
+                return ((RegexMatchModel) targetModel).getGroups();
+            } else if (targetModel instanceof RegexMatchModel.MatchWithGroups) {
+                return new NativeStringArraySequence(((RegexMatchModel.MatchWithGroups) targetModel).groups);
+
+            } else {
+                throw new UnexpectedTypeException(target, targetModel,
+                        "regular expression matcher",
+                        new Class[] { RegexMatchModel.class, RegexMatchModel.MatchWithGroups.class },
+                        env);
+            }
+        }
+    }
+    
+    static class matchesBI extends BuiltInForString {
+        class MatcherBuilder implements TemplateMethodModel {
+            
+            String matchString;
+            
+            MatcherBuilder(String matchString) throws TemplateModelException {
+                this.matchString = matchString;
+            }
+            
+            @Override
+            public Object exec(List args) throws TemplateModelException {
+                int argCnt = args.size();
+                checkMethodArgCount(argCnt, 1, 2);
+                
+                String patternString = (String) args.get(0);
+                long flags = argCnt > 1 ? RegexpHelper.parseFlagString((String) args.get(1)) : 0;
+                if ((flags & RegexpHelper.RE_FLAG_FIRST_ONLY) != 0) {
+                    RegexpHelper.logFlagWarning("?" + key + " doesn't support the \"f\" flag.");
+                }
+                Pattern pattern = RegexpHelper.getPattern(patternString, (int) flags);
+                return new RegexMatchModel(pattern, matchString);
+            }
+        }
+        
+        @Override
+        TemplateModel calculateResult(String s, Environment env) throws TemplateModelException {
+            return new MatcherBuilder(s);
+        }
+        
+    }
+    
+    static class replace_reBI extends BuiltInForString {
+        
+        class ReplaceMethod implements TemplateMethodModel {
+            private String s;
+
+            ReplaceMethod(String s) {
+                this.s = s;
+            }
+
+            @Override
+            public Object exec(List args) throws TemplateModelException {
+                int argCnt = args.size();
+                checkMethodArgCount(argCnt, 2, 3);
+                String arg1 = (String) args.get(0);
+                String arg2 = (String) args.get(1);
+                long flags = argCnt > 2 ? RegexpHelper.parseFlagString((String) args.get(2)) : 0;
+                String result;
+                if ((flags & RegexpHelper.RE_FLAG_REGEXP) == 0) {
+                    RegexpHelper.checkNonRegexpFlags("replace", flags);
+                    result = _StringUtil.replace(s, arg1, arg2,
+                            (flags & RegexpHelper.RE_FLAG_CASE_INSENSITIVE) != 0,
+                            (flags & RegexpHelper.RE_FLAG_FIRST_ONLY) != 0);
+                } else {
+                    Pattern pattern = RegexpHelper.getPattern(arg1, (int) flags);
+                    Matcher matcher = pattern.matcher(s);
+                    result = (flags & RegexpHelper.RE_FLAG_FIRST_ONLY) != 0
+                            ? matcher.replaceFirst(arg2)
+                            : matcher.replaceAll(arg2);
+                } 
+                return new SimpleScalar(result);
+            }
+
+        }
+        
+        @Override
+        TemplateModel calculateResult(String s, Environment env) throws TemplateModelException {
+            return new ReplaceMethod(s);
+        }
+        
+    }
+    
+    // Represents the match
+  
+    static class RegexMatchModel 
+    implements TemplateBooleanModel, TemplateCollectionModel, TemplateSequenceModel {
+        static class MatchWithGroups implements TemplateScalarModel {
+            final String matchedInputPart;
+            final String[] groups;
+
+            MatchWithGroups(String input, Matcher matcher) {
+                matchedInputPart = input.substring(matcher.start(), matcher.end());
+                final int grpCount = matcher.groupCount() + 1;
+                groups = new String[grpCount];
+                for (int i = 0; i < grpCount; i++) {
+                    groups[i] = matcher.group(i);
+                }
+            }
+            
+            @Override
+            public String getAsString() {
+                return matchedInputPart;
+            }
+        }
+        final Pattern pattern;
+        
+        final String input;
+        private Matcher firedEntireInputMatcher;
+        private Boolean entireInputMatched;
+        
+        private TemplateSequenceModel entireInputMatchGroups;
+        
+        private ArrayList matchingInputParts;
+        
+        RegexMatchModel(Pattern pattern, String input) {
+            this.pattern = pattern;
+            this.input = input;
+        }
+        
+        @Override
+        public TemplateModel get(int i) throws TemplateModelException {
+            ArrayList matchingInputParts = this.matchingInputParts;
+            if (matchingInputParts == null) {
+                matchingInputParts = getMatchingInputPartsAndStoreResults();
+            }
+            return (TemplateModel) matchingInputParts.get(i);
+        }
+        
+        @Override
+        public boolean getAsBoolean() {
+            Boolean result = entireInputMatched;
+            return result != null ? result.booleanValue() : isEntrieInputMatchesAndStoreResults();
+        }
+        
+        TemplateModel getGroups() {
+           TemplateSequenceModel entireInputMatchGroups = this.entireInputMatchGroups;
+           if (entireInputMatchGroups == null) {
+               Matcher t = firedEntireInputMatcher;
+               if (t == null) {
+                   isEntrieInputMatchesAndStoreResults();
+                   t = firedEntireInputMatcher;
+               }
+               final Matcher firedEntireInputMatcher = t;
+               
+                entireInputMatchGroups = new TemplateSequenceModel() {
+                    
+                    @Override
+                    public TemplateModel get(int i) throws TemplateModelException {
+                        try {
+                            // Avoid IndexOutOfBoundsException:
+                            if (i > firedEntireInputMatcher.groupCount()) {
+                                return null;
+                            }
+
+                            return new SimpleScalar(firedEntireInputMatcher.group(i));
+                        } catch (Exception e) {
+                            throw new _TemplateModelException(e, "Failed to read match group");
+                        }
+                    }
+                    
+                    @Override
+                    public int size() throws TemplateModelException {
+                        try {
+                            return firedEntireInputMatcher.groupCount() + 1;
+                        } catch (Exception e) {
+                            throw new _TemplateModelException(e, "Failed to get match group count");
+                        }
+                    }
+                    
+                };
+                this.entireInputMatchGroups = entireInputMatchGroups;
+            }
+            return entireInputMatchGroups;
+        }
+        
+        private ArrayList getMatchingInputPartsAndStoreResults() throws TemplateModelException {
+            ArrayList matchingInputParts = new ArrayList();
+            
+            Matcher matcher = pattern.matcher(input);
+            while (matcher.find()) {
+                matchingInputParts.add(new MatchWithGroups(input, matcher));
+            }
+    
+            this.matchingInputParts = matchingInputParts;
+            return matchingInputParts;
+        }
+        
+        private boolean isEntrieInputMatchesAndStoreResults() {
+            Matcher matcher = pattern.matcher(input);
+            boolean matches = matcher.matches();
+            firedEntireInputMatcher = matcher;
+            entireInputMatched = Boolean.valueOf(matches);
+            return matches;
+        }
+        
+        @Override
+        public TemplateModelIterator iterator() {
+            final ArrayList matchingInputParts = this.matchingInputParts;
+            if (matchingInputParts == null) {
+                final Matcher matcher = pattern.matcher(input);
+                return new TemplateModelIterator() {
+                    
+                    private int nextIdx = 0;
+                    boolean hasFindInfo = matcher.find();
+                    
+                    @Override
+                    public boolean hasNext() {
+                        final ArrayList matchingInputParts = RegexMatchModel.this.matchingInputParts;
+                        if (matchingInputParts == null) {
+                            return hasFindInfo;
+                        } else {
+                            return nextIdx < matchingInputParts.size();
+                        }
+                    }
+                    
+                    @Override
+                    public TemplateModel next() throws TemplateModelException {
+                        final ArrayList matchingInputParts = RegexMatchModel.this.matchingInputParts;
+                        if (matchingInputParts == null) {
+                            if (!hasFindInfo) throw new _TemplateModelException("There were no more matches");
+                            MatchWithGroups result = new MatchWithGroups(input, matcher);
+                            nextIdx++;
+                            hasFindInfo = matcher.find();
+                            return result;
+                        } else {
+                            try {
+                                return (TemplateModel) matchingInputParts.get(nextIdx++);
+                            } catch (IndexOutOfBoundsException e) {
+                                throw new _TemplateModelException(e, "There were no more matches");
+                            }
+                        }
+                    }
+                    
+                };
+            } else {
+                return new TemplateModelIterator() {
+                    
+                    private int nextIdx = 0;
+                    
+                    @Override
+                    public boolean hasNext() {
+                        return nextIdx < matchingInputParts.size();
+                    }
+                    
+                    @Override
+                    public TemplateModel next() throws TemplateModelException {
+                        try {
+                            return (TemplateModel) matchingInputParts.get(nextIdx++);
+                        } catch (IndexOutOfBoundsException e) {
+                            throw new _TemplateModelException(e, "There were no more matches");
+                        }
+                    }
+                };
+            }
+        }
+        
+        @Override
+        public int size() throws TemplateModelException {
+            ArrayList matchingInputParts = this.matchingInputParts;
+            if (matchingInputParts == null) {
+                matchingInputParts = getMatchingInputPartsAndStoreResults();
+            }
+            return matchingInputParts.size();
+        }
+    }
+
+    // Can't be instantiated
+    private BuiltInsForStringsRegexp() { }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsWithParseTimeParameters.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsWithParseTimeParameters.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsWithParseTimeParameters.java
new file mode 100644
index 0000000..1126410
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsWithParseTimeParameters.java
@@ -0,0 +1,157 @@
+/*
+ * 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.util.ArrayList;
+import java.util.List;
+
+import org.apache.freemarker.core.model.TemplateModel;
+
+
+final class BuiltInsWithParseTimeParameters {
+    
+    /**
+     * Behaves similarly to the ternary operator of Java.
+     */
+    static class then_BI extends BuiltInWithParseTimeParameters {
+        
+        private ASTExpression whenTrueExp;
+        private ASTExpression whenFalseExp;
+
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            boolean lho = target.evalToBoolean(env);
+            return (lho ? whenTrueExp : whenFalseExp).evalToNonMissing(env);
+        }
+
+        @Override
+        void bindToParameters(List parameters, Token openParen, Token closeParen) throws ParseException {
+            if (parameters.size() != 2) {
+                throw newArgumentCountException("requires exactly 2", openParen, closeParen);
+            }
+            whenTrueExp = (ASTExpression) parameters.get(0);
+            whenFalseExp = (ASTExpression) parameters.get(1);
+        }
+        
+        @Override
+        protected ASTExpression getArgumentParameterValue(final int argIdx) {
+            switch (argIdx) {
+            case 0: return whenTrueExp;
+            case 1: return whenFalseExp;
+            default: throw new IndexOutOfBoundsException();
+            }
+        }
+
+        @Override
+        protected int getArgumentsCount() {
+            return 2;
+        }
+        
+        @Override
+        protected List getArgumentsAsList() {
+            ArrayList args = new ArrayList(2);
+            args.add(whenTrueExp);
+            args.add(whenFalseExp);
+            return args;
+        }
+        
+        @Override
+        protected void cloneArguments(ASTExpression cloneExp, String replacedIdentifier,
+                ASTExpression replacement, ReplacemenetState replacementState) {
+            then_BI clone = (then_BI) cloneExp;
+            clone.whenTrueExp = whenTrueExp.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState);
+            clone.whenFalseExp = whenFalseExp.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState);
+        }
+        
+    }
+    
+    private BuiltInsWithParseTimeParameters() {
+        // Not to be instantiated
+    }
+
+    static class switch_BI extends BuiltInWithParseTimeParameters {
+        
+        private List/*<ASTExpression>*/ parameters;
+
+        @Override
+        void bindToParameters(List parameters, Token openParen, Token closeParen) throws ParseException {
+            if (parameters.size() < 2) {
+                throw newArgumentCountException("must have at least 2", openParen, closeParen);
+            }
+            this.parameters = parameters;
+        }
+
+        @Override
+        protected List getArgumentsAsList() {
+            return parameters;
+        }
+
+        @Override
+        protected int getArgumentsCount() {
+            return parameters.size();
+        }
+
+        @Override
+        protected ASTExpression getArgumentParameterValue(int argIdx) {
+            return (ASTExpression) parameters.get(argIdx);
+        }
+
+        @Override
+        protected void cloneArguments(ASTExpression clone, String replacedIdentifier, ASTExpression replacement,
+                ReplacemenetState replacementState) {
+            ArrayList parametersClone = new ArrayList(parameters.size());
+            for (int i = 0; i < parameters.size(); i++) {
+                parametersClone.add(((ASTExpression) parameters.get(i))
+                        .deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState));
+            }
+            ((switch_BI) clone).parameters = parametersClone;
+        }
+
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel targetValue = target.evalToNonMissing(env);
+            
+            List parameters = this.parameters;
+            int paramCnt = parameters.size();
+            for (int i = 0; i + 1 < paramCnt; i += 2) {
+                ASTExpression caseExp = (ASTExpression) parameters.get(i);
+                TemplateModel caseValue = caseExp.evalToNonMissing(env);
+                if (_EvalUtil.compare(
+                        targetValue, target,
+                        _EvalUtil.CMP_OP_EQUALS, "==",
+                        caseValue, caseExp,
+                        this, true,
+                        false, false, false,
+                        env)) {
+                    return ((ASTExpression) parameters.get(i + 1)).evalToNonMissing(env);
+                }
+            }
+            
+            if (paramCnt % 2 == 0) {
+                throw new _MiscTemplateException(target,
+                        "The value before ?", key, "(case1, value1, case2, value2, ...) didn't match any of the "
+                        + "case parameters, and there was no default value parameter (an additional last parameter) "
+                        + "eithter. ");
+            }
+            return ((ASTExpression) parameters.get(paramCnt - 1)).evalToNonMissing(env);
+        }
+        
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/CallPlaceCustomDataInitializationException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/CallPlaceCustomDataInitializationException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/CallPlaceCustomDataInitializationException.java
new file mode 100644
index 0000000..ffaa2b0
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/CallPlaceCustomDataInitializationException.java
@@ -0,0 +1,33 @@
+/*
+ * 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;
+
+/**
+ * Thrown by {@link DirectiveCallPlace#getOrCreateCustomData(Object, org.apache.freemarker.core.util.ObjectFactory)}
+ * 
+ * @since 2.3.22
+ */
+public class CallPlaceCustomDataInitializationException extends Exception {
+
+    public CallPlaceCustomDataInitializationException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+}


[23/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultIteratorAdapter.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultIteratorAdapter.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultIteratorAdapter.java
new file mode 100644
index 0000000..60d9243
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultIteratorAdapter.java
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.model.impl;
+
+import java.io.Serializable;
+import java.util.Iterator;
+
+import org.apache.freemarker.core.model.AdapterTemplateModel;
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.ObjectWrapperWithAPISupport;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+import org.apache.freemarker.core.model.TemplateModelWithAPISupport;
+import org.apache.freemarker.core.model.WrapperTemplateModel;
+import org.apache.freemarker.core.model.WrappingTemplateModel;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+/**
+ * Adapts an {@link Iterator} to the corresponding {@link TemplateModel} interface(s), most importantly to
+ * {@link TemplateCollectionModel}. The resulting {@link TemplateCollectionModel} can only be listed (iterated) once.
+ * If the user tries list the variable for a second time, an exception will be thrown instead of silently gettig an
+ * empty (or partial) listing.
+ * 
+ * <p>
+ * Thread safety: A {@link DefaultListAdapter} is as thread-safe as the array that it wraps is. Normally you only
+ * have to consider read-only access, as the FreeMarker template language doesn't allow writing these sequences (though
+ * of course, Java methods called from the template can violate this rule).
+ * 
+ * <p>
+ * This adapter is used by {@link DefaultObjectWrapper} if its {@code useAdaptersForCollections} property is
+ * {@code true}, which is the default when its {@code incompatibleImprovements} property is 2.3.22 or higher.
+ * 
+ * @since 2.3.22
+ */
+public class DefaultIteratorAdapter extends WrappingTemplateModel implements TemplateCollectionModel,
+        AdapterTemplateModel, WrapperTemplateModel, TemplateModelWithAPISupport, Serializable {
+
+    @SuppressFBWarnings(value="SE_BAD_FIELD", justification="We hope it's Seralizable")
+    private final Iterator iterator;
+    private boolean iteratorOwnedBySomeone;
+
+    /**
+     * Factory method for creating new adapter instances.
+     * 
+     * @param iterator
+     *            The iterator to adapt; can't be {@code null}.
+     */
+    public static DefaultIteratorAdapter adapt(Iterator iterator, ObjectWrapper wrapper) {
+        return new DefaultIteratorAdapter(iterator, wrapper);
+    }
+
+    private DefaultIteratorAdapter(Iterator iterator, ObjectWrapper wrapper) {
+        super(wrapper);
+        this.iterator = iterator;
+    }
+
+    @Override
+    public Object getWrappedObject() {
+        return iterator;
+    }
+
+    @Override
+    public Object getAdaptedObject(Class hint) {
+        return getWrappedObject();
+    }
+
+    @Override
+    public TemplateModelIterator iterator() throws TemplateModelException {
+        return new SimpleTemplateModelIterator();
+    }
+
+    @Override
+    public TemplateModel getAPI() throws TemplateModelException {
+        return ((ObjectWrapperWithAPISupport) getObjectWrapper()).wrapAsAPI(iterator);
+    }
+
+    /**
+     * Not thread-safe.
+     */
+    private class SimpleTemplateModelIterator implements TemplateModelIterator {
+
+        private boolean iteratorOwnedByMe;
+
+        @Override
+        public TemplateModel next() throws TemplateModelException {
+            if (!iteratorOwnedByMe) {
+                checkNotOwner();
+                iteratorOwnedBySomeone = true;
+                iteratorOwnedByMe = true;
+            }
+
+            if (!iterator.hasNext()) {
+                throw new TemplateModelException("The collection has no more items.");
+            }
+
+            Object value = iterator.next();
+            return value instanceof TemplateModel ? (TemplateModel) value : wrap(value);
+        }
+
+        @Override
+        public boolean hasNext() throws TemplateModelException {
+            // Calling hasNext may looks safe, but I have met sync. problems.
+            if (!iteratorOwnedByMe) {
+                checkNotOwner();
+            }
+
+            return iterator.hasNext();
+        }
+
+        private void checkNotOwner() throws TemplateModelException {
+            if (iteratorOwnedBySomeone) {
+                throw new TemplateModelException(
+                        "This collection value wraps a java.util.Iterator, thus it can be listed only once.");
+            }
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultListAdapter.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultListAdapter.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultListAdapter.java
new file mode 100644
index 0000000..e58cc5e
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultListAdapter.java
@@ -0,0 +1,123 @@
+/*
+ * 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.io.Serializable;
+import java.util.AbstractSequentialList;
+import java.util.List;
+
+import org.apache.freemarker.core.model.AdapterTemplateModel;
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.ObjectWrapperWithAPISupport;
+import org.apache.freemarker.core.model.RichObjectWrapper;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+import org.apache.freemarker.core.model.TemplateModelWithAPISupport;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.WrapperTemplateModel;
+import org.apache.freemarker.core.model.WrappingTemplateModel;
+
+/**
+ * Adapts a {@link List} to the corresponding {@link TemplateModel} interface(s), most importantly to
+ * {@link TemplateSequenceModel}. If you aren't wrapping an already existing {@link List}, but build a sequence
+ * specifically to be used from a template, also consider using {@link SimpleSequence} (see comparison there).
+ * 
+ * <p>
+ * Thread safety: A {@link DefaultListAdapter} is as thread-safe as the {@link List} that it wraps is. Normally you only
+ * have to consider read-only access, as the FreeMarker template language doesn't allow writing these sequences (though
+ * of course, Java methods called from the template can violate this rule).
+ * 
+ * <p>
+ * This adapter is used by {@link DefaultObjectWrapper} if its {@code useAdaptersForCollections} property is
+ * {@code true}, which is the default when its {@code incompatibleImprovements} property is 2.3.22 or higher.
+ * 
+ * @see SimpleSequence
+ * @see DefaultArrayAdapter
+ * @see TemplateSequenceModel
+ * 
+ * @since 2.3.22
+ */
+public class DefaultListAdapter extends WrappingTemplateModel implements TemplateSequenceModel,
+        AdapterTemplateModel, WrapperTemplateModel, TemplateModelWithAPISupport, Serializable {
+
+    protected final List list;
+
+    /**
+     * Factory method for creating new adapter instances.
+     * 
+     * @param list
+     *            The list to adapt; can't be {@code null}.
+     * @param wrapper
+     *            The {@link ObjectWrapper} used to wrap the items in the array.
+     */
+    public static DefaultListAdapter adapt(List list, RichObjectWrapper wrapper) {
+        // [2.4] DefaultListAdapter should implement TemplateCollectionModelEx, so this choice becomes unnecessary
+        return list instanceof AbstractSequentialList
+                ? new DefaultListAdapterWithCollectionSupport(list, wrapper)
+                : new DefaultListAdapter(list, wrapper);
+    }
+
+    private DefaultListAdapter(List list, RichObjectWrapper wrapper) {
+        super(wrapper);
+        this.list = list;
+    }
+
+    @Override
+    public TemplateModel get(int index) throws TemplateModelException {
+        return index >= 0 && index < list.size() ? wrap(list.get(index)) : null;
+    }
+
+    @Override
+    public int size() throws TemplateModelException {
+        return list.size();
+    }
+
+    @Override
+    public Object getAdaptedObject(Class hint) {
+        return getWrappedObject();
+    }
+
+    @Override
+    public Object getWrappedObject() {
+        return list;
+    }
+
+    private static class DefaultListAdapterWithCollectionSupport extends DefaultListAdapter implements
+            TemplateCollectionModel {
+
+        private DefaultListAdapterWithCollectionSupport(List list, RichObjectWrapper wrapper) {
+            super(list, wrapper);
+        }
+
+        @Override
+        public TemplateModelIterator iterator() throws TemplateModelException {
+            return new DefaultUnassignableIteratorAdapter(list.iterator(), getObjectWrapper());
+        }
+
+    }
+
+    @Override
+    public TemplateModel getAPI() throws TemplateModelException {
+        return ((ObjectWrapperWithAPISupport) getObjectWrapper()).wrapAsAPI(list);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultMapAdapter.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultMapAdapter.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultMapAdapter.java
new file mode 100644
index 0000000..e3b3115
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultMapAdapter.java
@@ -0,0 +1,171 @@
+/*
+ * 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.io.Serializable;
+import java.util.Map;
+import java.util.SortedMap;
+
+import org.apache.freemarker.core._DelayedJQuote;
+import org.apache.freemarker.core._TemplateModelException;
+import org.apache.freemarker.core.model.AdapterTemplateModel;
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.ObjectWrapperWithAPISupport;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateHashModelEx2;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelWithAPISupport;
+import org.apache.freemarker.core.model.WrapperTemplateModel;
+import org.apache.freemarker.core.model.WrappingTemplateModel;
+
+/**
+ * Adapts a {@link Map} to the corresponding {@link TemplateModel} interface(s), most importantly to
+ * {@link TemplateHashModelEx}. If you aren't wrapping an already existing {@link Map}, but build a hash specifically to
+ * be used from a template, also consider using {@link SimpleHash} (see comparison there).
+ * 
+ * <p>
+ * Thread safety: A {@link DefaultMapAdapter} is as thread-safe as the {@link Map} that it wraps is. Normally you only
+ * have to consider read-only access, as the FreeMarker template language doesn't allow writing these hashes (though of
+ * course, Java methods called from the template can violate this rule).
+ * 
+ * <p>
+ * This adapter is used by {@link DefaultObjectWrapper} if its {@code useAdaptersForCollections} property is
+ * {@code true}, which is the default when its {@code incompatibleImprovements} property is 2.3.22 or higher.
+ * 
+ * @since 2.3.22
+ */
+public class DefaultMapAdapter extends WrappingTemplateModel
+        implements TemplateHashModelEx2, AdapterTemplateModel, WrapperTemplateModel, TemplateModelWithAPISupport,
+        Serializable {
+
+    private final Map map;
+
+    /**
+     * Factory method for creating new adapter instances.
+     * 
+     * @param map
+     *            The map to adapt; can't be {@code null}.
+     * @param wrapper
+     *            The {@link ObjectWrapper} used to wrap the items in the array.
+     */
+    public static DefaultMapAdapter adapt(Map map, ObjectWrapperWithAPISupport wrapper) {
+        return new DefaultMapAdapter(map, wrapper);
+    }
+    
+    private DefaultMapAdapter(Map map, ObjectWrapper wrapper) {
+        super(wrapper);
+        this.map = map;
+    }
+
+    @Override
+    public TemplateModel get(String key) throws TemplateModelException {
+        Object val;
+        try {
+            val = map.get(key);
+        } catch (ClassCastException e) {
+            throw new _TemplateModelException(e,
+                    "ClassCastException while getting Map entry with String key ",
+                    new _DelayedJQuote(key));
+        } catch (NullPointerException e) {
+            throw new _TemplateModelException(e,
+                    "NullPointerException while getting Map entry with String key ",
+                    new _DelayedJQuote(key));
+        }
+            
+        if (val == null) {
+            // Check for Character key if this is a single-character string.
+            // In SortedMap-s, however, we can't do that safely, as it can cause ClassCastException.
+            if (key.length() == 1 && !(map instanceof SortedMap)) {
+                Character charKey = Character.valueOf(key.charAt(0));
+                try {
+                    val = map.get(charKey);
+                    if (val == null) {
+                        TemplateModel wrappedNull = wrap(null);
+                        if (wrappedNull == null || !(map.containsKey(key) || map.containsKey(charKey))) {
+                            return null;
+                        } else {
+                            return wrappedNull;
+                        }
+                    } 
+                } catch (ClassCastException e) {
+                    throw new _TemplateModelException(e,
+                                    "Class casting exception while getting Map entry with Character key ",
+                                    new _DelayedJQuote(charKey));
+                } catch (NullPointerException e) {
+                    throw new _TemplateModelException(e,
+                                    "NullPointerException while getting Map entry with Character key ",
+                                    new _DelayedJQuote(charKey));
+                }
+            } else {  // No char key fallback was possible
+                TemplateModel wrappedNull = wrap(null);
+                if (wrappedNull == null || !map.containsKey(key)) {
+                    return null;
+                } else {
+                    return wrappedNull;
+                }
+            }
+        }
+        
+        return wrap(val);
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return map.isEmpty();
+    }
+
+    @Override
+    public int size() {
+        return map.size();
+    }
+
+    @Override
+    public TemplateCollectionModel keys() {
+        return new SimpleCollection(map.keySet(), getObjectWrapper());
+    }
+
+    @Override
+    public TemplateCollectionModel values() {
+        return new SimpleCollection(map.values(), getObjectWrapper());
+    }
+
+    @Override
+    public KeyValuePairIterator keyValuePairIterator() {
+        return new MapKeyValuePairIterator(map, getObjectWrapper());
+    }
+
+    @Override
+    public Object getAdaptedObject(Class hint) {
+        return map;
+    }
+
+    @Override
+    public Object getWrappedObject() {
+        return map;
+    }
+
+    @Override
+    public TemplateModel getAPI() throws TemplateModelException {
+        return ((ObjectWrapperWithAPISupport) getObjectWrapper()).wrapAsAPI(map);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultNonListCollectionAdapter.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultNonListCollectionAdapter.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultNonListCollectionAdapter.java
new file mode 100644
index 0000000..3b128fd
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultNonListCollectionAdapter.java
@@ -0,0 +1,103 @@
+/*
+ * 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.io.Serializable;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.freemarker.core.model.AdapterTemplateModel;
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.ObjectWrapperAndUnwrapper;
+import org.apache.freemarker.core.model.ObjectWrapperWithAPISupport;
+import org.apache.freemarker.core.model.TemplateCollectionModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+import org.apache.freemarker.core.model.TemplateModelWithAPISupport;
+import org.apache.freemarker.core.model.WrapperTemplateModel;
+import org.apache.freemarker.core.model.WrappingTemplateModel;
+
+/**
+ * Adapts a non-{@link List} Java {@link Collection} to the corresponding {@link TemplateModel} interface(s), most
+ * importantly to {@link TemplateCollectionModelEx}. For {@link List}-s, use {@link DefaultListAdapter}, or else you
+ * lose indexed element access.
+ * 
+ * <p>
+ * Thread safety: A {@link DefaultNonListCollectionAdapter} is as thread-safe as the {@link Collection} that it wraps
+ * is. Normally you only have to consider read-only access, as the FreeMarker template language doesn't allow writing
+ * these collections (though of course, Java methods called from the template can violate this rule).
+ *
+ * @since 2.3.22
+ */
+public class DefaultNonListCollectionAdapter extends WrappingTemplateModel implements TemplateCollectionModelEx,
+        AdapterTemplateModel, WrapperTemplateModel, TemplateModelWithAPISupport, Serializable {
+
+    private final Collection collection;
+
+    /**
+     * Factory method for creating new adapter instances.
+     * 
+     * @param collection
+     *            The collection to adapt; can't be {@code null}.
+     * @param wrapper
+     *            The {@link ObjectWrapper} used to wrap the items in the collection. Has to be
+     *            {@link ObjectWrapperAndUnwrapper} because of planned future features.
+     */
+    public static DefaultNonListCollectionAdapter adapt(Collection collection, ObjectWrapperWithAPISupport wrapper) {
+        return new DefaultNonListCollectionAdapter(collection, wrapper);
+    }
+
+    private DefaultNonListCollectionAdapter(Collection collection, ObjectWrapperWithAPISupport wrapper) {
+        super(wrapper);
+        this.collection = collection;
+    }
+
+    @Override
+    public TemplateModelIterator iterator() throws TemplateModelException {
+        return new DefaultUnassignableIteratorAdapter(collection.iterator(), getObjectWrapper());
+    }
+
+    @Override
+    public int size() {
+        return collection.size();
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return collection.isEmpty();
+    }
+
+    @Override
+    public Object getWrappedObject() {
+        return collection;
+    }
+
+    @Override
+    public Object getAdaptedObject(Class hint) {
+        return getWrappedObject();
+    }
+
+    @Override
+    public TemplateModel getAPI() throws TemplateModelException {
+        return ((ObjectWrapperWithAPISupport) getObjectWrapper()).wrapAsAPI(collection);
+    }
+
+}


[11/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_StringUtil.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_StringUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_StringUtil.java
new file mode 100644
index 0000000..b53aae8
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_StringUtil.java
@@ -0,0 +1,1675 @@
+/*
+ * 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.util;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.StringTokenizer;
+import java.util.regex.Pattern;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.ParsingConfiguration;
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core.Version;
+
+/** Don't use this; used internally by FreeMarker, might changes without notice. */
+public class _StringUtil {
+
+    private static final char[] LT = new char[] { '&', 'l', 't', ';' };
+    private static final char[] GT = new char[] { '&', 'g', 't', ';' };
+    private static final char[] AMP = new char[] { '&', 'a', 'm', 'p', ';' };
+    private static final char[] QUOT = new char[] { '&', 'q', 'u', 'o', 't', ';' };
+    private static final char[] HTML_APOS = new char[] { '&', '#', '3', '9', ';' };
+    private static final char[] XML_APOS = new char[] { '&', 'a', 'p', 'o', 's', ';' };
+
+    /**
+     *  XML Encoding.
+     *  Replaces all '&gt;' '&lt;' '&amp;', "'" and '"' with entity reference
+     */
+    public static String XMLEnc(String s) {
+        return XMLOrHTMLEnc(s, true, true, XML_APOS);
+    }
+
+    /**
+     * Like {@link #XMLEnc(String)}, but writes the result into a {@link Writer}.
+     * 
+     * @since 2.3.24
+     */
+    public static void XMLEnc(String s, Writer out) throws IOException {
+        XMLOrHTMLEnc(s, XML_APOS, out);
+    }
+    
+    /**
+     *  XHTML Encoding.
+     *  Replaces all '&gt;' '&lt;' '&amp;', "'" and '"' with entity reference
+     *  suitable for XHTML decoding in common user agents (including legacy
+     *  user agents, which do not decode "&amp;apos;" to "'", so "&amp;#39;" is used
+     *  instead [see http://www.w3.org/TR/xhtml1/#C_16])
+     */
+    public static String XHTMLEnc(String s) {
+        return XMLOrHTMLEnc(s, true, true, HTML_APOS);
+    }
+
+    /**
+     * Like {@link #XHTMLEnc(String)}, but writes the result into a {@link Writer}.
+     * 
+     * @since 2.3.24
+     */
+    public static void XHTMLEnc(String s, Writer out) throws IOException {
+        XMLOrHTMLEnc(s, HTML_APOS, out);
+    }
+    
+    private static String XMLOrHTMLEnc(String s, boolean escGT, boolean escQuot, char[] apos) {
+        final int ln = s.length();
+        
+        // First we find out if we need to escape, and if so, what the length of the output will be:
+        int firstEscIdx = -1;
+        int lastEscIdx = 0;
+        int plusOutLn = 0;
+        for (int i = 0; i < ln; i++) {
+            escape: do {
+                final char c = s.charAt(i);
+                switch (c) {
+                case '<':
+                    plusOutLn += LT.length - 1;
+                    break;
+                case '>':
+                    if (!(escGT || maybeCDataEndGT(s, i))) {
+                        break escape;
+                    }
+                    plusOutLn += GT.length - 1;
+                    break;
+                case '&':
+                    plusOutLn += AMP.length - 1;
+                    break;
+                case '"':
+                    if (!escQuot) {
+                        break escape;
+                    }
+                    plusOutLn += QUOT.length - 1;
+                    break;
+                case '\'': // apos
+                    if (apos == null) {
+                        break escape;
+                    }
+                    plusOutLn += apos.length - 1;
+                    break;
+                default:
+                    break escape;
+                }
+                
+                if (firstEscIdx == -1) {
+                    firstEscIdx = i;
+                }
+                lastEscIdx = i;
+            } while (false);
+        }
+        
+        if (firstEscIdx == -1) {
+            return s; // Nothing to escape
+        } else {
+            final char[] esced = new char[ln + plusOutLn];
+            if (firstEscIdx != 0) {
+                s.getChars(0, firstEscIdx, esced, 0);
+            }
+            int dst = firstEscIdx;
+            scan: for (int i = firstEscIdx; i <= lastEscIdx; i++) {
+                final char c = s.charAt(i);
+                switch (c) {
+                case '<':
+                    dst = shortArrayCopy(LT, esced, dst);
+                    continue scan;
+                case '>':
+                    if (!(escGT || maybeCDataEndGT(s, i))) {
+                        break;
+                    }
+                    dst = shortArrayCopy(GT, esced, dst);
+                    continue scan;
+                case '&':
+                    dst = shortArrayCopy(AMP, esced, dst);
+                    continue scan;
+                case '"':
+                    if (!escQuot) {
+                        break;
+                    }
+                    dst = shortArrayCopy(QUOT, esced, dst);
+                    continue scan;
+                case '\'': // apos
+                    if (apos == null) {
+                        break;
+                    }
+                    dst = shortArrayCopy(apos, esced, dst);
+                    continue scan;
+                }
+                esced[dst++] = c;
+            }
+            if (lastEscIdx != ln - 1) {
+                s.getChars(lastEscIdx + 1, ln, esced, dst);
+            }
+            
+            return String.valueOf(esced);
+        }
+    }
+    
+    private static boolean maybeCDataEndGT(String s, int i) {
+        if (i == 0) return true;
+        if (s.charAt(i - 1) != ']') return false;
+        return i == 1 || s.charAt(i - 2) == ']';
+    }
+
+    private static void XMLOrHTMLEnc(String s, char[] apos, Writer out) throws IOException {
+        int writtenEnd = 0;  // exclusive end
+        int ln = s.length();
+        for (int i = 0; i < ln; i++) {
+            char c = s.charAt(i);
+            if (c == '<' || c == '>' || c == '&' || c == '"' || c == '\'') {
+                int flushLn = i - writtenEnd;
+                if (flushLn != 0) {
+                    out.write(s, writtenEnd, flushLn);
+                }
+                writtenEnd = i + 1;
+                
+                switch (c) {
+                case '<': out.write(LT); break;
+                case '>': out.write(GT); break;
+                case '&': out.write(AMP); break;
+                case '"': out.write(QUOT); break;
+                default: out.write(apos); break;
+                }
+            }
+        }
+        if (writtenEnd < ln) {
+            out.write(s, writtenEnd, ln - writtenEnd);
+        }
+    }
+    
+    /**
+     * For efficiently copying very short char arrays.
+     */
+    private static int shortArrayCopy(char[] src, char[] dst, int dstOffset) {
+        for (char aSrc : src) {
+            dst[dstOffset++] = aSrc;
+        }
+        return dstOffset;
+    }
+    
+    /**
+     *  XML encoding without replacing apostrophes.
+     *  @see #XMLEnc(String)
+     */
+    public static String XMLEncNA(String s) {
+        return XMLOrHTMLEnc(s, true, true, null);
+    }
+
+    /**
+     *  XML encoding for attribute values quoted with <tt>"</tt> (not with <tt>'</tt>!).
+     *  Also can be used for HTML attributes that are quoted with <tt>"</tt>.
+     *  @see #XMLEnc(String)
+     */
+    public static String XMLEncQAttr(String s) {
+        return XMLOrHTMLEnc(s, false, true, null);
+    }
+
+    /**
+     *  XML encoding without replacing apostrophes and quotation marks and
+     *  greater-thans (except in {@code ]]>}).
+     *  @see #XMLEnc(String)
+     */
+    public static String XMLEncNQG(String s) {
+        return XMLOrHTMLEnc(s, false, false, null);
+    }
+    
+    /**
+     *  Rich Text Format encoding (does not replace line breaks).
+     *  Escapes all '\' '{' '}'.
+     */
+    public static String RTFEnc(String s) {
+        int ln = s.length();
+        
+        // First we find out if we need to escape, and if so, what the length of the output will be:
+        int firstEscIdx = -1;
+        int lastEscIdx = 0;
+        int plusOutLn = 0;
+        for (int i = 0; i < ln; i++) {
+            char c = s.charAt(i);
+            if (c == '{' || c == '}' || c == '\\') {
+                if (firstEscIdx == -1) {
+                    firstEscIdx = i;
+                }
+                lastEscIdx = i;
+                plusOutLn++;
+            }
+        }
+        
+        if (firstEscIdx == -1) {
+            return s; // Nothing to escape
+        } else {
+            char[] esced = new char[ln + plusOutLn];
+            if (firstEscIdx != 0) {
+                s.getChars(0, firstEscIdx, esced, 0);
+            }
+            int dst = firstEscIdx;
+            for (int i = firstEscIdx; i <= lastEscIdx; i++) {
+                char c = s.charAt(i);
+                if (c == '{' || c == '}' || c == '\\') {
+                    esced[dst++] = '\\';
+                }
+                esced[dst++] = c;
+            }
+            if (lastEscIdx != ln - 1) {
+                s.getChars(lastEscIdx + 1, ln, esced, dst);
+            }
+            
+            return String.valueOf(esced);
+        }
+    }
+    
+    /**
+     * Like {@link #RTFEnc(String)}, but writes the result into a {@link Writer}.
+     * 
+     * @since 2.3.24
+     */
+    public static void RTFEnc(String s, Writer out) throws IOException {
+        int writtenEnd = 0;  // exclusive end
+        int ln = s.length();
+        for (int i = 0; i < ln; i++) {
+            char c = s.charAt(i);
+            if (c == '{' || c == '}' || c == '\\') {
+                int flushLn = i - writtenEnd;
+                if (flushLn != 0) {
+                    out.write(s, writtenEnd, flushLn);
+                }
+                out.write('\\');
+                writtenEnd = i; // Not i + 1, so c will be written out later
+            }
+        }
+        if (writtenEnd < ln) {
+            out.write(s, writtenEnd, ln - writtenEnd);
+        }
+    }
+    
+
+    /**
+     * URL encoding (like%20this) for query parameter values, path <em>segments</em>, fragments; this encodes all
+     * characters that are reserved anywhere.
+     */
+    public static String URLEnc(String s, Charset charset) throws UnsupportedEncodingException {
+        return URLEnc(s, charset, false);
+    }
+    
+    /**
+     * Like {@link #URLEnc(String, Charset)} but doesn't escape the slash character ({@code /}).
+     * This can be used to encode a path only if you know that no folder or file name will contain {@code /}
+     * character (not in the path, but in the name itself), which usually stands, as the commonly used OS-es don't
+     * allow that.
+     * 
+     * @since 2.3.21
+     */
+    public static String URLPathEnc(String s, Charset charset) throws UnsupportedEncodingException {
+        return URLEnc(s, charset, true);
+    }
+    
+    private static String URLEnc(String s, Charset charset, boolean keepSlash)
+            throws UnsupportedEncodingException {
+        int ln = s.length();
+        int i;
+        for (i = 0; i < ln; i++) {
+            char c = s.charAt(i);
+            if (!safeInURL(c, keepSlash)) {
+                break;
+            }
+        }
+        if (i == ln) {
+            // Nothing to escape
+            return s;
+        }
+
+        StringBuilder b = new StringBuilder(ln + ln / 3 + 2);
+        b.append(s.substring(0, i));
+
+        int encStart = i;
+        for (i++; i < ln; i++) {
+            char c = s.charAt(i);
+            if (safeInURL(c, keepSlash)) {
+                if (encStart != -1) {
+                    byte[] o = s.substring(encStart, i).getBytes(charset);
+                    for (byte bc : o) {
+                        b.append('%');
+                        int c1 = bc & 0x0F;
+                        int c2 = (bc >> 4) & 0x0F;
+                        b.append((char) (c2 < 10 ? c2 + '0' : c2 - 10 + 'A'));
+                        b.append((char) (c1 < 10 ? c1 + '0' : c1 - 10 + 'A'));
+                    }
+                    encStart = -1;
+                }
+                b.append(c);
+            } else {
+                if (encStart == -1) {
+                    encStart = i;
+                }
+            }
+        }
+        if (encStart != -1) {
+            byte[] o = s.substring(encStart, i).getBytes(charset);
+            for (byte bc : o) {
+                b.append('%');
+                int c1 = bc & 0x0F;
+                int c2 = (bc >> 4) & 0x0F;
+                b.append((char) (c2 < 10 ? c2 + '0' : c2 - 10 + 'A'));
+                b.append((char) (c1 < 10 ? c1 + '0' : c1 - 10 + 'A'));
+            }
+        }
+        
+        return b.toString();
+    }
+
+    private static boolean safeInURL(char c, boolean keepSlash) {
+        return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'
+                || c >= '0' && c <= '9'
+                || c == '_' || c == '-' || c == '.' || c == '!' || c == '~'
+                || c >= '\'' && c <= '*'
+                || keepSlash && c == '/';
+    }
+
+    public static Locale deduceLocale(String input) {
+       if (input == null) return null;
+       Locale locale = Locale.getDefault();
+       if (input.length() > 0 && input.charAt(0) == '"') input = input.substring(1, input.length() - 1);
+       StringTokenizer st = new StringTokenizer(input, ",_ ");
+       String lang = "", country = "";
+       if (st.hasMoreTokens()) {
+          lang = st.nextToken();
+       }
+       if (st.hasMoreTokens()) {
+          country = st.nextToken();
+       }
+       if (!st.hasMoreTokens()) {
+          locale = new Locale(lang, country);
+       } else {
+          locale = new Locale(lang, country, st.nextToken());
+       }
+       return locale;
+    }
+
+    public static String capitalize(String s) {
+        StringTokenizer st = new StringTokenizer(s, " \t\r\n", true);
+        StringBuilder buf = new StringBuilder(s.length());
+        while (st.hasMoreTokens()) {
+            String tok = st.nextToken();
+            buf.append(tok.substring(0, 1).toUpperCase());
+            buf.append(tok.substring(1).toLowerCase());
+        }
+        return buf.toString();
+    }
+
+    public static boolean getYesNo(String s) {
+        if (s.startsWith("\"")) {
+            s = s.substring(1, s.length() - 1);
+
+        }
+        if (s.equalsIgnoreCase("n")
+                || s.equalsIgnoreCase("no")
+                || s.equalsIgnoreCase("f")
+                || s.equalsIgnoreCase("false")) {
+            return false;
+        } else if (s.equalsIgnoreCase("y")
+                || s.equalsIgnoreCase("yes")
+                || s.equalsIgnoreCase("t")
+                || s.equalsIgnoreCase("true")) {
+            return true;
+        }
+        throw new IllegalArgumentException("Illegal boolean value: " + s);
+    }
+
+    /**
+     * Splits a string at the specified character.
+     */
+    public static String[] split(String s, char c) {
+        int i, b, e;
+        int cnt;
+        String res[];
+        int ln = s.length();
+
+        i = 0;
+        cnt = 1;
+        while ((i = s.indexOf(c, i)) != -1) {
+            cnt++;
+            i++;
+        }
+        res = new String[cnt];
+
+        i = 0;
+        b = 0;
+        while (b <= ln) {
+            e = s.indexOf(c, b);
+            if (e == -1) e = ln;
+            res[i++] = s.substring(b, e);
+            b = e + 1;
+        }
+        return res;
+    }
+
+    /**
+     * Splits a string at the specified string.
+     */
+    public static String[] split(String s, String sep, boolean caseInsensitive) {
+        String splitString = caseInsensitive ? sep.toLowerCase() : sep;
+        String input = caseInsensitive ? s.toLowerCase() : s;
+        int i, b, e;
+        int cnt;
+        String res[];
+        int ln = s.length();
+        int sln = sep.length();
+
+        if (sln == 0) throw new IllegalArgumentException(
+                "The separator string has 0 length");
+
+        i = 0;
+        cnt = 1;
+        while ((i = input.indexOf(splitString, i)) != -1) {
+            cnt++;
+            i += sln;
+        }
+        res = new String[cnt];
+
+        i = 0;
+        b = 0;
+        while (b <= ln) {
+            e = input.indexOf(splitString, b);
+            if (e == -1) e = ln;
+            res[i++] = s.substring(b, e);
+            b = e + sln;
+        }
+        return res;
+    }
+
+    /**
+     * Same as {@link #replace(String, String, String, boolean, boolean)} with two {@code false} parameters. 
+     * @since 2.3.20
+     */
+    public static String replace(String text, String oldSub, String newSub) {
+        return replace(text, oldSub, newSub, false, false);
+    }
+    
+    /**
+     * Replaces all occurrences of a sub-string in a string.
+     * @param text The string where it will replace <code>oldSub</code> with
+     *     <code>newSub</code>.
+     * @return String The string after the replacements.
+     */
+    public static String replace(String text, 
+                                  String oldSub,
+                                  String newSub,
+                                  boolean caseInsensitive,
+                                  boolean firstOnly) {
+        StringBuilder buf;
+        int tln;
+        int oln = oldSub.length();
+        
+        if (oln == 0) {
+            int nln = newSub.length();
+            if (nln == 0) {
+                return text;
+            } else {
+                if (firstOnly) {
+                    return newSub + text;
+                } else {
+                    tln = text.length();
+                    buf = new StringBuilder(tln + (tln + 1) * nln);
+                    buf.append(newSub);
+                    for (int i = 0; i < tln; i++) {
+                        buf.append(text.charAt(i));
+                        buf.append(newSub);
+                    }
+                    return buf.toString();
+                }
+            }
+        } else {
+            oldSub = caseInsensitive ? oldSub.toLowerCase() : oldSub;
+            String input = caseInsensitive ? text.toLowerCase() : text;
+            int e = input.indexOf(oldSub);
+            if (e == -1) {
+                return text;
+            }
+            int b = 0;
+            tln = text.length();
+            buf = new StringBuilder(
+                    tln + Math.max(newSub.length() - oln, 0) * 3);
+            do {
+                buf.append(text.substring(b, e));
+                buf.append(newSub);
+                b = e + oln;
+                e = input.indexOf(oldSub, b);
+            } while (e != -1 && !firstOnly);
+            buf.append(text.substring(b));
+            return buf.toString();
+        }
+    }
+
+    /**
+     * Removes a line-break from the end of the string (if there's any).
+     */
+    public static String chomp(String s) {
+        if (s.endsWith("\r\n")) return s.substring(0, s.length() - 2);
+        if (s.endsWith("\r") || s.endsWith("\n"))
+                return s.substring(0, s.length() - 1);
+        return s;
+    }
+
+    /**
+     * Converts a 0-length string to null, leaves the string as is otherwise.
+     * @param s maybe {@code null}.
+     */
+    public static String emptyToNull(String s) {
+    	if (s == null) return null;
+    	return s.length() == 0 ? null : s;
+    }
+    
+    /**
+     * Converts the parameter with <code>toString</code> (if it's not <code>null</code>) and passes it to
+     * {@link #jQuote(String)}.
+     */
+    public static String jQuote(Object obj) {
+        return jQuote(obj != null ? obj.toString() : null);
+    }
+    
+    /**
+     * Quotes string as Java Language string literal.
+     * Returns string <code>"null"</code> if <code>s</code>
+     * is <code>null</code>.
+     */
+    public static String jQuote(String s) {
+        if (s == null) {
+            return "null";
+        }
+        int ln = s.length();
+        StringBuilder b = new StringBuilder(ln + 4);
+        b.append('"');
+        for (int i = 0; i < ln; i++) {
+            char c = s.charAt(i);
+            if (c == '"') {
+                b.append("\\\"");
+            } else if (c == '\\') {
+                b.append("\\\\");
+            } else if (c < 0x20) {
+                if (c == '\n') {
+                    b.append("\\n");
+                } else if (c == '\r') {
+                    b.append("\\r");
+                } else if (c == '\f') {
+                    b.append("\\f");
+                } else if (c == '\b') {
+                    b.append("\\b");
+                } else if (c == '\t') {
+                    b.append("\\t");
+                } else {
+                    b.append("\\u00");
+                    int x = c / 0x10;
+                    b.append(toHexDigit(x));
+                    x = c & 0xF;
+                    b.append(toHexDigit(x));
+                }
+            } else {
+                b.append(c);
+            }
+        } // for each characters
+        b.append('"');
+        return b.toString();
+    }
+
+    /**
+     * Converts the parameter with <code>toString</code> (if not
+     * <code>null</code>)and passes it to {@link #jQuoteNoXSS(String)}. 
+     */
+    public static String jQuoteNoXSS(Object obj) {
+        return jQuoteNoXSS(obj != null ? obj.toString() : null);
+    }
+    
+    /**
+     * Same as {@link #jQuote(String)} but also escapes <code>'&lt;'</code>
+     * as <code>\</code><code>u003C</code>. This is used for log messages to prevent XSS
+     * on poorly written Web-based log viewers. 
+     */
+    public static String jQuoteNoXSS(String s) {
+        if (s == null) {
+            return "null";
+        }
+        int ln = s.length();
+        StringBuilder b = new StringBuilder(ln + 4);
+        b.append('"');
+        for (int i = 0; i < ln; i++) {
+            char c = s.charAt(i);
+            if (c == '"') {
+                b.append("\\\"");
+            } else if (c == '\\') {
+                b.append("\\\\");
+            } else if (c == '<') {
+                b.append("\\u003C");
+            } else if (c < 0x20) {
+                if (c == '\n') {
+                    b.append("\\n");
+                } else if (c == '\r') {
+                    b.append("\\r");
+                } else if (c == '\f') {
+                    b.append("\\f");
+                } else if (c == '\b') {
+                    b.append("\\b");
+                } else if (c == '\t') {
+                    b.append("\\t");
+                } else {
+                    b.append("\\u00");
+                    int x = c / 0x10;
+                    b.append(toHexDigit(x));
+                    x = c & 0xF;
+                    b.append(toHexDigit(x));
+                }
+            } else {
+                b.append(c);
+            }
+        } // for each characters
+        b.append('"');
+        return b.toString();
+    }
+
+    /**
+     * Escapes the <code>String</code> with the escaping rules of Java language
+     * string literals, so it's safe to insert the value into a string literal.
+     * The resulting string will not be quoted.
+     * 
+     * <p>All characters under UCS code point 0x20 will be escaped.
+     * Where they have no dedicated escape sequence in Java, they will
+     * be replaced with hexadecimal escape (<tt>\</tt><tt>u<i>XXXX</i></tt>). 
+     * 
+     * @see #jQuote(String)
+     */ 
+    public static String javaStringEnc(String s) {
+        int ln = s.length();
+        for (int i = 0; i < ln; i++) {
+            char c = s.charAt(i);
+            if (c == '"' || c == '\\' || c < 0x20) {
+                StringBuilder b = new StringBuilder(ln + 4);
+                b.append(s.substring(0, i));
+                while (true) {
+                    if (c == '"') {
+                        b.append("\\\"");
+                    } else if (c == '\\') {
+                        b.append("\\\\");
+                    } else if (c < 0x20) {
+                        if (c == '\n') {
+                            b.append("\\n");
+                        } else if (c == '\r') {
+                            b.append("\\r");
+                        } else if (c == '\f') {
+                            b.append("\\f");
+                        } else if (c == '\b') {
+                            b.append("\\b");
+                        } else if (c == '\t') {
+                            b.append("\\t");
+                        } else {
+                            b.append("\\u00");
+                            int x = c / 0x10;
+                            b.append((char)
+                                    (x < 0xA ? x + '0' : x - 0xA + 'a'));
+                            x = c & 0xF;
+                            b.append((char)
+                                    (x < 0xA ? x + '0' : x - 0xA + 'a'));
+                        }
+                    } else {
+                        b.append(c);
+                    }
+                    i++;
+                    if (i >= ln) {
+                        return b.toString();
+                    }
+                    c = s.charAt(i);
+                }
+            } // if has to be escaped
+        } // for each characters
+        return s;
+    }
+    
+    /**
+     * Escapes a {@link String} to be safely insertable into a JavaScript string literal; for more see
+     * {@link #jsStringEnc(String, boolean) jsStringEnc(s, false)}.
+     */
+    public static String javaScriptStringEnc(String s) {
+        return jsStringEnc(s, false);
+    }
+
+    /**
+     * Escapes a {@link String} to be safely insertable into a JSON string literal; for more see
+     * {@link #jsStringEnc(String, boolean) jsStringEnc(s, true)}.
+     */
+    public static String jsonStringEnc(String s) {
+        return jsStringEnc(s, true);
+    }
+
+    private static final int NO_ESC = 0;
+    private static final int ESC_HEXA = 1;
+    private static final int ESC_BACKSLASH = 3;
+    
+    /**
+     * Escapes a {@link String} to be safely insertable into a JavaScript or a JSON string literal.
+     * The resulting string will <em>not</em> be quoted; the caller must ensure that they are there in the final
+     * output. Note that for JSON, the quotation marks must be {@code "}, not {@code '}, because JSON doesn't escape
+     * {@code '}.
+     * 
+     * <p>The escaping rules guarantee that if the inside of the JavaScript/JSON string literal is from one or more
+     * touching pieces that were escaped with this, no character sequence can occur that closes the
+     * JavaScript/JSON string literal, or has a meaning in HTML/XML that causes the HTML script section to be closed.
+     * (If, however, the escaped section is preceded by or followed by strings from other sources, this can't be
+     * guaranteed in some rare cases. Like <tt>x = "&lt;/${a?js_string}"</tt> might closes the "script"
+     * element if {@code a} is {@code "script>"}.)
+     * 
+     * The escaped characters are:
+     * 
+     * <table style="width: auto; border-collapse: collapse" border="1" summary="Characters escaped by jsStringEnc">
+     * <tr>
+     *   <th>Input
+     *   <th>Output
+     * <tr>
+     *   <td><tt>"</tt>
+     *   <td><tt>\"</tt>
+     * <tr>
+     *   <td><tt>'</tt> if not in JSON-mode
+     *   <td><tt>\'</tt>
+     * <tr>
+     *   <td><tt>\</tt>
+     *   <td><tt>\\</tt>
+     * <tr>
+     *   <td><tt>/</tt> if the method can't know that it won't be directly after <tt>&lt;</tt>
+     *   <td><tt>\/</tt>
+     * <tr>
+     *   <td><tt>&gt;</tt> if the method can't know that it won't be directly after <tt>]]</tt> or <tt>--</tt>
+     *   <td>JavaScript: <tt>\&gt;</tt>; JSON: <tt>\</tt><tt>u003E</tt>
+     * <tr>
+     *   <td><tt>&lt;</tt> if the method can't know that it won't be directly followed by <tt>!</tt> or <tt>?</tt> 
+     *   <td><tt><tt>\</tt>u003C</tt>
+     * <tr>
+     *   <td>
+     *     u0000-u001f (UNICODE control characters - disallowed by JSON)<br>
+     *     u007f-u009f (UNICODE control characters - disallowed by JSON)
+     *   <td><tt>\n</tt>, <tt>\r</tt> and such, or if there's no such dedicated escape:
+     *       JavaScript: <tt>\x<i>XX</i></tt>, JSON: <tt>\<tt>u</tt><i>XXXX</i></tt>
+     * <tr>
+     *   <td>
+     *     u2028 (Line separator - source code line-break in ECMAScript)<br>
+     *     u2029 (Paragraph separator - source code line-break in ECMAScript)<br>
+     *   <td><tt>\<tt>u</tt><i>XXXX</i></tt>
+     * </table>
+     * 
+     * @since 2.3.20
+     */
+    public static String jsStringEnc(String s, boolean json) {
+        _NullArgumentException.check("s", s);
+        
+        int ln = s.length();
+        StringBuilder sb = null;
+        for (int i = 0; i < ln; i++) {
+            final char c = s.charAt(i);
+            final int escapeType;  // 
+            if (!(c > '>' && c < 0x7F && c != '\\') && c != ' ' && !(c >= 0xA0 && c < 0x2028)) {  // skip common chars
+                if (c <= 0x1F) {  // control chars range 1
+                    if (c == '\n') {
+                        escapeType = 'n';
+                    } else if (c == '\r') {
+                        escapeType = 'r';
+                    } else if (c == '\f') {
+                        escapeType = 'f';
+                    } else if (c == '\b') {
+                        escapeType = 'b';
+                    } else if (c == '\t') {
+                        escapeType = 't';
+                    } else {
+                        escapeType = ESC_HEXA;
+                    }
+                } else if (c == '"') {
+                    escapeType = ESC_BACKSLASH;
+                } else if (c == '\'') {
+                    escapeType = json ? NO_ESC : ESC_BACKSLASH; 
+                } else if (c == '\\') {
+                    escapeType = ESC_BACKSLASH; 
+                } else if (c == '/' && (i == 0 || s.charAt(i - 1) == '<')) {  // against closing elements
+                    escapeType = ESC_BACKSLASH; 
+                } else if (c == '>') {  // against "]]> and "-->"
+                    final boolean dangerous;
+                    if (i == 0) {
+                        dangerous = true;
+                    } else {
+                        final char prevC = s.charAt(i - 1);
+                        if (prevC == ']' || prevC == '-') {
+                            if (i == 1) {
+                                dangerous = true;
+                            } else {
+                                final char prevPrevC = s.charAt(i - 2);
+                                dangerous = prevPrevC == prevC;
+                            }
+                        } else {
+                            dangerous = false;
+                        }
+                    }
+                    escapeType = dangerous ? (json ? ESC_HEXA : ESC_BACKSLASH) : NO_ESC;
+                } else if (c == '<') {  // against "<!"
+                    final boolean dangerous;
+                    if (i == ln - 1) {
+                        dangerous = true;
+                    } else {
+                        char nextC = s.charAt(i + 1);
+                        dangerous = nextC == '!' || nextC == '?';
+                    }
+                    escapeType = dangerous ? ESC_HEXA : NO_ESC;
+                } else if ((c >= 0x7F && c <= 0x9F)  // control chars range 2
+                            || (c == 0x2028 || c == 0x2029)  // UNICODE line terminators
+                            ) {
+                    escapeType = ESC_HEXA;
+                } else {
+                    escapeType = NO_ESC;
+                }
+                
+                if (escapeType != NO_ESC) { // If needs escaping
+                    if (sb == null) {
+                        sb = new StringBuilder(ln + 6);
+                        sb.append(s.substring(0, i));
+                    }
+                    
+                    sb.append('\\');
+                    if (escapeType > 0x20) {
+                        sb.append((char) escapeType);
+                    } else if (escapeType == ESC_HEXA) {
+                        if (!json && c < 0x100) {
+                            sb.append('x');
+                            sb.append(toHexDigit(c >> 4));
+                            sb.append(toHexDigit(c & 0xF));
+                        } else {
+                            sb.append('u');
+                            sb.append(toHexDigit((c >> 12) & 0xF));
+                            sb.append(toHexDigit((c >> 8) & 0xF));
+                            sb.append(toHexDigit((c >> 4) & 0xF));
+                            sb.append(toHexDigit(c & 0xF));
+                        }
+                    } else {  // escapeType == ESC_BACKSLASH
+                        sb.append(c);
+                    }
+                    continue; 
+                }
+                // Falls through when escapeType == NO_ESC 
+            }
+            // Needs no escaping
+                
+            if (sb != null) sb.append(c);
+        } // for each characters
+        
+        return sb == null ? s : sb.toString();
+    }
+
+    private static char toHexDigit(int d) {
+        return (char) (d < 0xA ? d + '0' : d - 0xA + 'A');
+    }
+    
+    /**
+     * Parses a name-value pair list, where the pairs are separated with comma,
+     * and the name and value is separated with colon.
+     * The keys and values can contain only letters, digits and <tt>_</tt>. They
+     * can't be quoted. White-space around the keys and values are ignored. The
+     * value can be omitted if <code>defaultValue</code> is not null. When a
+     * value is omitted, then the colon after the key must be omitted as well.
+     * The same key can't be used for multiple times.
+     * 
+     * @param s the string to parse.
+     *     For example: <code>"strong:100, soft:900"</code>.
+     * @param defaultValue the value used when the value is omitted in a
+     *     key-value pair.
+     * 
+     * @return the map that contains the name-value pairs.
+     * 
+     * @throws java.text.ParseException if the string is not a valid name-value
+     *     pair list.
+     */
+    public static Map parseNameValuePairList(String s, String defaultValue)
+    throws java.text.ParseException {
+        Map map = new HashMap();
+        
+        char c = ' ';
+        int ln = s.length();
+        int p = 0;
+        int keyStart;
+        int valueStart;
+        String key;
+        String value;
+        
+        fetchLoop: while (true) {
+            // skip ws
+            while (p < ln) {
+                c = s.charAt(p);
+                if (!Character.isWhitespace(c)) {
+                    break;
+                }
+                p++;
+            }
+            if (p == ln) {
+                break fetchLoop;
+            }
+            keyStart = p;
+
+            // seek key end
+            while (p < ln) {
+                c = s.charAt(p);
+                if (!(Character.isLetterOrDigit(c) || c == '_')) {
+                    break;
+                }
+                p++;
+            }
+            if (keyStart == p) {
+                throw new java.text.ParseException(
+                       "Expecting letter, digit or \"_\" "
+                        + "here, (the first character of the key) but found "
+                        + jQuote(String.valueOf(c))
+                        + " at position " + p + ".",
+                        p);
+            }
+            key = s.substring(keyStart, p);
+
+            // skip ws
+            while (p < ln) {
+                c = s.charAt(p);
+                if (!Character.isWhitespace(c)) {
+                    break;
+                }
+                p++;
+            }
+            if (p == ln) {
+                if (defaultValue == null) {
+                    throw new java.text.ParseException(
+                            "Expecting \":\", but reached "
+                            + "the end of the string "
+                            + " at position " + p + ".",
+                            p);
+                }
+                value = defaultValue;
+            } else if (c != ':') {
+                if (defaultValue == null || c != ',') {
+                    throw new java.text.ParseException(
+                            "Expecting \":\" here, but found "
+                            + jQuote(String.valueOf(c))
+                            + " at position " + p + ".",
+                            p);
+                }
+
+                // skip ","
+                p++;
+                
+                value = defaultValue;
+            } else {
+                // skip ":"
+                p++;
+    
+                // skip ws
+                while (p < ln) {
+                    c = s.charAt(p);
+                    if (!Character.isWhitespace(c)) {
+                        break;
+                    }
+                    p++;
+                }
+                if (p == ln) {
+                    throw new java.text.ParseException(
+                            "Expecting the value of the key "
+                            + "here, but reached the end of the string "
+                            + " at position " + p + ".",
+                            p);
+                }
+                valueStart = p;
+    
+                // seek value end
+                while (p < ln) {
+                    c = s.charAt(p);
+                    if (!(Character.isLetterOrDigit(c) || c == '_')) {
+                        break;
+                    }
+                    p++;
+                }
+                if (valueStart == p) {
+                    throw new java.text.ParseException(
+                            "Expecting letter, digit or \"_\" "
+                            + "here, (the first character of the value) "
+                            + "but found "
+                            + jQuote(String.valueOf(c))
+                            + " at position " + p + ".",
+                            p);
+                }
+                value = s.substring(valueStart, p);
+
+                // skip ws
+                while (p < ln) {
+                    c = s.charAt(p);
+                    if (!Character.isWhitespace(c)) {
+                        break;
+                    }
+                    p++;
+                }
+                
+                // skip ","
+                if (p < ln) {
+                    if (c != ',') {
+                        throw new java.text.ParseException(
+                                "Excpecting \",\" or the end "
+                                + "of the string here, but found "
+                                + jQuote(String.valueOf(c))
+                                + " at position " + p + ".",
+                                p);
+                    } else {
+                        p++;
+                    }
+                }
+            }
+            
+            // store the key-value pair
+            if (map.put(key, value) != null) {
+                throw new java.text.ParseException(
+                        "Dublicated key: "
+                        + jQuote(key), keyStart);
+            }
+        }
+        
+        return map;
+    }
+    
+    /**
+     * @return whether the qname matches the combination of nodeName, nsURI, and environment prefix settings.
+     */
+    static public boolean matchesQName(String qname, String nodeName, String nsURI, Environment env) {
+        String defaultNS = env.getDefaultNS();
+        if ((defaultNS != null) && defaultNS.equals(nsURI)) {
+            return qname.equals(nodeName)
+                    || qname.equals(Template.DEFAULT_NAMESPACE_PREFIX + ":" + nodeName);
+        }
+        if ("".equals(nsURI)) {
+            if (defaultNS != null) {
+                return qname.equals(Template.NO_NS_PREFIX + ":" + nodeName);
+            } else {
+                return qname.equals(nodeName) || qname.equals(Template.NO_NS_PREFIX + ":" + nodeName);
+            }
+        }
+        String prefix = env.getPrefixForNamespace(nsURI);
+        if (prefix == null) {
+            return false; // Is this the right thing here???
+        }
+        return qname.equals(prefix + ":" + nodeName);
+    }
+    
+    /**
+     * Pads the string at the left with spaces until it reaches the desired
+     * length. If the string is longer than this length, then it returns the
+     * unchanged string. 
+     * 
+     * @param s the string that will be padded.
+     * @param minLength the length to reach.
+     */
+    public static String leftPad(String s, int minLength) {
+        return leftPad(s, minLength, ' ');
+    }
+    
+    /**
+     * Pads the string at the left with the specified character until it reaches
+     * the desired length. If the string is longer than this length, then it
+     * returns the unchanged string.
+     * 
+     * @param s the string that will be padded.
+     * @param minLength the length to reach.
+     * @param filling the filling pattern.
+     */
+    public static String leftPad(String s, int minLength, char filling) {
+        int ln = s.length();
+        if (minLength <= ln) {
+            return s;
+        }
+        
+        StringBuilder res = new StringBuilder(minLength);
+        
+        int dif = minLength - ln;
+        for (int i = 0; i < dif; i++) {
+            res.append(filling);
+        }
+        
+        res.append(s);
+        
+        return res.toString();
+    }
+
+    /**
+     * Pads the string at the left with a filling pattern until it reaches the
+     * desired length. If the string is longer than this length, then it returns
+     * the unchanged string. For example: <code>leftPad('ABC', 9, '1234')</code>
+     * returns <code>"123412ABC"</code>.
+     * 
+     * @param s the string that will be padded.
+     * @param minLength the length to reach.
+     * @param filling the filling pattern. Must be at least 1 characters long.
+     *     Can't be <code>null</code>.
+     */
+    public static String leftPad(String s, int minLength, String filling) {
+        int ln = s.length();
+        if (minLength <= ln) {
+            return s;
+        }
+        
+        StringBuilder res = new StringBuilder(minLength);
+
+        int dif = minLength - ln;
+        int fln = filling.length();
+        if (fln == 0) {
+            throw new IllegalArgumentException(
+                    "The \"filling\" argument can't be 0 length string.");
+        }
+        int cnt = dif / fln;
+        for (int i = 0; i < cnt; i++) {
+            res.append(filling);
+        }
+        cnt = dif % fln;
+        for (int i = 0; i < cnt; i++) {
+            res.append(filling.charAt(i));
+        }
+        
+        res.append(s);
+        
+        return res.toString();
+    }
+    
+    /**
+     * Pads the string at the right with spaces until it reaches the desired
+     * length. If the string is longer than this length, then it returns the
+     * unchanged string. 
+     * 
+     * @param s the string that will be padded.
+     * @param minLength the length to reach.
+     */
+    public static String rightPad(String s, int minLength) {
+        return rightPad(s, minLength, ' ');
+    }
+    
+    /**
+     * Pads the string at the right with the specified character until it
+     * reaches the desired length. If the string is longer than this length,
+     * then it returns the unchanged string.
+     * 
+     * @param s the string that will be padded.
+     * @param minLength the length to reach.
+     * @param filling the filling pattern.
+     */
+    public static String rightPad(String s, int minLength, char filling) {
+        int ln = s.length();
+        if (minLength <= ln) {
+            return s;
+        }
+        
+        StringBuilder res = new StringBuilder(minLength);
+
+        res.append(s);
+        
+        int dif = minLength - ln;
+        for (int i = 0; i < dif; i++) {
+            res.append(filling);
+        }
+        
+        return res.toString();
+    }
+
+    /**
+     * Pads the string at the right with a filling pattern until it reaches the
+     * desired length. If the string is longer than this length, then it returns
+     * the unchanged string. For example: <code>rightPad('ABC', 9, '1234')</code>
+     * returns <code>"ABC412341"</code>. Note that the filling pattern is
+     * started as if you overlay <code>"123412341"</code> with the left-aligned
+     * <code>"ABC"</code>, so it starts with <code>"4"</code>.
+     * 
+     * @param s the string that will be padded.
+     * @param minLength the length to reach.
+     * @param filling the filling pattern. Must be at least 1 characters long.
+     *     Can't be <code>null</code>.
+     */
+    public static String rightPad(String s, int minLength, String filling) {
+        int ln = s.length();
+        if (minLength <= ln) {
+            return s;
+        }
+        
+        StringBuilder res = new StringBuilder(minLength);
+
+        res.append(s);
+
+        int dif = minLength - ln;
+        int fln = filling.length();
+        if (fln == 0) {
+            throw new IllegalArgumentException(
+                    "The \"filling\" argument can't be 0 length string.");
+        }
+        int start = ln % fln;
+        int end = fln - start <= dif
+                ? fln
+                : start + dif;
+        for (int i = start; i < end; i++) {
+            res.append(filling.charAt(i));
+        }
+        dif -= end - start;
+        int cnt = dif / fln;
+        for (int i = 0; i < cnt; i++) {
+            res.append(filling);
+        }
+        cnt = dif % fln;
+        for (int i = 0; i < cnt; i++) {
+            res.append(filling.charAt(i));
+        }
+        
+        return res.toString();
+    }
+    
+    /**
+     * Converts a version number string to an integer for easy comparison.
+     * The version number must start with numbers separated with
+     * dots. There can be any number of such dot-separated numbers, but only
+     * the first three will be considered. After the numbers arbitrary text can
+     * follow, and will be ignored.
+     * 
+     * The string will be trimmed before interpretation.
+     * 
+     * @return major * 1000000 + minor * 1000 + micro
+     */
+    public static int versionStringToInt(String version) {
+        return new Version(version).intValue();
+    }
+
+    /**
+     * Tries to run {@code toString()}, but if that fails, returns a
+     * {@code "[com.example.SomeClass.toString() failed: " + e + "]"} instead. Also, it returns {@code null} for
+     * {@code null} parameter.
+     * 
+     * @since 2.3.20
+     */
+    public static String tryToString(Object object) {
+        if (object == null) return null;
+        
+        try {
+            return object.toString();
+        } catch (Throwable e) {
+            return failedToStringSubstitute(object, e);
+        }
+    }
+
+    private static String failedToStringSubstitute(Object object, Throwable e) {
+        String eStr;
+        try {
+            eStr = e.toString();
+        } catch (Throwable e2) {
+            eStr = _ClassUtil.getShortClassNameOfObject(e);
+        }
+        return "[" + _ClassUtil.getShortClassNameOfObject(object) + ".toString() failed: " + eStr + "]";
+    }
+    
+    /**
+     * Converts {@code 1}, {@code 2}, {@code 3} and so forth to {@code "A"}, {@code "B"}, {@code "C"} and so fort. When
+     * reaching {@code "Z"}, it continues like {@code "AA"}, {@code "AB"}, etc. The lowest supported number is 1, but
+     * there's no upper limit.
+     * 
+     * @throws IllegalArgumentException
+     *             If the argument is 0 or less.
+     * 
+     * @since 2.3.22
+     */
+    public static String toUpperABC(int n) {
+        return toABC(n, 'A');
+    }
+
+    /**
+     * Same as {@link #toUpperABC(int)}, but produces lower case result, like {@code "ab"}.
+     * 
+     * @since 2.3.22
+     */
+    public static String toLowerABC(int n) {
+        return toABC(n, 'a');
+    }
+
+    /**
+     * @param oneDigit
+     *            The character that stands for the value 1.
+     */
+    private static String toABC(final int n, char oneDigit) {
+        if (n < 1) {
+            throw new IllegalArgumentException("Can't convert 0 or negative "
+                    + "numbers to latin-number: " + n);
+        }
+        
+        // First find out how many "digits" will we need. We start from A, then
+        // try AA, then AAA, etc. (Note that the smallest digit is "A", which is
+        // 1, not 0. Hence this isn't like a usual 26-based number-system):
+        int reached = 1;
+        int weight = 1;
+        while (true) {
+            int nextWeight = weight * 26;
+            int nextReached = reached + nextWeight;
+            if (nextReached <= n) {
+                // So we will have one more digit
+                weight = nextWeight;
+                reached = nextReached;
+            } else {
+                // No more digits
+                break;
+            }
+        }
+        
+        // Increase the digits of the place values until we get as close
+        // to n as possible (but don't step over it).
+        StringBuilder sb = new StringBuilder();
+        while (weight != 0) {
+            // digitIncrease: how many we increase the digit which is already 1
+            final int digitIncrease = (n - reached) / weight;
+            sb.append((char) (oneDigit + digitIncrease));
+            reached += digitIncrease * weight;
+            
+            weight /= 26;
+        }
+        
+        return sb.toString();
+    }
+
+    /**
+     * Behaves exactly like {@link String#trim()}, but works on arrays. If the resulting array would have the same
+     * content after trimming, it returns the original array instance. Otherwise it returns a new array instance (or
+     * {@link _CollectionUtil#EMPTY_CHAR_ARRAY}).
+     * 
+     * @since 2.3.22
+     */
+    public static char[] trim(final char[] cs) {
+        if (cs.length == 0) {
+            return cs;
+        }
+        
+        int start = 0;
+        int end = cs.length;
+        while (start < end && cs[start] <= ' ') {
+            start++;
+        }
+        while (start < end && cs[end - 1] <= ' ') {
+            end--;
+        }
+        
+        if (start == 0 && end == cs.length) {
+            return cs;
+        }
+        if (start == end) {
+            return _CollectionUtil.EMPTY_CHAR_ARRAY;
+        }
+        
+        char[] newCs = new char[end - start];
+        System.arraycopy(cs, start, newCs, 0, end - start);
+        return newCs;
+    }
+
+    /**
+     * Tells if {@link String#trim()} will return a 0-length string for the {@link String} equivalent of the argument.
+     * 
+     * @since 2.3.22
+     */
+    public static boolean isTrimmableToEmpty(char[] text) {
+        return isTrimmableToEmpty(text, 0, text.length);
+    }
+
+    /**
+     * Like {@link #isTrimmableToEmpty(char[])}, but acts on a sub-array that starts at {@code start} (inclusive index).
+     * 
+     * @since 2.3.23
+     */
+    public static boolean isTrimmableToEmpty(char[] text, int start) {
+        return isTrimmableToEmpty(text, start, text.length);
+    }
+    
+    /**
+     * Like {@link #isTrimmableToEmpty(char[])}, but acts on a sub-array that starts at {@code start} (inclusive index)
+     * and ends at {@code end} (exclusive index).
+     * 
+     * @since 2.3.23
+     */
+    public static boolean isTrimmableToEmpty(char[] text, int start, int end) {
+        for (int i = start; i < end; i++) {
+            // We follow Java's String.trim() here, which simply states that c <= ' ' is whitespace.
+            if (text[i] > ' ') {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Same as {@link #globToRegularExpression(String, boolean)} with {@code caseInsensitive} argument {@code false}.
+     * 
+     * @since 2.3.24
+     */
+    public static Pattern globToRegularExpression(String glob) {
+        return globToRegularExpression(glob, false);
+    }
+    
+    /**
+     * Creates a regular expression from a glob. The glob must use {@code /} for as file separator, not {@code \}
+     * (backslash), and is always case sensitive.
+     *
+     * <p>This glob implementation recognizes these special characters:
+     * <ul>
+     *   <li>{@code ?}: Wildcard that matches exactly one character, other than {@code /} 
+     *   <li>{@code *}: Wildcard that matches zero, one or multiple characters, other than {@code /}
+     *   <li>{@code **}: Wildcard that matches zero, one or multiple directories. For example, {@code **}{@code /head.ftl}
+     *       matches {@code foo/bar/head.ftl}, {@code foo/head.ftl} and {@code head.ftl} too. {@code **} must be either
+     *       preceded by {@code /} or be at the beginning of the glob. {@code **} must be either followed by {@code /} or be
+     *       at the end of the glob. When {@code **} is at the end of the glob, it also matches file names, like
+     *       {@code a/**} matches {@code a/b/c.ftl}. If the glob only consist of a {@code **}, it will be a match for
+     *       everything.
+     *   <li>{@code \} (backslash): Makes the next character non-special (a literal). For example {@code How\?.ftl} will
+     *       match {@code How?.ftl}, but not {@code HowX.ftl}. Naturally, two backslashes produce one literal backslash. 
+     *   <li>{@code [}: Reserved for future purposes; can't be used
+     *   <li><code>{</code>: Reserved for future purposes; can't be used
+     * </ul>
+     * 
+     * @since 2.3.24
+     */
+    public static Pattern globToRegularExpression(String glob, boolean caseInsensitive) {
+        StringBuilder regex = new StringBuilder();
+        
+        int nextStart = 0;
+        boolean escaped = false;
+        int ln = glob.length();
+        for (int idx = 0; idx < ln; idx++) {
+            char c = glob.charAt(idx);
+            if (!escaped) {
+                if (c == '?') {
+                    appendLiteralGlobSection(regex, glob, nextStart, idx);
+                    regex.append("[^/]");
+                    nextStart = idx + 1;
+                } else if (c == '*') {
+                    appendLiteralGlobSection(regex, glob, nextStart, idx);
+                    if (idx + 1 < ln && glob.charAt(idx + 1) == '*') {
+                        if (!(idx == 0 || glob.charAt(idx - 1) == '/')) {
+                            throw new IllegalArgumentException(
+                                    "The \"**\" wildcard must be directly after a \"/\" or it must be at the "
+                                    + "beginning, in this glob: " + glob);
+                        }
+                        
+                        if (idx + 2 == ln) { // trailing "**"
+                            regex.append(".*");
+                            idx++;
+                        } else { // "**/"
+                            if (!(idx + 2 < ln && glob.charAt(idx + 2) == '/')) {
+                                throw new IllegalArgumentException(
+                                        "The \"**\" wildcard must be followed by \"/\", or must be at tehe end, "
+                                        + "in this glob: " + glob);
+                            }
+                            regex.append("(.*?/)*");
+                            idx += 2;  // "*/".length()
+                        }
+                    } else {
+                        regex.append("[^/]*");
+                    }
+                    nextStart = idx + 1;
+                } else if (c == '\\') {
+                    escaped = true;
+                } else if (c == '[' || c == '{') {
+                    throw new IllegalArgumentException(
+                            "The \"" + c + "\" glob operator is currently unsupported "
+                            + "(precede it with \\ for literal matching), "
+                            + "in this glob: " + glob);
+                }
+            } else {
+                escaped = false;
+            }
+        }
+        appendLiteralGlobSection(regex, glob, nextStart, glob.length());
+        
+        return Pattern.compile(regex.toString(), caseInsensitive ? Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE : 0);
+    }
+
+    private static void appendLiteralGlobSection(StringBuilder regex, String glob, int start, int end) {
+        if (start == end) return;
+        String part = unescapeLiteralGlobSection(glob.substring(start, end));
+        regex.append(Pattern.quote(part));
+    }
+
+    private static String unescapeLiteralGlobSection(String s) {
+        int backslashIdx = s.indexOf('\\');
+        if (backslashIdx == -1) {
+            return s;
+        }
+        int ln = s.length();
+        StringBuilder sb = new StringBuilder(ln - 1);
+        int nextStart = 0; 
+        do {
+            sb.append(s, nextStart, backslashIdx);
+            nextStart = backslashIdx + 1;
+        } while ((backslashIdx = s.indexOf('\\', nextStart + 1)) != -1);
+        if (nextStart < ln) {
+            sb.append(s, nextStart, ln);
+        }
+        return sb.toString();
+    }
+
+    public static String toFTLIdentifierReferenceAfterDot(String name) {
+        return FTLUtil.escapeIdentifier(name);
+    }
+
+    public static String toFTLTopLevelIdentifierReference(String name) {
+        return FTLUtil.escapeIdentifier(name);
+    }
+
+    public static String toFTLTopLevelTragetIdentifier(final String name) {
+        char quotationType = 0;
+        scanForQuotationType: for (int i = 0; i < name.length(); i++) {
+            final char c = name.charAt(i);
+            if (!(i == 0 ? FTLUtil.isNonEscapedIdentifierStart(c) : FTLUtil.isNonEscapedIdentifierPart(c)) && c != '@') {
+                if ((quotationType == 0 || quotationType == '\\') && (c == '-' || c == '.' || c == ':')) {
+                    quotationType = '\\';
+                } else {
+                    quotationType = '"';
+                    break scanForQuotationType;
+                }
+            }
+        }
+        switch (quotationType) {
+        case 0:
+            return name;
+        case '"':
+            return FTLUtil.toStringLiteral(name);
+        case '\\':
+            return FTLUtil.escapeIdentifier(name);
+        default:
+            throw new BugException();
+        }
+    }
+
+    /**
+     * @return {@link ParsingConfiguration#CAMEL_CASE_NAMING_CONVENTION}, or {@link ParsingConfiguration#LEGACY_NAMING_CONVENTION}
+     *         or, {@link ParsingConfiguration#AUTO_DETECT_NAMING_CONVENTION} when undecidable.
+     */
+    public static int getIdentifierNamingConvention(String name) {
+        final int ln = name.length();
+        for (int i = 0; i < ln; i++) {
+            final char c = name.charAt(i);
+            if (c == '_') {
+                return ParsingConfiguration.LEGACY_NAMING_CONVENTION;
+            }
+            if (_StringUtil.isUpperUSASCII(c)) {
+                return ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION;
+            }
+        }
+        return ParsingConfiguration.AUTO_DETECT_NAMING_CONVENTION;
+    }
+
+    // [2.4] Won't be needed anymore
+    /**
+     * A deliberately very inflexible camel case to underscored converter; it must not convert improper camel case
+     * names to a proper underscored name.
+     */
+    public static String camelCaseToUnderscored(String camelCaseName) {
+        int i = 0;
+        while (i < camelCaseName.length() && Character.isLowerCase(camelCaseName.charAt(i))) {
+            i++;
+        }
+        if (i == camelCaseName.length()) {
+            // No conversion needed
+            return camelCaseName;
+        }
+        
+        StringBuilder sb = new StringBuilder();
+        sb.append(camelCaseName.substring(0, i));
+        while (i < camelCaseName.length()) {
+            final char c = camelCaseName.charAt(i);
+            if (_StringUtil.isUpperUSASCII(c)) {
+                sb.append('_');
+                sb.append(Character.toLowerCase(c));
+            } else {
+                sb.append(c);
+            }
+            i++;
+        }
+        return sb.toString();
+    }
+
+    public static boolean isASCIIDigit(char c) {
+        return c >= '0' && c <= '9';
+    }
+    
+    public static boolean isUpperUSASCII(char c) {
+        return c >= 'A' && c <= 'Z';
+    }
+
+    private static final Pattern NORMALIZE_EOLS_REGEXP = Pattern.compile("\\r\\n?+");
+
+    /**
+     * Converts all non UN*X End-Of-Line character sequences (CR and CRLF) to UN*X format (LF).
+     * Returns {@code null} for {@code null} input.
+     */
+    public static String normalizeEOLs(String s) {
+        if (s == null) {
+            return null;
+        }
+        return NORMALIZE_EOLS_REGEXP.matcher(s).replaceAll("\n");
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_UnmodifiableCompositeSet.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_UnmodifiableCompositeSet.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_UnmodifiableCompositeSet.java
new file mode 100644
index 0000000..ab88f57
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_UnmodifiableCompositeSet.java
@@ -0,0 +1,98 @@
+/*
+ * 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.util;
+
+import java.util.Iterator;
+import java.util.Set;
+
+/** Don't use this; used internally by FreeMarker, might changes without notice. */
+public class _UnmodifiableCompositeSet<E> extends _UnmodifiableSet<E> {
+    
+    private final Set<E> set1, set2;
+    
+    public _UnmodifiableCompositeSet(Set<E> set1, Set<E> set2) {
+        this.set1 = set1;
+        this.set2 = set2;
+    }
+
+    @Override
+    public Iterator<E> iterator() {
+        return new CompositeIterator();
+    }
+    
+    @Override
+    public boolean contains(Object o) {
+        return set1.contains(o) || set2.contains(o);
+    }
+
+    @Override
+    public int size() {
+        return set1.size() + set2.size();
+    }
+    
+    private class CompositeIterator implements Iterator<E> {
+
+        private Iterator<E> it1, it2;
+        private boolean it1Deplected;
+        
+        @Override
+        public boolean hasNext() {
+            if (!it1Deplected) {
+                if (it1 == null) {
+                    it1 = set1.iterator();
+                }
+                if (it1.hasNext()) {
+                    return true;
+                }
+                
+                it2 = set2.iterator();
+                it1 = null;
+                it1Deplected = true;
+                // Falls through
+            }
+            return it2.hasNext();
+        }
+
+        @Override
+        public E next() {
+            if (!it1Deplected) {
+                if (it1 == null) {
+                    it1 = set1.iterator();
+                }
+                if (it1.hasNext()) {
+                    return it1.next();
+                }
+                
+                it2 = set2.iterator();
+                it1 = null;
+                it1Deplected = true;
+                // Falls through
+            }
+            return it2.next();
+        }
+
+        @Override
+        public void remove() {
+            throw new UnsupportedOperationException();
+        }
+
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_UnmodifiableSet.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_UnmodifiableSet.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_UnmodifiableSet.java
new file mode 100644
index 0000000..7e08815
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_UnmodifiableSet.java
@@ -0,0 +1,47 @@
+/*
+ * 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.util;
+
+import java.util.AbstractSet;
+
+/** Don't use this; used internally by FreeMarker, might changes without notice. */
+public abstract class _UnmodifiableSet<E> extends AbstractSet<E> {
+
+    @Override
+    public boolean add(E o) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean remove(Object o) {
+        if (contains(o)) {
+            throw new UnsupportedOperationException();
+        }
+        return false;
+    }
+
+    @Override
+    public void clear() {
+        if (!isEmpty()) {
+            throw new UnsupportedOperationException();
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/package.html
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/package.html b/freemarker-core/src/main/java/org/apache/freemarker/core/util/package.html
new file mode 100644
index 0000000..e90df61
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/package.html
@@ -0,0 +1,25 @@
+<!--
+  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.
+-->
+<html>
+<head>
+</head>
+<body bgcolor="white">
+<p>Various classes used by core FreeMarker code but might be useful outside of it too.</p>
+</body>
+</html>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/InvalidFormatParametersException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/InvalidFormatParametersException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/InvalidFormatParametersException.java
new file mode 100644
index 0000000..ba09574
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/InvalidFormatParametersException.java
@@ -0,0 +1,37 @@
+/*
+ * 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.valueformat;
+
+/**
+ * Used when creating {@link TemplateDateFormat}-s and {@link TemplateNumberFormat}-s to indicate that the parameters
+ * part of the format string (like some kind of pattern) is malformed.
+ * 
+ * @since 2.3.24
+ */
+public final class InvalidFormatParametersException extends InvalidFormatStringException {
+    
+    public InvalidFormatParametersException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public InvalidFormatParametersException(String message) {
+        this(message, null);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/InvalidFormatStringException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/InvalidFormatStringException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/InvalidFormatStringException.java
new file mode 100644
index 0000000..05c7ed1
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/InvalidFormatStringException.java
@@ -0,0 +1,37 @@
+/*
+ * 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.valueformat;
+
+/**
+ * Used when creating {@link TemplateDateFormat}-s and {@link TemplateNumberFormat}-s to indicate that the format
+ * string (like the value of the {@code dateFormat} setting) is malformed.
+ * 
+ * @since 2.3.24
+ */
+public abstract class InvalidFormatStringException extends TemplateValueFormatException {
+    
+    public InvalidFormatStringException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public InvalidFormatStringException(String message) {
+        this(message, null);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/ParsingNotSupportedException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/ParsingNotSupportedException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/ParsingNotSupportedException.java
new file mode 100644
index 0000000..08eba37
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/ParsingNotSupportedException.java
@@ -0,0 +1,37 @@
+/*
+ * 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.valueformat;
+
+/**
+ * Thrown when the {@link TemplateValueFormat} doesn't support parsing, and parsing was invoked.
+ * 
+ * @since 2.3.24
+ */
+public class ParsingNotSupportedException extends TemplateValueFormatException {
+
+    public ParsingNotSupportedException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public ParsingNotSupportedException(String message) {
+        this(message, null);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateDateFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateDateFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateDateFormat.java
new file mode 100644
index 0000000..7626d90
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateDateFormat.java
@@ -0,0 +1,110 @@
+/*
+ * 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.valueformat;
+
+import java.text.DateFormat;
+import java.util.Date;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+
+/**
+ * Represents a date/time/dateTime format; used in templates for formatting and parsing with that format. This is
+ * similar to Java's {@link DateFormat}, but made to fit the requirements of FreeMarker. Also, it makes easier to define
+ * formats that can't be represented with Java's existing {@link DateFormat} implementations.
+ * 
+ * <p>
+ * Implementations need not be thread-safe if the {@link TemplateNumberFormatFactory} doesn't recycle them among
+ * different {@link Environment}-s. As far as FreeMarker's concerned, instances are bound to a single
+ * {@link Environment}, and {@link Environment}-s are thread-local objects.
+ * 
+ * @since 2.3.24
+ */
+public abstract class TemplateDateFormat extends TemplateValueFormat {
+    
+    /**
+     * @param dateModel
+     *            The date/time/dateTime to format; not {@code null}. Most implementations will just work with the return value of
+     *            {@link TemplateDateModel#getAsDate()}, but some may format differently depending on the properties of
+     *            a custom {@link TemplateDateModel} implementation.
+     * 
+     * @return The date/time/dateTime as text, with no escaping (like no HTML escaping); can't be {@code null}.
+     * 
+     * @throws TemplateValueFormatException
+     *             When a problem occurs during the formatting of the value. Notable subclass:
+     *             {@link UnknownDateTypeFormattingUnsupportedException}
+     * @throws TemplateModelException
+     *             Exception thrown by the {@code dateModel} object when calling its methods.
+     */
+    public abstract String formatToPlainText(TemplateDateModel dateModel)
+            throws TemplateValueFormatException, TemplateModelException;
+
+    /**
+     * Formats the model to markup instead of to plain text if the result markup will be more than just plain text
+     * escaped, otherwise falls back to formatting to plain text. If the markup result would be just the result of
+     * {@link #formatToPlainText(TemplateDateModel)} escaped, it must return the {@link String} that
+     * {@link #formatToPlainText(TemplateDateModel)} does.
+     * 
+     * <p>The implementation in {@link TemplateDateFormat} simply calls {@link #formatToPlainText(TemplateDateModel)}.
+     * 
+     * @return A {@link String} or a {@link TemplateMarkupOutputModel}; not {@code null}.
+     */
+    public Object format(TemplateDateModel dateModel) throws TemplateValueFormatException, TemplateModelException {
+        return formatToPlainText(dateModel);
+    }
+
+    /**
+     * Parsers a string to date/time/datetime, according to this format. Some format implementations may throw
+     * {@link ParsingNotSupportedException} here.
+     * 
+     * @param s
+     *            The string to parse
+     * @param dateType
+     *            The expected date type of the result. Not all {@link TemplateDateFormat}-s will care about this;
+     *            though those who return a {@link TemplateDateModel} instead of {@link Date} often will. When strings
+     *            are parsed via {@code ?date}, {@code ?time}, or {@code ?datetime}, then this parameter is
+     *            {@link TemplateDateModel#DATE}, {@link TemplateDateModel#TIME}, or {@link TemplateDateModel#DATETIME},
+     *            respectively. This parameter rarely if ever {@link TemplateDateModel#UNKNOWN}, but the implementation
+     *            that cares about this parameter should be prepared for that. If nothing else, it should throw
+     *            {@link UnknownDateTypeParsingUnsupportedException} then.
+     * 
+     * @return The interpretation of the text either as a {@link Date} or {@link TemplateDateModel}. Typically, a
+     *         {@link Date}. {@link TemplateDateModel} is used if you have to attach some application-specific
+     *         meta-information thats also extracted during {@link #formatToPlainText(TemplateDateModel)} (so if you format
+     *         something and then parse it, you get back an equivalent result). It can't be {@code null}. Known issue
+     *         (at least in FTL 2): {@code ?date}/{@code ?time}/{@code ?datetime}, when not invoked as a method, can't
+     *         return the {@link TemplateDateModel}, only the {@link Date} from inside it, hence the additional
+     *         application-specific meta-info will be lost.
+     */
+    public abstract Object parse(String s, int dateType) throws TemplateValueFormatException;
+    
+    /**
+     * Tells if this formatter should be re-created if the locale changes.
+     */
+    public abstract boolean isLocaleBound();
+
+    /**
+     * Tells if this formatter should be re-created if the time zone changes. Currently always {@code true}.
+     */
+    public abstract boolean isTimeZoneBound();
+        
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateDateFormatFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateDateFormatFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateDateFormatFactory.java
new file mode 100644
index 0000000..ca3b4bd
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateDateFormatFactory.java
@@ -0,0 +1,95 @@
+/*
+ * 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.valueformat;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import org.apache.freemarker.core.CustomStateKey;
+import org.apache.freemarker.core.MutableProcessingConfiguration;
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.model.TemplateDateModel;
+
+/**
+ * Factory for a certain kind of date/time/dateTime formatting ({@link TemplateDateFormat}). Usually a singleton
+ * (one-per-VM or one-per-{@link Configuration}), and so must be thread-safe.
+ * 
+ * @see MutableProcessingConfiguration#setCustomDateFormats(java.util.Map)
+ * 
+ * @since 2.3.24
+ */
+public abstract class TemplateDateFormatFactory extends TemplateValueFormatFactory {
+    
+    /**
+     * Returns a formatter for the given parameters.
+     * 
+     * <p>
+     * The returned formatter can be a new instance or a reused (cached) instance. Note that {@link Environment} itself
+     * caches the returned instances, though that cache is lost with the {@link Environment} (i.e., when the top-level
+     * template execution ends), also it might flushes lot of entries if the locale or time zone is changed during
+     * template execution. So caching on the factory level is still useful, unless creating the formatters is
+     * sufficiently cheap.
+     * 
+     * @param params
+     *            The string that further describes how the format should look. For example, when the
+     *            {@link MutableProcessingConfiguration#getDateFormat() dateFormat} is {@code "@fooBar 1, 2"}, then it will be
+     *            {@code "1, 2"} (and {@code "@fooBar"} selects the factory). The format of this string is up to the
+     *            {@link TemplateDateFormatFactory} implementation. Not {@code null}, often an empty string.
+     * @param dateType
+     *            {@link TemplateDateModel#DATE}, {@link TemplateDateModel#TIME}, {@link TemplateDateModel#DATETIME} or
+     *            {@link TemplateDateModel#UNKNOWN}. Supporting {@link TemplateDateModel#UNKNOWN} is not necessary, in
+     *            which case the method should throw an {@link UnknownDateTypeFormattingUnsupportedException} exception.
+     * @param locale
+     *            The locale to format for. Not {@code null}. The resulting format should be bound to this locale
+     *            forever (i.e. locale changes in the {@link Environment} must not be followed).
+     * @param timeZone
+     *            The time zone to format for. Not {@code null}. The resulting format must be bound to this time zone
+     *            forever (i.e. time zone changes in the {@link Environment} must not be followed).
+     * @param zonelessInput
+     *            Indicates that the input Java {@link Date} is not from a time zone aware source. When this is
+     *            {@code true}, the formatters shouldn't override the time zone provided to its constructor (most
+     *            formatters don't do that anyway), and it shouldn't show the time zone, if it can hide it (like a
+     *            {@link SimpleDateFormat} pattern-based formatter may can't do that, as the pattern prescribes what to
+     *            show).
+     *            <p>
+     *            As of FreeMarker 2.3.21, this is {@code true} exactly when the date is an SQL "date without time of
+     *            the day" (i.e., a {@link java.sql.Date java.sql.Date}) or an SQL "time of the day" value (i.e., a
+     *            {@link java.sql.Time java.sql.Time}, although this rule can change in future, depending on
+     *            configuration settings and such, so you shouldn't rely on this rule, just accept what this parameter
+     *            says.
+     * @param env
+     *            The runtime environment from which the formatting was called. This is mostly meant to be used for
+     *            {@link Environment#getCustomState(CustomStateKey)}.
+     * 
+     * @throws TemplateValueFormatException
+     *             If any problem occurs while parsing/getting the format. Notable subclasses:
+     *             {@link InvalidFormatParametersException} if {@code params} is malformed;
+     *             {@link UnknownDateTypeFormattingUnsupportedException} if {@code dateType} is
+     *             {@link TemplateDateModel#UNKNOWN} and that's unsupported by this factory.
+     */
+    public abstract TemplateDateFormat get(
+            String params,
+            int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput,
+            Environment env)
+                    throws TemplateValueFormatException;
+
+}



[34/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/NativeCollectionEx.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/NativeCollectionEx.java b/freemarker-core/src/main/java/org/apache/freemarker/core/NativeCollectionEx.java
new file mode 100644
index 0000000..5afa98a
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/NativeCollectionEx.java
@@ -0,0 +1,73 @@
+/*
+ * 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.util.Collection;
+import java.util.Iterator;
+
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateCollectionModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+
+/**
+ * A collection where each items is already a {@link TemplateModel}, so no {@link ObjectWrapper} need to be specified.
+ */
+class NativeCollectionEx implements TemplateCollectionModelEx {
+
+    private final Collection<TemplateModel> collection;
+
+    public NativeCollectionEx(Collection<TemplateModel> collection) {
+        this.collection = collection;
+    }
+
+    @Override
+    public int size() {
+        return collection.size();
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return collection.isEmpty();
+    }
+
+    @Override
+    public TemplateModelIterator iterator() throws TemplateModelException {
+        return new TemplateModelIterator() {
+
+            private final Iterator<TemplateModel> iterator = collection.iterator();
+
+            @Override
+            public TemplateModel next() throws TemplateModelException {
+                if (!iterator.hasNext()) {
+                    throw new TemplateModelException("The collection has no more items.");
+                }
+
+                return iterator.next();
+            }
+
+            @Override
+            public boolean hasNext() {
+                return iterator.hasNext();
+            }
+        };
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/NativeHashEx2.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/NativeHashEx2.java b/freemarker-core/src/main/java/org/apache/freemarker/core/NativeHashEx2.java
new file mode 100644
index 0000000..2850255
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/NativeHashEx2.java
@@ -0,0 +1,106 @@
+/*
+ * 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.Serializable;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx2;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+
+/**
+ * A hash where each value is already a {@link TemplateModel}, so no {@link ObjectWrapper} need to be specified.
+ *
+ * <p>While this class allows adding items, doing so is not thread-safe, and thus only meant to be done during the
+ * initialization of the sequence.
+ */
+class NativeHashEx2 implements TemplateHashModelEx2, Serializable {
+
+    private final LinkedHashMap<String, TemplateModel> map;
+
+    public NativeHashEx2() {
+        this.map = new LinkedHashMap<>();
+    }
+
+    @Override
+    public int size() throws TemplateModelException {
+        return map.size();
+    }
+
+    @Override
+    public TemplateModel get(String key) throws TemplateModelException {
+        return map.get(key);
+    }
+
+    @Override
+    public boolean isEmpty() throws TemplateModelException {
+        return map.isEmpty();
+    }
+
+    @Override
+    public KeyValuePairIterator keyValuePairIterator() throws TemplateModelException {
+        return new KeyValuePairIterator() {
+            private final Iterator<Map.Entry<String, TemplateModel>> entrySetIterator = map.entrySet().iterator();
+
+            @Override
+            public boolean hasNext() throws TemplateModelException {
+                return entrySetIterator.hasNext();
+            }
+
+            @Override
+            public KeyValuePair next() throws TemplateModelException {
+                return new KeyValuePair() {
+                    private final Map.Entry<String, TemplateModel> entry = entrySetIterator.next();
+
+                    @Override
+                    public TemplateModel getKey() throws TemplateModelException {
+                        return new SimpleScalar(entry.getKey());
+                    }
+
+                    @Override
+                    public TemplateModel getValue() throws TemplateModelException {
+                        return entry.getValue();
+                    }
+                };
+            }
+        };
+    }
+
+    @Override
+    public TemplateCollectionModel keys() throws TemplateModelException {
+        return new NativeStringCollectionCollectionEx(map.keySet());
+    }
+
+    @Override
+    public TemplateCollectionModel values() throws TemplateModelException {
+        return new NativeCollectionEx(map.values());
+    }
+
+    public TemplateModel put(String key, TemplateModel value) {
+        return map.put(key, value);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/NativeSequence.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/NativeSequence.java b/freemarker-core/src/main/java/org/apache/freemarker/core/NativeSequence.java
new file mode 100644
index 0000000..d1b6886
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/NativeSequence.java
@@ -0,0 +1,74 @@
+/*
+ * 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.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+
+/**
+ * A sequence where each items is already a {@link TemplateModel}, so no {@link ObjectWrapper} need to be specified.
+ *
+ * <p>While this class allows adding items, doing so is not thread-safe, and thus only meant to be done during the
+ * initialization of the sequence.
+ */
+class NativeSequence implements TemplateSequenceModel, Serializable {
+
+    private final ArrayList<TemplateModel> items;
+
+    public NativeSequence(int capacity) {
+        items = new ArrayList<>(capacity);
+    }
+
+    /**
+     * Copies the collection
+     */
+    public NativeSequence(Collection<TemplateModel> items) {
+        this.items = new ArrayList<>(items.size());
+        this.items.addAll(items);
+    }
+
+    public void add(TemplateModel tm) {
+        items.add(tm);
+    }
+
+    public void addAll(Collection<TemplateModel> items) {
+        this.items.addAll(items);
+    }
+
+    public void clear() {
+        items.clear();
+    }
+
+    @Override
+    public TemplateModel get(int index) throws TemplateModelException {
+        return items.get(index);
+    }
+
+    @Override
+    public int size() throws TemplateModelException {
+        return items.size();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/NativeStringArraySequence.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/NativeStringArraySequence.java b/freemarker-core/src/main/java/org/apache/freemarker/core/NativeStringArraySequence.java
new file mode 100644
index 0000000..96e9899
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/NativeStringArraySequence.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.impl.DefaultArrayAdapter;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+
+/**
+ * Adapts (not copies) a {@link String} array with on-the-fly wrapping of the items to {@link SimpleScalar}-s. The
+ * important difference to {@link DefaultArrayAdapter} is that it doesn't depend on an {@link ObjectWrapper}, which is
+ * needed to guarantee the behavior of some template language constructs. The important difference to
+ * {@link NativeSequence} is that it doesn't need upfront conversion to {@link TemplateModel}-s (performance).
+ */
+class NativeStringArraySequence implements TemplateSequenceModel {
+
+    private final String[] items;
+
+    public NativeStringArraySequence(String[] items) {
+        this.items = items;
+    }
+
+    @Override
+    public TemplateModel get(int index) throws TemplateModelException {
+        return index < items.length ? new SimpleScalar(items[index]) : null;
+    }
+
+    @Override
+    public int size() throws TemplateModelException {
+        return items.length;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/NativeStringCollectionCollectionEx.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/NativeStringCollectionCollectionEx.java b/freemarker-core/src/main/java/org/apache/freemarker/core/NativeStringCollectionCollectionEx.java
new file mode 100644
index 0000000..b2437e7
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/NativeStringCollectionCollectionEx.java
@@ -0,0 +1,79 @@
+/*
+ * 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.util.Collection;
+import java.util.Iterator;
+
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateCollectionModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+import org.apache.freemarker.core.model.impl.DefaultNonListCollectionAdapter;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+
+/**
+ * Adapts (not copies) a {@link Collection} of {@link String}-s with on-the-fly wrapping of the items to {@link
+ * SimpleScalar}-s. The important difference to {@link DefaultNonListCollectionAdapter} is that it doesn't depend on an
+ * {@link ObjectWrapper}, which is needed to guarantee the behavior of some template language constructs. The important
+ * difference to {@link NativeCollectionEx} is that it doesn't need upfront conversion to {@link TemplateModel}-s
+ * (performance).
+ */
+class NativeStringCollectionCollectionEx implements TemplateCollectionModelEx {
+
+    private final Collection<String> collection;
+
+    public NativeStringCollectionCollectionEx(Collection<String> collection) {
+        this.collection = collection;
+    }
+
+    @Override
+    public int size() {
+        return collection.size();
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return collection.isEmpty();
+    }
+
+    @Override
+    public TemplateModelIterator iterator() throws TemplateModelException {
+        return new TemplateModelIterator() {
+
+            private final Iterator<String> iterator = collection.iterator();
+
+            @Override
+            public TemplateModel next() throws TemplateModelException {
+                if (!iterator.hasNext()) {
+                    throw new TemplateModelException("The collection has no more items.");
+                }
+
+                return new SimpleScalar(iterator.next());
+            }
+
+            @Override
+            public boolean hasNext() {
+                return iterator.hasNext();
+            }
+        };
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/NativeStringListSequence.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/NativeStringListSequence.java b/freemarker-core/src/main/java/org/apache/freemarker/core/NativeStringListSequence.java
new file mode 100644
index 0000000..7846fd3
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/NativeStringListSequence.java
@@ -0,0 +1,56 @@
+/*
+ * 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.util.List;
+
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.impl.DefaultListAdapter;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+
+/**
+ * Adapts (not copies) a {@link List} of {@link String}-s with on-the-fly wrapping of the items to {@link
+ * SimpleScalar}-s. The important difference to {@link DefaultListAdapter} is that it doesn't depend on an {@link
+ * ObjectWrapper}, which is needed to guarantee the behavior of some template language constructs. The important
+ * difference to {@link NativeSequence} is that it doesn't need upfront conversion to {@link TemplateModel}-s
+ * (performance).
+ */
+class NativeStringListSequence implements TemplateSequenceModel {
+
+    private final List<String> items;
+
+    public NativeStringListSequence(List<String> items) {
+        this.items = items;
+    }
+
+    @Override
+    public TemplateModel get(int index) throws TemplateModelException {
+        return index < items.size() ? new SimpleScalar(items.get(index)) : null;
+    }
+
+    @Override
+    public int size() throws TemplateModelException {
+        return items.size();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/NestedContentNotSupportedException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/NestedContentNotSupportedException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/NestedContentNotSupportedException.java
new file mode 100644
index 0000000..cca60ad
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/NestedContentNotSupportedException.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.Environment.NestedElementTemplateDirectiveBody;
+import org.apache.freemarker.core.ThreadInterruptionSupportTemplatePostProcessor.ASTThreadInterruptionCheck;
+import org.apache.freemarker.core.model.TemplateDirectiveBody;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * Used in custom {@link org.apache.freemarker.core.model.TemplateDirectiveModel}-s to check if the directive invocation
+ * has no body. This is more intelligent than a {@code null} check; for example, when the body
+ * only contains a thread interruption check node, it treats it as valid.
+ */
+public class NestedContentNotSupportedException extends TemplateException {
+
+    public static void check(TemplateDirectiveBody body) throws NestedContentNotSupportedException {
+        if (body == null) {
+            return;
+        }
+        if (body instanceof NestedElementTemplateDirectiveBody) {
+            ASTElement[] tes = ((NestedElementTemplateDirectiveBody) body).getChildrenBuffer();
+            if (tes == null || tes.length == 0
+                    || tes[0] instanceof ASTThreadInterruptionCheck && (tes.length == 1 || tes[1] == null)) {
+                return;
+            }
+        }
+        throw new NestedContentNotSupportedException(Environment.getCurrentEnvironment());
+    }
+    
+    
+    private NestedContentNotSupportedException(Environment env) {
+        this(null, null, env);
+    }
+
+    private NestedContentNotSupportedException(Exception cause, Environment env) {
+        this(null, cause, env);
+    }
+
+    private NestedContentNotSupportedException(String description, Environment env) {
+        this(description, null, env);
+    }
+
+    private NestedContentNotSupportedException(String description, Exception cause, Environment env) {
+        super( "Nested content (body) not supported."
+                + (description != null ? " " + _StringUtil.jQuote(description) : ""),
+                cause, env);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/NonBooleanException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/NonBooleanException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/NonBooleanException.java
new file mode 100644
index 0000000..3844fd0
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/NonBooleanException.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 org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateModel;
+
+/**
+ * Indicates that a {@link TemplateBooleanModel} value was expected, but the value had a different type.
+ */
+public class NonBooleanException extends UnexpectedTypeException {
+    
+    private static final Class[] EXPECTED_TYPES = new Class[] { TemplateBooleanModel.class }; 
+
+    public NonBooleanException(Environment env) {
+        super(env, "Expecting boolean value here");
+    }
+
+    public NonBooleanException(String description, Environment env) {
+        super(env, description);
+    }
+
+    NonBooleanException(Environment env, _ErrorDescriptionBuilder description) {
+        super(env, description);
+    }
+
+    NonBooleanException(
+            ASTExpression blamed, TemplateModel model, Environment env)
+            throws InvalidReferenceException {
+        super(blamed, model, "boolean", EXPECTED_TYPES, env);
+    }
+
+    NonBooleanException(
+            ASTExpression blamed, TemplateModel model, String tip,
+            Environment env)
+            throws InvalidReferenceException {
+        super(blamed, model, "boolean", EXPECTED_TYPES, tip, env);
+    }
+
+    NonBooleanException(
+            ASTExpression blamed, TemplateModel model, String[] tips, Environment env) throws InvalidReferenceException {
+        super(blamed, model, "boolean", EXPECTED_TYPES, tips, env);
+    }    
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/NonDateException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/NonDateException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/NonDateException.java
new file mode 100644
index 0000000..2e63e48
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/NonDateException.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 org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateModel;
+
+/**
+ * Indicates that a {@link TemplateDateModel} value was expected, but the value had a different type.
+ */
+public class NonDateException extends UnexpectedTypeException {
+
+    private static final Class[] EXPECTED_TYPES = new Class[] { TemplateDateModel.class };
+
+    public NonDateException(Environment env) {
+        super(env, "Expecting date/time value here");
+    }
+
+    public NonDateException(String description, Environment env) {
+        super(env, description);
+    }
+
+    NonDateException(
+            ASTExpression blamed, TemplateModel model, Environment env)
+            throws InvalidReferenceException {
+        super(blamed, model, "date/time", EXPECTED_TYPES, env);
+    }
+
+    NonDateException(
+            ASTExpression blamed, TemplateModel model, String tip,
+            Environment env)
+            throws InvalidReferenceException {
+        super(blamed, model, "date/time", EXPECTED_TYPES, tip, env);
+    }
+
+    NonDateException(
+            ASTExpression blamed, TemplateModel model, String[] tips, Environment env) throws InvalidReferenceException {
+        super(blamed, model, "date/time", EXPECTED_TYPES, tips, env);
+    }    
+        
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/NonExtendedHashException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/NonExtendedHashException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/NonExtendedHashException.java
new file mode 100644
index 0000000..5614b23
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/NonExtendedHashException.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 org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+
+/**
+ * Indicates that a {@link TemplateHashModelEx} value was expected, but the value had a different type.
+ */
+public class NonExtendedHashException extends UnexpectedTypeException {
+
+    private static final Class[] EXPECTED_TYPES = new Class[] { TemplateHashModelEx.class };
+    
+    public NonExtendedHashException(Environment env) {
+        super(env, "Expecting extended hash value here");
+    }
+
+    public NonExtendedHashException(String description, Environment env) {
+        super(env, description);
+    }
+
+    NonExtendedHashException(Environment env, _ErrorDescriptionBuilder description) {
+        super(env, description);
+    }
+
+    NonExtendedHashException(
+            ASTExpression blamed, TemplateModel model, Environment env)
+            throws InvalidReferenceException {
+        super(blamed, model, "extended hash", EXPECTED_TYPES, env);
+    }
+
+    NonExtendedHashException(
+            ASTExpression blamed, TemplateModel model, String tip,
+            Environment env)
+            throws InvalidReferenceException {
+        super(blamed, model, "extended hash", EXPECTED_TYPES, tip, env);
+    }
+
+    NonExtendedHashException(
+            ASTExpression blamed, TemplateModel model, String[] tips, Environment env) throws InvalidReferenceException {
+        super(blamed, model, "extended hash", EXPECTED_TYPES, tips, env);
+    }    
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/NonExtendedNodeException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/NonExtendedNodeException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/NonExtendedNodeException.java
new file mode 100644
index 0000000..b95cf77
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/NonExtendedNodeException.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 org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateNodeModelEx;
+
+/**
+ * Indicates that a {@link TemplateNodeModelEx} value was expected, but the value had a different type.
+ * 
+ * @since 2.3.26
+ */
+public class NonExtendedNodeException extends UnexpectedTypeException {
+
+    private static final Class<?>[] EXPECTED_TYPES = new Class[] { TemplateNodeModelEx.class };
+    
+    public NonExtendedNodeException(Environment env) {
+        super(env, "Expecting extended node value here");
+    }
+
+    public NonExtendedNodeException(String description, Environment env) {
+        super(env, description);
+    }
+
+    NonExtendedNodeException(Environment env, _ErrorDescriptionBuilder description) {
+        super(env, description);
+    }
+
+    NonExtendedNodeException(
+            ASTExpression blamed, TemplateModel model, Environment env)
+            throws InvalidReferenceException {
+        super(blamed, model, "extended node", EXPECTED_TYPES, env);
+    }
+
+    NonExtendedNodeException(
+            ASTExpression blamed, TemplateModel model, String tip,
+            Environment env)
+            throws InvalidReferenceException {
+        super(blamed, model, "extended node", EXPECTED_TYPES, tip, env);
+    }
+
+    NonExtendedNodeException(
+            ASTExpression blamed, TemplateModel model, String[] tips, Environment env) throws InvalidReferenceException {
+        super(blamed, model, "extended node", EXPECTED_TYPES, tips, env);
+    }    
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/NonHashException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/NonHashException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/NonHashException.java
new file mode 100644
index 0000000..7c26bf2
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/NonHashException.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 org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateModel;
+
+/**
+ * Indicates that a {@link TemplateHashModel} value was expected, but the value had a different type.
+ * 
+ * @since 2.3.21
+ */
+public class NonHashException extends UnexpectedTypeException {
+
+    private static final Class[] EXPECTED_TYPES = new Class[] { TemplateHashModel.class };
+    
+    public NonHashException(Environment env) {
+        super(env, "Expecting hash value here");
+    }
+
+    public NonHashException(String description, Environment env) {
+        super(env, description);
+    }
+
+    NonHashException(Environment env, _ErrorDescriptionBuilder description) {
+        super(env, description);
+    }
+
+    NonHashException(
+            ASTExpression blamed, TemplateModel model, Environment env)
+            throws InvalidReferenceException {
+        super(blamed, model, "hash", EXPECTED_TYPES, env);
+    }
+
+    NonHashException(
+            ASTExpression blamed, TemplateModel model, String tip,
+            Environment env)
+            throws InvalidReferenceException {
+        super(blamed, model, "hash", EXPECTED_TYPES, tip, env);
+    }
+
+    NonHashException(
+            ASTExpression blamed, TemplateModel model, String[] tips, Environment env) throws InvalidReferenceException {
+        super(blamed, model, "hash", EXPECTED_TYPES, tips, env);
+    }    
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/NonMarkupOutputException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/NonMarkupOutputException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/NonMarkupOutputException.java
new file mode 100644
index 0000000..9a2d5c4
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/NonMarkupOutputException.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 org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.TemplateModel;
+
+/**
+ * Indicates that a {@link TemplateMarkupOutputModel} value was expected, but the value had a different type.
+ * 
+ * @since 2.3.24
+ */
+public class NonMarkupOutputException extends UnexpectedTypeException {
+
+    private static final Class[] EXPECTED_TYPES = new Class[] { TemplateMarkupOutputModel.class };
+    
+    public NonMarkupOutputException(Environment env) {
+        super(env, "Expecting markup output value here");
+    }
+
+    public NonMarkupOutputException(String description, Environment env) {
+        super(env, description);
+    }
+
+    NonMarkupOutputException(Environment env, _ErrorDescriptionBuilder description) {
+        super(env, description);
+    }
+
+    NonMarkupOutputException(
+            ASTExpression blamed, TemplateModel model, Environment env)
+            throws InvalidReferenceException {
+        super(blamed, model, "markup output", EXPECTED_TYPES, env);
+    }
+
+    NonMarkupOutputException(
+            ASTExpression blamed, TemplateModel model, String tip,
+            Environment env)
+            throws InvalidReferenceException {
+        super(blamed, model, "markup output", EXPECTED_TYPES, tip, env);
+    }
+
+    NonMarkupOutputException(
+            ASTExpression blamed, TemplateModel model, String[] tips, Environment env) throws InvalidReferenceException {
+        super(blamed, model, "markup output", EXPECTED_TYPES, tips, env);
+    }    
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/NonMethodException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/NonMethodException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/NonMethodException.java
new file mode 100644
index 0000000..b6c461e
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/NonMethodException.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 org.apache.freemarker.core.model.TemplateMethodModel;
+import org.apache.freemarker.core.model.TemplateModel;
+
+/**
+ * Indicates that a {@link TemplateMethodModel} value was expected, but the value had a different type.
+ * 
+ * @since 2.3.21
+ */
+public class NonMethodException extends UnexpectedTypeException {
+
+    private static final Class[] EXPECTED_TYPES = new Class[] { TemplateMethodModel.class };
+    
+    public NonMethodException(Environment env) {
+        super(env, "Expecting method value here");
+    }
+
+    public NonMethodException(String description, Environment env) {
+        super(env, description);
+    }
+
+    NonMethodException(Environment env, _ErrorDescriptionBuilder description) {
+        super(env, description);
+    }
+
+    NonMethodException(
+            ASTExpression blamed, TemplateModel model, Environment env)
+            throws InvalidReferenceException {
+        super(blamed, model, "method", EXPECTED_TYPES, env);
+    }
+
+    NonMethodException(
+            ASTExpression blamed, TemplateModel model, String tip,
+            Environment env)
+            throws InvalidReferenceException {
+        super(blamed, model, "method", EXPECTED_TYPES, tip, env);
+    }
+
+    NonMethodException(
+            ASTExpression blamed, TemplateModel model, String[] tips, Environment env) throws InvalidReferenceException {
+        super(blamed, model, "method", EXPECTED_TYPES, tips, env);
+    }    
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/NonNamespaceException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/NonNamespaceException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/NonNamespaceException.java
new file mode 100644
index 0000000..bf66312
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/NonNamespaceException.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 org.apache.freemarker.core.model.TemplateModel;
+
+/**
+ * Indicates that a {@link Environment.Namespace} value was expected, but the value had a different type.
+ * 
+ * @since 2.3.21
+ */
+class NonNamespaceException extends UnexpectedTypeException {
+
+    private static final Class[] EXPECTED_TYPES = new Class[] { Environment.Namespace.class };
+    
+    public NonNamespaceException(Environment env) {
+        super(env, "Expecting namespace value here");
+    }
+
+    public NonNamespaceException(String description, Environment env) {
+        super(env, description);
+    }
+
+    NonNamespaceException(Environment env, _ErrorDescriptionBuilder description) {
+        super(env, description);
+    }
+
+    NonNamespaceException(
+            ASTExpression blamed, TemplateModel model, Environment env)
+            throws InvalidReferenceException {
+        super(blamed, model, "namespace", EXPECTED_TYPES, env);
+    }
+
+    NonNamespaceException(
+            ASTExpression blamed, TemplateModel model, String tip,
+            Environment env)
+            throws InvalidReferenceException {
+        super(blamed, model, "namespace", EXPECTED_TYPES, tip, env);
+    }
+
+    NonNamespaceException(
+            ASTExpression blamed, TemplateModel model, String[] tips, Environment env) throws InvalidReferenceException {
+        super(blamed, model, "namespace", EXPECTED_TYPES, tips, env);
+    }    
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/NonNodeException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/NonNodeException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/NonNodeException.java
new file mode 100644
index 0000000..9c9e566
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/NonNodeException.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 org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateNodeModel;
+
+/**
+ * Indicates that a {@link TemplateNodeModel} value was expected, but the value had a different type.
+ * 
+ * @since 2.3.21
+ */
+public class NonNodeException extends UnexpectedTypeException {
+
+    private static final Class[] EXPECTED_TYPES = new Class[] { TemplateNodeModel.class };
+    
+    public NonNodeException(Environment env) {
+        super(env, "Expecting node value here");
+    }
+
+    public NonNodeException(String description, Environment env) {
+        super(env, description);
+    }
+
+    NonNodeException(Environment env, _ErrorDescriptionBuilder description) {
+        super(env, description);
+    }
+
+    NonNodeException(
+            ASTExpression blamed, TemplateModel model, Environment env)
+            throws InvalidReferenceException {
+        super(blamed, model, "node", EXPECTED_TYPES, env);
+    }
+
+    NonNodeException(
+            ASTExpression blamed, TemplateModel model, String tip,
+            Environment env)
+            throws InvalidReferenceException {
+        super(blamed, model, "node", EXPECTED_TYPES, tip, env);
+    }
+
+    NonNodeException(
+            ASTExpression blamed, TemplateModel model, String[] tips, Environment env) throws InvalidReferenceException {
+        super(blamed, model, "node", EXPECTED_TYPES, tips, env);
+    }    
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/NonNumericalException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/NonNumericalException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/NonNumericalException.java
new file mode 100644
index 0000000..f70bd83
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/NonNumericalException.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+
+/**
+ * Indicates that a {@link TemplateNumberModel} value was expected, but the value had a different type.
+ */
+public class NonNumericalException extends UnexpectedTypeException {
+
+    private static final Class[] EXPECTED_TYPES = new Class[] { TemplateNumberModel.class };
+    
+    public NonNumericalException(Environment env) {
+        super(env, "Expecting numerical value here");
+    }
+
+    public NonNumericalException(String description, Environment env) {
+        super(env, description);
+    }
+ 
+    NonNumericalException(_ErrorDescriptionBuilder description, Environment env) {
+        super(env, description);
+    }
+
+    NonNumericalException(
+            ASTExpression blamed, TemplateModel model, Environment env)
+            throws InvalidReferenceException {
+        super(blamed, model, "number", EXPECTED_TYPES, env);
+    }
+
+    NonNumericalException(
+            ASTExpression blamed, TemplateModel model, String tip,
+            Environment env)
+            throws InvalidReferenceException {
+        super(blamed, model, "number", EXPECTED_TYPES, tip, env);
+    }
+
+    NonNumericalException(
+            ASTExpression blamed, TemplateModel model, String[] tips, Environment env) throws InvalidReferenceException {
+        super(blamed, model, "number", EXPECTED_TYPES, tips, env);
+    }
+
+    NonNumericalException(
+            String assignmentTargetVarName, TemplateModel model, String[] tips, Environment env)
+            throws InvalidReferenceException {
+        super(assignmentTargetVarName, model, "number", EXPECTED_TYPES, tips, env);
+    }
+    static NonNumericalException newMalformedNumberException(ASTExpression blamed, String text, Environment env) {
+        return new NonNumericalException(
+                new _ErrorDescriptionBuilder("Can't convert this string to number: ", new _DelayedJQuote(text))
+                .blame(blamed),
+                env);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/NonSequenceException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/NonSequenceException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/NonSequenceException.java
new file mode 100644
index 0000000..5018dc8
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/NonSequenceException.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 org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+
+/**
+ * Indicates that a {@link TemplateSequenceModel} value was expected, but the value had a different type.
+ * 
+ * @since 2.3.21
+ */
+public class NonSequenceException extends UnexpectedTypeException {
+
+    private static final Class[] EXPECTED_TYPES = new Class[] { TemplateSequenceModel.class };
+    
+    public NonSequenceException(Environment env) {
+        super(env, "Expecting sequence value here");
+    }
+
+    public NonSequenceException(String description, Environment env) {
+        super(env, description);
+    }
+
+    NonSequenceException(Environment env, _ErrorDescriptionBuilder description) {
+        super(env, description);
+    }
+
+    NonSequenceException(
+            ASTExpression blamed, TemplateModel model, Environment env)
+            throws InvalidReferenceException {
+        super(blamed, model, "sequence", EXPECTED_TYPES, env);
+    }
+
+    NonSequenceException(
+            ASTExpression blamed, TemplateModel model, String tip,
+            Environment env)
+            throws InvalidReferenceException {
+        super(blamed, model, "sequence", EXPECTED_TYPES, tip, env);
+    }
+
+    NonSequenceException(
+            ASTExpression blamed, TemplateModel model, String[] tips, Environment env) throws InvalidReferenceException {
+        super(blamed, model, "sequence", EXPECTED_TYPES, tips, env);
+    }    
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/NonSequenceOrCollectionException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/NonSequenceOrCollectionException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/NonSequenceOrCollectionException.java
new file mode 100644
index 0000000..0baa5c5
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/NonSequenceOrCollectionException.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.WrapperTemplateModel;
+import org.apache.freemarker.core.util._CollectionUtil;
+
+/**
+ * Indicates that a {@link TemplateSequenceModel} or {@link TemplateCollectionModel} value was expected, but the value
+ * had a different type.
+ * 
+ * @since 2.3.21
+ */
+public class NonSequenceOrCollectionException extends UnexpectedTypeException {
+
+    private static final Class[] EXPECTED_TYPES = new Class[] {
+        TemplateSequenceModel.class, TemplateCollectionModel.class
+    };
+    private static final String ITERABLE_SUPPORT_HINT = "The problematic value is a java.lang.Iterable. Using "
+            + "DefaultObjectWrapper(..., iterableSupport=true) as the object_wrapper setting of the FreeMarker "
+            + "configuration should solve this.";
+    
+    public NonSequenceOrCollectionException(Environment env) {
+        super(env, "Expecting sequence or collection value here");
+    }
+
+    public NonSequenceOrCollectionException(String description, Environment env) {
+        super(env, description);
+    }
+
+    NonSequenceOrCollectionException(Environment env, _ErrorDescriptionBuilder description) {
+        super(env, description);
+    }
+
+    NonSequenceOrCollectionException(
+            ASTExpression blamed, TemplateModel model, Environment env)
+            throws InvalidReferenceException {
+        this(blamed, model, _CollectionUtil.EMPTY_OBJECT_ARRAY, env);
+    }
+
+    NonSequenceOrCollectionException(
+            ASTExpression blamed, TemplateModel model, String tip,
+            Environment env)
+            throws InvalidReferenceException {
+        this(blamed, model, new Object[] { tip }, env);
+    }
+
+    NonSequenceOrCollectionException(
+            ASTExpression blamed, TemplateModel model, Object[] tips, Environment env) throws InvalidReferenceException {
+        super(blamed, model, "sequence or collection", EXPECTED_TYPES, extendTipsIfIterable(model, tips), env);
+    }
+    
+    private static Object[] extendTipsIfIterable(TemplateModel model, Object[] tips) {
+        if (isWrappedIterable(model)) {
+            final int tipsLen = tips != null ? tips.length : 0;
+            Object[] extendedTips = new Object[tipsLen + 1];
+            for (int i = 0; i < tipsLen; i++) {
+                extendedTips[i] = tips[i];
+            }
+            extendedTips[tipsLen] = ITERABLE_SUPPORT_HINT;
+            return extendedTips;
+        } else {
+            return tips;
+        }
+    }
+
+    public static boolean isWrappedIterable(TemplateModel model) {
+        return model instanceof WrapperTemplateModel
+                && ((WrapperTemplateModel) model).getWrappedObject() instanceof Iterable;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/NonStringException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/NonStringException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/NonStringException.java
new file mode 100644
index 0000000..c8cbc6d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/NonStringException.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+
+/**
+ * Indicates that a {@link TemplateScalarModel} value was expected (or maybe something that can be automatically coerced
+ * to that), but the value had a different type.
+ */
+public class NonStringException extends UnexpectedTypeException {
+
+    static final String STRING_COERCABLE_TYPES_DESC
+            = "string or something automatically convertible to string (number, date or boolean)";
+    
+    static final Class[] STRING_COERCABLE_TYPES = new Class[] {
+        TemplateScalarModel.class, TemplateNumberModel.class, TemplateDateModel.class, TemplateBooleanModel.class
+    };
+    
+    private static final String DEFAULT_DESCRIPTION
+            = "Expecting " + NonStringException.STRING_COERCABLE_TYPES_DESC + " value here";
+
+    public NonStringException(Environment env) {
+        super(env, DEFAULT_DESCRIPTION);
+    }
+
+    public NonStringException(String description, Environment env) {
+        super(env, description);
+    }
+ 
+    NonStringException(Environment env, _ErrorDescriptionBuilder description) {
+        super(env, description);
+    }
+
+    NonStringException(
+            ASTExpression blamed, TemplateModel model, Environment env)
+            throws InvalidReferenceException {
+        super(blamed, model, NonStringException.STRING_COERCABLE_TYPES_DESC, STRING_COERCABLE_TYPES, env);
+    }
+
+    NonStringException(
+            ASTExpression blamed, TemplateModel model, String tip,
+            Environment env)
+            throws InvalidReferenceException {
+        super(blamed, model, NonStringException.STRING_COERCABLE_TYPES_DESC, STRING_COERCABLE_TYPES, tip, env);
+    }
+
+    NonStringException(
+            ASTExpression blamed, TemplateModel model, String[] tips, Environment env) throws InvalidReferenceException {
+        super(blamed, model, NonStringException.STRING_COERCABLE_TYPES_DESC, STRING_COERCABLE_TYPES, tips, env);
+    }
+        
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/NonStringOrTemplateOutputException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/NonStringOrTemplateOutputException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/NonStringOrTemplateOutputException.java
new file mode 100644
index 0000000..ddeb811
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/NonStringOrTemplateOutputException.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+
+/**
+ * Indicates that a {@link TemplateScalarModel} (or maybe something that can be automatically coerced
+ * to that) or {@link TemplateMarkupOutputModel} value was expected, but the value had a different type.
+ */
+public class NonStringOrTemplateOutputException extends UnexpectedTypeException {
+
+    static final String STRING_COERCABLE_TYPES_OR_TOM_DESC
+            = NonStringException.STRING_COERCABLE_TYPES_DESC + ", or \"template output\"";
+    
+    static final Class[] STRING_COERCABLE_TYPES_AND_TOM;
+    static {
+        STRING_COERCABLE_TYPES_AND_TOM = new Class[NonStringException.STRING_COERCABLE_TYPES.length + 1];
+        int i;
+        for (i = 0; i < NonStringException.STRING_COERCABLE_TYPES.length; i++) {
+            STRING_COERCABLE_TYPES_AND_TOM[i] = NonStringException.STRING_COERCABLE_TYPES[i];
+        }
+        STRING_COERCABLE_TYPES_AND_TOM[i] = TemplateMarkupOutputModel.class;
+    }
+
+    private static final String DEFAULT_DESCRIPTION
+            = "Expecting " + NonStringOrTemplateOutputException.STRING_COERCABLE_TYPES_OR_TOM_DESC + " value here";
+
+    public NonStringOrTemplateOutputException(Environment env) {
+        super(env, DEFAULT_DESCRIPTION);
+    }
+
+    public NonStringOrTemplateOutputException(String description, Environment env) {
+        super(env, description);
+    }
+ 
+    NonStringOrTemplateOutputException(Environment env, _ErrorDescriptionBuilder description) {
+        super(env, description);
+    }
+
+    NonStringOrTemplateOutputException(
+            ASTExpression blamed, TemplateModel model, Environment env)
+            throws InvalidReferenceException {
+        super(blamed, model, NonStringOrTemplateOutputException.STRING_COERCABLE_TYPES_OR_TOM_DESC, STRING_COERCABLE_TYPES_AND_TOM, env);
+    }
+
+    NonStringOrTemplateOutputException(
+            ASTExpression blamed, TemplateModel model, String tip,
+            Environment env)
+            throws InvalidReferenceException {
+        super(blamed, model, NonStringOrTemplateOutputException.STRING_COERCABLE_TYPES_OR_TOM_DESC, STRING_COERCABLE_TYPES_AND_TOM, tip, env);
+    }
+
+    NonStringOrTemplateOutputException(
+            ASTExpression blamed, TemplateModel model, String[] tips, Environment env) throws InvalidReferenceException {
+        super(blamed, model, NonStringOrTemplateOutputException.STRING_COERCABLE_TYPES_OR_TOM_DESC, STRING_COERCABLE_TYPES_AND_TOM, tips, env);
+    }
+        
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/NonUserDefinedDirectiveLikeException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/NonUserDefinedDirectiveLikeException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/NonUserDefinedDirectiveLikeException.java
new file mode 100644
index 0000000..918c720
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/NonUserDefinedDirectiveLikeException.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateDirectiveModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateTransformModel;
+
+/**
+ * Indicates that a {@link TemplateDirectiveModel} or {@link TemplateTransformModel} or {@link ASTDirMacro} value was
+ * expected, but the value had a different type.
+ * 
+ * @since 2.3.21
+ */
+class NonUserDefinedDirectiveLikeException extends UnexpectedTypeException {
+
+    private static final Class[] EXPECTED_TYPES = new Class[] {
+        TemplateDirectiveModel.class, TemplateTransformModel.class, ASTDirMacro.class };
+    
+    public NonUserDefinedDirectiveLikeException(Environment env) {
+        super(env, "Expecting user-defined directive, transform or macro value here");
+    }
+
+    public NonUserDefinedDirectiveLikeException(String description, Environment env) {
+        super(env, description);
+    }
+
+    NonUserDefinedDirectiveLikeException(Environment env, _ErrorDescriptionBuilder description) {
+        super(env, description);
+    }
+
+    NonUserDefinedDirectiveLikeException(
+            ASTExpression blamed, TemplateModel model, Environment env)
+            throws InvalidReferenceException {
+        super(blamed, model, "user-defined directive, transform or macro", EXPECTED_TYPES, env);
+    }
+
+    NonUserDefinedDirectiveLikeException(
+            ASTExpression blamed, TemplateModel model, String tip,
+            Environment env)
+            throws InvalidReferenceException {
+        super(blamed, model, "user-defined directive, transform or macro", EXPECTED_TYPES, tip, env);
+    }
+
+    NonUserDefinedDirectiveLikeException(
+            ASTExpression blamed, TemplateModel model, String[] tips, Environment env) throws InvalidReferenceException {
+        super(blamed, model, "user-defined directive, transform or macro", EXPECTED_TYPES, tips, env);
+    }    
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/OutputFormatBoundBuiltIn.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/OutputFormatBoundBuiltIn.java b/freemarker-core/src/main/java/org/apache/freemarker/core/OutputFormatBoundBuiltIn.java
new file mode 100644
index 0000000..c67f2c0
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/OutputFormatBoundBuiltIn.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.outputformat.OutputFormat;
+import org.apache.freemarker.core.util._NullArgumentException;
+
+abstract class OutputFormatBoundBuiltIn extends SpecialBuiltIn {
+    
+    protected OutputFormat outputFormat;
+    protected int autoEscapingPolicy;
+    
+    void bindToOutputFormat(OutputFormat outputFormat, int autoEscapingPolicy) {
+        _NullArgumentException.check(outputFormat);
+        this.outputFormat = outputFormat;
+        this.autoEscapingPolicy = autoEscapingPolicy;
+    }
+    
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        if (outputFormat == null) {
+            // The parser should prevent this situation
+            throw new NullPointerException("outputFormat was null");
+        }
+        return calculateResult(env);
+    }
+
+    protected abstract TemplateModel calculateResult(Environment env)
+            throws TemplateException;
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ParameterRole.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ParameterRole.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ParameterRole.java
new file mode 100644
index 0000000..146f0b8
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ParameterRole.java
@@ -0,0 +1,91 @@
+/*
+ * 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;
+
+// Change this to an Enum in Java 5
+/**
+ * @see ASTNode#getParameterRole(int)
+ */
+final class ParameterRole {
+    
+    private final String name;
+
+    static final ParameterRole UNKNOWN = new ParameterRole("[unknown role]");
+    
+    // When figuring out the names of these, always read them after the possible getName() values. It should sound OK.
+    // Like "`+` left hand operand", or "`#if` parameter". That is, the roles (only) have to make sense in the
+    // context of the possible ASTNode classes.
+    static final ParameterRole LEFT_HAND_OPERAND = new ParameterRole("left-hand operand"); 
+    static final ParameterRole RIGHT_HAND_OPERAND = new ParameterRole("right-hand operand"); 
+    static final ParameterRole ENCLOSED_OPERAND = new ParameterRole("enclosed operand"); 
+    static final ParameterRole ITEM_VALUE = new ParameterRole("item value"); 
+    static final ParameterRole ITEM_KEY = new ParameterRole("item key");
+    static final ParameterRole ASSIGNMENT_TARGET = new ParameterRole("assignment target");
+    static final ParameterRole ASSIGNMENT_OPERATOR = new ParameterRole("assignment operator");
+    static final ParameterRole ASSIGNMENT_SOURCE = new ParameterRole("assignment source");
+    static final ParameterRole VARIABLE_SCOPE = new ParameterRole("variable scope");
+    static final ParameterRole NAMESPACE = new ParameterRole("namespace");
+    static final ParameterRole ERROR_HANDLER = new ParameterRole("error handler");
+    static final ParameterRole PASSED_VALUE = new ParameterRole("passed value");
+    static final ParameterRole CONDITION = new ParameterRole("condition"); 
+    static final ParameterRole VALUE = new ParameterRole("value");
+    static final ParameterRole AST_NODE_SUBTYPE = new ParameterRole("AST-node subtype");
+    static final ParameterRole PLACEHOLDER_VARIABLE = new ParameterRole("placeholder variable");
+    static final ParameterRole EXPRESSION_TEMPLATE = new ParameterRole("expression template");
+    static final ParameterRole LIST_SOURCE = new ParameterRole("list source");
+    static final ParameterRole TARGET_LOOP_VARIABLE = new ParameterRole("target loop variable");
+    static final ParameterRole TEMPLATE_NAME = new ParameterRole("template name");
+    static final ParameterRole IGNORE_MISSING_PARAMETER = new ParameterRole("\"ignore_missing\" parameter");
+    static final ParameterRole PARAMETER_NAME = new ParameterRole("parameter name");
+    static final ParameterRole PARAMETER_DEFAULT = new ParameterRole("parameter default");
+    static final ParameterRole CATCH_ALL_PARAMETER_NAME = new ParameterRole("catch-all parameter name");
+    static final ParameterRole ARGUMENT_NAME = new ParameterRole("argument name");
+    static final ParameterRole ARGUMENT_VALUE = new ParameterRole("argument value");
+    static final ParameterRole CONTENT = new ParameterRole("content");
+    static final ParameterRole EMBEDDED_TEMPLATE = new ParameterRole("embedded template");
+    static final ParameterRole VALUE_PART = new ParameterRole("value part");
+    static final ParameterRole MINIMUM_DECIMALS = new ParameterRole("minimum decimals");
+    static final ParameterRole MAXIMUM_DECIMALS = new ParameterRole("maximum decimals");
+    static final ParameterRole NODE = new ParameterRole("node");
+    static final ParameterRole CALLEE = new ParameterRole("callee");
+    static final ParameterRole MESSAGE = new ParameterRole("message");
+    
+    private ParameterRole(String name) {
+        this.name = name;
+    }
+    
+    static ParameterRole forBinaryOperatorOperand(int paramIndex) {
+        switch (paramIndex) {
+        case 0: return LEFT_HAND_OPERAND;
+        case 1: return RIGHT_HAND_OPERAND;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+    
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public String toString() {
+        return name;
+    }
+    
+}


[17/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/XMLOutputFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/XMLOutputFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/XMLOutputFormat.java
new file mode 100644
index 0000000..644f323
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/XMLOutputFormat.java
@@ -0,0 +1,77 @@
+/*
+ * 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.outputformat.impl;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.outputformat.CommonMarkupOutputFormat;
+import org.apache.freemarker.core.outputformat.OutputFormat;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * Represents the XML output format (MIME type "application/xml", name "XML"). This format escapes by default (via
+ * {@link _StringUtil#XMLEnc(String)}). The {@code ?html}, {@code ?xhtml} and {@code ?xml} built-ins silently bypass
+ * template output values of the type produced by this output format ({@link TemplateXHTMLOutputModel}).
+ * 
+ * @since 2.3.24
+ */
+public final class XMLOutputFormat extends CommonMarkupOutputFormat<TemplateXMLOutputModel> {
+
+    /**
+     * The only instance (singleton) of this {@link OutputFormat}.
+     */
+    public static final XMLOutputFormat INSTANCE = new XMLOutputFormat();
+
+    private XMLOutputFormat() {
+        // Only to decrease visibility
+    }
+
+    @Override
+    public String getName() {
+        return "XML";
+    }
+
+    @Override
+    public String getMimeType() {
+        return "application/xml";
+    }
+
+    @Override
+    public void output(String textToEsc, Writer out) throws IOException, TemplateModelException {
+        _StringUtil.XMLEnc(textToEsc, out);
+    }
+
+    @Override
+    public String escapePlainText(String plainTextContent) {
+        return _StringUtil.XMLEnc(plainTextContent);
+    }
+
+    @Override
+    public boolean isLegacyBuiltInBypassed(String builtInName) {
+        return builtInName.equals("xml");
+    }
+
+    @Override
+    protected TemplateXMLOutputModel newTemplateMarkupOutputModel(String plainTextContent, String markupContent) {
+        return new TemplateXMLOutputModel(plainTextContent, markupContent);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/package.html
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/package.html b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/package.html
new file mode 100644
index 0000000..6cb5c21
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/package.html
@@ -0,0 +1,26 @@
+<!--
+  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.
+-->
+<html>
+<head>
+</head>
+<body>
+<p>Template output format: Standard implementations. This package is part of the
+published API, that is, user code can safely depend on it.</p>
+</body>
+</html>

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/package.html
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/package.html b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/package.html
new file mode 100644
index 0000000..b25de83
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/package.html
@@ -0,0 +1,25 @@
+<!--
+  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.
+-->
+<html>
+<head>
+</head>
+<body>
+<p>Template output format: Base classes/interfaces</p>
+</body>
+</html>

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/package.html
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/package.html b/freemarker-core/src/main/java/org/apache/freemarker/core/package.html
new file mode 100644
index 0000000..be9dab9
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/package.html
@@ -0,0 +1,27 @@
+<!--
+  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.
+-->
+<html>
+<head>
+</head>
+<body>
+<p><b>The most commonly used API-s of FreeMarker;</b>
+start with {@link freemarker.template.Configuration Configuration} (see also the
+<a href="http://freemarker.org/docs/pgui_quickstart.html" target="_blank">Getting Stared</a> in the Manual.)</p>
+</body>
+</html>

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/AndMatcher.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/AndMatcher.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/AndMatcher.java
new file mode 100644
index 0000000..27b4156
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/AndMatcher.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.templateresolver;
+
+import java.io.IOException;
+
+/**
+ * Logical "and" operation among the given matchers.
+ * 
+ * @since 2.3.24
+ */
+public class AndMatcher extends TemplateSourceMatcher {
+    
+    private final TemplateSourceMatcher[] matchers;
+    
+    public AndMatcher(TemplateSourceMatcher... matchers) {
+        if (matchers.length == 0) throw new IllegalArgumentException("Need at least 1 matcher, had 0.");
+        this.matchers = matchers;
+    }
+
+    @Override
+    public boolean matches(String sourceName, Object templateSource) throws IOException {
+        for (TemplateSourceMatcher matcher : matchers) {
+            if (!(matcher.matches(sourceName, templateSource))) return false;
+        }
+        return true;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/CacheStorage.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/CacheStorage.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/CacheStorage.java
new file mode 100644
index 0000000..c70fa94
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/CacheStorage.java
@@ -0,0 +1,37 @@
+/*
+ * 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.templateresolver;
+
+import org.apache.freemarker.core.Configuration;
+
+/**
+ * Cache storage abstracts away the storage aspects of a cache - associating
+ * an object with a key, retrieval and removal via the key. It is actually a
+ * small subset of the {@link java.util.Map} interface. 
+ * The implementations must be thread safe.
+ *
+ * @see Configuration#getCacheStorage()
+ */
+public interface CacheStorage {
+    Object get(Object key);
+    void put(Object key, Object value);
+    void remove(Object key);
+    void clear();
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/CacheStorageWithGetSize.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/CacheStorageWithGetSize.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/CacheStorageWithGetSize.java
new file mode 100644
index 0000000..945d049
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/CacheStorageWithGetSize.java
@@ -0,0 +1,36 @@
+/*
+ * 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.templateresolver;
+
+/**
+ * A cache storage that has a {@code getSize()} method for returning the current number of cache entries.
+ * 
+ * @since 2.3.21
+ */
+public interface CacheStorageWithGetSize extends CacheStorage {
+    
+    /**
+     * Returns the current number of cache entries. This is intended to be used for monitoring. Note that depending on
+     * the implementation, the cost of this operation is not necessary trivial, although calling it a few times per
+     * minute should not be a problem.
+     */
+    int getSize();
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/ConditionalTemplateConfigurationFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/ConditionalTemplateConfigurationFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/ConditionalTemplateConfigurationFactory.java
new file mode 100644
index 0000000..8fab61f
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/ConditionalTemplateConfigurationFactory.java
@@ -0,0 +1,65 @@
+/*
+ * 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.templateresolver;
+
+import java.io.IOException;
+
+import org.apache.freemarker.core.TemplateConfiguration;
+
+/**
+ * Returns the given {@link TemplateConfiguration} directly, or another {@link TemplateConfigurationFactory}'s result, when
+ * the specified matcher matches the template source.
+ * 
+ * @since 2.3.24
+ */
+public class ConditionalTemplateConfigurationFactory extends TemplateConfigurationFactory {
+
+    private final TemplateSourceMatcher matcher;
+    private final TemplateConfiguration templateConfiguration;
+    private final TemplateConfigurationFactory templateConfigurationFactory;
+
+    public ConditionalTemplateConfigurationFactory(
+            TemplateSourceMatcher matcher, TemplateConfigurationFactory templateConfigurationFactory) {
+        this.matcher = matcher;
+        templateConfiguration = null;
+        this.templateConfigurationFactory = templateConfigurationFactory;
+    }
+    
+    public ConditionalTemplateConfigurationFactory(
+            TemplateSourceMatcher matcher, TemplateConfiguration templateConfiguration) {
+        this.matcher = matcher;
+        this.templateConfiguration = templateConfiguration;
+        templateConfigurationFactory = null;
+    }
+
+    @Override
+    public TemplateConfiguration get(String sourceName, TemplateLoadingSource templateLoadingSource)
+            throws IOException, TemplateConfigurationFactoryException {
+        if (matcher.matches(sourceName, templateLoadingSource)) {
+            if (templateConfigurationFactory != null) {
+                return templateConfigurationFactory.get(sourceName, templateLoadingSource);
+            } else {
+                return templateConfiguration;
+            }
+        } else {
+            return null;
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/FileExtensionMatcher.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/FileExtensionMatcher.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/FileExtensionMatcher.java
new file mode 100644
index 0000000..c89a478
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/FileExtensionMatcher.java
@@ -0,0 +1,85 @@
+/*
+ * 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.templateresolver;
+
+import java.io.IOException;
+
+/**
+ * Matches the file extension; unlike other matchers, by default case <em>insensitive</em>. A name (a path) is
+ * considered to have the given extension exactly if it ends with a dot plus the extension. 
+ * 
+ * @since 2.3.24
+ */
+public class FileExtensionMatcher extends TemplateSourceMatcher {
+
+    private final String extension;
+    private boolean caseInsensitive = true;
+    
+    /**
+     * @param extension
+     *            The file extension (without the initial dot). Can't contain there characters:
+     *            {@code '/'}, {@code '*'}, {@code '?'}. May contains {@code '.'}, but can't start with it.
+     */
+    public FileExtensionMatcher(String extension) {
+        if (extension.indexOf('/') != -1) {
+            throw new IllegalArgumentException("A file extension can't contain \"/\": " + extension);
+        }
+        if (extension.indexOf('*') != -1) {
+            throw new IllegalArgumentException("A file extension can't contain \"*\": " + extension);
+        }
+        if (extension.indexOf('?') != -1) {
+            throw new IllegalArgumentException("A file extension can't contain \"*\": " + extension);
+        }
+        if (extension.startsWith(".")) {
+            throw new IllegalArgumentException("A file extension can't start with \".\": " + extension);
+        }
+        this.extension = extension;
+    }
+
+    @Override
+    public boolean matches(String sourceName, Object templateSource) throws IOException {
+        int ln = sourceName.length();
+        int extLn = extension.length();
+        if (ln < extLn + 1 || sourceName.charAt(ln - extLn - 1) != '.') {
+            return false;
+        }
+        
+        return sourceName.regionMatches(caseInsensitive, ln - extLn, extension, 0, extLn);
+    }
+    
+    public boolean isCaseInsensitive() {
+        return caseInsensitive;
+    }
+    
+    /**
+     * Sets if the matching will be case insensitive (UNICODE compliant); default is {@code true}.
+     */
+    public void setCaseInsensitive(boolean caseInsensitive) {
+        this.caseInsensitive = caseInsensitive;
+    }
+    
+    /**
+     * Fluid API variation of {@link #setCaseInsensitive(boolean)}
+     */
+    public FileExtensionMatcher caseInsensitive(boolean caseInsensitive) {
+        setCaseInsensitive(caseInsensitive);
+        return this;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/FileNameGlobMatcher.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/FileNameGlobMatcher.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/FileNameGlobMatcher.java
new file mode 100644
index 0000000..7a9692a
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/FileNameGlobMatcher.java
@@ -0,0 +1,86 @@
+/*
+ * 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.templateresolver;
+
+import java.io.IOException;
+import java.util.regex.Pattern;
+
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * As opposed to {@link PathGlobMatcher}, it only compares the "file name" part (the part after the last {@code /}) of
+ * the source name with the given glob. For example, the file name glob {@code *.ftlh} matches both {@code foo.ftlh} and
+ * {@code foo/bar.ftlh}. With other words, that file name glob is equivalent with the {@code **}{@code /*.ftlh})
+ * <em>path</em> glob ( {@link PathGlobMatcher}).
+ * 
+ * @since 2.3.24
+ */
+public class FileNameGlobMatcher extends TemplateSourceMatcher {
+
+    private final String glob;
+    
+    private Pattern pattern;
+    private boolean caseInsensitive;
+    
+    /**
+     * @param glob
+     *            Glob with the syntax defined by {@link _StringUtil#globToRegularExpression(String, boolean)}. Must not
+     *            start with {@code /}.
+     */
+    public FileNameGlobMatcher(String glob) {
+        if (glob.indexOf('/') != -1) {
+            throw new IllegalArgumentException("A file name glob can't contain \"/\": " + glob);
+        }
+        this.glob = glob;
+        buildPattern();
+    }
+
+    private void buildPattern() {
+        pattern = _StringUtil.globToRegularExpression("**/" + glob, caseInsensitive);
+    }
+
+    @Override
+    public boolean matches(String sourceName, Object templateSource) throws IOException {
+        return pattern.matcher(sourceName).matches();
+    }
+    
+    public boolean isCaseInsensitive() {
+        return caseInsensitive;
+    }
+    
+    /**
+     * Sets if the matching will be case insensitive (UNICODE compliant); default is {@code false}.
+     */
+    public void setCaseInsensitive(boolean caseInsensitive) {
+        boolean lastCaseInsensitive = this.caseInsensitive;
+        this.caseInsensitive = caseInsensitive;
+        if (lastCaseInsensitive != caseInsensitive) {
+            buildPattern();
+        }
+    }
+    
+    /**
+     * Fluid API variation of {@link #setCaseInsensitive(boolean)}
+     */
+    public FileNameGlobMatcher caseInsensitive(boolean caseInsensitive) {
+        setCaseInsensitive(caseInsensitive);
+        return this;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/FirstMatchTemplateConfigurationFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/FirstMatchTemplateConfigurationFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/FirstMatchTemplateConfigurationFactory.java
new file mode 100644
index 0000000..0f09d3d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/FirstMatchTemplateConfigurationFactory.java
@@ -0,0 +1,110 @@
+/*
+ * 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.templateresolver;
+
+import java.io.IOException;
+
+import org.apache.freemarker.core.TemplateConfiguration;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * Returns the first non-{@code null} result of the child factories, ignoring all further child factories. The child
+ * factories are called in the order as they were added.
+ */
+public class FirstMatchTemplateConfigurationFactory extends TemplateConfigurationFactory {
+    
+    private final TemplateConfigurationFactory[] templateConfigurationFactories;
+    private boolean allowNoMatch;
+    private String noMatchErrorDetails;
+    
+    public FirstMatchTemplateConfigurationFactory(TemplateConfigurationFactory... templateConfigurationFactories) {
+        this.templateConfigurationFactories = templateConfigurationFactories;
+    }
+
+    @Override
+    public TemplateConfiguration get(String sourceName, TemplateLoadingSource templateLoadingSource)
+            throws IOException, TemplateConfigurationFactoryException {
+        for (TemplateConfigurationFactory tcf : templateConfigurationFactories) {
+            TemplateConfiguration tc = tcf.get(sourceName, templateLoadingSource); 
+            if (tc != null) {
+                return tc;
+            }
+        }
+        if (!allowNoMatch) {
+            throw new TemplateConfigurationFactoryException(
+                    FirstMatchTemplateConfigurationFactory.class.getSimpleName()
+                    + " has found no matching choice for source name "
+                    + _StringUtil.jQuote(sourceName) + ". "
+                    + (noMatchErrorDetails != null
+                            ? "Error details: " + noMatchErrorDetails 
+                            : "(Set the noMatchErrorDetails property of the factory bean to give a more specific error "
+                                    + "message. Set allowNoMatch to true if this shouldn't be an error.)"));
+        }
+        return null;
+    }
+
+    /**
+     * Getter pair of {@link #setAllowNoMatch(boolean)}.
+     */
+    public boolean getAllowNoMatch() {
+        return allowNoMatch;
+    }
+
+    /**
+     * Use this to specify if having no matching choice is an error. The default is {@code false}, that is, it's an
+     * error if there was no matching choice.
+     * 
+     * @see #setNoMatchErrorDetails(String)
+     */
+    public void setAllowNoMatch(boolean allowNoMatch) {
+        this.allowNoMatch = allowNoMatch;
+    }
+
+    /**
+     * Use this to specify the text added to the exception error message when there was no matching choice.
+     * The default is {@code null} (no error details).
+     * 
+     * @see #setAllowNoMatch(boolean)
+     */
+    public String getNoMatchErrorDetails() {
+        return noMatchErrorDetails;
+    }
+
+    
+    public void setNoMatchErrorDetails(String noMatchErrorDetails) {
+        this.noMatchErrorDetails = noMatchErrorDetails;
+    }
+    
+    /**
+     * Same as {@link #setAllowNoMatch(boolean)}, but return this object to support "fluent API" style. 
+     */
+    public FirstMatchTemplateConfigurationFactory allowNoMatch(boolean allow) {
+        setAllowNoMatch(allow);
+        return this;
+    }
+
+    /**
+     * Same as {@link #setNoMatchErrorDetails(String)}, but return this object to support "fluent API" style. 
+     */
+    public FirstMatchTemplateConfigurationFactory noMatchErrorDetails(String message) {
+        setNoMatchErrorDetails(message);
+        return this;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/GetTemplateResult.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/GetTemplateResult.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/GetTemplateResult.java
new file mode 100644
index 0000000..58c9ea9
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/GetTemplateResult.java
@@ -0,0 +1,89 @@
+/*
+ * 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.templateresolver;
+
+import java.io.Serializable;
+import java.util.Locale;
+
+import org.apache.freemarker.core.Template;
+
+/**
+ * Used for the return value of {@link TemplateResolver#getTemplate(String, Locale, Serializable)} .
+ * 
+ * @since 3.0.0
+ */
+//TODO DRAFT only [FM3]
+public final class GetTemplateResult {
+    
+    private final Template template;
+    private final String missingTemplateNormalizedName;
+    private final String missingTemplateReason;
+    private final Exception missingTemplateCauseException;
+    
+    public GetTemplateResult(Template template) {
+        this.template = template;
+        missingTemplateNormalizedName = null;
+        missingTemplateReason = null;
+        missingTemplateCauseException = null;
+    }
+    
+    public GetTemplateResult(String normalizedName, Exception missingTemplateCauseException) {
+        template = null;
+        missingTemplateNormalizedName = normalizedName;
+        missingTemplateReason = null;
+        this.missingTemplateCauseException = missingTemplateCauseException;
+    }
+    
+    public GetTemplateResult(String normalizedName, String missingTemplateReason) {
+        template = null;
+        missingTemplateNormalizedName = normalizedName;
+        this.missingTemplateReason = missingTemplateReason;
+        missingTemplateCauseException = null;
+    }
+    
+    /**
+     * The {@link Template} if it wasn't missing, otherwise {@code null}.
+     */
+    public Template getTemplate() {
+        return template;
+    }
+
+    /**
+     * When the template was missing, this <em>possibly</em> contains the explanation, or {@code null}. If the
+     * template wasn't missing (i.e., when {@link #getTemplate()} return non-{@code null}) this is always
+     * {@code null}.
+     */
+    public String getMissingTemplateReason() {
+        return missingTemplateReason != null
+                ? missingTemplateReason
+                : (missingTemplateCauseException != null
+                        ? missingTemplateCauseException.getMessage()
+                        : null);
+    }
+    
+    /**
+     * When the template was missing, this <em>possibly</em> contains its normalized name. If the template wasn't
+     * missing (i.e., when {@link #getTemplate()} return non-{@code null}) this is always {@code null}. When the
+     * template is missing, it will be {@code null} for example if the normalization itself was unsuccessful.
+     */
+    public String getMissingTemplateNormalizedName() {
+        return missingTemplateNormalizedName;
+    }
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/MalformedTemplateNameException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/MalformedTemplateNameException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/MalformedTemplateNameException.java
new file mode 100644
index 0000000..cf19e93
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/MalformedTemplateNameException.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.templateresolver;
+
+import java.io.IOException;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.TemplateNotFoundException;
+import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateNameFormat;
+import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateNameFormatFM2;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * Indicates that the template name given was malformed according the {@link TemplateNameFormat} in use. Note that for
+ * backward compatibility, {@link DefaultTemplateNameFormatFM2} doesn't throw this exception,
+ * {@link DefaultTemplateNameFormat} does. This exception extends {@link IOException} for backward compatibility.
+ * 
+ * @since 2.3.22
+ * 
+ * @see TemplateNotFoundException
+ * @see Configuration#getTemplate(String)
+ */
+@SuppressWarnings("serial")
+public class MalformedTemplateNameException extends IOException {
+    
+    private final String templateName;
+    private final String malformednessDescription;
+
+    public MalformedTemplateNameException(String templateName, String malformednessDescription) {
+        super("Malformed template name, " + _StringUtil.jQuote(templateName) + ": " + malformednessDescription);
+        this.templateName = templateName;
+        this.malformednessDescription = malformednessDescription;
+    }
+
+    public String getTemplateName() {
+        return templateName;
+    }
+
+    public String getMalformednessDescription() {
+        return malformednessDescription;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/MergingTemplateConfigurationFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/MergingTemplateConfigurationFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/MergingTemplateConfigurationFactory.java
new file mode 100644
index 0000000..9b3106f
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/MergingTemplateConfigurationFactory.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.templateresolver;
+
+import java.io.IOException;
+
+import org.apache.freemarker.core.TemplateConfiguration;
+
+/**
+ * Returns the merged results of all the child factories. The factories are merged in the order as they were added.
+ * {@code null} results from the child factories will be ignored. If all child factories return {@code null}, the result
+ * of this factory will be {@code null} too.
+ * 
+ * @since 2.3.24
+ */
+public class MergingTemplateConfigurationFactory extends TemplateConfigurationFactory {
+    
+    private final TemplateConfigurationFactory[] templateConfigurationFactories;
+    
+    public MergingTemplateConfigurationFactory(TemplateConfigurationFactory... templateConfigurationFactories) {
+        this.templateConfigurationFactories = templateConfigurationFactories;
+    }
+
+    @Override
+    public TemplateConfiguration get(String sourceName, TemplateLoadingSource templateLoadingSource)
+            throws IOException, TemplateConfigurationFactoryException {
+        TemplateConfiguration.Builder mergedTCBuilder = null;
+        TemplateConfiguration firstResultTC = null;
+        for (TemplateConfigurationFactory tcf : templateConfigurationFactories) {
+            TemplateConfiguration tc = tcf.get(sourceName, templateLoadingSource);
+            if (tc != null) {
+                if (firstResultTC == null) {
+                    firstResultTC = tc;
+                } else {
+                    if (mergedTCBuilder == null) {
+                        mergedTCBuilder = new TemplateConfiguration.Builder();
+                        mergedTCBuilder.merge(firstResultTC);
+                    }
+                    mergedTCBuilder.merge(tc);
+                }
+            }
+        }
+
+        return mergedTCBuilder == null ? firstResultTC /* Maybe null */ : mergedTCBuilder.build();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/NotMatcher.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/NotMatcher.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/NotMatcher.java
new file mode 100644
index 0000000..d608282
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/NotMatcher.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.templateresolver;
+
+import java.io.IOException;
+
+/**
+ * Logical "not" operation on the given matcher.
+ * 
+ * @since 2.3.24
+ */
+public class NotMatcher extends TemplateSourceMatcher {
+    
+    private final TemplateSourceMatcher matcher;
+    
+    public NotMatcher(TemplateSourceMatcher matcher) {
+        this.matcher = matcher;
+    }
+
+    @Override
+    public boolean matches(String sourceName, Object templateSource) throws IOException {
+        return !matcher.matches(sourceName, templateSource);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/OrMatcher.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/OrMatcher.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/OrMatcher.java
new file mode 100644
index 0000000..922f293
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/OrMatcher.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.templateresolver;
+
+import java.io.IOException;
+
+/**
+ * Logical "or" operation among the given matchers.
+ * 
+ * @since 2.3.24
+ */
+public class OrMatcher extends TemplateSourceMatcher {
+    
+    private final TemplateSourceMatcher[] matchers;
+    
+    public OrMatcher(TemplateSourceMatcher... matchers) {
+        if (matchers.length == 0) throw new IllegalArgumentException("Need at least 1 matcher, had 0.");
+        this.matchers = matchers;
+    }
+
+    @Override
+    public boolean matches(String sourceName, Object templateSource) throws IOException {
+        for (TemplateSourceMatcher matcher : matchers) {
+            if ((matcher.matches(sourceName, templateSource))) return true;
+        }
+        return false;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/PathGlobMatcher.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/PathGlobMatcher.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/PathGlobMatcher.java
new file mode 100644
index 0000000..fa4213a
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/PathGlobMatcher.java
@@ -0,0 +1,100 @@
+/*
+ * 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.templateresolver;
+
+import java.io.IOException;
+import java.util.regex.Pattern;
+
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * Matches the whole template source name (also known as template source path) with the given glob.
+ * Note that the template source name is relative to the template storage root defined by the {@link TemplateLoader};
+ * it's not the full path of a file on the file system.
+ * 
+ * <p>This glob implementation recognizes {@code **} (Ant-style directory wildcard) among others. For more details see
+ * {@link _StringUtil#globToRegularExpression(String, boolean)}.
+ * 
+ * <p>About the usage of {@code /} (slash):
+ * <ul>
+ *   <li>You aren't allowed to start the glob with {@code /}, because template names (template paths) never start with
+ *       it. 
+ *   <li>Future FreeMarker versions (compared to 2.3.24) might will support importing whole directories. Directory paths
+ *       in FreeMarker should end with {@code /}. Hence, {@code foo/bar} refers to the file {bar}, while
+ *       {@code foo/bar/} refers to the {bar} directory.
+ * </ul>
+ * 
+ * <p>By default the glob is case sensitive, but this can be changed with {@link #setCaseInsensitive(boolean)} (or
+ * {@link #caseInsensitive(boolean)}).
+ * 
+ * @since 2.3.24
+ */
+public class PathGlobMatcher extends TemplateSourceMatcher {
+    
+    private final String glob;
+    
+    private Pattern pattern;
+    private boolean caseInsensitive;
+    
+    /**
+     * @param glob
+     *            Glob with the syntax defined by {@link _StringUtil#globToRegularExpression(String, boolean)}. Must not
+     *            start with {@code /}.
+     */
+    public PathGlobMatcher(String glob) {
+        if (glob.startsWith("/")) {
+            throw new IllegalArgumentException("Absolute template paths need no inital \"/\"; remove it from: " + glob);
+        }
+        this.glob = glob;
+        buildPattern();
+    }
+
+    private void buildPattern() {
+        pattern = _StringUtil.globToRegularExpression(glob, caseInsensitive);
+    }
+    
+    @Override
+    public boolean matches(String sourceName, Object templateSource) throws IOException {
+        return pattern.matcher(sourceName).matches();
+    }
+    
+    public boolean isCaseInsensitive() {
+        return caseInsensitive;
+    }
+    
+    /**
+     * Sets if the matching will be case insensitive (UNICODE compliant); default is {@code false}.
+     */
+    public void setCaseInsensitive(boolean caseInsensitive) {
+        boolean lastCaseInsensitive = this.caseInsensitive;
+        this.caseInsensitive = caseInsensitive;
+        if (lastCaseInsensitive != caseInsensitive) {
+            buildPattern();
+        }
+    }
+    
+    /**
+     * Fluid API variation of {@link #setCaseInsensitive(boolean)}
+     */
+    public PathGlobMatcher caseInsensitive(boolean caseInsensitive) {
+        setCaseInsensitive(caseInsensitive);
+        return this;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/PathRegexMatcher.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/PathRegexMatcher.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/PathRegexMatcher.java
new file mode 100644
index 0000000..d015b1e
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/PathRegexMatcher.java
@@ -0,0 +1,54 @@
+/*
+ * 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.templateresolver;
+
+import java.io.IOException;
+import java.util.regex.Pattern;
+
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * Matches the whole template source name (also known as template source path) with the given regular expression.
+ * Note that the template source name is relative to the template storage root defined by the {@link TemplateLoader};
+ * it's not the full path of a file on the file system.
+ * 
+ * @since 2.3.24
+ */
+public class PathRegexMatcher extends TemplateSourceMatcher {
+    
+    private final Pattern pattern;
+    
+    /**
+     * @param regex
+     *            Glob with the syntax defined by {@link _StringUtil#globToRegularExpression(String)}. Must not
+     *            start with {@code /}.
+     */
+    public PathRegexMatcher(String regex) {
+        if (regex.startsWith("/")) {
+            throw new IllegalArgumentException("Absolute template paths need no inital \"/\"; remove it from: " + regex);
+        }
+        pattern = Pattern.compile(regex);
+    }
+
+    @Override
+    public boolean matches(String sourceName, Object templateSource) throws IOException {
+        return pattern.matcher(sourceName).matches();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateConfigurationFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateConfigurationFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateConfigurationFactory.java
new file mode 100644
index 0000000..fe9255d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateConfigurationFactory.java
@@ -0,0 +1,54 @@
+/*
+ * 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.templateresolver;
+
+import java.io.IOException;
+
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core.TemplateConfiguration;
+
+/**
+ * Creates (or returns) {@link TemplateConfiguration}-s for template sources.
+ * 
+ * @since 2.3.24
+ */
+public abstract class TemplateConfigurationFactory {
+
+    /**
+     * Returns (maybe creates) the {@link TemplateConfiguration} for the given template source.
+     * 
+     * @param sourceName
+     *            The name (path) that was used for {@link TemplateLoader#load}. See
+     *            {@link Template#getSourceName()} for details.
+     * @param templateLoadingSource
+     *            The object returned by {@link TemplateLoadingResult#getSource()}.
+     * 
+     * @return The {@link TemplateConfiguration} to apply, or {@code null} if the there's no {@link TemplateConfiguration} for
+     *         this template source.
+     * 
+     * @throws IOException
+     *             Typically, if there factory needs further I/O to find out more about the template source, but that
+     *             fails.
+     * @throws TemplateConfigurationFactoryException
+     *             If there's a problem that's specific to the factory logic.
+     */
+    public abstract TemplateConfiguration get(String sourceName, TemplateLoadingSource templateLoadingSource)
+            throws IOException, TemplateConfigurationFactoryException;
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateConfigurationFactoryException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateConfigurationFactoryException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateConfigurationFactoryException.java
new file mode 100644
index 0000000..26c4c7e
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateConfigurationFactoryException.java
@@ -0,0 +1,36 @@
+/*
+ * 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.templateresolver;
+
+/**
+ * Non-I/O exception thrown by {@link TemplateConfigurationFactory}-s.  
+ * 
+ * @since 2.3.24
+ */
+public class TemplateConfigurationFactoryException extends Exception {
+
+    public TemplateConfigurationFactoryException(String message) {
+        super(message);
+    }
+
+    public TemplateConfigurationFactoryException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoader.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoader.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoader.java
new file mode 100644
index 0000000..fc6a4aa
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoader.java
@@ -0,0 +1,104 @@
+/*
+ * 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.templateresolver;
+
+import java.io.IOException;
+import java.io.Serializable;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.TemplateNotFoundException;
+import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateResolver;
+
+/**
+ * FreeMarker loads template "files" through objects that implement this interface, thus the templates need not be real
+ * files, and can come from any kind of data source (like classpath, servlet context, database, etc). While FreeMarker
+ * provides a few template loader implementations out-of-the-box, it's normal for embedding frameworks to use their own
+ * implementations.
+ * 
+ * <p>
+ * The {@link TemplateLoader} used by FreeMaker is specified by the {@link Configuration#getTemplateLoader()
+ * templateLoader} configuration setting.
+ * 
+ * <p>
+ * Implementations of this interface should be thread-safe.
+ * 
+ * <p>
+ * Implementations should override {@link Object#toString()} to show information about from where the
+ * {@link TemplateLoader} loads the templates. For example, for a template loader that loads template from database
+ * table {@code toString} could return something like
+ * {@code "MyDatabaseTemplateLoader(user=\"cms\", table=\"mail_templates\")"}. This string will be shown in
+ * {@link TemplateNotFoundException} exception messages, next to the template name.
+ * 
+ * <p>
+ * For those who has to dig deeper, note that the {@link TemplateLoader} is actually stored inside the
+ * {@link DefaultTemplateResolver} of the {@link Configuration}, and is normally only accessed directly by the
+ * {@link DefaultTemplateResolver}, and templates are get via the {@link DefaultTemplateResolver} API-s.
+ */
+public interface TemplateLoader {
+
+    /**
+     * Creates a new session, or returns {@code null} if the template loader implementation doesn't support sessions.
+     * See {@link TemplateLoaderSession} for more information about sessions.
+     */
+    TemplateLoaderSession createSession();
+    
+    /**
+     * Loads the template content together with meta-data such as the version (usually the last modification time).
+     * 
+     * @param name
+     *            The name (template root directory relative path) of the template, already localized and normalized by
+     *            the {@link org.apache.freemarker.core.templateresolver.impl.DefaultTemplateResolver cache}. It is completely up to the loader implementation to
+     *            interpret the name, however it should expect to receive hierarchical paths where path components are
+     *            separated by a slash (not backslash). Backslashes (or any other OS specific separator character) are
+     *            not considered as separators by FreeMarker, and thus they will not be replaced with slash before
+     *            passing to this method, so it's up to the template loader to handle them (say, by throwing an
+     *            exception that tells the user that the path (s)he has entered is invalid, as (s)he must use slash --
+     *            typical mistake of Windows users). The passed names are always considered relative to some
+     *            loader-defined root location (often referred as the "template root directory"), and will never start
+     *            with a slash, nor will they contain a path component consisting of either a single or a double dot --
+     *            these are all resolved by the template cache before passing the name to the loader. As a side effect,
+     *            paths that trivially reach outside template root directory, such as <tt>../my.ftl</tt>, will be
+     *            rejected by the template cache, so they never reach the template loader. Note again, that if the path
+     *            uses backslash as path separator instead of slash as (the template loader should not accept that), the
+     *            normalization will not properly happen, as FreeMarker (the cache) recognizes only the slashes as
+     *            separators.
+     * @param ifSourceDiffersFrom
+     *            If we only want to load the template if its source differs from this. {@code null} if you want the
+     *            template to be loaded unconditionally. If this is {@code null} then the
+     *            {@code ifVersionDiffersFrom} parameter must be {@code null} too. See
+     *            {@link TemplateLoadingResult#getSource()} for more about sources.
+     * @param ifVersionDiffersFrom
+     *            If we only want to load the template if its version (which is usually the last modification time)
+     *            differs from this. {@code null} if {@code ifSourceDiffersFrom} is {@code null}, or if the backing
+     *            storage from which the {@code ifSourceDiffersFrom} template source comes from doesn't store a version.
+     *            See {@link TemplateLoadingResult#getVersion()} for more about versions.
+     * 
+     * @return Not {@code null}.
+     */
+    TemplateLoadingResult load(String name, TemplateLoadingSource ifSourceDiffersFrom, Serializable ifVersionDiffersFrom,
+            TemplateLoaderSession session) throws IOException;
+    
+    /**
+     * Invoked by {@link Configuration#clearTemplateCache()} to instruct this template loader to throw away its current
+     * state (some kind of cache usually) and start afresh. For most {@link TemplateLoader} implementations this does
+     * nothing.
+     */
+    void resetState();
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoaderSession.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoaderSession.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoaderSession.java
new file mode 100644
index 0000000..6bf1b1f
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoaderSession.java
@@ -0,0 +1,76 @@
+/*
+ * 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.templateresolver;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.Serializable;
+
+import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateResolver;
+
+/**
+ * Stores shared state between {@link TemplateLoader} operations that are executed close to each other in the same
+ * thread. For example, a {@link TemplateLoader} that reads from a database might wants to store the database
+ * connection in it for reuse. The goal of sessions is mostly to increase performance. However, because a
+ * {@link DefaultTemplateResolver#getTemplate(String, java.util.Locale, Serializable)} call is executed inside a single
+ * session, sessions can be also be utilized to ensure that the template lookup (see {@link TemplateLookupStrategy})
+ * happens on a consistent view (a snapshot) of the backing storage, if the backing storage mechanism supports such
+ * thing.
+ * 
+ * <p>
+ * The {@link TemplateLoaderSession} implementation is (usually) specific to the {@link TemplateLoader}
+ * implementation. If your {@link TemplateLoader} implementation can't take advantage of sessions, you don't have to
+ * implement this interface, just return {@code null} for {@link TemplateLoader#createSession()}.
+ * 
+ * <p>
+ * {@link TemplateLoaderSession}-s should be lazy, that is, creating an instance should be very fast and should not
+ * cause I/O. Only when (and if ever) the shared resource stored in the session is needed for the first time should the
+ * shared resource be initialized.
+ *
+ * <p>
+ * {@link TemplateLoaderSession}-s need not be thread safe.
+ */
+public interface TemplateLoaderSession {
+
+    /**
+     * Closes this session, freeing any resources it holds. Further operations involving this session should fail, with
+     * the exception of {@link #close()} itself, which should be silently ignored.
+     * 
+     * <p>
+     * The {@link Reader} or {@link InputStream} contained in the {@link TemplateLoadingResult} must be closed before
+     * {@link #close()} is called on the session in which the {@link TemplateLoadingResult} was created. Except, if
+     * closing the {@link Reader} or {@link InputStream} has thrown an exception, the caller should just proceed with
+     * closing the session regardless. After {@link #close()} was called on the session, the methods of the
+     * {@link Reader} or {@link InputStream} is allowed to throw an exception, or behave in any other erratic way.
+     * (Because the caller of this interface is usually FreeMarker (the {@link DefaultTemplateResolver}), normally you don't have
+     * to deal with these rules, but it's useful to know the expectations if you implement
+     * {@link TemplateLoaderSession}.)
+     * 
+     * <p>The caller of {@link TemplateLoader#createSession()} has to guarantee that {@link #close()} will be called on
+     * the created session.
+     */
+    void close() throws IOException;
+    
+    /**
+     * Tells if this session is closed.
+     */
+    boolean isClosed();
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoadingResult.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoadingResult.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoadingResult.java
new file mode 100644
index 0000000..c685d93
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoadingResult.java
@@ -0,0 +1,208 @@
+/*
+ * 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.templateresolver;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.Serializable;
+import java.nio.charset.Charset;
+import java.util.Date;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.Configuration.ExtendableBuilder;
+import org.apache.freemarker.core.TemplateConfiguration;
+import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateResolver;
+import org.apache.freemarker.core.util._NullArgumentException;
+
+/**
+ * Return value of {@link TemplateLoader#load(String, TemplateLoadingSource, Serializable, TemplateLoaderSession)}
+ */
+public final class TemplateLoadingResult {
+    private final TemplateLoadingResultStatus status;
+    private final TemplateLoadingSource source;
+    private final Serializable version;
+    private final Reader reader;
+    private final InputStream inputStream;
+    private final TemplateConfiguration templateConfiguration; 
+
+    public static final TemplateLoadingResult NOT_FOUND = new TemplateLoadingResult(
+            TemplateLoadingResultStatus.NOT_FOUND);
+    public static final TemplateLoadingResult NOT_MODIFIED = new TemplateLoadingResult(
+            TemplateLoadingResultStatus.NOT_MODIFIED);
+
+    /**
+     * Creates an instance with status {@link TemplateLoadingResultStatus#OPENED}, for a storage mechanism that
+     * naturally returns the template content as sequence of {@code char}-s as opposed to a sequence of {@code byte}-s.
+     * This is the case for example when you store the template in a database in a varchar or CLOB. Do <em>not</em> use
+     * this constructor for stores that naturally return binary data instead (like files, class loader resources,
+     * BLOB-s, etc.), because using this constructor will disable FreeMarker's charset selection mechanism.
+     * 
+     * @param source
+     *            See {@link #getSource()}
+     * @param version
+     *            See {@link #getVersion()} for the meaning of this. Can be {@code null}, but use that only if the
+     *            backing storage mechanism doesn't know this information.
+     * @param reader
+     *            Gives the content of the template. It will be read in few thousand character chunks by FreeMarker, so
+     *            generally it need not be a {@link BufferedReader}.
+     * @param templateConfiguration
+     *            Usually {@code null}, as usually the backing storage mechanism doesn't store such information;
+     *            see {@link #getTemplateConfiguration()}.
+     */
+    public TemplateLoadingResult(TemplateLoadingSource source, Serializable version, Reader reader,
+            TemplateConfiguration templateConfiguration) {
+        _NullArgumentException.check("templateSource", source);
+        _NullArgumentException.check("reader", reader);
+        status = TemplateLoadingResultStatus.OPENED;
+        this.source = source;
+        this.version = version;
+        this.reader = reader;
+        inputStream = null;
+        this.templateConfiguration = templateConfiguration; 
+    }
+
+    /**
+     * Creates an instance with status {@link TemplateLoadingResultStatus#OPENED}, for a storage mechanism that
+     * naturally returns the template content as sequence of {@code byte}-s as opposed to a sequence of {@code char}-s.
+     * This is the case for example when you store the template in a file, classpath resource, or BLOB. Do <em>not</em>
+     * use this constructor for stores that naturally return text instead (like database varchar and CLOB columns).
+     * 
+     * @param source
+     *            See {@link #getSource()}
+     * @param version
+     *            See {@link #getVersion()} for the meaning of this. Can be {@code null}, but use that only if the
+     *            backing storage mechanism doesn't know this information.
+     * @param inputStream
+     *            Gives the content of the template. It will be read in few thousand byte chunks by FreeMarker, so
+     *            generally it need not be a {@link BufferedInputStream}.
+     * @param templateConfiguration
+     *            Usually {@code null}, as usually the backing storage mechanism doesn't store such information; see
+     *            {@link #getTemplateConfiguration()}. The most probable application is supplying the charset (encoding)
+     *            used by the {@link InputStream} (via
+     *            {@link ExtendableBuilder#setSourceEncoding(Charset)}), but only do that if the storage
+     *            mechanism really knows what the charset is.
+     */
+    public TemplateLoadingResult(TemplateLoadingSource source, Serializable version, InputStream inputStream,
+            TemplateConfiguration templateConfiguration) {
+        _NullArgumentException.check("templateSource", source);
+        _NullArgumentException.check("inputStream", inputStream);
+        status = TemplateLoadingResultStatus.OPENED;
+        this.source = source;
+        this.version = version;
+        reader = null;
+        this.inputStream = inputStream;
+        this.templateConfiguration = templateConfiguration; 
+    }
+
+    /**
+     * Used internally for creating the singleton instances which has a state where all other fields are {@code null}.
+     */
+    private TemplateLoadingResult(TemplateLoadingResultStatus status) {
+        this.status = status;
+        source = null;
+        version = null;
+        reader = null;
+        inputStream = null;
+        templateConfiguration = null;
+    }
+
+    /**
+     * Returns non-{@code null} exactly if {@link #getStatus()} is {@link TemplateLoadingResultStatus#OPENED} and the
+     * backing store mechanism returns content as {@code byte}-s, as opposed to as {@code chars}-s. See also
+     * {@link #TemplateLoadingResult(TemplateLoadingSource, Serializable, InputStream, TemplateConfiguration)}. It's the
+     * responsibility of the caller (which is {@link DefaultTemplateResolver} usually) to {@code close()} the {@link InputStream}.
+     * The return value is always the same instance, no mater when and how many times this method is called.
+     * 
+     * <p>
+     * The returned {@code InputStream} will be read in few kilobyte chunks by FreeMarker, so generally it need not
+     * be a {@link BufferedInputStream}. 
+     * 
+     * @return {@code null} or a {@code InputStream} to read the template content; see method description for more.
+     */
+    public InputStream getInputStream() {
+        return inputStream;
+    }
+
+    /**
+     * Tells what kind of result this is; see the documentation of {@link TemplateLoadingResultStatus}.
+     */
+    public TemplateLoadingResultStatus getStatus() {
+        return status;
+    }
+
+    /**
+     * Identifies the source on the level of the storage mechanism; stored in the cache together with the version
+     * ({@link #getVersion()}). When checking if a cache entry is up to date, the sources are compared, and only if they
+     * are equal are the versions compared. See more at {@link TemplateLoadingSource}.
+     */
+    public TemplateLoadingSource getSource() {
+        return source;
+    }
+
+    /**
+     * If the result status is {@link TemplateLoadingResultStatus#OPENED} and the backing storage stores such
+     * information, the version (usually the last modification time) of the loaded template, otherwise {@code null}. The
+     * version is some kind of value which changes when the template in the backing storage is updated. Usually, it's
+     * the last modification time (a {@link Date} or {@link Long}), though that can be problematic if the template can
+     * change twice within the granularity of the clock used by the storage. Thus some storages may use a revision
+     * number instead that's always increased when the template is updated, or the cryptographic hash of the template
+     * content as the version. Version objects are compared with each other with their {@link Object#equals(Object)}
+     * method, to see if a cache entry is outdated (though only when the source objects ({@link #getSource()}) are
+     * equal). Thus, the version object must have proper {@link Object#equals(Object)} and {@link Object#hashCode()}
+     * methods.
+     */
+    public Serializable getVersion() {
+        return version;
+    }
+
+    /**
+     * Returns non-{@code null} exactly if {@link #getStatus()} is {@link TemplateLoadingResultStatus#OPENED} and the
+     * backing store mechanism returns content as {@code char}-s, as opposed to as {@code byte}-s. See also
+     * {@link #TemplateLoadingResult(TemplateLoadingSource, Serializable, Reader, TemplateConfiguration)}. It's the
+     * responsibility of the caller (which is {@link DefaultTemplateResolver} usually) to {@code close()} the {@link Reader}. The
+     * return value is always the same instance, no mater when and how many times this method is called.
+     * 
+     * <p>
+     * The returned {@code Reader} will be read in few thousand character chunks by FreeMarker, so generally it need not
+     * be a {@link BufferedReader}. 
+     * 
+     * @return {@code null} or a {@code Reader} to read the template content; see method description for more.
+     */
+    public Reader getReader() {
+        return reader;
+    }
+
+    /**
+     * If {@link #getStatus()} is {@link TemplateLoadingResultStatus#OPENED}, and the template loader stores such
+     * information (which is rare) then it returns the {@link TemplateConfiguration} applicable to the template,
+     * otherwise it returns {@code null}. If {@link #getStatus()} is not {@link TemplateLoadingResultStatus#OPENED},
+     * then this should always return {@code null}. If there are {@link TemplateConfiguration}-s coming from other
+     * sources, such as from {@link Configuration#getTemplateConfigurations()}, this won't replace them, but will be
+     * merged with them, with properties coming from the returned {@link TemplateConfiguration} having the highest
+     * priority.
+     * 
+     * @return {@code null}, or a {@link TemplateConfiguration}.
+     */
+    public TemplateConfiguration getTemplateConfiguration() {
+        return templateConfiguration;
+    }
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoadingResultStatus.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoadingResultStatus.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoadingResultStatus.java
new file mode 100644
index 0000000..0ac8d00
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoadingResultStatus.java
@@ -0,0 +1,49 @@
+/*
+ * 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.templateresolver;
+
+import java.io.Serializable;
+
+/**
+ * Used for the value of {@link TemplateLoadingResult#getStatus()}.
+ */
+public enum TemplateLoadingResultStatus {
+
+    /**
+     * The template with the requested name doesn't exist (not to be confused with "wasn't accessible due to error"). If
+     * there was and error because of which we can't know for sure if the template is there or not (for example we
+     * couldn't access the backing store due to a network connection error or other unexpected I/O error or
+     * authorization problem), this value must not be used, instead an exception should be thrown by
+     * {@link TemplateLoader#load(String, TemplateLoadingSource, Serializable, TemplateLoaderSession)}.
+     */
+    NOT_FOUND,
+
+    /**
+     * If the template was found, but its source and version is the same as that which was provided to
+     * {@link TemplateLoader#load(String, TemplateLoadingSource, Serializable, TemplateLoaderSession)} (from a cache
+     * presumably), so its content wasn't opened for reading.
+     */
+    NOT_MODIFIED,
+
+    /**
+     * If the template was found and its content is ready for reading.
+     */
+    OPENED
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoadingSource.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoadingSource.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoadingSource.java
new file mode 100644
index 0000000..bfe47e4
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoadingSource.java
@@ -0,0 +1,69 @@
+/*
+ * 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.templateresolver;
+
+import java.io.Serializable;
+import java.util.HashMap;
+
+import org.apache.freemarker.core.templateresolver.impl.ByteArrayTemplateLoader;
+import org.apache.freemarker.core.templateresolver.impl.FileTemplateLoader;
+
+/**
+ * The point of {@link TemplateLoadingSource} is that with their {@link Object#equals(Object)} method we can tell if two
+ * cache entries were generated from the same physical resource or not. Comparing the template names isn't enough,
+ * because a {@link TemplateLoader} may uses some kind of fallback mechanism, such as delegating to other
+ * {@link TemplateLoader}-s until the template is found. Like if we have two {@link FileTemplateLoader}-s with different
+ * physical root directories, both can contain {@code "foo/bar.ftl"}, but obviously the two files aren't the same.
+ * 
+ * <p>
+ * When implementing this interface, check these:
+ * 
+ * <ul>
+ * <li>{@link Object#equals(Object)} must not be based on object identity, because two instances of
+ * {@link TemplateLoadingSource} that describe the same resource must be equivalent.
+ * 
+ * <li>Each {@link TemplateLoader} implementation should have its own {@link TemplateLoadingSource} implementation, so
+ * that {@link TemplateLoadingSource}-s coming from different {@link TemplateLoader} implementations can't be
+ * accidentally equal (according to {@link Object#equals(Object)}).
+ * 
+ * <li>{@link Object#equals(Object)} must still work properly if there are multiple instances of the same
+ * {@link TemplateLoader} implementation. Like if you have an implementation that loads from a database table, the
+ * {@link TemplateLoadingSource} should certain contain the JDBC connection string, the table name and the row ID, not
+ * just the row ID.
+ * 
+ * <li>Together with {@link Object#equals(Object)}, {@link Object#hashCode()} must be also overridden. The template
+ * source may be used as a {@link HashMap} key.
+ * 
+ * <li>Because {@link TemplateLoadingSource}-s are {@link Serializable}, they can't contain non-{@link Serializable}
+ * fields. Most notably, a reference to the creator {@link TemplateLoader}, so if it's an inner class of the
+ * {@link TemplateLoader}, it should be static.
+ * 
+ * <li>Consider that cache entries, in which the source is stored, may move between JVM-s (because of clustering with a
+ * distributed cache). Thus they should remain meaningful for the purpose of {@link Object#equals(Object)} even in
+ * another JVM.
+ * 
+ * <li>A {@link TemplateLoader} may chose not to support distributed caches, like {@link ByteArrayTemplateLoader}
+ * doesn't support that for example. In that case the serialization related points above can be ignored, but the
+ * {@link TemplateLoadingSource} implementation should define the {@code writeObject} method (a Java serialization
+ * feature) and throw an exception there to block serialization attempts.
+ * </ul>
+ */
+public interface TemplateLoadingSource extends Serializable {
+    // Empty
+}


[41/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForSequences.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForSequences.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForSequences.java
new file mode 100644
index 0000000..814b362
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForSequences.java
@@ -0,0 +1,871 @@
+/*
+ * 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.Serializable;
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+
+import org.apache.freemarker.core.arithmetic.ArithmeticEngine;
+import org.apache.freemarker.core.model.Constants;
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateMethodModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.impl.CollectionAndSequence;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.apache.freemarker.core.model.impl.TemplateModelListSequence;
+import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * A holder for builtins that operate exclusively on sequence or collection left-hand value.
+ */
+class BuiltInsForSequences {
+    
+    static class chunkBI extends BuiltInForSequence {
+
+        private class BIMethod implements TemplateMethodModelEx {
+            
+            private final TemplateSequenceModel tsm;
+
+            private BIMethod(TemplateSequenceModel tsm) {
+                this.tsm = tsm;
+            }
+
+            @Override
+            public Object exec(List args) throws TemplateModelException {
+                checkMethodArgCount(args, 1, 2);
+                int chunkSize = getNumberMethodArg(args, 0).intValue();
+                
+                return new ChunkedSequence(
+                        tsm,
+                        chunkSize,
+                        args.size() > 1 ? (TemplateModel) args.get(1) : null);
+            }
+        }
+
+        private static class ChunkedSequence implements TemplateSequenceModel {
+            
+            private final TemplateSequenceModel wrappedTsm;
+            
+            private final int chunkSize;
+            
+            private final TemplateModel fillerItem;
+            
+            private final int numberOfChunks;
+            
+            private ChunkedSequence(
+                    TemplateSequenceModel wrappedTsm, int chunkSize, TemplateModel fillerItem)
+                    throws TemplateModelException {
+                if (chunkSize < 1) {
+                    throw new _TemplateModelException("The 1st argument to ?', key, ' (...) must be at least 1.");
+                }
+                this.wrappedTsm = wrappedTsm;
+                this.chunkSize = chunkSize;
+                this.fillerItem = fillerItem;
+                numberOfChunks = (wrappedTsm.size() + chunkSize - 1) / chunkSize; 
+            }
+
+            @Override
+            public TemplateModel get(final int chunkIndex)
+                    throws TemplateModelException {
+                if (chunkIndex >= numberOfChunks) {
+                    return null;
+                }
+                
+                return new TemplateSequenceModel() {
+                    
+                    private final int baseIndex = chunkIndex * chunkSize;
+
+                    @Override
+                    public TemplateModel get(int relIndex)
+                            throws TemplateModelException {
+                        int absIndex = baseIndex + relIndex;
+                        if (absIndex < wrappedTsm.size()) {
+                            return wrappedTsm.get(absIndex);
+                        } else {
+                            return absIndex < numberOfChunks * chunkSize
+                                ? fillerItem
+                                : null;
+                        }
+                    }
+
+                    @Override
+                    public int size() throws TemplateModelException {
+                        return fillerItem != null || chunkIndex + 1 < numberOfChunks
+                                ? chunkSize
+                                : wrappedTsm.size() - baseIndex;
+                    }
+                    
+                };
+            }
+
+            @Override
+            public int size() throws TemplateModelException {
+                return numberOfChunks;
+            }
+            
+        }
+        
+        @Override
+        TemplateModel calculateResult(TemplateSequenceModel tsm) throws TemplateModelException {
+            return new BIMethod(tsm);
+        }
+        
+    }
+    
+    static class firstBI extends ASTExpBuiltIn {
+        
+        @Override
+        TemplateModel _eval(Environment env)
+                throws TemplateException {
+            TemplateModel model = target.eval(env);
+            // In 2.3.x only, we prefer TemplateSequenceModel for
+            // backward compatibility. In 2.4.x, we prefer TemplateCollectionModel. 
+            if (model instanceof TemplateSequenceModel) {
+                return calculateResultForSequence((TemplateSequenceModel) model);
+            } else if (model instanceof TemplateCollectionModel) {
+                return calculateResultForColletion((TemplateCollectionModel) model);
+            } else {
+                throw new NonSequenceOrCollectionException(target, model, env);
+            }
+        }        
+        
+        private TemplateModel calculateResultForSequence(TemplateSequenceModel seq)
+        throws TemplateModelException {
+            if (seq.size() == 0) {
+                return null;
+            }
+            return seq.get(0);
+        }
+        
+        private TemplateModel calculateResultForColletion(TemplateCollectionModel coll)
+        throws TemplateModelException {
+            TemplateModelIterator iter = coll.iterator();
+            if (!iter.hasNext()) {
+                return null;
+            }
+            return iter.next();
+        }
+        
+    }
+
+    static class joinBI extends ASTExpBuiltIn {
+        
+        private class BIMethodForCollection implements TemplateMethodModelEx {
+            
+            private final Environment env;
+            private final TemplateCollectionModel coll;
+
+            private BIMethodForCollection(Environment env, TemplateCollectionModel coll) {
+                this.env = env;
+                this.coll = coll;
+            }
+
+            @Override
+            public Object exec(List args)
+                    throws TemplateModelException {
+                checkMethodArgCount(args, 1, 3);
+                final String separator = getStringMethodArg(args, 0);
+                final String whenEmpty = getOptStringMethodArg(args, 1);
+                final String afterLast = getOptStringMethodArg(args, 2);
+                
+                StringBuilder sb = new StringBuilder();
+                
+                TemplateModelIterator it = coll.iterator();
+                
+                int idx = 0;
+                boolean hadItem = false;
+                while (it.hasNext()) {
+                    TemplateModel item = it.next();
+                    if (item != null) {
+                        if (hadItem) {
+                            sb.append(separator);
+                        } else {
+                            hadItem = true;
+                        }
+                        try {
+                            sb.append(_EvalUtil.coerceModelToStringOrUnsupportedMarkup(item, null, null, env));
+                        } catch (TemplateException e) {
+                            throw new _TemplateModelException(e,
+                                    "\"?", key, "\" failed at index ", Integer.valueOf(idx), " with this error:\n\n",
+                                    MessageUtil.EMBEDDED_MESSAGE_BEGIN,
+                                    new _DelayedGetMessageWithoutStackTop(e),
+                                    MessageUtil.EMBEDDED_MESSAGE_END);
+                        }
+                    }
+                    idx++;
+                }
+                if (hadItem) {
+                    if (afterLast != null) sb.append(afterLast);
+                } else {
+                    if (whenEmpty != null) sb.append(whenEmpty);
+                }
+                return new SimpleScalar(sb.toString());
+           }
+
+        }
+
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel model = target.eval(env);
+            if (model instanceof TemplateCollectionModel) {
+                if (model instanceof RightUnboundedRangeModel) {
+                    throw new _TemplateModelException(
+                            "The sequence to join was right-unbounded numerical range, thus it's infinitely long.");
+                }
+                return new BIMethodForCollection(env, (TemplateCollectionModel) model);
+            } else if (model instanceof TemplateSequenceModel) {
+                return new BIMethodForCollection(env, new CollectionAndSequence((TemplateSequenceModel) model));
+            } else {
+                throw new NonSequenceOrCollectionException(target, model, env);
+            }
+        }
+   
+    }
+
+    static class lastBI extends BuiltInForSequence {
+        @Override
+        TemplateModel calculateResult(TemplateSequenceModel tsm)
+        throws TemplateModelException {
+            if (tsm.size() == 0) {
+                return null;
+            }
+            return tsm.get(tsm.size() - 1);
+        }
+    }
+
+    static class reverseBI extends BuiltInForSequence {
+        private static class ReverseSequence implements TemplateSequenceModel {
+            private final TemplateSequenceModel seq;
+
+            ReverseSequence(TemplateSequenceModel seq) {
+                this.seq = seq;
+            }
+
+            @Override
+            public TemplateModel get(int index) throws TemplateModelException {
+                return seq.get(seq.size() - 1 - index);
+            }
+
+            @Override
+            public int size() throws TemplateModelException {
+                return seq.size();
+            }
+        }
+
+        @Override
+        TemplateModel calculateResult(TemplateSequenceModel tsm) {
+            if (tsm instanceof ReverseSequence) {
+                return ((ReverseSequence) tsm).seq;
+            } else {
+                return new ReverseSequence(tsm);
+            }
+        }
+    }
+
+    static class seq_containsBI extends ASTExpBuiltIn {
+        private class BIMethodForCollection implements TemplateMethodModelEx {
+            private TemplateCollectionModel m_coll;
+            private Environment m_env;
+
+            private BIMethodForCollection(TemplateCollectionModel coll, Environment env) {
+                m_coll = coll;
+                m_env = env;
+            }
+
+            @Override
+            public Object exec(List args)
+                    throws TemplateModelException {
+                checkMethodArgCount(args, 1);
+                TemplateModel arg = (TemplateModel) args.get(0);
+                TemplateModelIterator it = m_coll.iterator();
+                int idx = 0;
+                while (it.hasNext()) {
+                    if (modelsEqual(idx, it.next(), arg, m_env))
+                        return TemplateBooleanModel.TRUE;
+                    idx++;
+                }
+                return TemplateBooleanModel.FALSE;
+            }
+
+        }
+
+        private class BIMethodForSequence implements TemplateMethodModelEx {
+            private TemplateSequenceModel m_seq;
+            private Environment m_env;
+
+            private BIMethodForSequence(TemplateSequenceModel seq, Environment env) {
+                m_seq = seq;
+                m_env = env;
+            }
+
+            @Override
+            public Object exec(List args)
+                    throws TemplateModelException {
+                checkMethodArgCount(args, 1);
+                TemplateModel arg = (TemplateModel) args.get(0);
+                int size = m_seq.size();
+                for (int i = 0; i < size; i++) {
+                    if (modelsEqual(i, m_seq.get(i), arg, m_env))
+                        return TemplateBooleanModel.TRUE;
+                }
+                return TemplateBooleanModel.FALSE;
+            }
+
+        }
+    
+        @Override
+        TemplateModel _eval(Environment env)
+                throws TemplateException {
+            TemplateModel model = target.eval(env);
+            // In 2.3.x only, we prefer TemplateSequenceModel for
+            // backward compatibility. In 2.4.x, we prefer TemplateCollectionModel. 
+            if (model instanceof TemplateSequenceModel) {
+                return new BIMethodForSequence((TemplateSequenceModel) model, env);
+            } else if (model instanceof TemplateCollectionModel) {
+                return new BIMethodForCollection((TemplateCollectionModel) model, env);
+            } else {
+                throw new NonSequenceOrCollectionException(target, model, env);
+            }
+        }
+    
+    }
+    
+    static class seq_index_ofBI extends ASTExpBuiltIn {
+        
+        private class BIMethod implements TemplateMethodModelEx {
+            
+            protected final TemplateSequenceModel m_seq;
+            protected final TemplateCollectionModel m_col;
+            protected final Environment m_env;
+
+            private BIMethod(Environment env)
+                    throws TemplateException {
+                TemplateModel model = target.eval(env);
+                m_seq = model instanceof TemplateSequenceModel
+                        ? (TemplateSequenceModel) model
+                        : null;
+                // [FM3] Rework the below
+                // In 2.3.x only, we deny the possibility of collection
+                // access if there's sequence access. This is so to minimize
+                // the change of compatibility issues; without this, objects
+                // that implement both the sequence and collection interfaces
+                // would suddenly start using the collection interface, and if
+                // that's buggy that would surface now, breaking the application
+                // that despite its bugs has worked earlier.
+                m_col = m_seq == null && model instanceof TemplateCollectionModel
+                        ? (TemplateCollectionModel) model
+                        : null;
+                if (m_seq == null && m_col == null) {
+                    throw new NonSequenceOrCollectionException(target, model, env);
+                }
+                
+                m_env = env;
+            }
+
+            @Override
+            public final Object exec(List args)
+                    throws TemplateModelException {
+                int argCnt = args.size();
+                checkMethodArgCount(argCnt, 1, 2);
+                
+                TemplateModel target = (TemplateModel) args.get(0);
+                int foundAtIdx;
+                if (argCnt > 1) {
+                    int startIndex = getNumberMethodArg(args, 1).intValue();
+                    // In 2.3.x only, we prefer TemplateSequenceModel for
+                    // backward compatibility:
+                    foundAtIdx = m_seq != null
+                            ? findInSeq(target, startIndex)
+                            : findInCol(target, startIndex);
+                } else {
+                    // In 2.3.x only, we prefer TemplateSequenceModel for
+                    // backward compatibility:
+                    foundAtIdx = m_seq != null
+                            ? findInSeq(target)
+                            : findInCol(target);
+                }
+                return foundAtIdx == -1 ? Constants.MINUS_ONE : new SimpleNumber(foundAtIdx);
+            }
+            
+            int findInCol(TemplateModel target) throws TemplateModelException {
+                return findInCol(target, 0, Integer.MAX_VALUE);
+            }
+            
+            protected int findInCol(TemplateModel target, int startIndex)
+                    throws TemplateModelException {
+                if (m_dir == 1) {
+                    return findInCol(target, startIndex, Integer.MAX_VALUE);
+                } else {
+                    return findInCol(target, 0, startIndex);
+                }
+            }
+        
+            protected int findInCol(TemplateModel target,
+                    final int allowedRangeStart, final int allowedRangeEnd)
+                    throws TemplateModelException {
+                if (allowedRangeEnd < 0) return -1;
+                
+                TemplateModelIterator it = m_col.iterator();
+                
+                int foundAtIdx = -1;  // -1 is the return value for "not found"
+                int idx = 0; 
+                searchItem: while (it.hasNext()) {
+                    if (idx > allowedRangeEnd) break searchItem;
+                    
+                    TemplateModel current = it.next();
+                    if (idx >= allowedRangeStart) {
+                        if (modelsEqual(idx, current, target, m_env)) {
+                            foundAtIdx = idx;
+                            if (m_dir == 1) break searchItem; // "find first"
+                            // Otherwise it's "find last".
+                        }
+                    }
+                    idx++;
+                }
+                return foundAtIdx;
+            }
+
+            int findInSeq(TemplateModel target)
+            throws TemplateModelException {
+                final int seqSize = m_seq.size();
+                final int actualStartIndex;
+                
+                if (m_dir == 1) {
+                    actualStartIndex = 0;
+                } else {
+                    actualStartIndex = seqSize - 1;
+                }
+            
+                return findInSeq(target, actualStartIndex, seqSize); 
+            }
+
+            private int findInSeq(TemplateModel target, int startIndex)
+                    throws TemplateModelException {
+                int seqSize = m_seq.size();
+                
+                if (m_dir == 1) {
+                    if (startIndex >= seqSize) {
+                        return -1;
+                    }
+                    if (startIndex < 0) {
+                        startIndex = 0;
+                    }
+                } else {
+                    if (startIndex >= seqSize) {
+                        startIndex = seqSize - 1;
+                    }
+                    if (startIndex < 0) {
+                        return -1;
+                    }
+                }
+                
+                return findInSeq(target, startIndex, seqSize); 
+            }
+            
+            private int findInSeq(
+                    TemplateModel target, int scanStartIndex, int seqSize)
+                    throws TemplateModelException {
+                if (m_dir == 1) {
+                    for (int i = scanStartIndex; i < seqSize; i++) {
+                        if (modelsEqual(i, m_seq.get(i), target, m_env)) return i;
+                    }
+                } else {
+                    for (int i = scanStartIndex; i >= 0; i--) {
+                        if (modelsEqual(i, m_seq.get(i), target, m_env)) return i;
+                    }
+                }
+                return -1;
+            }
+            
+        }
+
+        private int m_dir;
+
+        seq_index_ofBI(int dir) {
+            m_dir = dir;
+        }
+
+        @Override
+        TemplateModel _eval(Environment env)
+                throws TemplateException {
+            return new BIMethod(env);
+        }
+    }
+
+    static class sort_byBI extends sortBI {
+        class BIMethod implements TemplateMethodModelEx {
+            TemplateSequenceModel seq;
+            
+            BIMethod(TemplateSequenceModel seq) {
+                this.seq = seq;
+            }
+            
+            @Override
+            public Object exec(List args)
+                    throws TemplateModelException {
+                // Should be:
+                // checkMethodArgCount(args, 1);
+                // But for BC:
+                if (args.size() < 1) throw MessageUtil.newArgCntError("?" + key, args.size(), 1);
+                
+                String[] subvars;
+                Object obj = args.get(0);
+                if (obj instanceof TemplateScalarModel) {
+                    subvars = new String[]{((TemplateScalarModel) obj).getAsString()};
+                } else if (obj instanceof TemplateSequenceModel) {
+                    TemplateSequenceModel seq = (TemplateSequenceModel) obj;
+                    int ln = seq.size();
+                    subvars = new String[ln];
+                    for (int i = 0; i < ln; i++) {
+                        Object item = seq.get(i);
+                        try {
+                            subvars[i] = ((TemplateScalarModel) item)
+                                    .getAsString();
+                        } catch (ClassCastException e) {
+                            if (!(item instanceof TemplateScalarModel)) {
+                                throw new _TemplateModelException(
+                                        "The argument to ?", key, "(key), when it's a sequence, must be a "
+                                        + "sequence of strings, but the item at index ", Integer.valueOf(i),
+                                        " is not a string.");
+                            }
+                        }
+                    }
+                } else {
+                    throw new _TemplateModelException(
+                            "The argument to ?", key, "(key) must be a string (the name of the subvariable), or a "
+                            + "sequence of strings (the \"path\" to the subvariable).");
+                }
+                return sort(seq, subvars); 
+            }
+        }
+        
+        @Override
+        TemplateModel calculateResult(TemplateSequenceModel seq) {
+            return new BIMethod(seq);
+        }
+    }
+
+    static class sortBI extends BuiltInForSequence {
+        
+        private static class BooleanKVPComparator implements Comparator, Serializable {
+
+            @Override
+            public int compare(Object arg0, Object arg1) {
+                // JDK 1.2 doesn't have Boolean.compareTo
+                boolean b0 = ((Boolean) ((KVP) arg0).key).booleanValue();
+                boolean b1 = ((Boolean) ((KVP) arg1).key).booleanValue();
+                if (b0) {
+                    return b1 ? 0 : 1;
+                } else {
+                    return b1 ? -1 : 0;
+                }
+            }
+        }
+        private static class DateKVPComparator implements Comparator, Serializable {
+
+            @Override
+            public int compare(Object arg0, Object arg1) {
+                return ((Date) ((KVP) arg0).key).compareTo(
+                        (Date) ((KVP) arg1).key);
+            }
+        }
+        /**
+         * Stores a key-value pair.
+         */
+        private static class KVP {
+            private Object key;
+
+            private Object value;
+            private KVP(Object key, Object value) {
+                this.key = key;
+                this.value = value;
+            }
+        }
+        private static class LexicalKVPComparator implements Comparator {
+            private Collator collator;
+
+            LexicalKVPComparator(Collator collator) {
+                this.collator = collator;
+            }
+
+            @Override
+            public int compare(Object arg0, Object arg1) {
+                return collator.compare(
+                        ((KVP) arg0).key, ((KVP) arg1).key);
+            }
+        }
+        private static class NumericalKVPComparator implements Comparator {
+            private ArithmeticEngine ae;
+
+            private NumericalKVPComparator(ArithmeticEngine ae) {
+                this.ae = ae;
+            }
+
+            @Override
+            public int compare(Object arg0, Object arg1) {
+                try {
+                    return ae.compareNumbers(
+                            (Number) ((KVP) arg0).key,
+                            (Number) ((KVP) arg1).key);
+                } catch (TemplateException e) {
+                    throw new ClassCastException(
+                        "Failed to compare numbers: " + e);
+                }
+            }
+        }
+        
+        static TemplateModelException newInconsistentSortKeyTypeException(
+                int keyNamesLn, String firstType, String firstTypePlural, int index, TemplateModel key) {
+            String valueInMsg;
+            String valuesInMsg;
+            if (keyNamesLn == 0) {
+                valueInMsg  = "value";
+                valuesInMsg  = "values";
+            } else {
+                valueInMsg  = "key value";
+                valuesInMsg  = "key values";
+            }
+            return new _TemplateModelException(
+                    startErrorMessage(keyNamesLn, index),
+                    "All ", valuesInMsg, " in the sequence must be ",
+                    firstTypePlural, ", because the first ", valueInMsg,
+                    " was that. However, the ", valueInMsg,
+                    " of the current item isn't a ", firstType, " but a ",
+                    new _DelayedFTLTypeDescription(key), ".");
+        }
+
+        /**
+         * Sorts a sequence for the <tt>sort</tt> and <tt>sort_by</tt>
+         * built-ins.
+         * 
+         * @param seq the sequence to sort.
+         * @param keyNames the name of the subvariable whose value is used for the
+         *     sorting. If the sorting is done by a sub-subvaruable, then this
+         *     will be of length 2, and so on. If the sorting is done by the
+         *     sequene items directly, then this argument has to be 0 length
+         *     array or <code>null</code>.
+         * @return a new sorted sequence, or the original sequence if the
+         *     sequence length was 0.
+         */
+        static TemplateSequenceModel sort(TemplateSequenceModel seq, String[] keyNames)
+                throws TemplateModelException {
+            int ln = seq.size();
+            if (ln == 0) return seq;
+            
+            ArrayList res = new ArrayList(ln);
+
+            int keyNamesLn = keyNames == null ? 0 : keyNames.length;
+
+            // Copy the Seq into a Java List[KVP] (also detects key type at the 1st item):
+            int keyType = KEY_TYPE_NOT_YET_DETECTED;
+            Comparator keyComparator = null;
+            for (int i = 0; i < ln; i++) {
+                final TemplateModel item = seq.get(i);
+                TemplateModel key = item;
+                for (int keyNameI = 0; keyNameI < keyNamesLn; keyNameI++) {
+                    try {
+                        key = ((TemplateHashModel) key).get(keyNames[keyNameI]);
+                    } catch (ClassCastException e) {
+                        if (!(key instanceof TemplateHashModel)) {
+                            throw new _TemplateModelException(
+                                    startErrorMessage(keyNamesLn, i),
+                                    (keyNameI == 0
+                                            ? "Sequence items must be hashes when using ?sort_by. "
+                                            : "The " + _StringUtil.jQuote(keyNames[keyNameI - 1])),
+                                    " subvariable is not a hash, so ?sort_by ",
+                                    "can't proceed with getting the ",
+                                    new _DelayedJQuote(keyNames[keyNameI]),
+                                    " subvariable.");
+                        } else {
+                            throw e;
+                        }
+                    }
+                    if (key == null) {
+                        throw new _TemplateModelException(
+                                startErrorMessage(keyNamesLn, i),
+                                "The " + _StringUtil.jQuote(keyNames[keyNameI]), " subvariable was null or missing.");
+                    }
+                } // for each key
+                
+                if (keyType == KEY_TYPE_NOT_YET_DETECTED) {
+                    if (key instanceof TemplateScalarModel) {
+                        keyType = KEY_TYPE_STRING;
+                        keyComparator = new LexicalKVPComparator(
+                                Environment.getCurrentEnvironment().getCollator());
+                    } else if (key instanceof TemplateNumberModel) {
+                        keyType = KEY_TYPE_NUMBER;
+                        keyComparator = new NumericalKVPComparator(
+                                Environment.getCurrentEnvironment()
+                                        .getArithmeticEngine());
+                    } else if (key instanceof TemplateDateModel) {
+                        keyType = KEY_TYPE_DATE;
+                        keyComparator = new DateKVPComparator();
+                    } else if (key instanceof TemplateBooleanModel) {
+                        keyType = KEY_TYPE_BOOLEAN;
+                        keyComparator = new BooleanKVPComparator();
+                    } else {
+                        throw new _TemplateModelException(
+                                startErrorMessage(keyNamesLn, i),
+                                "Values used for sorting must be numbers, strings, date/times or booleans.");
+                    }
+                }
+                switch(keyType) {
+                    case KEY_TYPE_STRING:
+                        try {
+                            res.add(new KVP(
+                                    ((TemplateScalarModel) key).getAsString(),
+                                    item));
+                        } catch (ClassCastException e) {
+                            if (!(key instanceof TemplateScalarModel)) {
+                                throw newInconsistentSortKeyTypeException(
+                                        keyNamesLn, "string", "strings", i, key);
+                            } else {
+                                throw e;
+                            }
+                        }
+                        break;
+                        
+                    case KEY_TYPE_NUMBER:
+                        try {
+                            res.add(new KVP(
+                                    ((TemplateNumberModel) key).getAsNumber(),
+                                    item));
+                        } catch (ClassCastException e) {
+                            if (!(key instanceof TemplateNumberModel)) {
+                                throw newInconsistentSortKeyTypeException(
+                                        keyNamesLn, "number", "numbers", i, key);
+                            }
+                        }
+                        break;
+                        
+                    case KEY_TYPE_DATE:
+                        try {
+                            res.add(new KVP(
+                                    ((TemplateDateModel) key).getAsDate(),
+                                    item));
+                        } catch (ClassCastException e) {
+                            if (!(key instanceof TemplateDateModel)) {
+                                throw newInconsistentSortKeyTypeException(
+                                        keyNamesLn, "date/time", "date/times", i, key);
+                            }
+                        }
+                        break;
+                        
+                    case KEY_TYPE_BOOLEAN:
+                        try {
+                            res.add(new KVP(
+                                    Boolean.valueOf(((TemplateBooleanModel) key).getAsBoolean()),
+                                    item));
+                        } catch (ClassCastException e) {
+                            if (!(key instanceof TemplateBooleanModel)) {
+                                throw newInconsistentSortKeyTypeException(
+                                        keyNamesLn, "boolean", "booleans", i, key);
+                            }
+                        }
+                        break;
+                        
+                    default:
+                        throw new BugException("Unexpected key type");
+                }
+            }
+
+            // Sort tje List[KVP]:
+            try {
+                Collections.sort(res, keyComparator);
+            } catch (Exception exc) {
+                throw new _TemplateModelException(exc,
+                        startErrorMessage(keyNamesLn), "Unexpected error while sorting:" + exc);
+            }
+
+            // Convert the List[KVP] to List[V]:
+            for (int i = 0; i < ln; i++) {
+                res.set(i, ((KVP) res.get(i)).value);
+            }
+
+            return new TemplateModelListSequence(res);
+        }
+
+        static Object[] startErrorMessage(int keyNamesLn) {
+            return new Object[] { (keyNamesLn == 0 ? "?sort" : "?sort_by(...)"), " failed: " };
+        }
+        
+        static Object[] startErrorMessage(int keyNamesLn, int index) {
+            return new Object[] {
+                    (keyNamesLn == 0 ? "?sort" : "?sort_by(...)"),
+                    " failed at sequence index ", Integer.valueOf(index),
+                    (index == 0 ? ": " : " (0-based): ") };
+        }
+        
+        static final int KEY_TYPE_NOT_YET_DETECTED = 0;
+
+        static final int KEY_TYPE_STRING = 1;
+
+        static final int KEY_TYPE_NUMBER = 2;
+
+        static final int KEY_TYPE_DATE = 3;
+        
+        static final int KEY_TYPE_BOOLEAN = 4;
+        
+        @Override
+        TemplateModel calculateResult(TemplateSequenceModel seq)
+                throws TemplateModelException {
+            return sort(seq, null);
+        }
+        
+    }
+
+    private static boolean modelsEqual(
+            int seqItemIndex, TemplateModel seqItem, TemplateModel searchedItem,
+            Environment env)
+            throws TemplateModelException {
+        try {
+            return _EvalUtil.compare(
+                    seqItem, null,
+                    _EvalUtil.CMP_OP_EQUALS, null,
+                    searchedItem, null,
+                    null, false,
+                    true, true, true, // The last one is true to emulate an old bug for BC 
+                    env);
+        } catch (TemplateException ex) {
+            throw new _TemplateModelException(ex,
+                    "This error has occurred when comparing sequence item at 0-based index ", Integer.valueOf(seqItemIndex),
+                    " to the searched item:\n", new _DelayedGetMessage(ex));
+        }
+    }
+ 
+    // Can't be instantiated
+    private BuiltInsForSequences() { }
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsBasic.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsBasic.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsBasic.java
new file mode 100644
index 0000000..bcf00c4
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsBasic.java
@@ -0,0 +1,697 @@
+/*
+ * 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.util.ArrayList;
+import java.util.List;
+import java.util.StringTokenizer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateMethodModel;
+import org.apache.freemarker.core.model.TemplateMethodModelEx;
+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.model.impl.SimpleNumber;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.apache.freemarker.core.util._StringUtil;
+
+class BuiltInsForStringsBasic {
+
+    static class cap_firstBI extends BuiltInForString {
+        @Override
+        TemplateModel calculateResult(String s, Environment env) {
+            int i = 0;
+            int ln = s.length();
+            while (i < ln  &&  Character.isWhitespace(s.charAt(i))) {
+                i++;
+            }
+            if (i < ln) {
+                StringBuilder b = new StringBuilder(s);
+                b.setCharAt(i, Character.toUpperCase(s.charAt(i)));
+                s = b.toString();
+            }
+            return new SimpleScalar(s);
+        }
+    }
+
+    static class capitalizeBI extends BuiltInForString {
+        @Override
+        TemplateModel calculateResult(String s, Environment env) {
+            return new SimpleScalar(_StringUtil.capitalize(s));
+        }
+    }
+
+    static class chop_linebreakBI extends BuiltInForString {
+        @Override
+        TemplateModel calculateResult(String s, Environment env) {
+            return new SimpleScalar(_StringUtil.chomp(s));
+        }
+    }
+
+    static class containsBI extends ASTExpBuiltIn {
+        
+        private class BIMethod implements TemplateMethodModelEx {
+            
+            private final String s;
+    
+            private BIMethod(String s) {
+                this.s = s;
+            }
+    
+            @Override
+            public Object exec(List args) throws TemplateModelException {
+                checkMethodArgCount(args, 1);
+                return s.indexOf(getStringMethodArg(args, 0)) != -1
+                        ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+            }
+        }
+    
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            return new BIMethod(target.evalAndCoerceToStringOrUnsupportedMarkup(env,
+                    "For sequences/collections (lists and such) use \"?seq_contains\" instead."));
+        }
+    }
+
+    static class ends_withBI extends BuiltInForString {
+    
+        private class BIMethod implements TemplateMethodModelEx {
+            private String s;
+    
+            private BIMethod(String s) {
+                this.s = s;
+            }
+    
+            @Override
+            public Object exec(List args) throws TemplateModelException {
+                checkMethodArgCount(args, 1);
+                return s.endsWith(getStringMethodArg(args, 0)) ?
+                        TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+            }
+        }
+    
+        @Override
+        TemplateModel calculateResult(String s, Environment env) throws TemplateException {
+            return new BIMethod(s);
+        }
+    }
+
+    static class ensure_ends_withBI extends BuiltInForString {
+        
+        private class BIMethod implements TemplateMethodModelEx {
+            private String s;
+    
+            private BIMethod(String s) {
+                this.s = s;
+            }
+    
+            @Override
+            public Object exec(List args) throws TemplateModelException {
+                checkMethodArgCount(args, 1);
+                String suffix = getStringMethodArg(args, 0);
+                return new SimpleScalar(s.endsWith(suffix) ? s : s + suffix);
+            }
+        }
+    
+        @Override
+        TemplateModel calculateResult(String s, Environment env) throws TemplateException {
+            return new BIMethod(s);
+        }
+    }
+
+    static class ensure_starts_withBI extends BuiltInForString {
+        
+        private class BIMethod implements TemplateMethodModelEx {
+            private String s;
+    
+            private BIMethod(String s) {
+                this.s = s;
+            }
+    
+            @Override
+            public Object exec(List args) throws TemplateModelException {
+                checkMethodArgCount(args, 1, 3);
+                
+                final String checkedPrefix = getStringMethodArg(args, 0);
+                
+                final boolean startsWithPrefix;
+                final String addedPrefix; 
+                if (args.size() > 1) {
+                    addedPrefix = getStringMethodArg(args, 1);
+                    long flags = args.size() > 2
+                            ? RegexpHelper.parseFlagString(getStringMethodArg(args, 2))
+                            : RegexpHelper.RE_FLAG_REGEXP;
+                  
+                    if ((flags & RegexpHelper.RE_FLAG_REGEXP) == 0) {
+                        RegexpHelper.checkOnlyHasNonRegexpFlags(key, flags, true);
+                        if ((flags & RegexpHelper.RE_FLAG_CASE_INSENSITIVE) == 0) {
+                            startsWithPrefix = s.startsWith(checkedPrefix);
+                        } else {
+                            startsWithPrefix = s.toLowerCase().startsWith(checkedPrefix.toLowerCase());
+                        }
+                    } else {
+                        Pattern pattern = RegexpHelper.getPattern(checkedPrefix, (int) flags);
+                        final Matcher matcher = pattern.matcher(s);
+                        startsWithPrefix = matcher.lookingAt();
+                    } 
+                } else {
+                    startsWithPrefix = s.startsWith(checkedPrefix);
+                    addedPrefix = checkedPrefix;
+                }
+                return new SimpleScalar(startsWithPrefix ? s : addedPrefix + s);
+            }
+        }
+    
+        @Override
+        TemplateModel calculateResult(String s, Environment env) throws TemplateException {
+            return new BIMethod(s);
+        }
+    }
+
+    static class index_ofBI extends ASTExpBuiltIn {
+        
+        private class BIMethod implements TemplateMethodModelEx {
+            
+            private final String s;
+            
+            private BIMethod(String s) {
+                this.s = s;
+            }
+            
+            @Override
+            public Object exec(List args) throws TemplateModelException {
+                int argCnt = args.size();
+                checkMethodArgCount(argCnt, 1, 2);
+                String subStr = getStringMethodArg(args, 0);
+                if (argCnt > 1) {
+                    int startIdx = getNumberMethodArg(args, 1).intValue();
+                    return new SimpleNumber(findLast ? s.lastIndexOf(subStr, startIdx) : s.indexOf(subStr, startIdx));
+                } else {
+                    return new SimpleNumber(findLast ? s.lastIndexOf(subStr) : s.indexOf(subStr));
+                }
+            }
+        }
+        
+        private final boolean findLast;
+    
+        index_ofBI(boolean findLast) {
+            this.findLast = findLast;
+        }
+        
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            return new BIMethod(target.evalAndCoerceToStringOrUnsupportedMarkup(env,
+                    "For sequences/collections (lists and such) use \"?seq_index_of\" instead."));
+        }
+    }
+    
+    static class keep_afterBI extends BuiltInForString {
+        class KeepAfterMethod implements TemplateMethodModelEx {
+            private String s;
+
+            KeepAfterMethod(String s) {
+                this.s = s;
+            }
+
+            @Override
+            public Object exec(List args) throws TemplateModelException {
+                int argCnt = args.size();
+                checkMethodArgCount(argCnt, 1, 2);
+                String separatorString = getStringMethodArg(args, 0);
+                long flags = argCnt > 1 ? RegexpHelper.parseFlagString(getStringMethodArg(args, 1)) : 0;
+                
+                int startIndex;
+                if ((flags & RegexpHelper.RE_FLAG_REGEXP) == 0) {
+                    RegexpHelper.checkOnlyHasNonRegexpFlags(key, flags, true);
+                    if ((flags & RegexpHelper.RE_FLAG_CASE_INSENSITIVE) == 0) {
+                        startIndex = s.indexOf(separatorString);
+                    } else {
+                        startIndex = s.toLowerCase().indexOf(separatorString.toLowerCase());
+                    }
+                    if (startIndex >= 0) {
+                        startIndex += separatorString.length();
+                    }
+                } else {
+                    Pattern pattern = RegexpHelper.getPattern(separatorString, (int) flags);
+                    final Matcher matcher = pattern.matcher(s);
+                    if (matcher.find()) {
+                        startIndex = matcher.end();
+                    } else {
+                        startIndex = -1;
+                    }
+                } 
+                return startIndex == -1 ? TemplateScalarModel.EMPTY_STRING : new SimpleScalar(s.substring(startIndex));
+            }
+        }
+        
+        @Override
+        TemplateModel calculateResult(String s, Environment env) throws TemplateModelException {
+            return new KeepAfterMethod(s);
+        }
+        
+    }
+    
+    static class keep_after_lastBI extends BuiltInForString {
+        class KeepAfterMethod implements TemplateMethodModelEx {
+            private String s;
+
+            KeepAfterMethod(String s) {
+                this.s = s;
+            }
+
+            @Override
+            public Object exec(List args) throws TemplateModelException {
+                int argCnt = args.size();
+                checkMethodArgCount(argCnt, 1, 2);
+                String separatorString = getStringMethodArg(args, 0);
+                long flags = argCnt > 1 ? RegexpHelper.parseFlagString(getStringMethodArg(args, 1)) : 0;
+                
+                int startIndex;
+                if ((flags & RegexpHelper.RE_FLAG_REGEXP) == 0) {
+                    RegexpHelper.checkOnlyHasNonRegexpFlags(key, flags, true);
+                    if ((flags & RegexpHelper.RE_FLAG_CASE_INSENSITIVE) == 0) {
+                        startIndex = s.lastIndexOf(separatorString);
+                    } else {
+                        startIndex = s.toLowerCase().lastIndexOf(separatorString.toLowerCase());
+                    }
+                    if (startIndex >= 0) {
+                        startIndex += separatorString.length();
+                    }
+                } else {
+                    if (separatorString.length() == 0) {
+                        startIndex = s.length();
+                    } else {
+                        Pattern pattern = RegexpHelper.getPattern(separatorString, (int) flags);
+                        final Matcher matcher = pattern.matcher(s);
+                        if (matcher.find()) {
+                            startIndex = matcher.end();
+                            while (matcher.find(matcher.start() + 1)) {
+                                startIndex = matcher.end();
+                            }
+                        } else {
+                            startIndex = -1;
+                        }
+                    }
+                } 
+                return startIndex == -1 ? TemplateScalarModel.EMPTY_STRING : new SimpleScalar(s.substring(startIndex));
+            }
+        }
+        
+        @Override
+        TemplateModel calculateResult(String s, Environment env) throws TemplateModelException {
+            return new KeepAfterMethod(s);
+        }
+        
+    }
+    
+    static class keep_beforeBI extends BuiltInForString {
+        class KeepUntilMethod implements TemplateMethodModelEx {
+            private String s;
+
+            KeepUntilMethod(String s) {
+                this.s = s;
+            }
+
+            @Override
+            public Object exec(List args) throws TemplateModelException {
+                int argCnt = args.size();
+                checkMethodArgCount(argCnt, 1, 2);
+                String separatorString = getStringMethodArg(args, 0);
+                long flags = argCnt > 1 ? RegexpHelper.parseFlagString(getStringMethodArg(args, 1)) : 0;
+                
+                int stopIndex;
+                if ((flags & RegexpHelper.RE_FLAG_REGEXP) == 0) {
+                    RegexpHelper.checkOnlyHasNonRegexpFlags(key, flags, true);
+                    if ((flags & RegexpHelper.RE_FLAG_CASE_INSENSITIVE) == 0) {
+                        stopIndex = s.indexOf(separatorString);
+                    } else {
+                        stopIndex = s.toLowerCase().indexOf(separatorString.toLowerCase());
+                    }
+                } else {
+                    Pattern pattern = RegexpHelper.getPattern(separatorString, (int) flags);
+                    final Matcher matcher = pattern.matcher(s);
+                    if (matcher.find()) {
+                        stopIndex = matcher.start();
+                    } else {
+                        stopIndex = -1;
+                    }
+                } 
+                return stopIndex == -1 ? new SimpleScalar(s) : new SimpleScalar(s.substring(0, stopIndex));
+            }
+        }
+        
+        @Override
+        TemplateModel calculateResult(String s, Environment env) throws TemplateModelException {
+            return new KeepUntilMethod(s);
+        }
+        
+    }
+    
+    // TODO
+    static class keep_before_lastBI extends BuiltInForString {
+        class KeepUntilMethod implements TemplateMethodModelEx {
+            private String s;
+
+            KeepUntilMethod(String s) {
+                this.s = s;
+            }
+
+            @Override
+            public Object exec(List args) throws TemplateModelException {
+                int argCnt = args.size();
+                checkMethodArgCount(argCnt, 1, 2);
+                String separatorString = getStringMethodArg(args, 0);
+                long flags = argCnt > 1 ? RegexpHelper.parseFlagString(getStringMethodArg(args, 1)) : 0;
+                
+                int stopIndex;
+                if ((flags & RegexpHelper.RE_FLAG_REGEXP) == 0) {
+                    RegexpHelper.checkOnlyHasNonRegexpFlags(key, flags, true);
+                    if ((flags & RegexpHelper.RE_FLAG_CASE_INSENSITIVE) == 0) {
+                        stopIndex = s.lastIndexOf(separatorString);
+                    } else {
+                        stopIndex = s.toLowerCase().lastIndexOf(separatorString.toLowerCase());
+                    }
+                } else {
+                    if (separatorString.length() == 0) {
+                        stopIndex = s.length();
+                    } else {
+                        Pattern pattern = RegexpHelper.getPattern(separatorString, (int) flags);
+                        final Matcher matcher = pattern.matcher(s);
+                        if (matcher.find()) {
+                            stopIndex = matcher.start();
+                            while (matcher.find(stopIndex + 1)) {
+                                stopIndex = matcher.start();
+                            }
+                        } else {
+                            stopIndex = -1;
+                        }
+                    }
+                } 
+                return stopIndex == -1 ? new SimpleScalar(s) : new SimpleScalar(s.substring(0, stopIndex));
+            }
+        }
+        
+        @Override
+        TemplateModel calculateResult(String s, Environment env) throws TemplateModelException {
+            return new KeepUntilMethod(s);
+        }
+        
+    }
+    
+    static class lengthBI extends BuiltInForString {
+    
+        @Override
+        TemplateModel calculateResult(String s, Environment env) throws TemplateException {
+            return new SimpleNumber(s.length());
+        }
+        
+    }    
+
+    static class lower_caseBI extends BuiltInForString {
+        @Override
+        TemplateModel calculateResult(String s, Environment env) {
+            return new SimpleScalar(s.toLowerCase(env.getLocale()));
+        }
+    }    
+
+    static class padBI extends BuiltInForString {
+        
+        private class BIMethod implements TemplateMethodModelEx {
+            
+            private final String s;
+    
+            private BIMethod(String s) {
+                this.s = s;
+            }
+    
+            @Override
+            public Object exec(List args) throws TemplateModelException {
+                int argCnt  = args.size();
+                checkMethodArgCount(argCnt, 1, 2);
+    
+                int width = getNumberMethodArg(args, 0).intValue();
+    
+                if (argCnt > 1) {
+                    String filling = getStringMethodArg(args, 1);
+                    try {
+                        return new SimpleScalar(
+                                leftPadder
+                                        ? _StringUtil.leftPad(s, width, filling)
+                                        : _StringUtil.rightPad(s, width, filling));
+                    } catch (IllegalArgumentException e) {
+                        if (filling.length() == 0) {
+                            throw new _TemplateModelException(
+                                    "?", key, "(...) argument #2 can't be a 0-length string.");
+                        } else {
+                            throw new _TemplateModelException(e,
+                                    "?", key, "(...) failed: ", e);
+                        }
+                    }
+                } else {
+                    return new SimpleScalar(leftPadder ? _StringUtil.leftPad(s, width) : _StringUtil.rightPad(s, width));
+                }
+            }
+        }
+    
+        private final boolean leftPadder;
+    
+        padBI(boolean leftPadder) {
+            this.leftPadder = leftPadder;
+        }
+    
+        @Override
+        TemplateModel calculateResult(String s, Environment env) throws TemplateException {
+            return new BIMethod(s);
+        }
+    }
+    
+    static class remove_beginningBI extends BuiltInForString {
+        
+        private class BIMethod implements TemplateMethodModelEx {
+            private String s;
+    
+            private BIMethod(String s) {
+                this.s = s;
+            }
+    
+            @Override
+            public Object exec(List args) throws TemplateModelException {
+                checkMethodArgCount(args, 1);
+                String prefix = getStringMethodArg(args, 0);
+                return new SimpleScalar(s.startsWith(prefix) ? s.substring(prefix.length()) : s);
+            }
+        }
+    
+        @Override
+        TemplateModel calculateResult(String s, Environment env) throws TemplateException {
+            return new BIMethod(s);
+        }
+    }
+
+    static class remove_endingBI extends BuiltInForString {
+    
+        private class BIMethod implements TemplateMethodModelEx {
+            private String s;
+    
+            private BIMethod(String s) {
+                this.s = s;
+            }
+    
+            @Override
+            public Object exec(List args) throws TemplateModelException {
+                checkMethodArgCount(args, 1);
+                String suffix = getStringMethodArg(args, 0);
+                return new SimpleScalar(s.endsWith(suffix) ? s.substring(0, s.length() - suffix.length()) : s);
+            }
+        }
+    
+        @Override
+        TemplateModel calculateResult(String s, Environment env) throws TemplateException {
+            return new BIMethod(s);
+        }
+    }
+    
+    static class split_BI extends BuiltInForString {
+        class SplitMethod implements TemplateMethodModel {
+            private String s;
+
+            SplitMethod(String s) {
+                this.s = s;
+            }
+
+            @Override
+            public Object exec(List args) throws TemplateModelException {
+                int argCnt = args.size();
+                checkMethodArgCount(argCnt, 1, 2);
+                String splitString = (String) args.get(0);
+                long flags = argCnt > 1 ? RegexpHelper.parseFlagString((String) args.get(1)) : 0;
+                String[] result = null;
+                if ((flags & RegexpHelper.RE_FLAG_REGEXP) == 0) {
+                    RegexpHelper.checkNonRegexpFlags("split", flags);
+                    result = _StringUtil.split(s, splitString,
+                            (flags & RegexpHelper.RE_FLAG_CASE_INSENSITIVE) != 0);
+                } else {
+                    Pattern pattern = RegexpHelper.getPattern(splitString, (int) flags);
+                    result = pattern.split(s);
+                } 
+                return new NativeStringArraySequence(result);
+            }
+        }
+        
+        @Override
+        TemplateModel calculateResult(String s, Environment env) throws TemplateModelException {
+            return new SplitMethod(s);
+        }
+        
+    }
+    
+    static class starts_withBI extends BuiltInForString {
+    
+        private class BIMethod implements TemplateMethodModelEx {
+            private String s;
+    
+            private BIMethod(String s) {
+                this.s = s;
+            }
+    
+            @Override
+            public Object exec(List args) throws TemplateModelException {
+                checkMethodArgCount(args, 1);
+                return s.startsWith(getStringMethodArg(args, 0)) ?
+                        TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+            }
+        }
+    
+        @Override
+        TemplateModel calculateResult(String s, Environment env) throws TemplateException {
+            return new BIMethod(s);
+        }
+    }
+
+    static class substringBI extends BuiltInForString {
+        
+        @Override
+        TemplateModel calculateResult(final String s, final Environment env) throws TemplateException {
+            return new TemplateMethodModelEx() {
+                
+                @Override
+                public Object exec(java.util.List args) throws TemplateModelException {
+                    int argCount = args.size();
+                    checkMethodArgCount(argCount, 1, 2);
+    
+                    int beginIdx = getNumberMethodArg(args, 0).intValue();
+    
+                    final int len = s.length();
+    
+                    if (beginIdx < 0) {
+                        throw newIndexLessThan0Exception(0, beginIdx);
+                    } else if (beginIdx > len) {
+                        throw newIndexGreaterThanLengthException(0, beginIdx, len);
+                    }
+    
+                    if (argCount > 1) {
+                        int endIdx = getNumberMethodArg(args, 1).intValue();
+                        if (endIdx < 0) {
+                            throw newIndexLessThan0Exception(1, endIdx);
+                        } else if (endIdx > len) {
+                            throw newIndexGreaterThanLengthException(1, endIdx, len);
+                        }
+                        if (beginIdx > endIdx) {
+                            throw MessageUtil.newMethodArgsInvalidValueException("?" + key,
+                                    "The begin index argument, ", Integer.valueOf(beginIdx),
+                                    ", shouldn't be greater than the end index argument, ",
+                                    Integer.valueOf(endIdx), ".");
+                        }
+                        return new SimpleScalar(s.substring(beginIdx, endIdx));
+                    } else {
+                        return new SimpleScalar(s.substring(beginIdx));
+                    }
+                }
+    
+                private TemplateModelException newIndexGreaterThanLengthException(
+                        int argIdx, int idx, final int len) throws TemplateModelException {
+                    return MessageUtil.newMethodArgInvalidValueException(
+                            "?" + key, argIdx,
+                            "The index mustn't be greater than the length of the string, ",
+                            Integer.valueOf(len),
+                            ", but it was ", Integer.valueOf(idx), ".");
+                }
+    
+                private TemplateModelException newIndexLessThan0Exception(
+                        int argIdx, int idx) throws TemplateModelException {
+                    return MessageUtil.newMethodArgInvalidValueException(
+                            "?" + key, argIdx,
+                            "The index must be at least 0, but was ", Integer.valueOf(idx), ".");
+                }
+                
+            };
+        }
+    }
+
+    static class trimBI extends BuiltInForString {
+        @Override
+        TemplateModel calculateResult(String s, Environment env) {
+            return new SimpleScalar(s.trim());
+        }
+    }
+
+    static class uncap_firstBI extends BuiltInForString {
+        @Override
+        TemplateModel calculateResult(String s, Environment env) {
+            int i = 0;
+            int ln = s.length();
+            while (i < ln  &&  Character.isWhitespace(s.charAt(i))) {
+                i++;
+            }
+            if (i < ln) {
+                StringBuilder b = new StringBuilder(s);
+                b.setCharAt(i, Character.toLowerCase(s.charAt(i)));
+                s = b.toString();
+            }
+            return new SimpleScalar(s);
+        }
+    }
+
+    static class upper_caseBI extends BuiltInForString {
+        @Override
+        TemplateModel calculateResult(String s, Environment env) {
+            return new SimpleScalar(s.toUpperCase(env.getLocale()));
+        }
+    }
+
+    static class word_listBI extends BuiltInForString {
+        @Override
+        TemplateModel calculateResult(String s, Environment env) {
+            ArrayList<String> result = new ArrayList<>();
+            StringTokenizer st = new StringTokenizer(s);
+            while (st.hasMoreTokens()) {
+               result.add(st.nextToken());
+            }
+            return new  NativeStringListSequence(result);
+        }
+    }
+
+    // Can't be instantiated
+    private BuiltInsForStringsBasic() { }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsEncoding.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsEncoding.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsEncoding.java
new file mode 100644
index 0000000..80eb9d3
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsEncoding.java
@@ -0,0 +1,195 @@
+/*
+ * 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.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.UnsupportedCharsetException;
+import java.util.List;
+
+import org.apache.freemarker.core.model.TemplateMethodModel;
+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.model.impl.SimpleScalar;
+import org.apache.freemarker.core.util._StringUtil;
+
+class BuiltInsForStringsEncoding {
+
+    static class htmlBI extends BuiltInForLegacyEscaping {
+        
+        @Override
+        TemplateModel calculateResult(String s, Environment env) {
+            return new SimpleScalar(_StringUtil.XHTMLEnc(s));
+        }
+        
+    }
+
+    static class j_stringBI extends BuiltInForString {
+        @Override
+        TemplateModel calculateResult(String s, Environment env) {
+            return new SimpleScalar(_StringUtil.javaStringEnc(s));
+        }
+    }
+
+    static class js_stringBI extends BuiltInForString {
+        @Override
+        TemplateModel calculateResult(String s, Environment env) {
+            return new SimpleScalar(_StringUtil.javaScriptStringEnc(s));
+        }
+    }
+
+    static class json_stringBI extends BuiltInForString {
+        @Override
+        TemplateModel calculateResult(String s, Environment env) {
+            return new SimpleScalar(_StringUtil.jsonStringEnc(s));
+        }
+    }
+
+    static class rtfBI extends BuiltInForLegacyEscaping {
+        @Override
+        TemplateModel calculateResult(String s, Environment env) {
+            return new SimpleScalar(_StringUtil.RTFEnc(s));
+        }
+    }
+
+    static class urlBI extends BuiltInForString {
+        
+        static class UrlBIResult extends BuiltInsForStringsEncoding.AbstractUrlBIResult {
+    
+            protected UrlBIResult(ASTExpBuiltIn parent, String target, Environment env) {
+                super(parent, target, env);
+            }
+    
+            @Override
+            protected String encodeWithCharset(Charset charset) throws UnsupportedEncodingException {
+                return _StringUtil.URLEnc(targetAsString, charset);
+            }
+            
+        }
+        
+        @Override
+        TemplateModel calculateResult(String s, Environment env) {
+            return new UrlBIResult(this, s, env);
+        }
+        
+    }
+
+    static class urlPathBI extends BuiltInForString {
+    
+        static class UrlPathBIResult extends BuiltInsForStringsEncoding.AbstractUrlBIResult {
+    
+            protected UrlPathBIResult(ASTExpBuiltIn parent, String target, Environment env) {
+                super(parent, target, env);
+            }
+    
+            @Override
+            protected String encodeWithCharset(Charset charset) throws UnsupportedEncodingException {
+                return _StringUtil.URLPathEnc(targetAsString, charset);
+            }
+            
+        }
+        
+        @Override
+        TemplateModel calculateResult(String s, Environment env) {
+            return new UrlPathBIResult(this, s, env);
+        }
+        
+    }
+
+    static class xhtmlBI extends BuiltInForLegacyEscaping {
+        @Override
+        TemplateModel calculateResult(String s, Environment env) {
+            return new SimpleScalar(_StringUtil.XHTMLEnc(s));
+        }
+    }
+
+    static class xmlBI extends BuiltInForLegacyEscaping {
+        @Override
+        TemplateModel calculateResult(String s, Environment env) {
+            return new SimpleScalar(_StringUtil.XMLEnc(s));
+        }
+    }
+
+    // Can't be instantiated
+    private BuiltInsForStringsEncoding() { }
+
+    static abstract class AbstractUrlBIResult implements
+    TemplateScalarModel, TemplateMethodModel {
+        
+        protected final ASTExpBuiltIn parent;
+        protected final String targetAsString;
+        private final Environment env;
+        private String cachedResult;
+        
+        protected AbstractUrlBIResult(ASTExpBuiltIn parent, String targetAsString, Environment env) {
+            this.parent = parent;
+            this.targetAsString = targetAsString;
+            this.env = env;
+        }
+        
+        protected abstract String encodeWithCharset(Charset charset) throws UnsupportedEncodingException;
+    
+        @Override
+        public Object exec(List args) throws TemplateModelException {
+            parent.checkMethodArgCount(args.size(), 1);
+            try {
+                String charsetName = (String) args.get(0);
+                Charset charset = null;
+                try {
+                    charset = Charset.forName(charsetName);
+                } catch (UnsupportedCharsetException e) {
+                    throw new _TemplateModelException(e, "Wrong charset name, or charset is unsupported by the runtime "
+                            + "environment: " + _StringUtil.jQuote(charsetName));
+                }
+                return new SimpleScalar(encodeWithCharset(charset));
+            } catch (Exception e) {
+                throw new _TemplateModelException(e, "Failed to execute URL encoding.");
+            }
+        }
+        
+        @Override
+        public String getAsString() throws TemplateModelException {
+            if (cachedResult == null) {
+                Charset charset = env.getEffectiveURLEscapingCharset();
+                if (charset == null) {
+                    throw new _TemplateModelException(
+                            "To do URL encoding, the framework that encloses "
+                            + "FreeMarker must specify the output encoding "
+                            + "or the URL encoding charset, so ask the "
+                            + "programmers to fix it. Or, as a last chance, "
+                            + "you can set the url_encoding_charset setting in "
+                            + "the template, e.g. "
+                            + "<#setting url_escaping_charset='ISO-8859-1'>, or "
+                            + "give the charset explicitly to the buit-in, e.g. "
+                            + "foo?url('ISO-8859-1').");
+                }
+                try {
+                    cachedResult = encodeWithCharset(charset);
+                } catch (UnsupportedEncodingException e) {
+                    throw new _TemplateModelException(e, "Failed to execute URL encoding.");
+                }
+            }
+            return cachedResult;
+        }
+        
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsMisc.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsMisc.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsMisc.java
new file mode 100644
index 0000000..21c2a9d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsMisc.java
@@ -0,0 +1,305 @@
+/*
+ * 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.Writer;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateMethodModelEx;
+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.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.TemplateTransformModel;
+import org.apache.freemarker.core.model.impl.BeanModel;
+import org.apache.freemarker.core.model.impl.DefaultObjectWrapper;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+
+class BuiltInsForStringsMisc {
+
+    // Can't be instantiated
+    private BuiltInsForStringsMisc() { }
+    
+    static class booleanBI extends BuiltInForString {
+        @Override
+        TemplateModel calculateResult(String s, Environment env)  throws TemplateException {
+            final boolean b;
+            if (s.equals(MiscUtil.C_TRUE)) {
+                b = true;
+            } else if (s.equals(MiscUtil.C_FALSE)) {
+                b = false;
+            } else if (s.equals(env.getTemplateBooleanFormat().getTrueStringValue())) {
+                b = true;
+            } else if (s.equals(env.getTemplateBooleanFormat().getFalseStringValue())) {
+                b = false;
+            } else {
+                throw new _MiscTemplateException(this, env,
+                        "Can't convert this string to boolean: ", new _DelayedJQuote(s));
+            }
+            return b ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class evalBI extends OutputFormatBoundBuiltIn {
+        
+        @Override
+        protected TemplateModel calculateResult(Environment env) throws TemplateException {
+            return calculateResult(BuiltInForString.getTargetString(target, env), env);
+        }
+        
+        TemplateModel calculateResult(String s, Environment env) throws TemplateException {
+            Template parentTemplate = getTemplate();
+            
+            ASTExpression exp = null;
+            try {
+                try {
+                    ParsingConfiguration pCfg = parentTemplate.getParsingConfiguration();
+                    
+                    SimpleCharStream simpleCharStream = new SimpleCharStream(
+                            new StringReader("(" + s + ")"),
+                            RUNTIME_EVAL_LINE_DISPLACEMENT, 1,
+                            s.length() + 2);
+                    simpleCharStream.setTabSize(pCfg.getTabSize());
+                    FMParserTokenManager tkMan = new FMParserTokenManager(
+                            simpleCharStream);
+                    tkMan.SwitchTo(FMParserConstants.FM_EXPRESSION);
+
+                    // pCfg.outputFormat+autoEscapingPolicy is exceptional: it's inherited from the lexical context
+                    FMParser parser = new FMParser(
+                            parentTemplate, false, tkMan,
+                            pCfg, outputFormat, autoEscapingPolicy,
+                            null);
+                    
+                    exp = parser.ASTExpression();
+                } catch (TokenMgrError e) {
+                    throw e.toParseException(parentTemplate);
+                }
+            } catch (ParseException e) {
+                throw new _MiscTemplateException(this, env,
+                        "Failed to \"?", key, "\" string with this error:\n\n",
+                        MessageUtil.EMBEDDED_MESSAGE_BEGIN,
+                        new _DelayedGetMessage(e),
+                        MessageUtil.EMBEDDED_MESSAGE_END,
+                        "\n\nThe failing expression:");
+            }
+            try {
+                return exp.eval(env);
+            } catch (TemplateException e) {
+                throw new _MiscTemplateException(this, env,
+                        "Failed to \"?", key, "\" string with this error:\n\n",
+                        MessageUtil.EMBEDDED_MESSAGE_BEGIN,
+                        new _DelayedGetMessageWithoutStackTop(e),
+                        MessageUtil.EMBEDDED_MESSAGE_END,
+                        "\n\nThe failing expression:");
+            }
+        }
+        
+    }
+    
+    /**
+     * A method that takes a parameter and evaluates it as a scalar,
+     * then treats that scalar as template source code and returns a
+     * transform model that evaluates the template in place.
+     * The template inherits the configuration and environment of the executing
+     * template. By default, its name will be equal to 
+     * <tt>executingTemplate.getLookupName() + "$anonymous_interpreted"</tt>. You can
+     * specify another parameter to the method call in which case the
+     * template name suffix is the specified id instead of "anonymous_interpreted".
+     */
+    static class interpretBI extends OutputFormatBoundBuiltIn {
+        
+        /**
+         * Constructs a template on-the-fly and returns it embedded in a
+         * {@link TemplateTransformModel}.
+         * 
+         * <p>The built-in has two arguments:
+         * the arguments passed to the method. It can receive at
+         * least one and at most two arguments, both must evaluate to a scalar. 
+         * The first scalar is interpreted as a template source code and a template
+         * is built from it. The second (optional) is used to give the generated
+         * template a name.
+         * 
+         * @return a {@link TemplateTransformModel} that when executed inside
+         * a <tt>&lt;transform></tt> block will process the generated template
+         * just as if it had been <tt>&lt;transform></tt>-ed at that point.
+         */
+        @Override
+        protected TemplateModel calculateResult(Environment env) throws TemplateException {
+            TemplateModel model = target.eval(env);
+            ASTExpression sourceExpr = null;
+            String id = "anonymous_interpreted";
+            if (model instanceof TemplateSequenceModel) {
+                sourceExpr = ((ASTExpression) new ASTExpDynamicKeyName(target, new ASTExpNumberLiteral(Integer.valueOf(0))).copyLocationFrom(target));
+                if (((TemplateSequenceModel) model).size() > 1) {
+                    id = ((ASTExpression) new ASTExpDynamicKeyName(target, new ASTExpNumberLiteral(Integer.valueOf(1))).copyLocationFrom(target)).evalAndCoerceToPlainText(env);
+                }
+            } else if (model instanceof TemplateScalarModel) {
+                sourceExpr = target;
+            } else {
+                throw new UnexpectedTypeException(
+                        target, model,
+                        "sequence or string", new Class[] { TemplateSequenceModel.class, TemplateScalarModel.class },
+                        env);
+            }
+            String templateSource = sourceExpr.evalAndCoerceToPlainText(env);
+            Template parentTemplate = env.getCurrentTemplate();
+            
+            final Template interpretedTemplate;
+            try {
+                ParsingConfiguration pCfg = parentTemplate.getParsingConfiguration();
+                // pCfg.outputFormat+autoEscapingPolicy is exceptional: it's inherited from the lexical context
+                interpretedTemplate = new Template(
+                        (parentTemplate.getLookupName() != null ? parentTemplate.getLookupName() : "nameless_template") + "->" + id,
+                        null,
+                        new StringReader(templateSource),
+                        parentTemplate.getConfiguration(), parentTemplate.getTemplateConfiguration(),
+                        outputFormat, autoEscapingPolicy,
+                        null, null);
+            } catch (IOException e) {
+                throw new _MiscTemplateException(this, e, env,
+                        "Template parsing with \"?", key, "\" has failed with this error:\n\n",
+                        MessageUtil.EMBEDDED_MESSAGE_BEGIN,
+                        new _DelayedGetMessage(e),
+                        MessageUtil.EMBEDDED_MESSAGE_END,
+                        "\n\nThe failed expression:");
+            }
+            
+            return new TemplateProcessorModel(interpretedTemplate);
+        }
+
+        private class TemplateProcessorModel
+        implements
+            TemplateTransformModel {
+            private final Template template;
+            
+            TemplateProcessorModel(Template template) {
+                this.template = template;
+            }
+            
+            @Override
+            public Writer getWriter(final Writer out, Map args) throws TemplateModelException, IOException {
+                try {
+                    Environment env = Environment.getCurrentEnvironment();
+                    boolean lastFIRE = env.setFastInvalidReferenceExceptions(false);
+                    try {
+                        env.include(template);
+                    } finally {
+                        env.setFastInvalidReferenceExceptions(lastFIRE);
+                    }
+                } catch (Exception e) {
+                    throw new _TemplateModelException(e,
+                            "Template created with \"?", key, "\" has stopped with this error:\n\n",
+                            MessageUtil.EMBEDDED_MESSAGE_BEGIN,
+                            new _DelayedGetMessage(e),
+                            MessageUtil.EMBEDDED_MESSAGE_END);
+                }
+        
+                return new Writer(out)
+                {
+                    @Override
+                    public void close() {
+                    }
+                    
+                    @Override
+                    public void flush() throws IOException {
+                        out.flush();
+                    }
+                    
+                    @Override
+                    public void write(char[] cbuf, int off, int len) throws IOException {
+                        out.write(cbuf, off, len);
+                    }
+                };
+            }
+        }
+
+    }
+
+    static class numberBI extends BuiltInForString {
+        @Override
+        TemplateModel calculateResult(String s, Environment env)  throws TemplateException {
+            try {
+                return new SimpleNumber(env.getArithmeticEngine().toNumber(s));
+            } catch (NumberFormatException nfe) {
+                throw NonNumericalException.newMalformedNumberException(this, s, env);
+            }
+        }
+    }
+
+    /**
+     * A built-in that allows us to instantiate an instance of a java class.
+     * Usage is something like: <tt>&lt;#assign foobar = "foo.bar.MyClass"?new()></tt>;
+     */
+    static class newBI extends ASTExpBuiltIn {
+        
+        @Override
+        TemplateModel _eval(Environment env)
+                throws TemplateException {
+            return new ConstructorFunction(target.evalAndCoerceToPlainText(env), env, target.getTemplate());
+        }
+
+        class ConstructorFunction implements TemplateMethodModelEx {
+
+            private final Class<?> cl;
+            private final Environment env;
+            
+            public ConstructorFunction(String classname, Environment env, Template template) throws TemplateException {
+                this.env = env;
+                cl = env.getNewBuiltinClassResolver().resolve(classname, env, template);
+                if (!TemplateModel.class.isAssignableFrom(cl)) {
+                    throw new _MiscTemplateException(newBI.this, env,
+                            "Class ", cl.getName(), " does not implement org.apache.freemarker.core.TemplateModel");
+                }
+                if (BeanModel.class.isAssignableFrom(cl)) {
+                    throw new _MiscTemplateException(newBI.this, env,
+                            "Bean Models cannot be instantiated using the ?", key, " built-in");
+                }
+            }
+
+            @Override
+            public Object exec(List arguments) throws TemplateModelException {
+                ObjectWrapper ow = env.getObjectWrapper();
+                if (ow instanceof DefaultObjectWrapper) {
+                    return ((DefaultObjectWrapper) ow).newInstance(cl, arguments);
+                }
+
+                if (!arguments.isEmpty()) {
+                    throw new TemplateModelException(
+                            "className?new(args) only supports 0 arguments in the current configuration, because "
+                            + " the objectWrapper setting value is not a "
+                            + DefaultObjectWrapper.class.getName() +
+                            " (or its subclass).");
+                }
+                try {
+                    return cl.newInstance();
+                } catch (Exception e) {
+                    throw new TemplateModelException("Failed to instantiate "
+                            + cl.getName() + " with its parameterless constructor; see cause exception", e);
+                }
+            }
+        }
+    }
+    
+}


[43/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTStaticText.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTStaticText.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTStaticText.java
new file mode 100644
index 0000000..7766012
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTStaticText.java
@@ -0,0 +1,408 @@
+/*
+ * 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.core.util._CollectionUtil;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * AST node representing static text.
+ */
+final class ASTStaticText extends ASTElement {
+    
+    // We're using char[] instead of String for storing the text block because
+    // Writer.write(String) involves copying the String contents to a char[] 
+    // using String.getChars(), and then calling Writer.write(char[]). By
+    // using Writer.write(char[]) directly, we avoid array copying on each 
+    // write. 
+    private char[] text;
+    private final boolean unparsed;
+
+    public ASTStaticText(String text) {
+        this(text, false);
+    }
+
+    public ASTStaticText(String text, boolean unparsed) {
+        this(text.toCharArray(), unparsed);
+    }
+
+    ASTStaticText(char[] text, boolean unparsed) {
+        this.text = text;
+        this.unparsed = unparsed;
+    }
+    
+    void replaceText(String text) {
+        this.text = text.toCharArray();
+    }
+
+    /**
+     * Simply outputs the text.
+     * 
+     * @deprecated This is an internal API; don't call or override it.
+     */
+    @Deprecated
+    @Override
+    public ASTElement[] accept(Environment env)
+    throws IOException {
+        env.getOut().write(text);
+        return null;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        if (canonical) {
+            String text = new String(this.text);
+            if (unparsed) {
+                return "<#noparse>" + text + "</#noparse>";
+            }
+            return text;
+        } else {
+            return "text " + _StringUtil.jQuote(new String(text));
+        }
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "#text";
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 1;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return new String(text);
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return ParameterRole.CONTENT;
+    }
+
+    @Override
+    ASTElement postParseCleanup(boolean stripWhitespace) {
+        if (text.length == 0) return this;
+        int openingCharsToStrip = 0, trailingCharsToStrip = 0;
+        boolean deliberateLeftTrim = deliberateLeftTrim();
+        boolean deliberateRightTrim = deliberateRightTrim();
+        if (!stripWhitespace || text.length == 0 ) {
+            return this;
+        }
+        ASTElement parentElement = getParent();
+        if (isTopLevelTextIfParentIs(parentElement) && previousSibling() == null) {
+            return this;
+        }
+        if (!deliberateLeftTrim) {
+            trailingCharsToStrip = trailingCharsToStrip();
+        }
+        if (!deliberateRightTrim) {
+            openingCharsToStrip = openingCharsToStrip();
+        }
+        if (openingCharsToStrip == 0 && trailingCharsToStrip == 0) {
+            return this;
+        }
+        text = substring(text, openingCharsToStrip, text.length - trailingCharsToStrip);
+        if (openingCharsToStrip > 0) {
+            beginLine++;
+            beginColumn = 1;
+        }
+        if (trailingCharsToStrip > 0) {
+            endColumn = 0;
+        }
+        return this;
+    }
+    
+    /**
+     * Scans forward the nodes on the same line to see whether there is a 
+     * deliberate left trim in effect. Returns true if the left trim was present.
+     */
+    private boolean deliberateLeftTrim() {
+        boolean result = false;
+        for (ASTElement elem = nextTerminalNode();
+             elem != null && elem.beginLine == endLine;
+             elem = elem.nextTerminalNode()) {
+            if (elem instanceof ASTDirTOrTrOrTl) {
+                ASTDirTOrTrOrTl ti = (ASTDirTOrTrOrTl) elem;
+                if (!ti.left && !ti.right) {
+                    result = true;
+                }
+                if (ti.left) {
+                    result = true;
+                    int lastNewLineIndex = lastNewLineIndex();
+                    if (lastNewLineIndex >= 0  || beginColumn == 1) {
+                        char[] firstPart = substring(text, 0, lastNewLineIndex + 1);
+                        char[] lastLine = substring(text, 1 + lastNewLineIndex); 
+                        if (_StringUtil.isTrimmableToEmpty(lastLine)) {
+                            text = firstPart;
+                            endColumn = 0;
+                        } else {
+                            int i = 0;
+                            while (Character.isWhitespace(lastLine[i])) {
+                                i++;
+                            }
+                            char[] printablePart = substring(lastLine, i);
+                            text = concat(firstPart, printablePart);
+                        }
+                    }
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Checks for the presence of a t or rt directive on the 
+     * same line. Returns true if the right trim directive was present.
+     */
+    private boolean deliberateRightTrim() {
+        boolean result = false;
+        for (ASTElement elem = prevTerminalNode();
+             elem != null && elem.endLine == beginLine;
+             elem = elem.prevTerminalNode()) {
+            if (elem instanceof ASTDirTOrTrOrTl) {
+                ASTDirTOrTrOrTl ti = (ASTDirTOrTrOrTl) elem;
+                if (!ti.left && !ti.right) {
+                    result = true;
+                }
+                if (ti.right) {
+                    result = true;
+                    int firstLineIndex = firstNewLineIndex() + 1;
+                    if (firstLineIndex == 0) {
+                        return false;
+                    }
+                    if (text.length > firstLineIndex 
+                        && text[firstLineIndex - 1] == '\r' 
+                        && text[firstLineIndex] == '\n') {
+                        firstLineIndex++;
+                    }
+                    char[] trailingPart = substring(text, firstLineIndex);
+                    char[] openingPart = substring(text, 0, firstLineIndex);
+                    if (_StringUtil.isTrimmableToEmpty(openingPart)) {
+                        text = trailingPart;
+                        beginLine++;
+                        beginColumn = 1;
+                    } else {
+                        int lastNonWS = openingPart.length - 1;
+                        while (Character.isWhitespace(text[lastNonWS])) {
+                            lastNonWS--;
+                        }
+                        char[] printablePart = substring(text, 0, lastNonWS + 1);
+                        if (_StringUtil.isTrimmableToEmpty(trailingPart)) {
+                        // THIS BLOCK IS HEINOUS! THERE MUST BE A BETTER WAY! REVISIT (JR)
+                            boolean trimTrailingPart = true;
+                            for (ASTElement te = nextTerminalNode();
+                                 te != null && te.beginLine == endLine;
+                                 te = te.nextTerminalNode()) {
+                                if (te.heedsOpeningWhitespace()) {
+                                    trimTrailingPart = false;
+                                }
+                                if (te instanceof ASTDirTOrTrOrTl && ((ASTDirTOrTrOrTl) te).left) {
+                                    trimTrailingPart = true;
+                                    break;
+                                }
+                            }
+                            if (trimTrailingPart) trailingPart = _CollectionUtil.EMPTY_CHAR_ARRAY;
+                        }
+                        text = concat(printablePart, trailingPart);
+                    }
+                }
+            }
+        }
+        return result;
+    }
+    
+    private int firstNewLineIndex() {
+        char[] text = this.text;
+        for (int i = 0; i < text.length; i++) {
+            char c = text[i];
+            if (c == '\r' || c == '\n' ) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    private int lastNewLineIndex() {
+        char[] text = this.text;
+        for (int i = text.length - 1; i >= 0; i--) {
+            char c = text[i];
+            if (c == '\r' || c == '\n' ) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * figures out how many opening whitespace characters to strip
+     * in the post-parse cleanup phase.
+     */
+    private int openingCharsToStrip() {
+        int newlineIndex = firstNewLineIndex();
+        if (newlineIndex == -1 && beginColumn != 1) {
+            return 0;
+        }
+        ++newlineIndex;
+        if (text.length > newlineIndex) {
+            if (newlineIndex > 0 && text[newlineIndex - 1] == '\r' && text[newlineIndex] == '\n') {
+                ++newlineIndex;
+            }
+        }
+        if (!_StringUtil.isTrimmableToEmpty(text, 0, newlineIndex)) {
+            return 0;
+        }
+        // We look at the preceding elements on the line to see if we should
+        // strip the opening newline and any whitespace preceding it.
+        for (ASTElement elem = prevTerminalNode();
+             elem != null && elem.endLine == beginLine;
+             elem = elem.prevTerminalNode()) {
+            if (elem.heedsOpeningWhitespace()) {
+                return 0;
+            }
+        }
+        return newlineIndex;
+    }
+
+    /**
+     * figures out how many trailing whitespace characters to strip
+     * in the post-parse cleanup phase.
+     */
+    private int trailingCharsToStrip() {
+        int lastNewlineIndex = lastNewLineIndex();
+        if (lastNewlineIndex == -1 && beginColumn != 1) {
+            return 0;
+        }
+        if (!_StringUtil.isTrimmableToEmpty(text, lastNewlineIndex + 1)) {
+            return 0;
+        }
+        // We look at the elements afterward on the same line to see if we should
+        // strip any whitespace after the last newline
+        for (ASTElement elem = nextTerminalNode();
+             elem != null && elem.beginLine == endLine;
+             elem = elem.nextTerminalNode()) {
+            if (elem.heedsTrailingWhitespace()) {
+                return 0;
+            }
+        }
+        return text.length - (lastNewlineIndex + 1);
+    }
+
+    @Override
+    boolean heedsTrailingWhitespace() {
+        if (isIgnorable(true)) {
+            return false;
+        }
+        for (char c : text) {
+            if (c == '\n' || c == '\r') {
+                return false;
+            }
+            if (!Character.isWhitespace(c)) {
+                return true;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    boolean heedsOpeningWhitespace() {
+        if (isIgnorable(true)) {
+            return false;
+        }
+        for (int i = text.length - 1; i >= 0; i--) {
+            char c = text[i];
+            if (c == '\n' || c == '\r') {
+                return false;
+            }
+            if (!Character.isWhitespace(c)) {
+                return true;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    boolean isIgnorable(boolean stripWhitespace) {
+        if (text == null || text.length == 0) {
+            return true;
+        }
+        if (stripWhitespace) {
+            if (!_StringUtil.isTrimmableToEmpty(text)) {
+                return false;
+            }
+            ASTElement parentElement = getParent();
+            boolean atTopLevel = isTopLevelTextIfParentIs(parentElement);
+            ASTElement prevSibling = previousSibling();
+            ASTElement nextSibling = nextSibling();
+            return ((prevSibling == null && atTopLevel) || nonOutputtingType(prevSibling))
+                    && ((nextSibling == null && atTopLevel) || nonOutputtingType(nextSibling));
+        } else {
+            return false;
+        }
+    }
+
+    private boolean isTopLevelTextIfParentIs(ASTElement parentElement) {
+        return parentElement == null
+                || parentElement.getParent() == null && parentElement instanceof ASTImplicitParent;
+    }
+    
+
+    private boolean nonOutputtingType(ASTElement element) {
+        return (element instanceof ASTDirMacro ||
+                element instanceof ASTDirAssignment || 
+                element instanceof ASTDirAssignmentsContainer ||
+                element instanceof ASTDirSetting ||
+                element instanceof ASTDirImport ||
+                element instanceof ASTComment);
+    }
+
+    private static char[] substring(char[] c, int from, int to) {
+        char[] c2 = new char[to - from];
+        System.arraycopy(c, from, c2, 0, c2.length);
+        return c2;
+    }
+    
+    private static char[] substring(char[] c, int from) {
+        return substring(c, from, c.length);
+    }
+    
+    private static char[] concat(char[] c1, char[] c2) {
+        char[] c = new char[c1.length + c2.length];
+        System.arraycopy(c1, 0, c, 0, c1.length);
+        System.arraycopy(c2, 0, c, c1.length, c2.length);
+        return c;
+    }
+    
+    @Override
+    boolean isOutputCacheable() {
+        return true;
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ArithmeticExpression.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ArithmeticExpression.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ArithmeticExpression.java
new file mode 100644
index 0000000..764ec8a
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ArithmeticExpression.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.arithmetic.ArithmeticEngine;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+
+/**
+ * An operator for arithmetic operations. Note that the + operator is in {@link ASTExpAddOrConcat}, because its
+ * overloaded (does string concatenation and more).
+ */
+final class ArithmeticExpression extends ASTExpression {
+
+    static final int TYPE_SUBSTRACTION = 0;
+    static final int TYPE_MULTIPLICATION = 1;
+    static final int TYPE_DIVISION = 2;
+    static final int TYPE_MODULO = 3;
+
+    private static final char[] OPERATOR_IMAGES = new char[] { '-', '*', '/', '%' };
+
+    private final ASTExpression lho;
+    private final ASTExpression rho;
+    private final int operator;
+
+    ArithmeticExpression(ASTExpression lho, ASTExpression rho, int operator) {
+        this.lho = lho;
+        this.rho = rho;
+        this.operator = operator;
+    }
+
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        return _eval(env, this, lho.evalToNumber(env), operator, rho.evalToNumber(env));
+    }
+
+    static TemplateModel _eval(Environment env, ASTNode parent, Number lhoNumber, int operator, Number rhoNumber)
+            throws TemplateException {
+        ArithmeticEngine ae = _EvalUtil.getArithmeticEngine(env, parent);
+        switch (operator) {
+            case TYPE_SUBSTRACTION : 
+                return new SimpleNumber(ae.subtract(lhoNumber, rhoNumber));
+            case TYPE_MULTIPLICATION :
+                return new SimpleNumber(ae.multiply(lhoNumber, rhoNumber));
+            case TYPE_DIVISION :
+                return new SimpleNumber(ae.divide(lhoNumber, rhoNumber));
+            case TYPE_MODULO :
+                return new SimpleNumber(ae.modulus(lhoNumber, rhoNumber));
+            default:
+                if (parent instanceof ASTExpression) {
+                    throw new _MiscTemplateException((ASTExpression) parent,
+                            "Unknown operation: ", Integer.valueOf(operator));
+                } else {
+                    throw new _MiscTemplateException("Unknown operation: ", Integer.valueOf(operator));
+                }
+        }
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return lho.getCanonicalForm() + ' ' + getOperatorSymbol(operator) + ' ' + rho.getCanonicalForm();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return String.valueOf(getOperatorSymbol(operator));
+    }
+
+    static char getOperatorSymbol(int operator) {
+        return OPERATOR_IMAGES[operator];
+    }
+    
+    @Override
+    boolean isLiteral() {
+        return constantValue != null || (lho.isLiteral() && rho.isLiteral());
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+    	return new ArithmeticExpression(
+    	        lho.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
+    	        rho.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
+    	        operator);
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 3;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return lho;
+        case 1: return rho;
+        case 2: return Integer.valueOf(operator);
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        switch (idx) {
+        case 0: return ParameterRole.LEFT_HAND_OPERAND;
+        case 1: return ParameterRole.RIGHT_HAND_OPERAND;
+        case 2: return ParameterRole.AST_NODE_SUBTYPE;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BoundedRangeModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BoundedRangeModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BoundedRangeModel.java
new file mode 100644
index 0000000..05efa98
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BoundedRangeModel.java
@@ -0,0 +1,70 @@
+/*
+ * 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;
+
+
+/**
+ * A range between two integers (maybe 0 long).
+ */
+final class BoundedRangeModel extends RangeModel {
+
+    private final int step, size;
+    private final boolean rightAdaptive;
+    private final boolean affectedByStringSlicingBug;
+    
+    /**
+     * @param inclusiveEnd Tells if the {@code end} index is part of the range. 
+     * @param rightAdaptive Tells if the right end of the range adapts to the size of the sliced value, if otherwise
+     *     it would be bigger than that. 
+     */
+    BoundedRangeModel(int begin, int end, boolean inclusiveEnd, boolean rightAdaptive) {
+        super(begin);
+        step = begin <= end ? 1 : -1;
+        size = Math.abs(end - begin) + (inclusiveEnd ? 1 : 0);
+        this.rightAdaptive = rightAdaptive;
+        affectedByStringSlicingBug = inclusiveEnd;
+    }
+
+    @Override
+    public int size() {
+        return size;
+    }
+    
+    @Override
+    int getStep() {
+        return step;
+    }
+
+    @Override
+    boolean isRightUnbounded() {
+        return false;
+    }
+
+    @Override
+    boolean isRightAdaptive() {
+        return rightAdaptive;
+    }
+
+    @Override
+    boolean isAffactedByStringSlicingBug() {
+        return affectedByStringSlicingBug;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInBannedWhenAutoEscaping.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInBannedWhenAutoEscaping.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInBannedWhenAutoEscaping.java
new file mode 100644
index 0000000..642b939
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInBannedWhenAutoEscaping.java
@@ -0,0 +1,27 @@
+/*
+ * 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;
+
+/**
+ * A string built-in whose usage is banned when auto-escaping with a markup-output format is active.
+ * This is just a marker; the actual checking is in {@code FTL.jj}.
+ */
+abstract class BuiltInBannedWhenAutoEscaping extends SpecialBuiltIn {
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForDate.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForDate.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForDate.java
new file mode 100644
index 0000000..33971f5
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForDate.java
@@ -0,0 +1,56 @@
+/*
+ * 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.util.Date;
+
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateModel;
+
+abstract class BuiltInForDate extends ASTExpBuiltIn {
+    @Override
+    TemplateModel _eval(Environment env)
+            throws TemplateException {
+        TemplateModel model = target.eval(env);
+        if (model instanceof TemplateDateModel) {
+            TemplateDateModel tdm = (TemplateDateModel) model;
+            return calculateResult(_EvalUtil.modelToDate(tdm, target), tdm.getDateType(), env);
+        } else {
+            throw newNonDateException(env, model, target);
+        }
+    }
+
+    /** Override this to implement the built-in. */
+    protected abstract TemplateModel calculateResult(
+            Date date, int dateType, Environment env)
+    throws TemplateException;
+    
+    static TemplateException newNonDateException(Environment env, TemplateModel model, ASTExpression target)
+            throws InvalidReferenceException {
+        TemplateException e;
+        if (model == null) {
+            e = InvalidReferenceException.getInstance(target, env);
+        } else {
+            e = new NonDateException(target, model, "date", env);
+        }
+        return e;
+    }
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForHashEx.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForHashEx.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForHashEx.java
new file mode 100644
index 0000000..ec21061
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForHashEx.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+
+abstract class BuiltInForHashEx extends ASTExpBuiltIn {
+
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        TemplateModel model = target.eval(env);
+        if (model instanceof TemplateHashModelEx) {
+            return calculateResult((TemplateHashModelEx) model, env);
+        }
+        throw new NonExtendedHashException(target, model, env);
+    }
+    
+    abstract TemplateModel calculateResult(TemplateHashModelEx hashExModel, Environment env)
+            throws TemplateModelException, InvalidReferenceException;
+    
+    protected InvalidReferenceException newNullPropertyException(
+            String propertyName, TemplateModel tm, Environment env) {
+        if (env.getFastInvalidReferenceExceptions()) {
+            return InvalidReferenceException.FAST_INSTANCE;
+        } else {
+            return new InvalidReferenceException(
+                    new _ErrorDescriptionBuilder(
+                        "The exteneded hash (of class ", tm.getClass().getName(), ") has returned null for its \"",
+                        propertyName,
+                        "\" property. This is maybe a bug. The extended hash was returned by this expression:")
+                    .blame(target),
+                    env, this);
+        }
+    }
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForLegacyEscaping.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForLegacyEscaping.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForLegacyEscaping.java
new file mode 100644
index 0000000..8cdcbf6
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForLegacyEscaping.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.TemplateModel;
+
+/**
+ * A string built-in whose usage is banned when auto-escaping with a markup-output format is active.
+ * This is just a marker; the actual checking is in {@code FTL.jj}.
+ */
+abstract class BuiltInForLegacyEscaping extends BuiltInBannedWhenAutoEscaping {
+    
+    @Override
+    TemplateModel _eval(Environment env)
+    throws TemplateException {
+        TemplateModel tm = target.eval(env);
+        Object moOrStr = _EvalUtil.coerceModelToStringOrMarkup(tm, target, null, env);
+        if (moOrStr instanceof String) {
+            return calculateResult((String) moOrStr, env);
+        } else {
+            TemplateMarkupOutputModel<?> mo = (TemplateMarkupOutputModel<?>) moOrStr;
+            if (mo.getOutputFormat().isLegacyBuiltInBypassed(key)) {
+                return mo;
+            }
+            throw new NonStringException(target, tm, env);
+        }
+    }
+    
+    abstract TemplateModel calculateResult(String s, Environment env) throws TemplateException;
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForLoopVariable.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForLoopVariable.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForLoopVariable.java
new file mode 100644
index 0000000..5b7d9c3
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForLoopVariable.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.ASTDirList.IterationContext;
+import org.apache.freemarker.core.model.TemplateModel;
+
+abstract class BuiltInForLoopVariable extends SpecialBuiltIn {
+    
+    private String loopVarName;
+    
+    void bindToLoopVariable(String loopVarName) {
+        this.loopVarName = loopVarName;
+    }
+    
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        IterationContext iterCtx = ASTDirList.findEnclosingIterationContext(env, loopVarName);
+        if (iterCtx == null) {
+            // The parser should prevent this situation
+            throw new _MiscTemplateException(
+                    this, env,
+                    "There's no iteration in context that uses loop variable ", new _DelayedJQuote(loopVarName), ".");
+        }
+        
+        return calculateResult(iterCtx, env);
+    }
+
+    abstract TemplateModel calculateResult(IterationContext iterCtx, Environment env) throws TemplateException;
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForMarkupOutput.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForMarkupOutput.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForMarkupOutput.java
new file mode 100644
index 0000000..fa617c2
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForMarkupOutput.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+
+abstract class BuiltInForMarkupOutput extends ASTExpBuiltIn {
+    
+    @Override
+    TemplateModel _eval(Environment env)
+            throws TemplateException {
+        TemplateModel model = target.eval(env);
+        if (!(model instanceof TemplateMarkupOutputModel)) {
+            throw new NonMarkupOutputException(target, model, env);
+        }
+        return calculateResult((TemplateMarkupOutputModel) model);
+    }
+    
+    protected abstract TemplateModel calculateResult(TemplateMarkupOutputModel model) throws TemplateModelException;
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForNode.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForNode.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForNode.java
new file mode 100644
index 0000000..ca0cd61
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForNode.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNodeModel;
+
+abstract class BuiltInForNode extends ASTExpBuiltIn {
+    @Override
+    TemplateModel _eval(Environment env)
+            throws TemplateException {
+        TemplateModel model = target.eval(env);
+        if (model instanceof TemplateNodeModel) {
+            return calculateResult((TemplateNodeModel) model, env);
+        } else {
+            throw new NonNodeException(target, model, env);
+        }
+    }
+    abstract TemplateModel calculateResult(TemplateNodeModel nodeModel, Environment env)
+            throws TemplateModelException;
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForNodeEx.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForNodeEx.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForNodeEx.java
new file mode 100644
index 0000000..8360cbd
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForNodeEx.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNodeModelEx;
+
+abstract class BuiltInForNodeEx extends ASTExpBuiltIn {
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        TemplateModel model = target.eval(env);
+        if (model instanceof TemplateNodeModelEx) {
+            return calculateResult((TemplateNodeModelEx) model, env);
+        } else {
+            throw new NonExtendedNodeException(target, model, env);
+        }
+    }
+    abstract TemplateModel calculateResult(TemplateNodeModelEx nodeModel, Environment env)
+            throws TemplateModelException;
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForNumber.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForNumber.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForNumber.java
new file mode 100644
index 0000000..02954f0
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForNumber.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+
+abstract class BuiltInForNumber extends ASTExpBuiltIn {
+    @Override
+    TemplateModel _eval(Environment env)
+            throws TemplateException {
+        TemplateModel model = target.eval(env);
+        return calculateResult(target.modelToNumber(model, env), model);
+    }
+    
+    abstract TemplateModel calculateResult(Number num, TemplateModel model)
+    throws TemplateModelException;
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForSequence.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForSequence.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForSequence.java
new file mode 100644
index 0000000..8c36823
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForSequence.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+
+abstract class BuiltInForSequence extends ASTExpBuiltIn {
+    @Override
+    TemplateModel _eval(Environment env)
+            throws TemplateException {
+        TemplateModel model = target.eval(env);
+        if (!(model instanceof TemplateSequenceModel)) {
+            throw new NonSequenceException(target, model, env);
+        }
+        return calculateResult((TemplateSequenceModel) model);
+    }
+    abstract TemplateModel calculateResult(TemplateSequenceModel tsm)
+    throws TemplateModelException;
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForString.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForString.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForString.java
new file mode 100644
index 0000000..1102169
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInForString.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateModel;
+
+abstract class BuiltInForString extends ASTExpBuiltIn {
+    @Override
+    TemplateModel _eval(Environment env)
+    throws TemplateException {
+        return calculateResult(getTargetString(target, env), env);
+    }
+    abstract TemplateModel calculateResult(String s, Environment env) throws TemplateException;
+    
+    static String getTargetString(ASTExpression target, Environment env) throws TemplateException {
+        return target.evalAndCoerceToStringOrUnsupportedMarkup(env);
+    }
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInWithParseTimeParameters.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInWithParseTimeParameters.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInWithParseTimeParameters.java
new file mode 100644
index 0000000..d2fa8be
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInWithParseTimeParameters.java
@@ -0,0 +1,109 @@
+/*
+ * 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.util.List;
+
+
+abstract class BuiltInWithParseTimeParameters extends SpecialBuiltIn {
+
+    abstract void bindToParameters(List/*<ASTExpression>*/ parameters, Token openParen, Token closeParen)
+            throws ParseException;
+
+    @Override
+    public String getCanonicalForm() {
+        StringBuilder buf = new StringBuilder();
+        
+        buf.append(super.getCanonicalForm());
+        
+        buf.append("(");
+        List/*<ASTExpression>*/args = getArgumentsAsList();
+        int size = args.size();
+        for (int i = 0; i < size; i++) {
+            if (i != 0) {
+                buf.append(", ");
+            }
+            ASTExpression arg = (ASTExpression) args.get(i);
+            buf.append(arg.getCanonicalForm());
+        }
+        buf.append(")");
+        
+        return buf.toString();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return super.getNodeTypeSymbol() + "(...)";
+    }        
+    
+    @Override
+    int getParameterCount() {
+        return super.getParameterCount() + getArgumentsCount();
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        final int superParamCnt = super.getParameterCount();
+        if (idx < superParamCnt) {
+            return super.getParameterValue(idx); 
+        }
+        
+        final int argIdx = idx - superParamCnt;
+        return getArgumentParameterValue(argIdx);
+    }
+    
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        final int superParamCnt = super.getParameterCount();
+        if (idx < superParamCnt) {
+            return super.getParameterRole(idx); 
+        }
+        
+        if (idx - superParamCnt < getArgumentsCount()) {
+            return ParameterRole.ARGUMENT_VALUE;
+        } else {
+            throw new IndexOutOfBoundsException();
+        }
+    }
+
+    protected ParseException newArgumentCountException(String ordinalityDesc, Token openParen, Token closeParen) {
+        return new ParseException(
+                "?" + key + "(...) " + ordinalityDesc + " parameters", getTemplate(),
+                openParen.beginLine, openParen.beginColumn,
+                closeParen.endLine, closeParen.endColumn);
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+        final ASTExpression clone = super.deepCloneWithIdentifierReplaced_inner(replacedIdentifier, replacement, replacementState);
+        cloneArguments(clone, replacedIdentifier, replacement, replacementState);
+        return clone;
+    }
+
+    protected abstract List getArgumentsAsList();
+    
+    protected abstract int getArgumentsCount();
+
+    protected abstract ASTExpression getArgumentParameterValue(int argIdx);
+    
+    protected abstract void cloneArguments(ASTExpression clone, String replacedIdentifier,
+            ASTExpression replacement, ReplacemenetState replacementState);
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForDates.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForDates.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForDates.java
new file mode 100644
index 0000000..ad11b37
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForDates.java
@@ -0,0 +1,212 @@
+/*
+ * 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.util.Date;
+import java.util.List;
+import java.util.TimeZone;
+
+import org.apache.freemarker.core.model.AdapterTemplateModel;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateMethodModelEx;
+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.model.impl.SimpleDate;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.apache.freemarker.core.util.UnrecognizedTimeZoneException;
+import org.apache.freemarker.core.util._DateUtil;
+
+/**
+ * A holder for built-ins that operate exclusively on date left-hand values.
+ */
+class BuiltInsForDates {
+    
+    static class dateType_if_unknownBI extends ASTExpBuiltIn {
+        
+        private final int dateType;
+
+        dateType_if_unknownBI(int dateType) {
+            this.dateType = dateType;
+        }
+
+        @Override
+        TemplateModel _eval(Environment env)
+                throws TemplateException {
+            TemplateModel model = target.eval(env);
+            if (model instanceof TemplateDateModel) {
+                TemplateDateModel tdm = (TemplateDateModel) model;
+                int tdmDateType = tdm.getDateType();
+                if (tdmDateType != TemplateDateModel.UNKNOWN) {
+                    return tdm;
+                }
+                return new SimpleDate(_EvalUtil.modelToDate(tdm, target), dateType);
+            } else {
+                throw BuiltInForDate.newNonDateException(env, model, target);
+            }
+        }
+
+        protected TemplateModel calculateResult(Date date, int dateType, Environment env) throws TemplateException {
+            // TODO Auto-generated method stub
+            return null;
+        }
+        
+    }
+    
+    /**
+     * Implements {@code ?iso(timeZone)}.
+     */
+    static class iso_BI extends AbstractISOBI {
+        
+        class Result implements TemplateMethodModelEx {
+            private final Date date;
+            private final int dateType;
+            private final Environment env;
+            
+            Result(Date date, int dateType, Environment env) {
+                this.date = date;
+                this.dateType = dateType;
+                this.env = env;
+            }
+
+            @Override
+            public Object exec(List args) throws TemplateModelException {
+                checkMethodArgCount(args, 1);
+                
+                TemplateModel tzArgTM = (TemplateModel) args.get(0);
+                TimeZone tzArg; 
+                Object adaptedObj;
+                if (tzArgTM instanceof AdapterTemplateModel
+                        && (adaptedObj =
+                                ((AdapterTemplateModel) tzArgTM)
+                                .getAdaptedObject(TimeZone.class))
+                            instanceof TimeZone) {
+                    tzArg = (TimeZone) adaptedObj;                    
+                } else if (tzArgTM instanceof TemplateScalarModel) {
+                    String tzName = _EvalUtil.modelToString((TemplateScalarModel) tzArgTM, null, null);
+                    try {
+                        tzArg = _DateUtil.getTimeZone(tzName);
+                    } catch (UnrecognizedTimeZoneException e) {
+                        throw new _TemplateModelException(
+                                "The time zone string specified for ?", key,
+                                "(...) is not recognized as a valid time zone name: ",
+                                new _DelayedJQuote(tzName));
+                    }
+                } else {
+                    throw MessageUtil.newMethodArgUnexpectedTypeException(
+                            "?" + key, 0, "string or java.util.TimeZone", tzArgTM);
+                }
+                
+                return new SimpleScalar(_DateUtil.dateToISO8601String(
+                        date,
+                        dateType != TemplateDateModel.TIME,
+                        dateType != TemplateDateModel.DATE,
+                        shouldShowOffset(date, dateType, env),
+                        accuracy,
+                        tzArg, 
+                        env.getISOBuiltInCalendarFactory()));
+            }
+            
+        }
+
+        iso_BI(Boolean showOffset, int accuracy) {
+            super(showOffset, accuracy);
+        }
+        
+        @Override
+        protected TemplateModel calculateResult(
+                Date date, int dateType, Environment env)
+        throws TemplateException {
+            checkDateTypeNotUnknown(dateType);
+            return new Result(date, dateType, env);
+        }
+        
+    }
+
+    /**
+     * Implements {@code ?iso_utc} and {@code ?iso_local} variants, but not
+     * {@code ?iso(timeZone)}.
+     */
+    static class iso_utc_or_local_BI extends AbstractISOBI {
+        
+        private final boolean useUTC;
+        
+        iso_utc_or_local_BI(Boolean showOffset, int accuracy, boolean useUTC) {
+            super(showOffset, accuracy);
+            this.useUTC = useUTC;
+        }
+
+        @Override
+        protected TemplateModel calculateResult(
+                Date date, int dateType, Environment env)
+        throws TemplateException {
+            checkDateTypeNotUnknown(dateType);
+            return new SimpleScalar(_DateUtil.dateToISO8601String(
+                    date,
+                    dateType != TemplateDateModel.TIME,
+                    dateType != TemplateDateModel.DATE,
+                    shouldShowOffset(date, dateType, env),
+                    accuracy,
+                    useUTC
+                            ? _DateUtil.UTC
+                            : env.shouldUseSQLDTTZ(date.getClass())
+                                    ? env.getSQLDateAndTimeTimeZone()
+                                    : env.getTimeZone(),
+                    env.getISOBuiltInCalendarFactory()));
+        }
+
+    }
+    
+    // Can't be instantiated
+    private BuiltInsForDates() { }
+
+    static abstract class AbstractISOBI extends BuiltInForDate {
+        protected final Boolean showOffset;
+        protected final int accuracy;
+    
+        protected AbstractISOBI(Boolean showOffset, int accuracy) {
+            this.showOffset = showOffset;
+            this.accuracy = accuracy;
+        }
+        
+        protected void checkDateTypeNotUnknown(int dateType)
+        throws TemplateException {
+            if (dateType == TemplateDateModel.UNKNOWN) {
+                throw new _MiscTemplateException(new _ErrorDescriptionBuilder(
+                            "The value of the following has unknown date type, but ?", key,
+                            " needs a value where it's known if it's a date (no time part), time, or date-time value:"                        
+                        ).blame(target).tip(MessageUtil.UNKNOWN_DATE_TYPE_ERROR_TIP));
+            }
+        }
+    
+        protected boolean shouldShowOffset(Date date, int dateType, Environment env) {
+            if (dateType == TemplateDateModel.DATE) {
+                return false;  // ISO 8061 doesn't allow zone for date-only values
+            } else if (showOffset != null) {
+                return showOffset.booleanValue();
+            } else {
+                // java.sql.Time values meant to carry calendar field values only, so we don't show offset for them.
+                return !(date instanceof java.sql.Time);
+            }
+        }
+        
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForExistenceHandling.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForExistenceHandling.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForExistenceHandling.java
new file mode 100644
index 0000000..6e7cce0
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForExistenceHandling.java
@@ -0,0 +1,133 @@
+/*
+ * 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.util.List;
+
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateMethodModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+
+/**
+ * A holder for builtins that deal with null left-hand values.
+ */
+class BuiltInsForExistenceHandling {
+
+    // Can't be instantiated
+    private BuiltInsForExistenceHandling() { }
+
+    private static abstract class ExistenceBuiltIn extends ASTExpBuiltIn {
+    
+        protected TemplateModel evalMaybeNonexistentTarget(Environment env) throws TemplateException {
+            TemplateModel tm;
+            if (target instanceof ASTExpParenthesis) {
+                boolean lastFIRE = env.setFastInvalidReferenceExceptions(true);
+                try {
+                    tm = target.eval(env);
+                } catch (InvalidReferenceException ire) {
+                    tm = null;
+                } finally {
+                    env.setFastInvalidReferenceExceptions(lastFIRE);
+                }
+            } else {
+                tm = target.eval(env);
+            }
+            return tm;
+        }
+        
+    }
+    
+    static class defaultBI extends BuiltInsForExistenceHandling.ExistenceBuiltIn {
+        
+        @Override
+        TemplateModel _eval(final Environment env) throws TemplateException {
+            TemplateModel model = evalMaybeNonexistentTarget(env);
+            return model == null ? FIRST_NON_NULL_METHOD : new ConstantMethod(model);
+        }
+
+        private static class ConstantMethod implements TemplateMethodModelEx {
+            private final TemplateModel constant;
+
+            ConstantMethod(TemplateModel constant) {
+                this.constant = constant;
+            }
+
+            @Override
+            public Object exec(List args) {
+                return constant;
+            }
+        }
+
+        /**
+         * A method that goes through the arguments one by one and returns
+         * the first one that is non-null. If all args are null, returns null.
+         */
+        private static final TemplateMethodModelEx FIRST_NON_NULL_METHOD =
+            new TemplateMethodModelEx() {
+                @Override
+                public Object exec(List args) throws TemplateModelException {
+                    int argCnt = args.size();
+                    if (argCnt == 0) throw MessageUtil.newArgCntError("?default", argCnt, 1, Integer.MAX_VALUE);
+                    for (int i = 0; i < argCnt; i++ ) {
+                        TemplateModel result = (TemplateModel) args.get(i);
+                        if (result != null) return result;
+                    }
+                    return null;
+                }
+            };
+    }
+    
+    static class existsBI extends BuiltInsForExistenceHandling.ExistenceBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            return evalMaybeNonexistentTarget(env) == null ? TemplateBooleanModel.FALSE : TemplateBooleanModel.TRUE;
+        }
+    
+        @Override
+        boolean evalToBoolean(Environment env) throws TemplateException {
+            return _eval(env) == TemplateBooleanModel.TRUE;
+        }
+    }
+
+    static class has_contentBI extends BuiltInsForExistenceHandling.ExistenceBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            return ASTExpression.isEmpty(evalMaybeNonexistentTarget(env))
+                    ? TemplateBooleanModel.FALSE
+                    : TemplateBooleanModel.TRUE;
+        }
+    
+        @Override
+        boolean evalToBoolean(Environment env) throws TemplateException {
+            return _eval(env) == TemplateBooleanModel.TRUE;
+        }
+    }
+
+    static class if_existsBI extends BuiltInsForExistenceHandling.ExistenceBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env)
+                throws TemplateException {
+            TemplateModel model = evalMaybeNonexistentTarget(env);
+            return model == null ? TemplateModel.NOTHING : model;
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForHashes.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForHashes.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForHashes.java
new file mode 100644
index 0000000..8baa9cc
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForHashes.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 org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.impl.CollectionAndSequence;
+
+/**
+ * A holder for builtins that operate exclusively on hash left-hand value.
+ */
+class BuiltInsForHashes {
+
+    static class keysBI extends BuiltInForHashEx {
+
+        @Override
+        TemplateModel calculateResult(TemplateHashModelEx hashExModel, Environment env)
+                throws TemplateModelException, InvalidReferenceException {
+            TemplateCollectionModel keys = hashExModel.keys();
+            if (keys == null) throw newNullPropertyException("keys", hashExModel, env);
+            return keys instanceof TemplateSequenceModel ? keys : new CollectionAndSequence(keys);
+        }
+        
+    }
+    
+    static class valuesBI extends BuiltInForHashEx {
+        @Override
+        TemplateModel calculateResult(TemplateHashModelEx hashExModel, Environment env)
+                throws TemplateModelException, InvalidReferenceException {
+            TemplateCollectionModel values = hashExModel.values();
+            if (values == null) throw newNullPropertyException("values", hashExModel, env);
+            return values instanceof TemplateSequenceModel ? values : new CollectionAndSequence(values);
+        }
+    }
+
+    // Can't be instantiated
+    private BuiltInsForHashes() { }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForLoopVariables.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForLoopVariables.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForLoopVariables.java
new file mode 100644
index 0000000..8d55e0e
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForLoopVariables.java
@@ -0,0 +1,156 @@
+/*
+ * 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.util.List;
+
+import org.apache.freemarker.core.ASTDirList.IterationContext;
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateMethodModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+
+
+class BuiltInsForLoopVariables {
+    
+    static class indexBI extends BuiltInForLoopVariable {
+
+        @Override
+        TemplateModel calculateResult(IterationContext iterCtx, Environment env) throws TemplateException {
+            return new SimpleNumber(iterCtx.getIndex());
+        }
+        
+    }
+    
+    static class counterBI extends BuiltInForLoopVariable {
+
+        @Override
+        TemplateModel calculateResult(IterationContext iterCtx, Environment env) throws TemplateException {
+            return new SimpleNumber(iterCtx.getIndex() + 1);
+        }
+        
+    }
+
+    static abstract class BooleanBuiltInForLoopVariable extends BuiltInForLoopVariable {
+
+        @Override
+        final TemplateModel calculateResult(IterationContext iterCtx, Environment env) throws TemplateException {
+            return calculateBooleanResult(iterCtx, env) ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+
+        protected abstract boolean calculateBooleanResult(IterationContext iterCtx, Environment env);
+        
+    }
+    
+    static class has_nextBI extends BooleanBuiltInForLoopVariable {
+
+        @Override
+        protected boolean calculateBooleanResult(IterationContext iterCtx, Environment env) {
+            return iterCtx.hasNext();
+        }
+
+    }
+
+    static class is_lastBI extends BooleanBuiltInForLoopVariable {
+
+        @Override
+        protected boolean calculateBooleanResult(IterationContext iterCtx, Environment env) {
+            return !iterCtx.hasNext();
+        }
+        
+    }
+
+    static class is_firstBI extends BooleanBuiltInForLoopVariable {
+
+        @Override
+        protected boolean calculateBooleanResult(IterationContext iterCtx, Environment env) {
+            return iterCtx.getIndex() == 0;
+        }
+        
+    }
+
+    static class is_odd_itemBI extends BooleanBuiltInForLoopVariable {
+
+        @Override
+        protected boolean calculateBooleanResult(IterationContext iterCtx, Environment env) {
+            return iterCtx.getIndex() % 2 == 0;
+        }
+        
+    }
+
+    static class is_even_itemBI extends BooleanBuiltInForLoopVariable {
+
+        @Override
+        protected boolean calculateBooleanResult(IterationContext iterCtx, Environment env) {
+            return iterCtx.getIndex() % 2 != 0;
+        }
+        
+    }
+    
+    static class item_parityBI extends BuiltInForLoopVariable {
+        
+        private static final SimpleScalar ODD = new SimpleScalar("odd");
+        private static final SimpleScalar EVEN = new SimpleScalar("even");
+
+        @Override
+        TemplateModel calculateResult(IterationContext iterCtx, Environment env) throws TemplateException {
+            return iterCtx.getIndex() % 2 == 0 ? ODD: EVEN;
+        }
+        
+    }
+
+    static class item_parity_capBI extends BuiltInForLoopVariable {
+        
+        private static final SimpleScalar ODD = new SimpleScalar("Odd");
+        private static final SimpleScalar EVEN = new SimpleScalar("Even");
+
+        @Override
+        TemplateModel calculateResult(IterationContext iterCtx, Environment env) throws TemplateException {
+            return iterCtx.getIndex() % 2 == 0 ? ODD: EVEN;
+        }
+        
+    }
+
+    static class item_cycleBI extends BuiltInForLoopVariable {
+
+        private class BIMethod implements TemplateMethodModelEx {
+            
+            private final IterationContext iterCtx;
+    
+            private BIMethod(IterationContext iterCtx) {
+                this.iterCtx = iterCtx;
+            }
+    
+            @Override
+            public Object exec(List args) throws TemplateModelException {
+                checkMethodArgCount(args, 1, Integer.MAX_VALUE);
+                return args.get(iterCtx.getIndex() % args.size());
+            }
+        }
+        
+        @Override
+        TemplateModel calculateResult(IterationContext iterCtx, Environment env) throws TemplateException {
+            return new BIMethod(iterCtx);
+        }
+        
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForMarkupOutputs.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForMarkupOutputs.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForMarkupOutputs.java
new file mode 100644
index 0000000..f895526
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForMarkupOutputs.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 org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+
+/**
+ * A holder for builtins that operate exclusively on markup output left-hand value.
+ */
+class BuiltInsForMarkupOutputs {
+    
+    static class markup_stringBI extends BuiltInForMarkupOutput {
+
+        @Override
+        protected TemplateModel calculateResult(TemplateMarkupOutputModel model) throws TemplateModelException {
+            return new SimpleScalar(model.getOutputFormat().getMarkupString(model));
+        }
+        
+    }
+    
+}


[15/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateResolver.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateResolver.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateResolver.java
new file mode 100644
index 0000000..ea1cc63
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateResolver.java
@@ -0,0 +1,904 @@
+/*
+ * 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.templateresolver.impl;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.StringTokenizer;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core.TemplateConfiguration;
+import org.apache.freemarker.core.TemplateLanguage;
+import org.apache.freemarker.core.TemplateNotFoundException;
+import org.apache.freemarker.core.WrongTemplateCharsetException;
+import org.apache.freemarker.core._CoreLogs;
+import org.apache.freemarker.core.templateresolver.CacheStorage;
+import org.apache.freemarker.core.templateresolver.GetTemplateResult;
+import org.apache.freemarker.core.templateresolver.MalformedTemplateNameException;
+import org.apache.freemarker.core.templateresolver.TemplateConfigurationFactory;
+import org.apache.freemarker.core.templateresolver.TemplateConfigurationFactoryException;
+import org.apache.freemarker.core.templateresolver.TemplateLoader;
+import org.apache.freemarker.core.templateresolver.TemplateLoaderSession;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingResult;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingResultStatus;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingSource;
+import org.apache.freemarker.core.templateresolver.TemplateLookupStrategy;
+import org.apache.freemarker.core.templateresolver.TemplateNameFormat;
+import org.apache.freemarker.core.templateresolver.TemplateResolver;
+import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.util.UndeclaredThrowableException;
+import org.apache.freemarker.core.util._NullArgumentException;
+import org.apache.freemarker.core.util._StringUtil;
+import org.slf4j.Logger;
+
+/**
+ * Performs caching and on-demand loading of the templates.
+ * The actual template "file" loading is delegated to a {@link TemplateLoader} that you can specify in the constructor.
+ * Some aspects of caching is delegated to a {@link CacheStorage} that you can also specify in the constructor.
+ * 
+ * <p>Typically you don't instantiate or otherwise use this class directly. By default the {@link Configuration} embeds
+ * an instance of this class, that you access indirectly through {@link Configuration#getTemplate(String)} and other
+ * {@link Configuration} API-s. When you set the {@link Configuration#getTemplateLoader() templateLoader} or
+ * {@link Configuration#getCacheStorage() cacheStorage} of the {@link Configuration}, you indirectly configure the
+ * {@link TemplateResolver}.
+ */
+public class DefaultTemplateResolver extends TemplateResolver {
+    
+    /**
+     * The default template update delay; see {@link Configuration#getTemplateUpdateDelayMilliseconds()}.
+     * 
+     * @since 2.3.23
+     */
+    public static final long DEFAULT_TEMPLATE_UPDATE_DELAY_MILLIS = 5000L;
+    
+    private static final String ASTERISKSTR = "*";
+    private static final char ASTERISK = '*';
+    private static final char SLASH = '/';
+    private static final String LOCALE_PART_SEPARATOR = "_";
+    private static final Logger LOG = _CoreLogs.TEMPLATE_RESOLVER;
+
+    /** Maybe {@code null}. */
+    private final TemplateLoader templateLoader;
+    
+    /** Here we keep our cached templates */
+    private final CacheStorage cacheStorage;
+    private final TemplateLookupStrategy templateLookupStrategy;
+    private final TemplateNameFormat templateNameFormat;
+    private final TemplateConfigurationFactory templateConfigurations;
+    private final long templateUpdateDelayMilliseconds;
+    private final boolean localizedLookup;
+
+    private Configuration config;
+    
+    /**
+     * @param templateLoader
+     *            The {@link TemplateLoader} to use. Can be {@code null}, though then every request will result in
+     *            {@link TemplateNotFoundException}.
+     * @param cacheStorage
+     *            The {@link CacheStorage} to use. Can't be {@code null}.
+     * @param templateLookupStrategy
+     *            The {@link TemplateLookupStrategy} to use. Can't be {@code null}.
+     * @param templateUpdateDelayMilliseconds
+     *            See {@link Configuration#getTemplateUpdateDelayMilliseconds()}
+     * @param templateNameFormat
+     *            The {@link TemplateNameFormat} to use. Can't be {@code null}.
+     * @param templateConfigurations
+     *            The {@link TemplateConfigurationFactory} to use. Can be {@code null} (then all templates will use the
+     *            settings coming from the {@link Configuration} as is, except in the very rare case where a
+     *            {@link TemplateLoader} itself specifies a {@link TemplateConfiguration}).
+     * @param config
+     *            The {@link Configuration} this cache will be used for. Can't be {@code null}.
+     * 
+     * @since 2.3.24
+     */
+    public DefaultTemplateResolver(
+            TemplateLoader templateLoader,
+            CacheStorage cacheStorage, long templateUpdateDelayMilliseconds,
+            TemplateLookupStrategy templateLookupStrategy, boolean localizedLookup,
+            TemplateNameFormat templateNameFormat,
+            TemplateConfigurationFactory templateConfigurations,
+            Configuration config) {
+        super(config);
+        
+        this.templateLoader = templateLoader;
+        
+        _NullArgumentException.check("cacheStorage", cacheStorage);
+        this.cacheStorage = cacheStorage;
+        
+        this.templateUpdateDelayMilliseconds = templateUpdateDelayMilliseconds;
+        
+        this.localizedLookup = localizedLookup;
+        
+        _NullArgumentException.check("templateLookupStrategy", templateLookupStrategy);
+        this.templateLookupStrategy = templateLookupStrategy;
+
+        _NullArgumentException.check("templateNameFormat", templateNameFormat);
+        this.templateNameFormat = templateNameFormat;
+
+        // Can be null
+        this.templateConfigurations = templateConfigurations;
+        
+        _NullArgumentException.check("config", config);
+        this.config = config;
+    }
+    
+    /**
+     * Returns the configuration for internal usage.
+     */
+    @Override
+    public Configuration getConfiguration() {
+        return config;
+    }
+
+    public TemplateLoader getTemplateLoader() {
+        return templateLoader;
+    }
+
+    public CacheStorage getCacheStorage() {
+        return cacheStorage;
+    }
+    
+    /**
+     * @since 2.3.22
+     */
+    public TemplateLookupStrategy getTemplateLookupStrategy() {
+        return templateLookupStrategy;
+    }
+    
+    /**
+     * @since 2.3.22
+     */
+    public TemplateNameFormat getTemplateNameFormat() {
+        return templateNameFormat;
+    }
+    
+    /**
+     * @since 2.3.24
+     */
+    public TemplateConfigurationFactory getTemplateConfigurations() {
+        return templateConfigurations;
+    }
+
+    /**
+     * Retrieves the template with the given name (and according the specified further parameters) from the template
+     * cache, loading it into the cache first if it's missing/staled.
+     * 
+     * <p>
+     * All parameters must be non-{@code null}, except {@code customLookupCondition}. For the meaning of the parameters
+     * see {@link Configuration#getTemplate(String, Locale, Serializable, boolean)}.
+     *
+     * @return A {@link GetTemplateResult} object that contains the {@link Template}, or a
+     *         {@link GetTemplateResult} object that contains {@code null} as the {@link Template} and information
+     *         about the missing template. The return value itself is never {@code null}. Note that exceptions occurring
+     *         during template loading will not be classified as a missing template, so they will cause an exception to
+     *         be thrown by this method instead of returning a {@link GetTemplateResult}. The idea is that having a
+     *         missing template is normal (not exceptional), providing that the backing storage mechanism could indeed
+     *         check that it's missing.
+     * 
+     * @throws MalformedTemplateNameException
+     *             If the {@code name} was malformed according the current {@link TemplateNameFormat}. However, if the
+     *             {@link TemplateNameFormat} is {@link DefaultTemplateNameFormatFM2#INSTANCE} and
+     *             {@link Configuration#getIncompatibleImprovements()} is less than 2.4.0, then instead of throwing this
+     *             exception, a {@link GetTemplateResult} will be returned, similarly as if the template were missing
+     *             (the {@link GetTemplateResult#getMissingTemplateReason()} will describe the real error).
+     * 
+     * @throws IOException
+     *             If reading the template has failed from a reason other than the template is missing. This method
+     *             should never be a {@link TemplateNotFoundException}, as that condition is indicated in the return
+     *             value.
+     * 
+     * @since 2.3.22
+     */
+    @Override
+    public GetTemplateResult getTemplate(String name, Locale locale, Serializable customLookupCondition)
+    throws IOException {
+        _NullArgumentException.check("name", name);
+        _NullArgumentException.check("locale", locale);
+
+        name = templateNameFormat.normalizeRootBasedName(name);
+        
+        if (templateLoader == null) {
+            return new GetTemplateResult(name, "The TemplateLoader (and TemplateLoader2) was null.");
+        }
+        
+        Template template = getTemplateInternal(name, locale, customLookupCondition);
+        return template != null ? new GetTemplateResult(template) : new GetTemplateResult(name, (String) null);
+    }
+
+    @Override
+    public String toRootBasedName(String baseName, String targetName) throws MalformedTemplateNameException {
+        return templateNameFormat.toRootBasedName(baseName, targetName);
+    }
+
+    @Override
+    public String normalizeRootBasedName(String name) throws MalformedTemplateNameException {
+        return templateNameFormat.normalizeRootBasedName(name);
+    }
+
+    private Template getTemplateInternal(
+            final String name, final Locale locale, final Serializable customLookupCondition)
+    throws IOException {
+        final boolean debug = LOG.isDebugEnabled();
+        final String debugPrefix = debug
+                ? getDebugPrefix("getTemplate", name, locale, customLookupCondition)
+                : null;
+        final CachedResultKey cacheKey = new CachedResultKey(name, locale, customLookupCondition);
+        
+        CachedResult oldCachedResult = (CachedResult) cacheStorage.get(cacheKey);
+        
+        final long now = System.currentTimeMillis();
+        
+        boolean rethrownCachedException = false;
+        boolean suppressFinallyException = false;
+        TemplateLoaderBasedTemplateLookupResult newLookupResult = null;
+        CachedResult newCachedResult = null;
+        TemplateLoaderSession session = null;
+        try {
+            if (oldCachedResult != null) {
+                // If we're within the refresh delay, return the cached result
+                if (now - oldCachedResult.lastChecked < templateUpdateDelayMilliseconds) {
+                    if (debug) {
+                        LOG.debug(debugPrefix + "Cached copy not yet stale; using cached.");
+                    }
+                    Object t = oldCachedResult.templateOrException;
+                    // t can be null, indicating a cached negative lookup
+                    if (t instanceof Template || t == null) {
+                        return (Template) t;
+                    } else if (t instanceof RuntimeException) {
+                        rethrowCachedException((RuntimeException) t);
+                    } else if (t instanceof IOException) {
+                        rethrownCachedException = true;
+                        rethrowCachedException((IOException) t);
+                    }
+                    throw new BugException("Unhandled class for t: " + t.getClass().getName());
+                }
+                // The freshness of the cache result must be checked.
+                
+                // Clone, as the instance in the cache store must not be modified to ensure proper concurrent behavior.
+                newCachedResult = oldCachedResult.clone();
+                newCachedResult.lastChecked = now;
+
+                session = templateLoader.createSession();
+                if (debug && session != null) {
+                    LOG.debug(debugPrefix + "Session created.");
+                }
+                
+                // Find the template source, load it if it doesn't correspond to the cached result.
+                newLookupResult = lookupAndLoadTemplateIfChanged(
+                        name, locale, customLookupCondition, oldCachedResult.source, oldCachedResult.version, session);
+
+                // Template source was removed (TemplateLoader2ResultStatus.NOT_FOUND, or no TemplateLoader2Result)
+                if (!newLookupResult.isPositive()) { 
+                    if (debug) {
+                        LOG.debug(debugPrefix + "No source found.");
+                    } 
+                    setToNegativeAndPutIntoCache(cacheKey, newCachedResult, null);
+                    return null;
+                }
+
+                final TemplateLoadingResult newTemplateLoaderResult = newLookupResult.getTemplateLoaderResult();
+                if (newTemplateLoaderResult.getStatus() == TemplateLoadingResultStatus.NOT_MODIFIED) {
+                    // Return the cached version.
+                    if (debug) {
+                        LOG.debug(debugPrefix + ": Using cached template "
+                                + "(source: " + newTemplateLoaderResult.getSource() + ")"
+                                + " as it hasn't been changed on the backing store.");
+                    }
+                    cacheStorage.put(cacheKey, newCachedResult);
+                    return (Template) newCachedResult.templateOrException;
+                } else {
+                    if (newTemplateLoaderResult.getStatus() != TemplateLoadingResultStatus.OPENED) {
+                        // TemplateLoader2ResultStatus.NOT_FOUND was already handler earlier
+                        throw new BugException("Unxpected status: " + newTemplateLoaderResult.getStatus());
+                    }
+                    if (debug) {
+                        StringBuilder debugMsg = new StringBuilder();
+                        debugMsg.append(debugPrefix)
+                                .append("Reloading template instead of using the cached result because ");
+                        if (newCachedResult.templateOrException instanceof Throwable) {
+                            debugMsg.append("it's a cached error (retrying).");
+                        } else {
+                            Object newSource = newTemplateLoaderResult.getSource();
+                            if (!nullSafeEquals(newSource, oldCachedResult.source)) {
+                                debugMsg.append("the source has been changed: ")
+                                        .append("cached.source=").append(_StringUtil.jQuoteNoXSS(oldCachedResult.source))
+                                        .append(", current.source=").append(_StringUtil.jQuoteNoXSS(newSource));
+                            } else {
+                                Serializable newVersion = newTemplateLoaderResult.getVersion();
+                                if (!nullSafeEquals(oldCachedResult.version, newVersion)) {
+                                    debugMsg.append("the version has been changed: ")
+                                            .append("cached.version=").append(oldCachedResult.version) 
+                                            .append(", current.version=").append(newVersion);
+                                } else {
+                                    debugMsg.append("??? (unknown reason)");
+                                }
+                            }
+                        }
+                        LOG.debug(debugMsg.toString());
+                    }
+                }
+            } else { // if there was no cached result
+                if (debug) {
+                    LOG.debug(debugPrefix + "No cached result was found; will try to load template.");
+                }
+                
+                newCachedResult = new CachedResult();
+                newCachedResult.lastChecked = now;
+            
+                session = templateLoader.createSession();
+                if (debug && session != null) {
+                    LOG.debug(debugPrefix + "Session created.");
+                } 
+                
+                newLookupResult = lookupAndLoadTemplateIfChanged(
+                        name, locale, customLookupCondition, null, null, session);
+                
+                if (!newLookupResult.isPositive()) {
+                    setToNegativeAndPutIntoCache(cacheKey, newCachedResult, null);
+                    return null;
+                }
+            }
+            // We have newCachedResult and newLookupResult initialized at this point.
+
+            TemplateLoadingResult templateLoaderResult = newLookupResult.getTemplateLoaderResult();
+            newCachedResult.source = templateLoaderResult.getSource();
+            
+            // If we get here, then we need to (re)load the template
+            if (debug) {
+                LOG.debug(debugPrefix + "Reading template content (source: "
+                        + _StringUtil.jQuoteNoXSS(newCachedResult.source) + ")");
+            }
+            
+            Template template = loadTemplate(
+                    templateLoaderResult,
+                    name, newLookupResult.getTemplateSourceName(), locale, customLookupCondition);
+            if (session != null) {
+                session.close();
+                if (debug) {
+                    LOG.debug(debugPrefix + "Session closed.");
+                } 
+            }
+            newCachedResult.templateOrException = template;
+            newCachedResult.version = templateLoaderResult.getVersion();
+            cacheStorage.put(cacheKey, newCachedResult);
+            return template;
+        } catch (RuntimeException e) {
+            if (newCachedResult != null) {
+                setToNegativeAndPutIntoCache(cacheKey, newCachedResult, e);
+            }
+            suppressFinallyException = true;
+            throw e;
+        } catch (IOException e) {
+            // Rethrown cached exceptions are wrapped into IOException-s, so we only need this condition here.
+            if (!rethrownCachedException) {
+                setToNegativeAndPutIntoCache(cacheKey, newCachedResult, e);
+            }
+            suppressFinallyException = true;
+            throw e;
+        } finally {
+            try {
+                // Close streams first:
+                
+                if (newLookupResult != null && newLookupResult.isPositive()) {
+                    TemplateLoadingResult templateLoaderResult = newLookupResult.getTemplateLoaderResult();
+                    Reader reader = templateLoaderResult.getReader();
+                    if (reader != null) {
+                        try {
+                            reader.close();
+                        } catch (IOException e) { // [FM3] Exception e
+                            if (suppressFinallyException) {
+                                if (LOG.isWarnEnabled()) { 
+                                    LOG.warn("Failed to close template content Reader for: " + name, e);
+                                }
+                            } else {
+                                suppressFinallyException = true;
+                                throw e;
+                            }
+                        }
+                    } else if (templateLoaderResult.getInputStream() != null) {
+                        try {
+                            templateLoaderResult.getInputStream().close();
+                        } catch (IOException e) { // [FM3] Exception e
+                            if (suppressFinallyException) {
+                                if (LOG.isWarnEnabled()) { 
+                                    LOG.warn("Failed to close template content InputStream for: " + name, e);
+                                }
+                            } else {
+                                suppressFinallyException = true;
+                                throw e;
+                            }
+                        }
+                    }
+                }
+            } finally {
+                // Then close streams:
+                
+                if (session != null && !session.isClosed()) {
+                    try {
+                        session.close();
+                        if (debug) {
+                            LOG.debug(debugPrefix + "Session closed.");
+                        } 
+                    } catch (IOException e) { // [FM3] Exception e
+                        if (suppressFinallyException) {
+                            if (LOG.isWarnEnabled()) { 
+                                LOG.warn("Failed to close template loader session for" + name, e);
+                            }
+                        } else {
+                            suppressFinallyException = true;
+                            throw e;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    
+    
+    private static final Method INIT_CAUSE = getInitCauseMethod();
+    
+    private static Method getInitCauseMethod() {
+        try {
+            return Throwable.class.getMethod("initCause", Throwable.class);
+        } catch (NoSuchMethodException e) {
+            return null;
+        }
+    }
+    
+    /**
+     * Creates an {@link IOException} that has a cause exception.
+     */
+    // [Java 6] Remove
+    private IOException newIOException(String message, Throwable cause) {
+        if (cause == null) {
+            return new IOException(message);
+        }
+        
+        IOException ioe;
+        if (INIT_CAUSE != null) {
+            ioe = new IOException(message);
+            try {
+                INIT_CAUSE.invoke(ioe, cause);
+            } catch (RuntimeException ex) {
+                throw ex;
+            } catch (Exception ex) {
+                throw new UndeclaredThrowableException(ex);
+            }
+        } else {
+            ioe = new IOException(message + "\nCaused by: " + cause.getClass().getName() + 
+            ": " + cause.getMessage());
+        }
+        return ioe;
+    }
+    
+    private void rethrowCachedException(Throwable e) throws IOException {
+        throw newIOException("There was an error loading the " +
+                "template on an earlier attempt; see cause exception.", e);
+    }
+
+    private void setToNegativeAndPutIntoCache(CachedResultKey cacheKey, CachedResult cachedResult, Exception e) {
+        cachedResult.templateOrException = e;
+        cachedResult.source = null;
+        cachedResult.version = null;
+        cacheStorage.put(cacheKey, cachedResult);
+    }
+
+    private Template loadTemplate(
+            TemplateLoadingResult templateLoaderResult,
+            final String name, final String sourceName, Locale locale, final Serializable customLookupCondition)
+            throws IOException {
+        TemplateConfiguration tc;
+        {
+            TemplateConfiguration cfgTC;
+            try {
+                cfgTC = templateConfigurations != null
+                        ? templateConfigurations.get(sourceName, templateLoaderResult.getSource()) : null;
+            } catch (TemplateConfigurationFactoryException e) {
+                throw newIOException("Error while getting TemplateConfiguration; see cause exception.", e);
+            }
+            TemplateConfiguration templateLoaderResultTC = templateLoaderResult.getTemplateConfiguration();
+            if (templateLoaderResultTC != null) {
+                TemplateConfiguration.Builder mergedTCBuilder = new TemplateConfiguration.Builder();
+                if (cfgTC != null) {
+                    mergedTCBuilder.merge(cfgTC);
+                }
+                mergedTCBuilder.merge(templateLoaderResultTC);
+
+                tc = mergedTCBuilder.build();
+            } else {
+                tc = cfgTC;
+            }
+        }
+
+        if (tc != null && tc.isLocaleSet()) {
+            locale = tc.getLocale();
+        }
+        Charset initialEncoding = tc != null && tc.isSourceEncodingSet() ? tc.getSourceEncoding()
+                : config.getSourceEncoding();
+        TemplateLanguage templateLanguage = tc != null && tc.isTemplateLanguageSet() ? tc.getTemplateLanguage()
+                : config.getTemplateLanguage();
+
+        Template template;
+        {
+            Reader reader = templateLoaderResult.getReader();
+            InputStream inputStream = templateLoaderResult.getInputStream();
+            InputStream markedInputStream;
+            if (reader != null) {
+                if (inputStream != null) {
+                    throw new IllegalStateException("For a(n) " + templateLoaderResult.getClass().getName()
+                            + ", both getReader() and getInputStream() has returned non-null.");
+                }
+                initialEncoding = null;  // No charset decoding has happened
+                markedInputStream = null;
+            } else if (inputStream != null) {
+                if (templateLanguage.getCanSpecifyCharsetInContent()) {
+                    // We need mark support, to restart if the charset suggested by <#ftl encoding=...> differs
+                    // from that we use initially.
+                    if (!inputStream.markSupported()) {
+                        inputStream = new BufferedInputStream(inputStream);
+                    }
+                    inputStream.mark(Integer.MAX_VALUE); // Mark is released after the 1st FTL tag
+                    markedInputStream = inputStream;
+                } else {
+                    markedInputStream = null;
+                }
+                // Regarding buffering worries: On the Reader side we should only read in chunks (like through a
+                // BufferedReader), so there shouldn't be a problem if the InputStream is not buffered. (Also, at least
+                // on Oracle JDK and OpenJDK 7 the InputStreamReader itself has an internal ~8K buffer.)
+                reader = new InputStreamReader(inputStream, initialEncoding);
+            } else {
+                throw new IllegalStateException("For a(n) " + templateLoaderResult.getClass().getName()
+                        + ", both getReader() and getInputStream() has returned null.");
+            }
+            
+            try {
+                try {
+                    template = templateLanguage.parse(name, sourceName, reader, config, tc,
+                            initialEncoding, markedInputStream);
+                } catch (WrongTemplateCharsetException charsetException) {
+                    final Charset templateSpecifiedEncoding = charsetException.getTemplateSpecifiedEncoding();
+
+                    if (inputStream != null) {
+                        // We restart InputStream to re-decode it with the new charset.
+                        inputStream.reset();
+
+                        // Don't close `reader`; it's an InputStreamReader that would close the wrapped InputStream.
+                        reader = new InputStreamReader(inputStream, templateSpecifiedEncoding);
+                    } else {
+                        throw new IllegalStateException(
+                                "TemplateLanguage " + _StringUtil.jQuote(templateLanguage.getName()) + " has thrown "
+                                + WrongTemplateCharsetException.class.getName()
+                                + ", but its canSpecifyCharsetInContent property is false.");
+                    }
+
+                    template = templateLanguage.parse(name, sourceName, reader, config, tc,
+                            templateSpecifiedEncoding, markedInputStream);
+                }
+            } finally {
+                reader.close();
+            }
+        }
+
+        template.setLookupLocale(locale);
+        template.setCustomLookupCondition(customLookupCondition);
+        return template;
+    }
+
+    /**
+     * Gets the delay in milliseconds between checking for newer versions of a
+     * template source.
+     * @return the current value of the delay
+     */
+    public long getTemplateUpdateDelayMilliseconds() {
+        // synchronized was moved here so that we don't advertise that it's thread-safe, as it's not.
+        synchronized (this) {
+            return templateUpdateDelayMilliseconds;
+        }
+    }
+
+    /**
+     * Returns if localized template lookup is enabled or not.
+     */
+    public boolean getLocalizedLookup() {
+        // synchronized was moved here so that we don't advertise that it's thread-safe, as it's not.
+        synchronized (this) {
+            return localizedLookup;
+        }
+    }
+
+    /**
+     * Removes all entries from the cache, forcing reloading of templates on subsequent
+     * {@link #getTemplate(String, Locale, Serializable)} calls.
+     * 
+     * @param resetTemplateLoader
+     *            Whether to call {@link TemplateLoader#resetState()}. on the template loader.
+     */
+    public void clearTemplateCache(boolean resetTemplateLoader) {
+        synchronized (cacheStorage) {
+            cacheStorage.clear();
+            if (templateLoader != null && resetTemplateLoader) {
+                templateLoader.resetState();
+            }
+        }
+    }
+    
+    /**
+     * Same as {@link #clearTemplateCache(boolean)} with {@code true} {@code resetTemplateLoader} argument.
+     */
+    @Override
+    public void clearTemplateCache() {
+        synchronized (cacheStorage) {
+            cacheStorage.clear();
+            if (templateLoader != null) {
+                templateLoader.resetState();
+            }
+        }
+    }
+
+    /**
+     * Removes an entry from the cache, hence forcing the re-loading of it when it's next time requested. (It doesn't
+     * delete the template file itself.) This is to give the application finer control over cache updating than the
+     * update delay ({@link #getTemplateUpdateDelayMilliseconds()}) alone does.
+     * 
+     * For the meaning of the parameters, see
+     * {@link Configuration#getTemplate(String, Locale, Serializable, boolean)}
+     */
+    @Override
+    public void removeTemplateFromCache(
+            String name, Locale locale, Serializable customLookupCondition)
+    throws IOException {
+        if (name == null) {
+            throw new IllegalArgumentException("Argument \"name\" can't be null");
+        }
+        if (locale == null) {
+            throw new IllegalArgumentException("Argument \"locale\" can't be null");
+        }
+        name = templateNameFormat.normalizeRootBasedName(name);
+        if (name != null && templateLoader != null) {
+            boolean debug = LOG.isDebugEnabled();
+            String debugPrefix = debug
+                    ? getDebugPrefix("removeTemplate", name, locale, customLookupCondition)
+                    : null;
+            CachedResultKey tk = new CachedResultKey(name, locale, customLookupCondition);
+            
+            cacheStorage.remove(tk);
+            if (debug) {
+                LOG.debug(debugPrefix + "Template was removed from the cache, if it was there");
+            }
+        }
+    }
+
+    private String getDebugPrefix(String operation, String name, Locale locale, Object customLookupCondition) {
+        return operation + " " + _StringUtil.jQuoteNoXSS(name) + "("
+                + _StringUtil.jQuoteNoXSS(locale)
+                + (customLookupCondition != null ? ", cond=" + _StringUtil.jQuoteNoXSS(customLookupCondition) : "")
+                + "): ";
+    }    
+
+    /**
+     * Looks up according the {@link TemplateLookupStrategy} and then starts reading the template, if it was changed
+     * compared to the cached result, or if there was no cached result yet.
+     */
+    private TemplateLoaderBasedTemplateLookupResult lookupAndLoadTemplateIfChanged(
+            String name, Locale locale, Object customLookupCondition,
+            TemplateLoadingSource cachedResultSource, Serializable cachedResultVersion,
+            TemplateLoaderSession session) throws IOException {
+        final TemplateLoaderBasedTemplateLookupResult lookupResult = templateLookupStrategy.lookup(
+                new DefaultTemplateResolverTemplateLookupContext(
+                        name, locale, customLookupCondition,
+                        cachedResultSource, cachedResultVersion,
+                        session));
+        if (lookupResult == null) {
+            throw new NullPointerException("Lookup result shouldn't be null");
+        }
+        return lookupResult;
+    }
+
+    private String concatPath(List<String> pathSteps, int from, int to) {
+        StringBuilder buf = new StringBuilder((to - from) * 16);
+        for (int i = from; i < to; ++i) {
+            buf.append(pathSteps.get(i));
+            if (i < pathSteps.size() - 1) {
+                buf.append('/');
+            }
+        }
+        return buf.toString();
+    }
+    
+    // Replace with Objects.equals in Java 7
+    private static boolean nullSafeEquals(Object o1, Object o2) {
+        if (o1 == o2) return true;
+        if (o1 == null || o2 == null) return false;
+        return o1.equals(o2);
+    }
+    
+    /**
+     * Used as cache key to look up a {@link CachedResult}. 
+     */
+    @SuppressWarnings("serial")
+    private static final class CachedResultKey implements Serializable {
+        private final String name;
+        private final Locale locale;
+        private final Serializable customLookupCondition;
+
+        CachedResultKey(String name, Locale locale, Serializable customLookupCondition) {
+            this.name = name;
+            this.locale = locale;
+            this.customLookupCondition = customLookupCondition;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof CachedResultKey)) {
+                return false;
+            }
+            CachedResultKey tk = (CachedResultKey) o;
+            return
+                    name.equals(tk.name) &&
+                    locale.equals(tk.locale) &&
+                    nullSafeEquals(customLookupCondition, tk.customLookupCondition);
+        }
+
+        @Override
+        public int hashCode() {
+            int result = name.hashCode();
+            result = 31 * result + locale.hashCode();
+            result = 31 * result + (customLookupCondition != null ? customLookupCondition.hashCode() : 0);
+            return result;
+        }
+
+    }
+
+    /**
+     * Hold the a cached {@link #getTemplate(String, Locale, Serializable)} result and the associated
+     * information needed to check if the cached value is up to date.
+     * 
+     * <p>
+     * Note: this class is Serializable to allow custom 3rd party CacheStorage implementations to serialize/replicate
+     * them; FreeMarker code itself doesn't rely on its serializability.
+     * 
+     * @see CachedResultKey
+     */
+    private static final class CachedResult implements Cloneable, Serializable {
+        private static final long serialVersionUID = 1L;
+
+        Object templateOrException;
+        TemplateLoadingSource source;
+        Serializable version;
+        long lastChecked;
+        
+        @Override
+        public CachedResult clone() {
+            try {
+                return (CachedResult) super.clone();
+            } catch (CloneNotSupportedException e) {
+                throw new UndeclaredThrowableException(e);
+            }
+        }
+    }
+    
+    private class DefaultTemplateResolverTemplateLookupContext extends TemplateLoaderBasedTemplateLookupContext {
+
+        private final TemplateLoaderSession session; 
+        
+        DefaultTemplateResolverTemplateLookupContext(String templateName, Locale templateLocale, Object customLookupCondition,
+                TemplateLoadingSource cachedResultSource, Serializable cachedResultVersion,
+                TemplateLoaderSession session) {
+            super(templateName, localizedLookup ? templateLocale : null, customLookupCondition,
+                    cachedResultSource, cachedResultVersion);
+            this.session = session;
+        }
+
+        @Override
+        public TemplateLoaderBasedTemplateLookupResult lookupWithAcquisitionStrategy(String path) throws IOException {
+            // Only one of the possible ways of making a name non-normalized, but is the easiest mistake to do:
+            if (path.startsWith("/")) {
+                throw new IllegalArgumentException("Non-normalized name, starts with \"/\": " + path);
+            }
+            
+            int asterisk = path.indexOf(ASTERISK);
+            // Shortcut in case there is no acquisition
+            if (asterisk == -1) {
+                return createLookupResult(
+                        path,
+                        templateLoader.load(path, getCachedResultSource(), getCachedResultVersion(), session));
+            }
+            StringTokenizer pathTokenizer = new StringTokenizer(path, "/");
+            int lastAsterisk = -1;
+            List<String> pathSteps = new ArrayList<>();
+            while (pathTokenizer.hasMoreTokens()) {
+                String pathStep = pathTokenizer.nextToken();
+                if (pathStep.equals(ASTERISKSTR)) {
+                    if (lastAsterisk != -1) {
+                        pathSteps.remove(lastAsterisk);
+                    }
+                    lastAsterisk = pathSteps.size();
+                }
+                pathSteps.add(pathStep);
+            }
+            if (lastAsterisk == -1) {  // if there was no real "*" step after all
+                return createLookupResult(
+                        path,
+                        templateLoader.load(path, getCachedResultSource(), getCachedResultVersion(), session));
+            }
+            String basePath = concatPath(pathSteps, 0, lastAsterisk);
+            String postAsteriskPath = concatPath(pathSteps, lastAsterisk + 1, pathSteps.size());
+            StringBuilder buf = new StringBuilder(path.length()).append(basePath);
+            int basePathLen = basePath.length();
+            while (true) {
+                String fullPath = buf.append(postAsteriskPath).toString();
+                TemplateLoadingResult templateLoaderResult = templateLoader.load(
+                        fullPath, getCachedResultSource(), getCachedResultVersion(), session);
+                if (templateLoaderResult.getStatus() == TemplateLoadingResultStatus.OPENED) {
+                    return createLookupResult(fullPath, templateLoaderResult);
+                }
+                if (basePathLen == 0) {
+                    return createNegativeLookupResult();
+                }
+                basePathLen = basePath.lastIndexOf(SLASH, basePathLen - 2) + 1;
+                buf.setLength(basePathLen);
+            }
+        }
+
+        @Override
+        public TemplateLoaderBasedTemplateLookupResult lookupWithLocalizedThenAcquisitionStrategy(final String templateName,
+                final Locale templateLocale) throws IOException {
+            
+                if (templateLocale == null) {
+                    return lookupWithAcquisitionStrategy(templateName);
+                }
+                
+                int lastDot = templateName.lastIndexOf('.');
+                String prefix = lastDot == -1 ? templateName : templateName.substring(0, lastDot);
+                String suffix = lastDot == -1 ? "" : templateName.substring(lastDot);
+                String localeName = LOCALE_PART_SEPARATOR + templateLocale.toString();
+                StringBuilder buf = new StringBuilder(templateName.length() + localeName.length());
+                buf.append(prefix);
+                tryLocaleNameVariations: while (true) {
+                    buf.setLength(prefix.length());
+                    String path = buf.append(localeName).append(suffix).toString();
+                    TemplateLoaderBasedTemplateLookupResult lookupResult = lookupWithAcquisitionStrategy(path);
+                    if (lookupResult.isPositive()) {
+                        return lookupResult;
+                    }
+                    
+                    int lastUnderscore = localeName.lastIndexOf('_');
+                    if (lastUnderscore == -1) {
+                        break tryLocaleNameVariations;
+                    }
+                    localeName = localeName.substring(0, lastUnderscore);
+                }
+                return createNegativeLookupResult();
+        }
+        
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/FileTemplateLoader.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/FileTemplateLoader.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/FileTemplateLoader.java
new file mode 100644
index 0000000..e2437c1
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/FileTemplateLoader.java
@@ -0,0 +1,383 @@
+/*
+ * 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.templateresolver.impl;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Serializable;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.Objects;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core._CoreLogs;
+import org.apache.freemarker.core.templateresolver.TemplateLoader;
+import org.apache.freemarker.core.templateresolver.TemplateLoaderSession;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingResult;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingSource;
+import org.apache.freemarker.core.util._SecurityUtil;
+import org.apache.freemarker.core.util._StringUtil;
+import org.slf4j.Logger;
+
+/**
+ * A {@link TemplateLoader} that uses files inside a specified directory as the source of templates. By default it does
+ * security checks on the <em>canonical</em> path that will prevent it serving templates outside that specified
+ * directory. If you want symbolic links that point outside the template directory to work, you need to disable this
+ * feature by using {@link #FileTemplateLoader(File, boolean)} with {@code true} second argument, but before that,
+ * check the security implications there!
+ */
+public class FileTemplateLoader implements TemplateLoader {
+    
+    /**
+     * By setting this Java system property to {@code true}, you can change the default of
+     * {@code #getEmulateCaseSensitiveFileSystem()}.
+     */
+    public static String SYSTEM_PROPERTY_NAME_EMULATE_CASE_SENSITIVE_FILE_SYSTEM
+            = "org.freemarker.emulateCaseSensitiveFileSystem";
+    private static final boolean EMULATE_CASE_SENSITIVE_FILE_SYSTEM_DEFAULT;
+    static {
+        final String s = _SecurityUtil.getSystemProperty(SYSTEM_PROPERTY_NAME_EMULATE_CASE_SENSITIVE_FILE_SYSTEM,
+                "false");
+        boolean emuCaseSensFS;
+        try {
+            emuCaseSensFS = _StringUtil.getYesNo(s);
+        } catch (Exception e) {
+            emuCaseSensFS = false;
+        }
+        EMULATE_CASE_SENSITIVE_FILE_SYSTEM_DEFAULT = emuCaseSensFS;
+    }
+
+    private static final int CASE_CHECH_CACHE_HARD_SIZE = 50;
+    private static final int CASE_CHECK_CACHE__SOFT_SIZE = 1000;
+    private static final boolean SEP_IS_SLASH = File.separatorChar == '/';
+    
+    private static final Logger LOG = _CoreLogs.TEMPLATE_RESOLVER;
+    
+    public final File baseDir;
+    private final String canonicalBasePath;
+    private boolean emulateCaseSensitiveFileSystem;
+    private MruCacheStorage correctCasePaths;
+
+    /**
+     * Creates a new file template loader that will use the specified directory
+     * as the base directory for loading templates. It will not allow access to
+     * template files that are accessible through symlinks that point outside 
+     * the base directory.
+     * @param baseDir the base directory for loading templates
+     */
+    public FileTemplateLoader(final File baseDir)
+    throws IOException {
+        this(baseDir, false);
+    }
+
+    /**
+     * Creates a new file template loader that will use the specified directory as the base directory for loading
+     * templates. See the parameters for allowing symlinks that point outside the base directory.
+     * 
+     * @param baseDir
+     *            the base directory for loading templates
+     * 
+     * @param disableCanonicalPathCheck
+     *            If {@code true}, it will not check if the file to be loaded is inside the {@code baseDir} or not,
+     *            according the <em>canonical</em> paths of the {@code baseDir} and the file to load. Note that
+     *            {@link Configuration#getTemplate(String)} and (its overloads) already prevents backing out from the
+     *            template directory with paths like {@code /../../../etc/password}, however, that can be circumvented
+     *            with symbolic links or other file system features. If you really want to use symbolic links that point
+     *            outside the {@code baseDir}, set this parameter to {@code true}, but then be very careful with
+     *            template paths that are supplied by the visitor or an external system.
+     */
+    public FileTemplateLoader(final File baseDir, final boolean disableCanonicalPathCheck)
+    throws IOException {
+        try {
+            Object[] retval = AccessController.doPrivileged(new PrivilegedExceptionAction<Object[]>() {
+                @Override
+                public Object[] run() throws IOException {
+                    if (!baseDir.exists()) {
+                        throw new FileNotFoundException(baseDir + " does not exist.");
+                    }
+                    if (!baseDir.isDirectory()) {
+                        throw new IOException(baseDir + " is not a directory.");
+                    }
+                    Object[] retval = new Object[2];
+                    if (disableCanonicalPathCheck) {
+                        retval[0] = baseDir;
+                        retval[1] = null;
+                    } else {
+                        retval[0] = baseDir.getCanonicalFile();
+                        String basePath = ((File) retval[0]).getPath();
+                        // Most canonical paths don't end with File.separator,
+                        // but some does. Like, "C:\" VS "C:\templates".
+                        if (!basePath.endsWith(File.separator)) {
+                            basePath += File.separatorChar;
+                        }
+                        retval[1] = basePath;
+                    }
+                    return retval;
+                }
+            });
+            this.baseDir = (File) retval[0];
+            canonicalBasePath = (String) retval[1];
+            
+            setEmulateCaseSensitiveFileSystem(getEmulateCaseSensitiveFileSystemDefault());
+        } catch (PrivilegedActionException e) {
+            throw (IOException) e.getException();
+        }
+    }
+    
+    private File getFile(final String name) throws IOException {
+        try {
+            return AccessController.doPrivileged(new PrivilegedExceptionAction<File>() {
+                @Override
+                public File run() throws IOException {
+                    File source = new File(baseDir, SEP_IS_SLASH ? name : 
+                        name.replace('/', File.separatorChar));
+                    if (!source.isFile()) {
+                        return null;
+                    }
+                    // Security check for inadvertently returning something 
+                    // outside the template directory when linking is not 
+                    // allowed.
+                    if (canonicalBasePath != null) {
+                        String normalized = source.getCanonicalPath();
+                        if (!normalized.startsWith(canonicalBasePath)) {
+                            throw new SecurityException(source.getAbsolutePath() 
+                                    + " resolves to " + normalized + " which " + 
+                                    " doesn't start with " + canonicalBasePath);
+                        }
+                    }
+                    
+                    if (emulateCaseSensitiveFileSystem && !isNameCaseCorrect(source)) {
+                        return null;
+                    }
+                    
+                    return source;
+                }
+            });
+        } catch (PrivilegedActionException e) {
+            throw (IOException) e.getException();
+        }
+    }
+    
+    private long getLastModified(final File templateSource) {
+        return (AccessController.<Long>doPrivileged(new PrivilegedAction<Long>() {
+            @Override
+            public Long run() {
+                return Long.valueOf((templateSource).lastModified());
+            }
+        })).longValue();
+    }
+    
+    private InputStream getInputStream(final File templateSource)
+    throws IOException {
+        try {
+            return AccessController.doPrivileged(new PrivilegedExceptionAction<InputStream>() {
+                @Override
+                public InputStream run() throws IOException {
+                    return new FileInputStream(templateSource);
+                }
+            });
+        } catch (PrivilegedActionException e) {
+            throw (IOException) e.getException();
+        }
+    }
+    
+    /**
+     * Called by {@link #getFile(String)} when {@link #getEmulateCaseSensitiveFileSystem()} is {@code true}.
+     */
+    private boolean isNameCaseCorrect(File source) throws IOException {
+        final String sourcePath = source.getPath();
+        synchronized (correctCasePaths) {
+            if (correctCasePaths.get(sourcePath) != null) {
+                return true;
+            }
+        }
+        
+        final File parentDir = source.getParentFile();
+        if (parentDir != null) {
+            if (!baseDir.equals(parentDir) && !isNameCaseCorrect(parentDir)) {
+                return false;
+            }
+            
+            final String[] listing = parentDir.list();
+            if (listing != null) {
+                final String fileName = source.getName();
+                
+                boolean identicalNameFound = false;
+                for (int i = 0; !identicalNameFound && i < listing.length; i++) {
+                    if (fileName.equals(listing[i])) {
+                        identicalNameFound = true;
+                    }
+                }
+        
+                if (!identicalNameFound) {
+                    // If we find a similarly named file that only differs in case, then this is a file-not-found.
+                    for (final String listingEntry : listing) {
+                        if (fileName.equalsIgnoreCase(listingEntry)) {
+                            if (LOG.isDebugEnabled()) {
+                                LOG.debug("Emulating file-not-found because of letter case differences to the "
+                                        + "real file, for: {}", sourcePath);
+                            }
+                            return false;
+                        }
+                    }
+                }
+            }
+        }
+
+        synchronized (correctCasePaths) {
+            correctCasePaths.put(sourcePath, Boolean.TRUE);        
+        }
+        return true;
+    }
+    
+    /**
+     * Returns the base directory in which the templates are searched. This comes from the constructor argument, but
+     * it's possibly a canonicalized version of that. 
+     *  
+     * @since 2.3.21
+     */
+    public File getBaseDirectory() {
+        return baseDir;
+    }
+    
+    /**
+     * Intended for development only, checks if the template name matches the case (upper VS lower case letters) of the
+     * actual file name, and if it doesn't, it emulates a file-not-found even if the file system is case insensitive.
+     * This is useful when developing application on Windows, which will be later installed on Linux, OS X, etc. This
+     * check can be resource intensive, as to check the file name the directories involved, up to the
+     * {@link #getBaseDirectory()} directory, must be listed. Positive results (matching case) will be cached without
+     * expiration time.
+     * 
+     * <p>The default in {@link FileTemplateLoader} is {@code false}, but subclasses may change they by overriding
+     * {@link #getEmulateCaseSensitiveFileSystemDefault()}.
+     * 
+     * @since 2.3.23
+     */
+    public void setEmulateCaseSensitiveFileSystem(boolean emulateCaseSensitiveFileSystem) {
+        // Ensure that the cache exists exactly when needed:
+        if (emulateCaseSensitiveFileSystem) {
+            if (correctCasePaths == null) {
+                correctCasePaths = new MruCacheStorage(CASE_CHECH_CACHE_HARD_SIZE, CASE_CHECK_CACHE__SOFT_SIZE);
+            }
+        } else {
+            correctCasePaths = null;
+        }
+        
+        this.emulateCaseSensitiveFileSystem = emulateCaseSensitiveFileSystem;
+    }
+
+    /**
+     * Getter pair of {@link #setEmulateCaseSensitiveFileSystem(boolean)}.
+     * 
+     * @since 2.3.23
+     */
+    public boolean getEmulateCaseSensitiveFileSystem() {
+        return emulateCaseSensitiveFileSystem;
+    }
+
+    /**
+     * Returns the default of {@link #getEmulateCaseSensitiveFileSystem()}. In {@link FileTemplateLoader} it's
+     * {@code false}, unless the {@link #SYSTEM_PROPERTY_NAME_EMULATE_CASE_SENSITIVE_FILE_SYSTEM} system property was
+     * set to {@code true}, but this can be overridden here in custom subclasses. For example, if your environment
+     * defines something like developer mode, you may want to override this to return {@code true} on Windows.
+     * 
+     * @since 2.3.23
+     */
+    protected boolean getEmulateCaseSensitiveFileSystemDefault() {
+        return EMULATE_CASE_SENSITIVE_FILE_SYSTEM_DEFAULT;
+    }
+
+    /**
+     * Show class name and some details that are useful in template-not-found errors.
+     * 
+     * @since 2.3.21
+     */
+    @Override
+    public String toString() {
+        // We don't _StringUtil.jQuote paths here, because on Windows there will be \\-s then that some may find
+        // confusing.
+        return _TemplateLoaderUtils.getClassNameForToString(this) + "("
+                + "baseDir=\"" + baseDir + "\""
+                + (canonicalBasePath != null ? ", canonicalBasePath=\"" + canonicalBasePath + "\"" : "")
+                + (emulateCaseSensitiveFileSystem ? ", emulateCaseSensitiveFileSystem=true" : "")
+                + ")";
+    }
+
+    @Override
+    public TemplateLoaderSession createSession() {
+        return null;
+    }
+
+    @Override
+    public TemplateLoadingResult load(String name, TemplateLoadingSource ifSourceDiffersFrom,
+            Serializable ifVersionDiffersFrom, TemplateLoaderSession session) throws IOException {
+        File file = getFile(name);
+        if (file == null) {
+            return TemplateLoadingResult.NOT_FOUND;
+        }
+        
+        FileTemplateLoadingSource source = new FileTemplateLoadingSource(file);
+        
+        long lmd = getLastModified(file);
+        Long version = lmd != -1 ? lmd : null;
+        
+        if (ifSourceDiffersFrom != null && ifSourceDiffersFrom.equals(source) 
+                && Objects.equals(ifVersionDiffersFrom, version)) {
+            return TemplateLoadingResult.NOT_MODIFIED;
+        }
+        
+        return new TemplateLoadingResult(source, version, getInputStream(file), null);
+    }
+
+    @Override
+    public void resetState() {
+        // Does nothing
+    }
+    
+    @SuppressWarnings("serial")
+    private static class FileTemplateLoadingSource implements TemplateLoadingSource {
+        
+        private final File file;
+
+        FileTemplateLoadingSource(File file) {
+            this.file = file;
+        }
+
+        @Override
+        public int hashCode() {
+            return file.hashCode();
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) return true;
+            if (obj == null) return false;
+            if (getClass() != obj.getClass()) return false;
+            return file.equals(((FileTemplateLoadingSource) obj).file);
+        }
+        
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/MruCacheStorage.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/MruCacheStorage.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/MruCacheStorage.java
new file mode 100644
index 0000000..9f004fe
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/MruCacheStorage.java
@@ -0,0 +1,330 @@
+/*
+ * 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.templateresolver.impl;
+
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.SoftReference;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.templateresolver.CacheStorageWithGetSize;
+
+/**
+ * A cache storage that implements a two-level Most Recently Used cache. In the
+ * first level, items are strongly referenced up to the specified maximum. When
+ * the maximum is exceeded, the least recently used item is moved into the  
+ * second level cache, where they are softly referenced, up to another 
+ * specified maximum. When the second level maximum is also exceeded, the least 
+ * recently used item is discarded altogether. This cache storage is a 
+ * generalization of both {@link StrongCacheStorage} and 
+ * {@link SoftCacheStorage} - the effect of both of them can be achieved by 
+ * setting one maximum to zero and the other to the largest positive integer. 
+ * On the other hand, if you wish to use this storage in a strong-only mode, or
+ * in a soft-only mode, you might consider using {@link StrongCacheStorage} or
+ * {@link SoftCacheStorage} instead, as they can be used by 
+ * {@link DefaultTemplateResolver} concurrently without any synchronization on a 5.0 or 
+ * later JRE.
+ *  
+ * <p>This class is <em>NOT</em> thread-safe. If it's accessed from multiple
+ * threads concurrently, proper synchronization must be provided by the callers.
+ * Note that {@link DefaultTemplateResolver}, the natural user of this class provides the
+ * necessary synchronizations when it uses the class.
+ * Also you might consider whether you need this sort of a mixed storage at all
+ * in your solution, as in most cases SoftCacheStorage can also be sufficient. 
+ * SoftCacheStorage will use Java soft references, and they already use access 
+ * timestamps internally to bias the garbage collector against clearing 
+ * recently used references, so you can get reasonably good (and 
+ * memory-sensitive) most-recently-used caching through 
+ * {@link SoftCacheStorage} as well.
+ *
+ * @see Configuration#getCacheStorage()
+ */
+public class MruCacheStorage implements CacheStorageWithGetSize {
+    private final MruEntry strongHead = new MruEntry();
+    private final MruEntry softHead = new MruEntry();
+    {
+        softHead.linkAfter(strongHead);
+    }
+    private final Map map = new HashMap();
+    private final ReferenceQueue refQueue = new ReferenceQueue();
+    private final int strongSizeLimit;
+    private final int softSizeLimit;
+    private int strongSize = 0;
+    private int softSize = 0;
+    
+    /**
+     * Creates a new MRU cache storage with specified maximum cache sizes. Each
+     * cache size can vary between 0 and {@link Integer#MAX_VALUE}.
+     * @param strongSizeLimit the maximum number of strongly referenced templates; when exceeded, the entry used
+     *          the least recently will be moved into the soft cache.
+     * @param softSizeLimit the maximum number of softly referenced templates; when exceeded, the entry used
+     *          the least recently will be discarded.
+     */
+    public MruCacheStorage(int strongSizeLimit, int softSizeLimit) {
+        if (strongSizeLimit < 0) throw new IllegalArgumentException("strongSizeLimit < 0");
+        if (softSizeLimit < 0) throw new IllegalArgumentException("softSizeLimit < 0");
+        this.strongSizeLimit = strongSizeLimit;
+        this.softSizeLimit = softSizeLimit;
+    }
+    
+    @Override
+    public Object get(Object key) {
+        removeClearedReferences();
+        MruEntry entry = (MruEntry) map.get(key);
+        if (entry == null) {
+            return null;
+        }
+        relinkEntryAfterStrongHead(entry, null);
+        Object value = entry.getValue();
+        if (value instanceof MruReference) {
+            // This can only happen with strongSizeLimit == 0
+            return ((MruReference) value).get();
+        }
+        return value;
+    }
+
+    @Override
+    public void put(Object key, Object value) {
+        removeClearedReferences();
+        MruEntry entry = (MruEntry) map.get(key);
+        if (entry == null) {
+            entry = new MruEntry(key, value);
+            map.put(key, entry);
+            linkAfterStrongHead(entry);
+        } else {
+            relinkEntryAfterStrongHead(entry, value);
+        }
+        
+    }
+
+    @Override
+    public void remove(Object key) {
+        removeClearedReferences();
+        removeInternal(key);
+    }
+
+    private void removeInternal(Object key) {
+        MruEntry entry = (MruEntry) map.remove(key);
+        if (entry != null) {
+            unlinkEntryAndInspectIfSoft(entry);
+        }
+    }
+
+    @Override
+    public void clear() {
+        strongHead.makeHead();
+        softHead.linkAfter(strongHead);
+        map.clear();
+        strongSize = softSize = 0;
+        // Quick refQueue processing
+        while (refQueue.poll() != null);
+    }
+
+    private void relinkEntryAfterStrongHead(MruEntry entry, Object newValue) {
+        if (unlinkEntryAndInspectIfSoft(entry) && newValue == null) {
+            // Turn soft reference into strong reference, unless is was cleared
+            MruReference mref = (MruReference) entry.getValue();
+            Object strongValue = mref.get();
+            if (strongValue != null) {
+                entry.setValue(strongValue);
+                linkAfterStrongHead(entry);
+            } else {
+                map.remove(mref.getKey());
+            }
+        } else {
+            if (newValue != null) {
+                entry.setValue(newValue);
+            }
+            linkAfterStrongHead(entry);
+        }
+    }
+
+    private void linkAfterStrongHead(MruEntry entry) {
+        entry.linkAfter(strongHead);
+        if (strongSize == strongSizeLimit) {
+            // softHead.previous is LRU strong entry
+            MruEntry lruStrong = softHead.getPrevious();
+            // Attila: This is equaivalent to strongSizeLimit != 0
+            // DD: But entry.linkAfter(strongHead) was just executed above, so
+            //     lruStrong != strongHead is true even if strongSizeLimit == 0.
+            if (lruStrong != strongHead) {
+                lruStrong.unlink();
+                if (softSizeLimit > 0) {
+                    lruStrong.linkAfter(softHead);
+                    lruStrong.setValue(new MruReference(lruStrong, refQueue));
+                    if (softSize == softSizeLimit) {
+                        // List is circular, so strongHead.previous is LRU soft entry
+                        MruEntry lruSoft = strongHead.getPrevious();
+                        lruSoft.unlink();
+                        map.remove(lruSoft.getKey());
+                    } else {
+                        ++softSize;
+                    }
+                } else {
+                    map.remove(lruStrong.getKey());
+                }
+            }
+        } else {
+            ++strongSize;
+        }
+    }
+
+    private boolean unlinkEntryAndInspectIfSoft(MruEntry entry) {
+        entry.unlink();
+        if (entry.getValue() instanceof MruReference) {
+            --softSize;
+            return true;
+        } else {
+            --strongSize;
+            return false;
+        }
+    }
+    
+    private void removeClearedReferences() {
+        for (; ; ) {
+            MruReference ref = (MruReference) refQueue.poll();
+            if (ref == null) {
+                break;
+            }
+            removeInternal(ref.getKey());
+        }
+    }
+    
+    /**
+     * Returns the configured upper limit of the number of strong cache entries.
+     *  
+     * @since 2.3.21
+     */
+    public int getStrongSizeLimit() {
+        return strongSizeLimit;
+    }
+
+    /**
+     * Returns the configured upper limit of the number of soft cache entries.
+     * 
+     * @since 2.3.21
+     */
+    public int getSoftSizeLimit() {
+        return softSizeLimit;
+    }
+
+    /**
+     * Returns the <em>current</em> number of strong cache entries.
+     *  
+     * @see #getStrongSizeLimit()
+     * @since 2.3.21
+     */
+    public int getStrongSize() {
+        return strongSize;
+    }
+
+    /**
+     * Returns a close approximation of the <em>current</em> number of soft cache entries.
+     * 
+     * @see #getSoftSizeLimit()
+     * @since 2.3.21
+     */
+    public int getSoftSize() {
+        removeClearedReferences();
+        return softSize;
+    }
+    
+    /**
+     * Returns a close approximation of the current number of cache entries.
+     * 
+     * @see #getStrongSize()
+     * @see #getSoftSize()
+     * @since 2.3.21
+     */
+    @Override
+    public int getSize() {
+        return getSoftSize() + getStrongSize();
+    }
+
+    private static final class MruEntry {
+        private MruEntry prev;
+        private MruEntry next;
+        private final Object key;
+        private Object value;
+        
+        /**
+         * Used solely to construct the head element
+         */
+        MruEntry() {
+            makeHead();
+            key = value = null;
+        }
+        
+        MruEntry(Object key, Object value) {
+            this.key = key;
+            this.value = value;
+        }
+        
+        Object getKey() {
+            return key;
+        }
+        
+        Object getValue() {
+            return value;
+        }
+        
+        void setValue(Object value) {
+            this.value = value;
+        }
+
+        MruEntry getPrevious() {
+            return prev;
+        }
+        
+        void linkAfter(MruEntry entry) {
+            next = entry.next;
+            entry.next = this;
+            prev = entry;
+            next.prev = this;
+        }
+        
+        void unlink() {
+            next.prev = prev;
+            prev.next = next;
+            prev = null;
+            next = null;
+        }
+        
+        void makeHead() {
+            prev = next = this;
+        }
+    }
+    
+    private static class MruReference extends SoftReference {
+        private final Object key;
+        
+        MruReference(MruEntry entry, ReferenceQueue queue) {
+            super(entry.getValue(), queue);
+            key = entry.getKey();
+        }
+        
+        Object getKey() {
+            return key;
+        }
+    }
+    
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/MultiTemplateLoader.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/MultiTemplateLoader.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/MultiTemplateLoader.java
new file mode 100644
index 0000000..883ec62
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/MultiTemplateLoader.java
@@ -0,0 +1,172 @@
+/*
+ * 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.templateresolver.impl;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.freemarker.core.templateresolver.TemplateLoader;
+import org.apache.freemarker.core.templateresolver.TemplateLoaderSession;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingResult;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingResultStatus;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingSource;
+import org.apache.freemarker.core.util._NullArgumentException;
+
+/**
+ * A {@link TemplateLoader} that uses a set of other loaders to load the templates. On every request, loaders are
+ * queried in the order of their appearance in the array of loaders provided to the constructor. Except, when the
+ * {@linkplain #setSticky(boolean)} sticky} setting is set to {@code true} (default is false {@code false}), if
+ * a request for some template name was already satisfied in the past by one of the loaders, that loader is queried
+ * first (stickiness).
+ * 
+ * <p>This class is thread-safe.
+ */
+// TODO JUnit test
+public class MultiTemplateLoader implements TemplateLoader {
+
+    private final TemplateLoader[] templateLoaders;
+    private final Map<String, TemplateLoader> lastTemplateLoaderForName = new ConcurrentHashMap<>();
+    
+    private boolean sticky = false;
+
+    /**
+     * Creates a new instance that will use the specified template loaders.
+     * 
+     * @param templateLoaders
+     *            the template loaders that are used to load templates, in the order as they will be searched
+     *            (except where {@linkplain #setSticky(boolean) stickiness} says otherwise).
+     */
+    public MultiTemplateLoader(TemplateLoader... templateLoaders) {
+        _NullArgumentException.check("templateLoaders", templateLoaders);
+        this.templateLoaders = templateLoaders.clone();
+    }
+
+    /**
+     * Clears the sickiness memory, also resets the state of all enclosed {@link TemplateLoader}-s.
+     */
+    @Override
+    public void resetState() {
+        lastTemplateLoaderForName.clear();
+        for (TemplateLoader templateLoader : templateLoaders) {
+            templateLoader.resetState();
+        }
+    }
+
+    /**
+     * Show class name and some details that are useful in template-not-found errors.
+     * 
+     * @since 2.3.21
+     */
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("MultiTemplateLoader(");
+        for (int i = 0; i < templateLoaders.length; i++) {
+            if (i != 0) {
+                sb.append(", ");
+            }
+            sb.append("loader").append(i + 1).append(" = ").append(templateLoaders[i]);
+        }
+        sb.append(")");
+        return sb.toString();
+    }
+
+    /**
+     * Returns the number of {@link TemplateLoader}-s directly inside this {@link TemplateLoader}.
+     * 
+     * @since 2.3.23
+     */
+    public int getTemplateLoaderCount() {
+        return templateLoaders.length;
+    }
+
+    /**
+     * Returns the {@link TemplateLoader} at the given index.
+     * 
+     * @param index
+     *            Must be below {@link #getTemplateLoaderCount()}.
+     */
+    public TemplateLoader getTemplateLoader(int index) {
+        return templateLoaders[index];
+    }
+
+    /**
+     * Getter pair of {@link #setSticky(boolean)}.
+     */
+    public boolean isSticky() {
+        return sticky;
+    }
+
+    /**
+     * Sets if for a name that was already loaded earlier the same {@link TemplateLoader} will be tried first, or
+     * we always try the {@link TemplateLoader}-s strictly in the order as it was specified in the constructor.
+     * The default is {@code false}.
+     */
+    public void setSticky(boolean sticky) {
+        this.sticky = sticky;
+    }
+
+    @Override
+    public TemplateLoaderSession createSession() {
+        return null;
+    }
+
+    @Override
+    public TemplateLoadingResult load(String name, TemplateLoadingSource ifSourceDiffersFrom,
+            Serializable ifVersionDiffersFrom, TemplateLoaderSession session) throws IOException {
+        TemplateLoader lastLoader = null;
+        if (sticky) {
+            // Use soft affinity - give the loader that last found this
+            // resource a chance to find it again first.
+            lastLoader = lastTemplateLoaderForName.get(name);
+            if (lastLoader != null) {
+                TemplateLoadingResult result = lastLoader.load(name, ifSourceDiffersFrom, ifVersionDiffersFrom, session);
+                if (result.getStatus() != TemplateLoadingResultStatus.NOT_FOUND) {
+                    return result;
+                }
+            }
+        }
+
+        // If there is no affine loader, or it could not find the resource
+        // again, try all loaders in order of appearance. If any manages
+        // to find the resource, then associate it as the new affine loader
+        // for this resource.
+        for (TemplateLoader templateLoader : templateLoaders) {
+            if (lastLoader != templateLoader) {
+                TemplateLoadingResult result = templateLoader.load(
+                        name, ifSourceDiffersFrom, ifVersionDiffersFrom, session);
+                if (result.getStatus() != TemplateLoadingResultStatus.NOT_FOUND) {
+                    if (sticky) {
+                        lastTemplateLoaderForName.put(name, templateLoader);
+                    }
+                    return result;
+                }
+            }
+        }
+
+        if (sticky) {
+            lastTemplateLoaderForName.remove(name);
+        }
+        return TemplateLoadingResult.NOT_FOUND;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/NullCacheStorage.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/NullCacheStorage.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/NullCacheStorage.java
new file mode 100644
index 0000000..c8ff55c
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/NullCacheStorage.java
@@ -0,0 +1,71 @@
+/*
+ * 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.templateresolver.impl;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.templateresolver.CacheStorage;
+import org.apache.freemarker.core.templateresolver.CacheStorageWithGetSize;
+
+/**
+ * A cache storage that doesn't store anything. Use this if you
+ * don't want caching.
+ *
+ * @see Configuration#getCacheStorage()
+ * 
+ * @since 2.3.17
+ */
+public class NullCacheStorage implements CacheStorage, CacheStorageWithGetSize {
+    
+    /**
+     * @since 2.3.22
+     */
+    public static final NullCacheStorage INSTANCE = new NullCacheStorage();
+    
+    @Override
+    public Object get(Object key) {
+        return null;
+    }
+
+    @Override
+    public void put(Object key, Object value) {
+        // do nothing
+    }
+
+    @Override
+    public void remove(Object key) {
+        // do nothing
+    }
+    
+    @Override
+    public void clear() {
+        // do nothing
+    }
+
+    /**
+     * Always returns 0.
+     * 
+     * @since 2.3.21
+     */
+    @Override
+    public int getSize() {
+        return 0;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/SoftCacheStorage.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/SoftCacheStorage.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/SoftCacheStorage.java
new file mode 100644
index 0000000..3e22c33
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/SoftCacheStorage.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.templateresolver.impl;
+
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.SoftReference;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.apache.freemarker.core.Configuration.ExtendableBuilder;
+import org.apache.freemarker.core.templateresolver.CacheStorage;
+import org.apache.freemarker.core.templateresolver.CacheStorageWithGetSize;
+
+/**
+ * Soft cache storage is a cache storage that uses {@link SoftReference} objects to hold the objects it was passed,
+ * therefore allows the garbage collector to purge the cache when it determines that it wants to free up memory. This
+ * class is thread-safe to the extent that its underlying map is. The parameterless constructor uses a thread-safe map
+ * since 2.3.24 or Java 5.
+ *
+ * @see ExtendableBuilder#setCacheStorage(CacheStorage)
+ */
+public class SoftCacheStorage implements CacheStorage, CacheStorageWithGetSize {
+    
+    private final ReferenceQueue queue = new ReferenceQueue();
+    private final ConcurrentMap map;
+    
+    /**
+     * Creates an instance that uses a {@link ConcurrentMap} internally.
+     */
+    public SoftCacheStorage() {
+        map = new ConcurrentHashMap();
+    }
+    
+    @Override
+    public Object get(Object key) {
+        processQueue();
+        Reference ref = (Reference) map.get(key);
+        return ref == null ? null : ref.get();
+    }
+
+    @Override
+    public void put(Object key, Object value) {
+        processQueue();
+        map.put(key, new SoftValueReference(key, value, queue));
+    }
+
+    @Override
+    public void remove(Object key) {
+        processQueue();
+        map.remove(key);
+    }
+
+    @Override
+    public void clear() {
+        map.clear();
+        processQueue();
+    }
+    
+    /**
+     * Returns a close approximation of the number of cache entries.
+     * 
+     * @since 2.3.21
+     */
+    @Override
+    public int getSize() {
+        processQueue();
+        return map.size();
+    }
+
+    private void processQueue() {
+        for (; ; ) {
+            SoftValueReference ref = (SoftValueReference) queue.poll();
+            if (ref == null) {
+                return;
+            }
+            Object key = ref.getKey();
+            map.remove(key, ref);
+        }
+    }
+
+    private static final class SoftValueReference extends SoftReference {
+        private final Object key;
+
+        SoftValueReference(Object key, Object value, ReferenceQueue queue) {
+            super(value, queue);
+            this.key = key;
+        }
+
+        Object getKey() {
+            return key;
+        }
+    }
+    
+}
\ No newline at end of file


[08/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/dom/NodeModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/dom/NodeModel.java b/freemarker-core/src/main/java/org/apache/freemarker/dom/NodeModel.java
new file mode 100644
index 0000000..37a5c7d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/dom/NodeModel.java
@@ -0,0 +1,613 @@
+/*
+ * 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.dom;
+
+
+import java.lang.ref.WeakReference;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core._UnexpectedTypeErrorExplainerTemplateModel;
+import org.apache.freemarker.core.model.AdapterTemplateModel;
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNodeModel;
+import org.apache.freemarker.core.model.TemplateNodeModelEx;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.WrapperTemplateModel;
+import org.apache.freemarker.core.model.impl.DefaultObjectWrapper;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.slf4j.Logger;
+import org.w3c.dom.Attr;
+import org.w3c.dom.CDATASection;
+import org.w3c.dom.CharacterData;
+import org.w3c.dom.Document;
+import org.w3c.dom.DocumentType;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.w3c.dom.ProcessingInstruction;
+import org.w3c.dom.Text;
+
+/**
+ * A base class for wrapping a single W3C DOM_WRAPPER Node as a FreeMarker template model.
+ * <p>
+ * Note that {@link DefaultObjectWrapper} automatically wraps W3C DOM_WRAPPER {@link Node}-s into this, so you may need do that
+ * with this class manually. However, before dropping the {@link Node}-s into the data-model, you certainly want to
+ * apply {@link NodeModel#simplify(Node)} on them.
+ * <p>
+ * This class is not guaranteed to be thread safe, so instances of this shouldn't be used as
+ * {@linkplain Configuration#getSharedVariables() shared variable}.
+ * <p>
+ * To represent a node sequence (such as a query result) of exactly 1 nodes, this class should be used instead of
+ * {@link NodeListModel}, as it adds extra capabilities by utilizing that we have exactly 1 node. If you need to wrap a
+ * node sequence of 0 or multiple nodes, you must use {@link NodeListModel}.
+ */
+abstract public class NodeModel implements TemplateNodeModelEx, TemplateHashModel, TemplateSequenceModel,
+    AdapterTemplateModel, WrapperTemplateModel, _UnexpectedTypeErrorExplainerTemplateModel {
+
+    static private final Logger LOG = DomLog.LOG;
+
+    private static final Object STATIC_LOCK = new Object();
+    
+    static private final Map xpathSupportMap = Collections.synchronizedMap(new WeakHashMap());
+    
+    static private XPathSupport jaxenXPathSupport;
+    
+    static Class xpathSupportClass;
+    
+    static {
+        try {
+            useDefaultXPathSupport();
+        } catch (Exception e) {
+            // do nothing
+        }
+        if (xpathSupportClass == null && LOG.isWarnEnabled()) {
+            LOG.warn("No XPath support is available.");
+        }
+    }
+    
+    /**
+     * The W3C DOM_WRAPPER Node being wrapped.
+     */
+    final Node node;
+    private TemplateSequenceModel children;
+    private NodeModel parent;
+    
+    protected NodeModel(Node node) {
+        this.node = node;
+    }
+    
+    /**
+     * @return the underling W3C DOM_WRAPPER Node object that this TemplateNodeModel
+     * is wrapping.
+     */
+    public Node getNode() {
+        return node;
+    }
+    
+    @Override
+    public TemplateModel get(String key) throws TemplateModelException {
+        if (key.startsWith("@@")) {
+            if (key.equals(AtAtKey.TEXT.getKey())) {
+                return new SimpleScalar(getText(node));
+            } else if (key.equals(AtAtKey.NAMESPACE.getKey())) {
+                String nsURI = node.getNamespaceURI();
+                return nsURI == null ? null : new SimpleScalar(nsURI);
+            } else if (key.equals(AtAtKey.LOCAL_NAME.getKey())) {
+                String localName = node.getLocalName();
+                if (localName == null) {
+                    localName = getNodeName();
+                }
+                return new SimpleScalar(localName);
+            } else if (key.equals(AtAtKey.MARKUP.getKey())) {
+                StringBuilder buf = new StringBuilder();
+                NodeOutputter nu = new NodeOutputter(node);
+                nu.outputContent(node, buf);
+                return new SimpleScalar(buf.toString());
+            } else if (key.equals(AtAtKey.NESTED_MARKUP.getKey())) {
+                StringBuilder buf = new StringBuilder();
+                NodeOutputter nu = new NodeOutputter(node);
+                nu.outputContent(node.getChildNodes(), buf);
+                return new SimpleScalar(buf.toString());
+            } else if (key.equals(AtAtKey.QNAME.getKey())) {
+                String qname = getQualifiedName();
+                return qname != null ? new SimpleScalar(qname) : null;
+            } else {
+                // As @@... would cause exception in the XPath engine, we throw a nicer exception now. 
+                if (AtAtKey.containsKey(key)) {
+                    throw new TemplateModelException(
+                            "\"" + key + "\" is not supported for an XML node of type \"" + getNodeType() + "\".");
+                } else {
+                    throw new TemplateModelException("Unsupported @@ key: " + key);
+                }
+            }
+        } else {
+            XPathSupport xps = getXPathSupport();
+            if (xps != null) {
+                return xps.executeQuery(node, key);
+            } else {
+                throw new TemplateModelException(
+                        "Can't try to resolve the XML query key, because no XPath support is available. "
+                        + "This is either malformed or an XPath expression: " + key);
+            }
+        }
+    }
+    
+    @Override
+    public TemplateNodeModel getParentNode() {
+        if (parent == null) {
+            Node parentNode = node.getParentNode();
+            if (parentNode == null) {
+                if (node instanceof Attr) {
+                    parentNode = ((Attr) node).getOwnerElement();
+                }
+            }
+            parent = wrap(parentNode);
+        }
+        return parent;
+    }
+
+    @Override
+    public TemplateNodeModelEx getPreviousSibling() throws TemplateModelException {
+        return wrap(node.getPreviousSibling());
+    }
+
+    @Override
+    public TemplateNodeModelEx getNextSibling() throws TemplateModelException {
+        return wrap(node.getNextSibling());
+    }
+
+    @Override
+    public TemplateSequenceModel getChildNodes() {
+        if (children == null) {
+            children = new NodeListModel(node.getChildNodes(), this);
+        }
+        return children;
+    }
+    
+    @Override
+    public final String getNodeType() throws TemplateModelException {
+        short nodeType = node.getNodeType();
+        switch (nodeType) {
+            case Node.ATTRIBUTE_NODE : return "attribute";
+            case Node.CDATA_SECTION_NODE : return "text";
+            case Node.COMMENT_NODE : return "comment";
+            case Node.DOCUMENT_FRAGMENT_NODE : return "document_fragment";
+            case Node.DOCUMENT_NODE : return "document";
+            case Node.DOCUMENT_TYPE_NODE : return "document_type";
+            case Node.ELEMENT_NODE : return "element";
+            case Node.ENTITY_NODE : return "entity";
+            case Node.ENTITY_REFERENCE_NODE : return "entity_reference";
+            case Node.NOTATION_NODE : return "notation";
+            case Node.PROCESSING_INSTRUCTION_NODE : return "pi";
+            case Node.TEXT_NODE : return "text";
+        }
+        throw new TemplateModelException("Unknown node type: " + nodeType + ". This should be impossible!");
+    }
+    
+    public TemplateModel exec(List args) throws TemplateModelException {
+        if (args.size() != 1) {
+            throw new TemplateModelException("Expecting exactly one arguments");
+        }
+        String query = (String) args.get(0);
+        // Now, we try to behave as if this is an XPath expression
+        XPathSupport xps = getXPathSupport();
+        if (xps == null) {
+            throw new TemplateModelException("No XPath support available");
+        }
+        return xps.executeQuery(node, query);
+    }
+    
+    /**
+     * Always returns 1.
+     */
+    @Override
+    public final int size() {
+        return 1;
+    }
+    
+    @Override
+    public final TemplateModel get(int i) {
+        return i == 0 ? this : null;
+    }
+    
+    @Override
+    public String getNodeNamespace() {
+        int nodeType = node.getNodeType();
+        if (nodeType != Node.ATTRIBUTE_NODE && nodeType != Node.ELEMENT_NODE) { 
+            return null;
+        }
+        String result = node.getNamespaceURI();
+        if (result == null && nodeType == Node.ELEMENT_NODE) {
+            result = "";
+        } else if ("".equals(result) && nodeType == Node.ATTRIBUTE_NODE) {
+            result = null;
+        }
+        return result;
+    }
+    
+    @Override
+    public final int hashCode() {
+        return node.hashCode();
+    }
+    
+    @Override
+    public boolean equals(Object other) {
+        if (other == null) return false;
+        return other.getClass() == getClass()
+                && ((NodeModel) other).node.equals(node);
+    }
+    
+    /**
+     * Creates a {@link NodeModel} from a DOM {@link Node}. It's strongly recommended modify the {@link Node} with
+     * {@link #simplify(Node)}, so the DOM will be easier to process in templates.
+     * 
+     * @param node
+     *            The DOM node to wrap. This is typically an {@link Element} or a {@link Document}, but all kind of node
+     *            types are supported. If {@code null}, {@code null} will be returned.
+     */
+    static public NodeModel wrap(Node node) {
+        if (node == null) {
+            return null;
+        }
+        NodeModel result = null;
+        switch (node.getNodeType()) {
+            case Node.DOCUMENT_NODE : result = new DocumentModel((Document) node); break;
+            case Node.ELEMENT_NODE : result = new ElementModel((Element) node); break;
+            case Node.ATTRIBUTE_NODE : result = new AttributeNodeModel((Attr) node); break;
+            case Node.CDATA_SECTION_NODE : 
+            case Node.COMMENT_NODE :
+            case Node.TEXT_NODE : result = new CharacterDataNodeModel((org.w3c.dom.CharacterData) node); break;
+            case Node.PROCESSING_INSTRUCTION_NODE : result = new PINodeModel((ProcessingInstruction) node); break;
+            case Node.DOCUMENT_TYPE_NODE : result = new DocumentTypeModel((DocumentType) node); break;
+            default: throw new IllegalArgumentException(
+                    "Unsupported node type: " + node.getNodeType() + " ("
+                    + node.getClass().getName() + ")");
+        }
+        return result;
+    }
+    
+    /**
+     * Recursively removes all comment nodes from the subtree.
+     *
+     * @see #simplify
+     */
+    static public void removeComments(Node parent) {
+        Node child = parent.getFirstChild();
+        while (child != null) {
+            Node nextSibling = child.getNextSibling();
+            if (child.getNodeType() == Node.COMMENT_NODE) {
+                parent.removeChild(child);
+            } else if (child.hasChildNodes()) {
+                removeComments(child);
+            }
+            child = nextSibling;
+        }
+    }
+    
+    /**
+     * Recursively removes all processing instruction nodes from the subtree.
+     *
+     * @see #simplify
+     */
+    static public void removePIs(Node parent) {
+        Node child = parent.getFirstChild();
+        while (child != null) {
+            Node nextSibling = child.getNextSibling();
+            if (child.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE) {
+                parent.removeChild(child);
+            } else if (child.hasChildNodes()) {
+                removePIs(child);
+            }
+            child = nextSibling;
+        }
+    }
+    
+    /**
+     * Merges adjacent text nodes (where CDATA counts as text node too). Operates recursively on the entire subtree.
+     * The merged node will have the type of the first node of the adjacent merged nodes.
+     * 
+     * <p>Because XPath assumes that there are no adjacent text nodes in the tree, not doing this can have
+     * undesirable side effects. Xalan queries like {@code text()} will only return the first of a list of matching
+     * adjacent text nodes instead of all of them, while Jaxen will return all of them as intuitively expected. 
+     *
+     * @see #simplify
+     */
+    static public void mergeAdjacentText(Node parent) {
+        mergeAdjacentText(parent, new StringBuilder(0));
+    }
+    
+    static private void mergeAdjacentText(Node parent, StringBuilder collectorBuf) {
+        Node child = parent.getFirstChild();
+        while (child != null) {
+            Node next = child.getNextSibling();
+            if (child instanceof Text) {
+                boolean atFirstText = true;
+                while (next instanceof Text) { //
+                    if (atFirstText) {
+                        collectorBuf.setLength(0);
+                        collectorBuf.ensureCapacity(child.getNodeValue().length() + next.getNodeValue().length());
+                        collectorBuf.append(child.getNodeValue());
+                        atFirstText = false;
+                    }
+                    collectorBuf.append(next.getNodeValue());
+                    
+                    parent.removeChild(next);
+                    
+                    next = child.getNextSibling();
+                }
+                if (!atFirstText && collectorBuf.length() != 0) {
+                    ((CharacterData) child).setData(collectorBuf.toString());
+                }
+            } else {
+                mergeAdjacentText(child, collectorBuf);
+            }
+            child = next;
+        }
+    }
+    
+    /**
+     * Removes all comments and processing instruction, and unites adjacent text nodes (here CDATA counts as text as
+     * well). This is similar to applying {@link #removeComments(Node)}, {@link #removePIs(Node)}, and finally
+     * {@link #mergeAdjacentText(Node)}, but it does all that somewhat faster.
+     */    
+    static public void simplify(Node parent) {
+        simplify(parent, new StringBuilder(0));
+    }
+    
+    static private void simplify(Node parent, StringBuilder collectorTextChildBuff) {
+        Node collectorTextChild = null;
+        Node child = parent.getFirstChild();
+        while (child != null) {
+            Node next = child.getNextSibling();
+            if (child.hasChildNodes()) {
+                if (collectorTextChild != null) {
+                    // Commit pending text node merge:
+                    if (collectorTextChildBuff.length() != 0) {
+                        ((CharacterData) collectorTextChild).setData(collectorTextChildBuff.toString());
+                        collectorTextChildBuff.setLength(0);
+                    }
+                    collectorTextChild = null;
+                }
+                
+                simplify(child, collectorTextChildBuff);
+            } else {
+                int type = child.getNodeType();
+                if (type == Node.TEXT_NODE || type == Node.CDATA_SECTION_NODE ) {
+                    if (collectorTextChild != null) {
+                        if (collectorTextChildBuff.length() == 0) {
+                            collectorTextChildBuff.ensureCapacity(
+                                    collectorTextChild.getNodeValue().length() + child.getNodeValue().length());
+                            collectorTextChildBuff.append(collectorTextChild.getNodeValue());
+                        }
+                        collectorTextChildBuff.append(child.getNodeValue());
+                        parent.removeChild(child);
+                    } else {
+                        collectorTextChild = child;
+                        collectorTextChildBuff.setLength(0);
+                    }
+                } else if (type == Node.COMMENT_NODE) {
+                    parent.removeChild(child);
+                } else if (type == Node.PROCESSING_INSTRUCTION_NODE) {
+                    parent.removeChild(child);
+                } else if (collectorTextChild != null) {
+                    // Commit pending text node merge:
+                    if (collectorTextChildBuff.length() != 0) {
+                        ((CharacterData) collectorTextChild).setData(collectorTextChildBuff.toString());
+                        collectorTextChildBuff.setLength(0);
+                    }
+                    collectorTextChild = null;
+                }
+            }
+            child = next;
+        }
+        
+        if (collectorTextChild != null) {
+            // Commit pending text node merge:
+            if (collectorTextChildBuff.length() != 0) {
+                ((CharacterData) collectorTextChild).setData(collectorTextChildBuff.toString());
+                collectorTextChildBuff.setLength(0);
+            }
+        }
+    }
+    
+    NodeModel getDocumentNodeModel() {
+        if (node instanceof Document) {
+            return this;
+        } else {
+            return wrap(node.getOwnerDocument());
+        }
+    }
+
+    /**
+     * Tells the system to use (restore) the default (initial) XPath system used by
+     * this FreeMarker version on this system.
+     */
+    static public void useDefaultXPathSupport() {
+        synchronized (STATIC_LOCK) {
+            xpathSupportClass = null;
+            jaxenXPathSupport = null;
+            try {
+                useXalanXPathSupport();
+            } catch (Exception e) {
+                // ignore
+            }
+            if (xpathSupportClass == null) try {
+            	useSunInternalXPathSupport();
+            } catch (Exception e) {
+                // ignore
+            }
+            if (xpathSupportClass == null) try {
+                useJaxenXPathSupport();
+            } catch (Exception e) {
+                // ignore
+            }
+        }
+    }
+    
+    /**
+     * Convenience method. Tells the system to use Jaxen for XPath queries.
+     * @throws Exception if the Jaxen classes are not present.
+     */
+    static public void useJaxenXPathSupport() throws Exception {
+        Class.forName("org.jaxen.dom.DOMXPath");
+        Class c = Class.forName("org.apache.freemarker.dom.JaxenXPathSupport");
+        jaxenXPathSupport = (XPathSupport) c.newInstance();
+        synchronized (STATIC_LOCK) {
+            xpathSupportClass = c;
+        }
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("Using Jaxen classes for XPath support");
+        }
+    }
+    
+    /**
+     * Convenience method. Tells the system to use Xalan for XPath queries.
+     * @throws Exception if the Xalan XPath classes are not present.
+     */
+    static public void useXalanXPathSupport() throws Exception {
+        Class.forName("org.apache.xpath.XPath");
+        Class c = Class.forName("org.apache.freemarker.dom.XalanXPathSupport");
+        synchronized (STATIC_LOCK) {
+            xpathSupportClass = c;
+        }
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("Using Xalan classes for XPath support");
+        }
+    }
+    
+    static public void useSunInternalXPathSupport() throws Exception {
+        Class.forName("com.sun.org.apache.xpath.internal.XPath");
+        Class c = Class.forName("org.apache.freemarker.dom.SunInternalXalanXPathSupport");
+        synchronized (STATIC_LOCK) {
+            xpathSupportClass = c;
+        }
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("Using Sun's internal Xalan classes for XPath support");
+        }
+    }
+    
+    /**
+     * Set an alternative implementation of org.apache.freemarker.dom.XPathSupport to use
+     * as the XPath engine.
+     * @param cl the class, or <code>null</code> to disable XPath support.
+     */
+    static public void setXPathSupportClass(Class cl) {
+        if (cl != null && !XPathSupport.class.isAssignableFrom(cl)) {
+            throw new RuntimeException("Class " + cl.getName()
+                    + " does not implement org.apache.freemarker.dom.XPathSupport");
+        }
+        synchronized (STATIC_LOCK) {
+            xpathSupportClass = cl;
+        }
+    }
+
+    /**
+     * Get the currently used org.apache.freemarker.dom.XPathSupport used as the XPath engine.
+     * Returns <code>null</code> if XPath support is disabled.
+     */
+    static public Class getXPathSupportClass() {
+        synchronized (STATIC_LOCK) {
+            return xpathSupportClass;
+        }
+    }
+
+    static private String getText(Node node) {
+        String result = "";
+        if (node instanceof Text || node instanceof CDATASection) {
+            result = ((org.w3c.dom.CharacterData) node).getData();
+        } else if (node instanceof Element) {
+            NodeList children = node.getChildNodes();
+            for (int i = 0; i < children.getLength(); i++) {
+                result += getText(children.item(i));
+            }
+        } else if (node instanceof Document) {
+            result = getText(((Document) node).getDocumentElement());
+        }
+        return result;
+    }
+    
+    XPathSupport getXPathSupport() {
+        if (jaxenXPathSupport != null) {
+            return jaxenXPathSupport;
+        }
+        XPathSupport xps = null;
+        Document doc = node.getOwnerDocument();
+        if (doc == null) {
+            doc = (Document) node;
+        }
+        synchronized (doc) {
+            WeakReference ref = (WeakReference) xpathSupportMap.get(doc);
+            if (ref != null) {
+                xps = (XPathSupport) ref.get();
+            }
+            if (xps == null) {
+                try {
+                    xps = (XPathSupport) xpathSupportClass.newInstance();
+                    xpathSupportMap.put(doc, new WeakReference(xps));
+                } catch (Exception e) {
+                    LOG.error("Error instantiating xpathSupport class", e);
+                }                
+            }
+        }
+        return xps;
+    }
+    
+    
+    String getQualifiedName() throws TemplateModelException {
+        return getNodeName();
+    }
+    
+    @Override
+    public Object getAdaptedObject(Class hint) {
+        return node;
+    }
+    
+    @Override
+    public Object getWrappedObject() {
+        return node;
+    }
+    
+    @Override
+    public Object[] explainTypeError(Class[] expectedClasses) {
+        for (Class expectedClass : expectedClasses) {
+            if (TemplateDateModel.class.isAssignableFrom(expectedClass)
+                    || TemplateNumberModel.class.isAssignableFrom(expectedClass)
+                    || TemplateBooleanModel.class.isAssignableFrom(expectedClass)) {
+                return new Object[]{
+                        "XML node values are always strings (text), that is, they can't be used as number, "
+                                + "date/time/datetime or boolean without explicit conversion (such as "
+                                + "someNode?number, someNode?datetime.xs, someNode?date.xs, someNode?time.xs, "
+                                + "someNode?boolean).",
+                };
+            }
+        }
+        return null;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/dom/NodeOutputter.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/dom/NodeOutputter.java b/freemarker-core/src/main/java/org/apache/freemarker/dom/NodeOutputter.java
new file mode 100644
index 0000000..bda38ac
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/dom/NodeOutputter.java
@@ -0,0 +1,258 @@
+/*
+ * 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.dom;
+
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.util._StringUtil;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.DocumentType;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+class NodeOutputter {
+    
+    private Element contextNode;
+    private Environment env;
+    private String defaultNS;
+    private boolean hasDefaultNS;
+    private boolean explicitDefaultNSPrefix;
+    private LinkedHashMap<String, String> namespacesToPrefixLookup = new LinkedHashMap<>();
+    private String namespaceDecl;
+    int nextGeneratedPrefixNumber = 1;
+    
+    NodeOutputter(Node node) {
+        if (node instanceof Element) {
+            setContext((Element) node);
+        } else if (node instanceof Attr) {
+            setContext(((Attr) node).getOwnerElement());
+        } else if (node instanceof Document) {
+            setContext(((Document) node).getDocumentElement());
+        }
+    }
+    
+    private void setContext(Element contextNode) {
+        this.contextNode = contextNode;
+        env = Environment.getCurrentEnvironment();
+        defaultNS = env.getDefaultNS();
+        hasDefaultNS = defaultNS != null && defaultNS.length() > 0;
+        namespacesToPrefixLookup.put(null, "");
+        namespacesToPrefixLookup.put("", "");
+        buildPrefixLookup(contextNode);
+        if (!explicitDefaultNSPrefix && hasDefaultNS) {
+            namespacesToPrefixLookup.put(defaultNS, "");
+        }
+        constructNamespaceDecl();
+    }
+    
+    private void buildPrefixLookup(Node n) {
+        String nsURI = n.getNamespaceURI();
+        if (nsURI != null && nsURI.length() > 0) {
+            String prefix = env.getPrefixForNamespace(nsURI);
+            if (prefix == null) {
+                prefix = namespacesToPrefixLookup.get(nsURI);
+                if (prefix == null) {
+                    // Assign a generated prefix:
+                    do {
+                        prefix = _StringUtil.toLowerABC(nextGeneratedPrefixNumber++);
+                    } while (env.getNamespaceForPrefix(prefix) != null);
+                }
+            }
+            namespacesToPrefixLookup.put(nsURI, prefix);
+        } else if (hasDefaultNS && n.getNodeType() == Node.ELEMENT_NODE) {
+            namespacesToPrefixLookup.put(defaultNS, Template.DEFAULT_NAMESPACE_PREFIX); 
+            explicitDefaultNSPrefix = true;
+        } else if (n.getNodeType() == Node.ATTRIBUTE_NODE && hasDefaultNS && defaultNS.equals(nsURI)) {
+            namespacesToPrefixLookup.put(defaultNS, Template.DEFAULT_NAMESPACE_PREFIX); 
+            explicitDefaultNSPrefix = true;
+        }
+        NodeList childNodes = n.getChildNodes();
+        for (int i = 0; i < childNodes.getLength(); i++) {
+            buildPrefixLookup(childNodes.item(i));
+        }
+    }
+    
+    private void constructNamespaceDecl() {
+        StringBuilder buf = new StringBuilder();
+        if (explicitDefaultNSPrefix) {
+            buf.append(" xmlns=\"");
+            buf.append(defaultNS);
+            buf.append("\"");
+        }
+        for (Iterator<String> it = namespacesToPrefixLookup.keySet().iterator(); it.hasNext(); ) {
+            String nsURI = it.next();
+            if (nsURI == null || nsURI.length() == 0) {
+                continue;
+            }
+            String prefix = namespacesToPrefixLookup.get(nsURI);
+            if (prefix == null) {
+                throw new BugException("No xmlns prefix was associated to URI: " + nsURI);
+            }
+            buf.append(" xmlns");
+            if (prefix.length() > 0) {
+                buf.append(":");
+                buf.append(prefix);
+            }
+            buf.append("=\"");
+            buf.append(nsURI);
+            buf.append("\"");
+        }
+        namespaceDecl = buf.toString();
+    }
+    
+    private void outputQualifiedName(Node n, StringBuilder buf) {
+        String nsURI = n.getNamespaceURI();
+        if (nsURI == null || nsURI.length() == 0) {
+            buf.append(n.getNodeName());
+        } else {
+            String prefix = namespacesToPrefixLookup.get(nsURI);
+            if (prefix == null) {
+                //REVISIT!
+                buf.append(n.getNodeName());
+            } else {
+                if (prefix.length() > 0) {
+                    buf.append(prefix);
+                    buf.append(':');
+                }
+                buf.append(n.getLocalName());
+            }
+        }
+    }
+    
+    void outputContent(Node n, StringBuilder buf) {
+        switch(n.getNodeType()) {
+            case Node.ATTRIBUTE_NODE: {
+                if (((Attr) n).getSpecified()) {
+                    buf.append(' ');
+                    outputQualifiedName(n, buf);
+                    buf.append("=\"")
+                       .append(_StringUtil.XMLEncQAttr(n.getNodeValue()))
+                       .append('"');
+                }
+                break;
+            }
+            case Node.COMMENT_NODE: {
+                buf.append("<!--").append(n.getNodeValue()).append("-->");
+                break;
+            }
+            case Node.DOCUMENT_NODE: {
+                outputContent(n.getChildNodes(), buf);
+                break;
+            }
+            case Node.DOCUMENT_TYPE_NODE: {
+                buf.append("<!DOCTYPE ").append(n.getNodeName());
+                DocumentType dt = (DocumentType) n;
+                if (dt.getPublicId() != null) {
+                    buf.append(" PUBLIC \"").append(dt.getPublicId()).append('"');
+                }
+                if (dt.getSystemId() != null) {
+                    buf.append(" \"").append(dt.getSystemId()).append('"');
+                }
+                if (dt.getInternalSubset() != null) {
+                    buf.append(" [").append(dt.getInternalSubset()).append(']');
+                }
+                buf.append('>');
+                break;
+            }
+            case Node.ELEMENT_NODE: {
+                buf.append('<');
+                outputQualifiedName(n, buf);
+                if (n == contextNode) {
+                    buf.append(namespaceDecl);
+                }
+                outputContent(n.getAttributes(), buf);
+                NodeList children = n.getChildNodes();
+                if (children.getLength() == 0) {
+                    buf.append(" />");
+                } else {
+                    buf.append('>');
+                    outputContent(n.getChildNodes(), buf);
+                    buf.append("</");
+                    outputQualifiedName(n, buf);
+                    buf.append('>');
+                }
+                break;
+            }
+            case Node.ENTITY_NODE: {
+                outputContent(n.getChildNodes(), buf);
+                break;
+            }
+            case Node.ENTITY_REFERENCE_NODE: {
+                buf.append('&').append(n.getNodeName()).append(';');
+                break;
+            }
+            case Node.PROCESSING_INSTRUCTION_NODE: {
+                buf.append("<?").append(n.getNodeName()).append(' ').append(n.getNodeValue()).append("?>");
+                break;
+            }
+            /*            
+                        case Node.CDATA_SECTION_NODE: {
+                            buf.append("<![CDATA[").append(n.getNodeValue()).append("]]>");
+                            break;
+                        }*/
+            case Node.CDATA_SECTION_NODE:
+            case Node.TEXT_NODE: {
+                buf.append(_StringUtil.XMLEncNQG(n.getNodeValue()));
+                break;
+            }
+        }
+    }
+
+    void outputContent(NodeList nodes, StringBuilder buf) {
+        for (int i = 0; i < nodes.getLength(); ++i) {
+            outputContent(nodes.item(i), buf);
+        }
+    }
+    
+    void outputContent(NamedNodeMap nodes, StringBuilder buf) {
+        for (int i = 0; i < nodes.getLength(); ++i) {
+            Node n = nodes.item(i);
+            if (n.getNodeType() != Node.ATTRIBUTE_NODE 
+                || (!n.getNodeName().startsWith("xmlns:") && !n.getNodeName().equals("xmlns"))) { 
+                outputContent(n, buf);
+            }
+        }
+    }
+    
+    String getOpeningTag(Element element) {
+        StringBuilder buf = new StringBuilder();
+        buf.append('<');
+        outputQualifiedName(element, buf);
+        buf.append(namespaceDecl);
+        outputContent(element.getAttributes(), buf);
+        buf.append('>');
+        return buf.toString();
+    }
+    
+    String getClosingTag(Element element) {
+        StringBuilder buf = new StringBuilder();
+        buf.append("</");
+        outputQualifiedName(element, buf);
+        buf.append('>');
+        return buf.toString();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/dom/NodeQueryResultItemObjectWrapper.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/dom/NodeQueryResultItemObjectWrapper.java b/freemarker-core/src/main/java/org/apache/freemarker/dom/NodeQueryResultItemObjectWrapper.java
new file mode 100644
index 0000000..e84e977
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/dom/NodeQueryResultItemObjectWrapper.java
@@ -0,0 +1,92 @@
+/*
+ * 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.dom;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelAdapter;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.WrappingTemplateModel;
+import org.apache.freemarker.core.model.impl.SimpleDate;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.w3c.dom.Node;
+
+/**
+ * Used for wrapping query result items (such as XPath query result items). Because {@link NodeModel} and such aren't
+ * {@link WrappingTemplateModel}-s, we can't use the actual {@link ObjectWrapper} from the {@link Environment}, also,
+ * even if we could, it might not be the right thing to do, because that  {@link ObjectWrapper} might not even wrap
+ * {@link Node}-s via {@link NodeModel}.
+ */
+class NodeQueryResultItemObjectWrapper implements ObjectWrapper {
+
+    static final NodeQueryResultItemObjectWrapper INSTANCE = new NodeQueryResultItemObjectWrapper();
+
+    private NodeQueryResultItemObjectWrapper() {
+        //
+    }
+
+    @Override
+    public TemplateModel wrap(Object obj) throws TemplateModelException {
+        if (obj instanceof NodeModel) {
+            return (NodeModel) obj;
+        }
+        if (obj instanceof Node) {
+            return NodeModel.wrap((Node) obj);
+        } else {
+            if (obj == null) {
+                return null;
+            }
+            if (obj instanceof TemplateModel) {
+                return (TemplateModel) obj;
+            }
+            if (obj instanceof TemplateModelAdapter) {
+                return ((TemplateModelAdapter) obj).getTemplateModel();
+            }
+
+            if (obj instanceof String) {
+                return new SimpleScalar((String) obj);
+            }
+            if (obj instanceof Number) {
+                return new SimpleNumber((Number) obj);
+            }
+            if (obj instanceof Boolean) {
+                return obj.equals(Boolean.TRUE) ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+            }
+            if (obj instanceof java.util.Date) {
+                if (obj instanceof java.sql.Date) {
+                    return new SimpleDate((java.sql.Date) obj);
+                }
+                if (obj instanceof java.sql.Time) {
+                    return new SimpleDate((java.sql.Time) obj);
+                }
+                if (obj instanceof java.sql.Timestamp) {
+                    return new SimpleDate((java.sql.Timestamp) obj);
+                }
+                return new SimpleDate((java.util.Date) obj, TemplateDateModel.UNKNOWN);
+            }
+            throw new TemplateModelException("Don't know how to wrap a W3C DOM query result item of this type: "
+                    + obj.getClass().getName());
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/dom/PINodeModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/dom/PINodeModel.java b/freemarker-core/src/main/java/org/apache/freemarker/dom/PINodeModel.java
new file mode 100644
index 0000000..381d4d6
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/dom/PINodeModel.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.dom;
+
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.w3c.dom.ProcessingInstruction;
+
+class PINodeModel extends NodeModel implements TemplateScalarModel {
+    
+    public PINodeModel(ProcessingInstruction pi) {
+        super(pi);
+    }
+    
+    @Override
+    public String getAsString() {
+        return ((ProcessingInstruction) node).getData();
+    }
+    
+    @Override
+    public String getNodeName() {
+        return "@pi$" + ((ProcessingInstruction) node).getTarget();
+    }
+    
+    @Override
+    public boolean isEmpty() {
+        return true;
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/dom/SunInternalXalanXPathSupport.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/dom/SunInternalXalanXPathSupport.java b/freemarker-core/src/main/java/org/apache/freemarker/dom/SunInternalXalanXPathSupport.java
new file mode 100644
index 0000000..991c93f
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/dom/SunInternalXalanXPathSupport.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.dom;
+
+import java.util.List;
+
+import javax.xml.transform.TransformerException;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.w3c.dom.Node;
+import org.w3c.dom.traversal.NodeIterator;
+
+import com.sun.org.apache.xml.internal.utils.PrefixResolver;
+import com.sun.org.apache.xpath.internal.XPath;
+import com.sun.org.apache.xpath.internal.XPathContext;
+import com.sun.org.apache.xpath.internal.objects.XBoolean;
+import com.sun.org.apache.xpath.internal.objects.XNodeSet;
+import com.sun.org.apache.xpath.internal.objects.XNull;
+import com.sun.org.apache.xpath.internal.objects.XNumber;
+import com.sun.org.apache.xpath.internal.objects.XObject;
+import com.sun.org.apache.xpath.internal.objects.XString;
+
+/**
+ * This is just the XalanXPathSupport class using the sun internal
+ * package names
+ */
+
+class SunInternalXalanXPathSupport implements XPathSupport {
+    
+    private XPathContext xpathContext = new XPathContext();
+        
+    private static final String ERRMSG_RECOMMEND_JAXEN
+            = "(Note that there is no such restriction if you "
+                    + "configure FreeMarker to use Jaxen instead of Xalan.)";
+
+    private static final String ERRMSG_EMPTY_NODE_SET
+            = "Cannot perform an XPath query against an empty node set." + ERRMSG_RECOMMEND_JAXEN;
+    
+    @Override
+    synchronized public TemplateModel executeQuery(Object context, String xpathQuery) throws TemplateModelException {
+        if (!(context instanceof Node)) {
+            if (context != null) {
+                if (isNodeList(context)) {
+                    int cnt = ((List) context).size();
+                    if (cnt != 0) {
+                        throw new TemplateModelException(
+                                "Cannot perform an XPath query against a node set of " + cnt
+                                + " nodes. Expecting a single node." + ERRMSG_RECOMMEND_JAXEN);
+                    } else {
+                        throw new TemplateModelException(ERRMSG_EMPTY_NODE_SET);
+                    }
+                } else {
+                    throw new TemplateModelException(
+                            "Cannot perform an XPath query against a " + context.getClass().getName()
+                            + ". Expecting a single org.w3c.dom.Node.");
+                }
+            } else {
+                throw new TemplateModelException(ERRMSG_EMPTY_NODE_SET);
+            }
+        }
+        Node node = (Node) context;
+        try {
+            XPath xpath = new XPath(xpathQuery, null, customPrefixResolver, XPath.SELECT, null);
+            int ctxtNode = xpathContext.getDTMHandleFromNode(node);
+            XObject xresult = xpath.execute(xpathContext, ctxtNode, customPrefixResolver);
+            if (xresult instanceof XNodeSet) {
+                NodeListModel result = new NodeListModel(node);
+                result.xpathSupport = this;
+                NodeIterator nodeIterator = xresult.nodeset();
+                Node n;
+                do {
+                    n = nodeIterator.nextNode();
+                    if (n != null) {
+                        result.add(n);
+                    }
+                } while (n != null);
+                return result.size() == 1 ? result.get(0) : result;
+            }
+            if (xresult instanceof XBoolean) {
+                return ((XBoolean) xresult).bool() ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+            }
+            if (xresult instanceof XNull) {
+                return null;
+            }
+            if (xresult instanceof XString) {
+                return new SimpleScalar(xresult.toString());
+            }
+            if (xresult instanceof XNumber) {
+                return new SimpleNumber(Double.valueOf(((XNumber) xresult).num()));
+            }
+            throw new TemplateModelException("Cannot deal with type: " + xresult.getClass().getName());
+        } catch (TransformerException te) {
+            throw new TemplateModelException(te);
+        }
+    }
+    
+    private static PrefixResolver customPrefixResolver = new PrefixResolver() {
+        
+        @Override
+        public String getNamespaceForPrefix(String prefix, Node node) {
+            return getNamespaceForPrefix(prefix);
+        }
+        
+        @Override
+        public String getNamespaceForPrefix(String prefix) {
+            if (prefix.equals(Template.DEFAULT_NAMESPACE_PREFIX)) {
+                return Environment.getCurrentEnvironment().getDefaultNS();
+            }
+            return Environment.getCurrentEnvironment().getNamespaceForPrefix(prefix);
+        }
+        
+        @Override
+        public String getBaseIdentifier() {
+            return null;
+        }
+        
+        @Override
+        public boolean handlesNullPrefixes() {
+            return false;
+        }
+    };
+    
+    /**
+     * Used for generating more intelligent error messages.
+     */
+    private static boolean isNodeList(Object context) {
+        if (context instanceof List) {
+            List ls = (List) context;
+            int ln = ls.size();
+            for (int i = 0; i < ln; i++) {
+                if (!(ls.get(i) instanceof Node)) {
+                    return false;
+                }
+            }
+            return true;
+        } else {
+            return false;
+        }
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/dom/XPathSupport.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/dom/XPathSupport.java b/freemarker-core/src/main/java/org/apache/freemarker/dom/XPathSupport.java
new file mode 100644
index 0000000..e94d391
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/dom/XPathSupport.java
@@ -0,0 +1,30 @@
+/*
+ * 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.dom;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+
+public interface XPathSupport {
+    
+    // [2.4] Add argument to pass down the ObjectWrapper to use 
+    TemplateModel executeQuery(Object context, String xpathQuery) throws TemplateModelException;
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/dom/XalanXPathSupport.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/dom/XalanXPathSupport.java b/freemarker-core/src/main/java/org/apache/freemarker/dom/XalanXPathSupport.java
new file mode 100644
index 0000000..99a4249
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/dom/XalanXPathSupport.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.dom;
+
+import java.util.List;
+
+import javax.xml.transform.TransformerException;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.apache.xml.utils.PrefixResolver;
+import org.apache.xpath.XPath;
+import org.apache.xpath.XPathContext;
+import org.apache.xpath.objects.XBoolean;
+import org.apache.xpath.objects.XNodeSet;
+import org.apache.xpath.objects.XNull;
+import org.apache.xpath.objects.XNumber;
+import org.apache.xpath.objects.XObject;
+import org.apache.xpath.objects.XString;
+import org.w3c.dom.Node;
+import org.w3c.dom.traversal.NodeIterator;
+
+/**
+ * Some glue code that bridges the Xalan XPath stuff (that is built into the JDK 1.4.x)
+ * with FreeMarker TemplateModel semantics
+ */
+
+class XalanXPathSupport implements XPathSupport {
+    
+    private XPathContext xpathContext = new XPathContext();
+        
+    /* I don't recommend Jaxen...
+    private static final String ERRMSG_RECOMMEND_JAXEN
+            = "(Note that there is no such restriction if you "
+                    + "configure FreeMarker to use Jaxen instead of Xalan.)";
+    */
+    private static final String ERRMSG_EMPTY_NODE_SET
+            = "Cannot perform an XPath query against an empty node set."; /* " + ERRMSG_RECOMMEND_JAXEN;*/
+    
+    @Override
+    synchronized public TemplateModel executeQuery(Object context, String xpathQuery) throws TemplateModelException {
+        if (!(context instanceof Node)) {
+            if (context != null) {
+                if (isNodeList(context)) {
+                    int cnt = ((List) context).size();
+                    if (cnt != 0) {
+                        throw new TemplateModelException(
+                                "Cannot perform an XPath query against a node set of " + cnt
+                                + " nodes. Expecting a single node."/* " + ERRMSG_RECOMMEND_JAXEN*/);
+                    } else {
+                        throw new TemplateModelException(ERRMSG_EMPTY_NODE_SET);
+                    }
+                } else {
+                    throw new TemplateModelException(
+                            "Cannot perform an XPath query against a " + context.getClass().getName()
+                            + ". Expecting a single org.w3c.dom.Node.");
+                }
+            } else {
+                throw new TemplateModelException(ERRMSG_EMPTY_NODE_SET);
+            }
+        }
+        Node node = (Node) context;
+        try {
+            XPath xpath = new XPath(xpathQuery, null, customPrefixResolver, XPath.SELECT, null);
+            int ctxtNode = xpathContext.getDTMHandleFromNode(node);
+            XObject xresult = xpath.execute(xpathContext, ctxtNode, customPrefixResolver);
+            if (xresult instanceof XNodeSet) {
+                NodeListModel result = new NodeListModel(node);
+                result.xpathSupport = this;
+                NodeIterator nodeIterator = xresult.nodeset();
+                Node n;
+                do {
+                    n = nodeIterator.nextNode();
+                    if (n != null) {
+                        result.add(n);
+                    }
+                } while (n != null);
+                return result.size() == 1 ? result.get(0) : result;
+            }
+            if (xresult instanceof XBoolean) {
+                return ((XBoolean) xresult).bool() ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+            }
+            if (xresult instanceof XNull) {
+                return null;
+            }
+            if (xresult instanceof XString) {
+                return new SimpleScalar(xresult.toString());
+            }
+            if (xresult instanceof XNumber) {
+                return new SimpleNumber(Double.valueOf(((XNumber) xresult).num()));
+            }
+            throw new TemplateModelException("Cannot deal with type: " + xresult.getClass().getName());
+        } catch (TransformerException te) {
+            throw new TemplateModelException(te);
+        }
+    }
+    
+    private static PrefixResolver customPrefixResolver = new PrefixResolver() {
+        
+        @Override
+        public String getNamespaceForPrefix(String prefix, Node node) {
+            return getNamespaceForPrefix(prefix);
+        }
+        
+        @Override
+        public String getNamespaceForPrefix(String prefix) {
+            if (prefix.equals(Template.DEFAULT_NAMESPACE_PREFIX)) {
+                return Environment.getCurrentEnvironment().getDefaultNS();
+            }
+            return Environment.getCurrentEnvironment().getNamespaceForPrefix(prefix);
+        }
+        
+        @Override
+        public String getBaseIdentifier() {
+            return null;
+        }
+        
+        @Override
+        public boolean handlesNullPrefixes() {
+            return false;
+        }
+    };
+    
+    /**
+     * Used for generating more intelligent error messages.
+     */
+    private static boolean isNodeList(Object context) {
+        if (context instanceof List) {
+            List ls = (List) context;
+            int ln = ls.size();
+            for (int i = 0; i < ln; i++) {
+                if (!(ls.get(i) instanceof Node)) {
+                    return false;
+                }
+            }
+            return true;
+        } else {
+            return false;
+        }
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/dom/package.html
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/dom/package.html b/freemarker-core/src/main/java/org/apache/freemarker/dom/package.html
new file mode 100644
index 0000000..61b1737
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/dom/package.html
@@ -0,0 +1,30 @@
+<!--
+  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.
+-->
+<html>
+<head>
+<title></title>
+</head>
+<body>
+
+<p>Exposes DOM XML nodes to templates as easily traversable trees;
+see <a href="http://freemarker.org/docs/xgui.html" target="_blank">in the Manual</a>.
+The default object wrapper of FreeMarker can automatically wraps W3C nodes with this.
+
+</body>
+</html>


[50/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/.gitignore
----------------------------------------------------------------------
diff --git a/.gitignore b/.gitignore
index 28532ff..955d490 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,15 +1,13 @@
-/.ivy/
+**/build/
+/.out/
+/bin/
 /.bin/
-/build/
-/build.properties
+/target/
+
+/gradle.properties
 /archive/
-/ide-dependencies/
 /META-INF
 
-/out/
-/bin/
-/target/
-
 .classpath
 .project
 .settings
@@ -19,7 +17,7 @@
 *.iws
 *.ipr
 .idea_modules/
-.out/
+/out/
 
 *.tmp
 *.bak

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/.travis.yml
----------------------------------------------------------------------
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 5d914d9..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-language: java
-install: ant download-ivy
-jdk:
-  - oraclejdk8
-script: ant ci
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/LICENSE
----------------------------------------------------------------------
diff --git a/LICENSE b/LICENSE
index 7449e24..7164877 100644
--- a/LICENSE
+++ b/LICENSE
@@ -201,14 +201,13 @@
    See the License for the specific language governing permissions and
    limitations under the License.
 
-=========================================================================
+==============================================================================
 
 The Apache FreeMarker (incubating) source code contains the following
-binaries, which were created at the Apache FreeMarker (incubating)
-project, and hence are covered by the same license as the other source
-files of it:
+binaries, which were created at the Apache FreeMarker (incubating) project,
+and hence are covered by the same license as the other source files of it:
 
-    src/main/misc/overloadedNumberRules/prices.ods
-    src/manual/en_US/docgen-originals/figures/overview.odg
+    freemarker-core/src/main/misc/overloadedNumberRules/prices.ods
+    freemarker-core/src/manual/en_US/docgen-originals/figures/overview.odg
 
-=========================================================================
+==============================================================================

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/README-gradle.md
----------------------------------------------------------------------
diff --git a/README-gradle.md b/README-gradle.md
deleted file mode 100644
index 8b0fe67..0000000
--- a/README-gradle.md
+++ /dev/null
@@ -1,13 +0,0 @@
-The current gradle build is work in progress, so use the Ant build, as
-described in README.md!
-
-To build the project, go to the project home directory, and issue:
-
-    ./gradlew jar test
-  
-On Windows this won't work if you are using an Apache source release (as
-opposed to checking the project out from Git), as due to Apache policy
-restricton `gradle\wrapper\gradle-wrapper.jar` is missing from that. So you
-have to download that very common artifact from somewhere manually. On
-UN*X-like systems (and from under Cygwin shell) you don't need that jar, as
-our custom `gradlew` shell script does everything itself.

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/README.md
----------------------------------------------------------------------
diff --git a/README.md b/README.md
index 6393f28..7168740 100644
--- a/README.md
+++ b/README.md
@@ -57,8 +57,8 @@ If you are using Maven, just add this dependency:
 
 ```xml
   <dependency>
-    <groupId>org.apache</groupId>
-    <artifactId>freemarker</artifactId>
+    <groupId>org.apache.freemarker</groupId>
+    <artifactId>freemarker-core</artifactId>
     <version>{version}</version>
   </dependency>
 ```
@@ -101,25 +101,30 @@ If you haven't yet, download the source release, or checkout FreeMarker from
 the source code repository. See repository locations here:
 http://freemarker.org/sourcecode.html
 
-You need JDK 8, Apache Ant (tested with 1.8.1) and Ivy (tested with 2.4.0) to
-be installed. To install Ivy (but be sure it's not already installed), issue
-`ant download-ivy`; it will copy Ivy under `~/.ant/lib`. (Alternatively, you
-can copy `ivy-<version>.jar` into the Ant home `lib` subfolder manually.)
+You need JDK 8 to be installed.
 
-It's recommended to copy `build.properties.sample` into `build.properties`,
-and edit its content to fit your system. (Although basic jar building should
-succeeds without the build.properties file too.)
+You must copy `gradle.properties.sample` into `gradle.properties`, and edit its
+content to fit your system.
 
-To build `freemarker.jar`, just issue `ant` in the project root directory, and
-it should download all dependencies automatically and build `freemarker.jar`. 
+To build `freemarker.jar`, just issue `./gradlew jar` in the project root
+directory (Windows users see the note below though), and it should download
+all dependencies (including Gradle itself) automatically and build the jar-s.
+You can found them in the build/libs subdirectory of each module
+(freemarker-core, freemarker-servlet, etc.). You can also install the jar-s
+into your local Maven repository with `./gradlew install`.
 
-If later you change the dependencies in `ivy.xml`, or otherwise want to
-re-download some of them, it will not happen automatically anymore, and you
-must issue `ant update-deps`.
+Note for Windows users: If you are using an Apache source release (as opposed
+to checking the project out from the Git repository), ./gradlew will fail as
+`gradle\wrapper\gradle-wrapper.jar` is missing. Due to Apache policy restricton
+we can't include that file in distributions, so you have to download that very
+common artifact from somewhere manually (like from out Git repository). (On
+UN*X-like systems you don't need that jar, as our custom `gradlew` shell script
+does everything itself.)
 
-To test your build, issue `ant test`.
+To test your build, issue `./gradlew test`.
 
-To generate documentation, issue `ant javadoc` and `ant manualOffline`.
+To generate documentation, issue `./gradlew javadoc` and
+`./gradlew manualOffline` (TODO: the last doesn't yet work).
 
 
 Eclipse and other IDE setup
@@ -129,10 +134,6 @@ Below you find the step-by-step setup for Eclipse Neon.1. If you are using a
 different version or an entierly different IDE, still read this, and try to
 apply it to your development environment:
 
-- Install Ant and Ivy, if you haven't yet; see earlier.
-- From the command line, run  `ant clean jar ide-dependencies`
-  (Note that now the folders `ide-dependencies`, `build/generated-sources` and
-  `META-INF` were created.)
 - Start Eclipse
 - You may prefer to start a new workspace (File -> "Switch workspace"), but
   it's optional.
@@ -156,8 +157,8 @@ apply it to your development environment:
     Number of imports required for .*: 99
     Number of static imports needed for .*: 1
   - Java -> Installed JRE-s:
-    Ensure that you have JDK 8 installed, and that it was added to Eclipse.
-    Note that it's not JRE, but JDK.
+    Ensure that you have JDK 7 and JDK 8 installed, and that it was added to
+    Eclipse. Note that it's not JRE, but JDK.
   - Java -> Compiler -> Javadoc:
     "Malformed Javadoc comments": Error
     "Only consider members as visible": Private
@@ -165,28 +166,14 @@ apply it to your development environment:
     "Missing tag descriptions": Validate @return tags
     "Missing Javadoc tags": Ignore
     "Missing Javadoc comments": Ignore
-- Create new "Java Project" in Eclipse:
-  - In the first window popping up:
-    - Change the "location" to the directory of the FreeMarker project
-    - Press "Next"
-  - In the next window, you see the build path settings:
-    - On "Source" tab, ensure that exactly these are marked as source
-      directories (be careful, Eclipse doesn't auto-detect these well):
-        build/generated-sources/java
-        src/main/java
-        src/main/resources
-        src/test/java
-        src/test/resources
-    - On the "Libraries" tab:
-      - Delete everyhing from there, except the "JRE System Library [...]"
-      - Edit "JRE System Library [...]" to "Execution Environment" "JavaSE 1.8"
-      - Add all jar-s that are directly under the "ide-dependencies" directory
-        (use the "Add JARs..." and select all those files).
-    - On the "Order and Export" tab find dom4j-*.jar, and send it to the
-        bottom of the list (becase, an old org.jaxen is included inside
-        dom4j-*.jar, which casues compilation errors if it wins over
-        jaxen-*.jar).
-   - Press "Finish"
+- TODO: How to import the Gradle project into Eclipse
+  On IntelliJ:
+  - Import the whole FreeMarker project as a Gradle project. There are things that you
+    will have to set manually, but first, build the project with Gradle if you haven't
+    (see earlier how).
+  - Open Project Structure (Alt+Ctrl+Shift+S), and in the "Dependencies" tab of each
+    module, set "Module SDK" to "1.7", except for freemarker-core-java8, where it should
+    be "1.8". [TODO: Check if now it happens automatically]
 - Project -> Properties -> Java Compiler -> Errors/Warnings:
   Check in "Enable project specific settings", then set "Forbidden reference
   (access rules)" from "Error" to "Warning".
@@ -198,7 +185,7 @@ apply it to your development environment:
   last should contain "Add missing @Override annotations",
   "Add missing @Override annotations to implementations of interface methods",
   "Add missing @Deprecated annotations", and "Remove unnecessary cast").
-- Right click on the project -> Run As -> JUnit Test
+- Right click on the root project -> Run As -> JUnit Test [TODO: Try this]
   It should run without problems (all green).
 - It's highly recommened to use the Eclipse FindBugs plugin.
   - Install it from Eclipse Marketplace (3.0.1 as of this writing)

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/build.gradle
----------------------------------------------------------------------
diff --git a/build.gradle b/build.gradle
index 1a86c94..d47fa9e 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,131 +1,279 @@
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
+ * or more contributor license agreements. See the NOTICE file
  * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
+ * 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
+ * with the License. You may obtain a copy of the License at
  *
- *   http://www.apache.org/licenses/LICENSE-2.0
+ * 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
+ * KIND, either express or implied. See the License for the
  * specific language governing permissions and limitations
  * under the License.
  */
 
-plugins {
-    id "ca.coglinc.javacc" version "2.4.0"
+// TODO: Versions should come form src/main/resource/o/a/f/c/version.properties
+ext.versionCanonical = "3.0.0-nightly-incubating"
+ext.versionForMaven = "3.0.0-SNAPSHOT"
+ext.versionForOSGi = "3.0.0.nightly-incubating"
+ext.versionForMf = "2.97.0"
+ 
+allprojects {
+    group = "org.apache.freemarker"
+    version = "${versionCanonical}"
 }
 
-apply plugin: "java"
+// Libraries that are referred from multiple places:
+ext.libraries = [
+    findbugs: "com.google.code.findbugs:annotations:3.0.0"
+]
+ext.slf4jVersion = "1.7.25"
 
-version = "3.0.0-nightly"
+// Unwanted transitive dependencies that often get in accidentally:
+ext.bannedLibraries = [
+    // Note that the version must be omitted in these entres!
+    // We're using SLF4J + Logback Classic, and xxx-over-slf4j to mimic other logger libraries.
+    "org.slf4j:slf4j-log4j12",
+    "org.slf4j:slf4j-jdk14",
+    "log4j:log4j",
+    "commons-logging:commons-logging"
+] as Set
 
-repositories {
-    // mavenLocal()
-    mavenCentral()
-}
-
-configurations.all {
-    // We use SLF4J with Logback binding, so exclude any other SLF4J bindings:
-    exclude group: "org.slf4j", module: "slf4j-log4j12"
-    exclude group: "org.slf4j", module: "slf4j-jdk14"
-
-    // We use xxx-over-slf4j to substitute logging libraries, so exclude them:
-    exclude group: "log4j", module: "log4j"
-    exclude group: "commons-logging", module: "commons-logging"
-
-    // xml-apis is part of the Java SE version for a good while; prevent old libraries pulling it in:
-    exclude group: "xml-apis", module: "xml-apis"
-}
-
-configurations.testCompile {
-    // Jetty pulls in its own version of Servlet/JSP classes, so don't inherit these from the "compile" configuration:
-    exclude group: "javax.servlet.jsp", module: "jsp-api"
-    exclude group: "javax.servlet.jsp", module: "servlet-api"
+['bootClasspathJava7', 'bootClasspathJava8'].each {
+    if (!project.hasProperty(it)) {
+        throw new org.gradle.api.GradleScriptException("The ${it} property " +
+                "must be set. Maybe you have missed this step: Copy gradle.properties.sample into gradle.properties, and " +
+                "edit it to describe your environment. Alternatively, pass the properties to gradle with " +
+                "-P${it}=\"...\".",
+                null);
+    }
 }
 
-dependencies {
-    def jettyVersion = "7.6.16.v20140903"
-    def slf4jVersion = "1.7.22"
-    def springVersion = "2.5.6.SEC03"
-
-    compile "com.google.guava:guava:20.0"
-
-    compile "jaxen:jaxen:1.0-FCS"
-    compile "saxpath:saxpath:1.0-FCS"
-    compile "xalan:xalan:2.7.0"
-
-    compile "org.slf4j:slf4j-api:$slf4jVersion"
-
-    compile "org.zeroturnaround:javarebel-sdk:1.2.2"
-
-    // TODO @SuppressFBWarnings-s should be removed before build, then this dependency is only needed for the IDE
-    compile "com.google.code.findbugs:annotations:3.0.0"
-
-    // TODO These will be moved to the freemarker-serlvet module:
-    compile "javax.servlet.jsp:jsp-api:2.1"
-    compile "javax.servlet:servlet-api:2.5"
-
-    // Test:
-
-    testCompile "junit:junit:4.12"
-    testCompile "org.hamcrest:hamcrest-library:1.3"
+subprojects {
+    apply plugin: "java"
+    apply plugin: "maven"
+    apply plugin: "osgi"
+    apply plugin: "idea"
 
-    testCompile "ch.qos.logback:logback-classic:1.1.8"
-    testCompile "org.slf4j:jcl-over-slf4j:$slf4jVersion"
+    // Default java compiler configuration (might be overridden in subprojects):
+    sourceCompatibility = "1.7"
+    targetCompatibility = "1.7"
+    [compileJava, compileTestJava]*.options*.encoding = "UTF-8"
+    [compileJava, compileTestJava]*.options*.bootClasspath = bootClasspathJava7
+    // TODO Remove SuppressFBWarning-s from compileJava output somehow
+    // TODO Ensure that JUnit tests run on Java 7, except for the modules that were made for later versions.
 
-    testCompile "commons-io:commons-io:2.2"
-    testCompile "com.google.guava:guava-jdk5:17.0"
-
-    testCompile "org.eclipse.jetty:jetty-server:$jettyVersion"
-    testCompile "org.eclipse.jetty:jetty-webapp:$jettyVersion"
-    testCompile "org.eclipse.jetty:jetty-jsp:$jettyVersion"
-    testCompile "org.eclipse.jetty:jetty-util:$jettyVersion"
-
-    testCompile("displaytag:displaytag:1.2") {
-        exclude group: "com.lowagie", module: "itext"
+    repositories {
+        // mavenLocal()
+        mavenCentral()
+    }
+    
+    // Dependencies used in all subprojects:
+    dependencies {
+        // All subprojects have access to SLF4J (regardless if they actually use it at the moment):
+        compile "org.slf4j:slf4j-api:$slf4jVersion"
+        // All subprojects might use Findbugs annotations:
+        compileOnly libraries.findbugs
+    
+        // Test libraries and utilities might come handy during testing:
+        testCompile project(":freemarker-test-utils")
     }
 
-    testCompile "org.springframework:spring-core:$springVersion"
-    testCompile "org.springframework:spring-test:$springVersion"
-}
-
-compileJava {
-    // TODO This will be 1.7 when freemarker-core-java8 is separated
-    sourceCompatibility = "1.8"
-    targetCompatibility = "1.8"
-
-    options.encoding = "UTF-8"
-}
+    // Like Maven's Enforcer plugin, make the build fail if certain libraries get in. (The problem with the
+    // customary `configurations.all { exclude ... }` soltion is that it bloats the genereated Maven POM-s a lot.)
+    test.doFirst {
+        configurations.testRuntime.getResolvedConfiguration().getResolvedArtifacts().each {
+            def artifactId = it.getModuleVersion().getId()
+            String artifactIdStr = "${artifactId.group}:${artifactId.name}"
+            if (artifactIdStr in bannedLibraries) {
+                throw new GradleScriptException(
+                        "Banned library in the dependency graph: ${artifactIdStr}. "
+                        + "Use `gradlew ${project.path}:dependencies` to find who pulls it in then exclude it there.",
+                        null);
+            }
+        }
+    }
 
-compileTestJava {
-    sourceCompatibility = "1.8"
-    targetCompatibility = "1.8"
+    jar {
+        manifest {   // org.gradle.api.plugins.osgi.OsgiManifest
+            version versionForOSGi
+            license "Apache License, Version 2.0" // TODO has no effect; bug?
+            vendor "Apache Software Foundation"
+            // TODO The autogenerated Bundle-SymbolicName is weird, esp. for freemarker-core-java8. How should it look?
+            
+            attributes(
+                "Bundle-License": "Apache License, Version 2.0",  // because `license "..."` above didn't work
+                "Specification-Version": versionForMf,
+                "Specification-Vendor": "Apache Software Foundation",
+                "Implementation-Version": versionForMf,
+                "Implementation-Vendor": "Apache Software Foundation"
+            )
+        }
+    }
 
-    options.encoding = "UTF-8"
-}
+    // The identical parts of Maven "deployer" and "installer" configuration:
+    def mavenCommons = { callerDelegate ->
+        delegate = callerDelegate
+        
+        pom.version = versionForMaven
+        pom.project {
+            organization {
+                name "Apache Software Foundation"
+                url "http://apache.org"
+            }
+            licenses {
+                license {
+                    name "Apache License, Version 2.0"
+                    url "http://www.apache.org/licenses/LICENSE-2.0.txt"
+                    distribution "repo"
+                }
+            }
+            scm {
+                connection "scm:git:https://git-wip-us.apache.org/repos/asf/incubator-freemarker.git"
+                developerConnection "scm:git:https://git-wip-us.apache.org/repos/asf/incubator-freemarker.git"
+                url "https://git-wip-us.apache.org/repos/asf?p=incubator-freemarker.git"
+                if (versionForOSGi.contains('.stable')) {
+                    tag "v${version}"
+                }
+            }
+            issueManagement {
+                system "jira"
+                url "https://issues.apache.org/jira/browse/FREEMARKER/"
+            }
+            mailingLists {
+                mailingList {
+                    name "FreeMarker developer list"
+                    post "dev@freemarker.incubator.apache.org"
+                    subscribe "dev-subscribe@freemarker.incubator.apache.org"
+                    unsubscribe "dev-unsubscribe@freemarker.incubator.apache.org"
+                    archive "http://mail-archives.apache.org/mod_mbox/incubator-freemarker-dev/"
+                }
+                mailingList {
+                    name "FreeMarker commit and Jira notifications list"
+                    post "notifications@freemarker.incubator.apache.org"
+                    subscribe "notifications-subscribe@freemarker.incubator.apache.org"
+                    unsubscribe "notifications-unsubscribe@freemarker.incubator.apache.org"
+                    archive "http://mail-archives.apache.org/mod_mbox/incubator-freemarker-notifications/"
+                }
+                mailingList {
+                    name "FreeMarker management private"
+                    post "private@freemarker.incubator.apache.org"
+                }
+            }
+        }
+    } // end mavenCommons
 
-compileJavacc {
-    arguments = [ grammar_encoding: "UTF-8" ]
-    doLast {
-        // TODO Some filtering is needed on the output - see in the original Ant build
+    uploadArchives {
+        repositories {
+            // TODO We must deploy source and javadoc artifact as well; see old Ant build.xml
+            mavenDeployer {
+                mavenCommons(delegate)
+                repository(
+                        // URL-s copy-pasted from the org.apacha:apache parent POM
+                        url: versionForMaven.contains('-SNAPSHOT')
+                                ? "https://repository.apache.org/content/repositories/snapshots/"
+                                : "https://repository.apache.org/service/local/staging/deploy/maven2"
+                )
+                // TODO Password authentication needed (can it use ~/.m2/settings.xml, like the real Maven?)
+                // TODO We must sign all artifacts with GPG; see old Ant build.xml
+            }
+        }
+    }    
 
-        // Note: The Gradle JavaCC plugin automatically removes generated java files that are already in
-        // src/main/java, so we don't need to get rid of ParseException.java and TokenMgrError.java (unlike in Ant)
+    install {
+        // TODO We must deploy source and javadoc artifact as well; see old Ant build.xml
+        repositories {
+            mavenInstaller {
+                mavenCommons(delegate)
+            }
+        }
     }
-}
+    
+    // Post-process fully generated POM-s to remove test scope dependencies, just for the sake of aesthetics.
+    [install.repositories.mavenInstaller, uploadArchives.repositories.mavenDeployer]*.pom*.whenConfigured { pom ->
+        pom.dependencies = pom.dependencies.findAll { dep -> dep.scope != "test" }        
+    }
+    
+    javadoc {
+        exclude "**/_*.java"
+        options.use = true
+        options.encoding = "UTF-8"
+        options.docEncoding = "UTF-8"
+        options.charSet = "UTF-8"
+        options.locale = "en_US"
+        options.links = [ "http://docs.oracle.com/javase/8/docs/api/" ]
+        doLast {
+            // We will fix low quality typography of JDK 8 Javadoc here. Bascially we make it look like JDK 7.
+            
+            File cssFile = new File(outputDirectory, "stylesheet.css")
+            assert cssFile.exists()
+            
+            // Tell that it's modified:
+            ant.replaceregexp(
+                file: cssFile, flags: "gs", encoding: "utf-8",
+                match: $//\* (Javadoc style sheet) \*//$, replace: $//\* \1 - JDK 8 usability fix regexp substitutions applied \*//$
+            )
 
-jar {
-    // TODO Use bnd - see in the Ant build
-    manifest {
-        attributes(
-                // TODO There were more here in the Ant build
-                "Implementation-Title": "Apache FreeMarker",
-                "Implementation-Version": project.version)
+            // Remove broken link:
+            ant.replaceregexp(
+                file: cssFile, flags: "gs", encoding: "utf-8",
+                match: $/@import url\('resources/fonts/dejavu.css'\);\s*/$, replace: ""
+            )
+            
+            // Font family fixes:
+            ant.replaceregexp(
+                file: cssFile, flags: "gsi", encoding: "utf-8",
+                match: $/['"]DejaVu Sans['"]/$, replace: "Arial"
+            )
+            ant.replaceregexp(
+                file: cssFile, flags: "gsi", encoding: "utf-8",
+                match: $/['"]DejaVu Sans Mono['"]/$, replace: "'Courier New'"
+            )
+            ant.replaceregexp(
+                file: cssFile, flags: "gsi", encoding: "utf-8",
+                match: $/['"]DejaVu Serif['"]/$, replace: "Arial"
+            )
+            ant.replaceregexp(
+                file: cssFile, flags: "gsi", encoding: "utf-8",
+                match: $/(?<=[\s,:])serif\b/$, replace: "sans-serif"
+            )
+            ant.replaceregexp(
+                file: cssFile, flags: "gsi", encoding: "utf-8",
+                match: $/(?<=[\s,:])Georgia,\s*/$, replace: ""
+            )
+            ant.replaceregexp(
+                file: cssFile, flags: "gsi", encoding: "utf-8",
+                match: $/['"]Times New Roman['"],\s*/$, replace: ""
+            )
+            ant.replaceregexp(
+                file: cssFile, flags: "gsi", encoding: "utf-8",
+                match: $/(?<=[\s,:])Times,\s*/$, replace: ""
+            )
+            ant.replaceregexp(
+                file: cssFile, flags: "gsi", encoding: "utf-8",
+                match: $/(?<=[\s,:])Arial\s*,\s*Arial\b/$, replace: "Arial"
+            )
+            
+            // "Parameters:", "Returns:", "Throws:", "Since:", "See also:" etc. fixes:
+            String ddSelectorStart = $/(?:\.contentContainer\s+\.(?:details|description)|\.serializedFormContainer)\s+dl\s+dd\b.*?\{[^\}]*\b/$
+            String ddPropertyEnd = $/\b.+?;/$
+            // - Put back description (dd) indentation:
+            ant.replaceregexp(
+                file: cssFile, flags: "gs", encoding: "utf-8",
+                match: $/(${ddSelectorStart})margin${ddPropertyEnd}/$, replace: $/\1margin: 5px 0 10px 20px;/$
+            )
+            // - No monospace font for the description (dd) part:
+            ant.replaceregexp(
+                file: cssFile, flags: "gs", encoding: "utf-8",
+                match: $/(${ddSelectorStart})font-family${ddPropertyEnd}/$, replace: $/\1/$
+            )
+            
+        }
     }
-}
+
+} // end subprojects
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/build.properties.sample
----------------------------------------------------------------------
diff --git a/build.properties.sample b/build.properties.sample
deleted file mode 100644
index 51d253a..0000000
--- a/build.properties.sample
+++ /dev/null
@@ -1,23 +0,0 @@
-# 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.
-
-# Copy this file to "build.properties" before editing!
-# These propeties should point to the rt.jar-s of the respective J2SE versions:
-boot.classpath.j2se1.7=C:/Program Files/Java/jdk1.7.0_25/jre/lib/rt.jar
-boot.classpath.j2se1.8=C:/Program Files/Java/jdk1.8.0_66/jre/lib/rt.jar
-mvnCommand=C:/Program Files (x86)/maven3/bin/mvn.cmd
-gpgCommand=C:/Program Files (x86)/GNU/GnuPG/pub/gpg.exe
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/build.xml
----------------------------------------------------------------------
diff --git a/build.xml b/build.xml
deleted file mode 100644
index 3270ff9..0000000
--- a/build.xml
+++ /dev/null
@@ -1,1093 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-  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.
--->
-
-<project basedir="." default="jar" name="freemarker"
-  xmlns:ivy="antlib:org.apache.ivy.ant"
-  xmlns:javacc="http://javacc.dev.java.net/"
-  xmlns:docgen="http://freemarker.org/docgen"
-  xmlns:bnd="http://www.aqute.biz/bnd"
-  xmlns:rat="antlib:org.apache.rat.anttasks"
-  xmlns:u="http://freemarker.org/util"
->
-
-  <!-- ================================================================== -->
-  <!-- Properties                                                         -->
-  <!-- ================================================================== -->
-
-  <!-- Maven project coordinates: -->
-  <property name="mavenGroupId" value="org.apache.freemarker" />
-  <property name="mavenArtifactId" value="freemarker" />
-  <!-- Ivy project coordinates: -->
-  <property name="moduleOrg" value="org.freemarker" />
-  <property name="moduleName" value="freemarker" />
-  <property name="moduleBranch" value="3" />
-
-  <!-- Will be overidden on the Continous Integration server: -->
-  <property name="server.ivy.repo.root" value="${basedir}/build/dummy-server-ivy-repo" />
-  
-  <property file="build.properties"/>
-  <condition property="has.explicit.boot.classpath.j2se1.7">
-    <isset property="boot.classpath.j2se1.7"/>
-  </condition>
-  <condition property="has.explicit.boot.classpath.j2se1.8">
-    <isset property="boot.classpath.j2se1.8"/>
-  </condition>
-  <condition property="has.all.explicit.boot.classpaths">
-    <and>
-      <isset property="has.explicit.boot.classpath.j2se1.7"/>
-      <isset property="has.explicit.boot.classpath.j2se1.8"/>
-    </and>
-  </condition>
-  <available property="atLeastJDK8" classname="java.util.function.Predicate"/>
-
-  <!-- When boot.classpath.j2se* is missing, these will be the defaults: -->
-  <!-- Note: Target "dist" doesn't allow using these. -->
-  <property name="boot.classpath.j2se1.7" value="${sun.boot.class.path}" />
-  <property name="boot.classpath.j2se1.8" value="${sun.boot.class.path}" />
-  
-  <!-- For checking the correctness of the boot.classpath.j2se* -->
-  <available classpath="${boot.classpath.j2se1.7}"
-    classname="java.util.Objects" ignoresystemclasses="true" 
-    property="boot.classpath.j2se1.7.correct"
-  />
-  <available classpath="${boot.classpath.j2se1.8}"
-    classname="java.time.Instant" ignoresystemclasses="true" 
-    property="boot.classpath.j2se1.8.correct"
-  />
-  
-  <!-- Set up version/timestamp filters and the version property: -->
-  <tstamp>
-    <format property="timestampNice" pattern="yyyy-MM-dd'T'HH:mm:ss'Z'"
-        timezone="UTC" />
-    <format property="timestampInVersion" pattern="yyyyMMdd'T'HHmmss'Z'"
-        timezone="UTC" />
-  </tstamp>
-  <filter token="timestampInVersion" value="${timestampInVersion}" />
-  <filter token="timestampNice" value="${timestampNice}" />
-  <mkdir dir="build"/>
-  <!-- Copying is needed to substitute the timestamps. -->
-  <copy
-      file="src/main/resources/org/apache/freemarker/core/version.properties"
-      tofile="build/version.properties.tmp"
-      filtering="true"
-      overwrite="true"
-  />
-  <property file="build/version.properties.tmp" />
-  <delete file="build/version.properties.tmp" />
-  <filter token="version" value="${version}" />
-  
-  <property name="dist.dir" value="build/dist" />
-  <property name="dist.archiveBaseName" value="apache-${mavenArtifactId}-${version}" />
-  <property name="dist.bin.dir" value="${dist.dir}/bin/${dist.archiveBaseName}-bin" />
-  <property name="dist.src.dir" value="${dist.dir}/src/${dist.archiveBaseName}-src" />
-  
-  <!-- ================================================================== -->
-  <!-- Initialization                                                     -->
-  <!-- ================================================================== -->
-
-  
-  <target name="clean" description="get rid of all generated files">
-    <delete dir="build" />
-    <delete dir="META-INF" />
-  </target>
-
-  <target name="clean-classes" description="get rid of compiled classes">
-    <delete dir="build/classes" />
-    <delete dir="build/test-classes" />
-    <delete dir="build/coverage/classes" />
-  </target>
-
-  <condition property="deps.available">
-    <available file=".ivy" />
-  </condition>
-  
-  <target name="init" depends="_autoget-deps"
-    description="Fetch dependencies if any are missing and create the build directory if necessary"
-  >
-    <mkdir dir="build"/>
-  </target>
-
-  <property name="ivy.install.version" value="2.4.0" />
-  <property name="ivy.home" value="${user.home}/.ant" />
-  <property name="ivy.jar.dir" value="${ivy.home}/lib" />
-  <property name="ivy.jar.file" value="${ivy.jar.dir}/ivy.jar" />
-  
-  <target name="download-ivy">
-    <mkdir dir="${ivy.jar.dir}"/>
-    <get src="https://repo1.maven.org/maven2/org/apache/ivy/ivy/${ivy.install.version}/ivy-${ivy.install.version}.jar"
-         dest="${ivy.jar.file}" usetimestamp="true"/>
-  </target>  
-  
-  <!-- ================================================================= -->
-  <!-- Compilation                                                       -->
-  <!-- ================================================================= -->
-  
-  <target name="javacc" depends="init" unless="parser.uptodate"
-    description="Build the parser from its grammar file"
-  >
-    <ivy:cachepath conf="parser" pathid="ivy.dep" />
-    <taskdef name="generate" classname="org.apache.tools.ant.taskdefs.optional.javacc.JavaCC"
-      uri="http://javacc.dev.java.net/"
-      classpathref="ivy.dep"
-    />
-    
-    <property name="_javaccOutputDir"
-      value="build/generated-sources/java/org/apache/freemarker/core"
-    />
-
-    <mkdir dir="${_javaccOutputDir}" />
-    <ivy:retrieve conf="parser" pattern="build/javacc-home.tmp/[artifact].[ext]" />
-    <javacc:generate
-      target="src/main/javacc/FTL.jj"
-      outputdirectory="${_javaccOutputDir}"
-      javacchome="build/javacc-home.tmp"
-    />
-    <delete dir="build/javacc-home.tmp" />
-    
-    <replace
-      file="${_javaccOutputDir}/FMParser.java"
-      token="private final LookaheadSuccess"
-      value="private static final LookaheadSuccess"
-    />
-    <replace
-      file="${_javaccOutputDir}/FMParserConstants.java"
-      token="public interface FMParserConstants"
-      value="interface FMParserConstants"
-    />
-    <replace
-      file="${_javaccOutputDir}/FMParserTokenManager.java"
-      token="public class FMParserTokenManager"
-      value="class FMParserTokenManager"
-    />
-    <replace
-      file="${_javaccOutputDir}/Token.java"
-      token="public class Token"
-      value="class Token"
-    />
-    <replace
-      file="${_javaccOutputDir}/SimpleCharStream.java"
-      token="public final class SimpleCharStream"
-      value="final class SimpleCharStream"
-    />
-    <replace
-      file="${_javaccOutputDir}/FMParser.java"
-      token="enum"
-      value="ENUM"
-    />
-    
-    <!-- As we have a modified version in src/main/java: -->
-    <move 
-      file="${_javaccOutputDir}/ParseException.java"
-      tofile="${_javaccOutputDir}/ParseException.java.ignore"
-    />
-    <move 
-      file="${_javaccOutputDir}/TokenMgrError.java"
-      tofile="${_javaccOutputDir}/TokenMgrError.java.ignore"
-    />
-  </target>
-   
-  <target name="compile" depends="javacc">
-    <fail unless="boot.classpath.j2se1.7.correct"><!--
-      -->The "boot.classpath.j2se1.7" property value (${boot.classpath.j2se1.7}) <!--
-      -->seems to be an incorrect boot classpath. Please fix it in <!--
-      -->the &lt;projectDir>/build.properties file, or wherever you <!--
-      -->set it.<!--
-    --></fail>
-    <fail unless="boot.classpath.j2se1.8.correct"><!--
-      -->The "boot.classpath.j2se1.8" property value (${boot.classpath.j2se1.8}) <!--
-      -->seems to be an incorrect boot classpath. Please fix it in <!--
-      -->the &lt;projectDir>/build.properties file, or wherever you <!--
-      -->set it.<!--
-    --></fail>
-    <echo level="info"><!--
-      -->Using boot classpaths:<!--
-      -->Java 7: ${boot.classpath.j2se1.7}; <!--
-      -->Java 8: ${boot.classpath.j2se1.8}; <!--
-    --></echo>
-
-    <!-- Comment out @SuppressFBWarnings, as it causes compilation warnings in dependent Gradle projects -->    
-    <delete dir="build/src-main-java-filtered" />
-    <mkdir dir="build/src-main-java-filtered" />
-    <copy toDir="build/src-main-java-filtered">
-      <fileset dir="src/main/java" />
-    </copy>
-    <replaceregexp
-        flags="gs" encoding="utf-8"
-        match='(@SuppressFBWarnings\(.+?"\s*\))' replace="/*\1*/"
-    >
-      <fileset dir="build/src-main-java-filtered" includes="**/*.java" />
-    </replaceregexp>
-    <replaceregexp
-        flags="gs" encoding="utf-8"
-        match='(import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;)' replace="// \1"
-    >
-      <fileset dir="build/src-main-java-filtered" includes="**/*.java" />
-    </replaceregexp>
-    
-    <mkdir dir="build/classes" />
-
-    <!-- Note: the "build.base" conf doesn't include optional FreeMarker dependencies. -->
-    <ivy:cachepath conf="build.base" pathid="ivy.dep" />
-    <javac destdir="build/classes" deprecation="off" 
-      debug="on" optimize="off" target="1.7" source="1.7" encoding="utf-8"
-      includeantruntime="false"
-      classpathref="ivy.dep"
-      bootclasspath="${boot.classpath.j2se1.7}"
-      excludes="
-        org/apache/freemarker/core/_Java?*Impl.java,
-        org/apache/freemarker/servlet/**"
-    >
-      <src>
-        <pathelement location="build/src-main-java-filtered" />
-        <pathelement location="build/generated-sources" />
-      </src>
-    </javac>
-    
-    <ivy:cachepath conf="build.base" pathid="ivy.dep" />
-    <javac srcdir="build/src-main-java-filtered" destdir="build/classes" deprecation="off" 
-      debug="on" optimize="off" target="1.8" source="1.8" encoding="utf-8"
-      includeantruntime="false"
-      classpathref="ivy.dep"
-      bootclasspath="${boot.classpath.j2se1.8}"
-      includes="org/apache/freemarker/core/_Java8Impl.java"
-    />
-    
-    <rmic
-      base="build/classes" includes="org/apache/freemarker/core/debug/impl/Rmi*Impl.class"
-      classpathref="ivy.dep"
-      verify="yes" stubversion="1.2"
-    />
-
-    <ivy:cachepath conf="build.jsp2.1" pathid="ivy.dep.jsp2.1" />
-    <javac srcdir="build/src-main-java-filtered" destdir="build/classes" deprecation="off" 
-      debug="on" optimize="off" target="1.7" source="1.7" encoding="utf-8"
-      includeantruntime="false"
-      classpathref="ivy.dep.jsp2.1"
-      bootclasspath="${boot.classpath.j2se1.7}"
-      includes="
-        org/apache/freemarker/servlet/**"
-    />
-        
-    <rmic base="build/classes" classpathref="ivy.dep"
-      includes="build/src-main-java-filtered/org/apache/freemarker/core/debug/Rmi*Impl.class"
-      verify="yes" stubversion="1.2"
-    />
-    
-    <copy toDir="build/classes">
-      <fileset dir="src/main/resources"
-        excludes="
-          org/apache/freemarker/core/version.properties"
-      />
-    </copy>
-    <copy toDir="build/classes" filtering="true" overwrite="true">
-      <fileset dir="src/main/resources"
-        includes="
-      		org/apache/freemarker/core/version.properties"
-      />
-    </copy>
-    <copy toDir="build/classes/META-INF">
-      <fileset dir="." includes="DISCLAIMER" />
-    </copy>
-    <copy toDir="build/classes/META-INF">
-      <fileset dir="src/dist/jar/META-INF" includes="*" />
-    </copy>
-    
-    <delete dir="build/src-main-java-filtered" />
-  </target>
-
-  <target name="compileTest" depends="compile">
-    <mkdir dir="build/test-classes" />
-  
-    <ivy:cachepath conf="build.test" pathid="ivy.dep.build.test" />
-    <javac srcdir="src/test/java" destdir="build/test-classes" deprecation="off" 
-      debug="on" optimize="off" target="1.8" source="1.8" encoding="utf-8"
-      includeantruntime="false"
-      classpath="build/classes"
-      classpathref="ivy.dep.build.test"
-      bootclasspath="${boot.classpath.j2se1.8}"
-    />
-    <copy toDir="build/test-classes">
-      <fileset dir="src/test/resources"
-        excludes=""
-      />
-    </copy>
-  </target>
-   
-   <target name="jar" depends="compile">
-    <ivy:cachepath pathid="ivy.dep" conf="bnd" />
-    <taskdef resource="aQute/bnd/ant/taskdef.properties"
-      uri="http://www.aqute.biz/bnd"
-      classpathref="ivy.dep"
-    />
-  
-    <bnd:bnd
-        files="osgi.bnd" eclipse="false"
-        output="build/freemarker.jar"
-    />
-  </target>
-
-  <!-- ================================================================= -->
-  <!-- Testing                                                           -->
-  <!-- ================================================================= -->
-
-  <target name="test" depends="compileTest" description="Run test cases">
-    <mkdir dir="build/junit-reports" />
-    <ivy:cachepath conf="run.test" pathid="ivy.dep.run.test" />
-    <junit haltonfailure="on" fork="true" forkmode="once">
-      <classpath>
-        <pathelement path="build/test-classes" />
-        <pathelement path="build/classes" />
-        <path refid="ivy.dep.run.test" />
-      </classpath>
-      <formatter type="plain" />
-      <formatter type="xml" />
-      <batchtest todir="build/junit-reports">
-        <fileset dir="src/test/java">
-          <include name="**/*Test.java" />
-          <include name="**/*TestSuite.java" />
-          <exclude name="**/Abstract*.java" />
-        </fileset>
-      </batchtest>
-    </junit>
-  </target>
-  
-  <!-- ================================================================= -->
-  <!-- Generate docs                                                     -->
-  <!-- ================================================================= -->
-
-  <target name="_rawJavadoc" depends="compile">
-    <mkdir dir="build/api" />
-    <delete includeEmptyDirs="yes">
-      <fileset dir="build/api" includes="**/*" />
-    </delete>
-    <!-- javadoc with <fileset> has bugs, so we invoke a filtered copy: -->
-    <copy todir="build/javadoc-sources">
-      <fileset dir="src/main/java">
-        <exclude name="**/_*.java" />
-        <exclude name="**/SunInternalXalanXPathSupport.java" />
-        <!-- Remove classes that are, I suppose, only accidentally public: -->
-        <exclude name="**/core/LocalContext.java" />
-        <exclude name="**/core/CollectionAndSequence.java" />
-        <exclude name="**/core/Comment.java" />
-        <exclude name="**/core/DebugBreak.java" />
-        <exclude name="**/core/Expression.java" />
-        <exclude name="**/core/LibraryLoad.java" />
-        <exclude name="**/core/Macro.java" />
-        <exclude name="**/core/ReturnInstruction.java" />
-        <exclude name="**/core/StringArraySequence.java" />
-        <exclude name="**/core/TemplateElement.java" />
-        <exclude name="**/core/TemplateObject.java" />
-        <exclude name="**/core/TextBlock.java" />
-        <exclude name="**/core/ReturnInstruction.java" />
-        <exclude name="**/core/TokenMgrError.java" />
-        <exclude name="**/template/EmptyMap.java" />
-        <exclude name="**/log/SLF4JLoggerFactory.java" />
-        <exclude name="**/log/CommonsLoggingLoggerFactory.java" />
-      </fileset>
-    </copy>
-    
-    <!-- conf="IDE": as that has to contain all depedencies -->
-    <ivy:cachepath conf="IDE" pathid="ivy.dep" />
-    <javadoc
-      sourcepath="build/javadoc-sources"
-      destdir="build/api"
-      doctitle="FreeMarker ${version}"
-      use="true"
-      version="true"
-      author="true"
-      windowtitle="FreeMarker ${version} API"
-      classpath="build/classes"
-      classpathref="ivy.dep"
-      failonerror="true"
-      charset="UTF-8"
-      docencoding="UTF-8"
-      locale="en_US"
-    >
-      <link href="http://docs.oracle.com/javase/8/docs/api/"/>
-    </javadoc>
-    <delete dir="build/javadoc-sources" />
-  </target>
-
-  <target name="javadoc" depends="_rawJavadoc, _fixJDK8JavadocCSS" description="Build the JavaDocs" />
-  
-  <target name="_fixJDK8JavadocCSS" depends="_rawJavadoc" if="atLeastJDK8">
-    <property name="file" value="build/api/stylesheet.css" />
-        
-    <available file="${file}" property="stylesheet.available"/>
-    <fail unless="stylesheet.available">CSS file not found: ${file}</fail>
-    <echo>Fixing JDK 8 CSS in ${file}</echo>
-    
-    <!-- Tell that it's modified: -->
-    <replaceregexp
-        file="${file}" flags="gs" encoding="utf-8"
-        match="/\* (Javadoc style sheet) \*/" replace="/\* \1 - JDK 8 usability fix regexp substitutions applied \*/"
-    />
-
-    <!-- Remove broken link: -->
-    <replaceregexp
-        file="${file}" flags="gs" encoding="utf-8"
-        match="@import url\('resources/fonts/dejavu.css'\);\s*" replace=""
-    />
-    
-    <!-- Font family fixes: -->
-    <replaceregexp
-        file="${file}" flags="gsi" encoding="utf-8"
-        match="['&quot;]DejaVu Sans['&quot;]" replace="Arial"
-    />
-    <replaceregexp
-        file="${file}" flags="gsi" encoding="utf-8"
-        match="['&quot;]DejaVu Sans Mono['&quot;]" replace="'Courier New'"
-    />
-    <replaceregexp
-        file="${file}" flags="gsi" encoding="utf-8"
-        match="['&quot;]DejaVu Serif['&quot;]" replace="Arial"
-    />
-    <replaceregexp
-        file="${file}" flags="gsi" encoding="utf-8"
-        match="(?&lt;=[\s,:])serif\b" replace="sans-serif"
-    />
-    <replaceregexp
-        file="${file}" flags="gsi" encoding="utf-8"
-        match="(?&lt;=[\s,:])Georgia,\s*" replace=""
-    />
-    <replaceregexp
-        file="${file}" flags="gsi" encoding="utf-8"
-        match="['&quot;]Times New Roman['&quot;],\s*" replace=""
-    />
-    <replaceregexp
-        file="${file}" flags="gsi" encoding="utf-8"
-        match="(?&lt;=[\s,:])Times,\s*" replace=""
-    />
-    <replaceregexp
-        file="${file}" flags="gsi" encoding="utf-8"
-        match="(?&lt;=[\s,:])Arial\s*,\s*Arial\b" replace="Arial"
-    />
-    
-    <!-- "Parameters:", "Returns:", "Throws:", "Since:", "See also:" etc. fixes: -->
-    <property name="ddSelectorStart" value="(?:\.contentContainer\s+\.(?:details|description)|\.serializedFormContainer)\s+dl\s+dd\b.*?\{[^\}]*\b" />
-    <property name="ddPropertyEnd" value="\b.+?;" />
-    <!-- - Put back description (dd) indentation: -->
-    <replaceregexp
-        file="${file}" flags="gs" encoding="utf-8"
-        match="(${ddSelectorStart})margin${ddPropertyEnd}" replace="\1margin: 5px 0 10px 20px;"
-    />
-    <!-- - No monospace font for the description (dd) part: -->
-    <replaceregexp
-        file="${file}" flags="gs" encoding="utf-8"
-        match="(${ddSelectorStart})font-family${ddPropertyEnd}" replace="\1"
-    />
-  </target>
-  
-  <!-- ====================== -->
-  <!-- Manual                 -->
-  <!-- ====================== -->
-  
-  <macrodef name="manual" uri="http://freemarker.org/util">
-    <attribute name="offline" />
-    <attribute name="locale" />
-    <sequential>
-      <ivy:cachepath conf="manual" pathid="ivy.dep" />
-      <taskdef resource="org/freemarker/docgen/antlib.properties"
-        uri="http://freemarker.org/docgen"
-        classpathref="ivy.dep"
-      />
-      
-      <docgen:transform
-        srcdir="src/manual/@{locale}" destdir="build/manual/@{locale}"
-        offline="@{offline}"
-      />
-    </sequential>
-  </macrodef>
-  
-  <target name="manualOffline" depends="init" description="Build the Manual for offline use" >
-    <u:manual offline="true" locale="en_US" />
-  </target>
-
-  <target name="manualFreemarkerOrg" depends="init" description="Build the Manual to be upload to freemarker.org" >
-    <u:manual offline="false" locale="en_US" />
-  </target>
-  
-  <target name="manualOffline_zh_CN" depends="init" description="Build the Manual for offline use" >
-    <u:manual offline="true" locale="zh_CN" />
-  </target>
-
-  <target name="manualFreemarkerOrg_zh_CN" depends="init" description="Build the Manual to be upload to freemarker.org">
-    <u:manual offline="false" locale="zh_CN" />
-  </target>
-  
-
-  <!-- ====================== -->
-  <!-- Distributuion building -->
-  <!-- ====================== -->
-
-  <target name="dist"
-    description="Build the FreeMarker distribution files"
-  >
-    <fail
-      unless="has.all.explicit.boot.classpaths"
-      message="All boot.classpath properties must be set in build.properties for dist!"
-    />
-    <fail unless="atLeastJDK8" message="The release should be built with JDK 8+ (you may need to set JAVA_HOME)" />
-    <antcall target="clean" />  <!-- To improve the reliability -->
-    <antcall target="_dist" />
-  </target>
-  
-  <target name="_dist"
-    depends="jar, test, javadoc, manualOffline"
-  >
-    <delete dir="${dist.dir}" />
-
-    <!-- ..................................... -->
-    <!-- Binary distribution                   -->
-    <!-- ..................................... -->
-    
-    <mkdir dir="${dist.bin.dir}" />
-    
-    <!-- Copy txt-s -->
-    <copy todir="${dist.bin.dir}" includeEmptyDirs="no">
-      <fileset dir="." defaultexcludes="no">
-        <include name="README.md" />
-        <!-- LICENSE is binary-distribution-specific, and is copied later. -->
-        <include name="NOTICE" />
-        <include name="DISCLAIMER" />
-        <include name="RELEASE-NOTES" />
-      </fileset>
-    </copy>
-    <replace
-      file="${dist.bin.dir}/README.md"
-      token="{version}"
-      value="${version}"
-    />
-    <!-- Copy binary-distribution-specific files: -->
-    <copy todir="${dist.bin.dir}/">
-      <fileset dir="src/dist/bin/" />
-    </copy>
-
-    <!-- Copy binary -->
-    <copy file="build/freemarker.jar" tofile="${dist.bin.dir}/freemarker.jar" />
-
-    <!-- Copy documentation -->
-    <mkdir dir="${dist.bin.dir}/documentation" />
-    
-    <!--
-      The US English Manual is the source of any translations and thus it's the
-      only one that is guaranteed to be up to date when doing the release, so we
-      only pack that into it.
-    -->
-    <copy todir="${dist.bin.dir}/documentation/_html" includeEmptyDirs="no">
-      <fileset dir="build/manual/en_US" />
-    </copy>
-    <copy todir="${dist.bin.dir}/documentation/_html/api" includeEmptyDirs="no">
-      <fileset dir="build/api" />
-    </copy>
-    
-    <u:packageAndSignDist
-        srcDir="${dist.bin.dir}/.."
-        archiveNameWithoutExt="${dist.archiveBaseName}-bin"
-    />
-
-    <!-- ..................................... -->
-    <!-- Source distribution                   -->
-    <!-- ..................................... -->
-    
-    <mkdir dir="${dist.src.dir}" />
-
-    <!-- Copy extensionless files: -->
-    <copy todir="${dist.src.dir}" includeEmptyDirs="no">
-      <fileset dir="." defaultexcludes="no">
-        <include name="README.md" />
-        <include name="LICENSE" />
-        <include name="NOTICE" />
-        <include name="DISCLAIMER" />
-        <include name="RELEASE-NOTES" />
-      </fileset>
-    </copy>
-    <replace
-      file="${dist.src.dir}/README.md"
-      token="{version}"
-      value="${version}"
-    />
-    
-    <copy todir="${dist.src.dir}" includeEmptyDirs="no">
-      <fileset dir="." defaultexcludes="no">
-        <exclude name="**/*.bak" />
-        <exclude name="**/*.~*" />
-        <exclude name="**/*.*~" />
-        <include name="src/**" />
-        <include name="*.xml" />
-        <include name="*.sample" />
-        <include name="*.txt" />
-        <include name="osgi.bnd" />
-        <include name=".git*" />
-      </fileset>
-    </copy>
-    
-    <u:packageAndSignDist
-        srcDir="${dist.src.dir}/.."
-        archiveNameWithoutExt="${dist.archiveBaseName}-src"
-    />
-  </target>
-
-  <macrodef name="packageAndSignDist" uri="http://freemarker.org/util">
-    <attribute name="srcDir" />
-    <attribute name="archiveNameWithoutExt" />
-    <sequential>
-      <local name="archive.tar"/>
-      <property name="archive.tar" value="build/dist/@{archiveNameWithoutExt}.tar" />
-      <local name="archive.gzip"/>
-      <property name="archive.gzip" value="${archive.tar}.gz" />
-      <delete file="${archive.tar}" />
-      <tar tarfile="${archive.tar}" basedir="@{srcDir}" />
-      <delete file="${archive.gzip}" />
-      <gzip zipfile="${archive.gzip}" src="${archive.tar}" />
-      <delete file="${archive.tar}" />
-
-      <echo>Signing "${archive.gzip}"...</echo>
-      <!-- gpg may hang if it exists: -->
-      <delete file="${archive.gzip}.asc" />
-      <exec executable="${gpgCommand}" failonerror="true">
-        <arg value="--armor" />
-        <arg value="--output" />
-        <arg value="${archive.gzip}.asc" />
-        <arg value="--detach-sig" />
-        <arg value="${archive.gzip}" />
-      </exec>
-      
-      <echo>*** Signature verification: ***</echo>
-      <exec executable="${gpgCommand}" failonerror="true">
-        <arg value="--verify" />
-        <arg value="${archive.gzip}.asc" />
-        <arg value="${archive.gzip}" />
-      </exec>
-      <local name="signatureGood" />
-      <local name="signatureGood.y" />
-      <input
-         validargs="y,n"
-         addproperty="signatureGood"
-      >Is the above signer the intended one for Apache releases?</input>
-      <condition property="signatureGood.y">
-        <equals arg1="y" arg2="${signatureGood}"/>
-      </condition>
-      <fail unless="signatureGood.y" message="Task aborted by user." />
-    
-      <echo>Creating checksum files for "${archive.gzip}"...</echo>
-      <checksum file="${archive.gzip}" fileext=".md5" algorithm="MD5" forceOverwrite="yes" />
-      <checksum file="${archive.gzip}" fileext=".sha512" algorithm="SHA-512" forceOverwrite="yes" />
-    </sequential>
-  </macrodef>
-  
-  <target name="maven-pom">
-    <echo file="build/pom.xml"><![CDATA[<?xml version="1.0" encoding="utf-8"?>
-<!--
-  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.
--->
-    
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-  
-  <parent>
-    <groupId>org.apache</groupId>
-    <artifactId>apache</artifactId>
-    <version>17</version>
-  </parent>
-  
-  <groupId>${mavenGroupId}</groupId>
-  <artifactId>${mavenArtifactId}</artifactId>
-  <version>${mavenVersion}</version>
-  
-  <packaging>jar</packaging>
-  
-  <name>Apache FreeMarker</name>
-  <description>
-    FreeMarker is a "template engine"; a generic tool to generate text output based on templates.
-  </description>
-  <url>http://freemarker.org/</url>
-  <organization>
-    <name>Apache Software Foundation</name>
-    <url>http://apache.org</url>
-  </organization>
-  
-  <licenses>
-    <license>
-      <name>Apache License, Version 2.0</name>
-      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
-      <distribution>repo</distribution>      
-    </license>
-  </licenses>
-  
-  <scm>
-    <connection>scm:git:https://git-wip-us.apache.org/repos/asf/incubator-freemarker.git</connection>
-    <developerConnection>scm:git:https://git-wip-us.apache.org/repos/asf/incubator-freemarker.git</developerConnection>
-    <url>https://git-wip-us.apache.org/repos/asf?p=incubator-freemarker.git</url>
-    <tag>v${version}</tag>
-  </scm>
-
-  <issueManagement>
-    <system>jira</system>
-    <url>https://issues.apache.org/jira/browse/FREEMARKER/</url>
-  </issueManagement>
-
-  <mailingLists>
-    <mailingList>
-        <name>FreeMarker developer list</name>
-        <post>dev@freemarker.incubator.apache.org</post>
-        <subscribe>dev-subscribe@freemarker.incubator.apache.org</subscribe>
-        <unsubscribe>dev-unsubscribe@freemarker.incubator.apache.org</unsubscribe>
-        <archive>http://mail-archives.apache.org/mod_mbox/incubator-freemarker-dev/</archive>
-    </mailingList>
-    <mailingList>
-        <name>FreeMarker commit and Jira notifications list</name>
-        <post>notifications@freemarker.incubator.apache.org</post>
-        <subscribe>notifications-subscribe@freemarker.incubator.apache.org</subscribe>
-        <unsubscribe>notifications-unsubscribe@freemarker.incubator.apache.org</unsubscribe>
-        <archive>http://mail-archives.apache.org/mod_mbox/incubator-freemarker-notifications/</archive>
-    </mailingList>
-    <mailingList>
-      <name>FreeMarker management private</name>
-      <post>private@freemarker.incubator.apache.org</post>
-    </mailingList>
-  </mailingLists>
-  
-  <dependencies>
-    <dependency>
-      <groupId>org.slf4j</groupId>
-      <artifactId>slf4j-api</artifactId>
-      <version>1.7.22</version>
-    </dependency>
-  </dependencies>
-</project>
-]]></echo>
-  </target>
-  
-  <!--
-    Uploads the freemarker.jar that is in the current DISTRIBUTION DIRECTORY
-    to a Maven repository (snapshot or central).
-    Use this after "dist" (without interleaving "clean").
-  -->
-  <target name="maven-dist" depends="maven-pom"
-      description="Releases the already built distro to a Maven repository">
-    <jar destfile="build/maven-source-attachment.jar">
-      <fileset dir="${dist.src.dir}/src/main/java" />
-      <fileset dir="${dist.src.dir}/src/main/resources" />
-      <fileset dir="${dist.src.dir}/src/main/javacc/" />
-      <fileset dir="build/generated-sources/java/" includes="**/*.java" />
-      <metainf dir="${dist.src.dir}" includes="LICENSE, NOTICE, DISCLAIMER" />
-    </jar>
-
-    <mkdir dir="build/javadoc-attachment-metainf"/>
-    <copy todir="build/javadoc-attachment-metainf">
-      <fileset dir="${dist.bin.dir}" includes="DISCLAIMER, NOTICE" />
-    </copy>
-    <copy todir="build/javadoc-attachment-metainf">
-      <fileset dir="src/dist/javadoc/META-INF/" />
-    </copy>
-    <jar destfile="build/maven-javadoc-attachment.jar">
-      <fileset dir="${dist.bin.dir}/documentation/_html/api" />
-      <metainf dir="build/javadoc-attachment-metainf" includes="**/*" />
-    </jar>
-    <delete dir="build/javadoc-attachment-metainf" />
-
-    <!-- These were copy-pasted from the org.apacha:apache parent POM: -->
-    <property name="maven-server-id" value="apache.releases.https" />
-    <condition property="maven-repository-url"
-        value="https://repository.apache.org/content/repositories/snapshots/"
-        else="https://repository.apache.org/service/local/staging/deploy/maven2">
-      <matches pattern="-SNAPSHOT$" string="${mavenVersion}" />
-    </condition>
-    <!-- Snapshot repo: https://repository.apache.org/content/repositories/snapshots/ -->
-    <input
-       validargs="y,n"
-       addproperty="mavenUpload.answer"
-    >
-You are about uploading
-${dist.bin.dir}/freemarker.jar
-and its attachments with Maven coordinates
-${mavenGroupId}:${mavenArtifactId}:${mavenVersion}
-to this Maven repository:
-${maven-repository-url}
-
-Note that it's assumed that you have run `ant dist` just before this.
-Proceed? </input>
-    <condition property="mavenUpload.yes">
-      <equals arg1="y" arg2="${mavenUpload.answer}"/>
-    </condition>
-    <fail unless="mavenUpload.yes" message="Task aborted by user." />
-    
-		<!-- Sign and deploy the main artifact -->
-		<exec executable="${mvnCommand}" failonerror="true">
-			<arg value="org.apache.maven.plugins:maven-gpg-plugin:1.3:sign-and-deploy-file" />
-      <!--
-        As we use the gpg plugin instead of a normal Maven "deploy", sadly we can't just
-        inherit the repo URL and repositoryId from the parent POM.
-      -->
-			<arg value="-Durl=${maven-repository-url}" />
-			<arg value="-DrepositoryId=${maven-server-id}" />
-			<arg value="-DpomFile=build/pom.xml" />
-			<arg value="-Dfile=${dist.bin.dir}/freemarker.jar" />
-      <arg value="-Pgpg" />
-		</exec>
-
-		<!-- Sign and deploy the sources artifact -->
-		<exec executable="${mvnCommand}" failonerror="true">
-			<arg value="org.apache.maven.plugins:maven-gpg-plugin:1.3:sign-and-deploy-file" />
-			<arg value="-Durl=${maven-repository-url}" />
-			<arg value="-DrepositoryId=${maven-server-id}" />
-			<arg value="-DpomFile=build/pom.xml" />
-			<arg value="-Dfile=build/maven-source-attachment.jar" />
-			<arg value="-Dclassifier=sources" />
-      <arg value="-Pgpg" />
-		</exec>
-
-		<!-- Sign and deploy the javadoc artifact -->
-		<exec executable="${mvnCommand}" failonerror="true">
-			<arg value="org.apache.maven.plugins:maven-gpg-plugin:1.3:sign-and-deploy-file" />
-			<arg value="-Durl=${maven-repository-url}" />
-			<arg value="-DrepositoryId=${maven-server-id}" />
-			<arg value="-DpomFile=build/pom.xml" />
-			<arg value="-Dfile=build/maven-javadoc-attachment.jar" />
-			<arg value="-Dclassifier=javadoc" />
-      <arg value="-Pgpg" />
-		</exec>
-    
-    <echo>*****************************************************************</echo>
-    <echo>Check the above lines for any Maven errors!</echo>
-    <echo>Now you need to close and maybe release the staged repo on</echo>
-    <echo>http://repository.apache.org.</echo>
-    <echo>Note that before releasing, voting is needed!</echo>
-    <echo>*****************************************************************</echo>
-  </target>
-
-  <!-- ================================================================= -->
-  <!-- CI (like Travis).......................                           -->
-  <!-- ================================================================= -->
-
-  <target name="ci"
-     depends="clean, update-deps, jar, test, javadoc"
-     description="CI should invoke this task"
-  />
-
-  <!-- ================================================================== -->
-  <!-- Dependency management (keep it exactly identical for all projects) -->
-  <!-- ================================================================== -->
-  
-  <target name="_autoget-deps" unless="deps.available">
-    <antcall target="update-deps" />
-  </target>
-  
-  <target name="update-deps"
-    description="Gets the latest version of the dependencies from the Web"
-  >
-    <echo>Getting dependencies...</echo>
-    <echo>-------------------------------------------------------</echo>
-    <ivy:settings id="remote" url="http://freemarker.org/repos/ivy/ivysettings-remote.xml" />
-    <!-- Build an own repository that will serve us even offline: -->
-    <ivy:retrieve settingsRef="remote" sync="true"
-      ivypattern=".ivy.part/repo/[organisation]/[module]/ivy-[revision].xml"
-      pattern=".ivy.part/repo/[organisation]/[module]/[artifact]-[revision].[ext]"
-    />
-    <echo>-------------------------------------------------------</echo>
-    <echo>*** Successfully acquired dependencies from the Web ***</echo>
-    <echo>Eclipse users: Now right-click on ivy.xml and Resolve! </echo>
-    <echo>-------------------------------------------------------</echo>
-    <!-- Only now that we got all the dependencies will we delete anything. -->
-    <!-- Thus a net or repo outage doesn't left us without the dependencies. -->
-
-    <!-- Save the resolution cache from the soon coming <delete>: -->
-    <move todir=".ivy.part/update-deps-reso-cache">
-      <fileset dir=".ivy/update-deps-reso-cache" />
-    </move>
-    <!-- Drop all the old stuff: -->
-    <delete dir=".ivy" />
-    <!-- And use the new stuff instead: -->
-    <move todir=".ivy">
-      <fileset dir=".ivy.part" />
-    </move>
-  </target>
-
-  <!-- Do NOT call this from 'clean'; offline guys would stuck after that. -->
-  <target name="clean-deps"
-    description="Deletes all dependencies"
-  >
-    <delete dir=".ivy" />
-  </target>
-
-  <target name="publish-override" depends="jar"
-    description="Ivy-publishes THIS project locally as an override"
-  >
-    <ivy:resolve />
-    <ivy:publish
-      pubrevision="${moduleBranch}-branch-head"
-      artifactspattern="build/[artifact].[ext]"
-      overwrite="true" forcedeliver="true"
-      resolver="freemarker-devel-local-override"
-    >
-      <artifact name="freemarker" type="jar" ext="jar" />
-    </ivy:publish>
-    <delete file="build/ivy.xml" />  <!-- ivy:publish makes this -->
-    <echo>-------------------------------------------------------</echo>
-    <echo>*** Don't forget to `ant unpublish-override` later! ***</echo>
-  </target>
-
-  <target name="unpublish-override"
-    description="Undoes publish-override (made in THIS project)"
-  >
-    <delete dir="${user.home}/.ivy2/freemarker-devel-local-override/${moduleOrg}/${moduleName}" />
-    <delete dir="${user.home}/.ivy2/freemarker-devel-local-override-cache/${moduleOrg}/${moduleName}" />
-  </target>  
-
-  <target name="unpublish-override-all"
-    description="Undoes publish-override-s made in ALL projects"
-  >
-    <delete dir="${user.home}/.ivy2/freemarker-devel-local-override" />
-    <delete dir="${user.home}/.ivy2/freemarker-devel-local-override-cache" />
-  </target>  
-
-  <target name="uninstall"
-    description="Deletes external files created by FreeMarker developement"
-  >
-    <delete dir="${user.home}/.ivy2/freemarker-devel-cache" />
-    <delete dir="${user.home}/.ivy2/freemarker-devel-local-override" />
-    <delete dir="${user.home}/.ivy2/freemarker-devel-local-override-cache " />
-  </target>
-
-  <target name="report-deps"
-    description="Creates a HTML document that summarizes the dependencies."
-  >
-    <mkdir dir="build/deps-report" />
-    <ivy:resolve />
-    <ivy:report todir="build/deps-report" />
-  </target>
-  
-  <target name="report-ide-deps"
-    description="Creates a HTML document that summarizes the dependencies."
-  >
-    <mkdir dir="build/ide-deps-report" />
-    <ivy:resolve conf="IDE" />
-    <ivy:report todir="build/ide-deps-report" />
-  </target>
-  
-  <target name="ide-dependencies" depends="jar"
-    description="If your IDE has no Ivy support, this generates ide-lib/*.jar for it">
-    <mkdir dir="ide-dependencies" />
-    <delete includeEmptyDirs="true">  
-      <fileset dir="ide-dependencies">  
-         <include name="*/**" />  
-      </fileset>  
-    </delete>    
-    <ivy:retrieve conf="IDE" pattern="ide-dependencies/[artifact]-[revision].[ext]" />
-
-    <!--
-      Extract META-INF/MANITSET.MF from freemarker.jar and put it into the project directory for Eclipse (this is
-      needed if you want to reference freemarker source code in the context of OSGI development with Eclipse)
-    -->
-    <unzip src="build/freemarker.jar" dest=".">
-      <patternset>
-        <include name="META-INF/*"/>
-        <exclude name="META-INF/LICENSE"/>
-        <exclude name="META-INF/DISCLAIMER"/>
-        <exclude name="META-INF/NOTICE"/>
-      </patternset>
-    </unzip>
-    <echo file="META-INF/DO-NOT-EDIT.txt"><!--
-      -->Do not edit the files in this directory! They are extracted from freemarker.jar as part of&#x0a;<!--
-      -->the ide-dependencies Ant task, because Eclipse OSGi support expects them to be here.<!--
-    --></echo>
-  </target>
-  
-  <!--
-    This meant to be called on the Continuous Integration server, so the
-    integration builds appear in the freemarker.org public Ivy repository.
-    The artifacts must be already built.
-  -->
-  <target name="server-publish-last-build"
-    description="(For the Continuous Integration server only)"
-  >
-    <delete dir="build/dummy-server-ivy-repo" />
-    <ivy:resolve />
-    <ivy:publish
-      pubrevision="${moduleBranch}-branch-head"
-      artifactspattern="build/[artifact].[ext]"
-      overwrite="true" forcedeliver="true"
-      resolver="server-publishing-target"
-    >
-      <artifact name="freemarker" type="jar" ext="jar" />
-    </ivy:publish>
-    <delete file="build/ivy.xml" />  <!-- ivy:publish makes this -->
-  </target>
-  
-  <target name="rat" description="Generates Apache RAT report">
-    <ivy:cachepath conf="rat" pathid="ivy.dep" />
-    <taskdef
-      uri="antlib:org.apache.rat.anttasks"
-      resource="org/apache/rat/anttasks/antlib.xml"
-      classpathref="ivy.dep"
-    />  
-    
-    <rat:report reportFile="build/rat-report-src.txt">
-        <fileset dir="src"/>
-    </rat:report>
-    <rat:report reportFile="build/rat-report-dist-src.txt">
-        <fileset dir="build/dist/src"/>
-    </rat:report>
-    <rat:report reportFile="build/rat-report-dist-bin.txt">
-        <fileset dir="build/dist/bin"/>
-    </rat:report>
-    <echo level="info"><!--
-    -->Rat reports were written into build/rat-report-*.txt<!--
-    --></echo>
-  </target>
-
-  <target name="archive" depends=""
-    description='Archives project with Git repo into the "archive" directory.'
-  >
-    <mkdir dir="archive" />
-    <tstamp>
-      <format property="tstamp" pattern="yyyyMMdd-HHmm" />
-    </tstamp>
-    <delete file="archive/freemarker-git-${tstamp}.tar" />
-    <delete file="archive/freemarker-git-${tstamp}.tar.bz2" />
-    <tar tarfile="archive/freemarker-git-${tstamp}.tar"
-      basedir="."
-      longfile="gnu"
-      excludes="build/** .build/** .bin/** .ivy/**  archive/**"
-    />
-    <bzip2 src="archive/freemarker-git-${tstamp}.tar"
-        zipfile="archive/freemarker-git-${tstamp}.tar.bz2" />
-    <delete file="archive/freemarker-git-${tstamp}.tar" />
-  </target>
-    
-</project>

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core-java8-test/build.gradle
----------------------------------------------------------------------
diff --git a/freemarker-core-java8-test/build.gradle b/freemarker-core-java8-test/build.gradle
new file mode 100644
index 0000000..ad08d33
--- /dev/null
+++ b/freemarker-core-java8-test/build.gradle
@@ -0,0 +1,19 @@
+// Override inherited default Java version:
+sourceCompatibility = "1.8"
+targetCompatibility = "1.8"
+[compileJava, compileTestJava]*.options*.bootClasspath = bootClasspathJava8
+
+dependencies {
+    compile project(":freemarker-core")
+}
+
+// We have nothing to put into the jar, as we have test classes only
+jar.enabled = false
+
+javadoc.enabled = false
+
+// Must not be deployed to a public Maven repository
+uploadArchives.enabled = false
+
+// Doesn't make sense to Maven "install" this, as the artifact won't contain test classes
+install.enabled = false

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core-java8-test/src/main/resources/META-INF/DISCLAIMER
----------------------------------------------------------------------
diff --git a/freemarker-core-java8-test/src/main/resources/META-INF/DISCLAIMER b/freemarker-core-java8-test/src/main/resources/META-INF/DISCLAIMER
new file mode 100644
index 0000000..569ba05
--- /dev/null
+++ b/freemarker-core-java8-test/src/main/resources/META-INF/DISCLAIMER
@@ -0,0 +1,8 @@
+Apache FreeMarker is an effort undergoing incubation at The Apache Software
+Foundation (ASF), sponsored by the Apache Incubator. Incubation is required of
+all newly accepted projects until a further review indicates that the
+infrastructure, communications, and decision making process have stabilized in
+a manner consistent with other successful ASF projects. While incubation
+status is not necessarily a reflection of the completeness or stability of the
+code, it does indicate that the project has yet to be fully endorsed by the
+ASF.
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core-java8-test/src/main/resources/META-INF/LICENSE
----------------------------------------------------------------------
diff --git a/freemarker-core-java8-test/src/main/resources/META-INF/LICENSE b/freemarker-core-java8-test/src/main/resources/META-INF/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/freemarker-core-java8-test/src/main/resources/META-INF/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed 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.

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/BridgeMethodsBean.java
----------------------------------------------------------------------
diff --git a/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/BridgeMethodsBean.java b/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/BridgeMethodsBean.java
new file mode 100644
index 0000000..2c9d4e9
--- /dev/null
+++ b/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/BridgeMethodsBean.java
@@ -0,0 +1,30 @@
+/*
+ * 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;
+
+public class BridgeMethodsBean extends BridgeMethodsBeanBase<String> {
+
+    static final String M1_RETURN_VALUE = "m1ReturnValue"; 
+    
+    @Override
+    public String m1() {
+        return M1_RETURN_VALUE;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/BridgeMethodsBeanBase.java
----------------------------------------------------------------------
diff --git a/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/BridgeMethodsBeanBase.java b/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/BridgeMethodsBeanBase.java
new file mode 100644
index 0000000..4ecec7c
--- /dev/null
+++ b/freemarker-core-java8-test/src/test/java/org/apache/freemarker/core/model/impl/BridgeMethodsBeanBase.java
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+public abstract class BridgeMethodsBeanBase<T> {
+
+    public abstract T m1();
+    
+    public T m2() {
+        return null;
+    }
+    
+}



[39/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/Configuration.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/Configuration.java b/freemarker-core/src/main/java/org/apache/freemarker/core/Configuration.java
new file mode 100644
index 0000000..710cad3
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/Configuration.java
@@ -0,0 +1,2616 @@
+/*
+ * 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.InputStream;
+import java.io.Serializable;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.freemarker.core.arithmetic.ArithmeticEngine;
+import org.apache.freemarker.core.arithmetic.impl.BigDecimalArithmeticEngine;
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.impl.DefaultObjectWrapper;
+import org.apache.freemarker.core.outputformat.MarkupOutputFormat;
+import org.apache.freemarker.core.outputformat.OutputFormat;
+import org.apache.freemarker.core.outputformat.UnregisteredOutputFormatException;
+import org.apache.freemarker.core.outputformat.impl.CSSOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.CombinedMarkupOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.HTMLOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.JSONOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.JavaScriptOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.PlainTextOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.RTFOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.UndefinedOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.XHTMLOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.XMLOutputFormat;
+import org.apache.freemarker.core.templateresolver.CacheStorage;
+import org.apache.freemarker.core.templateresolver.GetTemplateResult;
+import org.apache.freemarker.core.templateresolver.MalformedTemplateNameException;
+import org.apache.freemarker.core.templateresolver.TemplateConfigurationFactory;
+import org.apache.freemarker.core.templateresolver.TemplateLoader;
+import org.apache.freemarker.core.templateresolver.TemplateLookupContext;
+import org.apache.freemarker.core.templateresolver.TemplateLookupStrategy;
+import org.apache.freemarker.core.templateresolver.TemplateNameFormat;
+import org.apache.freemarker.core.templateresolver.TemplateResolver;
+import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateLookupStrategy;
+import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateNameFormat;
+import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateNameFormatFM2;
+import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateResolver;
+import org.apache.freemarker.core.templateresolver.impl.MruCacheStorage;
+import org.apache.freemarker.core.templateresolver.impl.SoftCacheStorage;
+import org.apache.freemarker.core.util.CaptureOutput;
+import org.apache.freemarker.core.util.CommonBuilder;
+import org.apache.freemarker.core.util.HtmlEscape;
+import org.apache.freemarker.core.util.NormalizeNewlines;
+import org.apache.freemarker.core.util.StandardCompress;
+import org.apache.freemarker.core.util.XmlEscape;
+import org.apache.freemarker.core.util._ClassUtil;
+import org.apache.freemarker.core.util._NullArgumentException;
+import org.apache.freemarker.core.util._SortedArraySet;
+import org.apache.freemarker.core.util._StringUtil;
+import org.apache.freemarker.core.util._UnmodifiableCompositeSet;
+import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+/**
+ * <b>The main entry point into the FreeMarker API</b>; encapsulates the configuration settings of FreeMarker,
+ * also serves as a central template-loading and caching service.
+ *
+ * <p>This class is meant to be used in a singleton pattern. That is, you create an instance of this at the beginning of
+ * the application life-cycle with {@link Configuration.Builder}, set its settings
+ * (either with the setter methods like {@link Configuration.Builder#setTemplateLoader(TemplateLoader)} or by loading a
+ * {@code .properties} file and use that with {@link Configuration.Builder#setSettings(Properties)}}), and then
+ * use that single instance everywhere in your application. Frequently re-creating {@link Configuration} is a typical
+ * and grave mistake from performance standpoint, as the {@link Configuration} holds the template cache, and often also
+ * the class introspection cache, which then will be lost. (Note that, naturally, having multiple long-lived instances,
+ * like one per component that internally uses FreeMarker is fine.)  
+ * 
+ * <p>The basic usage pattern is like:
+ * 
+ * <pre>
+ *  // Where the application is initialized; in general you do this ONLY ONCE in the application life-cycle!
+ *  Configuration cfg = new Configuration.Builder(VERSION_<i>X</i>_<i>Y</i>_<i>Z</i>));
+ *          .<i>someSetting</i>(...)
+ *          .<i>otherSetting</i>(...)
+ *          .build()
+ *  // VERSION_<i>X</i>_<i>Y</i>_<i>Z</i> enables the not-100%-backward-compatible fixes introduced in
+ *  // FreeMarker version X.Y.Z and earlier (see {@link Configuration#getIncompatibleImprovements()}).
+ *  ...
+ *  
+ *  // Later, whenever the application needs a template (so you may do this a lot, and from multiple threads):
+ *  {@link Template Template} myTemplate = cfg.{@link #getTemplate(String) getTemplate}("myTemplate.html");
+ *  myTemplate.{@link Template#process(Object, java.io.Writer) process}(dataModel, out);</pre>
+ * 
+ * <p>A couple of settings that you should not leave on its default value are:
+ * <ul>
+ *   <li>{@link #getTemplateLoader templateLoader}: The default value is {@code null}, so you won't be able to load
+ *       anything.
+ *   <li>{@link #getSourceEncoding sourceEncoding}: The default value is system dependent, which makes it
+ *       fragile on servers, so it should be set explicitly, like to "UTF-8" nowadays. 
+ *   <li>{@link #getTemplateExceptionHandler() templateExceptionHandler}: For developing
+ *       HTML pages, the most convenient value is {@link TemplateExceptionHandler#HTML_DEBUG_HANDLER}. For production,
+ *       {@link TemplateExceptionHandler#RETHROW_HANDLER} is safer to use.
+ * </ul>
+ * 
+ * <p>{@link Configuration} is thread-safe and (as of 3.0.0) immutable (apart from internal caches).
+ */
+public final class Configuration
+        implements TopLevelConfiguration, CustomStateScope {
+    
+    private static final String VERSION_PROPERTIES_PATH = "org/apache/freemarker/core/version.properties";
+
+    private static final String[] SETTING_NAMES_SNAKE_CASE = new String[] {
+        // Must be sorted alphabetically!
+        ExtendableBuilder.AUTO_ESCAPING_POLICY_KEY_SNAKE_CASE,
+        ExtendableBuilder.CACHE_STORAGE_KEY_SNAKE_CASE,
+        ExtendableBuilder.INCOMPATIBLE_IMPROVEMENTS_KEY_SNAKE_CASE,
+        ExtendableBuilder.LOCALIZED_LOOKUP_KEY_SNAKE_CASE,
+        ExtendableBuilder.NAMING_CONVENTION_KEY_SNAKE_CASE,
+        ExtendableBuilder.OUTPUT_FORMAT_KEY_SNAKE_CASE,
+        ExtendableBuilder.RECOGNIZE_STANDARD_FILE_EXTENSIONS_KEY_SNAKE_CASE,
+        ExtendableBuilder.REGISTERED_CUSTOM_OUTPUT_FORMATS_KEY_SNAKE_CASE,
+        ExtendableBuilder.SHARED_VARIABLES_KEY_SNAKE_CASE,
+        ExtendableBuilder.SOURCE_ENCODING_KEY_SNAKE_CASE,
+        ExtendableBuilder.TAB_SIZE_KEY_SNAKE_CASE,
+        ExtendableBuilder.TAG_SYNTAX_KEY_SNAKE_CASE,
+        ExtendableBuilder.TEMPLATE_CONFIGURATIONS_KEY_SNAKE_CASE,
+        ExtendableBuilder.TEMPLATE_LANGUAGE_KEY_SNAKE_CASE,
+        ExtendableBuilder.TEMPLATE_LOADER_KEY_SNAKE_CASE,
+        ExtendableBuilder.TEMPLATE_LOOKUP_STRATEGY_KEY_SNAKE_CASE,
+        ExtendableBuilder.TEMPLATE_NAME_FORMAT_KEY_SNAKE_CASE,
+        ExtendableBuilder.TEMPLATE_UPDATE_DELAY_KEY_SNAKE_CASE,
+        ExtendableBuilder.WHITESPACE_STRIPPING_KEY_SNAKE_CASE,
+    };
+
+    private static final String[] SETTING_NAMES_CAMEL_CASE = new String[] {
+        // Must be sorted alphabetically!
+        ExtendableBuilder.AUTO_ESCAPING_POLICY_KEY_CAMEL_CASE,
+        ExtendableBuilder.CACHE_STORAGE_KEY_CAMEL_CASE,
+        ExtendableBuilder.INCOMPATIBLE_IMPROVEMENTS_KEY_CAMEL_CASE,
+        ExtendableBuilder.LOCALIZED_LOOKUP_KEY_CAMEL_CASE,
+        ExtendableBuilder.NAMING_CONVENTION_KEY_CAMEL_CASE,
+        ExtendableBuilder.OUTPUT_FORMAT_KEY_CAMEL_CASE,
+        ExtendableBuilder.RECOGNIZE_STANDARD_FILE_EXTENSIONS_KEY_CAMEL_CASE,
+        ExtendableBuilder.REGISTERED_CUSTOM_OUTPUT_FORMATS_KEY_CAMEL_CASE,
+        ExtendableBuilder.SHARED_VARIABLES_KEY_CAMEL_CASE,
+        ExtendableBuilder.SOURCE_ENCODING_KEY_CAMEL_CASE,
+        ExtendableBuilder.TAB_SIZE_KEY_CAMEL_CASE,
+        ExtendableBuilder.TAG_SYNTAX_KEY_CAMEL_CASE,
+        ExtendableBuilder.TEMPLATE_CONFIGURATIONS_KEY_CAMEL_CASE,
+        ExtendableBuilder.TEMPLATE_LANGUAGE_KEY_CAMEL_CASE,
+        ExtendableBuilder.TEMPLATE_LOADER_KEY_CAMEL_CASE,
+        ExtendableBuilder.TEMPLATE_LOOKUP_STRATEGY_KEY_CAMEL_CASE,
+        ExtendableBuilder.TEMPLATE_NAME_FORMAT_KEY_CAMEL_CASE,
+        ExtendableBuilder.TEMPLATE_UPDATE_DELAY_KEY_CAMEL_CASE,
+        ExtendableBuilder.WHITESPACE_STRIPPING_KEY_CAMEL_CASE
+    };
+    
+    private static final Map<String, OutputFormat> STANDARD_OUTPUT_FORMATS;
+    static {
+        STANDARD_OUTPUT_FORMATS = new HashMap<>();
+        STANDARD_OUTPUT_FORMATS.put(UndefinedOutputFormat.INSTANCE.getName(), UndefinedOutputFormat.INSTANCE);
+        STANDARD_OUTPUT_FORMATS.put(HTMLOutputFormat.INSTANCE.getName(), HTMLOutputFormat.INSTANCE);
+        STANDARD_OUTPUT_FORMATS.put(XHTMLOutputFormat.INSTANCE.getName(), XHTMLOutputFormat.INSTANCE);
+        STANDARD_OUTPUT_FORMATS.put(XMLOutputFormat.INSTANCE.getName(), XMLOutputFormat.INSTANCE);
+        STANDARD_OUTPUT_FORMATS.put(RTFOutputFormat.INSTANCE.getName(), RTFOutputFormat.INSTANCE);
+        STANDARD_OUTPUT_FORMATS.put(PlainTextOutputFormat.INSTANCE.getName(), PlainTextOutputFormat.INSTANCE);
+        STANDARD_OUTPUT_FORMATS.put(CSSOutputFormat.INSTANCE.getName(), CSSOutputFormat.INSTANCE);
+        STANDARD_OUTPUT_FORMATS.put(JavaScriptOutputFormat.INSTANCE.getName(), JavaScriptOutputFormat.INSTANCE);
+        STANDARD_OUTPUT_FORMATS.put(JSONOutputFormat.INSTANCE.getName(), JSONOutputFormat.INSTANCE);
+    }
+
+    /** FreeMarker version 3.0.0 */
+    public static final Version VERSION_3_0_0 = new Version(3, 0, 0);
+    
+    /** The default of {@link #getIncompatibleImprovements()}, currently {@link #VERSION_3_0_0}. */
+    public static final Version DEFAULT_INCOMPATIBLE_IMPROVEMENTS = Configuration.VERSION_3_0_0;
+    
+    private static final Version VERSION;
+    static {
+        try {
+            Properties vp = new Properties();
+            InputStream ins = Configuration.class.getClassLoader()
+                    .getResourceAsStream(VERSION_PROPERTIES_PATH);
+            if (ins == null) {
+                throw new RuntimeException("Version file is missing.");
+            } else {
+                try {
+                    vp.load(ins);
+                } finally {
+                    ins.close();
+                }
+                
+                String versionString  = getRequiredVersionProperty(vp, "version");
+                
+                final Boolean gaeCompliant = Boolean.valueOf(getRequiredVersionProperty(vp, "isGAECompliant"));
+                
+                VERSION = new Version(versionString, gaeCompliant, null);
+            }
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to load and parse " + VERSION_PROPERTIES_PATH, e);
+        }
+    }
+
+    // Configuration-specific settings:
+
+    private final Version incompatibleImprovements;
+    private final DefaultTemplateResolver templateResolver;
+    private final boolean localizedLookup;
+    private final List<OutputFormat> registeredCustomOutputFormats;
+    private final Map<String, OutputFormat> registeredCustomOutputFormatsByName;
+    private final Map<String, Object> sharedVariables;
+    private final Map<String, TemplateModel> wrappedSharedVariables;
+
+    // ParsingConfiguration settings:
+
+    private final TemplateLanguage templateLanguage;
+    private final int tagSyntax;
+    private final int namingConvention;
+    private final boolean whitespaceStripping;
+    private final int autoEscapingPolicy;
+    private final OutputFormat outputFormat;
+    private final Boolean recognizeStandardFileExtensions;
+    private final int tabSize;
+    private final Charset sourceEncoding;
+
+    // ProcessingConfiguration settings:
+
+    private final Locale locale;
+    private final String numberFormat;
+    private final String timeFormat;
+    private final String dateFormat;
+    private final String dateTimeFormat;
+    private final TimeZone timeZone;
+    private final TimeZone sqlDateAndTimeTimeZone;
+    private final String booleanFormat;
+    private final TemplateExceptionHandler templateExceptionHandler;
+    private final ArithmeticEngine arithmeticEngine;
+    private final ObjectWrapper objectWrapper;
+    private final Charset outputEncoding;
+    private final Charset urlEscapingCharset;
+    private final Boolean autoFlush;
+    private final TemplateClassResolver newBuiltinClassResolver;
+    private final Boolean showErrorTips;
+    private final Boolean apiBuiltinEnabled;
+    private final Boolean logTemplateExceptions;
+    private final Map<String, TemplateDateFormatFactory> customDateFormats;
+    private final Map<String, TemplateNumberFormatFactory> customNumberFormats;
+    private final Map<String, String> autoImports;
+    private final List<String> autoIncludes;
+    private final Boolean lazyImports;
+    private final Boolean lazyAutoImports;
+    private final Map<Object, Object> customAttributes;
+
+    // CustomStateScope:
+
+    private final ConcurrentHashMap<CustomStateKey, Object> customStateMap = new ConcurrentHashMap<>(0);
+    private final Object customStateMapLock = new Object();
+
+    private <SelfT extends ExtendableBuilder<SelfT>> Configuration(ExtendableBuilder<SelfT> builder)
+            throws ConfigurationException {
+        // Configuration-specific settings:
+
+        incompatibleImprovements = builder.getIncompatibleImprovements();
+
+        templateResolver = new DefaultTemplateResolver(
+                builder.getTemplateLoader(),
+                builder.getCacheStorage(), builder.getTemplateUpdateDelayMilliseconds(),
+                builder.getTemplateLookupStrategy(), builder.getLocalizedLookup(),
+                builder.getTemplateNameFormat(),
+                builder.getTemplateConfigurations(),
+                this);
+
+        localizedLookup = builder.getLocalizedLookup();
+
+        {
+            Collection<OutputFormat> registeredCustomOutputFormats = builder.getRegisteredCustomOutputFormats();
+
+            _NullArgumentException.check(registeredCustomOutputFormats);
+            Map<String, OutputFormat> registeredCustomOutputFormatsByName = new LinkedHashMap<>(
+                    registeredCustomOutputFormats.size() * 4 / 3, 1f);
+            for (OutputFormat outputFormat : registeredCustomOutputFormats) {
+                String name = outputFormat.getName();
+                if (name.equals(UndefinedOutputFormat.INSTANCE.getName())) {
+                    throw new ConfigurationSettingValueException(
+                            ExtendableBuilder.REGISTERED_CUSTOM_OUTPUT_FORMATS_KEY, null, false,
+                            "The \"" + name + "\" output format can't be redefined",
+                            null);
+                }
+                if (name.equals(PlainTextOutputFormat.INSTANCE.getName())) {
+                    throw new ConfigurationSettingValueException(
+                            ExtendableBuilder.REGISTERED_CUSTOM_OUTPUT_FORMATS_KEY, null, false,
+                            "The \"" + name + "\" output format can't be redefined",
+                            null);
+                }
+                if (name.length() == 0) {
+                    throw new ConfigurationSettingValueException(
+                            ExtendableBuilder.REGISTERED_CUSTOM_OUTPUT_FORMATS_KEY, null, false,
+                            "The output format name can't be 0 long",
+                            null);
+                }
+                if (!Character.isLetterOrDigit(name.charAt(0))) {
+                    throw new ConfigurationSettingValueException(
+                            ExtendableBuilder.REGISTERED_CUSTOM_OUTPUT_FORMATS_KEY, null, false,
+                            "The output format name must start with letter or digit: " + name,
+                            null);
+                }
+                if (name.indexOf('+') != -1) {
+                    throw new ConfigurationSettingValueException(
+                            ExtendableBuilder.REGISTERED_CUSTOM_OUTPUT_FORMATS_KEY, null, false,
+                            "The output format name can't contain \"+\" character: " + name,
+                            null);
+                }
+                if (name.indexOf('{') != -1) {
+                    throw new ConfigurationSettingValueException(
+                            ExtendableBuilder.REGISTERED_CUSTOM_OUTPUT_FORMATS_KEY, null, false,
+                            "The output format name can't contain \"{\" character: " + name,
+                            null);
+                }
+                if (name.indexOf('}') != -1) {
+                    throw new ConfigurationSettingValueException(
+                            ExtendableBuilder.REGISTERED_CUSTOM_OUTPUT_FORMATS_KEY, null, false,
+                            "The output format name can't contain \"}\" character: " + name,
+                            null);
+                }
+
+                OutputFormat replaced = registeredCustomOutputFormatsByName.put(outputFormat.getName(), outputFormat);
+                if (replaced != null) {
+                    if (replaced == outputFormat) {
+                        throw new IllegalArgumentException(
+                                "Duplicate output format in the collection: " + outputFormat);
+                    }
+                    throw new ConfigurationSettingValueException(
+                            ExtendableBuilder.REGISTERED_CUSTOM_OUTPUT_FORMATS_KEY, null, false,
+                            "Clashing output format names between " + replaced + " and " + outputFormat + ".",
+                            null);
+                }
+            }
+
+            this.registeredCustomOutputFormatsByName = registeredCustomOutputFormatsByName;
+            this.registeredCustomOutputFormats = Collections.unmodifiableList(new
+                    ArrayList<OutputFormat>(registeredCustomOutputFormats));
+        }
+
+        ObjectWrapper objectWrapper = builder.getObjectWrapper();
+
+        {
+            Map<String, Object> sharedVariables = builder.getSharedVariables();
+
+            HashMap<String, TemplateModel> wrappedSharedVariables = new HashMap<>(
+                    (sharedVariables.size() + 5 /* [FM3] 5 legacy vars */) * 4 / 3 + 1, 0.75f);
+
+            // TODO [FM3] Get rid of this
+            wrappedSharedVariables.put("capture_output", new CaptureOutput());
+            wrappedSharedVariables.put("compress", StandardCompress.INSTANCE);
+            wrappedSharedVariables.put("html_escape", new HtmlEscape());
+            wrappedSharedVariables.put("normalize_newlines", new NormalizeNewlines());
+            wrappedSharedVariables.put("xml_escape", new XmlEscape());
+
+            // In case the inherited sharedVariables aren't empty, we want to merge the two maps:
+            wrapAndPutSharedVariables(wrappedSharedVariables, builder.getDefaultSharedVariables(),
+                    objectWrapper);
+            if (builder.isSharedVariablesSet()) {
+                wrapAndPutSharedVariables(wrappedSharedVariables, sharedVariables, objectWrapper);
+            }
+            this.wrappedSharedVariables = wrappedSharedVariables;
+            this.sharedVariables = Collections.unmodifiableMap(new LinkedHashMap<>(sharedVariables));
+        }
+
+        // ParsingConfiguration settings:
+
+        templateLanguage = builder.getTemplateLanguage();
+        tagSyntax = builder.getTagSyntax();
+        namingConvention = builder.getNamingConvention();
+        whitespaceStripping = builder.getWhitespaceStripping();
+        autoEscapingPolicy = builder.getAutoEscapingPolicy();
+        outputFormat = builder.getOutputFormat();
+        recognizeStandardFileExtensions = builder.getRecognizeStandardFileExtensions();
+        tabSize = builder.getTabSize();
+        sourceEncoding = builder.getSourceEncoding();
+
+        // ProcessingConfiguration settings:
+
+        locale = builder.getLocale();
+        numberFormat = builder.getNumberFormat();
+        timeFormat = builder.getTimeFormat();
+        dateFormat = builder.getDateFormat();
+        dateTimeFormat = builder.getDateTimeFormat();
+        timeZone = builder.getTimeZone();
+        sqlDateAndTimeTimeZone = builder.getSQLDateAndTimeTimeZone();
+        booleanFormat = builder.getBooleanFormat();
+        templateExceptionHandler = builder.getTemplateExceptionHandler();
+        arithmeticEngine = builder.getArithmeticEngine();
+        this.objectWrapper = objectWrapper;
+        outputEncoding = builder.getOutputEncoding();
+        urlEscapingCharset = builder.getURLEscapingCharset();
+        autoFlush = builder.getAutoFlush();
+        newBuiltinClassResolver = builder.getNewBuiltinClassResolver();
+        showErrorTips = builder.getShowErrorTips();
+        apiBuiltinEnabled = builder.getAPIBuiltinEnabled();
+        logTemplateExceptions = builder.getLogTemplateExceptions();
+        customDateFormats = Collections.unmodifiableMap(builder.getCustomDateFormats());
+        customNumberFormats = Collections.unmodifiableMap(builder.getCustomNumberFormats());
+        autoImports = Collections.unmodifiableMap(builder.getAutoImports());
+        autoIncludes = Collections.unmodifiableList(builder.getAutoIncludes());
+        lazyImports = builder.getLazyImports();
+        lazyAutoImports = builder.getLazyAutoImports();
+        customAttributes = Collections.unmodifiableMap(builder.getCustomAttributes());
+    }
+
+    private <SelfT extends ExtendableBuilder<SelfT>> void wrapAndPutSharedVariables(
+            HashMap<String, TemplateModel> wrappedSharedVariables, Map<String, Object> rawSharedVariables,
+            ObjectWrapper objectWrapper) throws ConfigurationSettingValueException {
+        if (rawSharedVariables.isEmpty()) {
+            return;
+        }
+
+        for (Entry<String, Object> ent : rawSharedVariables.entrySet()) {
+            try {
+                wrappedSharedVariables.put(ent.getKey(), objectWrapper.wrap(ent.getValue()));
+            } catch (TemplateModelException e) {
+                throw new ConfigurationSettingValueException(
+                        ExtendableBuilder.SHARED_VARIABLES_KEY, null, false,
+                        "Failed to wrap shared variable " + _StringUtil.jQuote(ent.getKey()),
+                        e);
+            }
+        }
+    }
+
+    @Override
+    public TemplateExceptionHandler getTemplateExceptionHandler() {
+        return templateExceptionHandler;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isTemplateExceptionHandlerSet() {
+        return true;
+    }
+
+    private static class DefaultSoftCacheStorage extends SoftCacheStorage {
+        // Nothing to override
+    }
+
+    @Override
+    public TemplateLoader getTemplateLoader() {
+        if (templateResolver == null) {
+            return null;
+        }
+        return templateResolver.getTemplateLoader();
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isTemplateLoaderSet() {
+        return true;
+    }
+
+    @Override
+    public TemplateLookupStrategy getTemplateLookupStrategy() {
+        if (templateResolver == null) {
+            return null;
+        }
+        return templateResolver.getTemplateLookupStrategy();
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isTemplateLookupStrategySet() {
+        return true;
+    }
+    
+    @Override
+    public TemplateNameFormat getTemplateNameFormat() {
+        if (templateResolver == null) {
+            return null;
+        }
+        return templateResolver.getTemplateNameFormat();
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isTemplateNameFormatSet() {
+        return true;
+    }
+
+    @Override
+    public TemplateConfigurationFactory getTemplateConfigurations() {
+        if (templateResolver == null) {
+            return null;
+        }
+        return templateResolver.getTemplateConfigurations();
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isTemplateConfigurationsSet() {
+        return true;
+    }
+
+    @Override
+    public CacheStorage getCacheStorage() {
+        return templateResolver.getCacheStorage();
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isCacheStorageSet() {
+        return true;
+    }
+
+    @Override
+    public long getTemplateUpdateDelayMilliseconds() {
+        return templateResolver.getTemplateUpdateDelayMilliseconds();
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isTemplateUpdateDelayMillisecondsSet() {
+        return true;
+    }
+
+    @Override
+    public Version getIncompatibleImprovements() {
+        return incompatibleImprovements;
+    }
+
+    @Override
+    public boolean getWhitespaceStripping() {
+        return whitespaceStripping;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isWhitespaceStrippingSet() {
+        return true;
+    }
+
+    /**
+     * When auto-escaping should be enabled depending on the current {@linkplain OutputFormat output format};
+     * default is {@link ParsingConfiguration#ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY}. Note that the default output
+     * format, {@link UndefinedOutputFormat}, is a non-escaping format, so there auto-escaping will be off.
+     * Note that the templates can turn auto-escaping on/off locally with directives like {@code <#ftl auto_esc=...>},
+     * which will ignore the policy.
+     *
+     * <p><b>About auto-escaping</b></p>
+     *
+     * <p>
+     * Auto-escaping has significance when a value is printed with <code>${...}</code> (or <code>#{...}</code>). If
+     * auto-escaping is on, FreeMarker will assume that the value is plain text (as opposed to markup or some kind of
+     * rich text), so it will escape it according the current output format (see {@link #getOutputFormat()}
+     * and {@link TemplateConfiguration.Builder#setOutputFormat(OutputFormat)}). If auto-escaping is off, FreeMarker
+     * will assume that the string value is already in the output format, so it prints it as is to the output.
+     *
+     * <p>Further notes on auto-escaping:
+     * <ul>
+     *   <li>When printing numbers, dates, and other kind of non-string values with <code>${...}</code>, they will be
+     *       first converted to string (according the formatting settings and locale), then they are escaped just like
+     *       string values.
+     *   <li>When printing {@link TemplateMarkupOutputModel}-s, they aren't escaped again (they are already escaped).
+     *   <li>Auto-escaping doesn't do anything if the current output format isn't an {@link MarkupOutputFormat}.
+     *       That's the case for the default output format, {@link UndefinedOutputFormat}, and also for
+     *       {@link PlainTextOutputFormat}.
+     *   <li>The output format inside a string literal expression is always {@link PlainTextOutputFormat}
+     *       (regardless of the output format of the containing template), which is a non-escaping format. Thus for
+     *       example, with <code>&lt;#assign s = "foo${bar}"&gt;</code>, {@code bar} will always get into {@code s}
+     *       without escaping, but with <code>&lt;#assign s&gt;foo${bar}&lt;#assign&gt;</code> it may will be escaped.
+     * </ul>
+     *
+     * <p>Note that what you set here is just a default, which can be overridden for individual templates with the
+     * {@linkplain #getTemplateConfigurations() template configurations setting}. This setting is also overridden by
+     * the standard file extensions; see them at {@link #getRecognizeStandardFileExtensions()}.
+     *
+     * @see Configuration.Builder#setAutoEscapingPolicy(int)
+     * @see TemplateConfiguration.Builder#setAutoEscapingPolicy(int)
+     * @see Configuration.Builder#setOutputFormat(OutputFormat)
+     * @see TemplateConfiguration.Builder#setOutputFormat(OutputFormat)
+     */
+    @Override
+    public int getAutoEscapingPolicy() {
+        return autoEscapingPolicy;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isAutoEscapingPolicySet() {
+        return true;
+    }
+
+    @Override
+    public OutputFormat getOutputFormat() {
+        return outputFormat;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isOutputFormatSet() {
+        return true;
+    }
+
+    /**
+     * Returns the output format for a name.
+     * 
+     * @param name
+     *            Either the name of the output format as it was registered with the
+     *            {@link Configuration#getRegisteredCustomOutputFormats registeredCustomOutputFormats} setting,
+     *            or a combined output format name.
+     *            A combined output format is created ad-hoc from the registered formats. For example, if you need RTF
+     *            embedded into HTML, the name will be <code>HTML{RTF}</code>, where "HTML" and "RTF" refer to the
+     *            existing formats. This logic can be used recursively, so for example <code>XML{HTML{RTF}}</code> is
+     *            also valid.
+     * 
+     * @return Not {@code null}.
+     * 
+     * @throws UnregisteredOutputFormatException
+     *             If there's no output format registered with the given name.
+     * @throws IllegalArgumentException
+     *             If the usage of <code>{</code> and <code>}</code> in the name is syntactically wrong, or if not all
+     *             {@link OutputFormat}-s are {@link MarkupOutputFormat}-s in the <code>...{...}</code> expression.
+     */
+    public OutputFormat getOutputFormat(String name) throws UnregisteredOutputFormatException {
+        if (name.length() == 0) {
+            throw new IllegalArgumentException("0-length format name");
+        }
+        if (name.charAt(name.length() - 1) == '}') {
+            // Combined markup
+            int openBrcIdx = name.indexOf('{');
+            if (openBrcIdx == -1) {
+                throw new IllegalArgumentException("Missing opening '{' in: " + name);
+            }
+            
+            MarkupOutputFormat outerOF = getMarkupOutputFormatForCombined(name.substring(0, openBrcIdx));
+            MarkupOutputFormat innerOF = getMarkupOutputFormatForCombined(
+                    name.substring(openBrcIdx + 1, name.length() - 1));
+            
+            return new CombinedMarkupOutputFormat(name, outerOF, innerOF);
+        } else {
+            OutputFormat custOF = registeredCustomOutputFormatsByName.get(name);
+            if (custOF != null) {
+                return custOF;
+            }
+            
+            OutputFormat stdOF = STANDARD_OUTPUT_FORMATS.get(name);
+            if (stdOF == null) {
+                StringBuilder sb = new StringBuilder();
+                sb.append("Unregistered output format name, ");
+                sb.append(_StringUtil.jQuote(name));
+                sb.append(". The output formats registered in the Configuration are: ");
+                
+                Set<String> registeredNames = new TreeSet<>();
+                registeredNames.addAll(STANDARD_OUTPUT_FORMATS.keySet());
+                registeredNames.addAll(registeredCustomOutputFormatsByName.keySet());
+                
+                boolean first = true;
+                for (String registeredName : registeredNames) {
+                    if (first) {
+                        first = false;
+                    } else {
+                        sb.append(", ");
+                    }
+                    sb.append(_StringUtil.jQuote(registeredName));
+                }
+                
+                throw new UnregisteredOutputFormatException(sb.toString());
+            }
+            return stdOF;
+        }
+    }
+
+    private MarkupOutputFormat getMarkupOutputFormatForCombined(String outerName)
+            throws UnregisteredOutputFormatException {
+        OutputFormat of = getOutputFormat(outerName);
+        if (!(of instanceof MarkupOutputFormat)) {
+            throw new IllegalArgumentException("The \"" + outerName + "\" output format can't be used in "
+                    + "...{...} expression, because it's not a markup format.");
+        }
+        return (MarkupOutputFormat) of;
+    }
+    
+    /**
+     * The custom output formats that can be referred by their unique name ({@link OutputFormat#getName()}) from
+     * templates. Names are also used to look up the {@link OutputFormat} for standard file extensions; see them at
+     * {@link #getRecognizeStandardFileExtensions()}. Each must be different and has a unique name
+     * ({@link OutputFormat#getName()}) within this collection.
+     *
+     * <p>
+     * When there's a clash between a custom output format name and a standard output format name, the custom format
+     * will win, thus you can override the meaning of standard output format names. Except, it's not allowed to override
+     * {@link UndefinedOutputFormat} and {@link PlainTextOutputFormat}.
+     *
+     * <p>
+     * The default value is an empty collection.
+     *
+     * @throws IllegalArgumentException
+     *             When multiple different {@link OutputFormat}-s have the same name in the parameter collection. When
+     *             the same {@link OutputFormat} object occurs for multiple times in the collection. If an
+     *             {@link OutputFormat} name is 0 long. If an {@link OutputFormat} name doesn't start with letter or
+     *             digit. If an {@link OutputFormat} name contains {@code '+'} or <code>'{'</code> or <code>'}'</code>.
+     *             If an {@link OutputFormat} name equals to {@link UndefinedOutputFormat#getName()} or
+     *             {@link PlainTextOutputFormat#getName()}.
+     */
+    public Collection<OutputFormat> getRegisteredCustomOutputFormats() {
+        return registeredCustomOutputFormats;
+    }
+
+    @Override
+    public boolean getRecognizeStandardFileExtensions() {
+        return recognizeStandardFileExtensions == null
+                ? true
+                : recognizeStandardFileExtensions.booleanValue();
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isRecognizeStandardFileExtensionsSet() {
+        return true;
+    }
+
+    @Override
+    public TemplateLanguage getTemplateLanguage() {
+        return templateLanguage;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isTemplateLanguageSet() {
+        return true;
+    }
+
+    @Override
+    public int getTagSyntax() {
+        return tagSyntax;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isTagSyntaxSet() {
+        return true;
+    }
+
+    // [FM3] Use enum; won't be needed
+    static void validateNamingConventionValue(int namingConvention) {
+        if (namingConvention != ParsingConfiguration.AUTO_DETECT_NAMING_CONVENTION
+                && namingConvention != ParsingConfiguration.LEGACY_NAMING_CONVENTION
+                && namingConvention != ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION) {
+            throw new IllegalArgumentException("\"naming_convention\" can only be set to one of these: "
+                    + "Configuration.AUTO_DETECT_NAMING_CONVENTION, "
+                    + "or Configuration.LEGACY_NAMING_CONVENTION"
+                    + "or Configuration.CAMEL_CASE_NAMING_CONVENTION");
+        }
+    }
+
+    @Override
+    public int getNamingConvention() {
+        return namingConvention;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isNamingConventionSet() {
+        return true;
+    }
+
+    @Override
+    public int getTabSize() {
+        return tabSize;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isTabSizeSet() {
+        return true;
+    }
+
+    @Override
+    public Locale getLocale() {
+        return locale;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isLocaleSet() {
+        return true;
+    }
+
+    @Override
+    public TimeZone getTimeZone() {
+        return timeZone;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isTimeZoneSet() {
+        return true;
+    }
+
+    @Override
+    public TimeZone getSQLDateAndTimeTimeZone() {
+        return sqlDateAndTimeTimeZone;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isSQLDateAndTimeTimeZoneSet() {
+        return true;
+    }
+
+    @Override
+    public ArithmeticEngine getArithmeticEngine() {
+        return arithmeticEngine;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isArithmeticEngineSet() {
+        return true;
+    }
+
+    @Override
+    public String getNumberFormat() {
+        return numberFormat;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isNumberFormatSet() {
+        return true;
+    }
+
+    @Override
+    public Map<String, TemplateNumberFormatFactory> getCustomNumberFormats() {
+        return customNumberFormats;
+    }
+
+    @Override
+    public TemplateNumberFormatFactory getCustomNumberFormat(String name) {
+        return customNumberFormats.get(name);
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isCustomNumberFormatsSet() {
+        return true;
+    }
+
+    @Override
+    public String getBooleanFormat() {
+        return booleanFormat;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isBooleanFormatSet() {
+        return true;
+    }
+
+    @Override
+    public String getTimeFormat() {
+        return timeFormat;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isTimeFormatSet() {
+        return true;
+    }
+
+    @Override
+    public String getDateFormat() {
+        return dateFormat;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isDateFormatSet() {
+        return true;
+    }
+
+    @Override
+    public String getDateTimeFormat() {
+        return dateTimeFormat;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isDateTimeFormatSet() {
+        return true;
+    }
+
+    @Override
+    public Map<String, TemplateDateFormatFactory> getCustomDateFormats() {
+        return customDateFormats;
+    }
+
+    @Override
+    public TemplateDateFormatFactory getCustomDateFormat(String name) {
+        return customDateFormats.get(name);
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isCustomDateFormatsSet() {
+        return true;
+    }
+
+    @Override
+    public ObjectWrapper getObjectWrapper() {
+        return objectWrapper;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isObjectWrapperSet() {
+        return true;
+    }
+
+    @Override
+    public Charset getOutputEncoding() {
+        return outputEncoding;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isOutputEncodingSet() {
+        return true;
+    }
+
+    @Override
+    public Charset getURLEscapingCharset() {
+        return urlEscapingCharset;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isURLEscapingCharsetSet() {
+        return true;
+    }
+
+    @Override
+    public TemplateClassResolver getNewBuiltinClassResolver() {
+        return newBuiltinClassResolver;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isNewBuiltinClassResolverSet() {
+        return true;
+    }
+
+    @Override
+    public boolean getAPIBuiltinEnabled() {
+        return apiBuiltinEnabled;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isAPIBuiltinEnabledSet() {
+        return true;
+    }
+
+    @Override
+    public boolean getAutoFlush() {
+        return autoFlush;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isAutoFlushSet() {
+        return true;
+    }
+
+    @Override
+    public boolean getShowErrorTips() {
+        return showErrorTips;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isShowErrorTipsSet() {
+        return true;
+    }
+
+    @Override
+    public boolean getLogTemplateExceptions() {
+        return logTemplateExceptions;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isLogTemplateExceptionsSet() {
+        return true;
+    }
+
+    @Override
+    public boolean getLazyImports() {
+        return lazyImports;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isLazyImportsSet() {
+        return true;
+    }
+
+    @Override
+    public Boolean getLazyAutoImports() {
+        return lazyAutoImports;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isLazyAutoImportsSet() {
+        return true;
+    }
+
+    @Override
+    public Map<String, String> getAutoImports() {
+        return autoImports;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isAutoImportsSet() {
+        return true;
+    }
+
+    @Override
+    public List<String> getAutoIncludes() {
+        return autoIncludes;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isAutoIncludesSet() {
+        return true;
+    }
+
+    @Override
+    public Map<Object, Object> getCustomAttributes() {
+        return customAttributes;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isCustomAttributesSet() {
+        return true;
+    }
+
+    @Override
+    public Object getCustomAttribute(Object key) {
+        return customAttributes.get(key);
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    @SuppressFBWarnings("AT_OPERATION_SEQUENCE_ON_CONCURRENT_ABSTRACTION")
+    public <T> T getCustomState(CustomStateKey<T> customStateKey) {
+        T customState = (T) customStateMap.get(customStateKey);
+        if (customState == null) {
+            synchronized (customStateMapLock) {
+                customState = (T) customStateMap.get(customStateKey);
+                if (customState == null) {
+                    customState = customStateKey.create();
+                    if (customState == null) {
+                        throw new IllegalStateException("CustomStateKey.create() must not return null (for key: "
+                                + customStateKey + ")");
+                    }
+                    customStateMap.put(customStateKey, customState);
+                }
+            }
+        }
+        return customState;
+    }
+    
+    /**
+     * Retrieves the template with the given name from the template cache, loading it into the cache first
+     * if it's missing/staled.
+     * 
+     * <p>
+     * This is a shorthand for {@link #getTemplate(String, Locale, Serializable, boolean)
+     * getTemplate(name, null, null, false)}; see more details there.
+     * 
+     * <p>
+     * See {@link Configuration} for an example of basic usage.
+     */
+    public Template getTemplate(String name)
+            throws TemplateNotFoundException, MalformedTemplateNameException, ParseException, IOException {
+        return getTemplate(name, null, null, false);
+    }
+
+    /**
+     * Shorthand for {@link #getTemplate(String, Locale, Serializable, boolean)
+     * getTemplate(name, locale, null, null, false)}.
+     */
+    public Template getTemplate(String name, Locale locale)
+            throws TemplateNotFoundException, MalformedTemplateNameException, ParseException, IOException {
+        return getTemplate(name, locale, null, false);
+    }
+
+    /**
+     * Shorthand for {@link #getTemplate(String, Locale, Serializable, boolean)
+     * getTemplate(name, locale, customLookupCondition, false)}.
+     */
+    public Template getTemplate(String name, Locale locale, Serializable customLookupCondition)
+            throws TemplateNotFoundException, MalformedTemplateNameException, ParseException, IOException {
+        return getTemplate(name, locale, customLookupCondition, false);
+    }
+
+    /**
+     * Retrieves the template with the given name (and according the specified further parameters) from the template
+     * cache, loading it into the cache first if it's missing/staled.
+     * 
+     * <p>
+     * This method is thread-safe.
+     * 
+     * <p>
+     * See {@link Configuration} for an example of basic usage.
+     *
+     * @param name
+     *            The name or path of the template, which is not a real path, but interpreted inside the current
+     *            {@link TemplateLoader}. Can't be {@code null}. The exact syntax of the name depends on the underlying
+     *            {@link TemplateLoader} (the {@link TemplateResolver} more generally), but the default
+     *            {@link TemplateResolver} has some assumptions. First, the name is expected to be a
+     *            hierarchical path, with path components separated by a slash character (not with backslash!). The path
+     *            (the name) given here must <em>not</em> begin with slash; it's always interpreted relative to the
+     *            "template root directory". Then, the {@code ..} and {@code .} path meta-elements will be resolved. For
+     *            example, if the name is {@code a/../b/./c.ftl}, then it will be simplified to {@code b/c.ftl}. The
+     *            rules regarding this are the same as with conventional UN*X paths. The path must not reach outside the
+     *            template root directory, that is, it can't be something like {@code "../templates/my.ftl"} (not even
+     *            if this path happens to be equivalent with {@code "/my.ftl"}). Furthermore, the path is allowed to
+     *            contain at most one path element whose name is {@code *} (asterisk). This path meta-element triggers
+     *            the <i>acquisition mechanism</i>. If the template is not found in the location described by the
+     *            concatenation of the path left to the asterisk (called base path) and the part to the right of the
+     *            asterisk (called resource path), the {@link TemplateResolver} (at least the default one) will attempt
+     *            to remove the rightmost path component from the base path ("go up one directory") and concatenate
+     *            that with the resource path. The process is repeated until either a template is found, or the base
+     *            path is completely exhausted.
+     *
+     * @param locale
+     *            The requested locale of the template. This is what {@link Template#getLocale()} on the resulting
+     *            {@link Template} will return (unless it's overridden via {@link #getTemplateConfigurations()}). This
+     *            parameter can be {@code null} since 2.3.22, in which case it defaults to
+     *            {@link Configuration#getLocale()} (note that {@link Template#getLocale()} will give the default value,
+     *            not {@code null}). This parameter also drives localized template lookup. Assuming that you have
+     *            specified {@code en_US} as the locale and {@code myTemplate.ftl} as the name of the template, and the
+     *            default {@link TemplateLookupStrategy} is used and
+     *            {@code #setLocalizedLookup(boolean) localized_lookup} is {@code true}, FreeMarker will first try to
+     *            retrieve {@code myTemplate_en_US.html}, then {@code myTemplate.en.ftl}, and finally
+     *            {@code myTemplate.ftl}. Note that that the template's locale will be {@code en_US} even if it only
+     *            finds {@code myTemplate.ftl}. Note that when the {@code locale} setting is overridden with a
+     *            {@link TemplateConfiguration} provided by {@link #getTemplateConfigurations()}, that overrides the
+     *            value specified here, but only after the localized lookup, that is, it modifies the template
+     *            found by the localized lookup.
+     * 
+     * @param customLookupCondition
+     *            This value can be used by a custom {@link TemplateLookupStrategy}; has no effect with the default one.
+     *            Can be {@code null} (though it's up to the custom {@link TemplateLookupStrategy} if it allows that).
+     *            This object will be used as part of the cache key, so it must to have a proper
+     *            {@link Object#equals(Object)} and {@link Object#hashCode()} method. It also should have reasonable
+     *            {@link Object#toString()}, as it's possibly quoted in error messages. The expected type is up to the
+     *            custom {@link TemplateLookupStrategy}. See also:
+     *            {@link TemplateLookupContext#getCustomLookupCondition()}.
+     *
+     * @param ignoreMissing
+     *            If {@code true}, the method won't throw {@link TemplateNotFoundException} if the template doesn't
+     *            exist, instead it returns {@code null}. Other kind of exceptions won't be suppressed.
+     * 
+     * @return the requested template; maybe {@code null} when the {@code ignoreMissing} parameter is {@code true}.
+     * 
+     * @throws TemplateNotFoundException
+     *             If the template could not be found. Note that this exception extends {@link IOException}.
+     * @throws MalformedTemplateNameException
+     *             If the template name given was in violation with the {@link TemplateNameFormat} in use. Note that
+     *             this exception extends {@link IOException}.
+     * @throws ParseException
+     *             (extends <code>IOException</code>) if the template is syntactically bad. Note that this exception
+     *             extends {@link IOException}.
+     * @throws IOException
+     *             If there was some other problem with reading the template "file". Note that the other exceptions
+     *             extend {@link IOException}, so this should be catched the last.
+     * 
+     * @since 2.3.22
+     */
+    public Template getTemplate(String name, Locale locale, Serializable customLookupCondition,
+            boolean ignoreMissing)
+            throws TemplateNotFoundException, MalformedTemplateNameException, ParseException, IOException {
+        if (locale == null) {
+            locale = getLocale();
+        }
+        final GetTemplateResult maybeTemp = templateResolver.getTemplate(name, locale, customLookupCondition);
+        final Template temp = maybeTemp.getTemplate();
+        if (temp == null) {
+            if (ignoreMissing) {
+                return null;
+            }
+            
+            TemplateLoader tl = getTemplateLoader();  
+            String msg; 
+            if (tl == null) {
+                msg = "Don't know where to load template " + _StringUtil.jQuote(name)
+                      + " from because the \"template_loader\" FreeMarker "
+                      + "setting wasn't set (Configuration.setTemplateLoader), so it's null.";
+            } else {
+                final String missingTempNormName = maybeTemp.getMissingTemplateNormalizedName();
+                final String missingTempReason = maybeTemp.getMissingTemplateReason();
+                final TemplateLookupStrategy templateLookupStrategy = getTemplateLookupStrategy();
+                msg = "Template not found for name " + _StringUtil.jQuote(name)
+                        + (missingTempNormName != null && name != null
+                                && !removeInitialSlash(name).equals(missingTempNormName)
+                                ? " (normalized: " + _StringUtil.jQuote(missingTempNormName) + ")"
+                                : "")
+                        + (customLookupCondition != null ? " and custom lookup condition "
+                        + _StringUtil.jQuote(customLookupCondition) : "")
+                        + "."
+                        + (missingTempReason != null
+                                ? "\nReason given: " + ensureSentenceIsClosed(missingTempReason)
+                                : "")
+                        + "\nThe name was interpreted by this TemplateLoader: "
+                        + _StringUtil.tryToString(tl) + "."
+                        + (!isKnownNonConfusingLookupStrategy(templateLookupStrategy)
+                                ? "\n(Before that, the name was possibly changed by this lookup strategy: "
+                                  + _StringUtil.tryToString(templateLookupStrategy) + ".)"
+                                : "")
+                        + (missingTempReason == null && name.indexOf('\\') != -1
+                                ? "\nWarning: The name contains backslash (\"\\\") instead of slash (\"/\"); "
+                                    + "template names should use slash only."
+                                : "");
+            }
+            
+            String normName = maybeTemp.getMissingTemplateNormalizedName();
+            throw new TemplateNotFoundException(
+                    normName != null ? normName : name,
+                    customLookupCondition,
+                    msg);
+        }
+        return temp;
+    }
+    
+    private boolean isKnownNonConfusingLookupStrategy(TemplateLookupStrategy templateLookupStrategy) {
+        return templateLookupStrategy == DefaultTemplateLookupStrategy.INSTANCE;
+    }
+
+    private String removeInitialSlash(String name) {
+        return name.startsWith("/") ? name.substring(1) : name;
+    }
+
+    private String ensureSentenceIsClosed(String s) {
+        if (s == null || s.length() == 0) {
+            return s;
+        }
+        
+        final char lastChar = s.charAt(s.length() - 1);
+        return lastChar == '.' || lastChar == '!' || lastChar == '?' ? s : s + ".";
+    }
+
+    @Override
+    public Charset getSourceEncoding() {
+        return sourceEncoding;
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isSourceEncodingSet() {
+        return true;
+    }
+
+    @Override
+    public Map<String, Object> getSharedVariables() {
+        return sharedVariables;
+    }
+
+    @Override
+    public boolean isSharedVariablesSet() {
+        return true;
+    }
+
+    /**
+     * Returns the shared variable as a {@link TemplateModel}, or {@code null} if it doesn't exist.
+     */
+    // TODO [FM3] How the caller can tell if a shared variable exists but null or it's missing?
+    public TemplateModel getWrappedSharedVariable(String key) {
+        return wrappedSharedVariables.get(key);
+    }
+
+    /**
+     * Removes all entries from the template cache, thus forcing reloading of templates
+     * on subsequent <code>getTemplate</code> calls.
+     * 
+     * <p>This method is thread-safe and can be called while the engine processes templates.
+     */
+    public void clearTemplateCache() {
+        templateResolver.clearTemplateCache();
+    }
+    
+    /**
+     * Removes a template from the template cache, hence forcing the re-loading
+     * of it when it's next time requested. This is to give the application
+     * finer control over cache updating than the
+     * {@link #getTemplateUpdateDelayMilliseconds() templateUpdateDelayMilliseconds} setting
+     * alone does.
+     * 
+     * <p>For the meaning of the parameters, see
+     * {@link #getTemplate(String, Locale, Serializable, boolean)}.
+     * 
+     * <p>This method is thread-safe and can be called while the engine processes templates.
+     */
+    public void removeTemplateFromCache(String name, Locale locale, Serializable customLookupCondition)
+            throws IOException {
+        templateResolver.removeTemplateFromCache(name, locale, customLookupCondition);
+    }
+
+    @Override
+    public boolean getLocalizedLookup() {
+        return templateResolver.getLocalizedLookup();
+    }
+
+    /**
+     * Always {@code true} in {@link Configuration}-s, so calling the corresponding getter is always safe.
+     */
+    @Override
+    public boolean isLocalizedLookupSet() {
+        return true;
+    }
+
+    /**
+     * Returns the FreeMarker version information, most importantly the major.minor.micro version numbers.
+     * 
+     * On FreeMarker version numbering rules:
+     * <ul>
+     *   <li>For final/stable releases the version number is like major.minor.micro, like 2.3.19. (Historically,
+     *       when micro was 0 the version strings was like major.minor instead of the proper major.minor.0, but that's
+     *       not like that anymore.)
+     *   <li>When only the micro version is increased, compatibility with previous versions with the same
+     *       major.minor is kept. Thus <tt>freemarker.jar</tt> can be replaced in an existing application without
+     *       breaking it.</li>
+     *   <li>For non-final/unstable versions (that almost nobody uses), the format is:
+     *       <ul>
+     *         <li>Starting from 2.3.20: major.minor.micro-extraInfo, like
+     *             2.3.20-nightly_20130506T123456Z, 2.4.0-RC01. The major.minor.micro
+     *             always indicates the target we move towards, so 2.3.20-nightly or 2.3.20-M01 is
+     *             after 2.3.19 and will eventually become to 2.3.20. "PRE", "M" and "RC" (uppercase!) means
+     *             "preview", "milestone" and "release candidate" respectively, and is always followed by a 2 digit
+     *             0-padded counter, like M03 is the 3rd milestone release of a given major.minor.micro.</li> 
+     *         <li>Before 2.3.20: The extraInfo wasn't preceded by a "-".
+     *             Instead of "nightly" there was "mod", where the major.minor.micro part has indicated where
+     *             are we coming from, so 2.3.19mod (read as: 2.3.19 modified) was after 2.3.19 but before 2.3.20.
+     *             Also, "pre" and "rc" was lowercase, and was followd by a number without 0-padding.</li>
+     *       </ul>
+     * </ul>
+     * 
+     * @since 2.3.20
+     */ 
+    public static Version getVersion() {
+        return VERSION;
+    }
+    
+    /**
+     * Same as {@link #getSupportedBuiltInNames(int)} with argument {@link #getNamingConvention()}.
+     * 
+     * @since 2.3.20
+     */
+    public Set getSupportedBuiltInNames() {
+        return getSupportedBuiltInNames(getNamingConvention());
+    }
+
+    /**
+     * Returns the names of the supported "built-ins". These are the ({@code expr?builtin_name}-like things). As of this
+     * writing, this information doesn't depend on the configuration options, so it could be a static method, but
+     * to be future-proof, it's an instance method. 
+     * 
+     * @param namingConvention
+     *            One of {@link ParsingConfiguration#AUTO_DETECT_NAMING_CONVENTION},
+     *            {@link ParsingConfiguration#LEGACY_NAMING_CONVENTION}, and
+     *            {@link ParsingConfiguration#CAMEL_CASE_NAMING_CONVENTION}. If it's
+     *            {@link ParsingConfiguration#AUTO_DETECT_NAMING_CONVENTION} then the union
+     *            of the names in all the naming conventions is returned.
+     * 
+     * @since 2.3.24
+     */
+    public Set<String> getSupportedBuiltInNames(int namingConvention) {
+        Set<String> names;
+        if (namingConvention == ParsingConfiguration.AUTO_DETECT_NAMING_CONVENTION) {
+            names = ASTExpBuiltIn.BUILT_INS_BY_NAME.keySet();
+        } else if (namingConvention == ParsingConfiguration.LEGACY_NAMING_CONVENTION) {
+            names = ASTExpBuiltIn.SNAKE_CASE_NAMES;
+        } else if (namingConvention == ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION) {
+            names = ASTExpBuiltIn.CAMEL_CASE_NAMES;
+        } else {
+            throw new IllegalArgumentException("Unsupported naming convention constant: " + namingConvention);
+        }
+        return Collections.unmodifiableSet(names);
+    }
+    
+    /**
+     * Same as {@link #getSupportedBuiltInDirectiveNames(int)} with argument {@link #getNamingConvention()}.
+     * 
+     * @since 2.3.21
+     */
+    public Set getSupportedBuiltInDirectiveNames() {
+        return getSupportedBuiltInDirectiveNames(getNamingConvention());
+    }
+
+    /**
+     * Returns the names of the directives that are predefined by FreeMarker. These are the things that you call like
+     * <tt>&lt;#directiveName ...&gt;</tt>.
+     * 
+     * @param namingConvention
+     *            One of {@link ParsingConfiguration#AUTO_DETECT_NAMING_CONVENTION},
+     *            {@link ParsingConfiguration#LEGACY_NAMING_CONVENTION}, and
+     *            {@link ParsingConfiguration#CAMEL_CASE_NAMING_CONVENTION}. If it's
+     *            {@link ParsingConfiguration#AUTO_DETECT_NAMING_CONVENTION} then the union
+     *            of the names in all the naming conventions is returned. 
+     * 
+     * @since 2.3.24
+     */
+    public Set<String> getSupportedBuiltInDirectiveNames(int namingConvention) {
+        if (namingConvention == AUTO_DETECT_NAMING_CONVENTION) {
+            return ASTDirective.ALL_BUILT_IN_DIRECTIVE_NAMES;
+        } else if (namingConvention == LEGACY_NAMING_CONVENTION) {
+            return ASTDirective.LEGACY_BUILT_IN_DIRECTIVE_NAMES;
+        } else if (namingConvention == CAMEL_CASE_NAMING_CONVENTION) {
+            return ASTDirective.CAMEL_CASE_BUILT_IN_DIRECTIVE_NAMES;
+        } else {
+            throw new IllegalArgumentException("Unsupported naming convention constant: " + namingConvention);
+        }
+    }
+    
+    private static String getRequiredVersionProperty(Properties vp, String properyName) {
+        String s = vp.getProperty(properyName);
+        if (s == null) {
+            throw new RuntimeException(
+                    "Version file is corrupt: \"" + properyName + "\" property is missing.");
+        }
+        return s;
+    }
+
+    /**
+     * Usually you use {@link Builder} instead of this abstract class, except where you declare the type of a method
+     * parameter or field, where the more generic {@link ExtendableBuilder} should be used. {@link ExtendableBuilder}
+     * might have other subclasses than {@link Builder}, because some applications needs different setting defaults
+     * or other changes.
+     */
+    public abstract static class ExtendableBuilder<SelfT extends ExtendableBuilder<SelfT>>
+            extends MutableParsingAndProcessingConfiguration<SelfT>
+            implements TopLevelConfiguration, CommonBuilder<Configuration> {
+
+        /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+        public static final String SOURCE_ENCODING_KEY_SNAKE_CASE = "source_encoding";
+        /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+        public static final String SOURCE_ENCODING_KEY = SOURCE_ENCODING_KEY_SNAKE_CASE;
+        /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+        public static final String SOURCE_ENCODING_KEY_CAMEL_CASE = "sourceEncoding";
+        /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+        public static final String LOCALIZED_LOOKUP_KEY_SNAKE_CASE = "localized_lookup";
+        /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+        public static final String LOCALIZED_LOOKUP_KEY = LOCALIZED_LOOKUP_KEY_SNAKE_CASE;
+        /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+        public static final String LOCALIZED_LOOKUP_KEY_CAMEL_CASE = "localizedLookup";
+        /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+        public static final String WHITESPACE_STRIPPING_KEY_SNAKE_CASE = "whitespace_stripping";
+        /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+        public static final String WHITESPACE_STRIPPING_KEY = WHITESPACE_STRIPPING_KEY_SNAKE_CASE;
+        /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+        public static final String WHITESPACE_STRIPPING_KEY_CAMEL_CASE = "whitespaceStripping";
+        /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.24 */
+        public static final String OUTPUT_FORMAT_KEY_SNAKE_CASE = "output_format";
+        /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+        public static final String OUTPUT_FORMAT_KEY = OUTPUT_FORMAT_KEY_SNAKE_CASE;
+        /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.24 */
+        public static final String OUTPUT_FORMAT_KEY_CAMEL_CASE = "outputFormat";
+        /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.24 */
+        public static final String RECOGNIZE_STANDARD_FILE_EXTENSIONS_KEY_SNAKE_CASE = "recognize_standard_file_extensions";
+        /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+        public static final String RECOGNIZE_STANDARD_FILE_EXTENSIONS_KEY
+                = RECOGNIZE_STANDARD_FILE_EXTENSIONS_KEY_SNAKE_CASE;
+        /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.24 */
+        public static final String RECOGNIZE_STANDARD_FILE_EXTENSIONS_KEY_CAMEL_CASE = "recognizeStandardFileExtensions";
+        /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.24 */
+        public static final String REGISTERED_CUSTOM_OUTPUT_FORMATS_KEY_SNAKE_CASE = "registered_custom_output_formats";
+        /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+        public static final String REGISTERED_CUSTOM_OUTPUT_FORMATS_KEY = REGISTERED_CUSTOM_OUTPUT_FORMATS_KEY_SNAKE_CASE;
+        /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.24 */
+        public static final String REGISTERED_CUSTOM_OUTPUT_FORMATS_KEY_CAMEL_CASE = "registeredCustomOutputFormats";
+        /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.24 */
+        public static final String AUTO_ESCAPING_POLICY_KEY_SNAKE_CASE = "auto_escaping_policy";
+        /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+        public static final String AUTO_ESCAPING_POLICY_KEY = AUTO_ESCAPING_POLICY_KEY_SNAKE_CASE;
+        /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.24 */
+        public static final String AUTO_ESCAPING_POLICY_KEY_CAMEL_CASE = "autoEscapingPolicy";
+        /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+        public static final String CACHE_STORAGE_KEY_SNAKE_CASE = "cache_storage";
+        /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+        public static final String CACHE_STORAGE_KEY = CACHE_STORAGE_KEY_SNAKE_CASE;
+        /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+        public static final String CACHE_STORAGE_KEY_CAMEL_CASE = "cacheStorage";
+        /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+        public static final String TEMPLATE_UPDATE_DELAY_KEY_SNAKE_CASE = "template_update_delay";
+        /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+        public static final String TEMPLATE_UPDATE_DELAY_KEY = TEMPLATE_UPDATE_DELAY_KEY_SNAKE_CASE;
+        /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+        public static final String TEMPLATE_UPDATE_DELAY_KEY_CAMEL_CASE = "templateUpdateDelay";
+        /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+        public static final String AUTO_INCLUDE_KEY_SNAKE_CASE = "auto_include";
+        /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+        public static final String AUTO_INCLUDE_KEY = AUTO_INCLUDE_KEY_SNAKE_CASE;
+        /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+        public static final String AUTO_INCLUDE_KEY_CAMEL_CASE = "autoInclude";
+        /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+        public static final String TEMPLATE_LANGUAGE_KEY_SNAKE_CASE = "template_language";
+        /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+        public static final String TEMPLATE_LANGUAGE_KEY = TEMPLATE_LANGUAGE_KEY_SNAKE_CASE;
+        /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+        public static final String TEMPLATE_LANGUAGE_KEY_CAMEL_CASE = "templateLanguage";
+        /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+        public static final String TAG_SYNTAX_KEY_SNAKE_CASE = "tag_syntax";
+        /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+        public static final String TAG_SYNTAX_KEY = TAG_SYNTAX_KEY_SNAKE_CASE;
+        /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+        public static final String TAG_SYNTAX_KEY_CAMEL_CASE = "tagSyntax";
+        /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+        public static final String NAMING_CONVENTION_KEY_SNAKE_CASE = "naming_convention";
+        /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+        public static final String NAMING_CONVENTION_KEY = NAMING_CONVENTION_KEY_SNAKE_CASE;
+        /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+        public static final String NAMING_CONVENTION_KEY_CAMEL_CASE = "namingConvention";
+        /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.25 */
+        public static final String TAB_SIZE_KEY_SNAKE_CASE = "tab_size";
+        /** Alias to the {@code ..._SNAKE_CASE} variation. @since 2.3.25 */
+        public static final String TAB_SIZE_KEY = TAB_SIZE_KEY_SNAKE_CASE;
+        /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.25 */
+        public static final String TAB_SIZE_KEY_CAMEL_CASE = "tabSize";
+        /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+        public static final String TEMPLATE_LOADER_KEY_SNAKE_CASE = "template_loader";
+        /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+        public static final String TEMPLATE_LOADER_KEY = TEMPLATE_LOADER_KEY_SNAKE_CASE;
+        /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+        public static final String TEMPLATE_LOADER_KEY_CAMEL_CASE = "templateLoader";
+        /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+        public static final String TEMPLATE_LOOKUP_STRATEGY_KEY_SNAKE_CASE = "template_lookup_strategy";
+        /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+        public static final String TEMPLATE_LOOKUP_STRATEGY_KEY = TEMPLATE_LOOKUP_STRATEGY_KEY_SNAKE_CASE;
+        /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+        public static final String TEMPLATE_LOOKUP_STRATEGY_KEY_CAMEL_CASE = "templateLookupStrategy";
+        /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+        public static final String TEMPLATE_NAME_FORMAT_KEY_SNAKE_CASE = "template_name_format";
+        /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+        public static final String TEMPLATE_NAME_FORMAT_KEY = TEMPLATE_NAME_FORMAT_KEY_SNAKE_CASE;
+        /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+        public static final String TEMPLATE_NAME_FORMAT_KEY_CAMEL_CASE = "templateNameFormat";
+        /** Legacy, snake case ({@code like_this}) variation of the setting name. */
+        public static final String SHARED_VARIABLES_KEY_SNAKE_CASE = "shared_variables";
+        /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+        public static final String SHARED_VARIABLES_KEY = SHARED_VARIABLES_KEY_SNAKE_CASE;
+        /** Modern, camel case ({@code likeThis}) variation of the setting name. */
+        public static final String SHARED_VARIABLES_KEY_CAMEL_CASE = "sharedVariables";
+        /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.24 */
+        public static final String TEMPLATE_CONFIGURATIONS_KEY_SNAKE_CASE = "template_configurations";
+        /** Alias to the {@code ..._SNAKE_CASE} variation. @since 2.3.24 */
+        public static final String TEMPLATE_CONFIGURATIONS_KEY = TEMPLATE_CONFIGURATIONS_KEY_SNAKE_CASE;
+        /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.24 */
+        public static final String TEMPLATE_CONFIGURATIONS_KEY_CAMEL_CASE = "templateConfigurations";
+        /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+        public static final String INCOMPATIBLE_IMPROVEMENTS_KEY_SNAKE_CASE = "incompatible_improvements";
+        /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+        public static final String INCOMPATIBLE_IMPROVEMENTS_KEY = INCOMPATIBLE_IMPROVEMENTS_KEY_SNAKE_CASE;
+        /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+        public static final String INCOMPATIBLE_IMPROVEMENTS_KEY_CAMEL_CASE = "incompatibleImprovements";
+        // Set early in the constructor to non-null
+        private Version incompatibleImprovements = Configuration.VERSION_3_0_0;
+
+        private TemplateLoader templateLoader;
+        private boolean templateLoaderSet;
+        private CacheStorage cacheStorage;
+        private CacheStorage cachedDefaultCacheStorage;
+        private TemplateLookupStrategy templateLookupStrategy;
+        private TemplateNameFormat templateNameFormat;
+        private TemplateConfigurationFactory templateConfigurations;
+        private boolean templateConfigurationsSet;
+        private Long templateUpdateDelayMilliseconds;
+        private Boolean localizedLookup;
+
+        private Collection<OutputFormat> registeredCustomOutputFormats;
+        private Map<String, Object> sharedVariables;
+
+        /**
+         * @param incompatibleImprovements
+         *         The inital value of the {@link Configuration#getIncompatibleImprovements() incompatibleImprovements};
+         *         can't {@code null}. This can be later changed via {@link #setIncompatibleImprovements(Version)}. The
+         *         point here is just to ensure that it's never {@code null}.
+         */
+        protected ExtendableBuilder(Version incompatibleImprovements) {
+            _NullArgumentException.check("incompatibleImprovements", incompatibleImprovements);
+            this.incompatibleImprovements = incompatibleImprovements;
+        }
+
+        @Override
+        public Configuration build() throws ConfigurationException {
+            return new Configuration(this);
+        }
+
+        @Override
+        public void setSetting(String name, String value) throws ConfigurationException {
+            boolean unknown = false;
+            try {
+                if ("TemplateUpdateInterval".equalsIgnoreCase(name)) {
+                    name = TEMPLATE_UPDATE_DELAY_KEY;
+                } else if ("DefaultEncoding".equalsIgnoreCase(name)) {
+                    name = SOURCE_ENCODING_KEY;
+                }
+
+                if (SOURCE_ENCODING_KEY_SNAKE_CASE.equals(name) || SOURCE_ENCODING_KEY_CAMEL_CASE.equals(name)) {
+                    if (JVM_DEFAULT_VALUE.equalsIgnoreCase(value)) {
+                        setSourceEncoding(Charset.defaultCharset());
+                    } else {
+                        setSourceEncoding(Charset.forName(value));
+                    }
+                } else if (LOCALIZED_LOOKUP_KEY_SNAKE_CASE.equals(name) || LOCALIZED_LOOKUP_KEY_CAMEL_CASE.equals(name)) {
+                    setLocalizedLookup(_StringUtil.getYesNo(value));
+                } else if (WHITESPACE_STRIPPING_KEY_SNAKE_CASE.equals(name)
+                        || WHITESPACE_STRIPPING_KEY_CAMEL_CASE.equals(name)) {
+                    setWhitespaceStripping(_StringUtil.getYesNo(value));
+                } else if (AUTO_ESCAPING_POLICY_KEY_SNAKE_CASE.equals(name) || AUTO_ESCAPING_POLICY_KEY_CAMEL_CASE.equals(name)) {
+                    if ("enable_if_default".equals(value) || "enableIfDefault".equals(value)) {
+                        setAutoEscapingPolicy(ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY);
+                    } else if ("enable_if_supported".equals(value) || "enableIfSupported".equals(value)) {
+                        setAutoEscapingPolicy(ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY);
+                    } else if ("disable".equals(value)) {
+                        setAutoEscapingPolicy(DISABLE_AUTO_ESCAPING_POLICY);
+                    } else {
+                        throw new ConfigurationSettingValueException( name, value,
+                                "No such predefined auto escaping policy name");
+                    }
+                } else if (OUTPUT_FORMAT_KEY_SNAKE_CASE.equals(name) || OUTPUT_FORMAT_KEY_CAMEL_CASE.equals(name)) {
+                    if (value.equalsIgnoreCase(DEFAULT_VALUE)) {
+                        unsetOutputFormat();
+                    } else {
+                        setOutputFormat((OutputFormat) _ObjectBuilderSettingEvaluator.eval(
+                                value, OutputFormat.class, true, _SettingEvaluationEnvironment.getCurrent()));
+                    }
+                } else if (REGISTERED_CUSTOM_OUTPUT_FORMATS_KEY_SNAKE_CASE.equals(name)
+                        || REGISTERED_CUSTOM_OUTPUT_FORMATS_KEY_CAMEL_CASE.equals(name)) {
+                    List list = (List) _ObjectBuilderSettingEvaluator.eval(
+                            value, List.class, true, _SettingEvaluationEnvironment.getCurrent());
+                    for (Object item : list) {
+                        if (!(item instanceof OutputFormat)) {
+                            throw new ConfigurationSettingValueException(name, value,
+                                    "List items must be " + OutputFormat.class.getName() + " instances.");
+                        }
+                    }
+                    setRegisteredCustomOutputFormats(list);
+                } else if (RECOGNIZE_STANDARD_FILE_EXTENSIONS_KEY_SNAKE_CASE.equals(name)
+                        || RECOGNIZE_STANDARD_FILE_EXTENSIONS_KEY_CAMEL_CASE.equals(name)) {
+                    if (value.equalsIgnoreCase(DEFAULT_VALUE)) {
+                        unsetRecognizeStandardFileExtensions();
+                    } else {
+                        setRecognizeStandardFileExtensions(_StringUtil.getYesNo(value));
+                    }
+                } else if (CACHE_STORAGE_KEY_SNAKE_CASE.equals(name) || CACHE_STORAGE_KEY_CAMEL_CASE.equals(name)) {
+                    if (value.equalsIgnoreCase(DEFAULT_VALUE)) {
+                        unsetCacheStorage();
+                    } if (value.indexOf('.') == -1) {
+                        int strongSize = 0;
+                        int softSize = 0;
+                        Map map = _StringUtil.parseNameValuePairList(
+                                value, String.valueOf(Integer.MAX_VALUE));
+                        Iterator it = map.entrySet().iterator();
+                        while (it.hasNext()) {
+                            Map.Entry ent = (Map.Entry) it.next();
+                            String pName = (String) ent.getKey();
+                            int pValue;
+                            try {
+                                pValue = Integer.parseInt((String) ent.getValue());
+                            } catch (NumberFormatException e) {
+                                throw new ConfigurationSettingValueException(name, value,
+                                        "Malformed integer number (shown quoted): " + _StringUtil.jQuote(ent.getValue()));
+                            }
+                            if ("soft".equalsIgnoreCase(pName)) {
+                                softSize = pValue;
+                            } else if ("strong".equalsIgnoreCase(pName)) {
+                                strongSize = pValue;
+                            } else {
+                                throw new ConfigurationSettingValueException(name, value,
+                                        "Unsupported cache parameter name (shown quoted): "
+                                                + _StringUtil.jQuote(ent.getValue()));
+                            }
+                        }
+                        if (softSize == 0 && strongSize == 0) {
+                            throw new ConfigurationSettingValueException(name, value,
+                                    "Either cache soft- or strong size must be set and non-0.");
+                        }
+                        setCacheStorage(new MruCacheStorage(strongSize, softSize));
+                    } else {
+                        setCacheStorage((CacheStorage) _ObjectBuilderSettingEvaluator.eval(
+                                value, CacheStorage.class, false, _SettingEvaluationEnvironment.getCurrent()));
+                    }
+                } else if (TEMPLATE_UPDATE_DELAY_KEY_SNAKE_CASE.equals(name)
+                        || TEMPLATE_UPDATE_DELAY_KEY_CAMEL_CASE.equals(name)) {
+                    final String valueWithoutUnit;
+                    final String unit;
+                    int numberEnd = 0;
+                    while (numberEnd < value.length() && !Character.isAlphabetic(value.charAt(numberEnd))) {
+                        numberEnd++;
+                    }
+                    valueWithoutUnit = value.substring(0, numberEnd).trim();
+                    unit = value.substring(numberEnd).trim();
+
+                    final long multipier;
+                    if (unit.equals("ms")) {
+          

<TRUNCATED>


[42/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForMultipleTypes.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForMultipleTypes.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForMultipleTypes.java
new file mode 100644
index 0000000..ab3df64
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForMultipleTypes.java
@@ -0,0 +1,717 @@
+/*
+ * 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.util.Date;
+import java.util.List;
+
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateCollectionModelEx;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateDirectiveModel;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.TemplateMethodModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelWithAPISupport;
+import org.apache.freemarker.core.model.TemplateNodeModel;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.TemplateTransformModel;
+import org.apache.freemarker.core.model.impl.SimpleDate;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.valueformat.TemplateDateFormat;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormat;
+import org.apache.freemarker.core.valueformat.TemplateValueFormatException;
+
+/**
+ * A holder for builtins that didn't fit into any other category.
+ */
+class BuiltInsForMultipleTypes {
+
+    static class cBI extends AbstractCBI {
+        
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel model = target.eval(env);
+            if (model instanceof TemplateNumberModel) {
+                return formatNumber(env, model);
+            } else if (model instanceof TemplateBooleanModel) {
+                return new SimpleScalar(((TemplateBooleanModel) model).getAsBoolean()
+                        ? MiscUtil.C_TRUE : MiscUtil.C_FALSE);
+            } else {
+                throw new UnexpectedTypeException(
+                        target, model,
+                        "number or boolean", new Class[] { TemplateNumberModel.class, TemplateBooleanModel.class },
+                        env);
+            }
+        }
+
+        @Override
+        protected TemplateModel formatNumber(Environment env, TemplateModel model) throws TemplateModelException {
+            Number num = _EvalUtil.modelToNumber((TemplateNumberModel) model, target);
+            if (num instanceof Integer || num instanceof Long) {
+                // Accelerate these fairly common cases
+                return new SimpleScalar(num.toString());
+            } else if (num instanceof Double) {
+                double n = num.doubleValue();
+                if (n == Double.POSITIVE_INFINITY) {
+                    return new SimpleScalar("INF");
+                }
+                if (n == Double.NEGATIVE_INFINITY) {
+                    return new SimpleScalar("-INF");
+                }
+                if (Double.isNaN(n)) {
+                    return new SimpleScalar("NaN");
+                }
+                // Deliberately falls through
+            } else if (num instanceof Float) {
+                float n = num.floatValue();
+                if (n == Float.POSITIVE_INFINITY) {
+                    return new SimpleScalar("INF");
+                }
+                if (n == Float.NEGATIVE_INFINITY) {
+                    return new SimpleScalar("-INF");
+                }
+                if (Float.isNaN(n)) {
+                    return new SimpleScalar("NaN");
+                }
+                // Deliberately falls through
+            }
+        
+            return new SimpleScalar(env.getCNumberFormat().format(num));
+        }
+        
+    }
+
+    static class dateBI extends ASTExpBuiltIn {
+        private class DateParser
+        implements
+            TemplateDateModel,
+            TemplateMethodModel,
+            TemplateHashModel {
+            private final String text;
+            private final Environment env;
+            private final TemplateDateFormat defaultFormat;
+            private TemplateDateModel cachedValue;
+            
+            DateParser(String text, Environment env)
+            throws TemplateException {
+                this.text = text;
+                this.env = env;
+                defaultFormat = env.getTemplateDateFormat(dateType, Date.class, target, false);
+            }
+            
+            @Override
+            public Object exec(List args) throws TemplateModelException {
+                checkMethodArgCount(args, 0, 1);
+                return args.size() == 0 ? getAsDateModel() : get((String) args.get(0));
+            }
+            
+            @Override
+            public TemplateModel get(String pattern) throws TemplateModelException {
+                TemplateDateFormat format;
+                try {
+                    format = env.getTemplateDateFormat(pattern, dateType, Date.class, target, dateBI.this, true);
+                } catch (TemplateException e) {
+                    // `e` should always be a TemplateModelException here, but to be sure: 
+                    throw _CoreAPI.ensureIsTemplateModelException("Failed to get format", e); 
+                }
+                return toTemplateDateModel(parse(format));
+            }
+
+            private TemplateDateModel toTemplateDateModel(Object date) throws _TemplateModelException {
+                if (date instanceof Date) {
+                    return new SimpleDate((Date) date, dateType);
+                } else {
+                    TemplateDateModel tm = (TemplateDateModel) date;
+                    if (tm.getDateType() != dateType) {
+                        throw new _TemplateModelException("The result of the parsing was of the wrong date type.");
+                    }
+                    return tm;
+                }
+            }
+
+            private TemplateDateModel getAsDateModel() throws TemplateModelException {
+                if (cachedValue == null) {
+                    cachedValue = toTemplateDateModel(parse(defaultFormat));
+                }
+                return cachedValue;
+            }
+            
+            @Override
+            public Date getAsDate() throws TemplateModelException {
+                return getAsDateModel().getAsDate();
+            }
+    
+            @Override
+            public int getDateType() {
+                return dateType;
+            }
+    
+            @Override
+            public boolean isEmpty() {
+                return false;
+            }
+    
+            private Object parse(TemplateDateFormat df)
+            throws TemplateModelException {
+                try {
+                    return df.parse(text, dateType);
+                } catch (TemplateValueFormatException e) {
+                    throw new _TemplateModelException(e,
+                            "The string doesn't match the expected date/time/date-time format. "
+                            + "The string to parse was: ", new _DelayedJQuote(text), ". ",
+                            "The expected format was: ", new _DelayedJQuote(df.getDescription()), ".",
+                            e.getMessage() != null ? "\nThe nested reason given follows:\n" : "",
+                            e.getMessage() != null ? e.getMessage() : "");
+                }
+            }
+            
+        }
+        
+        private final int dateType;
+        
+        dateBI(int dateType) {
+            this.dateType = dateType;
+        }
+        
+        @Override
+        TemplateModel _eval(Environment env)
+                throws TemplateException {
+            TemplateModel model = target.eval(env);
+            if (model instanceof TemplateDateModel) {
+                TemplateDateModel dmodel = (TemplateDateModel) model;
+                int dtype = dmodel.getDateType();
+                // Any date model can be coerced into its own type
+                if (dateType == dtype) {
+                    return model;
+                }
+                // unknown and datetime can be coerced into any date type
+                if (dtype == TemplateDateModel.UNKNOWN || dtype == TemplateDateModel.DATETIME) {
+                    return new SimpleDate(dmodel.getAsDate(), dateType);
+                }
+                throw new _MiscTemplateException(this,
+                            "Cannot convert ", TemplateDateModel.TYPE_NAMES.get(dtype),
+                            " to ", TemplateDateModel.TYPE_NAMES.get(dateType));
+            }
+            // Otherwise, interpret as a string and attempt 
+            // to parse it into a date.
+            String s = target.evalAndCoerceToPlainText(env);
+            return new DateParser(s, env);
+        }
+
+    }
+
+    static class apiBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            if (!env.getAPIBuiltinEnabled()) {
+                throw new _MiscTemplateException(this,
+                        "Can't use ?api, because the \"", MutableProcessingConfiguration.API_BUILTIN_ENABLED_KEY,
+                        "\" configuration setting is false. Think twice before you set it to true though. Especially, "
+                        + "it shouldn't abused for modifying Map-s and Collection-s.");
+            }
+            final TemplateModel tm = target.eval(env);
+            if (!(tm instanceof TemplateModelWithAPISupport)) {
+                target.assertNonNull(tm, env);
+                throw new APINotSupportedTemplateException(env, target, tm);
+            }
+            return ((TemplateModelWithAPISupport) tm).getAPI();
+        }
+    }
+
+    static class has_apiBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            final TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return tm instanceof TemplateModelWithAPISupport ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+    
+    static class is_booleanBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return (tm instanceof TemplateBooleanModel)  ?
+                TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_collectionBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return (tm instanceof TemplateCollectionModel) ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_collection_exBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return (tm instanceof TemplateCollectionModelEx) ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_dateLikeBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return (tm instanceof TemplateDateModel)  ?
+                TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_dateOfTypeBI extends ASTExpBuiltIn {
+        
+        private final int dateType;
+        
+        is_dateOfTypeBI(int dateType) {
+            this.dateType = dateType;
+        }
+
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return (tm instanceof TemplateDateModel) && ((TemplateDateModel) tm).getDateType() == dateType
+                ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_directiveBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            // WRONG: it also had to check ASTDirMacro.isFunction()
+            return (tm instanceof TemplateTransformModel || tm instanceof ASTDirMacro || tm instanceof TemplateDirectiveModel) ?
+                TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_enumerableBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return (tm instanceof TemplateSequenceModel || tm instanceof TemplateCollectionModel)
+                    ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_hash_exBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return (tm instanceof TemplateHashModelEx) ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_hashBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return (tm instanceof TemplateHashModel) ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_indexableBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return (tm instanceof TemplateSequenceModel) ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_macroBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            // WRONG: it also had to check ASTDirMacro.isFunction()
+            return (tm instanceof ASTDirMacro)  ?
+                TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_markup_outputBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return (tm instanceof TemplateMarkupOutputModel)  ?
+                TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+    
+    static class is_methodBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return (tm instanceof TemplateMethodModel)  ?
+                TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_nodeBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return (tm instanceof TemplateNodeModel)  ?
+                TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_numberBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return (tm instanceof TemplateNumberModel)  ?
+                TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_sequenceBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return tm instanceof TemplateSequenceModel
+                    ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_stringBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return (tm instanceof TemplateScalarModel)  ?
+                TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class is_transformBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            target.assertNonNull(tm, env);
+            return (tm instanceof TemplateTransformModel)  ?
+                TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    static class namespaceBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel tm = target.eval(env);
+            if (!(tm instanceof ASTDirMacro)) {
+                throw new UnexpectedTypeException(
+                        target, tm,
+                        "macro or function", new Class[] { ASTDirMacro.class },
+                        env);
+            } else {
+                return env.getMacroNamespace((ASTDirMacro) tm);
+            }
+        }
+    }
+
+    static class sizeBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel model = target.eval(env);
+
+            final int size;
+            if (model instanceof TemplateSequenceModel) {
+                size = ((TemplateSequenceModel) model).size();
+            } else if (model instanceof TemplateCollectionModelEx) {
+                size = ((TemplateCollectionModelEx) model).size();
+            } else if (model instanceof TemplateHashModelEx) {
+                size = ((TemplateHashModelEx) model).size();
+            } else {
+                throw new UnexpectedTypeException(
+                        target, model,
+                        "extended-hash or sequence or extended collection",
+                        new Class[] {
+                                TemplateHashModelEx.class,
+                                TemplateSequenceModel.class,
+                                TemplateCollectionModelEx.class
+                        },
+                        env);
+            }
+            return new SimpleNumber(size);
+        }
+    }
+    
+    static class stringBI extends ASTExpBuiltIn {
+        
+        private class BooleanFormatter
+        implements 
+            TemplateScalarModel, 
+            TemplateMethodModel {
+            private final TemplateBooleanModel bool;
+            private final Environment env;
+            
+            BooleanFormatter(TemplateBooleanModel bool, Environment env) {
+                this.bool = bool;
+                this.env = env;
+            }
+    
+            @Override
+            public Object exec(List args) throws TemplateModelException {
+                checkMethodArgCount(args, 2);
+                return new SimpleScalar((String) args.get(bool.getAsBoolean() ? 0 : 1));
+            }
+    
+            @Override
+            public String getAsString() throws TemplateModelException {
+                // Boolean should have come first... but that change would be non-BC. 
+                if (bool instanceof TemplateScalarModel) {
+                    return ((TemplateScalarModel) bool).getAsString();
+                } else {
+                    try {
+                        return env.formatBoolean(bool.getAsBoolean(), true);
+                    } catch (TemplateException e) {
+                        throw new TemplateModelException(e);
+                    }
+                }
+            }
+        }
+    
+        private class DateFormatter
+        implements
+            TemplateScalarModel,
+            TemplateHashModel,
+            TemplateMethodModel {
+            private final TemplateDateModel dateModel;
+            private final Environment env;
+            private final TemplateDateFormat defaultFormat;
+            private String cachedValue;
+    
+            DateFormatter(TemplateDateModel dateModel, Environment env)
+            throws TemplateException {
+                this.dateModel = dateModel;
+                this.env = env;
+                
+                final int dateType = dateModel.getDateType();
+                defaultFormat = dateType == TemplateDateModel.UNKNOWN
+                        ? null  // Lazy unknown type error in getAsString()
+                        : env.getTemplateDateFormat(
+                                dateType, _EvalUtil.modelToDate(dateModel, target).getClass(), target, true);
+            }
+    
+            @Override
+            public Object exec(List args) throws TemplateModelException {
+                checkMethodArgCount(args, 1);
+                return formatWith((String) args.get(0));
+            }
+
+            @Override
+            public TemplateModel get(String key)
+            throws TemplateModelException {
+                return formatWith(key);
+            }
+
+            private TemplateModel formatWith(String key)
+            throws TemplateModelException {
+                try {
+                    return new SimpleScalar(env.formatDateToPlainText(dateModel, key, target, stringBI.this, true));
+                } catch (TemplateException e) {
+                    // `e` should always be a TemplateModelException here, but to be sure: 
+                    throw _CoreAPI.ensureIsTemplateModelException("Failed to format value", e); 
+                }
+            }
+            
+            @Override
+            public String getAsString()
+            throws TemplateModelException {
+                if (cachedValue == null) {
+                    if (defaultFormat == null) {
+                        if (dateModel.getDateType() == TemplateDateModel.UNKNOWN) {
+                            throw MessageUtil.newCantFormatUnknownTypeDateException(target, null);
+                        } else {
+                            throw new BugException();
+                        }
+                    }
+                    try {
+                        cachedValue = _EvalUtil.assertFormatResultNotNull(defaultFormat.formatToPlainText(dateModel));
+                    } catch (TemplateValueFormatException e) {
+                        try {
+                            throw MessageUtil.newCantFormatDateException(defaultFormat, target, e, true);
+                        } catch (TemplateException e2) {
+                            // `e` should always be a TemplateModelException here, but to be sure: 
+                            throw _CoreAPI.ensureIsTemplateModelException("Failed to format date/time/datetime", e2); 
+                        }
+                    }
+                }
+                return cachedValue;
+            }
+    
+            @Override
+            public boolean isEmpty() {
+                return false;
+            }
+        }
+        
+        private class NumberFormatter
+        implements
+            TemplateScalarModel,
+            TemplateHashModel,
+            TemplateMethodModel {
+            private final TemplateNumberModel numberModel;
+            private final Number number;
+            private final Environment env;
+            private final TemplateNumberFormat defaultFormat;
+            private String cachedValue;
+    
+            NumberFormatter(TemplateNumberModel numberModel, Environment env) throws TemplateException {
+                this.env = env;
+                
+                // As we format lazily, we need a snapshot of the format inputs:
+                this.numberModel = numberModel;
+                number = _EvalUtil.modelToNumber(numberModel, target);  // for BackwardCompatibleTemplateNumberFormat-s
+                try {
+                    defaultFormat = env.getTemplateNumberFormat(stringBI.this, true);
+                } catch (TemplateException e) {
+                    // `e` should always be a TemplateModelException here, but to be sure: 
+                    throw _CoreAPI.ensureIsTemplateModelException("Failed to get default number format", e); 
+                }
+            }
+    
+            @Override
+            public Object exec(List args) throws TemplateModelException {
+                checkMethodArgCount(args, 1);
+                return get((String) args.get(0));
+            }
+    
+            @Override
+            public TemplateModel get(String key) throws TemplateModelException {
+                TemplateNumberFormat format;
+                try {
+                    format = env.getTemplateNumberFormat(key, stringBI.this, true);
+                } catch (TemplateException e) {
+                    // `e` should always be a TemplateModelException here, but to be sure: 
+                    throw _CoreAPI.ensureIsTemplateModelException("Failed to get number format", e); 
+                }
+                
+                String result;
+                try {
+                    result = env.formatNumberToPlainText(numberModel, format, target, true);
+                } catch (TemplateException e) {
+                    // `e` should always be a TemplateModelException here, but to be sure: 
+                    throw _CoreAPI.ensureIsTemplateModelException("Failed to format number", e); 
+                }
+                
+                return new SimpleScalar(result);
+            }
+            
+            @Override
+            public String getAsString() throws TemplateModelException {
+                if (cachedValue == null) {
+                    try {
+                        cachedValue = env.formatNumberToPlainText(numberModel, defaultFormat, target, true);
+                    } catch (TemplateException e) {
+                        // `e` should always be a TemplateModelException here, but to be sure: 
+                        throw _CoreAPI.ensureIsTemplateModelException("Failed to format number", e); 
+                    }
+                }
+                return cachedValue;
+            }
+    
+            @Override
+            public boolean isEmpty() {
+                return false;
+            }
+        }
+    
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel model = target.eval(env);
+            if (model instanceof TemplateNumberModel) {
+                TemplateNumberModel numberModel = (TemplateNumberModel) model;
+                Number num = _EvalUtil.modelToNumber(numberModel, target);
+                return new NumberFormatter(numberModel, env);
+            } else if (model instanceof TemplateDateModel) {
+                TemplateDateModel dm = (TemplateDateModel) model;
+                return new DateFormatter(dm, env);
+            } else if (model instanceof SimpleScalar) {
+                return model;
+            } else if (model instanceof TemplateBooleanModel) {
+                return new BooleanFormatter((TemplateBooleanModel) model, env);
+            } else if (model instanceof TemplateScalarModel) {
+                return new SimpleScalar(((TemplateScalarModel) model).getAsString());
+            } else {            
+                throw new UnexpectedTypeException(
+                        target, model,
+                        "number, date, boolean or string",
+                        new Class[] {
+                            TemplateNumberModel.class, TemplateDateModel.class, TemplateBooleanModel.class,
+                            TemplateScalarModel.class
+                        },
+                        env);
+            }
+        }
+    }
+
+    // Can't be instantiated
+    private BuiltInsForMultipleTypes() { }
+
+    static abstract class AbstractCBI extends ASTExpBuiltIn {
+        
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel model = target.eval(env);
+            if (model instanceof TemplateNumberModel) {
+                return formatNumber(env, model);
+            } else if (model instanceof TemplateBooleanModel) {
+                return new SimpleScalar(((TemplateBooleanModel) model).getAsBoolean()
+                        ? MiscUtil.C_TRUE : MiscUtil.C_FALSE);
+            } else {
+                throw new UnexpectedTypeException(
+                        target, model,
+                        "number or boolean", new Class[] { TemplateNumberModel.class, TemplateBooleanModel.class },
+                        env);
+            }
+        }
+    
+        protected abstract TemplateModel formatNumber(Environment env, TemplateModel model) throws TemplateModelException;
+        
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForNodes.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForNodes.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForNodes.java
new file mode 100644
index 0000000..39bc546
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForNodes.java
@@ -0,0 +1,154 @@
+/*
+ * 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.util.List;
+
+import org.apache.freemarker.core.model.TemplateMethodModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNodeModel;
+import org.apache.freemarker.core.model.TemplateNodeModelEx;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * A holder for builtins that operate exclusively on (XML-)node left-hand value.
+ */
+class BuiltInsForNodes {
+    
+    static class ancestorsBI extends BuiltInForNode {
+       @Override
+    TemplateModel calculateResult(TemplateNodeModel nodeModel, Environment env) throws TemplateModelException {
+           AncestorSequence result = new AncestorSequence(env);
+           TemplateNodeModel parent = nodeModel.getParentNode();
+           while (parent != null) {
+               result.add(parent);
+               parent = parent.getParentNode();
+           }
+           return result;
+       }
+    }
+    
+    static class childrenBI extends BuiltInForNode {
+       @Override
+    TemplateModel calculateResult(TemplateNodeModel nodeModel, Environment env) throws TemplateModelException {
+            return nodeModel.getChildNodes();
+       }
+    }
+    
+    static class node_nameBI extends BuiltInForNode {
+       @Override
+    TemplateModel calculateResult(TemplateNodeModel nodeModel, Environment env) throws TemplateModelException {
+            return new SimpleScalar(nodeModel.getNodeName());
+       }
+    }
+    
+    
+    static class node_namespaceBI extends BuiltInForNode {
+        @Override
+        TemplateModel calculateResult(TemplateNodeModel nodeModel, Environment env) throws TemplateModelException {
+            String nsURI = nodeModel.getNodeNamespace();
+            return nsURI == null ? null : new SimpleScalar(nsURI);
+        }
+    }
+    
+    static class node_typeBI extends BuiltInForNode {
+       @Override
+    TemplateModel calculateResult(TemplateNodeModel nodeModel, Environment env) throws TemplateModelException {
+            return new SimpleScalar(nodeModel.getNodeType());
+        }
+    }
+
+    static class parentBI extends BuiltInForNode {
+       @Override
+    TemplateModel calculateResult(TemplateNodeModel nodeModel, Environment env) throws TemplateModelException {
+            return nodeModel.getParentNode();
+       }
+    }
+    
+    static class rootBI extends BuiltInForNode {
+       @Override
+    TemplateModel calculateResult(TemplateNodeModel nodeModel, Environment env) throws TemplateModelException {
+            TemplateNodeModel result = nodeModel;
+            TemplateNodeModel parent = nodeModel.getParentNode();
+            while (parent != null) {
+                result = parent;
+                parent = result.getParentNode();
+            }
+            return result;
+       }
+    }
+
+    static class previousSiblingBI extends BuiltInForNodeEx {
+        @Override
+        TemplateModel calculateResult(TemplateNodeModelEx nodeModel, Environment env) throws TemplateModelException {
+            return nodeModel.getPreviousSibling();
+        }
+    }
+
+    static class nextSiblingBI extends  BuiltInForNodeEx {
+        @Override
+        TemplateModel calculateResult(TemplateNodeModelEx nodeModel, Environment env) throws TemplateModelException {
+            return nodeModel.getNextSibling();
+        }
+    }
+    
+    // Can't be instantiated
+    private BuiltInsForNodes() { }
+
+    static class AncestorSequence extends NativeSequence implements TemplateMethodModel {
+
+        private static final int INITIAL_CAPACITY = 12;
+
+        private Environment env;
+        
+        AncestorSequence(Environment env) {
+            super(INITIAL_CAPACITY);
+            this.env = env;
+        }
+        
+        @Override
+        public Object exec(List names) throws TemplateModelException {
+            if (names == null || names.isEmpty()) {
+                return this;
+            }
+            AncestorSequence result = new AncestorSequence(env);
+            for (int i = 0; i < size(); i++) {
+                TemplateNodeModel tnm = (TemplateNodeModel) get(i);
+                String nodeName = tnm.getNodeName();
+                String nsURI = tnm.getNodeNamespace();
+                if (nsURI == null) {
+                    if (names.contains(nodeName)) {
+                        result.add(tnm);
+                    }
+                } else {
+                    for (int j = 0; j < names.size(); j++) {
+                        if (_StringUtil.matchesQName((String) names.get(j), nodeName, nsURI, env)) {
+                            result.add(tnm);
+                            break;
+                        }
+                    }
+                }
+            }
+            return result;
+        }
+    }    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForNumbers.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForNumbers.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForNumbers.java
new file mode 100644
index 0000000..58a2aa6
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForNumbers.java
@@ -0,0 +1,319 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Date;
+
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.impl.SimpleDate;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.apache.freemarker.core.util._NumberUtil;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * A holder for builtins that operate exclusively on number left-hand value.
+ */
+class BuiltInsForNumbers {
+
+    private static abstract class abcBI extends BuiltInForNumber {
+
+        @Override
+        TemplateModel calculateResult(Number num, TemplateModel model) throws TemplateModelException {
+            final int n;
+            try {
+                n = _NumberUtil.toIntExact(num);
+            } catch (ArithmeticException e) {
+                throw new _TemplateModelException(target,
+                        "The left side operand value isn't compatible with ?", key, ": ", e.getMessage());
+         
+            }
+            if (n <= 0) {
+                throw new _TemplateModelException(target,
+                        "The left side operand of to ?", key, " must be at least 1, but was ", Integer.valueOf(n), ".");
+            }
+            return new SimpleScalar(toABC(n));
+        }
+
+        protected abstract String toABC(int n);
+        
+    }
+
+    static class lower_abcBI extends abcBI {
+
+        @Override
+        protected String toABC(int n) {
+            return _StringUtil.toLowerABC(n);
+        }
+        
+    }
+
+    static class upper_abcBI extends abcBI {
+
+        @Override
+        protected String toABC(int n) {
+            return _StringUtil.toUpperABC(n);
+        }
+        
+    }
+    
+    static class absBI extends BuiltInForNumber {
+        @Override
+        TemplateModel calculateResult(Number num, TemplateModel model) throws TemplateModelException {
+            if (num instanceof Integer) {
+                int n = num.intValue();
+                if (n < 0) {
+                    return new SimpleNumber(-n);
+                } else {
+                    return model;
+                }
+            } else if (num instanceof BigDecimal) {
+                BigDecimal n = (BigDecimal) num;
+                if (n.signum() < 0) {
+                    return new SimpleNumber(n.negate());
+                } else {
+                    return model;
+                }
+            } else if (num instanceof Double) {
+                double n = num.doubleValue();
+                if (n < 0) {
+                    return new SimpleNumber(-n);
+                } else {
+                    return model;
+                }
+            } else if (num instanceof Float) {
+                float n = num.floatValue();
+                if (n < 0) {
+                    return new SimpleNumber(-n);
+                } else {
+                    return model;
+                }
+            } else if (num instanceof Long) {
+                long n = num.longValue();
+                if (n < 0) {
+                    return new SimpleNumber(-n);
+                } else {
+                    return model;
+                }
+            } else if (num instanceof Short) {
+                short n = num.shortValue();
+                if (n < 0) {
+                    return new SimpleNumber(-n);
+                } else {
+                    return model;
+                }
+            } else if (num instanceof Byte) {
+                byte n = num.byteValue();
+                if (n < 0) {
+                    return new SimpleNumber(-n);
+                } else {
+                    return model;
+                }
+            } else if (num instanceof BigInteger) {
+                BigInteger n = (BigInteger) num;
+                if (n.signum() < 0) {
+                    return new SimpleNumber(n.negate());
+                } else {
+                    return model;
+                }
+            } else {
+                throw new _TemplateModelException("Unsupported number class: ", num.getClass());
+            }            
+        }
+    }
+    
+    static class byteBI extends BuiltInForNumber {
+        @Override
+        TemplateModel calculateResult(Number num, TemplateModel model) {
+            if (num instanceof Byte) {
+                return model;
+            }
+            return new SimpleNumber(Byte.valueOf(num.byteValue()));
+        }
+    }
+
+    static class ceilingBI extends BuiltInForNumber {
+        @Override
+        TemplateModel calculateResult(Number num, TemplateModel model) {
+            return new SimpleNumber(new BigDecimal(num.doubleValue()).divide(BIG_DECIMAL_ONE, 0, BigDecimal.ROUND_CEILING));
+        }
+    }
+
+    static class doubleBI extends BuiltInForNumber {
+        @Override
+        TemplateModel calculateResult(Number num, TemplateModel model) {
+            if (num instanceof Double) {
+                return model;
+            }
+            return new SimpleNumber(num.doubleValue());
+        }
+    }
+
+    static class floatBI extends BuiltInForNumber {
+        @Override
+        TemplateModel calculateResult(Number num, TemplateModel model) {
+            if (num instanceof Float) {
+                return model;
+            }
+            return new SimpleNumber(num.floatValue());
+        }
+    }
+
+    static class floorBI extends BuiltInForNumber {
+        @Override
+        TemplateModel calculateResult(Number num, TemplateModel model) {
+            return new SimpleNumber(new BigDecimal(num.doubleValue()).divide(BIG_DECIMAL_ONE, 0, BigDecimal.ROUND_FLOOR));
+        }
+    }
+
+    static class intBI extends BuiltInForNumber {
+        @Override
+        TemplateModel calculateResult(Number num, TemplateModel model) {
+            if (num instanceof Integer) {
+                return model;
+            }
+            return new SimpleNumber(num.intValue());
+        }
+    }
+
+    static class is_infiniteBI extends BuiltInForNumber {
+        @Override
+        TemplateModel calculateResult(Number num, TemplateModel model) throws TemplateModelException {
+            return _NumberUtil.isInfinite(num) ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+
+    static class is_nanBI extends BuiltInForNumber {
+        @Override
+        TemplateModel calculateResult(Number num, TemplateModel model) throws TemplateModelException {
+            return _NumberUtil.isNaN(num) ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        }
+    }
+
+    // Does both someNumber?long and someDate?long, thus it doesn't extend NumberBuiltIn
+    static class longBI extends ASTExpBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env)
+                throws TemplateException {
+            TemplateModel model = target.eval(env);
+            if (!(model instanceof TemplateNumberModel)
+                    && model instanceof TemplateDateModel) {
+                Date date = _EvalUtil.modelToDate((TemplateDateModel) model, target);
+                return new SimpleNumber(date.getTime());
+            } else {
+                Number num = target.modelToNumber(model, env);
+                if (num instanceof Long) {
+                    return model;
+                }
+                return new SimpleNumber(num.longValue());
+            }
+        }
+    }
+
+    static class number_to_dateBI extends BuiltInForNumber {
+        
+        private final int dateType;
+        
+        number_to_dateBI(int dateType) {
+            this.dateType = dateType;
+        }
+        
+        @Override
+        TemplateModel calculateResult(Number num, TemplateModel model)
+        throws TemplateModelException {
+            return new SimpleDate(new Date(safeToLong(num)), dateType);
+        }
+    }
+
+    static class roundBI extends BuiltInForNumber {
+        private static final BigDecimal half = new BigDecimal("0.5");
+        @Override
+        TemplateModel calculateResult(Number num, TemplateModel model) {
+            return new SimpleNumber(new BigDecimal(num.doubleValue()).add(half).divide(BIG_DECIMAL_ONE, 0, BigDecimal.ROUND_FLOOR));
+        }
+    }
+    
+    static class shortBI extends BuiltInForNumber {
+        @Override
+        TemplateModel calculateResult(Number num, TemplateModel model) {
+            if (num instanceof Short) {
+                return model;
+            }
+            return new SimpleNumber(Short.valueOf(num.shortValue()));
+        }
+    }
+
+    private static long safeToLong(Number num) throws TemplateModelException {
+        if (num instanceof Double) {
+            double d = Math.round(num.doubleValue());
+            if (d > Long.MAX_VALUE || d < Long.MIN_VALUE) {
+                throw new _TemplateModelException(
+                        "Number doesn't fit into a 64 bit signed integer (long): ", Double.valueOf(d));
+            } else {
+                return (long) d;
+            }
+        } else if (num instanceof Float) {
+            float f = Math.round(num.floatValue());
+            if (f > Long.MAX_VALUE || f < Long.MIN_VALUE) {
+                throw new _TemplateModelException(
+                        "Number doesn't fit into a 64 bit signed integer (long): ", Float.valueOf(f));
+            } else {
+                return (long) f;
+            }
+        } else if (num instanceof BigDecimal) {
+            BigDecimal bd = ((BigDecimal) num).setScale(0, BigDecimal.ROUND_HALF_UP);
+            if (bd.compareTo(BIG_DECIMAL_LONG_MAX) > 0 || bd.compareTo(BIG_DECIMAL_LONG_MIN) < 0) {
+                throw new _TemplateModelException("Number doesn't fit into a 64 bit signed integer (long): ", bd);
+            } else {
+                return bd.longValue();
+            }
+        } else if (num instanceof BigInteger) {
+            BigInteger bi = (BigInteger) num;
+            if (bi.compareTo(BIG_INTEGER_LONG_MAX) > 0 || bi.compareTo(BIG_INTEGER_LONG_MIN) < 0) {
+                throw new _TemplateModelException("Number doesn't fit into a 64 bit signed integer (long): ", bi);
+            } else {
+                return bi.longValue();
+            }
+        } else if (num instanceof Long || num instanceof Integer || num instanceof Byte || num instanceof Short) {
+            return num.longValue();
+        } else {
+            // Should add Atomic* types in 2.4...
+            throw new _TemplateModelException("Unsupported number type: ", num.getClass());
+        }
+    }
+    
+    private static final BigDecimal BIG_DECIMAL_ONE = new BigDecimal("1");
+    private static final BigDecimal BIG_DECIMAL_LONG_MIN = BigDecimal.valueOf(Long.MIN_VALUE); 
+    private static final BigDecimal BIG_DECIMAL_LONG_MAX = BigDecimal.valueOf(Long.MAX_VALUE);
+    private static final BigInteger BIG_INTEGER_LONG_MIN = BigInteger.valueOf(Long.MIN_VALUE); 
+    
+    private static final BigInteger BIG_INTEGER_LONG_MAX = BigInteger.valueOf(Long.MAX_VALUE);
+    
+    // Can't be instantiated
+    private BuiltInsForNumbers() { }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForOutputFormatRelated.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForOutputFormatRelated.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForOutputFormatRelated.java
new file mode 100644
index 0000000..2ae2fe7
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForOutputFormatRelated.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.outputformat.MarkupOutputFormat;
+
+class BuiltInsForOutputFormatRelated {
+
+    static class no_escBI extends AbstractConverterBI {
+
+        @Override
+        protected TemplateModel calculateResult(String lho, MarkupOutputFormat outputFormat, Environment env)
+                throws TemplateException {
+            return outputFormat.fromMarkup(lho);
+        }
+        
+    }
+
+    static class escBI extends AbstractConverterBI {
+
+        @Override
+        protected TemplateModel calculateResult(String lho, MarkupOutputFormat outputFormat, Environment env)
+                throws TemplateException {
+            return outputFormat.fromPlainTextByEscaping(lho);
+        }
+        
+    }
+    
+    static abstract class AbstractConverterBI extends MarkupOutputFormatBoundBuiltIn {
+
+        @Override
+        protected TemplateModel calculateResult(Environment env) throws TemplateException {
+            TemplateModel lhoTM = target.eval(env);
+            Object lhoMOOrStr = _EvalUtil.coerceModelToStringOrMarkup(lhoTM, target, null, env);
+            MarkupOutputFormat contextOF = outputFormat;
+            if (lhoMOOrStr instanceof String) { // TemplateMarkupOutputModel
+                return calculateResult((String) lhoMOOrStr, contextOF, env);
+            } else {
+                TemplateMarkupOutputModel lhoMO = (TemplateMarkupOutputModel) lhoMOOrStr;
+                MarkupOutputFormat lhoOF = lhoMO.getOutputFormat();
+                // ATTENTION: Keep this logic in sync. with ${...}'s logic!
+                if (lhoOF == contextOF || contextOF.isOutputFormatMixingAllowed()) {
+                    // bypass
+                    return lhoMO;
+                } else {
+                    // ATTENTION: Keep this logic in sync. with ${...}'s logic!
+                    String lhoPlainTtext = lhoOF.getSourcePlainText(lhoMO);
+                    if (lhoPlainTtext == null) {
+                        throw new _TemplateModelException(target,
+                                "The left side operand of ?", key, " is in ", new _DelayedToString(lhoOF),
+                                " format, which differs from the current output format, ",
+                                new _DelayedToString(contextOF), ". Conversion wasn't possible.");
+                    }
+                    // Here we know that lho is escaped plain text. So we re-escape it to the current format and
+                    // bypass it, just as if the two output formats were the same earlier.
+                    return contextOF.fromPlainTextByEscaping(lhoPlainTtext);
+                }
+            }
+        }
+        
+        protected abstract TemplateModel calculateResult(String lho, MarkupOutputFormat outputFormat, Environment env)
+                throws TemplateException;
+        
+    }
+    
+}


[16/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupContext.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupContext.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupContext.java
new file mode 100644
index 0000000..0a3b33a
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupContext.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.templateresolver;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Locale;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateLookupStrategy;
+
+/**
+ * Used as the parameter of {@link TemplateLookupStrategy#lookup(TemplateLookupContext)}.
+ * You can't invoke instances of this, only receive them from FreeMarker.
+ * 
+ * @since 2.3.22
+ */
+public abstract class TemplateLookupContext<R extends TemplateLookupResult> {
+    
+    private final String templateName;
+    private final Locale templateLocale;
+    private final Object customLookupCondition;
+
+    /**
+     * Finds the template source based on its <em>normalized</em> name; handles {@code *} steps (so called acquisition),
+     * otherwise it just calls {@link TemplateLoader#load(String, TemplateLoadingSource, Serializable,
+     * TemplateLoaderSession)}.
+     * 
+     * @param templateName
+     *            Must be a normalized name, like {@code "foo/bar/baaz.ftl"}. A name is not normalized when, among
+     *            others, it starts with {@code /}, or contains {@code .} or {@code ..} path steps, or it uses
+     *            backslash ({@code \}) instead of {@code /}. A normalized name might contains "*" path steps
+     *            (acquisition).
+     * 
+     * @return The result of the lookup. Not {@code null}; check {@link TemplateLookupResult#isPositive()} to see if the
+     *         lookup has found anything. Note that in a positive result the content of the template is possibly
+     *         also already loaded (this is the case for {@link TemplateLoader}-s when the cached content is stale, but
+     *         not for {@link TemplateLoader}-s). Hence discarding a positive result and looking for another can
+     *         generate substantial extra I/O.
+     */
+    public abstract R lookupWithAcquisitionStrategy(String templateName) throws IOException;
+
+    /**
+     * Finds the template source based on its <em>normalized</em> name; tries localized variations going from most
+     * specific to less specific, and for each variation it delegates to {@link #lookupWithAcquisitionStrategy(String)}.
+     * If {@code templateLocale} is {@code null} (typically, because {@link Configuration#getLocalizedLookup()} is
+     * {@code false})), then it's the same as calling {@link #lookupWithAcquisitionStrategy(String)} directly. This is
+     * the default strategy of FreeMarker (at least in 2.3.x), so for more information, see
+     * {@link DefaultTemplateLookupStrategy#INSTANCE}.
+     */
+    public abstract R lookupWithLocalizedThenAcquisitionStrategy(String templateName,
+            Locale templateLocale) throws IOException;
+    
+    /** Default visibility to prevent extending the class from outside this package. */
+    protected TemplateLookupContext(String templateName, Locale templateLocale, Object customLookupCondition) {
+        this.templateName = templateName;
+        this.templateLocale = templateLocale;
+        this.customLookupCondition = customLookupCondition;
+    }
+
+    /**
+     * The normalized name (path) of the template (relatively to the {@link TemplateLoader}). Not {@code null}. 
+     */
+    public String getTemplateName() {
+        return templateName;
+    }
+
+    /**
+     * {@code null} if localized lookup is disabled (see {@link Configuration#getLocalizedLookup()}), otherwise the
+     * locale requested.
+     */
+    public Locale getTemplateLocale() {
+        return templateLocale;
+    }
+
+    /**
+     * Returns the value of the {@code customLookupCondition} parameter of
+     * {@link Configuration#getTemplate(String, Locale, Serializable, boolean)}; see requirements there, such
+     * as having a proper {@link Object#equals(Object)} and {@link Object#hashCode()} method. The interpretation of this
+     * value is up to the custom {@link TemplateLookupStrategy}. Usually, it's used similarly to as the default lookup
+     * strategy uses {@link #getTemplateLocale()}, that is, to look for a template variation that satisfies the
+     * condition, and then maybe fall back to more generic template if that's missing.
+     */
+    public Object getCustomLookupCondition() {
+        return customLookupCondition;
+    }
+
+    /**
+     * Creates a not-found lookup result that then can be used as the return value of
+     * {@link TemplateLookupStrategy#lookup(TemplateLookupContext)}. (In the current implementation it just always
+     * returns the same static singleton, but that might need to change in the future.)
+     */
+    public abstract R createNegativeLookupResult();
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupResult.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupResult.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupResult.java
new file mode 100644
index 0000000..d9a2594
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupResult.java
@@ -0,0 +1,54 @@
+/*
+ * 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.templateresolver;
+
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core.templateresolver.impl.TemplateLoaderBasedTemplateLookupResult;
+
+/**
+ * The return value of {@link TemplateLookupStrategy#lookup(TemplateLookupContext)} and similar lookup methods. You
+ * usually get one from {@link TemplateLookupContext#lookupWithAcquisitionStrategy(String)} or
+ * {@link TemplateLookupContext#createNegativeLookupResult()}.
+ * 
+ * <p>
+ * Subclass this only if you are implementing a {@link TemplateLookupContext}; if the {@link TemplateLookupContext} that
+ * you are implementing uses {@link TemplateLoader}-s, consider using {@link TemplateLoaderBasedTemplateLookupResult}
+ * instead of writing your own subclass.
+ * 
+ * @since 2.3.22
+ */
+public abstract class TemplateLookupResult {
+
+    protected TemplateLookupResult() {
+        // nop
+    }
+    
+    /**
+     * The source name of the template found (see {@link Template#getSourceName()}), or {@code null} if
+     * {@link #isPositive()} is {@code false}.
+     */
+    public abstract String getTemplateSourceName();
+
+    /**
+     * Tells if the lookup has found a matching template.
+     */
+    public abstract boolean isPositive();
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupStrategy.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupStrategy.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupStrategy.java
new file mode 100644
index 0000000..7021b5b
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupStrategy.java
@@ -0,0 +1,78 @@
+/*
+ * 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.templateresolver;
+
+import java.io.IOException;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.Template;
+
+/**
+ * Finds the {@link TemplateLoader}-level (storage-level) template source for the template name with which the template
+ * was requested (as in {@link Configuration#getTemplate(String)}). This usually means trying various
+ * {@link TemplateLoader}-level template names (so called source names; see also {@link Template#getSourceName()}) that
+ * were deduced from the requested name. Trying a name usually means calling
+ * {@link TemplateLookupContext#lookupWithAcquisitionStrategy(String)} with it and checking the value of
+ * {@link TemplateLookupResult#isPositive()}.
+ * 
+ * <p>
+ * Before you write your own lookup strategy, know that:
+ * <ul>
+ * <li>A template lookup strategy meant to operate solely with template names, not with {@link TemplateLoader}-s
+ * directly. Basically, it's a mapping between the template names that templates and API-s like
+ * {@link Configuration#getTemplate(String)} see, and those that the underlying {@link TemplateLoader} sees.
+ * <li>A template lookup strategy doesn't influence the template's name ({@link Template#getLookupName()}), which is the
+ * normalized form of the template name as it was requested (with {@link Configuration#getTemplate(String)}, etc.). It
+ * only influences the so called source name of the template ({@link Template#getSourceName()}). The template's name is
+ * used as the basis for resolving relative inclusions/imports in the template. The source name is pretty much only used
+ * in error messages as error location, and of course, to actually load the template "file".
+ * <li>Understand the impact of the last point if your template lookup strategy fiddles not only with the file name part
+ * of the template name, but also with the directory part. For example, one may want to map "foo.ftl" to "en/foo.ftl",
+ * "fr/foo.ftl", etc. That's legal, but the result is kind of like if you had several root directories ("en/", "fr/",
+ * etc.) that are layered over each other to form a single merged directory. (This is what's desirable in typical
+ * applications, yet it can be confusing.)
+ * </ul>
+ * 
+ * @see Configuration#getTemplateLookupStrategy()
+ * 
+ * @since 2.3.22
+ */
+public abstract class TemplateLookupStrategy {
+
+    /**
+     * Finds the template source that matches the template name, locale (if not {@code null}) and other parameters
+     * specified in the {@link TemplateLookupContext}. See also the class-level {@link TemplateLookupStrategy}
+     * documentation to understand lookup strategies more.
+     * 
+     * @param ctx
+     *            Contains the parameters for which the matching template need to be found, and operations that
+     *            are needed to implement the strategy. Some of the important input parameters are:
+     *            {@link TemplateLookupContext#getTemplateName()}, {@link TemplateLookupContext#getTemplateLocale()}.
+     *            The most important operations are {@link TemplateLookupContext#lookupWithAcquisitionStrategy(String)}
+     *            and {@link TemplateLookupContext#createNegativeLookupResult()}. (Note that you deliberately can't
+     *            use {@link TemplateLoader}-s directly to implement lookup.)
+     * 
+     * @return Usually the return value of {@link TemplateLookupContext#lookupWithAcquisitionStrategy(String)}, or
+     *         {@code TemplateLookupContext#createNegativeLookupResult()} if no matching template exists. Can't be
+     *         {@code null}.
+     */
+    public abstract <R extends TemplateLookupResult> R lookup(TemplateLookupContext<R> ctx) throws IOException;
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateNameFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateNameFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateNameFormat.java
new file mode 100644
index 0000000..57773f4
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateNameFormat.java
@@ -0,0 +1,53 @@
+/*
+ * 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.templateresolver;
+
+/**
+ * Symbolizes a template name format, which defines the basic syntax of names through algorithms such as normalization.
+ */
+// TODO [FM3] Before it becomes a BC problem, shouldn't we add methods like splitting to directory name and file name?
+public abstract class TemplateNameFormat {
+
+    protected TemplateNameFormat() {
+       //  
+    }
+    
+    /**
+     * Implements {@link TemplateResolver#toRootBasedName(String, String)}; see more there.
+     */
+    public abstract String toRootBasedName(String baseName, String targetName) throws MalformedTemplateNameException;
+    
+    /**
+     * Implements {@link TemplateResolver#normalizeRootBasedName(String)}; see more there.
+     */
+    public abstract String normalizeRootBasedName(String name) throws MalformedTemplateNameException;
+
+    protected void checkNameHasNoNullCharacter(final String name) throws MalformedTemplateNameException {
+        if (name.indexOf(0) != -1) {
+            throw new MalformedTemplateNameException(name,
+                    "Null character (\\u0000) in the name; possible attack attempt");
+        }
+    }
+    
+    protected MalformedTemplateNameException newRootLeavingException(final String name) {
+        return new MalformedTemplateNameException(name, "Backing out from the root directory is not allowed");
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateResolver.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateResolver.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateResolver.java
new file mode 100644
index 0000000..bf7280a
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateResolver.java
@@ -0,0 +1,166 @@
+/*
+ * 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.templateresolver;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Locale;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.ParseException;
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core.TemplateNotFoundException;
+import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateResolver;
+
+/**
+ * This class was introduced to allow users to fully implement the template lookup, loading and caching logic,
+ * in case the standard mechanism ({@link DefaultTemplateResolver}) is not flexible enough. By implementing this class,
+ * you can take over the duty of the following {@link Configuration} settings, and it's up to the implementation if you
+ * delegate some of those duties back to the {@link Configuration} setting:
+ * 
+ * <ul>
+ * <li>{@link Configuration#getTemplateLoader() templateLoader}
+ * <li>{@link Configuration#getTemplateNameFormat() templateNameFormat}
+ * <li>{@link Configuration#getTemplateLookupStrategy() templateLookupStrategy}
+ * <li>{@link Configuration#getCacheStorage() cacheStorage}
+ * </ul>
+ * 
+ * @since 3.0.0
+ */
+//TODO DRAFT only [FM3]
+public abstract class TemplateResolver {
+
+    private final Configuration configuration;
+
+    protected TemplateResolver(Configuration configuration) {
+        this.configuration = configuration;
+    }
+
+    public Configuration getConfiguration() {
+        return configuration;
+    }
+
+    /**
+     * Retrieves the parsed template with the given name (and according the specified further parameters), or returns a
+     * result that indicates that no such template exists. The result should come from a cache most of the time
+     * (avoiding I/O and template parsing), as this method is typically called frequently.
+     * 
+     * <p>
+     * All parameters must be non-{@code null}, except {@code customLookupCondition}. For the meaning of the parameters
+     * see {@link Configuration#getTemplate(String, Locale, Serializable, boolean)}.
+     *
+     * @return A {@link GetTemplateResult} object that contains the {@link Template}, or a
+     *         {@link GetTemplateResult} object that contains {@code null} as the {@link Template} and information
+     *         about the missing template. The return value itself is never {@code null}. Note that exceptions occurring
+     *         during template loading mustn't be treated as a missing template, they must cause an exception to be
+     *         thrown by this method instead of returning a {@link GetTemplateResult}. The idea is that having a
+     *         missing template is normal (not exceptional), because of how some lookup strategies work. That the
+     *         backing storage mechanism should indeed check that it's missing though, and not cover an error as such.
+     * 
+     * @throws MalformedTemplateNameException
+     *             If the {@code name} was malformed. This is certainly originally thrown by
+     *             {@link #normalizeRootBasedName(String)}; see more there.
+     * 
+     * @throws IOException
+     *             If reading the template has failed from a reason other than the template is missing. This method
+     *             should never be a {@link TemplateNotFoundException}, as that condition is indicated in the return
+     *             value.
+     */
+    // [FM3] This parameters will be removed: String encoding
+    public abstract GetTemplateResult getTemplate(String name, Locale locale, Serializable customLookupCondition)
+            throws MalformedTemplateNameException, ParseException, IOException;
+
+    /**
+     * Clears the cache of templates, to enforce re-loading templates when they are get next time; this is an optional
+     * operation.
+     * 
+     * <p>
+     * Note that if the {@link TemplateResolver} implementation uses {@link TemplateLoader}-s, it should also call
+     * {@link TemplateLoader#resetState()} on them.
+     * 
+     * <p>
+     * This method is thread-safe and can be called while the engine processes templates.
+     * 
+     * @throws UnsupportedOperationException If the {@link TemplateResolver} implementation doesn't support this
+     *        operation.
+     */
+    public abstract void clearTemplateCache() throws UnsupportedOperationException;
+
+    /**
+     * Removes a template from the template cache, hence forcing the re-loading of it when it's next time requested;
+     * this is an optional operation. This is to give the application finer control over cache updating than the
+     * {@link Configuration#getTemplateUpdateDelayMilliseconds() templateUpdateDelayMilliseconds} setting alone gives.
+     * 
+     * <p>
+     * For the meaning of the parameters, see {@link #getTemplate(String, Locale, Serializable)}
+     * 
+     * <p>
+     * This method is thread-safe and can be called while the engine processes templates.
+     * 
+     * @throws UnsupportedOperationException If the {@link TemplateResolver} implementation doesn't support this
+     *        operation.
+     */
+    public abstract void removeTemplateFromCache(String name, Locale locale, Serializable customLookupCondition)
+            throws IOException, UnsupportedOperationException;
+
+    /**
+     * Converts a name to a template root directory based name, so that it can be used to find a template without
+     * knowing what (like which template) has referred to it. The rules depend on the name format, but a typical example
+     * is converting "t.ftl" with base "sub/contex.ftl" to "sub/t.ftl".
+     * 
+     * <p>
+     * Some implementations, notably {@link DefaultTemplateResolver}, delegates this check to the
+     * {@link TemplateNameFormat} coming from the {@link Configuration}.
+     * 
+     * @param baseName
+     *            Maybe a file name, maybe a directory name. The meaning of file name VS directory name depends on the
+     *            name format, but typically, something like "foo/bar/" is a directory name, and something like
+     *            "foo/bar" is a file name, and thus in the last case the effective base is "foo/" (i.e., the directory
+     *            that contains the file). Not {@code null}.
+     * @param targetName
+     *            The name to convert. This usually comes from a template that refers to another template by name. It
+     *            can be a relative name, or an absolute name. (In typical name formats absolute names start with
+     *            {@code "/"} or maybe with an URL scheme, and all others are relative). Not {@code null}.
+     * 
+     * @return The path in template root directory relative format, or even an absolute name (where the root directory
+     *         is not the real root directory of the file system, but the imaginary directory that exists to store the
+     *         templates). The standard implementations shipped with FreeMarker always return a root relative path
+     *         (except if the name starts with an URI schema, in which case a full URI is returned).
+     */
+    public abstract String toRootBasedName(String baseName, String targetName) throws MalformedTemplateNameException;
+
+    /**
+     * Normalizes a template root directory based name (relative to the root or absolute), so that equivalent names
+     * become equivalent according {@link String#equals(Object)} too. The rules depend on the name format, but typical
+     * examples are "sub/../t.ftl" to "t.ftl", "sub/./t.ftl" to "sub/t.ftl" and "/t.ftl" to "t.ftl".
+     * 
+     * <p>
+     * Some implementations, notably {@link DefaultTemplateResolver}, delegates this check to the {@link TemplateNameFormat}
+     * coming from the {@link Configuration}. The standard {@link TemplateNameFormat} implementations shipped with
+     * FreeMarker always returns a root relative path (except if the name starts with an URI schema, in which case a
+     * full URI is returned), for example, "/foo.ftl" becomes to "foo.ftl".
+     * 
+     * @param name
+     *            The root based name. Not {@code null}.
+     * 
+     * @return The normalized root based name. Not {@code null}.
+     */
+    public abstract String normalizeRootBasedName(String name) throws MalformedTemplateNameException;
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateSourceMatcher.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateSourceMatcher.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateSourceMatcher.java
new file mode 100644
index 0000000..ca38c39
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateSourceMatcher.java
@@ -0,0 +1,30 @@
+/*
+ * 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.templateresolver;
+
+import java.io.IOException;
+
+/**
+ * @since 2.3.24
+ */
+public abstract class TemplateSourceMatcher {
+    
+    abstract boolean matches(String sourceName, Object templateSource) throws IOException;
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/_CacheAPI.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/_CacheAPI.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/_CacheAPI.java
new file mode 100644
index 0000000..09b216f
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/_CacheAPI.java
@@ -0,0 +1,43 @@
+/*
+ * 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.templateresolver;
+
+/**
+ * For internal use only; don't depend on this, there's no backward compatibility guarantee at all!
+ * This class is to work around the lack of module system in Java, i.e., so that other FreeMarker packages can
+ * access things inside this package that users shouldn't. 
+ */ 
+public final class _CacheAPI {
+
+    private _CacheAPI() {
+        // Not meant to be instantiated
+    }
+    
+    public static String toRootBasedName(TemplateNameFormat templateNameFormat, String baseName, String targetName)
+            throws MalformedTemplateNameException {
+        return templateNameFormat.toRootBasedName(baseName, targetName);
+    }
+
+    public static String normalizeRootBasedName(TemplateNameFormat templateNameFormat, String name)
+            throws MalformedTemplateNameException {
+        return templateNameFormat.normalizeRootBasedName(name);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/ByteArrayTemplateLoader.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/ByteArrayTemplateLoader.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/ByteArrayTemplateLoader.java
new file mode 100644
index 0000000..417566f
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/ByteArrayTemplateLoader.java
@@ -0,0 +1,199 @@
+/*
+ * 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.templateresolver.impl;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.apache.freemarker.core.templateresolver.CacheStorage;
+import org.apache.freemarker.core.templateresolver.TemplateLoader;
+import org.apache.freemarker.core.templateresolver.TemplateLoaderSession;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingResult;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingSource;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * A {@link TemplateLoader} that uses a {@link Map} with {@code byte[]} as its source of templates. This is similar to
+ * {@link StringTemplateLoader}, but uses {@code byte[]} instead of {@link String}; see more details there.
+ * 
+ * <p>Note that {@link ByteArrayTemplateLoader} can't be used with a distributed (cluster-wide) {@link CacheStorage},
+ * as it produces {@link TemplateLoadingSource}-s that deliberately throw exception on serialization (because the
+ * content is only accessible within a single JVM, and is also volatile).
+ */
+// TODO JUnit tests
+public class ByteArrayTemplateLoader implements TemplateLoader {
+    
+    private static final AtomicLong INSTANCE_COUNTER = new AtomicLong();
+    
+    private final long instanceId = INSTANCE_COUNTER.get();
+    private final AtomicLong templatesRevision = new AtomicLong();
+    private final ConcurrentMap<String, ContentHolder> templates = new ConcurrentHashMap<>();
+    
+    /**
+     * Puts a template into the template loader. The name can contain slashes to denote logical directory structure, but
+     * must not start with a slash. Each template will get an unique revision number, thus replacing a template will
+     * cause the template cache to reload it (when the update delay expires).
+     * 
+     * <p>This method is thread-safe.
+     * 
+     * @param name
+     *            the name of the template.
+     * @param content
+     *            the source code of the template.
+     */
+    public void putTemplate(String name, byte[] content) {
+        templates.put(
+                name,
+                new ContentHolder(content, new Source(instanceId, name), templatesRevision.incrementAndGet()));
+    }
+    
+    /**
+     * Removes the template with the specified name if it was added earlier.
+     * 
+     * <p>
+     * This method is thread-safe.
+     * 
+     * @param name
+     *            Exactly the key with which the template was added.
+     * 
+     * @return Whether a template was found with the given key (and hence was removed now)
+     */ 
+    public boolean removeTemplate(String name) {
+        return templates.remove(name) != null;
+    }
+    
+    @Override
+    public TemplateLoaderSession createSession() {
+        return null;
+    }
+
+    @Override
+    public TemplateLoadingResult load(String name, TemplateLoadingSource ifSourceDiffersFrom,
+            Serializable ifVersionDiffersFrom, TemplateLoaderSession session) throws IOException {
+        ContentHolder contentHolder = templates.get(name);
+        if (contentHolder == null) {
+            return TemplateLoadingResult.NOT_FOUND;
+        } else if (ifSourceDiffersFrom != null && ifSourceDiffersFrom.equals(contentHolder.source)
+                && Objects.equals(ifVersionDiffersFrom, contentHolder.version)) {
+            return TemplateLoadingResult.NOT_MODIFIED;
+        } else {
+            return new TemplateLoadingResult(
+                    contentHolder.source, contentHolder.version,
+                    new ByteArrayInputStream(contentHolder.content),
+                    null);
+        }
+    }
+
+    @Override
+    public void resetState() {
+        // Do nothing
+    }
+
+    /**
+     * Show class name and some details that are useful in template-not-found errors.
+     */
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append(_TemplateLoaderUtils.getClassNameForToString(this));
+        sb.append("(Map { ");
+        int cnt = 0;
+        for (String name : templates.keySet()) {
+            cnt++;
+            if (cnt != 1) {
+                sb.append(", ");
+            }
+            if (cnt > 10) {
+                sb.append("...");
+                break;
+            }
+            sb.append(_StringUtil.jQuote(name));
+            sb.append("=...");
+        }
+        if (cnt != 0) {
+            sb.append(' ');
+        }
+        sb.append("})");
+        return sb.toString();
+    }
+
+    private static class ContentHolder {
+        private final byte[] content;
+        private final Source source;
+        private final long version;
+        
+        public ContentHolder(byte[] content, Source source, long version) {
+            this.content = content;
+            this.source = source;
+            this.version = version;
+        }
+        
+    }
+    
+    @SuppressWarnings("serial")
+    private static class Source implements TemplateLoadingSource {
+        
+        private final long instanceId;
+        private final String name;
+        
+        public Source(long instanceId, String name) {
+            this.instanceId = instanceId;
+            this.name = name;
+        }
+    
+        @Override
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + (int) (instanceId ^ (instanceId >>> 32));
+            result = prime * result + ((name == null) ? 0 : name.hashCode());
+            return result;
+        }
+    
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) return true;
+            if (obj == null) return false;
+            if (getClass() != obj.getClass()) return false;
+            Source other = (Source) obj;
+            if (instanceId != other.instanceId) return false;
+            if (name == null) {
+                if (other.name != null) return false;
+            } else if (!name.equals(other.name)) {
+                return false;
+            }
+            return true;
+        }
+        
+        private void writeObject(ObjectOutputStream out) throws IOException {
+            throw new IOException(ByteArrayTemplateLoader.class.getName()
+                    + " sources can't be serialized, as they don't support clustering.");
+        }
+        
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/ClassTemplateLoader.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/ClassTemplateLoader.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/ClassTemplateLoader.java
new file mode 100644
index 0000000..331307f
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/ClassTemplateLoader.java
@@ -0,0 +1,184 @@
+/*
+ * 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.templateresolver.impl;
+
+import java.io.IOException;
+import java.net.URL;
+import java.net.URLConnection;
+
+import org.apache.freemarker.core.templateresolver.TemplateLoader;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingResult;
+import org.apache.freemarker.core.util._NullArgumentException;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * A {@link TemplateLoader} that can load templates from the "classpath". Naturally, it can load from jar files, or from
+ * anywhere where Java can load classes from. Internally, it uses {@link Class#getResource(String)} or
+ * {@link ClassLoader#getResource(String)} to load templates.
+ */
+// TODO
+public class ClassTemplateLoader extends URLTemplateLoader {
+    
+    private final Class<?> resourceLoaderClass;
+    private final ClassLoader classLoader;
+    private final String basePackagePath;
+
+    /**
+     * Creates a template loader that will use the {@link Class#getResource(String)} method of the specified class to
+     * load the resources, and the specified base package path (absolute or relative).
+     *
+     * <p>
+     * Examples:
+     * <ul>
+     * <li>Relative base path (will load from the {@code com.example.myapplication.templates} package):<br>
+     * {@code new ClassTemplateLoader(com.example.myapplication.SomeClass.class, "templates")}
+     * <li>Absolute base path:<br>
+     * {@code new ClassTemplateLoader(somepackage.SomeClass.class, "/com/example/myapplication/templates")}
+     * </ul>
+     *
+     * @param resourceLoaderClass
+     *            The class whose {@link Class#getResource(String)} method will be used to load the templates. Be sure
+     *            that you chose a class whose defining class-loader sees the templates. This parameter can't be
+     *            {@code null}.
+     * @param basePackagePath
+     *            The package that contains the templates, in path ({@code /}-separated) format. If it doesn't start
+     *            with a {@code /} then it's relative to the path (package) of the {@code resourceLoaderClass} class. If
+     *            it starts with {@code /} then it's relative to the root of the package hierarchy. Note that path
+     *            components should be separated by forward slashes independently of the separator character used by the
+     *            underlying operating system. This parameter can't be {@code null}.
+     * 
+     * @see #ClassTemplateLoader(ClassLoader, String)
+     */
+    public ClassTemplateLoader(Class<?> resourceLoaderClass, String basePackagePath) {
+        this(resourceLoaderClass, false, null, basePackagePath);
+    }
+
+    /**
+     * Similar to {@link #ClassTemplateLoader(Class, String)}, but instead of {@link Class#getResource(String)} it uses
+     * {@link ClassLoader#getResource(String)}. Because a {@link ClassLoader} isn't bound to any Java package, it
+     * doesn't mater if the {@code basePackagePath} starts with {@code /} or not, it will be always relative to the root
+     * of the package hierarchy
+     */
+    public ClassTemplateLoader(ClassLoader classLoader, String basePackagePath) {
+        this(null, true, classLoader, basePackagePath);
+    }
+
+    private ClassTemplateLoader(Class<?> resourceLoaderClass, boolean allowNullResourceLoaderClass,
+            ClassLoader classLoader, String basePackagePath) {
+        if (!allowNullResourceLoaderClass) {
+            _NullArgumentException.check("resourceLoaderClass", resourceLoaderClass);
+        }
+        _NullArgumentException.check("basePackagePath", basePackagePath);
+
+        // Either set a non-null resourceLoaderClass or a non-null classLoader, not both:
+        this.resourceLoaderClass = classLoader == null ? (resourceLoaderClass == null ? getClass()
+                : resourceLoaderClass) : null;
+        if (this.resourceLoaderClass == null && classLoader == null) {
+            throw new _NullArgumentException("classLoader");
+        }
+        this.classLoader = classLoader;
+
+        String canonBasePackagePath = canonicalizePrefix(basePackagePath);
+        if (this.classLoader != null && canonBasePackagePath.startsWith("/")) {
+            canonBasePackagePath = canonBasePackagePath.substring(1);
+        }
+        this.basePackagePath = canonBasePackagePath;
+    }
+
+    private static boolean isSchemeless(String fullPath) {
+        int i = 0;
+        int ln = fullPath.length();
+
+        // Skip a single initial /, as things like "/file:/..." might work:
+        if (i < ln && fullPath.charAt(i) == '/') i++;
+
+        // Check if there's no ":" earlier than a '/', as the URLClassLoader
+        // could interpret that as an URL scheme:
+        while (i < ln) {
+            char c = fullPath.charAt(i);
+            if (c == '/') return true;
+            if (c == ':') return false;
+            i++;
+        }
+        return true;
+    }
+
+    /**
+     * Show class name and some details that are useful in template-not-found errors.
+     */
+    @Override
+    public String toString() {
+        return _TemplateLoaderUtils.getClassNameForToString(this) + "("
+                + (resourceLoaderClass != null
+                        ? "resourceLoaderClass=" + resourceLoaderClass.getName()
+                        : "classLoader=" + _StringUtil.jQuote(classLoader))
+                + ", basePackagePath"
+                + "="
+                + _StringUtil.jQuote(basePackagePath)
+                + (resourceLoaderClass != null
+                        ? (basePackagePath.startsWith("/") ? "" : " /* relatively to resourceLoaderClass pkg */")
+                        : ""
+                )
+                + ")";
+    }
+
+    /**
+     * See the similar parameter of {@link #ClassTemplateLoader(Class, String)}; {@code null} when other mechanism is
+     * used to load the resources.
+     */
+    public Class<?> getResourceLoaderClass() {
+        return resourceLoaderClass;
+    }
+
+    /**
+     * See the similar parameter of {@link #ClassTemplateLoader(ClassLoader, String)}; {@code null} when other mechanism
+     * is used to load the resources.
+     */
+    public ClassLoader getClassLoader() {
+        return classLoader;
+    }
+
+    /**
+     * See the similar parameter of {@link #ClassTemplateLoader(ClassLoader, String)}; note that this is a normalized
+     * version of what was actually passed to the constructor.
+     */
+    public String getBasePackagePath() {
+        return basePackagePath;
+    }
+
+    @Override
+    protected URL getURL(String name) {
+        String fullPath = basePackagePath + name;
+    
+        // Block java.net.URLClassLoader exploits:
+        if (basePackagePath.equals("/") && !isSchemeless(fullPath)) {
+            return null;
+        }
+    
+        return resourceLoaderClass != null ? resourceLoaderClass.getResource(fullPath) : classLoader
+                .getResource(fullPath);
+    }
+
+    @Override
+    protected TemplateLoadingResult extractNegativeResult(URLConnection conn) throws IOException {
+        return null;
+    }
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateLookupStrategy.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateLookupStrategy.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateLookupStrategy.java
new file mode 100644
index 0000000..185f5b9
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateLookupStrategy.java
@@ -0,0 +1,61 @@
+/*
+ * 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.templateresolver.impl;
+
+import java.io.IOException;
+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;
+
+/**
+ * <p>
+ * The default lookup strategy of FreeMarker.
+ * 
+ * <p>
+ * Through an example: Assuming localized lookup is enabled and that a template is requested for the name
+ * {@code example.ftl} and {@code Locale("es", "ES", "Traditional_WIN")}, it will try the following template names,
+ * in this order: {@code "foo_en_AU_Traditional_WIN.ftl"}, {@code "foo_en_AU_Traditional.ftl"},
+ * {@code "foo_en_AU.ftl"}, {@code "foo_en.ftl"}, {@code "foo.ftl"}. It stops at the first variation where it finds
+ * a template. (If the template name contains "*" steps, finding the template for the attempted localized variation
+ * happens with the template acquisition mechanism.) If localized lookup is disabled, it won't try to add any locale
+ * strings, so it just looks for {@code "foo.ftl"}.
+ * 
+ * <p>
+ * The generation of the localized name variation with the default lookup strategy, happens like this: It removes
+ * the file extension (the part starting with the <em>last</em> dot), then appends {@link Locale#toString()} after
+ * it, and puts back the extension. Then it starts to remove the parts from the end of the locale, considering
+ * {@code "_"} as the separator between the parts. It won't remove parts that are not part of the locale string
+ * (like if the requested template name is {@code foo_bar.ftl}, it won't remove the {@code "_bar"}).
+ */
+public class DefaultTemplateLookupStrategy extends TemplateLookupStrategy {
+    
+    public static final DefaultTemplateLookupStrategy INSTANCE = new DefaultTemplateLookupStrategy();
+    
+    private DefaultTemplateLookupStrategy() {
+        //
+    }
+    
+    @Override
+    public  <R extends TemplateLookupResult> R lookup(TemplateLookupContext<R> ctx) throws IOException {
+        return ctx.lookupWithLocalizedThenAcquisitionStrategy(ctx.getTemplateName(), ctx.getTemplateLocale());
+    }
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateNameFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateNameFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateNameFormat.java
new file mode 100644
index 0000000..69fa390
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateNameFormat.java
@@ -0,0 +1,309 @@
+/*
+ * 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.templateresolver.impl;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.TemplateNotFoundException;
+import org.apache.freemarker.core.templateresolver.MalformedTemplateNameException;
+import org.apache.freemarker.core.templateresolver.TemplateLoader;
+import org.apache.freemarker.core.templateresolver.TemplateNameFormat;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * The default template name format only when {@link Configuration#getIncompatibleImprovements()
+ * incompatible_improvements} is set to 2.4.0 (or higher). This is not the out-of-the-box default format of FreeMarker
+ * 2.4.x, because the default {@code incompatible_improvements} is still 2.3.0 there.
+ * 
+ * <p>
+ * Differences to the {@link DefaultTemplateNameFormatFM2} format:
+ * 
+ * <ul>
+ * 
+ * <li>The scheme and the path need not be separated with {@code "://"} anymore, only with {@code ":"}. This makes
+ * template names like {@code "classpath:foo.ftl"} interpreted as an absolute name with scheme {@code "classpath"}
+ * and absolute path "foo.ftl". The scheme name before the {@code ":"} can't contain {@code "/"}, or else it's
+ * treated as a malformed name. The scheme part can be separated either with {@code "://"} or just {@code ":"} from
+ * the path. Hence, {@code myschme:/x} is normalized to {@code myschme:x}, while {@code myschme:///x} is normalized
+ * to {@code myschme://x}, but {@code myschme://x} or {@code myschme:/x} aren't changed by normalization. It's up
+ * the {@link TemplateLoader} to which the normalized names are passed to decide which of these scheme separation
+ * conventions are valid (maybe both).</li>
+ * 
+ * <li>{@code ":"} is not allowed in template names, except as the scheme separator (see previous point).
+ * 
+ * <li>Malformed paths throw {@link MalformedTemplateNameException} instead of acting like if the template wasn't
+ * found.
+ * 
+ * <li>{@code "\"} (backslash) is not allowed in template names, and causes {@link MalformedTemplateNameException}.
+ * With {@link DefaultTemplateNameFormatFM2} you would certainly end up with a {@link TemplateNotFoundException} (or
+ * worse, it would work, but steps like {@code ".."} wouldn't be normalized by FreeMarker).
+ * 
+ * <li>Template names might end with {@code /}, like {@code "foo/"}, and the presence or lack of the terminating
+ * {@code /} is seen as significant. While their actual interpretation is up to the {@link TemplateLoader},
+ * operations that manipulate template names assume that the last step refers to a "directory" as opposed to a
+ * "file" exactly if the terminating {@code /} is present. Except, the empty name is assumed to refer to the root
+ * "directory" (despite that it doesn't end with {@code /}).
+ *
+ * <li>{@code //} is normalized to {@code /}, except of course if it's in the scheme name terminator. Like
+ * {@code foo//bar///baaz.ftl} is normalized to {@code foo/bar/baaz.ftl}. (In general, 0 long step names aren't
+ * possible anymore.)</li>
+ * 
+ * <li>The {@code ".."} bugs of the legacy normalizer are oms: {@code ".."} steps has removed the preceding
+ * {@code "."} or {@code "*"} or scheme steps, not treating them specially as they should be. Now these work as
+ * expected. Examples: {@code "a/./../c"} has become to {@code "a/c"}, now it will be {@code "c"}; {@code "a/b/*}
+ * {@code /../c"} has become to {@code "a/b/c"}, now it will be {@code "a/*}{@code /c"}; {@code "scheme://.."} has
+ * become to {@code "scheme:/"}, now it will be {@code null} ({@link TemplateNotFoundException}) for backing out of
+ * the root directory.</li>
+ * 
+ * <li>As now directory paths has to be handled as well, it recognizes terminating, leading, and lonely {@code ".."}
+ * and {@code "."} steps. For example, {@code "foo/bar/.."} now becomes to {@code "foo/"}</li>
+ * 
+ * <li>Multiple consecutive {@code *} steps are normalized to one</li>
+ * 
+ * </ul>
+ */
+public final class DefaultTemplateNameFormat extends TemplateNameFormat {
+    
+    public static DefaultTemplateNameFormat INSTANCE = new DefaultTemplateNameFormat();
+    
+    private DefaultTemplateNameFormat() {
+        //
+    }
+    
+    @Override
+    public String toRootBasedName(String baseName, String targetName) {
+        if (findSchemeSectionEnd(targetName) != 0) {
+            return targetName;
+        } else if (targetName.startsWith("/")) {  // targetName is an absolute path
+            final String targetNameAsRelative = targetName.substring(1);
+            final int schemeSectionEnd = findSchemeSectionEnd(baseName);
+            if (schemeSectionEnd == 0) {
+                return targetNameAsRelative;
+            } else {
+                // Prepend the scheme of baseName:
+                return baseName.substring(0, schemeSectionEnd) + targetNameAsRelative;
+            }
+        } else {  // targetName is a relative path
+            if (!baseName.endsWith("/")) {
+                // Not a directory name => get containing directory name
+                int baseEnd = baseName.lastIndexOf("/") + 1;
+                if (baseEnd == 0) {
+                    // For something like "classpath:t.ftl", must not remove the scheme part:
+                    baseEnd = findSchemeSectionEnd(baseName);
+                }
+                baseName = baseName.substring(0, baseEnd);
+            }
+            return baseName + targetName;
+        }
+    }
+
+    @Override
+    public String normalizeRootBasedName(final String name) throws MalformedTemplateNameException {
+        // Disallow 0 for security reasons.
+        checkNameHasNoNullCharacter(name);
+
+        if (name.indexOf('\\') != -1) {
+            throw new MalformedTemplateNameException(
+                    name,
+                    "Backslash (\"\\\") is not allowed in template names. Use slash (\"/\") instead.");
+        }
+        
+        // Split name to a scheme and a path:
+        final String scheme;
+        String path;
+        {
+            int schemeSectionEnd = findSchemeSectionEnd(name);
+            if (schemeSectionEnd == 0) {
+                scheme = null;
+                path = name;
+            } else {
+                scheme = name.substring(0, schemeSectionEnd);
+                path = name.substring(schemeSectionEnd);
+            }
+        }
+        
+        if (path.indexOf(':') != -1) {
+            throw new MalformedTemplateNameException(name,
+                    "The ':' character can only be used after the scheme name (if there's any), "
+                    + "not in the path part");
+        }
+        
+        path = removeRedundantSlashes(path);
+        // path now doesn't start with "/"
+        
+        path = removeDotSteps(path);
+        
+        path = resolveDotDotSteps(path, name);
+
+        path = removeRedundantStarSteps(path);
+        
+        return scheme == null ? path : scheme + path;
+    }
+
+    private int findSchemeSectionEnd(String name) {
+        int schemeColonIdx = name.indexOf(":");
+        if (schemeColonIdx == -1 || name.lastIndexOf('/', schemeColonIdx - 1) != -1) {
+            return 0;
+        } else {
+            // If there's a following "//", it's treated as the part of the scheme section:
+            if (schemeColonIdx + 2 < name.length()
+                    && name.charAt(schemeColonIdx + 1) == '/' && name.charAt(schemeColonIdx + 2) == '/') {
+                return schemeColonIdx + 3;
+            } else {
+                return schemeColonIdx + 1;
+            }
+        }
+    }
+
+    private String removeRedundantSlashes(String path) {
+        String prevName;
+        do {
+            prevName = path;
+            path = _StringUtil.replace(path, "//", "/");
+        } while (prevName != path);
+        return path.startsWith("/") ? path.substring(1) : path;
+    }
+
+    private String removeDotSteps(String path) {
+        int nextFromIdx = path.length() - 1;
+        findDotSteps: while (true) {
+            final int dotIdx = path.lastIndexOf('.', nextFromIdx);
+            if (dotIdx < 0) {
+                return path;
+            }
+            nextFromIdx = dotIdx - 1;
+            
+            if (dotIdx != 0 && path.charAt(dotIdx - 1) != '/') {
+                // False alarm
+                continue findDotSteps;
+            }
+            
+            final boolean slashRight;
+            if (dotIdx + 1 == path.length()) {
+                slashRight = false;
+            } else if (path.charAt(dotIdx + 1) == '/') {
+                slashRight = true;
+            } else {
+                // False alarm
+                continue findDotSteps;
+            }
+            
+            if (slashRight) { // "foo/./bar" or "./bar" 
+                path = path.substring(0, dotIdx) + path.substring(dotIdx + 2);
+            } else { // "foo/." or "."
+                path = path.substring(0, path.length() - 1);
+            }
+        }
+    }
+
+    /**
+     * @param name The original name, needed for exception error messages.
+     */
+    private String resolveDotDotSteps(String path, final String name) throws MalformedTemplateNameException {
+        int nextFromIdx = 0;
+        findDotDotSteps: while (true) {
+            final int dotDotIdx = path.indexOf("..", nextFromIdx);
+            if (dotDotIdx < 0) {
+                return path;
+            }
+
+            if (dotDotIdx == 0) {
+                throw newRootLeavingException(name);
+            } else if (path.charAt(dotDotIdx - 1) != '/') {
+                // False alarm
+                nextFromIdx = dotDotIdx + 3;
+                continue findDotDotSteps;
+            }
+            // Here we know that it has a preceding "/".
+            
+            final boolean slashRight;
+            if (dotDotIdx + 2 == path.length()) {
+                slashRight = false;
+            } else if (path.charAt(dotDotIdx + 2) == '/') {
+                slashRight = true;
+            } else {
+                // False alarm
+                nextFromIdx = dotDotIdx + 3;
+                continue findDotDotSteps;
+            }
+            
+            int previousSlashIdx;
+            boolean skippedStarStep = false;
+            {
+                int searchSlashBacwardsFrom = dotDotIdx - 2; // before the "/.."
+                scanBackwardsForSlash: while (true) {
+                    if (searchSlashBacwardsFrom == -1) {
+                        throw newRootLeavingException(name);
+                    }
+                    previousSlashIdx = path.lastIndexOf('/', searchSlashBacwardsFrom);
+                    if (previousSlashIdx == -1) {
+                        if (searchSlashBacwardsFrom == 0 && path.charAt(0) == '*') {
+                            // "*/.."
+                            throw newRootLeavingException(name);
+                        }
+                        break scanBackwardsForSlash;
+                    }
+                    if (path.charAt(previousSlashIdx + 1) == '*' && path.charAt(previousSlashIdx + 2) == '/') {
+                        skippedStarStep = true;
+                        searchSlashBacwardsFrom = previousSlashIdx - 1; 
+                    } else {
+                        break scanBackwardsForSlash;
+                    }
+                }
+            }
+            
+            // Note: previousSlashIdx is possibly -1
+            // Removed part in {}: "a/{b/*/../}c" or "a/{b/*/..}"
+            path = path.substring(0, previousSlashIdx + 1)
+                    + (skippedStarStep ? "*/" : "")
+                    + path.substring(dotDotIdx + (slashRight ? 3 : 2));
+            nextFromIdx = previousSlashIdx + 1;
+        }
+    }
+
+    private String removeRedundantStarSteps(String path) {
+        String prevName;
+        removeDoubleStarSteps: do {
+            int supiciousIdx = path.indexOf("*/*");
+            if (supiciousIdx == -1) {
+                break removeDoubleStarSteps;
+            }
+    
+            prevName = path;
+            
+            // Is it delimited on both sided by "/" or by the string boundaires? 
+            if ((supiciousIdx == 0 || path.charAt(supiciousIdx - 1) == '/')
+                    && (supiciousIdx + 3 == path.length() || path.charAt(supiciousIdx + 3) == '/')) {
+                path = path.substring(0, supiciousIdx) + path.substring(supiciousIdx + 2); 
+            }
+        } while (prevName != path);
+        
+        // An initial "*" step is redundant:
+        if (path.startsWith("*")) {
+            if (path.length() == 1) {
+                path = "";
+            } else if (path.charAt(1) == '/') {
+                path = path.substring(2); 
+            }
+            // else: it's wasn't a "*" step.
+        }
+        
+        return path;
+    }
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateNameFormatFM2.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateNameFormatFM2.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateNameFormatFM2.java
new file mode 100644
index 0000000..c5db8e5
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateNameFormatFM2.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.templateresolver.impl;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.templateresolver.MalformedTemplateNameException;
+import org.apache.freemarker.core.templateresolver.TemplateNameFormat;
+
+/**
+ * The default template name format when {@link Configuration#getIncompatibleImprovements() incompatible_improvements}
+ * is below 2.4.0. As of FreeMarker 2.4.0, the default {@code incompatible_improvements} is still {@code 2.3.0}, and it
+ * will certainly remain so for a very long time. In new projects it's highly recommended to use
+ * {@link DefaultTemplateNameFormat#INSTANCE} instead.
+ * 
+ * @deprecated [FM3] Remove
+ */
+@Deprecated
+public final class DefaultTemplateNameFormatFM2 extends TemplateNameFormat {
+    
+    public static final DefaultTemplateNameFormatFM2 INSTANCE = new DefaultTemplateNameFormatFM2();
+    
+    private DefaultTemplateNameFormatFM2() {
+        //
+    }
+    
+    @Override
+    public String toRootBasedName(String baseName, String targetName) {
+        if (targetName.indexOf("://") > 0) {
+            return targetName;
+        } else if (targetName.startsWith("/")) {
+            int schemeSepIdx = baseName.indexOf("://");
+            if (schemeSepIdx > 0) {
+                return baseName.substring(0, schemeSepIdx + 2) + targetName;
+            } else {
+                return targetName.substring(1);
+            }
+        } else {
+            if (!baseName.endsWith("/")) {
+                baseName = baseName.substring(0, baseName.lastIndexOf("/") + 1);
+            }
+            return baseName + targetName;
+        }
+    }
+
+    @Override
+    public String normalizeRootBasedName(final String name) throws MalformedTemplateNameException {
+        // Disallow 0 for security reasons.
+        checkNameHasNoNullCharacter(name);
+        
+        // The legacy algorithm haven't considered schemes, so the name is in effect a path.
+        // Also, note that `path` will be repeatedly replaced below, while `name` is final.
+        String path = name;
+        
+        for (; ; ) {
+            int parentDirPathLoc = path.indexOf("/../");
+            if (parentDirPathLoc == 0) {
+                // If it starts with /../, then it reaches outside the template
+                // root.
+                throw newRootLeavingException(name);
+            }
+            if (parentDirPathLoc == -1) {
+                if (path.startsWith("../")) {
+                    throw newRootLeavingException(name);
+                }
+                break;
+            }
+            int previousSlashLoc = path.lastIndexOf('/', parentDirPathLoc - 1);
+            path = path.substring(0, previousSlashLoc + 1) +
+                   path.substring(parentDirPathLoc + "/../".length());
+        }
+        for (; ; ) {
+            int currentDirPathLoc = path.indexOf("/./");
+            if (currentDirPathLoc == -1) {
+                if (path.startsWith("./")) {
+                    path = path.substring("./".length());
+                }
+                break;
+            }
+            path = path.substring(0, currentDirPathLoc) +
+                   path.substring(currentDirPathLoc + "/./".length() - 1);
+        }
+        // Editing can leave us with a leading slash; strip it.
+        if (path.length() > 1 && path.charAt(0) == '/') {
+            path = path.substring(1);
+        }
+        return path;
+    }
+    
+}
\ No newline at end of file


[30/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/TemplatePostProcessor.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplatePostProcessor.java b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplatePostProcessor.java
new file mode 100644
index 0000000..c664d01
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplatePostProcessor.java
@@ -0,0 +1,31 @@
+/*
+ * 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;
+
+/**
+ * Note yet public; will change in 2.4 (as it has to process {@code UnboundTemplate}-s).
+ */
+abstract class TemplatePostProcessor {
+
+    public abstract void postProcess(Template e) throws TemplatePostProcessorException;
+    
+    // TODO: getPriority, getPhase, getMustBeBefore, getMustBeAfter
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/TemplatePostProcessorException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplatePostProcessorException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplatePostProcessorException.java
new file mode 100644
index 0000000..cebaf36
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplatePostProcessorException.java
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+/**
+ * Not yet public; subject to change.
+ */
+class TemplatePostProcessorException extends Exception {
+
+    public TemplatePostProcessorException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public TemplatePostProcessorException(String message) {
+        super(message);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ThreadInterruptionSupportTemplatePostProcessor.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ThreadInterruptionSupportTemplatePostProcessor.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ThreadInterruptionSupportTemplatePostProcessor.java
new file mode 100644
index 0000000..d05ba08
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ThreadInterruptionSupportTemplatePostProcessor.java
@@ -0,0 +1,140 @@
+/*
+ * 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.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateDirectiveBody;
+
+/**
+ * Not yet public; subject to change.
+ * 
+ * <p>
+ * Known compatibility risks when using this post-processor:
+ * <ul>
+ * <li>{@link TemplateDateModel}-s that care to explicitly check if their nested content is {@code null} might start to
+ *   complain that you have specified a body despite that the directive doesn't support that. Directives should use
+ *   {@link NestedContentNotSupportedException#check(TemplateDirectiveBody)} instead of a simple
+ *   {@code null}-check to avoid this problem.</li>
+ * <li>
+ *   Software that uses {@link DirectiveCallPlace#isNestedOutputCacheable()} will always get {@code false}, because
+ *   interruption checks ({@link ASTThreadInterruptionCheck} elements) are, obviously, not cacheable. This should only
+ *   impact the performance.
+ * <li>
+ *   Software that investigates the AST will see the injected {@link ASTThreadInterruptionCheck} elements. As of this
+ *   writing the AST API-s aren't published, also such software need to be able to deal with new kind of elements
+ *   anyway, so this shouldn't be a problem.
+ * </ul>
+ */
+class ThreadInterruptionSupportTemplatePostProcessor extends TemplatePostProcessor {
+
+    @Override
+    public void postProcess(Template t) throws TemplatePostProcessorException {
+        final ASTElement te = t.getRootASTNode();
+        addInterruptionChecks(te);
+    }
+
+    private void addInterruptionChecks(final ASTElement te) throws TemplatePostProcessorException {
+        if (te == null) {
+            return;
+        }
+        
+        final int childCount = te.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            addInterruptionChecks(te.getChild(i));
+        }
+        
+        if (te.isNestedBlockRepeater()) {
+            try {
+                te.addChild(0, new ASTThreadInterruptionCheck(te));
+            } catch (ParseException e) {
+                throw new TemplatePostProcessorException("Unexpected error; see cause", e);
+            }
+        }
+    }
+
+    /**
+     * AST directive-like node: Checks if the current thread's "interrupted" flag is set, and throws
+     * {@link TemplateProcessingThreadInterruptedException} if it is. We inject this to some points into the AST.
+     */
+    static class ASTThreadInterruptionCheck extends ASTElement {
+        
+        private ASTThreadInterruptionCheck(ASTElement te) throws ParseException {
+            setLocation(te.getTemplate(), te.beginColumn, te.beginLine, te.beginColumn, te.beginLine);
+        }
+
+        @Override
+        ASTElement[] accept(Environment env) throws TemplateException, IOException {
+            // As the API doesn't allow throwing InterruptedException here (nor anywhere else, most importantly,
+            // Template.process can't throw it), we must not clear the "interrupted" flag of the thread.
+            if (Thread.currentThread().isInterrupted()) {
+                throw new TemplateProcessingThreadInterruptedException();
+            }
+            return null;
+        }
+
+        @Override
+        protected String dump(boolean canonical) {
+            return canonical ? "" : "<#--" + getNodeTypeSymbol() + "--#>";
+        }
+
+        @Override
+        String getNodeTypeSymbol() {
+            return "##threadInterruptionCheck";
+        }
+
+        @Override
+        int getParameterCount() {
+            return 0;
+        }
+
+        @Override
+        Object getParameterValue(int idx) {
+            throw new IndexOutOfBoundsException();
+        }
+
+        @Override
+        ParameterRole getParameterRole(int idx) {
+            throw new IndexOutOfBoundsException();
+        }
+
+        @Override
+        boolean isNestedBlockRepeater() {
+            return false;
+        }
+        
+    }
+    
+    /**
+     * Indicates that the template processing thread's "interrupted" flag was found to be set.
+     * 
+     * <p>ATTENTION: This is used by https://github.com/kenshoo/freemarker-online. Don't break backward
+     * compatibility without updating that project too! 
+     */
+    static class TemplateProcessingThreadInterruptedException extends RuntimeException {
+        
+        TemplateProcessingThreadInterruptedException() {
+            super("Template processing thread \"interrupted\" flag was set.");
+        }
+        
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/TokenMgrError.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/TokenMgrError.java b/freemarker-core/src/main/java/org/apache/freemarker/core/TokenMgrError.java
new file mode 100644
index 0000000..4587358
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/TokenMgrError.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;
+
+/**
+ * Exception thrown on lower (lexical) level parsing errors. Shouldn't reach normal FreeMarker users, as FreeMarker
+ * usually catches this and wraps it into a {@link ParseException}.
+ * 
+ * This is a modified version of file generated by JavaCC from FTL.jj.
+ * You can modify this class to customize the error reporting mechanisms so long as the public interface
+ * remains compatible with the original.
+ * 
+ * @see ParseException
+ */
+class TokenMgrError extends Error {
+   /*
+    * Ordinals for various reasons why an Error of this type can be thrown.
+    */
+
+   /**
+    * Lexical error occurred.
+    */
+   static final int LEXICAL_ERROR = 0;
+
+   /**
+    * An attempt was made to invoke a second instance of a static token manager.
+    */
+   static final int STATIC_LEXER_ERROR = 1;
+
+   /**
+    * Tried to change to an invalid lexical state.
+    */
+   static final int INVALID_LEXICAL_STATE = 2;
+
+   /**
+    * Detected (and bailed out of) an infinite loop in the token manager.
+    */
+   static final int LOOP_DETECTED = 3;
+
+   /**
+    * Indicates the reason why the exception is thrown. It will have
+    * one of the above 4 values.
+    */
+   int errorCode;
+
+   private String detail;
+   private Integer lineNumber, columnNumber;
+   private Integer endLineNumber, endColumnNumber;
+
+   /**
+    * Replaces unprintable characters by their espaced (or unicode escaped)
+    * equivalents in the given string
+    */
+   protected static String addEscapes(String str) {
+      StringBuilder retval = new StringBuilder();
+      char ch;
+      for (int i = 0; i < str.length(); i++) {
+        switch (str.charAt(i))
+        {
+           case 0 :
+              continue;
+           case '\b':
+              retval.append("\\b");
+              continue;
+           case '\t':
+              retval.append("\\t");
+              continue;
+           case '\n':
+              retval.append("\\n");
+              continue;
+           case '\f':
+              retval.append("\\f");
+              continue;
+           case '\r':
+              retval.append("\\r");
+              continue;
+           case '\"':
+              retval.append("\\\"");
+              continue;
+           case '\'':
+              retval.append("\\\'");
+              continue;
+           case '\\':
+              retval.append("\\\\");
+              continue;
+           default:
+              if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) {
+                 String s = "0000" + Integer.toString(ch, 16);
+                 retval.append("\\u" + s.substring(s.length() - 4, s.length()));
+              } else {
+                 retval.append(ch);
+              }
+              continue;
+        }
+      }
+      return retval.toString();
+   }
+
+   /**
+    * Returns a detailed message for the Error when it's thrown by the
+    * token manager to indicate a lexical error.
+    * Parameters : 
+    *    EOFSeen     : indicates if EOF caused the lexicl error
+    *    curLexState : lexical state in which this error occurred
+    *    errorLine   : line number when the error occurred
+    *    errorColumn : column number when the error occurred
+    *    errorAfter  : prefix that was seen before this error occurred
+    *    curchar     : the offending character
+    * Note: You can customize the lexical error message by modifying this method.
+    */
+   protected static String LexicalError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar) {
+      return("Lexical error: encountered " +
+           (EOFSeen ? "<EOF> " : ("\"" + addEscapes(String.valueOf(curChar)) + "\"") + " (" + (int) curChar + "), ") +
+           "after \"" + addEscapes(errorAfter) + "\".");
+   }
+
+   /**
+    * You can also modify the body of this method to customize your error messages.
+    * For example, cases like LOOP_DETECTED and INVALID_LEXICAL_STATE are not
+    * of end-users concern, so you can return something like : 
+    *
+    *     "Internal Error : Please file a bug report .... "
+    *
+    * from this method for such cases in the release version of your parser.
+    */
+   @Override
+public String getMessage() {
+      return super.getMessage();
+   }
+
+   /*
+    * Constructors of various flavors follow.
+    */
+
+   public TokenMgrError() {
+   }
+
+   public TokenMgrError(String detail, int reason) {
+      super(detail);  // the "detail" must not contain location information, the "message" might does
+      this.detail = detail;
+      errorCode = reason;
+   }
+   
+   /**
+    * @since 2.3.21
+    */
+   public TokenMgrError(String detail, int reason,
+           int errorLine, int errorColumn,
+           int endLineNumber, int endColumnNumber) {
+       super(detail);  // the "detail" must not contain location information, the "message" might does
+       this.detail = detail;
+       errorCode = reason;
+
+      lineNumber = Integer.valueOf(errorLine);  // In J2SE there was no Integer.valueOf(int)
+      columnNumber = Integer.valueOf(errorColumn);
+       this.endLineNumber = Integer.valueOf(endLineNumber); 
+       this.endColumnNumber = Integer.valueOf(endColumnNumber); 
+    }
+
+   /**
+    * Overload for JavaCC 6 compatibility.
+    * 
+    * @since 2.3.24
+    */
+   TokenMgrError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, int curChar, int reason) {
+       this(EOFSeen, lexState, errorLine, errorColumn, errorAfter, (char) curChar, reason);
+   }
+   
+   public TokenMgrError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar, int reason) {
+      this(LexicalError(EOFSeen, lexState, errorLine, errorColumn, errorAfter, curChar), reason);
+
+      lineNumber = Integer.valueOf(errorLine);  // In J2SE there was no Integer.valueOf(int)
+      columnNumber = Integer.valueOf(errorColumn);
+      // We blame the single character that can't be the start of a legal token: 
+      endLineNumber = lineNumber;
+      endColumnNumber = columnNumber;
+   }
+
+   /**
+    * 1-based line number of the unexpected character(s).
+    * 
+    * @since 2.3.20
+    */
+   public Integer getLineNumber() {
+      return lineNumber;
+   }
+    
+   /**
+    * 1-based column number of the unexpected character(s).
+    * 
+    * @since 2.3.20
+    */
+   public Integer getColumnNumber() {
+      return columnNumber;
+   }
+   
+   /**
+    * Returns the 1-based line at which the last character of the wrong section is. This will be usually (but not
+    * always) the same as {@link #getLineNumber()} because the lexer can only point to the single character that
+    * doesn't match any patterns.
+    * 
+    * @since 2.3.21
+    */
+   public Integer getEndLineNumber() {
+      return endLineNumber;
+   }
+
+   /**
+    * Returns the 1-based column at which the last character of the wrong section is. This will be usually (but not
+    * always) the same as {@link #getColumnNumber()} because the lexer can only point to the single character that
+    * doesn't match any patterns.
+    * 
+    * @since 2.3.21
+    */
+   public Integer getEndColumnNumber() {
+      return endColumnNumber;
+   }
+
+   public String getDetail() {
+       return detail;
+   }
+
+   public ParseException toParseException(Template template) {
+       return new ParseException(getDetail(),
+               template,
+               getLineNumber() != null ? getLineNumber().intValue() : 0,
+               getColumnNumber() != null ? getColumnNumber().intValue() : 0,
+               getEndLineNumber() != null ? getEndLineNumber().intValue() : 0,
+               getEndColumnNumber() != null ? getEndColumnNumber().intValue() : 0);
+   }
+   
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/TopLevelConfiguration.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/TopLevelConfiguration.java b/freemarker-core/src/main/java/org/apache/freemarker/core/TopLevelConfiguration.java
new file mode 100644
index 0000000..8b253a2
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/TopLevelConfiguration.java
@@ -0,0 +1,194 @@
+/*
+ * 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.util.Map;
+
+import org.apache.freemarker.core.templateresolver.CacheStorage;
+import org.apache.freemarker.core.templateresolver.TemplateConfigurationFactory;
+import org.apache.freemarker.core.templateresolver.TemplateLoader;
+import org.apache.freemarker.core.templateresolver.TemplateLookupStrategy;
+import org.apache.freemarker.core.templateresolver.TemplateNameFormat;
+import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateLookupStrategy;
+import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateNameFormat;
+import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateNameFormatFM2;
+import org.apache.freemarker.core.templateresolver.impl.FileTemplateLoader;
+import org.apache.freemarker.core.templateresolver.impl.MultiTemplateLoader;
+import org.apache.freemarker.core.templateresolver.impl.SoftCacheStorage;
+
+/**
+ * Implemented by FreeMarker core classes (not by you) that provide {@link Configuration}-level settings. <b>New
+ * methods may be added any time in future FreeMarker versions, so don't try to implement this interface yourself!</b>
+ *
+ * @see ParsingAndProcessingConfiguration
+ */
+public interface TopLevelConfiguration extends ParsingAndProcessingConfiguration {
+
+    /**
+     * The {@link TemplateLoader} that is used to look up and load templates.
+     * By providing your own {@link TemplateLoader} implementation, you can load templates from whatever kind of
+     * storages, like from relational databases, NoSQL-storages, etc.
+     *
+     * <p>You can chain several {@link TemplateLoader}-s together with {@link MultiTemplateLoader}.
+     *
+     * <p>Default value: You should always set the template loader instead of relying on the default value.
+     * (But if you still care what it is, before "incompatible improvements" 2.3.21 it's a {@link FileTemplateLoader}
+     * that uses the current directory as its root; as it's hard tell what that directory will be, it's not very useful
+     * and dangerous. Starting with "incompatible improvements" 2.3.21 the default is {@code null}.)
+     */
+    TemplateLoader getTemplateLoader();
+
+    /**
+     * Tells if this setting was explicitly set (otherwise its value will be the default value).
+     */
+    boolean isTemplateLoaderSet();
+
+    /**
+     * The {@link TemplateLookupStrategy} that is used to look up templates based on the requested name, locale and
+     * custom lookup condition. Its default is {@link DefaultTemplateLookupStrategy#INSTANCE}.
+     */
+    TemplateLookupStrategy getTemplateLookupStrategy();
+
+    /**
+     * Tells if this setting was explicitly set (otherwise its value will be the default value).
+     */
+    boolean isTemplateLookupStrategySet();
+
+    /**
+     * The template name format used; see {@link TemplateNameFormat}. The default is
+     * {@link DefaultTemplateNameFormatFM2#INSTANCE}, while the recommended value for new projects is
+     * {@link DefaultTemplateNameFormat#INSTANCE}.
+     */
+    TemplateNameFormat getTemplateNameFormat();
+
+    /**
+     * Tells if this setting was explicitly set (otherwise its value will be the default value).
+     */
+    boolean isTemplateNameFormatSet();
+
+    /**
+     * The {@link TemplateConfigurationFactory} that will configure individual templates where their settings differ
+     * from those coming from the common {@link Configuration} object. A typical use case for that is specifying the
+     * {@link #getOutputFormat() outputFormat} or {@link #getSourceEncoding() sourceEncoding} for templates based on
+     * their file extension or parent directory.
+     * <p>
+     * Note that the settings suggested by standard file extensions are stronger than that you set here. See
+     * {@link #getRecognizeStandardFileExtensions()} for more information about standard file extensions.
+     * <p>
+     * See "Template configurations" in the FreeMarker Manual for examples.
+     */
+    TemplateConfigurationFactory getTemplateConfigurations();
+
+    /**
+     * Tells if this setting was explicitly set (otherwise its value will be the default value).
+     */
+    boolean isTemplateConfigurationsSet();
+
+    /**
+     * The map-like object used for caching templates to avoid repeated loading and parsing of the template "files".
+     * Its {@link Configuration}-level default is a {@link SoftCacheStorage}.
+     */
+    CacheStorage getCacheStorage();
+
+    /**
+     * Tells if this setting was explicitly set (otherwise its value will be the default value).
+     */
+    boolean isCacheStorageSet();
+
+    /**
+     * The time in milliseconds that must elapse before checking whether there is a newer version of a template
+     * "file" than the cached one. Defaults to 5000 ms.
+     */
+    long getTemplateUpdateDelayMilliseconds();
+
+    /**
+     * Tells if this setting was explicitly set (otherwise its value will be the default value).
+     */
+    boolean isTemplateUpdateDelayMillisecondsSet();
+
+    /**
+     * Returns the value of the "incompatible improvements" setting; this is the FreeMarker version number where the
+     * not 100% backward compatible bug fixes and improvements that you want to enable were already implemented. In
+     * new projects you should set this to the FreeMarker version that you are actually using. In older projects it's
+     * also usually better to keep this high, however you better check the changes activated (find them below), at
+     * least if not only the 3rd version number (the micro version) of {@code incompatibleImprovements} is increased.
+     * Generally, as far as you only increase the last version number of this setting, the changes are always low
+     * risk.
+     * <p>
+     * Bugfixes and improvements that are fully backward compatible, also those that are important security fixes,
+     * are enabled regardless of the incompatible improvements setting.
+     * <p>
+     * An important consequence of setting this setting is that now your application will check if the stated minimum
+     * FreeMarker version requirement is met. Like if you set this setting to 3.0.1, but accidentally the
+     * application is deployed with FreeMarker 3.0.0, then FreeMarker will fail, telling that a higher version is
+     * required. After all, the fixes/improvements you have requested aren't available on a lower version.
+     * <p>
+     * Note that as FreeMarker's minor (2nd) or major (1st) version number increments, it's possible that emulating
+     * some of the old bugs will become unsupported, that is, even if you set this setting to a low value, it
+     * silently wont bring back the old behavior anymore. Information about that will be present here.
+     *
+     * <p>Currently the effects of this setting are:
+     * <ul>
+     *   <li><p>
+     *     3.0.0: This is the lowest supported value in FreeMarker 3.
+     *   </li>
+     * </ul>
+     *
+     * @return Never {@code null}.
+     */
+    @Override
+    Version getIncompatibleImprovements();
+
+    /**
+     * Whether localized template lookup is enabled. Enabled by default.
+     *
+     * <p>
+     * With the default {@link TemplateLookupStrategy}, localized lookup works like this: Let's say your locale setting
+     * is {@code Locale("en", "AU")}, and you call {@link Configuration#getTemplate(String) cfg.getTemplate("foo.ftl")}.
+     * Then FreeMarker will look for the template under these names, stopping at the first that exists:
+     * {@code "foo_en_AU.ftl"}, {@code "foo_en.ftl"}, {@code "foo.ftl"}. See the description of the default value at
+     * {@link #getTemplateLookupStrategy()} for a more details. If you need to generate different
+     * template names, set your own a {@link TemplateLookupStrategy} implementation as the value of the
+     * {@link #getTemplateLookupStrategy() templateLookupStrategy} setting.
+     */
+    boolean getLocalizedLookup();
+
+    /**
+     * Tells if this setting was explicitly set (otherwise its value will be the default value).
+     */
+    boolean isLocalizedLookupSet();
+
+    /**
+     * Shared variables are variables that are visible as top-level variables for all templates, except where the data
+     * model contains a variable with the same name (which then shadows the shared variable).
+     *
+     * @return Not {@code null}; the {@link Map} is possibly mutable in builders, but immutable in
+     *      {@link Configuration}.
+     *
+     * @see Configuration.Builder#setSharedVariables(Map)
+     */
+    Map<String, Object> getSharedVariables();
+
+    /**
+     * Tells if this setting was explicitly set (if not, the default value of the setting will be used).
+     */
+    boolean isSharedVariablesSet();
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/UnexpectedTypeException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/UnexpectedTypeException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/UnexpectedTypeException.java
new file mode 100644
index 0000000..0f9a013
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/UnexpectedTypeException.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateModel;
+
+/**
+ * The type of a value differs from what was expected.
+ * 
+ * @since 2.3.20
+ */
+public class UnexpectedTypeException extends TemplateException {
+    
+    public UnexpectedTypeException(Environment env, String description) {
+        super(description, env);
+    }
+
+    UnexpectedTypeException(Environment env, _ErrorDescriptionBuilder description) {
+        super(null, env, null, description);
+    }
+
+    UnexpectedTypeException(
+            ASTExpression blamed, TemplateModel model, String expectedTypesDesc, Class[] expectedTypes, Environment env)
+            throws InvalidReferenceException {
+        super(null, env, blamed, newDesciptionBuilder(blamed, null, model, expectedTypesDesc, expectedTypes, env));
+    }
+
+    UnexpectedTypeException(
+            ASTExpression blamed, TemplateModel model, String expectedTypesDesc, Class[] expectedTypes, String tip,
+            Environment env)
+            throws InvalidReferenceException {
+        super(null, env, blamed, newDesciptionBuilder(blamed, null, model, expectedTypesDesc, expectedTypes, env)
+                .tip(tip));
+    }
+
+    UnexpectedTypeException(
+            ASTExpression blamed, TemplateModel model, String expectedTypesDesc, Class[] expectedTypes, Object[] tips,
+            Environment env)
+            throws InvalidReferenceException {
+        super(null, env, blamed, newDesciptionBuilder(blamed, null, model, expectedTypesDesc, expectedTypes, env)
+                .tips(tips));
+    }
+
+    UnexpectedTypeException(
+            String blamedAssignmentTargetVarName, TemplateModel model, String expectedTypesDesc, Class[] expectedTypes,
+            Object[] tips,
+            Environment env)
+            throws InvalidReferenceException {
+        super(null, env, null, newDesciptionBuilder(
+                null, blamedAssignmentTargetVarName, model, expectedTypesDesc, expectedTypes, env).tips(tips));
+    }
+    
+    /**
+     * @param blamedAssignmentTargetVarName
+     *            Used for assignments that use {@code +=} and such, in which case the {@code blamed} expression
+     *            parameter will be null {@code null} and this parameter will be non-{null}.
+     */
+    private static _ErrorDescriptionBuilder newDesciptionBuilder(
+            ASTExpression blamed, String blamedAssignmentTargetVarName,
+            TemplateModel model, String expectedTypesDesc, Class[] expectedTypes, Environment env)
+            throws InvalidReferenceException {
+        if (model == null) throw InvalidReferenceException.getInstance(blamed, env);
+
+        _ErrorDescriptionBuilder errorDescBuilder = new _ErrorDescriptionBuilder(
+                unexpectedTypeErrorDescription(expectedTypesDesc, blamed, blamedAssignmentTargetVarName, model))
+                .blame(blamed).showBlamer(true);
+        if (model instanceof _UnexpectedTypeErrorExplainerTemplateModel) {
+            Object[] tip = ((_UnexpectedTypeErrorExplainerTemplateModel) model).explainTypeError(expectedTypes);
+            if (tip != null) {
+                errorDescBuilder.tip(tip);
+            }
+        }
+        return errorDescBuilder;
+    }
+
+    private static Object[] unexpectedTypeErrorDescription(
+            String expectedTypesDesc,
+            ASTExpression blamed, String blamedAssignmentTargetVarName,
+            TemplateModel model) {
+        return new Object[] {
+                "Expected ", new _DelayedAOrAn(expectedTypesDesc), ", but ",
+                (blamedAssignmentTargetVarName == null
+                        ? blamed != null ? "this" : "the expression"
+                        : new Object[] {
+                                "assignment target variable ",
+                                new _DelayedJQuote(blamedAssignmentTargetVarName) }), 
+                " has evaluated to ",
+                new _DelayedAOrAn(new _DelayedFTLTypeDescription(model)),
+                (blamedAssignmentTargetVarName == null ? ":" : ".")};
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/UnknownConfigurationSettingException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/UnknownConfigurationSettingException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/UnknownConfigurationSettingException.java
new file mode 100644
index 0000000..a4e562c
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/UnknownConfigurationSettingException.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.Configuration.ExtendableBuilder;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * Thrown by {@link ExtendableBuilder#setSetting(String, String)} if the setting name was not recognized.
+ */
+@SuppressWarnings("serial")
+public class UnknownConfigurationSettingException extends ConfigurationException {
+
+    UnknownConfigurationSettingException(String name, String correctedName) {
+        super("Unknown FreeMarker configuration setting: " + _StringUtil.jQuote(name)
+                + (correctedName == null ? "" : ". You may meant: " + _StringUtil.jQuote(correctedName)));
+    }
+
+    UnknownConfigurationSettingException(String name, Version removedInVersion) {
+        super("Unknown FreeMarker configuration setting: " + _StringUtil.jQuote(name)
+                + (removedInVersion == null ? "" : ". This setting was removed in version " + removedInVersion));
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/Version.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/Version.java b/freemarker-core/src/main/java/org/apache/freemarker/core/Version.java
new file mode 100644
index 0000000..273f9f7
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/Version.java
@@ -0,0 +1,297 @@
+/*
+ * 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.Serializable;
+import java.util.Date;
+
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * Represents a version number plus the further qualifiers and build info. This is
+ * mostly used for representing a FreeMarker version number, but should also be able
+ * to parse the version strings of 3rd party libraries.
+ * 
+ * @see Configuration#getVersion()
+ * 
+ * @since 2.3.20
+ */
+public final class Version implements Serializable {
+    
+    private final int major;
+    private final int minor;
+    private final int micro;
+    private final String extraInfo;
+    private final String originalStringValue;
+    
+    private final Boolean gaeCompliant;
+    private final Date buildDate;
+    
+    private final int intValue;
+    private volatile String calculatedStringValue;  // not final because it's calculated on demand
+    private int hashCode;  // not final because it's calculated on demand
+
+    /**
+     * @throws IllegalArgumentException if the version string is malformed
+     */
+    public Version(String stringValue) {
+        this(stringValue, null, null);
+    }
+    
+    /**
+     * @throws IllegalArgumentException if the version string is malformed
+     */
+    public Version(String stringValue, Boolean gaeCompliant, Date buildDate) {
+        stringValue = stringValue.trim();
+        originalStringValue = stringValue; 
+        
+        int[] parts = new int[3];
+        String extraInfoTmp = null;
+        {
+            int partIdx = 0;
+            for (int i = 0; i < stringValue.length(); i++) {
+                char c = stringValue.charAt(i);
+                if (isNumber(c)) {
+                    parts[partIdx] = parts[partIdx] * 10 + (c - '0');
+                } else {
+                    if (i == 0) {
+                        throw new IllegalArgumentException(
+                                "The version number string " + _StringUtil.jQuote(stringValue)
+                                + " doesn't start with a number.");
+                    }
+                    if (c == '.') {
+                        char nextC = i + 1 >= stringValue.length() ? 0 : stringValue.charAt(i + 1);
+                        if (nextC == '.') {
+                            throw new IllegalArgumentException(
+                                    "The version number string " + _StringUtil.jQuote(stringValue)
+                                    + " contains multiple dots after a number.");
+                        }
+                        if (partIdx == 2 || !isNumber(nextC)) {
+                            extraInfoTmp = stringValue.substring(i);
+                            break;
+                        } else {
+                            partIdx++;
+                        }
+                    } else {
+                        extraInfoTmp = stringValue.substring(i);
+                        break;
+                    }
+                }
+            }
+            
+            if (extraInfoTmp != null) {
+                char firstChar = extraInfoTmp.charAt(0); 
+                if (firstChar == '.' || firstChar == '-' || firstChar == '_') {
+                    extraInfoTmp = extraInfoTmp.substring(1);
+                    if (extraInfoTmp.length() == 0) {
+                        throw new IllegalArgumentException(
+                            "The version number string " + _StringUtil.jQuote(stringValue)
+                            + " has an extra info section opened with \"" + firstChar + "\", but it's empty.");
+                    }
+                }
+            }
+        }
+        extraInfo = extraInfoTmp;
+        
+        major = parts[0];
+        minor = parts[1];
+        micro = parts[2];
+        intValue = calculateIntValue();
+        
+        this.gaeCompliant = gaeCompliant;
+        this.buildDate = buildDate;
+        
+    }
+
+    private boolean isNumber(char c) {
+        return c >= '0' && c <= '9';
+    }
+
+    public Version(int major, int minor, int micro) {
+        this(major, minor, micro, null, null, null);
+    }
+
+    /**
+     * Creates an object based on the {@code int} value that uses the same kind of encoding as {@link #intValue()}.
+     * 
+     * @since 2.3.24
+     */
+    public Version(int intValue) {
+        this.intValue = intValue;
+
+        micro = intValue % 1000;
+        minor = (intValue / 1000) % 1000;
+        major = intValue / 1000000;
+
+        extraInfo = null;
+        gaeCompliant = null;
+        buildDate = null;
+        originalStringValue = null;
+    }
+    
+    public Version(int major, int minor, int micro, String extraInfo, Boolean gaeCompliant, Date buildDate) {
+        this.major = major;
+        this.minor = minor;
+        this.micro = micro;
+        this.extraInfo = extraInfo;
+        this.gaeCompliant = gaeCompliant;
+        this.buildDate = buildDate;
+        intValue = calculateIntValue();
+        originalStringValue = null;
+    }
+
+    private int calculateIntValue() {
+        return intValueFor(major, minor, micro);
+    }
+    
+    static public int intValueFor(int major, int minor, int micro) {
+        return major * 1000000 + minor * 1000 + micro;
+    }
+    
+    private String getStringValue() {
+        if (originalStringValue != null) return originalStringValue;
+        
+        String calculatedStringValue = this.calculatedStringValue;
+        if (calculatedStringValue == null) {
+            synchronized (this) {
+                calculatedStringValue = this.calculatedStringValue;
+                if (calculatedStringValue == null) {
+                    calculatedStringValue = major + "." + minor + "." + micro;
+                    if (extraInfo != null) calculatedStringValue += "-" + extraInfo;
+                    this.calculatedStringValue = calculatedStringValue;
+                }
+            }
+        }
+        return calculatedStringValue;
+    }
+    
+    /**
+     * Contains the major.minor.micor numbers and the extraInfo part, not the other information.
+     */
+    @Override
+    public String toString() {
+        return getStringValue();
+    }
+
+    /**
+     * The 1st version number, like 1 in "1.2.3".
+     */
+    public int getMajor() {
+        return major;
+    }
+
+    /**
+     * The 2nd version number, like 2 in "1.2.3".
+     */
+    public int getMinor() {
+        return minor;
+    }
+
+    /**
+     * The 3rd version number, like 3 in "1.2.3".
+     */
+    public int getMicro() {
+        return micro;
+    }
+
+    /**
+     * The arbitrary string after the micro version number without leading dot, dash or underscore,
+     * like "RC03" in "2.4.0-RC03".
+     * This is usually a qualifier (RC, SNAPHOST, nightly, beta, etc) and sometimes build info (like
+     * date).
+     */
+    public String getExtraInfo() {
+        return extraInfo;
+    }
+    
+    /**
+     * @return The Google App Engine compliance, or {@code null}.
+     */
+    public Boolean isGAECompliant() {
+        return gaeCompliant;
+    }
+
+    /**
+     * @return The build date if known, or {@code null}.
+     */
+    public Date getBuildDate() {
+        return buildDate;
+    }
+
+    /**
+     * @return major * 1000000 + minor * 1000 + micro.
+     */
+    public int intValue() {
+        return intValue;
+    }
+
+    @Override
+    public int hashCode() {
+        int r = hashCode;
+        if (r != 0) return r;
+        synchronized (this) {
+            if (hashCode == 0) {
+                final int prime = 31;
+                int result = 1;
+                result = prime * result + (buildDate == null ? 0 : buildDate.hashCode());
+                result = prime * result + (extraInfo == null ? 0 : extraInfo.hashCode());
+                result = prime * result + (gaeCompliant == null ? 0 : gaeCompliant.hashCode());
+                result = prime * result + intValue;
+                if (result == 0) result = -1;  // 0 is reserved for "not set"
+                hashCode = result;
+            }
+            return hashCode;
+        }
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+        if (obj == null) return false;
+        if (getClass() != obj.getClass()) return false;
+
+        Version other = (Version) obj;
+
+        if (intValue != other.intValue) return false;
+        
+        if (other.hashCode() != hashCode()) return false;
+        
+        if (buildDate == null) {
+            if (other.buildDate != null) return false;
+        } else if (!buildDate.equals(other.buildDate)) {
+            return false;
+        }
+        
+        if (extraInfo == null) {
+            if (other.extraInfo != null) return false;
+        } else if (!extraInfo.equals(other.extraInfo)) {
+            return false;
+        }
+        
+        if (gaeCompliant == null) {
+            if (other.gaeCompliant != null) return false;
+        } else if (!gaeCompliant.equals(other.gaeCompliant)) {
+            return false;
+        }
+        
+        return true;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/WrongTemplateCharsetException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/WrongTemplateCharsetException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/WrongTemplateCharsetException.java
new file mode 100644
index 0000000..799efb4
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/WrongTemplateCharsetException.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.nio.charset.Charset;
+
+/**
+ * Thrown by the {@link Template} constructors that specify a non-{@code null} encoding whoch doesn't match the
+ * encoding specified in the {@code #ftl} header of the template.
+ */
+public class WrongTemplateCharsetException extends ParseException {
+    private static final long serialVersionUID = 1L;
+
+    private final Charset templateSpecifiedEncoding;
+    private final Charset constructorSpecifiedEncoding;
+
+    /**
+     * @since 2.3.22
+     */
+    public WrongTemplateCharsetException(Charset templateSpecifiedEncoding, Charset constructorSpecifiedEncoding) {
+        this.templateSpecifiedEncoding = templateSpecifiedEncoding;
+        this.constructorSpecifiedEncoding = constructorSpecifiedEncoding;
+    }
+
+    @Override
+    public String getMessage() {
+        return "Encoding specified inside the template (" + templateSpecifiedEncoding
+                + ") doesn't match the encoding specified for the Template constructor"
+                + (constructorSpecifiedEncoding != null ? " (" + constructorSpecifiedEncoding + ")." : ".");
+    }
+
+    /**
+     * @since 2.3.22
+     */
+    public Charset getTemplateSpecifiedEncoding() {
+        return templateSpecifiedEncoding;
+    }
+
+    /**
+     * @since 2.3.22
+     */
+    public Charset getConstructorSpecifiedEncoding() {
+        return constructorSpecifiedEncoding;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_CharsetBuilder.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_CharsetBuilder.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_CharsetBuilder.java
new file mode 100644
index 0000000..3234de8
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_CharsetBuilder.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.nio.charset.Charset;
+
+import org.apache.freemarker.core.util._NullArgumentException;
+
+/**
+ * For internal use only; don't depend on this, there's no backward compatibility guarantee at all!
+ */
+public class _CharsetBuilder {
+
+    private final String name;
+
+    public _CharsetBuilder(String name) {
+        _NullArgumentException.check(name);
+        this.name = name;
+    }
+
+    public Charset build() {
+        return Charset.forName(name);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_CoreAPI.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_CoreAPI.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_CoreAPI.java
new file mode 100644
index 0000000..b575901
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_CoreAPI.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.util._NullArgumentException;
+
+/**
+ * For internal use only; don't depend on this, there's no backward compatibility guarantee at all!
+ * This class is to work around the lack of module system in Java, i.e., so that other FreeMarker packages can
+ * access things inside this package that users shouldn't. 
+ */ 
+public final class _CoreAPI {
+
+    public static final int VERSION_INT_3_0_0 = Configuration.VERSION_3_0_0.intValue();
+
+    // Can't be instantiated
+    private _CoreAPI() { }
+
+    /**
+     * ATTENTION: This is used by https://github.com/kenshoo/freemarker-online. Don't break backward
+     * compatibility without updating that project too! 
+     */
+    static public void addThreadInterruptedChecks(Template template) {
+        try {
+            new ThreadInterruptionSupportTemplatePostProcessor().postProcess(template);
+        } catch (TemplatePostProcessorException e) {
+            throw new RuntimeException("Template post-processing failed", e);
+        }
+    }
+
+    /**
+     * The work around the problematic cases where we should throw a {@link TemplateException}, but we are inside
+     * a {@link TemplateModel} method and so we can only throw {@link TemplateModelException}-s.  
+     */
+    // [FM3] Get rid of this problem, then delete this method
+    public static TemplateModelException ensureIsTemplateModelException(String modelOpMsg, TemplateException e) {
+        if (e instanceof TemplateModelException) {
+            return (TemplateModelException) e;
+        } else {
+            return new _TemplateModelException(
+                    e.getBlamedExpression(), e.getCause(), e.getEnvironment(), modelOpMsg);
+        }
+    }
+
+    // [FM3] Should become unnecessary as custom directive classes are reworked
+    public static boolean isMacroOrFunction(TemplateModel m) {
+        return m instanceof ASTDirMacro;
+    }
+
+    // [FM3] Should become unnecessary as custom directive classes are reworked
+    public static boolean isFunction(TemplateModel m) {
+        return m instanceof ASTDirMacro && ((ASTDirMacro) m).isFunction();
+    }
+
+    public static void checkVersionNotNullAndSupported(Version incompatibleImprovements) {
+        _NullArgumentException.check("incompatibleImprovements", incompatibleImprovements);
+        int iciV = incompatibleImprovements.intValue();
+        if (iciV > Configuration.getVersion().intValue()) {
+            throw new IllegalArgumentException("The FreeMarker version requested by \"incompatibleImprovements\" was "
+                    + incompatibleImprovements + ", but the installed FreeMarker version is only "
+                    + Configuration.getVersion() + ". You may need to upgrade FreeMarker in your project.");
+        }
+        if (iciV < VERSION_INT_3_0_0) {
+            throw new IllegalArgumentException("\"incompatibleImprovements\" must be at least 3.0.0, but was "
+                    + incompatibleImprovements);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_CoreLogs.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_CoreLogs.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_CoreLogs.java
new file mode 100644
index 0000000..9179d7c
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_CoreLogs.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.freemarker.core;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * For internal use only; don't depend on this, there's no backward compatibility guarantee at all!
+ * This class is to work around the lack of module system in Java, i.e., so that other FreeMarker packages can
+ * access things inside this package that users shouldn't. 
+ */ 
+public final class _CoreLogs {
+
+    // [FM3] Why "Runtime"? "TemplateProcessing" maybe?
+    public static final Logger RUNTIME = LoggerFactory.getLogger("org.apache.freemarker.core.Runtime");
+    public static final Logger ATTEMPT = LoggerFactory.getLogger("org.apache.freemarker.core.Runtime.Attempt");
+    public static final Logger SECURITY = LoggerFactory.getLogger("org.apache.freemarker.core.Security");
+    public static final Logger OBJECT_WRAPPER = LoggerFactory.getLogger("org.apache.freemarker.core.model" +
+            ".ObjectWrapper");
+    public static final Logger TEMPLATE_RESOLVER = LoggerFactory.getLogger(
+            "org.apache.freemarker.core.templateresolver");
+    public static final Logger DEBUG_SERVER = LoggerFactory.getLogger("org.apache.freemarker.core.debug.server");
+    public static final Logger DEBUG_CLIENT = LoggerFactory.getLogger("org.apache.freemarker.core.debug.client");
+
+    private _CoreLogs() {
+        // Not meant to be instantiated
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_Debug.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_Debug.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_Debug.java
new file mode 100644
index 0000000..e15374b
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_Debug.java
@@ -0,0 +1,122 @@
+/*
+ * 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.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+
+/**
+ * Created by Én on 2/26/2017.
+ */
+public final class _Debug {
+
+    private _Debug() {
+        //
+    }
+
+
+    public static void insertDebugBreak(Template t, int line) {
+        ASTElement te = findTemplateElement(t.getRootASTNode(), line);
+        if (te == null) {
+            return;
+        }
+        ASTElement parent = te.getParent();
+        ASTDebugBreak db = new ASTDebugBreak(te);
+        // TODO: Ensure there always is a parent by making sure
+        // that the root element in the template is always a ASTImplicitParent
+        // Also make sure it doesn't conflict with anyone's code.
+        parent.setChildAt(parent.getIndex(te), db);
+    }
+
+    public static void removeDebugBreak(Template t, int line) {
+        ASTElement te = findTemplateElement(t.getRootASTNode(), line);
+        if (te == null) {
+            return;
+        }
+        ASTDebugBreak db = null;
+        while (te != null) {
+            if (te instanceof ASTDebugBreak) {
+                db = (ASTDebugBreak) te;
+                break;
+            }
+            te = te.getParent();
+        }
+        if (db == null) {
+            return;
+        }
+        ASTElement parent = db.getParent();
+        parent.setChildAt(parent.getIndex(db), db.getChild(0));
+    }
+
+    private static ASTElement findTemplateElement(ASTElement te, int line) {
+        if (te.getBeginLine() > line || te.getEndLine() < line) {
+            return null;
+        }
+        // Find the narrowest match
+        List childMatches = new ArrayList();
+        for (Enumeration children = te.children(); children.hasMoreElements(); ) {
+            ASTElement child = (ASTElement) children.nextElement();
+            ASTElement childmatch = findTemplateElement(child, line);
+            if (childmatch != null) {
+                childMatches.add(childmatch);
+            }
+        }
+        //find a match that exactly matches the begin/end line
+        ASTElement bestMatch = null;
+        for (int i = 0; i < childMatches.size(); i++) {
+            ASTElement e = (ASTElement) childMatches.get(i);
+
+            if ( bestMatch == null ) {
+                bestMatch = e;
+            }
+
+            if ( e.getBeginLine() == line && e.getEndLine() > line ) {
+                bestMatch = e;
+            }
+
+            if ( e.getBeginLine() == e.getEndLine() && e.getBeginLine() == line) {
+                bestMatch = e;
+                break;
+            }
+        }
+        if ( bestMatch != null) {
+            return bestMatch;
+        }
+        // If no child provides narrower match, return this
+        return te;
+    }
+
+    public static void removeDebugBreaks(Template t) {
+        removeDebugBreaks(t.getRootASTNode());
+    }
+
+    private static void removeDebugBreaks(ASTElement te) {
+        int count = te.getChildCount();
+        for (int i = 0; i < count; ++i) {
+            ASTElement child = te.getChild(i);
+            while (child instanceof ASTDebugBreak) {
+                ASTElement dbchild = child.getChild(0);
+                te.setChildAt(i, dbchild);
+                child = dbchild;
+            }
+            removeDebugBreaks(child);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedAOrAn.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedAOrAn.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedAOrAn.java
new file mode 100644
index 0000000..630fa26
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedAOrAn.java
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+/** Don't use this; used internally by FreeMarker, might changes without notice. */
+public class _DelayedAOrAn extends _DelayedConversionToString {
+
+    public _DelayedAOrAn(Object object) {
+        super(object);
+    }
+
+    @Override
+    protected String doConversion(Object obj) {
+        String s = obj.toString();
+        return MessageUtil.getAOrAn(s) + " " + s;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedConversionToString.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedConversionToString.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedConversionToString.java
new file mode 100644
index 0000000..4fbe13f
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedConversionToString.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;
+
+/** Don't use this; used internally by FreeMarker, might changes without notice. */
+public abstract class _DelayedConversionToString {
+
+    private static final String NOT_SET = new String();
+
+    private Object object;
+    private volatile String stringValue = NOT_SET;
+
+    public _DelayedConversionToString(Object object) {
+        this.object = object;
+    }
+
+    @Override
+    public String toString() {
+        String stringValue = this.stringValue;
+        if (stringValue == NOT_SET) {
+            synchronized (this) {
+                stringValue = this.stringValue;
+                if (stringValue == NOT_SET) {
+                    stringValue = doConversion(object);
+                    this.stringValue = stringValue;
+                    object = null;
+                }
+            }
+        }
+        return stringValue;
+    }
+
+    protected abstract String doConversion(Object obj);
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedFTLTypeDescription.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedFTLTypeDescription.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedFTLTypeDescription.java
new file mode 100644
index 0000000..21b6d55
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedFTLTypeDescription.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.util.FTLUtil;
+
+/** Don't use this; used internally by FreeMarker, might changes without notice. */
+public class _DelayedFTLTypeDescription extends _DelayedConversionToString {
+    
+    public _DelayedFTLTypeDescription(TemplateModel tm) {
+        super(tm);
+    }
+
+    @Override
+    protected String doConversion(Object obj) {
+        return FTLUtil.getTypeDescription((TemplateModel) obj);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedGetCanonicalForm.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedGetCanonicalForm.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedGetCanonicalForm.java
new file mode 100644
index 0000000..38a4cd8
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedGetCanonicalForm.java
@@ -0,0 +1,39 @@
+/*
+ * 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;
+
+
+/** Don't use this; used internally by FreeMarker, might changes without notice. */
+public class _DelayedGetCanonicalForm extends _DelayedConversionToString {
+    
+    public _DelayedGetCanonicalForm(ASTNode obj) {
+        super(obj);
+    }
+
+    @Override
+    protected String doConversion(Object obj) {
+        try {
+            return ((ASTNode) obj).getCanonicalForm();
+        } catch (Exception e) {
+            return "{Error getting canonical form}";
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedGetMessage.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedGetMessage.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedGetMessage.java
new file mode 100644
index 0000000..7bef399
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedGetMessage.java
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+/** Don't use this; used internally by FreeMarker, might changes without notice. */
+public class _DelayedGetMessage extends _DelayedConversionToString {
+
+    public _DelayedGetMessage(Throwable exception) {
+        super(exception);
+    }
+
+    @Override
+    protected String doConversion(Object obj) {
+        final String message = ((Throwable) obj).getMessage();
+        return message == null || message.length() == 0 ? "[No exception message]" : message;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedGetMessageWithoutStackTop.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedGetMessageWithoutStackTop.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedGetMessageWithoutStackTop.java
new file mode 100644
index 0000000..7694c15
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedGetMessageWithoutStackTop.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+/** Don't use this; used internally by FreeMarker, might changes without notice. */
+public class _DelayedGetMessageWithoutStackTop extends _DelayedConversionToString {
+
+    public _DelayedGetMessageWithoutStackTop(TemplateException exception) {
+        super(exception);
+    }
+
+    @Override
+    protected String doConversion(Object obj) {
+        return ((TemplateException) obj).getMessageWithoutStackTop();
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedJQuote.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedJQuote.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedJQuote.java
new file mode 100644
index 0000000..4caf71b
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedJQuote.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.util._StringUtil;
+
+/** Don't use this; used internally by FreeMarker, might changes without notice. */
+public class _DelayedJQuote extends _DelayedConversionToString {
+
+    public _DelayedJQuote(Object object) {
+        super(object);
+    }
+
+    @Override
+    protected String doConversion(Object obj) {
+        return _StringUtil.jQuote(_ErrorDescriptionBuilder.toString(obj));
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedJoinWithComma.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedJoinWithComma.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedJoinWithComma.java
new file mode 100644
index 0000000..7ae1da3
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedJoinWithComma.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+/** Don't use this; used internally by FreeMarker, might changes without notice. */
+public class _DelayedJoinWithComma extends _DelayedConversionToString {
+
+    public _DelayedJoinWithComma(String[] items) {
+        super(items);
+    }
+
+    @Override
+    protected String doConversion(Object obj) {
+        String[] items = (String[]) obj;
+        
+        int totalLength = 0;
+        for (int i = 0; i < items.length; i++) {
+            if (i != 0) totalLength += 2;
+            totalLength += items[i].length();
+        }
+        
+        StringBuilder sb = new StringBuilder(totalLength);
+        for (int i = 0; i < items.length; i++) {
+            if (i != 0) sb.append(", ");
+            sb.append(items[i]);
+        }
+        
+        return sb.toString();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedOrdinal.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedOrdinal.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedOrdinal.java
new file mode 100644
index 0000000..443210d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedOrdinal.java
@@ -0,0 +1,47 @@
+/*
+ * 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;
+
+/** 1 to "1st", 2 to "2nd", etc. */
+public class _DelayedOrdinal extends _DelayedConversionToString {
+
+    public _DelayedOrdinal(Object object) {
+        super(object);
+    }
+
+    @Override
+    protected String doConversion(Object obj) {
+        if (obj instanceof Number) {
+            long n = ((Number) obj).longValue();
+            if (n % 10 == 1 && n % 100 != 11) {
+                return n + "st";
+            } else if (n % 10 == 2 && n % 100 != 12) {
+                return n + "nd";
+            } else if (n % 10 == 3 && n % 100 != 13) {
+                return n + "rd";
+            } else {
+                return n + "th";
+            }
+        } else {
+            return "" + obj;
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedShortClassName.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedShortClassName.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedShortClassName.java
new file mode 100644
index 0000000..d9769b9
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedShortClassName.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.util._ClassUtil;
+
+public class _DelayedShortClassName extends _DelayedConversionToString {
+
+    public _DelayedShortClassName(Class pClass) {
+        super(pClass);
+    }
+
+    @Override
+    protected String doConversion(Object obj) {
+        return _ClassUtil.getShortClassName((Class) obj, true);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedToString.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedToString.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedToString.java
new file mode 100644
index 0000000..5eb5c54
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedToString.java
@@ -0,0 +1,37 @@
+/*
+ * 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;
+
+public class _DelayedToString extends _DelayedConversionToString {
+
+    public _DelayedToString(Object object) {
+        super(object);
+    }
+
+    public _DelayedToString(int object) {
+        super(Integer.valueOf(object));
+    }
+    
+    @Override
+    protected String doConversion(Object obj) {
+        return String.valueOf(obj);
+    }
+    
+}


[18/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/UnsafeMethods.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/UnsafeMethods.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/UnsafeMethods.java
new file mode 100644
index 0000000..e3270c3
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/UnsafeMethods.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.model.impl;
+
+import java.io.InputStream;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.StringTokenizer;
+
+import org.apache.freemarker.core._CoreLogs;
+import org.apache.freemarker.core.util._ClassUtil;
+import org.slf4j.Logger;
+
+class UnsafeMethods {
+    
+    private static final Logger LOG = _CoreLogs.OBJECT_WRAPPER;
+    private static final String UNSAFE_METHODS_PROPERTIES = "unsafeMethods.properties";
+    private static final Set UNSAFE_METHODS = createUnsafeMethodsSet();
+    
+    private UnsafeMethods() { }
+    
+    static boolean isUnsafeMethod(Method method) {
+        return UNSAFE_METHODS.contains(method);        
+    }
+    
+    private static Set createUnsafeMethodsSet() {
+        Properties props = new Properties();
+        InputStream in = DefaultObjectWrapper.class.getResourceAsStream(UNSAFE_METHODS_PROPERTIES);
+        if (in == null) {
+            throw new IllegalStateException("Class loader resource not found: "
+                        + DefaultObjectWrapper.class.getPackage().getName() + UNSAFE_METHODS_PROPERTIES);
+        }
+        String methodSpec = null;
+        try {
+            try {
+                props.load(in);
+            } finally {
+                in.close();
+            }
+            Set set = new HashSet(props.size() * 4 / 3, 1f);
+            Map primClasses = createPrimitiveClassesMap();
+            for (Iterator iterator = props.keySet().iterator(); iterator.hasNext(); ) {
+                methodSpec = (String) iterator.next();
+                try {
+                    set.add(parseMethodSpec(methodSpec, primClasses));
+                } catch (ClassNotFoundException | NoSuchMethodException e) {
+                    LOG.debug("Failed to get unsafe method", e);
+                }
+            }
+            return set;
+        } catch (Exception e) {
+            throw new RuntimeException("Could not load unsafe method " + methodSpec + " " + e.getClass().getName() + " " + e.getMessage());
+        }
+    }
+
+    private static Method parseMethodSpec(String methodSpec, Map primClasses)
+    throws ClassNotFoundException,
+        NoSuchMethodException {
+        int brace = methodSpec.indexOf('(');
+        int dot = methodSpec.lastIndexOf('.', brace);
+        Class clazz = _ClassUtil.forName(methodSpec.substring(0, dot));
+        String methodName = methodSpec.substring(dot + 1, brace);
+        String argSpec = methodSpec.substring(brace + 1, methodSpec.length() - 1);
+        StringTokenizer tok = new StringTokenizer(argSpec, ",");
+        int argcount = tok.countTokens();
+        Class[] argTypes = new Class[argcount];
+        for (int i = 0; i < argcount; i++) {
+            String argClassName = tok.nextToken();
+            argTypes[i] = (Class) primClasses.get(argClassName);
+            if (argTypes[i] == null) {
+                argTypes[i] = _ClassUtil.forName(argClassName);
+            }
+        }
+        return clazz.getMethod(methodName, argTypes);
+    }
+
+    private static Map createPrimitiveClassesMap() {
+        Map map = new HashMap();
+        map.put("boolean", Boolean.TYPE);
+        map.put("byte", Byte.TYPE);
+        map.put("char", Character.TYPE);
+        map.put("short", Short.TYPE);
+        map.put("int", Integer.TYPE);
+        map.put("long", Long.TYPE);
+        map.put("float", Float.TYPE);
+        map.put("double", Double.TYPE);
+        return map;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/_MethodUtil.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/_MethodUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/_MethodUtil.java
new file mode 100644
index 0000000..82da455
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/_MethodUtil.java
@@ -0,0 +1,319 @@
+/*
+ * 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.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Member;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.freemarker.core._DelayedConversionToString;
+import org.apache.freemarker.core._DelayedJQuote;
+import org.apache.freemarker.core._TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.util._ClassUtil;
+
+/**
+ * For internal use only; don't depend on this, there's no backward compatibility guarantee at all!
+ */
+public final class _MethodUtil {
+    
+    private _MethodUtil() {
+        // Not meant to be instantiated
+    }
+
+    /**
+     * Determines whether the type given as the 1st argument is convertible to the type given as the 2nd argument
+     * for method call argument conversion. This follows the rules of the Java reflection-based method call, except
+     * that since we don't have the value here, a boxed class is never seen as convertible to a primitive type. 
+     * 
+     * @return 0 means {@code false}, non-0 means {@code true}.
+     *         That is, 0 is returned less specificity or incomparable specificity, also when if
+     *         then method was aborted because of {@code ifHigherThan}.
+     *         The absolute value of the returned non-0 number symbolizes how more specific it is:
+     *         <ul>
+     *           <li>1: The two classes are identical</li>
+     *           <li>2: The 1st type is primitive, the 2nd type is the corresponding boxing class</li>
+     *           <li>3: Both classes are numerical, and one is convertible into the other with widening conversion.
+     *                  E.g., {@code int} is convertible to {@code long} and {#code double}, hence {@code int} is more
+     *                  specific.
+     *                  This ignores primitive VS boxed mismatches, except that a boxed class is never seen as
+     *                  convertible to a primitive class.</li>
+     *           <li>4: One class is {@code instanceof} of the other, but they aren't identical.
+     *               But unlike in Java, primitive numerical types are {@code instanceof} {@link Number} here.</li>
+     *         </ul> 
+     */
+    // TODO Seems that we don't use the full functionality of this anymore, so we could simplify this. See usages.
+    public static int isMoreOrSameSpecificParameterType(final Class specific, final Class generic, boolean bugfixed,
+            int ifHigherThan) {
+        if (ifHigherThan >= 4) return 0;
+        if (generic.isAssignableFrom(specific)) {
+            // Identity or widening reference conversion:
+            return generic == specific ? 1 : 4;
+        } else {
+            final boolean specificIsPrim = specific.isPrimitive(); 
+            final boolean genericIsPrim = generic.isPrimitive();
+            if (specificIsPrim) {
+                if (genericIsPrim) {
+                    if (ifHigherThan >= 3) return 0;
+                    return isWideningPrimitiveNumberConversion(specific, generic) ? 3 : 0;
+                } else {  // => specificIsPrim && !genericIsPrim
+                    if (bugfixed) {
+                        final Class specificAsBoxed = _ClassUtil.primitiveClassToBoxingClass(specific);
+                        if (specificAsBoxed == generic) {
+                            // A primitive class is more specific than its boxing class, because it can't store null
+                            return 2;
+                        } else if (generic.isAssignableFrom(specificAsBoxed)) {
+                            // Note: This only occurs if `specific` is a primitive numerical, and `generic == Number`
+                            return 4;
+                        } else if (ifHigherThan >= 3) {
+                            return 0;
+                        } else if (Number.class.isAssignableFrom(specificAsBoxed)
+                                && Number.class.isAssignableFrom(generic)) {
+                            return isWideningBoxedNumberConversion(specificAsBoxed, generic) ? 3 : 0;
+                        } else {
+                            return 0;
+                        }
+                    } else {
+                        return 0;
+                    }
+                }
+            } else {  // => !specificIsPrim
+                if (ifHigherThan >= 3) return 0;
+                if (bugfixed && !genericIsPrim
+                        && Number.class.isAssignableFrom(specific) && Number.class.isAssignableFrom(generic)) {
+                    return isWideningBoxedNumberConversion(specific, generic) ? 3 : 0;
+                } else {
+                    return 0;
+                }
+            }
+        }  // of: !generic.isAssignableFrom(specific) 
+    }
+
+    private static boolean isWideningPrimitiveNumberConversion(final Class source, final Class target) {
+        if (target == Short.TYPE && (source == Byte.TYPE)) {
+            return true;
+        } else if (target == Integer.TYPE && 
+           (source == Short.TYPE || source == Byte.TYPE)) {
+            return true;
+        } else if (target == Long.TYPE && 
+           (source == Integer.TYPE || source == Short.TYPE || 
+            source == Byte.TYPE)) {
+            return true;
+        } else if (target == Float.TYPE && 
+           (source == Long.TYPE || source == Integer.TYPE || 
+            source == Short.TYPE || source == Byte.TYPE)) {
+            return true;
+        } else if (target == Double.TYPE && 
+           (source == Float.TYPE || source == Long.TYPE || 
+            source == Integer.TYPE || source == Short.TYPE || 
+            source == Byte.TYPE)) {
+            return true; 
+        } else {
+            return false;
+        }
+    }
+
+    private static boolean isWideningBoxedNumberConversion(final Class source, final Class target) {
+        if (target == Short.class && source == Byte.class) {
+            return true;
+        } else if (target == Integer.class && 
+           (source == Short.class || source == Byte.class)) {
+            return true;
+        } else if (target == Long.class && 
+           (source == Integer.class || source == Short.class || 
+            source == Byte.class)) {
+            return true;
+        } else if (target == Float.class && 
+           (source == Long.class || source == Integer.class || 
+            source == Short.class || source == Byte.class)) {
+            return true;
+        } else if (target == Double.class && 
+           (source == Float.class || source == Long.class || 
+            source == Integer.class || source == Short.class || 
+            source == Byte.class)) {
+            return true; 
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Attention, this doesn't handle primitive classes correctly, nor numerical conversions.
+     */
+    public static Set getAssignables(Class c1, Class c2) {
+        Set s = new HashSet();
+        collectAssignables(c1, c2, s);
+        return s;
+    }
+    
+    private static void collectAssignables(Class c1, Class c2, Set s) {
+        if (c1.isAssignableFrom(c2)) {
+            s.add(c1);
+        }
+        Class sc = c1.getSuperclass();
+        if (sc != null) {
+            collectAssignables(sc, c2, s);
+        }
+        Class[] itf = c1.getInterfaces();
+        for (Class anItf : itf) {
+            collectAssignables(anItf, c2, s);
+        }
+    }
+
+    public static Class[] getParameterTypes(Member member) {
+        if (member instanceof Method) {
+            return ((Method) member).getParameterTypes();
+        }
+        if (member instanceof Constructor) {
+            return ((Constructor) member).getParameterTypes();
+        }
+        throw new IllegalArgumentException("\"member\" must be Method or Constructor");
+    }
+
+    public static boolean isVarargs(Member member) {
+        if (member instanceof Method) { 
+            return ((Method) member).isVarArgs();
+        }
+        if (member instanceof Constructor) {
+            return ((Constructor) member).isVarArgs();
+        }
+        throw new BugException();
+    }
+
+    /**
+     * Returns a more streamlined method or constructor description than {@code Member.toString()} does.
+     */
+    public static String toString(Member member) {
+        if (!(member instanceof Method || member instanceof Constructor)) {
+            throw new IllegalArgumentException("\"member\" must be a Method or Constructor");
+        }
+        
+        StringBuilder sb = new StringBuilder();
+        
+        if ((member.getModifiers() & Modifier.STATIC) != 0) {
+            sb.append("static ");
+        }
+        
+        String className = _ClassUtil.getShortClassName(member.getDeclaringClass());
+        if (className != null) {
+            sb.append(className);
+            sb.append('.');
+        }
+        sb.append(member.getName());
+
+        sb.append('(');
+        Class[] paramTypes = _MethodUtil.getParameterTypes(member);
+        for (int i = 0; i < paramTypes.length; i++) {
+            if (i != 0) sb.append(", ");
+            String paramTypeDecl = _ClassUtil.getShortClassName(paramTypes[i]);
+            if (i == paramTypes.length - 1 && paramTypeDecl.endsWith("[]") && _MethodUtil.isVarargs(member)) {
+                sb.append(paramTypeDecl.substring(0, paramTypeDecl.length() - 2));
+                sb.append("...");
+            } else {
+                sb.append(paramTypeDecl);
+            }
+        }
+        sb.append(')');
+        
+        return sb.toString();
+    }
+
+    public static Object[] invocationErrorMessageStart(Member member) {
+        return invocationErrorMessageStart(member, member instanceof Constructor);
+    }
+    
+    private static Object[] invocationErrorMessageStart(Object member, boolean isConstructor) {
+        return new Object[] { "Java ", isConstructor ? "constructor " : "method ", new _DelayedJQuote(member) };
+    }
+
+    public static TemplateModelException newInvocationTemplateModelException(Object object, Member member, Throwable e) {
+        return newInvocationTemplateModelException(
+                object,
+                member,
+                (member.getModifiers() & Modifier.STATIC) != 0,
+                member instanceof Constructor,
+                e);
+    }
+
+    public static TemplateModelException newInvocationTemplateModelException(Object object, CallableMemberDescriptor callableMemberDescriptor, Throwable e) {
+        return newInvocationTemplateModelException(
+                object,
+                new _DelayedConversionToString(callableMemberDescriptor) {
+                    @Override
+                    protected String doConversion(Object callableMemberDescriptor) {
+                        return ((CallableMemberDescriptor) callableMemberDescriptor).getDeclaration();
+                    }
+                },
+                callableMemberDescriptor.isStatic(),
+                callableMemberDescriptor.isConstructor(),
+                e);
+    }
+    
+    private static TemplateModelException newInvocationTemplateModelException(
+            Object parentObject, Object member, boolean isStatic, boolean isConstructor, Throwable e) {
+        while (e instanceof InvocationTargetException) {
+            Throwable cause = ((InvocationTargetException) e).getTargetException();
+            if (cause != null) {
+                e = cause;
+            } else {
+                break;
+            }
+        }
+
+        return new _TemplateModelException(e,
+                invocationErrorMessageStart(member, isConstructor),
+                " threw an exception",
+                isStatic || isConstructor ? "" : new Object[] {
+                    " when invoked on ", parentObject.getClass(), " object ", new _DelayedJQuote(parentObject) 
+                },
+                "; see cause exception in the Java stack trace.");
+    }
+
+    /**
+     * Extracts the JavaBeans property from a reader method name, or returns {@code null} if the method name doesn't
+     * look like a reader method name.
+     */
+    public static String getBeanPropertyNameFromReaderMethodName(String name, Class<?> returnType) {
+        int start;
+        if (name.startsWith("get")) {
+            start = 3;
+        } else if (returnType == boolean.class && name.startsWith("is")) {
+            start = 2;
+        } else {
+            return null;
+        }
+        int ln = name.length();
+
+        if (start == ln) {
+            return null;
+        }
+        char c1 = name.charAt(start);
+
+        return start + 1 < ln && Character.isUpperCase(name.charAt(start + 1)) && Character.isUpperCase(c1)
+                ? name.substring(start) // getFOOBar => "FOOBar" (not lower case) according the JavaBeans spec.
+                : new StringBuilder(ln - start).append(Character.toLowerCase(c1)).append(name, start + 1, ln)
+                .toString();
+    }
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/_ModelAPI.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/_ModelAPI.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/_ModelAPI.java
new file mode 100644
index 0000000..fecb0b0
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/_ModelAPI.java
@@ -0,0 +1,122 @@
+/*
+ * 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.lang.reflect.Array;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.util._CollectionUtil;
+
+/**
+ * For internal use only; don't depend on this, there's no backward compatibility guarantee at all!
+ * This class is to work around the lack of module system in Java, i.e., so that other FreeMarker packages can
+ * access things inside this package that users shouldn't. 
+ */ 
+public class _ModelAPI {
+
+    private _ModelAPI() { }
+    
+    public static Object newInstance(Class<?> pClass, Object[] args, DefaultObjectWrapper ow)
+            throws NoSuchMethodException, IllegalArgumentException, InstantiationException,
+            IllegalAccessException, InvocationTargetException, TemplateModelException {
+        return newInstance(getConstructorDescriptor(pClass, args), args, ow);
+    }
+    
+    /**
+     * Gets the constructor that matches the types of the arguments the best. So this is more
+     * than what the Java reflection API provides in that it can handle overloaded constructors. This re-uses the
+     * overloaded method selection logic of {@link DefaultObjectWrapper}.
+     */
+    private static CallableMemberDescriptor getConstructorDescriptor(Class<?> pClass, Object[] args)
+            throws NoSuchMethodException {
+        if (args == null) args = _CollectionUtil.EMPTY_OBJECT_ARRAY;
+        
+        final ArgumentTypes argTypes = new ArgumentTypes(args);
+        final List<ReflectionCallableMemberDescriptor> fixedArgMemberDescs
+                = new ArrayList<>();
+        final List<ReflectionCallableMemberDescriptor> varArgsMemberDescs
+                = new ArrayList<>();
+        for (Constructor<?> constr : pClass.getConstructors()) {
+            ReflectionCallableMemberDescriptor memberDesc = new ReflectionCallableMemberDescriptor(constr, constr.getParameterTypes());
+            if (!_MethodUtil.isVarargs(constr)) {
+                fixedArgMemberDescs.add(memberDesc);
+            } else {
+                varArgsMemberDescs.add(memberDesc);
+            }
+        }
+        
+        MaybeEmptyCallableMemberDescriptor contrDesc = argTypes.getMostSpecific(fixedArgMemberDescs, false);
+        if (contrDesc == EmptyCallableMemberDescriptor.NO_SUCH_METHOD) {
+            contrDesc = argTypes.getMostSpecific(varArgsMemberDescs, true);
+        }
+        
+        if (contrDesc instanceof EmptyCallableMemberDescriptor) {
+            if (contrDesc == EmptyCallableMemberDescriptor.NO_SUCH_METHOD) {
+                throw new NoSuchMethodException(
+                        "There's no public " + pClass.getName()
+                        + " constructor with compatible parameter list.");
+            } else if (contrDesc == EmptyCallableMemberDescriptor.AMBIGUOUS_METHOD) {
+                throw new NoSuchMethodException(
+                        "There are multiple public " + pClass.getName()
+                        + " constructors that match the compatible parameter list with the same preferability.");
+            } else {
+                throw new NoSuchMethodException();
+            }
+        } else {
+            return (CallableMemberDescriptor) contrDesc;
+        }
+    }
+    
+    private static Object newInstance(CallableMemberDescriptor constrDesc, Object[] args, DefaultObjectWrapper ow)
+            throws InstantiationException, IllegalAccessException, InvocationTargetException, IllegalArgumentException,
+            TemplateModelException {
+        if (args == null) args = _CollectionUtil.EMPTY_OBJECT_ARRAY;
+        
+        final Object[] packedArgs;
+        if (constrDesc.isVarargs()) {
+            // We have to put all the varargs arguments into a single array argument.
+
+            final Class<?>[] paramTypes = constrDesc.getParamTypes();
+            final int fixedArgCnt = paramTypes.length - 1;
+            
+            packedArgs = new Object[fixedArgCnt + 1]; 
+            for (int i = 0; i < fixedArgCnt; i++) {
+                packedArgs[i] = args[i];
+            }
+            
+            final Class<?> compType = paramTypes[fixedArgCnt].getComponentType();
+            final int varArgCnt = args.length - fixedArgCnt;
+            final Object varArgsArray = Array.newInstance(compType, varArgCnt);
+            for (int i = 0; i < varArgCnt; i++) {
+                Array.set(varArgsArray, i, args[fixedArgCnt + i]);
+            }
+            packedArgs[fixedArgCnt] = varArgsArray;
+        } else {
+            packedArgs = args;
+        }
+        
+        return constrDesc.invokeConstructor(ow, packedArgs);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/package.html
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/package.html b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/package.html
new file mode 100644
index 0000000..b3db746
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/package.html
@@ -0,0 +1,26 @@
+<!--
+  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.
+-->
+<html>
+<head>
+</head>
+<body>
+<p>Data model and template language type system: Standard implementations. This package is part of the
+published API, that is, user code can safely depend on it.</p>
+</body>
+</html>

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/package.html
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/package.html b/freemarker-core/src/main/java/org/apache/freemarker/core/model/package.html
new file mode 100644
index 0000000..a2a9cfe
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/package.html
@@ -0,0 +1,25 @@
+<!--
+  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.
+-->
+<html>
+<head>
+</head>
+<body>
+<p>Data model and template language type system: Base classes/interfaces.</p>
+</body>
+</html>

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/CommonMarkupOutputFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/CommonMarkupOutputFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/CommonMarkupOutputFormat.java
new file mode 100644
index 0000000..760f28b
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/CommonMarkupOutputFormat.java
@@ -0,0 +1,124 @@
+/*
+ * 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.outputformat;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import org.apache.freemarker.core.model.TemplateModelException;
+
+/**
+ * Common superclass for implementing {@link MarkupOutputFormat}-s that use a {@link CommonTemplateMarkupOutputModel}
+ * subclass.
+ * 
+ * @since 2.3.24
+ */
+public abstract class CommonMarkupOutputFormat<MO extends CommonTemplateMarkupOutputModel>
+        extends MarkupOutputFormat<MO> {
+
+    protected CommonMarkupOutputFormat() {
+        // Only to decrease visibility
+    }
+    
+    @Override
+    public final MO fromPlainTextByEscaping(String textToEsc) throws TemplateModelException {
+        return newTemplateMarkupOutputModel(textToEsc, null);
+    }
+
+    @Override
+    public final MO fromMarkup(String markupText) throws TemplateModelException {
+        return newTemplateMarkupOutputModel(null, markupText);
+    }
+
+    @Override
+    public final void output(MO mo, Writer out) throws IOException, TemplateModelException {
+        String mc = mo.getMarkupContent();
+        if (mc != null) {
+            out.write(mc);
+        } else {
+            output(mo.getPlainTextContent(), out);
+        }
+    }
+
+    @Override
+    public abstract void output(String textToEsc, Writer out) throws IOException, TemplateModelException;
+    
+    @Override
+    public final String getSourcePlainText(MO mo) throws TemplateModelException {
+        return mo.getPlainTextContent();
+    }
+
+    @Override
+    public final String getMarkupString(MO mo) throws TemplateModelException {
+        String mc = mo.getMarkupContent();
+        if (mc != null) {
+            return mc;
+        }
+        
+        mc = escapePlainText(mo.getPlainTextContent());
+        mo.setMarkupContent(mc);
+        return mc;
+    }
+    
+    @Override
+    public final MO concat(MO mo1, MO mo2) throws TemplateModelException {
+        String pc1 = mo1.getPlainTextContent();
+        String mc1 = mo1.getMarkupContent();
+        String pc2 = mo2.getPlainTextContent();
+        String mc2 = mo2.getMarkupContent();
+        
+        String pc3 = pc1 != null && pc2 != null ? pc1 + pc2 : null;
+        String mc3 = mc1 != null && mc2 != null ? mc1 + mc2 : null;
+        if (pc3 != null || mc3 != null) {
+            return newTemplateMarkupOutputModel(pc3, mc3);
+        }
+        
+        if (pc1 != null) {
+            return newTemplateMarkupOutputModel(null, getMarkupString(mo1) + mc2);
+        } else {
+            return newTemplateMarkupOutputModel(null, mc1 + getMarkupString(mo2));
+        }
+    }
+    
+    @Override
+    public boolean isEmpty(MO mo) throws TemplateModelException {
+        String s = mo.getPlainTextContent();
+        if (s != null) {
+            return s.length() == 0;
+        }
+        return mo.getMarkupContent().length() == 0;
+    }
+    
+    @Override
+    public boolean isOutputFormatMixingAllowed() {
+        return false;
+    }
+    
+    @Override
+    public boolean isAutoEscapedByDefault() {
+        return true;
+    }
+
+    /**
+     * Creates a new {@link CommonTemplateMarkupOutputModel} that's bound to this {@link OutputFormat} instance.
+     */
+    protected abstract MO newTemplateMarkupOutputModel(String plainTextContent, String markupContent)
+            throws TemplateModelException;
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/CommonTemplateMarkupOutputModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/CommonTemplateMarkupOutputModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/CommonTemplateMarkupOutputModel.java
new file mode 100644
index 0000000..c6a7894
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/CommonTemplateMarkupOutputModel.java
@@ -0,0 +1,69 @@
+/*
+ * 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.outputformat;
+
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+
+/**
+ * Common superclass for implementing {@link TemplateMarkupOutputModel}-s that belong to a
+ * {@link CommonMarkupOutputFormat} subclass format.
+ * 
+ * <p>
+ * Thread-safe after proper publishing. Calculated fields (typically, the markup calculated from plain text) might will
+ * be re-calculated for multiple times if accessed from multiple threads (this only affects performance, not
+ * functionality).
+ * 
+ * @since 2.3.24
+ */
+public abstract class CommonTemplateMarkupOutputModel<MO extends CommonTemplateMarkupOutputModel<MO>>
+        implements TemplateMarkupOutputModel<MO> {
+
+    private final String plainTextContent;
+    private String markupContent;
+
+    /**
+     * A least one of the parameters must be non-{@code null}!
+     */
+    protected CommonTemplateMarkupOutputModel(String plainTextContent, String markupContent) {
+        this.plainTextContent = plainTextContent;
+        this.markupContent = markupContent;
+    }
+
+    @Override
+    public abstract CommonMarkupOutputFormat<MO> getOutputFormat();
+
+    /** Maybe {@code null}, but then {@link #getMarkupContent()} isn't {@code null}. */
+    final String getPlainTextContent() {
+        return plainTextContent;
+    }
+
+    /** Maybe {@code null}, but then {@link #getPlainTextContent()} isn't {@code null}. */
+    final String getMarkupContent() {
+        return markupContent;
+    }
+
+    /**
+     * Use only to set the value calculated from {@link #getPlainTextContent()}, when {@link #getMarkupContent()} was
+     * still {@code null}!
+     */
+    final void setMarkupContent(String markupContent) {
+        this.markupContent = markupContent;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/MarkupOutputFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/MarkupOutputFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/MarkupOutputFormat.java
new file mode 100644
index 0000000..aac7d54
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/MarkupOutputFormat.java
@@ -0,0 +1,135 @@
+/*
+ * 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.outputformat;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.outputformat.impl.HTMLOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.TemplateHTMLOutputModel;
+
+/**
+ * Superclass of {@link OutputFormat}-s that represent a "markup" format, which is any format where certain character
+ * sequences have special meaning and thus may need escaping. (Escaping is important for FreeMarker, as typically it has
+ * to insert non-markup text from the data-model into the output markup. See also the
+ * {@link Configuration#getOutputFormat() outputFormat} configuration setting.)
+ * 
+ * <p>
+ * A {@link MarkupOutputFormat} subclass always has a corresponding {@link TemplateMarkupOutputModel} subclass pair
+ * (like {@link HTMLOutputFormat} has {@link TemplateHTMLOutputModel}). The {@link OutputFormat} implements the
+ * operations related to {@link TemplateMarkupOutputModel} objects of that kind, while the
+ * {@link TemplateMarkupOutputModel} only encapsulates the data (the actual markup or text).
+ * 
+ * <p>
+ * To implement a custom output format, you may want to extend {@link CommonMarkupOutputFormat}.
+ * 
+ * @param <MO>
+ *            The {@link TemplateMarkupOutputModel} class this output format can deal with.
+ * 
+ * @since 2.3.24
+ */
+public abstract class MarkupOutputFormat<MO extends TemplateMarkupOutputModel> extends OutputFormat {
+
+    protected MarkupOutputFormat() {
+        // Only to decrease visibility
+    }
+    
+    /**
+     * Converts a {@link String} that's assumed to be plain text to {@link TemplateMarkupOutputModel}, by escaping any
+     * special characters in the plain text. This corresponds to {@code ?esc}, or, to outputting with auto-escaping if
+     * that wasn't using {@link #output(String, Writer)} as an optimization.
+     * 
+     * @see #escapePlainText(String)
+     * @see #getSourcePlainText(TemplateMarkupOutputModel)
+     */
+    public abstract MO fromPlainTextByEscaping(String textToEsc) throws TemplateModelException;
+
+    /**
+     * Wraps a {@link String} that's already markup to {@link TemplateMarkupOutputModel} interface, to indicate its
+     * format. This corresponds to {@code ?noEsc}. (This methods is allowed to throw {@link TemplateModelException} if
+     * the parameter markup text is malformed, but it's unlikely that an implementation chooses to parse the parameter
+     * until, and if ever, that becomes necessary.)
+     * 
+     * @see #getMarkupString(TemplateMarkupOutputModel)
+     */
+    public abstract MO fromMarkup(String markupText) throws TemplateModelException;
+
+    /**
+     * Prints the parameter model to the output.
+     */
+    public abstract void output(MO mo, Writer out) throws IOException, TemplateModelException;
+
+    /**
+     * Equivalent to calling {@link #fromPlainTextByEscaping(String)} and then
+     * {@link #output(TemplateMarkupOutputModel, Writer)}, but the implementation may uses a more efficient solution.
+     */
+    public abstract void output(String textToEsc, Writer out) throws IOException, TemplateModelException;
+    
+    /**
+     * If this {@link TemplateMarkupOutputModel} was created with {@link #fromPlainTextByEscaping(String)}, it returns
+     * the original plain text, otherwise it returns {@code null}. Useful for converting between different types
+     * of markups, as if the source format can be converted to plain text without loss, then that just has to be
+     * re-escaped with the target format to do the conversion.
+     */
+    public abstract String getSourcePlainText(MO mo) throws TemplateModelException;
+
+    /**
+     * Returns the content as markup text; never {@code null}. If this {@link TemplateMarkupOutputModel} was created
+     * with {@link #fromMarkup(String)}, it might returns the original markup text literally, but this is not required
+     * as far as the returned markup means the same. If this {@link TemplateMarkupOutputModel} wasn't created
+     * with {@link #fromMarkup(String)} and it doesn't yet have the markup, it has to generate the markup now.
+     */
+    public abstract String getMarkupString(MO mo) throws TemplateModelException;
+    
+    /**
+     * Returns a {@link TemplateMarkupOutputModel} that contains the content of both {@link TemplateMarkupOutputModel}
+     * objects concatenated.
+     */
+    public abstract MO concat(MO mo1, MO mo2) throws TemplateModelException;
+    
+    /**
+     * Should give the same result as {@link #fromPlainTextByEscaping(String)} and then
+     * {@link #getMarkupString(TemplateMarkupOutputModel)}, but the implementation may uses a more efficient solution.
+     */
+    public abstract String escapePlainText(String plainTextContent) throws TemplateModelException;
+
+    /**
+     * Returns if the markup is empty (0 length). This is used by at least {@code ?hasContent}.
+     */
+    public abstract boolean isEmpty(MO mo) throws TemplateModelException;
+    
+    /**
+     * Tells if a string built-in that can't handle a {@link TemplateMarkupOutputModel} left hand operand can bypass
+     * this object as is. A typical such case would be when a {@link TemplateHTMLOutputModel} of "HTML" format bypasses
+     * {@code ?html}.
+     */
+    public abstract boolean isLegacyBuiltInBypassed(String builtInName) throws TemplateModelException;
+    
+    /**
+     * Tells if by default auto-escaping should be on for this format. It should be {@code true} if you need to escape
+     * on most of the places where you insert values.
+     * 
+     * @see Configuration#getAutoEscapingPolicy()
+     */
+    public abstract boolean isAutoEscapedByDefault();
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/OutputFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/OutputFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/OutputFormat.java
new file mode 100644
index 0000000..8004ae2
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/OutputFormat.java
@@ -0,0 +1,86 @@
+/*
+ * 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.outputformat;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.outputformat.impl.UndefinedOutputFormat;
+import org.apache.freemarker.core.util._ClassUtil;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * Represents an output format. If you need auto-escaping, see its subclass, {@link MarkupOutputFormat}.
+ * 
+ * @see Configuration#getOutputFormat()
+ * @see Configuration#getRegisteredCustomOutputFormats()
+ * @see MarkupOutputFormat
+ * 
+ * @since 2.3.24
+ */
+public abstract class OutputFormat {
+
+    /**
+     * The short name used to refer to this format (like in the {@code #ftl} header).
+     */
+    public abstract String getName();
+    
+    /**
+     * Returns the MIME type of the output format. This might comes handy when generating a HTTP response. {@code null}
+     * {@code null} if this output format doesn't clearly corresponds to a specific MIME type.
+     */
+    public abstract String getMimeType();
+
+    /**
+     * Tells if this output format allows inserting {@link TemplateMarkupOutputModel}-s of another output formats into
+     * it. If {@code true}, the foreign {@link TemplateMarkupOutputModel} will be inserted into the output as is (like
+     * if the surrounding output format was the same). This is usually a bad idea to allow, as such an event could
+     * indicate application bugs. If this method returns {@code false} (recommended), then FreeMarker will try to
+     * assimilate the inserted value by converting its format to this format, which will currently (2.3.24) cause
+     * exception, unless the inserted value is made by escaping plain text and the target format is non-escaping, in
+     * which case format conversion is trivially possible. (It's not impossible that conversions will be extended beyond
+     * this, if there will be demand for that.)
+     * 
+     * <p>
+     * {@code true} value is used by {@link UndefinedOutputFormat}.
+     */
+    public abstract boolean isOutputFormatMixingAllowed();
+
+    /**
+     * Returns the short description of this format, to be used in error messages.
+     * Override {@link #toStringExtraProperties()} to customize this.
+     */
+    @Override
+    public final String toString() {
+        String extras = toStringExtraProperties();
+        return getName() + "("
+                + "mimeType=" + _StringUtil.jQuote(getMimeType()) + ", "
+                + "class=" + _ClassUtil.getShortClassNameOfObject(this, true)
+                + (extras.length() != 0 ? ", " : "") + extras
+                + ")";
+    }
+    
+    /**
+     * Should be like {@code "foo=\"something\", bar=123"}; this will be inserted inside the parentheses in
+     * {@link #toString()}. Shouldn't return {@code null}; should return {@code ""} if there are no extra properties.  
+     */
+    protected String toStringExtraProperties() {
+        return "";
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/UnregisteredOutputFormatException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/UnregisteredOutputFormatException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/UnregisteredOutputFormatException.java
new file mode 100644
index 0000000..86dbfa3
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/UnregisteredOutputFormatException.java
@@ -0,0 +1,39 @@
+/*
+ * 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.outputformat;
+
+import org.apache.freemarker.core.Configuration;
+
+/**
+ * Thrown by {@link Configuration#getOutputFormat(String)}.
+ * 
+ * @since 2.3.24
+ */
+@SuppressWarnings("serial")
+public class UnregisteredOutputFormatException extends Exception {
+
+    public UnregisteredOutputFormatException(String message) {
+        this(message, null);
+    }
+    
+    public UnregisteredOutputFormatException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/CSSOutputFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/CSSOutputFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/CSSOutputFormat.java
new file mode 100644
index 0000000..6a03d54
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/CSSOutputFormat.java
@@ -0,0 +1,54 @@
+/*
+ * 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.outputformat.impl;
+
+import org.apache.freemarker.core.outputformat.OutputFormat;
+
+/**
+ * Represents the CSS output format (MIME type "text/css", name "CSS"). This format doesn't support escaping.
+ * 
+ * @since 2.3.24
+ */
+public class CSSOutputFormat extends OutputFormat {
+
+    /**
+     * The only instance (singleton) of this {@link OutputFormat}.
+     */
+    public static final CSSOutputFormat INSTANCE = new CSSOutputFormat();
+    
+    private CSSOutputFormat() {
+        // Only to decrease visibility
+    }
+    
+    @Override
+    public String getName() {
+        return "CSS";
+    }
+
+    @Override
+    public String getMimeType() {
+        return "text/css";
+    }
+
+    @Override
+    public boolean isOutputFormatMixingAllowed() {
+        return false;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/CombinedMarkupOutputFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/CombinedMarkupOutputFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/CombinedMarkupOutputFormat.java
new file mode 100644
index 0000000..5239e3f
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/CombinedMarkupOutputFormat.java
@@ -0,0 +1,108 @@
+/*
+ * 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.outputformat.impl;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.outputformat.CommonMarkupOutputFormat;
+import org.apache.freemarker.core.outputformat.MarkupOutputFormat;
+
+/**
+ * Represents two markup formats nested into each other. For example, markdown nested into HTML.
+ * 
+ * @since 2.3.24
+ */
+public final class CombinedMarkupOutputFormat extends CommonMarkupOutputFormat<TemplateCombinedMarkupOutputModel> {
+
+    private final String name;
+    
+    private final MarkupOutputFormat outer;
+    private final MarkupOutputFormat inner;
+
+    /**
+     * Same as {@link #CombinedMarkupOutputFormat(String, MarkupOutputFormat, MarkupOutputFormat)} with {@code null} as
+     * the {@code name} parameter.
+     */
+    public CombinedMarkupOutputFormat(MarkupOutputFormat outer, MarkupOutputFormat inner) {
+        this(null, outer, inner);
+    }
+    
+    /**
+     * @param name
+     *            Maybe {@code null}, in which case it defaults to
+     *            <code>outer.getName() + "{" + inner.getName() + "}"</code>.
+     */
+    public CombinedMarkupOutputFormat(String name, MarkupOutputFormat outer, MarkupOutputFormat inner) {
+        this.name = name != null ? null : outer.getName() + "{" + inner.getName() + "}";
+        this.outer = outer;
+        this.inner = inner;
+    }
+    
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public String getMimeType() {
+        return outer.getMimeType();
+    }
+
+    @Override
+    public void output(String textToEsc, Writer out) throws IOException, TemplateModelException {
+        outer.output(inner.escapePlainText(textToEsc), out);
+    }
+
+    @Override
+    public String escapePlainText(String plainTextContent) throws TemplateModelException {
+        return outer.escapePlainText(inner.escapePlainText(plainTextContent));
+    }
+
+    @Override
+    public boolean isLegacyBuiltInBypassed(String builtInName) throws TemplateModelException {
+        return outer.isLegacyBuiltInBypassed(builtInName);
+    }
+
+    @Override
+    public boolean isAutoEscapedByDefault() {
+        return outer.isAutoEscapedByDefault();
+    }
+    
+    @Override
+    public boolean isOutputFormatMixingAllowed() {
+        return outer.isOutputFormatMixingAllowed();
+    }
+
+    public MarkupOutputFormat getOuterOutputFormat() {
+        return outer;
+    }
+
+    public MarkupOutputFormat getInnerOutputFormat() {
+        return inner;
+    }
+
+    @Override
+    protected TemplateCombinedMarkupOutputModel newTemplateMarkupOutputModel(
+            String plainTextContent, String markupContent) {
+        return new TemplateCombinedMarkupOutputModel(plainTextContent, markupContent, this);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/HTMLOutputFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/HTMLOutputFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/HTMLOutputFormat.java
new file mode 100644
index 0000000..0cebf64
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/HTMLOutputFormat.java
@@ -0,0 +1,77 @@
+/*
+ * 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.outputformat.impl;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.outputformat.CommonMarkupOutputFormat;
+import org.apache.freemarker.core.outputformat.OutputFormat;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * Represents the HTML output format (MIME type "text/html", name "HTML"). This format escapes by default (via
+ * {@link _StringUtil#XHTMLEnc(String)}). The {@code ?html}, {@code ?xhtml} and {@code ?xml} built-ins silently bypass
+ * template output values of the type produced by this output format ({@link TemplateHTMLOutputModel}).
+ * 
+ * @since 2.3.24
+ */
+public final class HTMLOutputFormat extends CommonMarkupOutputFormat<TemplateHTMLOutputModel> {
+
+    /**
+     * The only instance (singleton) of this {@link OutputFormat}.
+     */
+    public static final HTMLOutputFormat INSTANCE = new HTMLOutputFormat();
+    
+    private HTMLOutputFormat() {
+        // Only to decrease visibility
+    }
+    
+    @Override
+    public String getName() {
+        return "HTML";
+    }
+
+    @Override
+    public String getMimeType() {
+        return "text/html";
+    }
+
+    @Override
+    public void output(String textToEsc, Writer out) throws IOException, TemplateModelException {
+        _StringUtil.XHTMLEnc(textToEsc, out);
+    }
+
+    @Override
+    public String escapePlainText(String plainTextContent) {
+        return _StringUtil.XHTMLEnc(plainTextContent);
+    }
+
+    @Override
+    public boolean isLegacyBuiltInBypassed(String builtInName) {
+        return builtInName.equals("html") || builtInName.equals("xml") || builtInName.equals("xhtml");
+    }
+
+    @Override
+    protected TemplateHTMLOutputModel newTemplateMarkupOutputModel(String plainTextContent, String markupContent) {
+        return new TemplateHTMLOutputModel(plainTextContent, markupContent);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/JSONOutputFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/JSONOutputFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/JSONOutputFormat.java
new file mode 100644
index 0000000..c420e69
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/JSONOutputFormat.java
@@ -0,0 +1,54 @@
+/*
+ * 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.outputformat.impl;
+
+import org.apache.freemarker.core.outputformat.OutputFormat;
+
+/**
+ * Represents the JSON output format (MIME type "application/json", name "JSON"). This format doesn't support escaping.
+ * 
+ * @since 2.3.24
+ */
+public class JSONOutputFormat extends OutputFormat {
+
+    /**
+     * The only instance (singleton) of this {@link OutputFormat}.
+     */
+    public static final JSONOutputFormat INSTANCE = new JSONOutputFormat();
+    
+    private JSONOutputFormat() {
+        // Only to decrease visibility
+    }
+    
+    @Override
+    public String getName() {
+        return "JSON";
+    }
+
+    @Override
+    public String getMimeType() {
+        return "application/json";
+    }
+
+    @Override
+    public boolean isOutputFormatMixingAllowed() {
+        return false;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/JavaScriptOutputFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/JavaScriptOutputFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/JavaScriptOutputFormat.java
new file mode 100644
index 0000000..b2e8176
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/JavaScriptOutputFormat.java
@@ -0,0 +1,55 @@
+/*
+ * 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.outputformat.impl;
+
+import org.apache.freemarker.core.outputformat.OutputFormat;
+
+/**
+ * Represents the JavaScript output format (MIME type "application/javascript", name "JavaScript"). This format doesn't
+ * support escaping.
+ * 
+ * @since 2.3.24
+ */
+public class JavaScriptOutputFormat extends OutputFormat {
+
+    /**
+     * The only instance (singleton) of this {@link OutputFormat}.
+     */
+    public static final JavaScriptOutputFormat INSTANCE = new JavaScriptOutputFormat();
+    
+    private JavaScriptOutputFormat() {
+        // Only to decrease visibility
+    }
+    
+    @Override
+    public String getName() {
+        return "JavaScript";
+    }
+
+    @Override
+    public String getMimeType() {
+        return "application/javascript";
+    }
+
+    @Override
+    public boolean isOutputFormatMixingAllowed() {
+        return false;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/PlainTextOutputFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/PlainTextOutputFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/PlainTextOutputFormat.java
new file mode 100644
index 0000000..13cddc8
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/PlainTextOutputFormat.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.outputformat.impl;
+
+import org.apache.freemarker.core.outputformat.OutputFormat;
+
+/**
+ * Represents the plain text output format (MIME type "text/plain", name "plainText"). This format doesn't support
+ * escaping. This format doesn't allow mixing in template output values of other output formats.
+ * 
+ * <p>
+ * The main difference from {@link UndefinedOutputFormat} is that this format doesn't allow inserting values of another
+ * output format into itself (unless they can be converted to plain text), while {@link UndefinedOutputFormat} would
+ * just insert the foreign "markup" as is. Also, this format has {"text/plain"} MIME type, while
+ * {@link UndefinedOutputFormat} has {@code null}.
+ * 
+ * @since 2.3.24
+ */
+public final class PlainTextOutputFormat extends OutputFormat {
+
+    public static final PlainTextOutputFormat INSTANCE = new PlainTextOutputFormat();
+    
+    private PlainTextOutputFormat() {
+        // Only to decrease visibility
+    }
+
+    @Override
+    public boolean isOutputFormatMixingAllowed() {
+        return false;
+    }
+
+    @Override
+    public String getName() {
+        return "plainText";
+    }
+
+    @Override
+    public String getMimeType() {
+        return "text/plain";
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/RTFOutputFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/RTFOutputFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/RTFOutputFormat.java
new file mode 100644
index 0000000..be38b89
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/RTFOutputFormat.java
@@ -0,0 +1,77 @@
+/*
+ * 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.outputformat.impl;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.outputformat.CommonMarkupOutputFormat;
+import org.apache.freemarker.core.outputformat.OutputFormat;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * Represents the Rich Text Format output format (MIME type "application/rtf", name "RTF"). This format escapes by
+ * default (via {@link _StringUtil#RTFEnc(String)}). The {@code ?rtf} built-in silently bypasses template output values
+ * of the type produced by this output format ({@link TemplateRTFOutputModel}).
+ * 
+ * @since 2.3.24
+ */
+public final class RTFOutputFormat extends CommonMarkupOutputFormat<TemplateRTFOutputModel> {
+
+    /**
+     * The only instance (singleton) of this {@link OutputFormat}.
+     */
+    public static final RTFOutputFormat INSTANCE = new RTFOutputFormat();
+    
+    private RTFOutputFormat() {
+        // Only to decrease visibility
+    }
+    
+    @Override
+    public String getName() {
+        return "RTF";
+    }
+
+    @Override
+    public String getMimeType() {
+        return "application/rtf";
+    }
+
+    @Override
+    public void output(String textToEsc, Writer out) throws IOException, TemplateModelException {
+        _StringUtil.RTFEnc(textToEsc, out);
+    }
+
+    @Override
+    public String escapePlainText(String plainTextContent) {
+        return _StringUtil.RTFEnc(plainTextContent);
+    }
+
+    @Override
+    public boolean isLegacyBuiltInBypassed(String builtInName) {
+        return builtInName.equals("rtf");
+    }
+
+    @Override
+    protected TemplateRTFOutputModel newTemplateMarkupOutputModel(String plainTextContent, String markupContent) {
+        return new TemplateRTFOutputModel(plainTextContent, markupContent);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/TemplateCombinedMarkupOutputModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/TemplateCombinedMarkupOutputModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/TemplateCombinedMarkupOutputModel.java
new file mode 100644
index 0000000..345a197
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/TemplateCombinedMarkupOutputModel.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.outputformat.impl;
+
+import org.apache.freemarker.core.outputformat.CommonTemplateMarkupOutputModel;
+
+/**
+ * Stores combined markup to be printed; used with {@link CombinedMarkupOutputFormat}.
+ * 
+ * @since 2.3.24
+ */
+public final class TemplateCombinedMarkupOutputModel
+        extends CommonTemplateMarkupOutputModel<TemplateCombinedMarkupOutputModel> {
+    
+    private final CombinedMarkupOutputFormat outputFormat;
+    
+    /**
+     * See {@link CommonTemplateMarkupOutputModel#CommonTemplateMarkupOutputModel(String, String)}.
+     * 
+     * @param outputFormat
+     *            The {@link CombinedMarkupOutputFormat} format this value is bound to. Because
+     *            {@link CombinedMarkupOutputFormat} has no singleton, we have to pass it in, unlike with most other
+     *            {@link CommonTemplateMarkupOutputModel}-s.
+     */
+    TemplateCombinedMarkupOutputModel(String plainTextContent, String markupContent,
+            CombinedMarkupOutputFormat outputFormat) {
+        super(plainTextContent, markupContent);
+        this.outputFormat = outputFormat; 
+    }
+
+    @Override
+    public CombinedMarkupOutputFormat getOutputFormat() {
+        return outputFormat;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/TemplateHTMLOutputModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/TemplateHTMLOutputModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/TemplateHTMLOutputModel.java
new file mode 100644
index 0000000..7bff952
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/TemplateHTMLOutputModel.java
@@ -0,0 +1,42 @@
+/*
+ * 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.outputformat.impl;
+
+import org.apache.freemarker.core.outputformat.CommonTemplateMarkupOutputModel;
+
+/**
+ * Stores HTML markup to be printed; used with {@link HTMLOutputFormat}.
+ * 
+ * @since 2.3.24
+ */
+public final class TemplateHTMLOutputModel extends CommonTemplateMarkupOutputModel<TemplateHTMLOutputModel> {
+    
+    /**
+     * See {@link CommonTemplateMarkupOutputModel#CommonTemplateMarkupOutputModel(String, String)}.
+     */
+    TemplateHTMLOutputModel(String plainTextContent, String markupContent) {
+        super(plainTextContent, markupContent);
+    }
+
+    @Override
+    public HTMLOutputFormat getOutputFormat() {
+        return HTMLOutputFormat.INSTANCE;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/TemplateRTFOutputModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/TemplateRTFOutputModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/TemplateRTFOutputModel.java
new file mode 100644
index 0000000..f01ff07
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/TemplateRTFOutputModel.java
@@ -0,0 +1,42 @@
+/*
+ * 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.outputformat.impl;
+
+import org.apache.freemarker.core.outputformat.CommonTemplateMarkupOutputModel;
+
+/**
+ * Stores RTF markup to be printed; used with {@link RTFOutputFormat}.
+ * 
+ * @since 2.3.24
+ */
+public final class TemplateRTFOutputModel extends CommonTemplateMarkupOutputModel<TemplateRTFOutputModel> {
+    
+    /**
+     * See {@link CommonTemplateMarkupOutputModel#CommonTemplateMarkupOutputModel(String, String)}.
+     */
+    TemplateRTFOutputModel(String plainTextContent, String markupContent) {
+        super(plainTextContent, markupContent);
+    }
+
+    @Override
+    public RTFOutputFormat getOutputFormat() {
+        return RTFOutputFormat.INSTANCE;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/TemplateXHTMLOutputModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/TemplateXHTMLOutputModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/TemplateXHTMLOutputModel.java
new file mode 100644
index 0000000..f0fbf1d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/TemplateXHTMLOutputModel.java
@@ -0,0 +1,42 @@
+/*
+ * 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.outputformat.impl;
+
+import org.apache.freemarker.core.outputformat.CommonTemplateMarkupOutputModel;
+
+/**
+ * Stores HTML markup to be printed; used with {@link HTMLOutputFormat}.
+ * 
+ * @since 2.3.24
+ */
+public final class TemplateXHTMLOutputModel extends CommonTemplateMarkupOutputModel<TemplateXHTMLOutputModel> {
+    
+    /**
+     * See {@link CommonTemplateMarkupOutputModel#CommonTemplateMarkupOutputModel(String, String)}.
+     */
+    TemplateXHTMLOutputModel(String plainTextContent, String markupContent) {
+        super(plainTextContent, markupContent);
+    }
+
+    @Override
+    public XHTMLOutputFormat getOutputFormat() {
+        return XHTMLOutputFormat.INSTANCE;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/TemplateXMLOutputModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/TemplateXMLOutputModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/TemplateXMLOutputModel.java
new file mode 100644
index 0000000..62e7867
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/TemplateXMLOutputModel.java
@@ -0,0 +1,42 @@
+/*
+ * 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.outputformat.impl;
+
+import org.apache.freemarker.core.outputformat.CommonTemplateMarkupOutputModel;
+
+/**
+ * Stores XML markup to be printed; used with {@link XMLOutputFormat}.
+ * 
+ * @since 2.3.24
+ */
+public final class TemplateXMLOutputModel extends CommonTemplateMarkupOutputModel<TemplateXMLOutputModel> {
+    
+    /**
+     * See {@link CommonTemplateMarkupOutputModel#CommonTemplateMarkupOutputModel(String, String)}.
+     */
+    TemplateXMLOutputModel(String plainTextContent, String markupContent) {
+        super(plainTextContent, markupContent);
+    }
+
+    @Override
+    public XMLOutputFormat getOutputFormat() {
+        return XMLOutputFormat.INSTANCE;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/UndefinedOutputFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/UndefinedOutputFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/UndefinedOutputFormat.java
new file mode 100644
index 0000000..b5412e2
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/UndefinedOutputFormat.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.outputformat.impl;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.outputformat.OutputFormat;
+
+/**
+ * Represents the output format used when the template output format is undecided. This is the default output format if
+ * FreeMarker can't select anything more specific (see {@link Configuration#getTemplateConfigurations()}). This format
+ * doesn't support auto-escaping ({@link Configuration#getAutoEscapingPolicy()}). It will print
+ * {@link TemplateMarkupOutputModel}-s as is (doesn't try to convert them).
+ * 
+ * @see PlainTextOutputFormat
+ * 
+ * @since 2.3.24
+ */
+public final class UndefinedOutputFormat extends OutputFormat {
+
+    public static final UndefinedOutputFormat INSTANCE = new UndefinedOutputFormat();
+    
+    private UndefinedOutputFormat() {
+        // Only to decrease visibility
+    }
+
+    @Override
+    public boolean isOutputFormatMixingAllowed() {
+        return true;
+    }
+
+    @Override
+    public String getName() {
+        return "undefined";
+    }
+
+    @Override
+    public String getMimeType() {
+        return null;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/XHTMLOutputFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/XHTMLOutputFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/XHTMLOutputFormat.java
new file mode 100644
index 0000000..4334ba3
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/outputformat/impl/XHTMLOutputFormat.java
@@ -0,0 +1,77 @@
+/*
+ * 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.outputformat.impl;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.outputformat.CommonMarkupOutputFormat;
+import org.apache.freemarker.core.outputformat.OutputFormat;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * Represents the XML output format (MIME type "application/xhtml+xml", name "XHTML"). This format escapes by default
+ * (via {@link _StringUtil#XHTMLEnc(String)}). The {@code ?xml} built-in silently bypasses template output values of the
+ * type produced by this output format ({@link TemplateXHTMLOutputModel}).
+ * 
+ * @since 2.3.24
+ */
+public final class XHTMLOutputFormat extends CommonMarkupOutputFormat<TemplateXHTMLOutputModel> {
+
+    /**
+     * The only instance (singleton) of this {@link OutputFormat}.
+     */
+    public static final XHTMLOutputFormat INSTANCE = new XHTMLOutputFormat();
+    
+    private XHTMLOutputFormat() {
+        // Only to decrease visibility
+    }
+    
+    @Override
+    public String getName() {
+        return "XHTML";
+    }
+
+    @Override
+    public String getMimeType() {
+        return "application/xhtml+xml";
+    }
+
+    @Override
+    public void output(String textToEsc, Writer out) throws IOException, TemplateModelException {
+        _StringUtil.XHTMLEnc(textToEsc, out);
+    }
+
+    @Override
+    public String escapePlainText(String plainTextContent) {
+        return _StringUtil.XHTMLEnc(plainTextContent);
+    }
+
+    @Override
+    public boolean isLegacyBuiltInBypassed(String builtInName) {
+        return builtInName.equals("html") || builtInName.equals("xml") || builtInName.equals("xhtml");
+    }
+
+    @Override
+    protected TemplateXHTMLOutputModel newTemplateMarkupOutputModel(String plainTextContent, String markupContent) {
+        return new TemplateXHTMLOutputModel(plainTextContent, markupContent);
+    }
+
+}



[24/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java
new file mode 100644
index 0000000..2159f31
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java
@@ -0,0 +1,1263 @@
+/*
+ * 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.BeanInfo;
+import java.beans.IndexedPropertyDescriptor;
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.MethodDescriptor;
+import java.beans.PropertyDescriptor;
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.freemarker.core.Version;
+import org.apache.freemarker.core._CoreLogs;
+import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.util.CommonBuilder;
+import org.apache.freemarker.core.util._JavaVersions;
+import org.apache.freemarker.core.util._NullArgumentException;
+import org.slf4j.Logger;
+
+/**
+ * Returns information about a {@link Class} that's useful for FreeMarker. Encapsulates a cache for this. Thread-safe,
+ * doesn't even require "proper publishing" starting from 2.3.24 or Java 5. Immutable, with the exception of the
+ * internal caches.
+ * 
+ * <p>
+ * Note that instances of this are cached on the level of FreeMarker's defining class loader. Hence, it must not do
+ * operations that depend on the Thread Context Class Loader, such as resolving class names.
+ */
+class ClassIntrospector {
+
+    // Attention: This class must be thread-safe (not just after proper publishing). This is important as some of
+    // these are shared by many object wrappers, and concurrency related glitches due to user errors must remain
+    // local to the object wrappers, not corrupting the shared ClassIntrospector.
+
+    private static final Logger LOG = _CoreLogs.OBJECT_WRAPPER;
+
+    private static final String JREBEL_SDK_CLASS_NAME = "org.zeroturnaround.javarebel.ClassEventListener";
+    private static final String JREBEL_INTEGRATION_ERROR_MSG
+            = "Error initializing JRebel integration. JRebel integration disabled.";
+
+    private static final ClassChangeNotifier CLASS_CHANGE_NOTIFIER;
+    static {
+        boolean jRebelAvailable;
+        try {
+            Class.forName(JREBEL_SDK_CLASS_NAME);
+            jRebelAvailable = true;
+        } catch (Throwable e) {
+            jRebelAvailable = false;
+            try {
+                if (!(e instanceof ClassNotFoundException)) {
+                    LOG.error(JREBEL_INTEGRATION_ERROR_MSG, e);
+                }
+            } catch (Throwable loggingE) {
+                // ignore
+            }
+        }
+
+        ClassChangeNotifier classChangeNotifier;
+        if (jRebelAvailable) {
+            try {
+                classChangeNotifier = (ClassChangeNotifier)
+                        Class.forName("org.apache.freemarker.core.model.impl.JRebelClassChangeNotifier").newInstance();
+            } catch (Throwable e) {
+                classChangeNotifier = null;
+                try {
+                    LOG.error(JREBEL_INTEGRATION_ERROR_MSG, e);
+                } catch (Throwable loggingE) {
+                    // ignore
+                }
+            }
+        } else {
+            classChangeNotifier = null;
+        }
+
+        CLASS_CHANGE_NOTIFIER = classChangeNotifier;
+    }
+
+    // -----------------------------------------------------------------------------------------------------------------
+    // Introspection info Map keys:
+
+    /** Key in the class info Map to the Map that maps method to argument type arrays */
+    private static final Object ARG_TYPES_BY_METHOD_KEY = new Object();
+    /** Key in the class info Map to the object that represents the constructors (one or multiple due to overloading) */
+    static final Object CONSTRUCTORS_KEY = new Object();
+    /** Key in the class info Map to the get(String|Object) Method */
+    static final Object GENERIC_GET_KEY = new Object();
+
+    // -----------------------------------------------------------------------------------------------------------------
+    // Introspection configuration properties:
+
+    // Note: These all must be *declared* final (or else synchronization is needed everywhere where they are accessed).
+
+    final int exposureLevel;
+    final boolean exposeFields;
+    final MethodAppearanceFineTuner methodAppearanceFineTuner;
+    final MethodSorter methodSorter;
+
+    /** See {@link #getHasSharedInstanceRestrictons()} */
+    final private boolean hasSharedInstanceRestrictons;
+
+    /** See {@link #isShared()} */
+    final private boolean shared;
+
+    // -----------------------------------------------------------------------------------------------------------------
+    // State fields:
+
+    private final Object sharedLock;
+    private final Map<Class<?>, Map<Object, Object>> cache
+            = new ConcurrentHashMap<>(0, 0.75f, 16);
+    private final Set<String> cacheClassNames = new HashSet<>(0);
+    private final Set<Class<?>> classIntrospectionsInProgress = new HashSet<>(0);
+
+    private final List<WeakReference<Object/*ClassBasedModelFactory|ModelCache>*/>> modelFactories
+            = new LinkedList<>();
+    private final ReferenceQueue<Object> modelFactoriesRefQueue = new ReferenceQueue<>();
+
+    private int clearingCounter;
+
+    // -----------------------------------------------------------------------------------------------------------------
+    // Instantiation:
+
+    /**
+     * Creates a new instance, that is hence surely not shared (singleton) instance.
+     * 
+     * @param pa
+     *            Stores what the values of the JavaBean properties of the returned instance will be. Not {@code null}.
+     */
+    ClassIntrospector(Builder pa, Object sharedLock) {
+        this(pa, sharedLock, false, false);
+    }
+
+    /**
+     * @param hasSharedInstanceRestrictons
+     *            {@code true} exactly if we are creating a new instance with {@link Builder}. Then
+     *            it's {@code true} even if it won't put the instance into the cache.
+     */
+    ClassIntrospector(Builder builder, Object sharedLock,
+                      boolean hasSharedInstanceRestrictons, boolean shared) {
+        _NullArgumentException.check("sharedLock", sharedLock);
+
+        exposureLevel = builder.getExposureLevel();
+        exposeFields = builder.getExposeFields();
+        methodAppearanceFineTuner = builder.getMethodAppearanceFineTuner();
+        methodSorter = builder.getMethodSorter();
+
+        this.sharedLock = sharedLock;
+
+        this.hasSharedInstanceRestrictons = hasSharedInstanceRestrictons;
+        this.shared = shared;
+
+        if (CLASS_CHANGE_NOTIFIER != null) {
+            CLASS_CHANGE_NOTIFIER.subscribe(this);
+        }
+    }
+
+    /**
+     * Returns a {@link Builder}-s that could be used to invoke an identical {@link #ClassIntrospector}
+     * . The returned {@link Builder} can be modified without interfering with anything.
+     */
+    Builder createBuilder() {
+        return new Builder(this);
+    }
+
+    // ------------------------------------------------------------------------------------------------------------------
+    // Introspection:
+
+    /**
+     * Gets the class introspection data from {@link #cache}, automatically creating the cache entry if it's missing.
+     * 
+     * @return A {@link Map} where each key is a property/method/field name (or a special {@link Object} key like
+     *         {@link #CONSTRUCTORS_KEY}), each value is a {@link PropertyDescriptor} or {@link Method} or
+     *         {@link OverloadedMethods} or {@link Field} (but better check the source code...).
+     */
+    Map<Object, Object> get(Class<?> clazz) {
+        {
+            Map<Object, Object> introspData = cache.get(clazz);
+            if (introspData != null) return introspData;
+        }
+
+        String className;
+        synchronized (sharedLock) {
+            Map<Object, Object> introspData = cache.get(clazz);
+            if (introspData != null) return introspData;
+
+            className = clazz.getName();
+            if (cacheClassNames.contains(className)) {
+                onSameNameClassesDetected(className);
+            }
+
+            while (introspData == null && classIntrospectionsInProgress.contains(clazz)) {
+                // Another thread is already introspecting this class;
+                // waiting for its result.
+                try {
+                    sharedLock.wait();
+                    introspData = cache.get(clazz);
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(
+                            "Class inrospection data lookup aborded: " + e);
+                }
+            }
+            if (introspData != null) return introspData;
+
+            // This will be the thread that introspects this class.
+            classIntrospectionsInProgress.add(clazz);
+        }
+        try {
+            Map<Object, Object> introspData = createClassIntrospectionData(clazz);
+            synchronized (sharedLock) {
+                cache.put(clazz, introspData);
+                cacheClassNames.add(className);
+            }
+            return introspData;
+        } finally {
+            synchronized (sharedLock) {
+                classIntrospectionsInProgress.remove(clazz);
+                sharedLock.notifyAll();
+            }
+        }
+    }
+
+    /**
+     * Creates a {@link Map} with the content as described for the return value of {@link #get(Class)}.
+     */
+    private Map<Object, Object> createClassIntrospectionData(Class<?> clazz) {
+        final Map<Object, Object> introspData = new HashMap<>();
+
+        if (exposeFields) {
+            addFieldsToClassIntrospectionData(introspData, clazz);
+        }
+
+        final Map<MethodSignature, List<Method>> accessibleMethods = discoverAccessibleMethods(clazz);
+
+        addGenericGetToClassIntrospectionData(introspData, accessibleMethods);
+
+        if (exposureLevel != DefaultObjectWrapper.EXPOSE_NOTHING) {
+            try {
+                addBeanInfoToClassIntrospectionData(introspData, clazz, accessibleMethods);
+            } catch (IntrospectionException e) {
+                LOG.warn("Couldn't properly perform introspection for class {}", clazz.getName(), e);
+                introspData.clear(); // FIXME NBC: Don't drop everything here.
+            }
+        }
+
+        addConstructorsToClassIntrospectionData(introspData, clazz);
+
+        if (introspData.size() > 1) {
+            return introspData;
+        } else if (introspData.size() == 0) {
+            return Collections.emptyMap();
+        } else { // map.size() == 1
+            Entry<Object, Object> e = introspData.entrySet().iterator().next();
+            return Collections.singletonMap(e.getKey(), e.getValue());
+        }
+    }
+
+    private void addFieldsToClassIntrospectionData(Map<Object, Object> introspData, Class<?> clazz)
+            throws SecurityException {
+        for (Field field : clazz.getFields()) {
+            if ((field.getModifiers() & Modifier.STATIC) == 0) {
+                introspData.put(field.getName(), field);
+            }
+        }
+    }
+
+    private void addBeanInfoToClassIntrospectionData(
+            Map<Object, Object> introspData, Class<?> clazz, Map<MethodSignature, List<Method>> accessibleMethods)
+            throws IntrospectionException {
+        BeanInfo beanInfo = Introspector.getBeanInfo(clazz);
+        List<PropertyDescriptor> pdas = getPropertyDescriptors(beanInfo, clazz);
+        int pdasLength = pdas.size();
+        // Reverse order shouldn't mater, but we keep it to not risk backward incompatibility.
+        for (int i = pdasLength - 1; i >= 0; --i) {
+            addPropertyDescriptorToClassIntrospectionData(
+                    introspData, pdas.get(i), clazz,
+                    accessibleMethods);
+        }
+
+        if (exposureLevel < DefaultObjectWrapper.EXPOSE_PROPERTIES_ONLY) {
+            final MethodAppearanceFineTuner.Decision decision = new MethodAppearanceFineTuner.Decision();
+            MethodAppearanceFineTuner.DecisionInput decisionInput = null;
+            List<MethodDescriptor> mds = getMethodDescriptors(beanInfo, clazz);
+            sortMethodDescriptors(mds);
+            int mdsSize = mds.size();
+            IdentityHashMap<Method, Void> argTypesUsedByIndexerPropReaders = null;
+            for (int i = mdsSize - 1; i >= 0; --i) {
+                final MethodDescriptor md = mds.get(i);
+                final Method method = getMatchingAccessibleMethod(md.getMethod(), accessibleMethods);
+                if (method != null && isAllowedToExpose(method)) {
+                    decision.setDefaults(method);
+                    if (methodAppearanceFineTuner != null) {
+                        if (decisionInput == null) {
+                            decisionInput = new MethodAppearanceFineTuner.DecisionInput();
+                        }
+                        decisionInput.setContainingClass(clazz);
+                        decisionInput.setMethod(method);
+
+                        methodAppearanceFineTuner.process(decisionInput, decision);
+                    }
+
+                    PropertyDescriptor propDesc = decision.getExposeAsProperty();
+                    if (propDesc != null && !(introspData.get(propDesc.getName()) instanceof PropertyDescriptor)) {
+                        addPropertyDescriptorToClassIntrospectionData(
+                                introspData, propDesc, clazz, accessibleMethods);
+                    }
+
+                    String methodKey = decision.getExposeMethodAs();
+                    if (methodKey != null) {
+                        Object previous = introspData.get(methodKey);
+                        if (previous instanceof Method) {
+                            // Overloaded method - replace Method with a OverloadedMethods
+                            OverloadedMethods overloadedMethods = new OverloadedMethods();
+                            overloadedMethods.addMethod((Method) previous);
+                            overloadedMethods.addMethod(method);
+                            introspData.put(methodKey, overloadedMethods);
+                            // Remove parameter type information (unless an indexed property reader needs it):
+                            if (argTypesUsedByIndexerPropReaders == null
+                                    || !argTypesUsedByIndexerPropReaders.containsKey(previous)) {
+                                getArgTypesByMethod(introspData).remove(previous);
+                            }
+                        } else if (previous instanceof OverloadedMethods) {
+                            // Already overloaded method - add new overload
+                            ((OverloadedMethods) previous).addMethod(method);
+                        } else if (decision.getMethodShadowsProperty()
+                                || !(previous instanceof PropertyDescriptor)) {
+                            // Simple method (this far)
+                            introspData.put(methodKey, method);
+                            Class<?>[] replaced = getArgTypesByMethod(introspData).put(method,
+                                    method.getParameterTypes());
+                            if (replaced != null) {
+                                if (argTypesUsedByIndexerPropReaders == null) {
+                                    argTypesUsedByIndexerPropReaders = new IdentityHashMap<Method, Void>();
+                                }
+                                argTypesUsedByIndexerPropReaders.put(method, null);
+                            }
+                        }
+                    }
+                }
+            } // for each in mds
+        } // end if (exposureLevel < EXPOSE_PROPERTIES_ONLY)
+    }
+
+    /**
+     * Very similar to {@link BeanInfo#getPropertyDescriptors()}, but can deal with Java 8 default methods too.
+     */
+    private List<PropertyDescriptor> getPropertyDescriptors(BeanInfo beanInfo, Class<?> clazz) {
+        PropertyDescriptor[] introspectorPDsArray = beanInfo.getPropertyDescriptors();
+        List<PropertyDescriptor> introspectorPDs = introspectorPDsArray != null ? Arrays.asList(introspectorPDsArray)
+                : Collections.<PropertyDescriptor>emptyList();
+
+        if (_JavaVersions.JAVA_8 == null) {
+            // java.beans.Introspector was good enough then.
+            return introspectorPDs;
+        }
+
+        // introspectorPDs contains each property exactly once. But as now we will search them manually too, it can
+        // happen that we find the same property for multiple times. Worse, because of indexed properties, it's possible
+        // that we have to merge entries (like one has the normal reader method, the other has the indexed reader
+        // method), instead of just replacing them in a Map. That's why we have introduced PropertyReaderMethodPair,
+        // which holds the methods belonging to the same property name. IndexedPropertyDescriptor is not good for that,
+        // as it can't store two methods whose types are incompatible, and we have to wait until all the merging was
+        // done to see if the incompatibility goes away.
+
+        // This could be Map<String, PropertyReaderMethodPair>, but since we rarely need to do merging, we try to avoid
+        // creating those and use the source objects as much as possible. Also note that we initialize this lazily.
+        LinkedHashMap<String, Object /*PropertyReaderMethodPair|Method|PropertyDescriptor*/> mergedPRMPs = null;
+
+        // Collect Java 8 default methods that look like property readers into mergedPRMPs:
+        // (Note that java.beans.Introspector discovers non-accessible public methods, and to emulate that behavior
+        // here, we don't utilize the accessibleMethods Map, which we might already have at this point.)
+        for (Method method : clazz.getMethods()) {
+            if (_JavaVersions.JAVA_8.isDefaultMethod(method) && method.getReturnType() != void.class
+                    && !method.isBridge()) {
+                Class<?>[] paramTypes = method.getParameterTypes();
+                if (paramTypes.length == 0
+                        || paramTypes.length == 1 && paramTypes[0] == int.class /* indexed property reader */) {
+                    String propName = _MethodUtil.getBeanPropertyNameFromReaderMethodName(
+                            method.getName(), method.getReturnType());
+                    if (propName != null) {
+                        if (mergedPRMPs == null) {
+                            // Lazy initialization
+                            mergedPRMPs = new LinkedHashMap<String, Object>();
+                        }
+                        if (paramTypes.length == 0) {
+                            mergeInPropertyReaderMethod(mergedPRMPs, propName, method);
+                        } else { // It's an indexed property reader method
+                            mergeInPropertyReaderMethodPair(mergedPRMPs, propName,
+                                    new PropertyReaderedMethodPair(null, method));
+                        }
+                    }
+                }
+            }
+        } // for clazz.getMethods()
+
+        if (mergedPRMPs == null) {
+            // We had no interfering Java 8 default methods, so we can chose the fast route.
+            return introspectorPDs;
+        }
+
+        for (PropertyDescriptor introspectorPD : introspectorPDs) {
+            mergeInPropertyDescriptor(mergedPRMPs, introspectorPD);
+        }
+
+        // Now we convert the PRMPs to PDs, handling case where the normal and the indexed read methods contradict.
+        List<PropertyDescriptor> mergedPDs = new ArrayList<PropertyDescriptor>(mergedPRMPs.size());
+        for (Entry<String, Object> entry : mergedPRMPs.entrySet()) {
+            String propName = entry.getKey();
+            Object propDescObj = entry.getValue();
+            if (propDescObj instanceof PropertyDescriptor) {
+                mergedPDs.add((PropertyDescriptor) propDescObj);
+            } else {
+                Method readMethod;
+                Method indexedReadMethod;
+                if (propDescObj instanceof Method) {
+                    readMethod = (Method) propDescObj;
+                    indexedReadMethod = null;
+                } else if (propDescObj instanceof PropertyReaderedMethodPair) {
+                    PropertyReaderedMethodPair prmp = (PropertyReaderedMethodPair) propDescObj;
+                    readMethod = prmp.readMethod;
+                    indexedReadMethod = prmp.indexedReadMethod;
+                    if (readMethod != null && indexedReadMethod != null
+                            && indexedReadMethod.getReturnType() != readMethod.getReturnType().getComponentType()) {
+                        // Here we copy the java.beans.Introspector behavior: If the array item class is not exactly the
+                        // the same as the indexed read method return type, we say that the property is not indexed.
+                        indexedReadMethod = null;
+                    }
+                } else {
+                    throw new BugException();
+                }
+                try {
+                    mergedPDs.add(
+                            indexedReadMethod != null
+                                    ? new IndexedPropertyDescriptor(propName,
+                                    readMethod, null, indexedReadMethod, null)
+                                    : new PropertyDescriptor(propName, readMethod, null));
+                } catch (IntrospectionException e) {
+                    if (LOG.isWarnEnabled()) {
+                        LOG.warn("Failed creating property descriptor for " + clazz.getName() + " property " + propName,
+                                e);
+                    }
+                }
+            }
+        }
+        return mergedPDs;
+    }
+
+    private static class PropertyReaderedMethodPair {
+        private final Method readMethod;
+        private final Method indexedReadMethod;
+
+        PropertyReaderedMethodPair(Method readerMethod, Method indexedReaderMethod) {
+            this.readMethod = readerMethod;
+            this.indexedReadMethod = indexedReaderMethod;
+        }
+
+        PropertyReaderedMethodPair(PropertyDescriptor pd) {
+            this(
+                    pd.getReadMethod(),
+                    pd instanceof IndexedPropertyDescriptor
+                            ? ((IndexedPropertyDescriptor) pd).getIndexedReadMethod() : null);
+        }
+
+        static PropertyReaderedMethodPair from(Object obj) {
+            if (obj instanceof PropertyReaderedMethodPair) {
+                return (PropertyReaderedMethodPair) obj;
+            } else if (obj instanceof PropertyDescriptor) {
+                return new PropertyReaderedMethodPair((PropertyDescriptor) obj);
+            } else if (obj instanceof Method) {
+                return new PropertyReaderedMethodPair((Method) obj, null);
+            } else {
+                throw new BugException("Unexpected obj type: " + obj.getClass().getName());
+            }
+        }
+
+        static PropertyReaderedMethodPair merge(PropertyReaderedMethodPair oldMethods, PropertyReaderedMethodPair newMethods) {
+            return new PropertyReaderedMethodPair(
+                    newMethods.readMethod != null ? newMethods.readMethod : oldMethods.readMethod,
+                    newMethods.indexedReadMethod != null ? newMethods.indexedReadMethod
+                            : oldMethods.indexedReadMethod);
+        }
+
+        @Override
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + ((indexedReadMethod == null) ? 0 : indexedReadMethod.hashCode());
+            result = prime * result + ((readMethod == null) ? 0 : readMethod.hashCode());
+            return result;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) return true;
+            if (obj == null) return false;
+            if (getClass() != obj.getClass()) return false;
+            PropertyReaderedMethodPair other = (PropertyReaderedMethodPair) obj;
+            return other.readMethod == readMethod && other.indexedReadMethod == indexedReadMethod;
+        }
+
+    }
+
+    private void mergeInPropertyDescriptor(LinkedHashMap<String, Object> mergedPRMPs, PropertyDescriptor pd) {
+        String propName = pd.getName();
+        Object replaced = mergedPRMPs.put(propName, pd);
+        if (replaced != null) {
+            PropertyReaderedMethodPair newPRMP = new PropertyReaderedMethodPair(pd);
+            putIfMergedPropertyReaderMethodPairDiffers(mergedPRMPs, propName, replaced, newPRMP);
+        }
+    }
+
+    private void mergeInPropertyReaderMethodPair(LinkedHashMap<String, Object> mergedPRMPs,
+                                                 String propName, PropertyReaderedMethodPair newPRM) {
+        Object replaced = mergedPRMPs.put(propName, newPRM);
+        if (replaced != null) {
+            putIfMergedPropertyReaderMethodPairDiffers(mergedPRMPs, propName, replaced, newPRM);
+        }
+    }
+
+    private void mergeInPropertyReaderMethod(LinkedHashMap<String, Object> mergedPRMPs,
+                                             String propName, Method readerMethod) {
+        Object replaced = mergedPRMPs.put(propName, readerMethod);
+        if (replaced != null) {
+            putIfMergedPropertyReaderMethodPairDiffers(mergedPRMPs, propName,
+                    replaced, new PropertyReaderedMethodPair(readerMethod, null));
+        }
+    }
+
+    private void putIfMergedPropertyReaderMethodPairDiffers(LinkedHashMap<String, Object> mergedPRMPs,
+                                                            String propName, Object replaced, PropertyReaderedMethodPair newPRMP) {
+        PropertyReaderedMethodPair replacedPRMP = PropertyReaderedMethodPair.from(replaced);
+        PropertyReaderedMethodPair mergedPRMP = PropertyReaderedMethodPair.merge(replacedPRMP, newPRMP);
+        if (!mergedPRMP.equals(newPRMP)) {
+            mergedPRMPs.put(propName, mergedPRMP);
+        }
+    }
+
+    /**
+     * Very similar to {@link BeanInfo#getMethodDescriptors()}, but can deal with Java 8 default methods too.
+     */
+    private List<MethodDescriptor> getMethodDescriptors(BeanInfo beanInfo, Class<?> clazz) {
+        MethodDescriptor[] introspectorMDArray = beanInfo.getMethodDescriptors();
+        List<MethodDescriptor> introspectionMDs = introspectorMDArray != null && introspectorMDArray.length != 0
+                ? Arrays.asList(introspectorMDArray) : Collections.<MethodDescriptor>emptyList();
+
+        if (_JavaVersions.JAVA_8 == null) {
+            // java.beans.Introspector was good enough then.
+            return introspectionMDs;
+        }
+
+        Map<String, List<Method>> defaultMethodsToAddByName = null;
+        for (Method method : clazz.getMethods()) {
+            if (_JavaVersions.JAVA_8.isDefaultMethod(method) && !method.isBridge()) {
+                if (defaultMethodsToAddByName == null) {
+                    defaultMethodsToAddByName = new HashMap<String, List<Method>>();
+                }
+                List<Method> overloads = defaultMethodsToAddByName.get(method.getName());
+                if (overloads == null) {
+                    overloads = new ArrayList<Method>(0);
+                    defaultMethodsToAddByName.put(method.getName(), overloads);
+                }
+                overloads.add(method);
+            }
+        }
+
+        if (defaultMethodsToAddByName == null) {
+            // We had no interfering default methods:
+            return introspectionMDs;
+        }
+
+        // Recreate introspectionMDs so that its size can grow:
+        ArrayList<MethodDescriptor> newIntrospectionMDs
+                = new ArrayList<MethodDescriptor>(introspectionMDs.size() + 16);
+        for (MethodDescriptor introspectorMD : introspectionMDs) {
+            Method introspectorM = introspectorMD.getMethod();
+            // Prevent cases where the same method is added with different return types both from the list of default
+            // methods and from the list of Introspector-discovered methods, as that would lead to overloaded method
+            // selection ambiguity later. This is known to happen when the default method in an interface has reified
+            // return type, and then the interface is implemented by a class where the compiler generates an override
+            // for the bridge method only. (Other tricky cases might exist.)
+            if (!containsMethodWithSameParameterTypes(
+                    defaultMethodsToAddByName.get(introspectorM.getName()), introspectorM)) {
+                newIntrospectionMDs.add(introspectorMD);
+            }
+        }
+        introspectionMDs = newIntrospectionMDs;
+
+        // Add default methods:
+        for (Entry<String, List<Method>> entry : defaultMethodsToAddByName.entrySet()) {
+            for (Method method : entry.getValue()) {
+                introspectionMDs.add(new MethodDescriptor(method));
+            }
+        }
+
+        return introspectionMDs;
+    }
+
+    private boolean containsMethodWithSameParameterTypes(List<Method> overloads, Method m) {
+        if (overloads == null) {
+            return false;
+        }
+
+        Class<?>[] paramTypes = m.getParameterTypes();
+        for (Method overload : overloads) {
+            if (Arrays.equals(overload.getParameterTypes(), paramTypes)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void addPropertyDescriptorToClassIntrospectionData(Map<Object, Object> introspData,
+            PropertyDescriptor pd, Class<?> clazz, Map<MethodSignature, List<Method>> accessibleMethods) {
+        if (pd instanceof IndexedPropertyDescriptor) {
+            IndexedPropertyDescriptor ipd =
+                    (IndexedPropertyDescriptor) pd;
+            Method readMethod = ipd.getIndexedReadMethod();
+            Method publicReadMethod = getMatchingAccessibleMethod(readMethod, accessibleMethods);
+            if (publicReadMethod != null && isAllowedToExpose(publicReadMethod)) {
+                try {
+                    if (readMethod != publicReadMethod) {
+                        ipd = new IndexedPropertyDescriptor(
+                                ipd.getName(), ipd.getReadMethod(),
+                                null, publicReadMethod,
+                                null);
+                    }
+                    introspData.put(ipd.getName(), ipd);
+                    getArgTypesByMethod(introspData).put(publicReadMethod, publicReadMethod.getParameterTypes());
+                } catch (IntrospectionException e) {
+                    LOG.warn("Failed creating a publicly-accessible property descriptor "
+                            + "for {} indexed property {}, read method {}",
+                            clazz.getName(), pd.getName(), publicReadMethod,
+                            e);
+                }
+            }
+        } else {
+            Method readMethod = pd.getReadMethod();
+            Method publicReadMethod = getMatchingAccessibleMethod(readMethod, accessibleMethods);
+            if (publicReadMethod != null && isAllowedToExpose(publicReadMethod)) {
+                try {
+                    if (readMethod != publicReadMethod) {
+                        pd = new PropertyDescriptor(pd.getName(), publicReadMethod, null);
+                    }
+                    introspData.put(pd.getName(), pd);
+                } catch (IntrospectionException e) {
+                    LOG.warn("Failed creating a publicly-accessible property descriptor "
+                            + "for {} property {}, read method {}",
+                            clazz.getName(), pd.getName(), publicReadMethod,
+                            e);
+                }
+            }
+        }
+    }
+
+    private void addGenericGetToClassIntrospectionData(Map<Object, Object> introspData,
+            Map<MethodSignature, List<Method>> accessibleMethods) {
+        Method genericGet = getFirstAccessibleMethod(
+                MethodSignature.GET_STRING_SIGNATURE, accessibleMethods);
+        if (genericGet == null) {
+            genericGet = getFirstAccessibleMethod(
+                    MethodSignature.GET_OBJECT_SIGNATURE, accessibleMethods);
+        }
+        if (genericGet != null) {
+            introspData.put(GENERIC_GET_KEY, genericGet);
+        }
+    }
+
+    private void addConstructorsToClassIntrospectionData(final Map<Object, Object> introspData,
+            Class<?> clazz) {
+        try {
+            Constructor<?>[] ctors = clazz.getConstructors();
+            if (ctors.length == 1) {
+                Constructor<?> ctor = ctors[0];
+                introspData.put(CONSTRUCTORS_KEY, new SimpleMethod(ctor, ctor.getParameterTypes()));
+            } else if (ctors.length > 1) {
+                OverloadedMethods overloadedCtors = new OverloadedMethods();
+                for (Constructor<?> ctor : ctors) {
+                    overloadedCtors.addConstructor(ctor);
+                }
+                introspData.put(CONSTRUCTORS_KEY, overloadedCtors);
+            }
+        } catch (SecurityException e) {
+            LOG.warn("Can't discover constructors for class {}", clazz.getName(), e);
+        }
+    }
+
+    /**
+     * Retrieves mapping of {@link MethodSignature}-s to a {@link List} of accessible methods for a class. In case the
+     * class is not public, retrieves methods with same signature as its public methods from public superclasses and
+     * interfaces. Basically upcasts every method to the nearest accessible method.
+     */
+    private static Map<MethodSignature, List<Method>> discoverAccessibleMethods(Class<?> clazz) {
+        Map<MethodSignature, List<Method>> accessibles = new HashMap<>();
+        discoverAccessibleMethods(clazz, accessibles);
+        return accessibles;
+    }
+
+    private static void discoverAccessibleMethods(Class<?> clazz, Map<MethodSignature, List<Method>> accessibles) {
+        if (Modifier.isPublic(clazz.getModifiers())) {
+            try {
+                Method[] methods = clazz.getMethods();
+                for (Method method : methods) {
+                    MethodSignature sig = new MethodSignature(method);
+                    // Contrary to intuition, a class can actually have several
+                    // different methods with same signature *but* different
+                    // return types. These can't be constructed using Java the
+                    // language, as this is illegal on source code level, but
+                    // the compiler can emit synthetic methods as part of
+                    // generic type reification that will have same signature
+                    // yet different return type than an existing explicitly
+                    // declared method. Consider:
+                    // public interface I<T> { T m(); }
+                    // public class C implements I<Integer> { Integer m() { return 42; } }
+                    // C.class will have both "Object m()" and "Integer m()" methods.
+                    List<Method> methodList = accessibles.get(sig);
+                    if (methodList == null) {
+                        // TODO Collection.singletonList is more efficient, though read only.
+                        methodList = new LinkedList<>();
+                        accessibles.put(sig, methodList);
+                    }
+                    methodList.add(method);
+                }
+                return;
+            } catch (SecurityException e) {
+                LOG.warn("Could not discover accessible methods of class {}, attemping superclasses/interfaces.",
+                        clazz.getName(), e);
+                // Fall through and attempt to discover superclass/interface methods
+            }
+        }
+
+        Class<?>[] interfaces = clazz.getInterfaces();
+        for (Class<?> anInterface : interfaces) {
+            discoverAccessibleMethods(anInterface, accessibles);
+        }
+        Class<?> superclass = clazz.getSuperclass();
+        if (superclass != null) {
+            discoverAccessibleMethods(superclass, accessibles);
+        }
+    }
+
+    private static Method getMatchingAccessibleMethod(Method m, Map<MethodSignature, List<Method>> accessibles) {
+        if (m == null) {
+            return null;
+        }
+        MethodSignature sig = new MethodSignature(m);
+        List<Method> ams = accessibles.get(sig);
+        if (ams == null) {
+            return null;
+        }
+        for (Method am : ams) {
+            if (am.getReturnType() == m.getReturnType()) {
+                return am;
+            }
+        }
+        return null;
+    }
+
+    private static Method getFirstAccessibleMethod(MethodSignature sig, Map<MethodSignature, List<Method>> accessibles) {
+        List<Method> ams = accessibles.get(sig);
+        if (ams == null || ams.isEmpty()) {
+            return null;
+        }
+        return ams.get(0);
+    }
+
+    /**
+     * As of this writing, this is only used for testing if method order really doesn't mater.
+     */
+    private void sortMethodDescriptors(List<MethodDescriptor> methodDescriptors) {
+        if (methodSorter != null) {
+            methodSorter.sortMethodDescriptors(methodDescriptors);
+        }
+    }
+
+    boolean isAllowedToExpose(Method method) {
+        return exposureLevel < DefaultObjectWrapper.EXPOSE_SAFE || !UnsafeMethods.isUnsafeMethod(method);
+    }
+
+    private static Map<Method, Class<?>[]> getArgTypesByMethod(Map<Object, Object> classInfo) {
+        @SuppressWarnings("unchecked")
+        Map<Method, Class<?>[]> argTypes = (Map<Method, Class<?>[]>) classInfo.get(ARG_TYPES_BY_METHOD_KEY);
+        if (argTypes == null) {
+            argTypes = new HashMap<>();
+            classInfo.put(ARG_TYPES_BY_METHOD_KEY, argTypes);
+        }
+        return argTypes;
+    }
+
+    private static final class MethodSignature {
+        private static final MethodSignature GET_STRING_SIGNATURE =
+                new MethodSignature("get", new Class[] { String.class });
+        private static final MethodSignature GET_OBJECT_SIGNATURE =
+                new MethodSignature("get", new Class[] { Object.class });
+
+        private final String name;
+        private final Class<?>[] args;
+
+        private MethodSignature(String name, Class<?>[] args) {
+            this.name = name;
+            this.args = args;
+        }
+
+        MethodSignature(Method method) {
+            this(method.getName(), method.getParameterTypes());
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o instanceof MethodSignature) {
+                MethodSignature ms = (MethodSignature) o;
+                return ms.name.equals(name) && Arrays.equals(args, ms.args);
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return name.hashCode() ^ args.length; // TODO That's a poor quality hash... isn't this a problem?
+        }
+    }
+
+    // -----------------------------------------------------------------------------------------------------------------
+    // Cache management:
+
+    /**
+     * Corresponds to {@link DefaultObjectWrapper#clearClassIntrospecitonCache()}.
+     * 
+     * @since 2.3.20
+     */
+    void clearCache() {
+        if (getHasSharedInstanceRestrictons()) {
+            throw new IllegalStateException(
+                    "It's not allowed to clear the whole cache in a read-only " + getClass().getName() +
+                            "instance. Use removeFromClassIntrospectionCache(String prefix) instead.");
+        }
+        forcedClearCache();
+    }
+
+    private void forcedClearCache() {
+        synchronized (sharedLock) {
+            cache.clear();
+            cacheClassNames.clear();
+            clearingCounter++;
+
+            for (WeakReference<Object> regedMfREf : modelFactories) {
+                Object regedMf = regedMfREf.get();
+                if (regedMf != null) {
+                    if (regedMf instanceof ClassBasedModelFactory) {
+                        ((ClassBasedModelFactory) regedMf).clearCache();
+                    } else {
+                        throw new BugException();
+                    }
+                }
+            }
+
+            removeClearedModelFactoryReferences();
+        }
+    }
+
+    /**
+     * Corresponds to {@link DefaultObjectWrapper#removeFromClassIntrospectionCache(Class)}.
+     * 
+     * @since 2.3.20
+     */
+    void remove(Class<?> clazz) {
+        synchronized (sharedLock) {
+            cache.remove(clazz);
+            cacheClassNames.remove(clazz.getName());
+            clearingCounter++;
+
+            for (WeakReference<Object> regedMfREf : modelFactories) {
+                Object regedMf = regedMfREf.get();
+                if (regedMf != null) {
+                    if (regedMf instanceof ClassBasedModelFactory) {
+                        ((ClassBasedModelFactory) regedMf).removeFromCache(clazz);
+                    } else {
+                        throw new BugException();
+                    }
+                }
+            }
+
+            removeClearedModelFactoryReferences();
+        }
+    }
+
+    /**
+     * Returns the number of events so far that could make class introspection data returned earlier outdated.
+     */
+    int getClearingCounter() {
+        synchronized (sharedLock) {
+            return clearingCounter;
+        }
+    }
+
+    private void onSameNameClassesDetected(String className) {
+        // TODO: This behavior should be pluggable, as in environments where
+        // some classes are often reloaded or multiple versions of the
+        // same class is normal (OSGi), this will drop the cache contents
+        // too often.
+        LOG.info(
+                "Detected multiple classes with the same name, \"{}\". "
+                + "Assuming it was a class-reloading. Clearing class introspection caches to release old data.",
+                className);
+        forcedClearCache();
+    }
+
+    // -----------------------------------------------------------------------------------------------------------------
+    // Managing dependent objects:
+
+    void registerModelFactory(ClassBasedModelFactory mf) {
+        registerModelFactory((Object) mf);
+    }
+
+    private void registerModelFactory(Object mf) {
+        // Note that this `synchronized (sharedLock)` is also need for the DefaultObjectWrapper constructor to work safely.
+        synchronized (sharedLock) {
+            modelFactories.add(new WeakReference<>(mf, modelFactoriesRefQueue));
+            removeClearedModelFactoryReferences();
+        }
+    }
+
+    void unregisterModelFactory(ClassBasedModelFactory mf) {
+        unregisterModelFactory((Object) mf);
+    }
+
+    void unregisterModelFactory(Object mf) {
+        synchronized (sharedLock) {
+            for (Iterator<WeakReference<Object>> it = modelFactories.iterator(); it.hasNext(); ) {
+                Object regedMf = it.next().get();
+                if (regedMf == mf) {
+                    it.remove();
+                }
+            }
+
+        }
+    }
+
+    private void removeClearedModelFactoryReferences() {
+        Reference<?> cleardRef;
+        while ((cleardRef = modelFactoriesRefQueue.poll()) != null) {
+            synchronized (sharedLock) {
+                findClearedRef: for (Iterator<WeakReference<Object>> it = modelFactories.iterator(); it.hasNext(); ) {
+                    if (it.next() == cleardRef) {
+                        it.remove();
+                        break findClearedRef;
+                    }
+                }
+            }
+        }
+    }
+
+    // -----------------------------------------------------------------------------------------------------------------
+    // Extracting from introspection info:
+
+    static Class<?>[] getArgTypes(Map<Object, Object> classInfo, Method method) {
+        @SuppressWarnings("unchecked")
+        Map<Method, Class<?>[]> argTypesByMethod = (Map<Method, Class<?>[]>) classInfo.get(ARG_TYPES_BY_METHOD_KEY);
+        return argTypesByMethod.get(method);
+    }
+
+    /**
+     * Returns the number of introspected methods/properties that should be available via the TemplateHashModel
+     * interface.
+     */
+    int keyCount(Class<?> clazz) {
+        Map<Object, Object> map = get(clazz);
+        int count = map.size();
+        if (map.containsKey(CONSTRUCTORS_KEY)) count--;
+        if (map.containsKey(GENERIC_GET_KEY)) count--;
+        if (map.containsKey(ARG_TYPES_BY_METHOD_KEY)) count--;
+        return count;
+    }
+
+    /**
+     * Returns the Set of names of introspected methods/properties that should be available via the TemplateHashModel
+     * interface.
+     */
+    Set<Object> keySet(Class<?> clazz) {
+        Set<Object> set = new HashSet<>(get(clazz).keySet());
+        set.remove(CONSTRUCTORS_KEY);
+        set.remove(GENERIC_GET_KEY);
+        set.remove(ARG_TYPES_BY_METHOD_KEY);
+        return set;
+    }
+
+    // -----------------------------------------------------------------------------------------------------------------
+    // Properties
+
+    int getExposureLevel() {
+        return exposureLevel;
+    }
+
+    boolean getExposeFields() {
+        return exposeFields;
+    }
+
+    MethodAppearanceFineTuner getMethodAppearanceFineTuner() {
+        return methodAppearanceFineTuner;
+    }
+
+    MethodSorter getMethodSorter() {
+        return methodSorter;
+    }
+
+    /**
+     * Returns {@code true} if this instance was created with {@link Builder}, even if it wasn't
+     * actually put into the cache (as we reserve the right to do so in later versions).
+     */
+    boolean getHasSharedInstanceRestrictons() {
+        return hasSharedInstanceRestrictons;
+    }
+
+    /**
+     * Tells if this instance is (potentially) shared among {@link DefaultObjectWrapper} instances.
+     * 
+     * @see #getHasSharedInstanceRestrictons()
+     */
+    boolean isShared() {
+        return shared;
+    }
+
+    /**
+     * Almost always, you want to use {@link DefaultObjectWrapper#getSharedIntrospectionLock()}, not this! The only exception is
+     * when you get this to set the field returned by {@link DefaultObjectWrapper#getSharedIntrospectionLock()}.
+     */
+    Object getSharedLock() {
+        return sharedLock;
+    }
+
+    // -----------------------------------------------------------------------------------------------------------------
+    // Monitoring:
+
+    /** For unit testing only */
+    Object[] getRegisteredModelFactoriesSnapshot() {
+        synchronized (sharedLock) {
+            return modelFactories.toArray();
+        }
+    }
+
+    static final class Builder implements CommonBuilder<ClassIntrospector>, Cloneable {
+
+        private static final Map/*<PropertyAssignments, Reference<ClassIntrospector>>*/ INSTANCE_CACHE = new HashMap();
+        private static final ReferenceQueue INSTANCE_CACHE_REF_QUEUE = new ReferenceQueue();
+
+        // Properties and their *defaults*:
+        private int exposureLevel = DefaultObjectWrapper.EXPOSE_SAFE;
+        private boolean exposureLevelSet;
+        private boolean exposeFields;
+        private boolean exposeFieldsSet;
+        private MethodAppearanceFineTuner methodAppearanceFineTuner;
+        private boolean methodAppearanceFineTunerSet;
+        private MethodSorter methodSorter;
+        // Attention:
+        // - This is also used as a cache key, so non-normalized field values should be avoided.
+        // - If some field has a default value, it must be set until the end of the constructor. No field that has a
+        //   default can be left unset (like null).
+        // - If you add a new field, review all methods in this class, also the ClassIntrospector constructor
+
+        Builder(ClassIntrospector ci) {
+            exposureLevel = ci.exposureLevel;
+            exposeFields = ci.exposeFields;
+            methodAppearanceFineTuner = ci.methodAppearanceFineTuner;
+            methodSorter = ci.methodSorter;
+        }
+
+        Builder(Version incompatibleImprovements) {
+            // Warning: incompatibleImprovements must not affect this object at versions increments where there's no
+            // change in the DefaultObjectWrapper.normalizeIncompatibleImprovements results. That is, this class may don't react
+            // to some version changes that affects DefaultObjectWrapper, but not the other way around.
+            _NullArgumentException.check(incompatibleImprovements);
+            // Currently nothing depends on incompatibleImprovements
+        }
+
+        @Override
+        protected Object clone() {
+            try {
+                return super.clone();
+            } catch (CloneNotSupportedException e) {
+                throw new RuntimeException("Failed to deepClone Builder", e);
+            }
+        }
+
+        @Override
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + (exposeFields ? 1231 : 1237);
+            result = prime * result + exposureLevel;
+            result = prime * result + System.identityHashCode(methodAppearanceFineTuner);
+            result = prime * result + System.identityHashCode(methodSorter);
+            return result;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) return true;
+            if (obj == null) return false;
+            if (getClass() != obj.getClass()) return false;
+            Builder other = (Builder) obj;
+
+            if (exposeFields != other.exposeFields) return false;
+            if (exposureLevel != other.exposureLevel) return false;
+            if (methodAppearanceFineTuner != other.methodAppearanceFineTuner) return false;
+            return methodSorter == other.methodSorter;
+        }
+
+        public int getExposureLevel() {
+            return exposureLevel;
+        }
+
+        /** See {@link DefaultObjectWrapper.ExtendableBuilder#setExposureLevel(int)}. */
+        public void setExposureLevel(int exposureLevel) {
+            if (exposureLevel < DefaultObjectWrapper.EXPOSE_ALL || exposureLevel > DefaultObjectWrapper.EXPOSE_NOTHING) {
+                throw new IllegalArgumentException("Illegal exposure level: " + exposureLevel);
+            }
+
+            this.exposureLevel = exposureLevel;
+            exposureLevelSet = true;
+        }
+
+        /**
+         * Tells if the property was explicitly set, as opposed to just holding its default value.
+         */
+        public boolean isExposureLevelSet() {
+            return exposureLevelSet;
+        }
+
+        public boolean getExposeFields() {
+            return exposeFields;
+        }
+
+        /** See {@link DefaultObjectWrapper.ExtendableBuilder#setExposeFields(boolean)}. */
+        public void setExposeFields(boolean exposeFields) {
+            this.exposeFields = exposeFields;
+            exposeFieldsSet = true;
+        }
+
+        /**
+         * Tells if the property was explicitly set, as opposed to just holding its default value.
+         */
+        public boolean isExposeFieldsSet() {
+            return exposeFieldsSet;
+        }
+
+        public MethodAppearanceFineTuner getMethodAppearanceFineTuner() {
+            return methodAppearanceFineTuner;
+        }
+
+        public void setMethodAppearanceFineTuner(MethodAppearanceFineTuner methodAppearanceFineTuner) {
+            this.methodAppearanceFineTuner = methodAppearanceFineTuner;
+            methodAppearanceFineTunerSet = true;
+        }
+
+        /**
+         * Tells if the property was explicitly set, as opposed to just holding its default value.
+         */
+        public boolean isMethodAppearanceFineTunerSet() {
+            return methodAppearanceFineTunerSet;
+        }
+
+        public MethodSorter getMethodSorter() {
+            return methodSorter;
+        }
+
+        public void setMethodSorter(MethodSorter methodSorter) {
+            this.methodSorter = methodSorter;
+        }
+
+        private static void removeClearedReferencesFromInstanceCache() {
+            Reference clearedRef;
+            while ((clearedRef = INSTANCE_CACHE_REF_QUEUE.poll()) != null) {
+                synchronized (INSTANCE_CACHE) {
+                    findClearedRef: for (Iterator it = INSTANCE_CACHE.values().iterator(); it.hasNext(); ) {
+                        if (it.next() == clearedRef) {
+                            it.remove();
+                            break findClearedRef;
+                        }
+                    }
+                }
+            }
+        }
+
+        /** For unit testing only */
+        static void clearInstanceCache() {
+            synchronized (INSTANCE_CACHE) {
+                INSTANCE_CACHE.clear();
+            }
+        }
+
+        /** For unit testing only */
+        static Map getInstanceCache() {
+            return INSTANCE_CACHE;
+        }
+
+        /**
+         * Returns an instance that is possibly shared (singleton). Note that this comes with its own "shared lock",
+         * since everyone who uses this object will have to lock with that common object.
+         */
+        @Override
+        public ClassIntrospector build() {
+            if ((methodAppearanceFineTuner == null || methodAppearanceFineTuner instanceof SingletonCustomizer)
+                    && (methodSorter == null || methodSorter instanceof SingletonCustomizer)) {
+                // Instance can be cached.
+                ClassIntrospector instance;
+                synchronized (INSTANCE_CACHE) {
+                    Reference instanceRef = (Reference) INSTANCE_CACHE.get(this);
+                    instance = instanceRef != null ? (ClassIntrospector) instanceRef.get() : null;
+                    if (instance == null) {
+                        Builder thisClone = (Builder) clone();  // prevent any aliasing issues
+                        instance = new ClassIntrospector(thisClone, new Object(), true, true);
+                        INSTANCE_CACHE.put(thisClone, new WeakReference(instance, INSTANCE_CACHE_REF_QUEUE));
+                    }
+                }
+
+                removeClearedReferencesFromInstanceCache();
+
+                return instance;
+            } else {
+                // If methodAppearanceFineTuner or methodSorter is specified and isn't marked as a singleton, the
+                // ClassIntrospector can't be shared/cached as those objects could contain a back-reference to the
+                // DefaultObjectWrapper.
+                return new ClassIntrospector(this, new Object(), true, false);
+            }
+        }
+
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CollectionAdapter.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CollectionAdapter.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CollectionAdapter.java
new file mode 100644
index 0000000..e9860ab
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CollectionAdapter.java
@@ -0,0 +1,88 @@
+/*
+ * 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.util.AbstractCollection;
+import java.util.Collection;
+import java.util.Iterator;
+
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelAdapter;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+import org.apache.freemarker.core.util.UndeclaredThrowableException;
+
+/**
+ * Adapts a {@link TemplateCollectionModel} to  {@link Collection}.
+ */
+class CollectionAdapter extends AbstractCollection implements TemplateModelAdapter {
+    private final DefaultObjectWrapper wrapper;
+    private final TemplateCollectionModel model;
+    
+    CollectionAdapter(TemplateCollectionModel model, DefaultObjectWrapper wrapper) {
+        this.model = model;
+        this.wrapper = wrapper;
+    }
+    
+    @Override
+    public TemplateModel getTemplateModel() {
+        return model;
+    }
+    
+    @Override
+    public int size() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Iterator iterator() {
+        try {
+            return new Iterator() {
+                final TemplateModelIterator i = model.iterator();
+    
+                @Override
+                public boolean hasNext() {
+                    try {
+                        return i.hasNext();
+                    } catch (TemplateModelException e) {
+                        throw new UndeclaredThrowableException(e);
+                    }
+                }
+                
+                @Override
+                public Object next() {
+                    try {
+                        return wrapper.unwrap(i.next());
+                    } catch (TemplateModelException e) {
+                        throw new UndeclaredThrowableException(e);
+                    }
+                }
+                
+                @Override
+                public void remove() {
+                    throw new UnsupportedOperationException();
+                }
+            };
+        } catch (TemplateModelException e) {
+            throw new UndeclaredThrowableException(e);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CollectionAndSequence.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CollectionAndSequence.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CollectionAndSequence.java
new file mode 100644
index 0000000..7979981
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CollectionAndSequence.java
@@ -0,0 +1,111 @@
+/*
+ * 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.util.ArrayList;
+
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateCollectionModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+
+/**
+ * Add sequence capabilities to an existing collection, or
+ * vice versa. Used by ?keys and ?values built-ins.
+ */
+// [FM3] FTL sequence should extend FTL collection, so we shouldn't need that direction, only the other.
+final public class CollectionAndSequence implements TemplateCollectionModel, TemplateSequenceModel {
+    private TemplateCollectionModel collection;
+    private TemplateSequenceModel sequence;
+    private ArrayList data;
+
+    public CollectionAndSequence(TemplateCollectionModel collection) {
+        this.collection = collection;
+    }
+
+    public CollectionAndSequence(TemplateSequenceModel sequence) {
+        this.sequence = sequence;
+    }
+
+    @Override
+    public TemplateModelIterator iterator() throws TemplateModelException {
+        if (collection != null) {
+            return collection.iterator();
+        } else {
+            return new SequenceIterator(sequence);
+        }
+    }
+
+    @Override
+    public TemplateModel get(int i) throws TemplateModelException {
+        if (sequence != null) {
+            return sequence.get(i);
+        } else {
+            initSequence();
+            return (TemplateModel) data.get(i);
+        }
+    }
+
+    @Override
+    public int size() throws TemplateModelException {
+        if (sequence != null) {
+            return sequence.size();
+        } else if (collection instanceof TemplateCollectionModelEx) {
+            return ((TemplateCollectionModelEx) collection).size();
+        } else {
+            initSequence();
+            return data.size();
+        }
+    }
+
+    private void initSequence() throws TemplateModelException {
+        if (data == null) {
+            data = new ArrayList();
+            TemplateModelIterator it = collection.iterator();
+            while (it.hasNext()) {
+                data.add(it.next());
+            }
+        }
+    }
+
+    private static class SequenceIterator
+    implements TemplateModelIterator {
+        private final TemplateSequenceModel sequence;
+        private final int size;
+        private int index = 0;
+
+        SequenceIterator(TemplateSequenceModel sequence) throws TemplateModelException {
+            this.sequence = sequence;
+            size = sequence.size();
+            
+        }
+        @Override
+        public TemplateModel next() throws TemplateModelException {
+            return sequence.get(index++);
+        }
+
+        @Override
+        public boolean hasNext() {
+            return index < size;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultArrayAdapter.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultArrayAdapter.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultArrayAdapter.java
new file mode 100644
index 0000000..2db536d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultArrayAdapter.java
@@ -0,0 +1,378 @@
+/*
+ * 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.io.Serializable;
+import java.lang.reflect.Array;
+
+import org.apache.freemarker.core.model.AdapterTemplateModel;
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.ObjectWrapperAndUnwrapper;
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.WrapperTemplateModel;
+import org.apache.freemarker.core.model.WrappingTemplateModel;
+
+/**
+ * Adapts an {@code array} of a non-primitive elements to the corresponding {@link TemplateModel} interface(s), most
+ * importantly to {@link TemplateHashModelEx}. If you aren't wrapping an already existing {@code array}, but build a
+ * sequence specifically to be used from a template, also consider using {@link SimpleSequence} (see comparison there).
+ *
+ * <p>
+ * Thread safety: A {@link DefaultListAdapter} is as thread-safe as the array that it wraps is. Normally you only
+ * have to consider read-only access, as the FreeMarker template language doesn't allow writing these sequences (though
+ * of course, Java methods called from the template can violate this rule).
+ * 
+ * <p>
+ * This adapter is used by {@link DefaultObjectWrapper} if its {@code useAdaptersForCollections} property is
+ * {@code true}, which is the default when its {@code incompatibleImprovements} property is 2.3.22 or higher.
+ * 
+ * @see SimpleSequence
+ * @see DefaultListAdapter
+ * @see TemplateSequenceModel
+ * 
+ * @since 2.3.22
+ */
+public abstract class DefaultArrayAdapter extends WrappingTemplateModel implements TemplateSequenceModel,
+        AdapterTemplateModel, WrapperTemplateModel, Serializable {
+
+    /**
+     * Factory method for creating new adapter instances.
+     * 
+     * @param array
+     *            The array to adapt; can't be {@code null}. Must be an array. 
+     * @param wrapper
+     *            The {@link ObjectWrapper} used to wrap the items in the array. Has to be
+     *            {@link ObjectWrapperAndUnwrapper} because of planned future features.
+     */
+    public static DefaultArrayAdapter adapt(Object array, ObjectWrapperAndUnwrapper wrapper) {
+        final Class componentType = array.getClass().getComponentType();
+        if (componentType == null) {
+            throw new IllegalArgumentException("Not an array");
+        }
+        
+        if (componentType.isPrimitive()) {
+            if (componentType == int.class) {
+                return new IntArrayAdapter((int[]) array, wrapper);
+            }
+            if (componentType == double.class) {
+                return new DoubleArrayAdapter((double[]) array, wrapper);
+            }
+            if (componentType == long.class) {
+                return new LongArrayAdapter((long[]) array, wrapper);
+            }
+            if (componentType == boolean.class) {
+                return new BooleanArrayAdapter((boolean[]) array, wrapper);
+            }
+            if (componentType == float.class) {
+                return new FloatArrayAdapter((float[]) array, wrapper);
+            }
+            if (componentType == char.class) {
+                return new CharArrayAdapter((char[]) array, wrapper);
+            }
+            if (componentType == short.class) {
+                return new ShortArrayAdapter((short[]) array, wrapper);
+            }
+            if (componentType == byte.class) {
+                return new ByteArrayAdapter((byte[]) array, wrapper);
+            }
+            return new GenericPrimitiveArrayAdapter(array, wrapper);
+        } else {
+            return new ObjectArrayAdapter((Object[]) array, wrapper);
+        }
+    }
+
+    private DefaultArrayAdapter(ObjectWrapper wrapper) {
+        super(wrapper);
+    }
+
+    @Override
+    public final Object getAdaptedObject(Class hint) {
+        return getWrappedObject();
+    }
+
+    private static class ObjectArrayAdapter extends DefaultArrayAdapter {
+
+        private final Object[] array;
+
+        private ObjectArrayAdapter(Object[] array, ObjectWrapper wrapper) {
+            super(wrapper);
+            this.array = array;
+        }
+
+        @Override
+        public TemplateModel get(int index) throws TemplateModelException {
+            return index >= 0 && index < array.length ? wrap(array[index]) : null;
+        }
+
+        @Override
+        public int size() throws TemplateModelException {
+            return array.length;
+        }
+
+        @Override
+        public Object getWrappedObject() {
+            return array;
+        }
+
+    }
+
+    private static class ByteArrayAdapter extends DefaultArrayAdapter {
+
+        private final byte[] array;
+
+        private ByteArrayAdapter(byte[] array, ObjectWrapper wrapper) {
+            super(wrapper);
+            this.array = array;
+        }
+
+        @Override
+        public TemplateModel get(int index) throws TemplateModelException {
+            return index >= 0 && index < array.length ? wrap(Byte.valueOf(array[index])) : null;
+        }
+
+        @Override
+        public int size() throws TemplateModelException {
+            return array.length;
+        }
+
+        @Override
+        public Object getWrappedObject() {
+            return array;
+        }
+
+    }
+
+    private static class ShortArrayAdapter extends DefaultArrayAdapter {
+
+        private final short[] array;
+
+        private ShortArrayAdapter(short[] array, ObjectWrapper wrapper) {
+            super(wrapper);
+            this.array = array;
+        }
+
+        @Override
+        public TemplateModel get(int index) throws TemplateModelException {
+            return index >= 0 && index < array.length ? wrap(Short.valueOf(array[index])) : null;
+        }
+
+        @Override
+        public int size() throws TemplateModelException {
+            return array.length;
+        }
+
+        @Override
+        public Object getWrappedObject() {
+            return array;
+        }
+
+    }
+
+    private static class IntArrayAdapter extends DefaultArrayAdapter {
+
+        private final int[] array;
+
+        private IntArrayAdapter(int[] array, ObjectWrapper wrapper) {
+            super(wrapper);
+            this.array = array;
+        }
+
+        @Override
+        public TemplateModel get(int index) throws TemplateModelException {
+            return index >= 0 && index < array.length ? wrap(Integer.valueOf(array[index])) : null;
+        }
+
+        @Override
+        public int size() throws TemplateModelException {
+            return array.length;
+        }
+
+        @Override
+        public Object getWrappedObject() {
+            return array;
+        }
+
+    }
+
+    private static class LongArrayAdapter extends DefaultArrayAdapter {
+
+        private final long[] array;
+
+        private LongArrayAdapter(long[] array, ObjectWrapper wrapper) {
+            super(wrapper);
+            this.array = array;
+        }
+
+        @Override
+        public TemplateModel get(int index) throws TemplateModelException {
+            return index >= 0 && index < array.length ? wrap(Long.valueOf(array[index])) : null;
+        }
+
+        @Override
+        public int size() throws TemplateModelException {
+            return array.length;
+        }
+
+        @Override
+        public Object getWrappedObject() {
+            return array;
+        }
+
+    }
+
+    private static class FloatArrayAdapter extends DefaultArrayAdapter {
+
+        private final float[] array;
+
+        private FloatArrayAdapter(float[] array, ObjectWrapper wrapper) {
+            super(wrapper);
+            this.array = array;
+        }
+
+        @Override
+        public TemplateModel get(int index) throws TemplateModelException {
+            return index >= 0 && index < array.length ? wrap(Float.valueOf(array[index])) : null;
+        }
+
+        @Override
+        public int size() throws TemplateModelException {
+            return array.length;
+        }
+
+        @Override
+        public Object getWrappedObject() {
+            return array;
+        }
+
+    }
+
+    private static class DoubleArrayAdapter extends DefaultArrayAdapter {
+
+        private final double[] array;
+
+        private DoubleArrayAdapter(double[] array, ObjectWrapper wrapper) {
+            super(wrapper);
+            this.array = array;
+        }
+
+        @Override
+        public TemplateModel get(int index) throws TemplateModelException {
+            return index >= 0 && index < array.length ? wrap(Double.valueOf(array[index])) : null;
+        }
+
+        @Override
+        public int size() throws TemplateModelException {
+            return array.length;
+        }
+
+        @Override
+        public Object getWrappedObject() {
+            return array;
+        }
+
+    }
+
+    private static class CharArrayAdapter extends DefaultArrayAdapter {
+
+        private final char[] array;
+
+        private CharArrayAdapter(char[] array, ObjectWrapper wrapper) {
+            super(wrapper);
+            this.array = array;
+        }
+
+        @Override
+        public TemplateModel get(int index) throws TemplateModelException {
+            return index >= 0 && index < array.length ? wrap(Character.valueOf(array[index])) : null;
+        }
+
+        @Override
+        public int size() throws TemplateModelException {
+            return array.length;
+        }
+
+        @Override
+        public Object getWrappedObject() {
+            return array;
+        }
+
+    }
+
+    private static class BooleanArrayAdapter extends DefaultArrayAdapter {
+
+        private final boolean[] array;
+
+        private BooleanArrayAdapter(boolean[] array, ObjectWrapper wrapper) {
+            super(wrapper);
+            this.array = array;
+        }
+
+        @Override
+        public TemplateModel get(int index) throws TemplateModelException {
+            return index >= 0 && index < array.length ? wrap(Boolean.valueOf(array[index])) : null;
+        }
+
+        @Override
+        public int size() throws TemplateModelException {
+            return array.length;
+        }
+
+        @Override
+        public Object getWrappedObject() {
+            return array;
+        }
+
+    }
+
+    /**
+     * Much slower than the specialized versions; used only as the last resort.
+     */
+    private static class GenericPrimitiveArrayAdapter extends DefaultArrayAdapter {
+
+        private final Object array;
+        private final int length;
+
+        private GenericPrimitiveArrayAdapter(Object array, ObjectWrapper wrapper) {
+            super(wrapper);
+            this.array = array;
+            length = Array.getLength(array);
+        }
+
+        @Override
+        public TemplateModel get(int index) throws TemplateModelException {
+            return index >= 0 && index < length ? wrap(Array.get(array, index)) : null;
+        }
+
+        @Override
+        public int size() throws TemplateModelException {
+            return length;
+        }
+
+        @Override
+        public Object getWrappedObject() {
+            return array;
+        }
+
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultEnumerationAdapter.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultEnumerationAdapter.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultEnumerationAdapter.java
new file mode 100644
index 0000000..d5b6989
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultEnumerationAdapter.java
@@ -0,0 +1,128 @@
+/*
+ * 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.io.Serializable;
+import java.util.Enumeration;
+import java.util.Iterator;
+
+import org.apache.freemarker.core.model.AdapterTemplateModel;
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.ObjectWrapperWithAPISupport;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+import org.apache.freemarker.core.model.TemplateModelWithAPISupport;
+import org.apache.freemarker.core.model.WrapperTemplateModel;
+import org.apache.freemarker.core.model.WrappingTemplateModel;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+/**
+ * Adapts an {@link Enumeration} to the corresponding {@link TemplateModel} interface(s), most importantly to
+ * {@link TemplateCollectionModel}. Putting aside that it wraps an {@link Enumeration} instead of an {@link Iterator},
+ * this is identical to {@link DefaultIteratorAdapter}, so see further details there.
+ */
+@SuppressWarnings("serial")
+public class DefaultEnumerationAdapter extends WrappingTemplateModel implements TemplateCollectionModel,
+        AdapterTemplateModel, WrapperTemplateModel, TemplateModelWithAPISupport, Serializable {
+
+    @SuppressFBWarnings(value="SE_BAD_FIELD", justification="We hope it's Seralizable")
+    private final Enumeration<?> enumeration;
+    private boolean enumerationOwnedBySomeone;
+
+    /**
+     * Factory method for creating new adapter instances.
+     *
+     * @param enumeration
+     *            The enumeration to adapt; can't be {@code null}.
+     */
+    public static DefaultEnumerationAdapter adapt(Enumeration<?> enumeration, ObjectWrapper wrapper) {
+        return new DefaultEnumerationAdapter(enumeration, wrapper);
+    }
+
+    private DefaultEnumerationAdapter(Enumeration<?> enumeration, ObjectWrapper wrapper) {
+        super(wrapper);
+        this.enumeration = enumeration;
+    }
+
+    @Override
+    public Object getWrappedObject() {
+        return enumeration;
+    }
+
+    @Override
+    public Object getAdaptedObject(Class<?> hint) {
+        return getWrappedObject();
+    }
+
+    @Override
+    public TemplateModelIterator iterator() throws TemplateModelException {
+        return new SimpleTemplateModelIterator();
+    }
+
+    @Override
+    public TemplateModel getAPI() throws TemplateModelException {
+        return ((ObjectWrapperWithAPISupport) getObjectWrapper()).wrapAsAPI(enumeration);
+    }
+
+    /**
+     * Not thread-safe.
+     */
+    private class SimpleTemplateModelIterator implements TemplateModelIterator {
+
+        private boolean enumerationOwnedByMe;
+
+        @Override
+        public TemplateModel next() throws TemplateModelException {
+            if (!enumerationOwnedByMe) {
+                checkNotOwner();
+                enumerationOwnedBySomeone = true;
+                enumerationOwnedByMe = true;
+            }
+
+            if (!enumeration.hasMoreElements()) {
+                throw new TemplateModelException("The collection has no more items.");
+            }
+
+            Object value = enumeration.nextElement();
+            return value instanceof TemplateModel ? (TemplateModel) value : wrap(value);
+        }
+
+        @Override
+        public boolean hasNext() throws TemplateModelException {
+            // Calling hasNext may looks safe, but I have met sync. problems.
+            if (!enumerationOwnedByMe) {
+                checkNotOwner();
+            }
+
+            return enumeration.hasMoreElements();
+        }
+
+        private void checkNotOwner() throws TemplateModelException {
+            if (enumerationOwnedBySomeone) {
+                throw new TemplateModelException(
+                        "This collection value wraps a java.util.Enumeration, thus it can be listed only once.");
+            }
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultIterableAdapter.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultIterableAdapter.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultIterableAdapter.java
new file mode 100644
index 0000000..6fd2680
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultIterableAdapter.java
@@ -0,0 +1,94 @@
+/*
+ * 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.io.Serializable;
+import java.util.Collection;
+import java.util.Iterator;
+
+import org.apache.freemarker.core.model.AdapterTemplateModel;
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.ObjectWrapperAndUnwrapper;
+import org.apache.freemarker.core.model.ObjectWrapperWithAPISupport;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+import org.apache.freemarker.core.model.TemplateModelWithAPISupport;
+import org.apache.freemarker.core.model.WrapperTemplateModel;
+import org.apache.freemarker.core.model.WrappingTemplateModel;
+
+/**
+ * Adapts an {@link Iterable} to the corresponding {@link TemplateModel} interface(s), most importantly to
+ * {@link TemplateCollectionModel}. This should only be used if {@link Collection} is not implemented by the adapted
+ * object, because then {@link DefaultListAdapter} and {@link DefaultNonListCollectionAdapter} gives more functionality.
+ * 
+ * <p>
+ * Thread safety: A {@link DefaultIterableAdapter} is as thread-safe as the {@link Iterable} that it wraps is. Normally
+ * you only have to consider read-only access, as the FreeMarker template language doesn't provide mean to call
+ * {@link Iterator} modifier methods (though of course, Java methods called from the template can violate this rule).
+ *
+ * @since 2.3.25
+ */
+@SuppressWarnings("serial")
+public class DefaultIterableAdapter extends WrappingTemplateModel implements TemplateCollectionModel,
+        AdapterTemplateModel, WrapperTemplateModel, TemplateModelWithAPISupport, Serializable {
+    
+    private final Iterable<?> iterable;
+
+    /**
+     * Factory method for creating new adapter instances.
+     * 
+     * @param iterable
+     *            The collection to adapt; can't be {@code null}.
+     * @param wrapper
+     *            The {@link ObjectWrapper} used to wrap the items in the array. Has to be
+     *            {@link ObjectWrapperAndUnwrapper} because of planned future features.
+     */
+    public static DefaultIterableAdapter adapt(Iterable<?> iterable, ObjectWrapperWithAPISupport wrapper) {
+        return new DefaultIterableAdapter(iterable, wrapper);
+    }
+
+    private DefaultIterableAdapter(Iterable<?> iterable, ObjectWrapperWithAPISupport wrapper) {
+        super(wrapper);
+        this.iterable = iterable;
+    }
+
+    @Override
+    public TemplateModelIterator iterator() throws TemplateModelException {
+        return new DefaultUnassignableIteratorAdapter(iterable.iterator(), getObjectWrapper());
+    }
+
+    @Override
+    public Object getWrappedObject() {
+        return iterable;
+    }
+
+    @Override
+    public Object getAdaptedObject(Class hint) {
+        return getWrappedObject();
+    }
+
+    @Override
+    public TemplateModel getAPI() throws TemplateModelException {
+        return ((ObjectWrapperWithAPISupport) getObjectWrapper()).wrapAsAPI(iterable);
+    }
+
+}



[06/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/misc/identifierChars/src/main/freemarker/adhoc/IdentifierCharGenerator.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/misc/identifierChars/src/main/freemarker/adhoc/IdentifierCharGenerator.java b/freemarker-core/src/main/misc/identifierChars/src/main/freemarker/adhoc/IdentifierCharGenerator.java
new file mode 100644
index 0000000..abb9e91
--- /dev/null
+++ b/freemarker-core/src/main/misc/identifierChars/src/main/freemarker/adhoc/IdentifierCharGenerator.java
@@ -0,0 +1,546 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.adhoc;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Predicate;
+
+/**
+ * This was used for generating the JavaCC pattern and the Java method to check if a character
+ * can appear in an FTL identifier.
+ */
+public class IdentifierCharGenerator {
+    
+    public static void main(String[] args) {
+        new IdentifierCharGenerator().run();
+    }
+
+    private void run() {
+        List<Range> ranges = generateRanges(this::generatorPredicate);
+        List<Range> ranges2 = generateRanges(IdentifierCharGenerator::isFTLIdentifierStart);
+        
+        if (!ranges.equals(ranges2)) {
+            throw new AssertionError();
+        }
+        
+        ranges.forEach(r -> p(toJavaCC(r) + ", "));
+        
+        int firstSplit = 0;
+        while (firstSplit < ranges.size() && ranges.get(firstSplit).getEnd() < 0x80) {
+            firstSplit++;
+        }
+        printJava(ranges, firstSplit, "");
+    }
+    
+    private List<Range> generateRanges(Predicate<Integer> charCodePredicate) {
+        List<Range> ranges = new ArrayList<Range>();
+        
+        int startedRange = -1;
+        int i;
+        for (i = 0; i < 0x10000; i++) {
+            if (charCodePredicate.test(i)) {
+                if (startedRange == -1) {
+                    startedRange = i;
+                }
+            } else {
+                if (startedRange != -1) {
+                    ranges.add(new Range(startedRange, i));
+                }
+                startedRange = -1;
+            }
+        }
+        if (startedRange != -1) {
+            ranges.add(new Range(startedRange, i));
+        }
+        
+        return ranges;
+    }
+    
+    private static void printJava(List<Range> ranges, int splitUntil, String indentation) {
+        final int rangeCount = ranges.size();
+        if (rangeCount > 2) {
+            Range secondHalfStart = ranges.get(splitUntil);
+            pp(indentation);
+            pp("if (c < "); pp(toJavaCharCode(secondHalfStart.getStart())); p(") {");
+            {
+                List<Range> firstHalf = ranges.subList(0, splitUntil);
+                printJava(firstHalf, (firstHalf.size() + 1) / 2, indentation + "    ");
+            }
+            pp(indentation);
+            pp("} else { // c >= "); p(toJavaCharCode(secondHalfStart.start));
+            {
+                List<Range> secondHalf = ranges.subList(splitUntil, ranges.size());
+                printJava(secondHalf, (secondHalf.size() + 1) / 2, indentation + "    ");
+            }
+            pp(indentation);
+            p("}");
+        } else if (rangeCount == 2) {
+            pp(indentation);
+            pp("return ");
+            printJavaCondition(ranges.get(0));
+            pp(" || ");
+            printJavaCondition(ranges.get(1));
+            p(";");
+        } else if (rangeCount == 1) {
+            pp(indentation);
+            pp("return ");
+            printJavaCondition(ranges.get(0));
+            p(";");
+        } else {
+            throw new IllegalArgumentException("Empty range list");
+        }
+    }
+    
+    private static void printJavaCondition(Range range) {
+        if (range.size() > 1) {
+            pp("c >= "); pp(toJavaCharCode(range.getStart()));
+            pp(" && c <= "); pp(toJavaCharCode(range.getEnd() - 1));
+        } else {
+            pp("c == "); pp(toJavaCharCode(range.getStart()));
+        }
+    }
+
+    private boolean generatorPredicate(int c) {
+        return isLegacyFTLIdStartChar(c)
+                || (Character.isJavaIdentifierPart(c) && Character.isLetterOrDigit(c) && !(c >= '0' && c <= '9'));
+    }
+
+    private static boolean isLegacyFTLIdStartChar(int i) {
+        return i == '$' || i == '_'
+                || (i >= 'a' && i <= 'z')
+                || (i >= '@' && i <= 'Z')
+                || (i >= '\u00c0' && i <= '\u00d6')
+                || (i >= '\u00d8' && i <= '\u00f6')
+                || (i >= '\u00f8' && i <= '\u1fff')
+                || (i >= '\u3040' && i <= '\u318f')
+                || (i >= '\u3300' && i <= '\u337f')
+                || (i >= '\u3400' && i <= '\u3d2d')
+                || (i >= '\u4e00' && i <= '\u9fff')
+                || (i >= '\uf900' && i <= '\ufaff');
+    }
+
+    private static boolean isXML11NameChar(int i) {
+        return isXML11NameStartChar(i)
+                || i == '-' || i == '.' || (i >= '0' && i <= '9') || i == 0xB7
+                || (i >= 0x0300 && i <= 0x036F) || (i >= 0x203F && i <= 0x2040);
+    }
+    
+    private static boolean isXML11NameStartChar(int i) {
+        return i == ':' || (i >= 'A' && i <= 'Z') || i == '_' || (i >= 'a' && i <= 'z')
+                || i >= 0xC0 && i <= 0xD6
+                || i >= 0xD8 && i <= 0xF6
+                || i >= 0xF8 && i <= 0x2FF
+                || i >= 0x370 && i <= 0x37D
+                || i >= 0x37F && i <= 0x1FFF
+                || i >= 0x200C && i <= 0x200D
+                || i >= 0x2070 && i <= 0x218F
+                || i >= 0x2C00 && i <= 0x2FEF
+                || i >= 0x3001 && i <= 0xD7FF
+                || i >= 0xF900 && i <= 0xFDCF
+                || i >= 0xFDF0 && i <= 0xFFFD;
+    }
+
+    private static String toJavaCC(Range range) {
+        final int start = range.getStart(), end = range.getEnd();
+        if (start == end) { 
+            throw new IllegalArgumentException("Empty range");
+        }
+        if (end - start == 1) {
+            return toJavaCCCharString(start);
+        } else {
+            return toJavaCCCharString(start) + " - " + toJavaCCCharString(end - 1);
+        }
+    }
+
+    private static String toJavaCCCharString(int cc) {
+        StringBuilder sb = new StringBuilder();
+
+        sb.append('"');
+        if (cc < 0x7F && cc >= 0x20) {
+            sb.append((char) cc);
+        } else {
+            sb.append("\\u");
+            sb.append(toHexDigit((cc >> 12) & 0xF));
+            sb.append(toHexDigit((cc >> 8) & 0xF));
+            sb.append(toHexDigit((cc >> 4) & 0xF));
+            sb.append(toHexDigit(cc & 0xF));
+        }
+        sb.append('"');
+
+        return sb.toString();
+    }
+
+    private static String toJavaCharCode(int cc) {
+        StringBuilder sb = new StringBuilder();
+
+        if (cc < 0x7F && cc >= 0x20) {
+            sb.append('\'');
+            sb.append((char) cc);
+            sb.append('\'');
+        } else {
+            sb.append("0x");
+            if (cc > 0xFF) {
+                sb.append(toHexDigit((cc >> 12) & 0xF));
+                sb.append(toHexDigit((cc >> 8) & 0xF));
+            }
+            sb.append(toHexDigit((cc >> 4) & 0xF));
+            sb.append(toHexDigit(cc & 0xF));
+        }
+
+        return sb.toString();
+    }
+
+    private static char toHexDigit(int d) {
+        return (char) (d < 0xA ? d + '0' : d - 0xA + 'A');
+    }
+    
+    private static class Range {
+        
+        final int start;
+        final int end;
+        
+        public Range(int start, int end) {
+            this.start = start;
+            this.end = end;
+        }
+
+        public int getStart() {
+            return start;
+        }
+
+        public int getEnd() {
+            return end;
+        }
+        
+        public int size() {
+            return end - start;
+        }
+
+        @Override
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + end;
+            result = prime * result + start;
+            return result;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) return true;
+            if (obj == null) return false;
+            if (getClass() != obj.getClass()) return false;
+            Range other = (Range) obj;
+            if (end != other.end) return false;
+            if (start != other.start) return false;
+            return true;
+        }
+        
+    }
+
+    static void p(final Object o) {
+        System.out.println(o);
+    }
+
+    static void pp(final Object o) {
+        System.out.print(o);
+    }
+    
+    static void p(final Object[] o) {
+        System.out.println("[");
+        for (final Object i : o) {
+            System.out.println("  " + i);
+        }
+        System.out.println("]");
+    }
+
+    static void p() {
+        System.out.println();
+    }
+    
+    // This is a copy of the generated code (somewhat modified), so that we can compare it with the generatorPredicate.
+    public static boolean isFTLIdentifierStart(int cc) {
+        char c = (char) cc;
+        
+        if (c < 0xAA) {
+            if (c >= 'a' && c <= 'z' || c >= '@' && c <= 'Z') {
+                return true;
+            } else {
+                return c == '$' || c == '_'; 
+            }
+        } else { // c >= 0xAA
+            if (c < 0xA7F8) {
+                if (c < 0x2D6F) {
+                    if (c < 0x2128) {
+                        if (c < 0x2090) {
+                            if (c < 0xD8) {
+                                if (c < 0xBA) {
+                                    return c == 0xAA || c == 0xB5;
+                                } else { // c >= 0xBA
+                                    return c == 0xBA || c >= 0xC0 && c <= 0xD6;
+                                }
+                            } else { // c >= 0xD8
+                                if (c < 0x2071) {
+                                    return c >= 0xD8 && c <= 0xF6 || c >= 0xF8 && c <= 0x1FFF;
+                                } else { // c >= 0x2071
+                                    return c == 0x2071 || c == 0x207F;
+                                }
+                            }
+                        } else { // c >= 0x2090
+                            if (c < 0x2115) {
+                                if (c < 0x2107) {
+                                    return c >= 0x2090 && c <= 0x209C || c == 0x2102;
+                                } else { // c >= 0x2107
+                                    return c == 0x2107 || c >= 0x210A && c <= 0x2113;
+                                }
+                            } else { // c >= 0x2115
+                                if (c < 0x2124) {
+                                    return c == 0x2115 || c >= 0x2119 && c <= 0x211D;
+                                } else { // c >= 0x2124
+                                    return c == 0x2124 || c == 0x2126;
+                                }
+                            }
+                        }
+                    } else { // c >= 0x2128
+                        if (c < 0x2C30) {
+                            if (c < 0x2145) {
+                                if (c < 0x212F) {
+                                    return c == 0x2128 || c >= 0x212A && c <= 0x212D;
+                                } else { // c >= 0x212F
+                                    return c >= 0x212F && c <= 0x2139 || c >= 0x213C && c <= 0x213F;
+                                }
+                            } else { // c >= 0x2145
+                                if (c < 0x2183) {
+                                    return c >= 0x2145 && c <= 0x2149 || c == 0x214E;
+                                } else { // c >= 0x2183
+                                    return c >= 0x2183 && c <= 0x2184 || c >= 0x2C00 && c <= 0x2C2E;
+                                }
+                            }
+                        } else { // c >= 0x2C30
+                            if (c < 0x2D00) {
+                                if (c < 0x2CEB) {
+                                    return c >= 0x2C30 && c <= 0x2C5E || c >= 0x2C60 && c <= 0x2CE4;
+                                } else { // c >= 0x2CEB
+                                    return c >= 0x2CEB && c <= 0x2CEE || c >= 0x2CF2 && c <= 0x2CF3;
+                                }
+                            } else { // c >= 0x2D00
+                                if (c < 0x2D2D) {
+                                    return c >= 0x2D00 && c <= 0x2D25 || c == 0x2D27;
+                                } else { // c >= 0x2D2D
+                                    return c == 0x2D2D || c >= 0x2D30 && c <= 0x2D67;
+                                }
+                            }
+                        }
+                    }
+                } else { // c >= 0x2D6F
+                    if (c < 0x31F0) {
+                        if (c < 0x2DD0) {
+                            if (c < 0x2DB0) {
+                                if (c < 0x2DA0) {
+                                    return c == 0x2D6F || c >= 0x2D80 && c <= 0x2D96;
+                                } else { // c >= 0x2DA0
+                                    return c >= 0x2DA0 && c <= 0x2DA6 || c >= 0x2DA8 && c <= 0x2DAE;
+                                }
+                            } else { // c >= 0x2DB0
+                                if (c < 0x2DC0) {
+                                    return c >= 0x2DB0 && c <= 0x2DB6 || c >= 0x2DB8 && c <= 0x2DBE;
+                                } else { // c >= 0x2DC0
+                                    return c >= 0x2DC0 && c <= 0x2DC6 || c >= 0x2DC8 && c <= 0x2DCE;
+                                }
+                            }
+                        } else { // c >= 0x2DD0
+                            if (c < 0x3031) {
+                                if (c < 0x2E2F) {
+                                    return c >= 0x2DD0 && c <= 0x2DD6 || c >= 0x2DD8 && c <= 0x2DDE;
+                                } else { // c >= 0x2E2F
+                                    return c == 0x2E2F || c >= 0x3005 && c <= 0x3006;
+                                }
+                            } else { // c >= 0x3031
+                                if (c < 0x3040) {
+                                    return c >= 0x3031 && c <= 0x3035 || c >= 0x303B && c <= 0x303C;
+                                } else { // c >= 0x3040
+                                    return c >= 0x3040 && c <= 0x318F || c >= 0x31A0 && c <= 0x31BA;
+                                }
+                            }
+                        }
+                    } else { // c >= 0x31F0
+                        if (c < 0xA67F) {
+                            if (c < 0xA4D0) {
+                                if (c < 0x3400) {
+                                    return c >= 0x31F0 && c <= 0x31FF || c >= 0x3300 && c <= 0x337F;
+                                } else { // c >= 0x3400
+                                    return c >= 0x3400 && c <= 0x4DB5 || c >= 0x4E00 && c <= 0xA48C;
+                                }
+                            } else { // c >= 0xA4D0
+                                if (c < 0xA610) {
+                                    return c >= 0xA4D0 && c <= 0xA4FD || c >= 0xA500 && c <= 0xA60C;
+                                } else { // c >= 0xA610
+                                    return c >= 0xA610 && c <= 0xA62B || c >= 0xA640 && c <= 0xA66E;
+                                }
+                            }
+                        } else { // c >= 0xA67F
+                            if (c < 0xA78B) {
+                                if (c < 0xA717) {
+                                    return c >= 0xA67F && c <= 0xA697 || c >= 0xA6A0 && c <= 0xA6E5;
+                                } else { // c >= 0xA717
+                                    return c >= 0xA717 && c <= 0xA71F || c >= 0xA722 && c <= 0xA788;
+                                }
+                            } else { // c >= 0xA78B
+                                if (c < 0xA7A0) {
+                                    return c >= 0xA78B && c <= 0xA78E || c >= 0xA790 && c <= 0xA793;
+                                } else { // c >= 0xA7A0
+                                    return c >= 0xA7A0 && c <= 0xA7AA;
+                                }
+                            }
+                        }
+                    }
+                }
+            } else { // c >= 0xA7F8
+                if (c < 0xAB20) {
+                    if (c < 0xAA44) {
+                        if (c < 0xA8FB) {
+                            if (c < 0xA840) {
+                                if (c < 0xA807) {
+                                    return c >= 0xA7F8 && c <= 0xA801 || c >= 0xA803 && c <= 0xA805;
+                                } else { // c >= 0xA807
+                                    return c >= 0xA807 && c <= 0xA80A || c >= 0xA80C && c <= 0xA822;
+                                }
+                            } else { // c >= 0xA840
+                                if (c < 0xA8D0) {
+                                    return c >= 0xA840 && c <= 0xA873 || c >= 0xA882 && c <= 0xA8B3;
+                                } else { // c >= 0xA8D0
+                                    return c >= 0xA8D0 && c <= 0xA8D9 || c >= 0xA8F2 && c <= 0xA8F7;
+                                }
+                            }
+                        } else { // c >= 0xA8FB
+                            if (c < 0xA984) {
+                                if (c < 0xA930) {
+                                    return c == 0xA8FB || c >= 0xA900 && c <= 0xA925;
+                                } else { // c >= 0xA930
+                                    return c >= 0xA930 && c <= 0xA946 || c >= 0xA960 && c <= 0xA97C;
+                                }
+                            } else { // c >= 0xA984
+                                if (c < 0xAA00) {
+                                    return c >= 0xA984 && c <= 0xA9B2 || c >= 0xA9CF && c <= 0xA9D9;
+                                } else { // c >= 0xAA00
+                                    return c >= 0xAA00 && c <= 0xAA28 || c >= 0xAA40 && c <= 0xAA42;
+                                }
+                            }
+                        }
+                    } else { // c >= 0xAA44
+                        if (c < 0xAAC0) {
+                            if (c < 0xAA80) {
+                                if (c < 0xAA60) {
+                                    return c >= 0xAA44 && c <= 0xAA4B || c >= 0xAA50 && c <= 0xAA59;
+                                } else { // c >= 0xAA60
+                                    return c >= 0xAA60 && c <= 0xAA76 || c == 0xAA7A;
+                                }
+                            } else { // c >= 0xAA80
+                                if (c < 0xAAB5) {
+                                    return c >= 0xAA80 && c <= 0xAAAF || c == 0xAAB1;
+                                } else { // c >= 0xAAB5
+                                    return c >= 0xAAB5 && c <= 0xAAB6 || c >= 0xAAB9 && c <= 0xAABD;
+                                }
+                            }
+                        } else { // c >= 0xAAC0
+                            if (c < 0xAAF2) {
+                                if (c < 0xAADB) {
+                                    return c == 0xAAC0 || c == 0xAAC2;
+                                } else { // c >= 0xAADB
+                                    return c >= 0xAADB && c <= 0xAADD || c >= 0xAAE0 && c <= 0xAAEA;
+                                }
+                            } else { // c >= 0xAAF2
+                                if (c < 0xAB09) {
+                                    return c >= 0xAAF2 && c <= 0xAAF4 || c >= 0xAB01 && c <= 0xAB06;
+                                } else { // c >= 0xAB09
+                                    return c >= 0xAB09 && c <= 0xAB0E || c >= 0xAB11 && c <= 0xAB16;
+                                }
+                            }
+                        }
+                    }
+                } else { // c >= 0xAB20
+                    if (c < 0xFB46) {
+                        if (c < 0xFB13) {
+                            if (c < 0xAC00) {
+                                if (c < 0xABC0) {
+                                    return c >= 0xAB20 && c <= 0xAB26 || c >= 0xAB28 && c <= 0xAB2E;
+                                } else { // c >= 0xABC0
+                                    return c >= 0xABC0 && c <= 0xABE2 || c >= 0xABF0 && c <= 0xABF9;
+                                }
+                            } else { // c >= 0xAC00
+                                if (c < 0xD7CB) {
+                                    return c >= 0xAC00 && c <= 0xD7A3 || c >= 0xD7B0 && c <= 0xD7C6;
+                                } else { // c >= 0xD7CB
+                                    return c >= 0xD7CB && c <= 0xD7FB || c >= 0xF900 && c <= 0xFB06;
+                                }
+                            }
+                        } else { // c >= 0xFB13
+                            if (c < 0xFB38) {
+                                if (c < 0xFB1F) {
+                                    return c >= 0xFB13 && c <= 0xFB17 || c == 0xFB1D;
+                                } else { // c >= 0xFB1F
+                                    return c >= 0xFB1F && c <= 0xFB28 || c >= 0xFB2A && c <= 0xFB36;
+                                }
+                            } else { // c >= 0xFB38
+                                if (c < 0xFB40) {
+                                    return c >= 0xFB38 && c <= 0xFB3C || c == 0xFB3E;
+                                } else { // c >= 0xFB40
+                                    return c >= 0xFB40 && c <= 0xFB41 || c >= 0xFB43 && c <= 0xFB44;
+                                }
+                            }
+                        }
+                    } else { // c >= 0xFB46
+                        if (c < 0xFF21) {
+                            if (c < 0xFDF0) {
+                                if (c < 0xFD50) {
+                                    return c >= 0xFB46 && c <= 0xFBB1 || c >= 0xFBD3 && c <= 0xFD3D;
+                                } else { // c >= 0xFD50
+                                    return c >= 0xFD50 && c <= 0xFD8F || c >= 0xFD92 && c <= 0xFDC7;
+                                }
+                            } else { // c >= 0xFDF0
+                                if (c < 0xFE76) {
+                                    return c >= 0xFDF0 && c <= 0xFDFB || c >= 0xFE70 && c <= 0xFE74;
+                                } else { // c >= 0xFE76
+                                    return c >= 0xFE76 && c <= 0xFEFC || c >= 0xFF10 && c <= 0xFF19;
+                                }
+                            }
+                        } else { // c >= 0xFF21
+                            if (c < 0xFFCA) {
+                                if (c < 0xFF66) {
+                                    return c >= 0xFF21 && c <= 0xFF3A || c >= 0xFF41 && c <= 0xFF5A;
+                                } else { // c >= 0xFF66
+                                    return c >= 0xFF66 && c <= 0xFFBE || c >= 0xFFC2 && c <= 0xFFC7;
+                                }
+                            } else { // c >= 0xFFCA
+                                if (c < 0xFFDA) {
+                                    return c >= 0xFFCA && c <= 0xFFCF || c >= 0xFFD2 && c <= 0xFFD7;
+                                } else { // c >= 0xFFDA
+                                    return c >= 0xFFDA && c <= 0xFFDC;
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/misc/overloadedNumberRules/README.txt
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/misc/overloadedNumberRules/README.txt b/freemarker-core/src/main/misc/overloadedNumberRules/README.txt
new file mode 100644
index 0000000..ec6eebe
--- /dev/null
+++ b/freemarker-core/src/main/misc/overloadedNumberRules/README.txt
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+
+This FMPP project is used for generating the source code of some
+`OverloadedNumberUtil` methods based on the content of `prices.ods`
+(LibreOffice spreadsheet).
+
+Usage:
+1. Edit `prices.ods`
+3. If you have introduced new types in it, also update `toCsFreqSorted` and
+   `toCsCostBoosts` and `toCsContCosts` in `config.fmpp`.
+4. Save it into `prices.csv` (use comma as field separator)
+5. Run FMPP from this directory. It will generate
+   `<freemarkerProjectDir>/build/getArgumentConversionPrice.java`.
+6. Copy-pase its content into `OverloadedNumberUtil.java`.
+7. Ensure that the value of OverloadedNumberUtil.BIG_MANTISSA_LOSS_PRICE
+   still matches the value coming from the ODS and the cellValue
+   multipier coming from generator.ftl.
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/misc/overloadedNumberRules/config.fmpp
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/misc/overloadedNumberRules/config.fmpp b/freemarker-core/src/main/misc/overloadedNumberRules/config.fmpp
new file mode 100644
index 0000000..cf32e92
--- /dev/null
+++ b/freemarker-core/src/main/misc/overloadedNumberRules/config.fmpp
@@ -0,0 +1,73 @@
+# 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.
+
+sources: generator.ftl
+outputFile: ../../../../build/overloadedNumberRules.java
+data: {
+  t: csv(prices.csv, { separator: ',' })
+  
+  # Conversion target types sorted by decreasing probablity of occurence
+  toCsFreqSorted: [ Integer, Long, Double, Float, Byte, Short, BigDecimal, BigInteger ]
+  
+  # Conversion target types associated to conversion price boost. The prices from the spreadsheet
+  # will be multipied by 10000 and the boost will be added to it. Thus, if the price of two possible
+  # targets are the same according the spreadsheet (and only then!), the choice will depend on
+  # this boost.
+  # The more specific the (the smaller) type is, the lower the boost should be. This is improtant,
+  # because this number is also used for comparing the specificity of numerical types where
+  # there's no argument type available.
+  # Note where the price from the spreadsheet is 0 or "-" or "N/A", the boost is not used.
+  toCsCostBoosts: {
+    'Byte': 1, 'Short': 2, 'Integer': 3, 'Long': 4, 'BigInteger': 5,
+    'Float': 6, 'Double': 7,
+    'BigDecimal': 8
+  }
+  
+  # Conversion source types sorted by decreasing probablity of occurence
+  fromCsFreqSorted: [
+    Integer, 
+    IntegerBigDecimal, 
+    BigDecimal, 
+    Long, 
+    Double, 
+    Float, 
+    Byte, 
+    BigInteger,
+    LongOrInteger
+    DoubleOrFloat, 
+    DoubleOrIntegerOrFloat, 
+    DoubleOrInteger, 
+    DoubleOrLong, 
+    IntegerOrByte,
+    DoubleOrByte, 
+    LongOrByte
+    Short, 
+    LongOrShort
+    ShortOrByte
+    FloatOrInteger, 
+    FloatOrByte, 
+    FloatOrShort, 
+    BigIntegerOrInteger, 
+    BigIntegerOrLong, 
+    BigIntegerOrDouble,   
+    BigIntegerOrFloat, 
+    BigIntegerOrByte, 
+    IntegerOrShort,
+    DoubleOrShort, 
+    BigIntegerOrShort, 
+  ]
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/misc/overloadedNumberRules/generator.ftl
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/misc/overloadedNumberRules/generator.ftl b/freemarker-core/src/main/misc/overloadedNumberRules/generator.ftl
new file mode 100644
index 0000000..2a2bfde
--- /dev/null
+++ b/freemarker-core/src/main/misc/overloadedNumberRules/generator.ftl
@@ -0,0 +1,80 @@
+<#--
+  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.
+-->
+    static int getArgumentConversionPrice(Class fromC, Class toC) {
+        // DO NOT EDIT, generated code!
+        // See: src\main\misc\overloadedNumberRules\README.txt
+        if (toC == fromC) {
+            return 0;
+        <#list toCsFreqSorted as toC><#t>
+        } else if (toC == ${toC}.class) {
+                <#assign firstFromC = true>
+                <#list fromCsFreqSorted as fromC>
+                    <#if toC != fromC>
+            <#assign row = []>
+            <#list t as i>
+                <#if i[0] == fromC>
+                    <#assign row = i>
+                    <#break>
+                </#if>
+            </#list>
+            <#if !row?has_content><#stop "Not found: " + fromC></#if>
+            <#if !firstFromC>else </#if>if (fromC == ${fromC}.class) return ${toPrice(row[toC], toCsCostBoosts[toC])};
+            <#assign firstFromC = false>
+                    </#if>
+                </#list>
+            else return Integer.MAX_VALUE;
+        </#list>
+        } else {
+            // Unknown toC; we don't know how to convert to it:
+            return Integer.MAX_VALUE;
+        }        
+    }
+
+    static int compareNumberTypeSpecificity(Class c1, Class c2) {
+        // DO NOT EDIT, generated code!
+        // See: src\main\misc\overloadedNumberRules\README.txt
+        c1 = ClassUtil.primitiveClassToBoxingClass(c1);
+        c2 = ClassUtil.primitiveClassToBoxingClass(c2);
+        
+        if (c1 == c2) return 0;
+        
+        <#list toCsFreqSorted as c1><#t>
+        if (c1 == ${c1}.class) {
+          <#list toCsFreqSorted as c2><#if c1 != c2><#t>
+            if (c2 == ${c2}.class) return ${toCsCostBoosts[c2]} - ${toCsCostBoosts[c1]};
+          </#if></#list>
+            return 0;
+        }
+        </#list>
+        return 0;
+    }
+
+<#function toPrice cellValue, boost>
+    <#if cellValue?starts_with("BC ")>
+        <#local cellValue = cellValue[3..]>
+    <#elseif cellValue == '-' || cellValue == 'N/A'>
+        <#return 'Integer.MAX_VALUE'>
+    </#if>
+    <#local cellValue = cellValue?number>
+    <#if cellValue != 0>
+        <#return cellValue * 10000 + boost>
+    <#else>
+        <#return 0>
+    </#if>
+</#function>

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/misc/overloadedNumberRules/prices.ods
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/misc/overloadedNumberRules/prices.ods b/freemarker-core/src/main/misc/overloadedNumberRules/prices.ods
new file mode 100644
index 0000000..4beabcf
Binary files /dev/null and b/freemarker-core/src/main/misc/overloadedNumberRules/prices.ods differ

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/resources/META-INF/DISCLAIMER
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/resources/META-INF/DISCLAIMER b/freemarker-core/src/main/resources/META-INF/DISCLAIMER
new file mode 100644
index 0000000..569ba05
--- /dev/null
+++ b/freemarker-core/src/main/resources/META-INF/DISCLAIMER
@@ -0,0 +1,8 @@
+Apache FreeMarker is an effort undergoing incubation at The Apache Software
+Foundation (ASF), sponsored by the Apache Incubator. Incubation is required of
+all newly accepted projects until a further review indicates that the
+infrastructure, communications, and decision making process have stabilized in
+a manner consistent with other successful ASF projects. While incubation
+status is not necessarily a reflection of the completeness or stability of the
+code, it does indicate that the project has yet to be fully endorsed by the
+ASF.
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/resources/META-INF/LICENSE
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/resources/META-INF/LICENSE b/freemarker-core/src/main/resources/META-INF/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/freemarker-core/src/main/resources/META-INF/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed 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.

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/resources/org/apache/freemarker/core/model/impl/unsafeMethods.properties
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/resources/org/apache/freemarker/core/model/impl/unsafeMethods.properties b/freemarker-core/src/main/resources/org/apache/freemarker/core/model/impl/unsafeMethods.properties
new file mode 100644
index 0000000..05c1981
--- /dev/null
+++ b/freemarker-core/src/main/resources/org/apache/freemarker/core/model/impl/unsafeMethods.properties
@@ -0,0 +1,98 @@
+# 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.
+
+java.lang.Object.wait()
+java.lang.Object.wait(long)
+java.lang.Object.wait(long,int)
+java.lang.Object.notify()
+java.lang.Object.notifyAll()
+
+java.lang.Class.getClassLoader()
+java.lang.Class.newInstance()
+java.lang.Class.forName(java.lang.String)
+java.lang.Class.forName(java.lang.String,boolean,java.lang.ClassLoader)
+
+java.lang.reflect.Constructor.newInstance([Ljava.lang.Object;)
+
+java.lang.reflect.Method.invoke(java.lang.Object,[Ljava.lang.Object;)
+
+java.lang.reflect.Field.set(java.lang.Object,java.lang.Object)
+java.lang.reflect.Field.setBoolean(java.lang.Object,boolean)
+java.lang.reflect.Field.setByte(java.lang.Object,byte)
+java.lang.reflect.Field.setChar(java.lang.Object,char)
+java.lang.reflect.Field.setDouble(java.lang.Object,double)
+java.lang.reflect.Field.setFloat(java.lang.Object,float)
+java.lang.reflect.Field.setInt(java.lang.Object,int)
+java.lang.reflect.Field.setLong(java.lang.Object,long)
+java.lang.reflect.Field.setShort(java.lang.Object,short)
+
+java.lang.reflect.AccessibleObject.setAccessible([Ljava.lang.reflect.AccessibleObject;,boolean)
+java.lang.reflect.AccessibleObject.setAccessible(boolean)
+
+java.lang.Thread.destroy()
+java.lang.Thread.getContextClassLoader()
+java.lang.Thread.interrupt()
+java.lang.Thread.join()
+java.lang.Thread.join(long)
+java.lang.Thread.join(long,int)
+java.lang.Thread.resume()
+java.lang.Thread.run()
+java.lang.Thread.setContextClassLoader(java.lang.ClassLoader)
+java.lang.Thread.setDaemon(boolean)
+java.lang.Thread.setName(java.lang.String)
+java.lang.Thread.setPriority(int)
+java.lang.Thread.sleep(long)
+java.lang.Thread.sleep(long,int)
+java.lang.Thread.start()
+java.lang.Thread.stop()
+java.lang.Thread.stop(java.lang.Throwable)
+java.lang.Thread.suspend()
+
+java.lang.ThreadGroup.allowThreadSuspension(boolean)
+java.lang.ThreadGroup.destroy()
+java.lang.ThreadGroup.interrupt()
+java.lang.ThreadGroup.resume()
+java.lang.ThreadGroup.setDaemon(boolean)
+java.lang.ThreadGroup.setMaxPriority(int)
+java.lang.ThreadGroup.stop()
+java.lang.Thread.suspend()
+
+java.lang.Runtime.addShutdownHook(java.lang.Thread)
+java.lang.Runtime.exec(java.lang.String)
+java.lang.Runtime.exec([Ljava.lang.String;)
+java.lang.Runtime.exec([Ljava.lang.String;,[Ljava.lang.String;)
+java.lang.Runtime.exec([Ljava.lang.String;,[Ljava.lang.String;,java.io.File)
+java.lang.Runtime.exec(java.lang.String,[Ljava.lang.String;)
+java.lang.Runtime.exec(java.lang.String,[Ljava.lang.String;,java.io.File)
+java.lang.Runtime.exit(int)
+java.lang.Runtime.halt(int)
+java.lang.Runtime.load(java.lang.String)
+java.lang.Runtime.loadLibrary(java.lang.String)
+java.lang.Runtime.removeShutdownHook(java.lang.Thread)
+java.lang.Runtime.traceInstructions(boolean)
+java.lang.Runtime.traceMethodCalls(boolean)
+
+java.lang.System.exit(int)
+java.lang.System.load(java.lang.String)
+java.lang.System.loadLibrary(java.lang.String)
+java.lang.System.runFinalizersOnExit(boolean)
+java.lang.System.setErr(java.io.PrintStream)
+java.lang.System.setIn(java.io.InputStream)
+java.lang.System.setOut(java.io.PrintStream)
+java.lang.System.setProperties(java.util.Properties)
+java.lang.System.setProperty(java.lang.String,java.lang.String)
+java.lang.System.setSecurityManager(java.lang.SecurityManager)

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/resources/org/apache/freemarker/core/version.properties
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/resources/org/apache/freemarker/core/version.properties b/freemarker-core/src/main/resources/org/apache/freemarker/core/version.properties
new file mode 100644
index 0000000..ff1f446
--- /dev/null
+++ b/freemarker-core/src/main/resources/org/apache/freemarker/core/version.properties
@@ -0,0 +1,100 @@
+# 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.
+
+# Version info for the builds.
+
+# Version number
+# --------------
+#
+# The version number format is (since FreeMarker 3):
+#
+#   Version ::= major '.' minor '.' micro ('-' Qualifier)?
+#   Qualifier :: = NightlyQualifier
+#                  |
+#                  ( ('pre'|'rc') twoDigitPositiveInteger ('-' NightlyQualifier)? )
+#                  '-incubating'?
+#   NightlyQualifier :: = 'nightly'
+#
+# This format is compatible both with Maven and JSR 277, and it must
+# remain so. Stable versions must not have a qualifier.
+# Note that qualifiers are compared with String.compareTo,
+# thus "nightly" < "pre" < "rc", etc.
+#
+# Examples:
+#   Version number        Means
+#   3.0.0                 3.0.0 stable release
+#   3.3.12                3.3.12 stable release
+#   3.3.13-nightly
+#                         Modified version after 3.3.12, which will
+#                         become to 3.3.13 one day.
+#   3.4.0-pre03           The 3rd preview of version 3.4.0
+#   3.4.0-pre04-nightly
+#                         Unreleased nightly version of the yet unfinished
+#                         3.4.0-pre04.
+#   3.4.0-rc01            1st release candidate of 3.4.0
+#
+# Backward-compatibility policy (since FreeMarker 2.3.20):
+# - When only the micro version number is increased, full backward
+#   compatibility is expected (ignoring extreme cases where the user
+#   code or template breaks if an exception is *not* thrown anymore
+#   as the FreeMarker bug causing it was fixed).
+# - When the minor version number is increased, some minor backward
+#   compatibility violations are allowed. Most dependent code should
+#   continue working without modification or recompilation.
+# - When the major version number is increased, major backward
+#   compatibility violations are allowed, but still should be avoided.
+# During Apache Incubation, "-incubating" is added to this string.
+version=3.0.0-nightly-incubating
+# This exists as oss.sonatype only allows SNAPSHOT and final releases,
+# so instead 2.3.21-rc01 and such we have to use 2.3.21-SNAPSHOT there.
+# For final releases it's the same as "version".
+# During Apache Incubation, "-incubating" is added to this string.
+mavenVersion=3.0.0-SNAPSHOT-incubating
+
+# Version string that conforms to OSGi
+# ------------------------------------
+#
+# This is different from the plain version string:
+# - "." is used instead of a "-" before the qualifier.
+# - The stable releases must use "stable" qualifier.
+#   Examples:
+#   2.4.0.stable
+#   2.4.0.rc01
+#   2.4.0.pre01
+#   2.4.0.nightly_@timestampInVersion@
+# During Apache Incubation, "-incubating" is added to this string.
+versionForOSGi=3.0.0.nightly_@timestampInVersion@-incubating
+
+# Version string that conforms to legacy MF
+# -----------------------------------------
+#
+# Examples:
+# version        -> versionForMf
+# 2.2.5          -> 2.2.5
+# 2.3.0          -> 2.3.0
+# 2.3.0.pre13    -> 2.2.98.13
+# 2.3.0.pre13-nightly -> 2.2.98.13.97
+# 2.3.0.rc1      -> 2.2.99.1
+# 2.3.0.nightly -> 2.2.97
+# 3.0.0.pre2     -> 2.98.2
+#
+# "97 denotes "nightly", 98 denotes "pre", 99 denotes "rc" build.
+# In general, for the nightly/preview/rc Y of version 2.X, the versionForMf is
+# 2.X-1.(99|98).Y. Note the X-1.
+versionForMf=2.99.97
+
+isGAECompliant=true

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/manual/en_US/FM3-CHANGE-LOG.txt
----------------------------------------------------------------------
diff --git a/freemarker-core/src/manual/en_US/FM3-CHANGE-LOG.txt b/freemarker-core/src/manual/en_US/FM3-CHANGE-LOG.txt
new file mode 100644
index 0000000..e5a898e
--- /dev/null
+++ b/freemarker-core/src/manual/en_US/FM3-CHANGE-LOG.txt
@@ -0,0 +1,226 @@
+/*
+ * 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.
+ */
+ 
+Because the Manual won't be updated for a good while, I will lead
+the FreeMarer 3 changelog here:
+
+- Increased version number to 3.0.0 (nightly aka. SNAPSHOT)
+- Removed legacy extensions: rhyno, jython, xml (not to be confused with dom), jdom, ant.
+- Removed JSP 2.0 support (2.1 and Servlet 2.5 is the minimum for now, but maybe it will be 2.2 and Servlet 3.0 later).
+- Removed freemarker.ext.log, our log abstraction layer from the old times when there was no clear winner on this field.
+  Added org.slf4j:slf4j-api as required dependency instead.
+- Removed all classes with "main" methods that were part of freemarker.jar. Such tools should be separate artifacts,
+  not part of the library, and they are often classified as CWE-489 "Leftover Debug Code". The removed classes are:
+  freemarker.core.CommandLine, freemarker.ext.dom.Transform, freemarker.template.utility.ToCanonical
+- Removed classic_compatible (classicCompatible) setting, which was used to emulate some of the FreeMarker 1.x behavior
+- Removed utility TemplateModel-s that can very easily mean a security problem: freemarker.template.utility.Execute and 
+  freemarker.template.utility.ObjectConstructor
+- Removed TemplateClassResolver.SAFER_RESOLVER, because the classes it has blocked were removed from FreeMarker, so it's
+  the same as UNRESTRICTED_RESOLVER
+- Removed the strict_syntax setting, and so also the support for FTL tags without #. This was a FreeMarker 1.x
+  compatibility option.
+- Removed deprecated FMParser contstructors.
+- Minimum Java version increased to 7, but without try-with-resource as that's unavailable before Android 4.4 KitKat.
+- Totally redesigned TemplateLoader interface. The FM2 TemplateLoader can't be adapted (wrapped) to it, but usually
+  it's fairly trivial to "rearrange" an old custom TemplateLoader for the new interface. The new TemplateLoader comes
+  with several advantages, such as:
+  - It can work more efficiently with sophisticated storage mechanisms like a database, as it's now possible to pack
+    together the existence check, the last modification change check, and reading operations into less storage level
+    operations (like you can do all of them with a single SQL statement).
+  - The new TemplateLoader allows returning the template content either as an InputStream or as a Reader. Almost all
+    TemplateLoader-s should return InputStream, and FreeMarker takes care of charset issues transparently (as a result,
+    TemplateLoader-s don't have to support re-reading a template anymore, as we solve charset detection misses in
+    memory). TemplateLoader-s that are inherently backed by text (String-s), such as templates stored in a varchar or
+    CLOB column, should return a Reader. Note that templates created from a Reader will have template.getEncoding()
+    null (logically, as no charset was involved), which was impossible in FreeMarker 2.
+  - The change detection of the template doesn't have to rely on a millisecond resolution timestamp anymore; you can
+    use what's most appropriate for the storage mechanism, such as a cryptographic hash or a revision number.
+  - Template lookups (where you try multiple names until you find the best template) can now be transactional and/or
+    atomic if the backing storage mechanism supports that, by utilizing the TemplateLoaderSession interface.
+  - TemplateLoader can now return template-level settings like the output format (MIME type basically) of the loaded
+    template, in case the backing storage stores such extra information. This mechanism can be used together with
+    the TemplateConfiguration mechanism (already familiar from FreeMarker 2), and overrides the individual settings
+    coming from there.
+- Template constructors won't close the Reader passed in as agrument anymore (because a Reader should be closed
+  by who has created it). This avoids some surprises from the past, such as the unablility to reset a Reader to a mark
+  after parsing. If you call those constructors, be sure that you close the Reader yourself. (Obviously, it doesn't
+  mater for StringReader-s.)
+- Renamed top level java package from freemarker to org.apache.freemarker 
+- Reorganized package structure. We will have a freemarker-core and a freemarker-servlet module later, so
+  we got org.apache.freemarker.core (has nothing to do with the old freemarker.core) and
+  org.apache.freemarker.servlet (this replaced freemarker.ext.servlet and freemarker.ext.jsp).
+  Directly inside org.apache.freemarker.core we have most of the classes that were in
+  freemarker.template and freemarker.core, however, model related classes (and object wrappers)
+  were moved to org.apache.freemarker.core.model, and template loading and caching related classes
+  to org.apache.freemarker.core.templateresolver (because later we will have a class called
+  TemplateResolver, which is the central class of loading and caching and template name rules).
+  OutputFormat realted classes were moved to org.apache.freemarker.core.outputformat.
+  ValueFormat related classes were moved to org.apache.freemarker.core.valueformat.
+  ArithmeticEngine related classes were moved to org.apache.freemarker.core.arithmetic.
+  freemarker.ext.dom was moved into org.apache.freemarker.dom.
+- Moved the all the static final ObjectWrapper-s to the new _StaticObjectWrappers class, and made them
+  write protected (non-configurable). Also now they come from the pool that ObjectWrapper builders use.
+- WrappingTemplateModel.objectWrapper is now final, and its statically stored default value can't be set anymore.
+- Removed SimpleObjectWrapper deprecated paramerless constructor
+- Removed ResourceBundleLocalizedString and LocalizedString: Hardly anybody has discovered these, and they had no
+  JUnit coverage.
+- Added early draft of TemplateResolver, renamed TemplateCache to DefaultTemplateResolver. TemplateResolver is not
+  yet directly used in Configuration. This was only added in a hurry, so that it's visible why the
+  o.a.f.core.templateresolver subpackage name makes sense.
+- Marked most static utility classes as internal, and renamed them to start with "_" (for example StringUtils was
+  renamed to _StringUtil, thus people won't accidentally use it when they wanted to autocomplete to Apache Commons
+  StringUtil). Created published static utility class, o.a.f.core.util.FTLUtil, which contains some methods moved
+  over from the now internal utility classes.
+- Deleted o.a.f.core.util.DOMNodeModel (it has noting to do with the standard XML support, o.a.f.core.model.dom)
+- All CacheStorage-s must be thread safe from now on (removed ConcurrentCacheStorage marker interface)
+- Removed support for incompatibleImprovements before 3.0.0. So currently 3.0.0 is the only support value.
+- Changed the default of logTemplateExceptions to false.
+- Removed `String Configurable.getSetting(String)` and `Properties getSettings()`. It has never worked well,
+  and is impossible to implement properly.
+- Even for setting values that are class names without following `()` or other argument list, the INSTANCE field and
+  the builder class will be searched now, and used instead of the constructor of the class. Earlier they weren't for
+  backward compatibility.
+- Removed several deprecated methods and constants. Some notes:
+  - strict_bean_models configuration setting was removed, as it should be set on the BeansWrapper itself
+  - .template_name now means the same as .current_template_name (it doesn't emulate 2.3 glitches anymore)
+  - Removed the deprecated BeansWrapper.nullModel setting. So null is always wrapped to null now.
+  - Removed the overridable BeansWrapper.finetuneMethodAppearance method, which was deprecated by the
+    finetuneMethodAppearance setting (BeansWrapper.setFinetuneMethodAppearance).
+  - Removed NodeModel static utility classes dealing with parsing XML to DOM. How it's best to do that is environment
+    and application dependent, and it has security implications. Since XML loading/parsing is not the topic of the
+    project, these were removed. Static methods that simplify an already loaded DOM have remained, because that's
+    FreeMarker-specific functionality.
+  - Removed parameterless DefaultObjectWrapper and BeansWrapper constructors. Now specifying the
+    incomplatibleImprovement version is required.
+  - Removed the static default Configuration instance. (It's not possible to create a template with null Configuration
+    constructor argument anymore.)
+  - When specifying the templateUpdateDelay configuration setting with a String (with Properties), the time unit is
+    required, unless the value is 0.
+- setSetting (and the like) doesn't throw ParseException (the same exception used when parsing templates) anymore,
+  but ConfigurationException. Also, on the places where ParseException was used for other than template parsing,
+  o.a.f.core.util.GenericParseException is used now instead, which doesn't have the template parsing related fields
+  that we can't fill.
+- Removed DefaultObjectWrapper settings that only exist so that you can set backward compatible behavior instead of
+  the recommended value: useAdaptersForContainers, forceLegacyNonListCollections, iterableSupport, simpleMapWrapper
+- Removed BeansWrapper, which was the superclass of DefaultObjectWrapper, but wasn't recommended to be used as is.
+  Removed many BeansWrapper-related classes that DefaultObjectWrapper doesn't use. This includes ModelCache and
+  related classes, because DefaultObjectWrapper has only used the cache for "generic" classes (because that's where it
+  has fallen back to BeansWrapper.wrap), which is inconsistent and doesn't worth the caching overhead and complexity.
+- Java methods (when using DefaultObjectWrapper) won't be accessible as sequences anyore. That is, earlier, instead of
+  obj.m(1), you could write obj.m[1]. This strange feature has led to some tricky cases, while almost nobody has
+  utilized it.
+- SimpleObjectWrapper was renamed to RestrictedObjectWrapper, also the "simple" setting value was rename to
+  "restricted".
+- Removed the global static final ObjectWrapper-s. It had a "few" consequences:
+  - Standard TemplateModel implementations that can have an ObjectWrapper contrucor parameter don't allow null there anymore.
+    Also, any constructor overloads where you cold omit the ObjectWrapper were removed (these were deprecated in FM2 too).
+    In FM2, such overloads has used the global static default DefaltObjectWrapper, but that was removed.
+  - If the ObjectWrapper is not a DefaultObjectWrapper (or a subclass of it), `className?new(args)` will only accept 0 arguments.
+    (Earlier we have fallen back to using the global static default DefaultObjectWrapper instance to handle argument unwrapping
+    and overloaded constructors.) Note that ?new is only used to instantiate TemplateModel-s, typically, template language
+    functions/directives implemented in Java, and so they hardly ever has an argument.
+  - FreemarkerServlet now requires that the ObjectWrapper it uses implements ObjectWrapperAndUnwrapper. (Thus, the return type
+    of FreemarerServlet.createDefaultObjectWrapper() has changed to ObjectWrapperAndUnwrapper.) The unwrapping functionality is
+    required when calling JSP custom tags/functions, and in FreeMarker 2 this was worked around with using the
+    global static default DefaultObjectWrapper when the ObjectWrapper wasn't an ObjectWrapperAndUnwrapper.
+- Removed some long deprecated template language directives:
+  - <#call ...> (deprecated by <@... />)
+  - <#comment>...</#comment> (deprecated by <#-- ... -->)
+  - <#transform ...>...</#transform> (deprecated by <@....@...>)
+  - <#foreach x in xs>...</#foreach> (deprecated by <#list xs as x>...</#list>)
+- If for an indexed JavaBean property there's both an indexed read method (like `Foo getFoo(int index)`) and a normal read method
+  (like Foo[] getFoo()), we prefer the normal read method, and so the result will be a clean FTL sequence (not a multi-type value
+  with sequence+method type). If there's only an indexed read method, then we don't expose the property anymore, but the indexed
+  read method can still be called as an usual method (as `myObj.getFoo(index)`). These changes were made because building on the
+  indexed read method we can't create a proper sequence (which the value of the property should be), since sequences are required
+  to support returning their size. (In FreeMarker 2 such sequences has thrown exception on calling size(), which caused more
+  problems and confusion than it solved.)
+- When looking for a builder class in builder expressions used in setting values like `com.example.Foo()`, now we first
+  look for com.example.Foo.Builder, and only then com.example.FooBuilder.
+- Removed DefaultObjectWrapper.methodsShadowItems setting, in effect defaulting it to true. This has decided if the generic
+  get method (`get(String)`) had priority over methods of similar name. The generic get method is only recognized from its
+  name and parameter type, so it's a quite consfusing feature, and might will be removed alltogether.
+- DefaultObjectWrapper is now immutable (has no setter methods), and can only be constructed with DefaultObjectWrapper.Builder
+- Configuration.getTemplate has no "parse" parameter anymore. Similarly #include has no "parse" parameter anymore. Whether a
+  template is parsed can be specified via Configuration.templateConfigurations, for example based on the file extension. Thus,
+  a certain template is either always parsed or never parsed, and whoever gets or include it need not know about that.
+  Also added a new setting, "templateLanguage", which decides this; the two available values are
+  TemplateLanguage.FTL and TemplateLanguage.STATIC_TEXT.
+- Configuration.getTemplate has no "encoding" parameter anymore. Similarly #include has no "encoding" parameter either. The charset
+  of templates can be specified via Configuration.defaultEncoding and Configuration.templateConfigurations (for example based on the
+  directory it is in), or wirh the #ftl directive inside the template. Thus, a given template always has the same charset, no mater how
+  it's accessed.
+- #include-d/#import-ed templates don't inheirit the charset (encoding) of the #include-ing/#import-ing template. (Because,
+  again, the charset of a template file is independent of how you access it.)
+- Removed Configuration.setEncoding(java.util.Locale, String) and the related other methods. Because of the new logic of template
+  encodings, the locale to encoding mapping doesn't make much sense anymore.
+- Require customLookupCondition-s to be Serializable.
+- Various refactorings of Configurable and its subclasses. This is part of the preparation for making such classes immutable, and offer
+  builders to create them.
+  - Removed CustomAttribute class. Custom attribute keys can be anything at the moment (this will be certainly restricted later)
+  - As customAttributes won't be modifiable after Builder.build(), they can't be used for on-demand created data structures anymore (such as
+    Template-scoped caches) anymore. To fulfill that role, the CustomStateKey class and the CustomStateScope interface was introduced, which
+    is somewhat similar to the now removed CustomAttribute. CustomStateScope contains one method, Object getCustomState(CustomStateKey), which
+    may calls CustomStateKey.create() to lazily create the state object for the key. Configuration, Template and Environment implements
+    CustomStateScope.
+  - Added getter/setter to access custom attributes as a Map. (This is to make it less an exceptional setting.)
+  - Environment.setCustomState(Object, Object) and getCustomState(Object) was replaced with CustomStateScope.getCustomState(CustomStateKey).
+  - Added ProcessingConfiguration interface for the read-only access of template processing settings. This is similar to the
+    already existing (in FM2) ParserConfiguration interface.
+  - Renamed Configurable to MutableProcessingAndParserConfiguration. Made it abstract too.
+  - Renamed Configuration.defaultEncoding to sourceEncoding, also added sourceEncoding to ParserConfiguration, and renamed
+    TemplateConfiguration.encoding and Template.encoding to sourceEncoding. (Before this, defaultEncoding was exclusive
+    to Configuration, but now it's like any other ParserConfiguration setting that can be overidden on the 3 levels.)
+  - Made TemplateConfiguration immutable, added a TemplateConfiguration.Builder.
+  - Made Template immutable (via public API-s). Template-specific settings now can only come from the TemplateConfiguration associated
+    to the template, or from the #ftl header for some settings (most notably for custom attributes).
+  - Renamed ParserConfiguration to ParsingConfiguration, so that the name is more consistent with the new ProcessingConfiguration.
+- Settings that have contained a charset name (sourceEncoding, outputEncoding, URLEscapingCharset) are now of type Charset,
+  not String. For string based configuration sources (such as .properties files) this means that:
+  - Unrecognized charset names are now errors
+  - For recognized names the charset name will be normalized (like "latin1" becomes to "ISO-8859-1").
+  - In "object builder expressions" Charset values can now be constructed like `Charset("ISO-8859-5")`.
+    Note that as the type of the settings have changed, now you can't just write something like
+    `TemplateConfiguration(sourceEncoding = "UTF-8")`, but `TemplateConfiguration(sourceEncoding = Charset("UTF-8"))`.
+- Removed Template.templateLanguageVersion, as we solely rely on incompatibleImprovements instead.
+- Template.sourceEncoding was renamed to Template.actualSourceEncoding, to emphasize that it's not the template-layer
+  equivalent of the sourceEncoding ParserConfiguration setting. This is in line with Template.actualTagSyntax and the
+  other "actual" properties. (Just as in FM2, Template.getParserConfiguration() still can be used get the
+  sourceEncoding used during parsing.)
+- Made TemplateModel classes used by the parser for literals Serializable. (Without this attribute values set in the #ftl
+  header wouldn't be always Serializable, which in turn will sabotage making Template-s Serializable in the future.)
+- Removed hasCustomFormats() from configuration related API-s (we don't need it anymore)
+- Template.name (getName()) was renamed to Template.lookupName (getLookupName()), and Template.sourceName (Template.getSourceName())
+  doesn't fall back to the lookup name anymore when it's null (however, Template.getSourceOrLookupName() was added for that). There's
+  no Template.name anymore, because since sourceName was introduced, and hence the concept of template name was split into the
+  lookup and the source name, its meaning wasn't clean (but it meant the lookup name). TemplateException and ParseException 
+  now also have the same properites: getTemplateSourceName(), getTemplateLookupName(), and even getSourceOrLookupName().
+  Location information in error messages show getTemplateSourceOrLookupName().
+- Configuration.setSharedVaribles (not the typo in it) was renamed to setSharedVariables
+- Configuration is now immutable. Instead, you should use Configuration.Builder to set up the setting values, then create
+  the Configuration with the builder's build() method. Further notes:
+  - Most of the mutator methods (like setters) were moved from Configuration to Configuration.Builder. However,
+    setClassForTemplateLoader, setDirectoryForTemplateLoading and the like were removed, instead there's just
+    setTemplateLoader. So for example. instead of  setClassForTemplateLoader(Foo.class, "templates") now you have
+    to write setTemplateLoader(new ClassTemplateLoader(Foo.class, "templates")). While it's a bit longer, it shows
+    more clearly what's happening, and always supports all TemplateLoader constructor overloads.
+  - It's now possible to change the Configuration setting defaults by using a custom Configuration.ExtendableBuilder
+    subclass instead of Configuration.Builder (which is also an ExtendableBuilder subclass). FreemarkerServlet has
+    switched to this approach, using its own builder subclass to provide defaults that makes the sense in that particular
+    application. Its API-s which were used for customizing FreemarkerServlet has bean changed accordingly.
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/manual/en_US/book.xml
----------------------------------------------------------------------
diff --git a/freemarker-core/src/manual/en_US/book.xml b/freemarker-core/src/manual/en_US/book.xml
new file mode 100644
index 0000000..72bf6bc
--- /dev/null
+++ b/freemarker-core/src/manual/en_US/book.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<book conformance="docgen" version="5.0" xml:lang="en"
+      xmlns="http://docbook.org/ns/docbook"
+      xmlns:xlink="http://www.w3.org/1999/xlink"
+      xmlns:xi="http://www.w3.org/2001/XInclude"
+      xmlns:ns5="http://www.w3.org/1999/xhtml"
+      xmlns:ns4="http://www.w3.org/2000/svg"
+      xmlns:ns3="http://www.w3.org/1998/Math/MathML"
+      xmlns:ns="http://docbook.org/ns/docbook">
+  <info>
+    <title>Apache FreeMarker Manual</title>
+
+    <titleabbrev>Manual</titleabbrev>
+
+    <productname>Freemarker 3.0.0</productname>
+  </info>
+
+  <preface role="index.html" xml:id="preface">
+    <title>TODO</title>
+
+    <para>TODO... Eventually, we might copy the FM2 Manual and rework
+    it.</para>
+
+    <para>Anchors to satisfy Docgen:</para>
+
+    <itemizedlist>
+      <listitem>
+        <para xml:id="app_versions">app_versions</para>
+      </listitem>
+
+      <listitem>
+        <para xml:id="app_license">app_license</para>
+      </listitem>
+
+      <listitem>
+        <para xml:id="exp_cheatsheet">exp_cheatsheet</para>
+      </listitem>
+
+      <listitem>
+        <para xml:id="ref_directive_alphaidx">ref_directive_alphaidx</para>
+      </listitem>
+
+      <listitem>
+        <para xml:id="ref_builtins_alphaidx">ref_builtins_alphaidx</para>
+      </listitem>
+
+      <listitem>
+        <para xml:id="ref_specvar">ref_specvar</para>
+      </listitem>
+
+      <listitem>
+        <para xml:id="alphaidx">alphaidx</para>
+      </listitem>
+
+      <listitem>
+        <para xml:id="gloss">gloss</para>
+      </listitem>
+
+      <listitem>
+        <para xml:id="app_faq">app_faq</para>
+      </listitem>
+    </itemizedlist>
+  </preface>
+</book>

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/manual/en_US/docgen-help/editors-readme.txt
----------------------------------------------------------------------
diff --git a/freemarker-core/src/manual/en_US/docgen-help/editors-readme.txt b/freemarker-core/src/manual/en_US/docgen-help/editors-readme.txt
new file mode 100644
index 0000000..24436e8
--- /dev/null
+++ b/freemarker-core/src/manual/en_US/docgen-help/editors-readme.txt
@@ -0,0 +1,130 @@
+/*
+ * 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.
+ */
+ 
+Guide to FreeMarker Manual for Editors
+======================================
+
+Non-technical
+-------------
+
+- The Template Author's Guide is for Web designers. Assume that a
+  designer is not a programmer, (s)he doesn't even know what is Java.
+  Forget that FM is implemented in Java when you edit the Template
+  Author's Guide. Try to avoid technical writing.
+
+- In the Guide chapters, be careful not to mention things that were
+  not explained earlier. The Guide chapters should be understandable
+  if you read them continuously.
+
+- If you add a new topic or term, don't forget to add it to the Index.
+  Also, consider adding entries for it to the Glossary.
+
+- Don't use too sophisticated English. Use basic words and grammar.
+
+
+Technical
+---------
+
+- For the editing use XXE (XMLmind XML Editor), with its default XML
+  *source* formatting settings (identation, max line length and like).
+  You should install the "DocBook 5 for Freemarker" addon, which you can
+  find inside the "docgen" top-level SVN module.
+
+- The HTML is generated with Docgen (docgen.jar), which will check some
+  of the rules described here. To invoke it, issue "ant manual" from
+  the root of the "freemarker" module. (Note: you may need to check out
+  and build "docgen" first.)
+
+- Understand all document conventions in the Preface chapter. Note that
+  all "programlisting"-s should have a "role" attribute with a value that
+  is either: "template", "dataModel", "output", "metaTemplate" or
+  "unspecified". (If you miss this, the XXE addon will show the
+  "programlisting" in red.)
+
+- Verbatim content in flow text:
+
+  * In flow text, all data object names, class names, FTL fragments,
+    HTML fragments, and all other verbatim content is inside "literal"
+    element.
+
+  * Use replaceable element inside literal element for replaceable
+    parts and meta-variables like:
+    <literal&lt;if <replaceable>condition</replaceable>></literal>
+    <literal><replaceable>templateDir</replaceable>/copyright.ftl</literal>
+
+- Hierarchy:
+
+  * The hierarchy should look like:
+
+      book -> part -> chapter -> section -> section -> section -> section
+
+    where the "part" and the "section"-s are optional.
+    Instead of chapter you may have "preface" or "appendix".
+
+  * Don't use "sect1", "sect2", etc. Instead nest "section"-s into each other,
+    but not deeper than 3 levels.
+
+  * Use "simplesect" if you want to divide up something visually, but
+    you don't want those sections to appear in the ToC, or go into their own
+    HTML page. "simplesect"-s can appear under all "section" nesting
+    levels, and they always look the same regardless of the "section"
+    nesting levels.
+
+- Lists:
+
+  * When you have list where the list items are short (a few words),
+    you should give spacing="compact" to the "itemizedlist" or
+    "orderedlist" element.
+
+  * Don't putting listings inside "para"-s. Put them between "para"-s instead.
+
+- Xrefs, id-s, links:
+
+  * id-s of parts, chapters, sections and similar elements must
+    contain US-ASCII lower case letters, US-ASCII numbers, and
+    underscore only. id-s of parts and chapters are used as the
+    filenames of HTML-s generated for that block.
+    When you find out the id, deduce it from the position in the ToC
+    hierarchy. The underscore is used as the separator between the path
+    steps.
+
+  * All other id-s must use prefix:
+    - example: E.g.: id="example.foreach"
+    - ref: Reference information...
+      * directive: about a directive. E.g.: "ref.directive.foreach"
+      * builtin
+    - gloss: Term in the Glossary
+    - topic: The recommended point of document in a certain topic
+      * designer: for designers.
+          E.g.: id="topic.designer.methodDataObject"
+      * programmer: for programmers
+      * or omit the secondary category if it is for everybody
+    - misc: Anything doesn't fit in the above categories
+
+  * When you refer to a part, chapter or section, often you should use
+    xref, not link. The xreflabel attribute of the link-end should not be set;
+    then it's deduced from the titles.
+
+- The "book" element must have this attribute: conformance="docgen"
+
+- It sometimes happens that you want to change some content that you see in
+  the generated output, which you can't find in the DocBook XML. In such case,
+  check the contents docgen.cjson, which should be in the same directory as
+  the XML. If it's not there either, it's perhaps hard-wired into the
+  templates in docgen.

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/manual/en_US/docgen-misc/copyrightComment.txt
----------------------------------------------------------------------
diff --git a/freemarker-core/src/manual/en_US/docgen-misc/copyrightComment.txt b/freemarker-core/src/manual/en_US/docgen-misc/copyrightComment.txt
new file mode 100644
index 0000000..60b675e
--- /dev/null
+++ b/freemarker-core/src/manual/en_US/docgen-misc/copyrightComment.txt
@@ -0,0 +1,16 @@
+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.

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/manual/en_US/docgen-misc/googleAnalytics.html
----------------------------------------------------------------------
diff --git a/freemarker-core/src/manual/en_US/docgen-misc/googleAnalytics.html b/freemarker-core/src/manual/en_US/docgen-misc/googleAnalytics.html
new file mode 100644
index 0000000..759564e
--- /dev/null
+++ b/freemarker-core/src/manual/en_US/docgen-misc/googleAnalytics.html
@@ -0,0 +1,14 @@
+<!--
+  This snippet was generated by Google Analytics.
+  Thus, the standard FreeMarker copyright comment was intentionally omitted.
+  <#DO_NOT_UPDATE_COPYRIGHT>
+-->
+<script>
+  (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
+  (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
+  m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
+  })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
+
+  ga('create', 'UA-55420501-1', 'auto');
+  ga('send', 'pageview');
+</script>



[02/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/IncludeAndImportTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/IncludeAndImportTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/IncludeAndImportTest.java
new file mode 100644
index 0000000..fa767ff
--- /dev/null
+++ b/freemarker-core/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/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/IncudeFromNamelessTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/IncudeFromNamelessTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/IncudeFromNamelessTest.java
new file mode 100644
index 0000000..f4908b8
--- /dev/null
+++ b/freemarker-core/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());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/InterpretAndEvalTemplateNameTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/InterpretAndEvalTemplateNameTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/InterpretAndEvalTemplateNameTest.java
new file mode 100644
index 0000000..ff5897f
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/InterpretAndEvalTemplateNameTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.core.templateresolver.impl.StringTemplateLoader;
+import org.apache.freemarker.test.TemplateTest;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Test;
+
+/**
+ * Test template names returned by special variables and relative path resolution in {@code ?interpret}-ed and
+ * {@code ?eval}-ed parts.  
+ */
+public class InterpretAndEvalTemplateNameTest extends TemplateTest {
+    
+    @Test
+    public void testInterpret() throws IOException, TemplateException {
+        for (String getTemplateNames : new String[] {
+                "c=${.current_template_name}, m=${.main_template_name}",
+                "c=${\".current_template_name\"?eval}, m=${\".main_template_name\"?eval}"
+                }) {
+            StringTemplateLoader tl = new StringTemplateLoader();
+            tl.putTemplate(
+                    "main.ftl",
+                    getTemplateNames + " "
+                    + "{<#include 'sub/t.ftl'>}");
+            tl.putTemplate(
+                    "sub/t.ftl",
+                    getTemplateNames + " "
+                    + "i{<@r'" + getTemplateNames + " {<#include \"a.ftl\">'?interpret />}} "
+                    + "i{<@[r'" + getTemplateNames + " {<#include \"a.ftl\">','named_interpreted']?interpret />}}");
+            tl.putTemplate("sub/a.ftl", "In sub/a.ftl, " + getTemplateNames);
+            tl.putTemplate("a.ftl", "In a.ftl");
+
+            setConfiguration(new TestConfigurationBuilder().templateLoader(tl).build());
+            
+            assertOutputForNamed("main.ftl",
+                    "c=main.ftl, m=main.ftl "
+                    + "{"
+                        + "c=sub/t.ftl, m=main.ftl "
+                        + "i{c=sub/t.ftl->anonymous_interpreted, m=main.ftl {In sub/a.ftl, c=sub/a.ftl, m=main.ftl}} "
+                        + "i{c=sub/t.ftl->named_interpreted, m=main.ftl {In sub/a.ftl, c=sub/a.ftl, m=main.ftl}}"
+                    + "}");
+            
+            assertOutputForNamed("sub/t.ftl",
+                    "c=sub/t.ftl, m=sub/t.ftl "
+                    + "i{c=sub/t.ftl->anonymous_interpreted, m=sub/t.ftl {In sub/a.ftl, c=sub/a.ftl, m=sub/t.ftl}} "
+                    + "i{c=sub/t.ftl->named_interpreted, m=sub/t.ftl {In sub/a.ftl, c=sub/a.ftl, m=sub/t.ftl}}");
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/InterpretSettingInheritanceTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/InterpretSettingInheritanceTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/InterpretSettingInheritanceTest.java
new file mode 100644
index 0000000..2d061d7
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/InterpretSettingInheritanceTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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;
+
+/**
+ * The {@code interpret} built-in must not consider the settings or established auto-detected syntax of the surrounding
+ * template. It can only depend on the {@link Configuration}.
+ */
+public class InterpretSettingInheritanceTest  extends TemplateTest {
+
+    private static final String FTL_A_S_A = "<#ftl><@'[#if true]s[/#if]<#if true>a</#if>'?interpret />";
+    private static final String FTL_A_A_S = "<#ftl><@'<#if true>a</#if>[#if true]s[/#if]'?interpret />";
+    private static final String FTL_S_S_A = "[#ftl][@'[#if true]s[/#if]<#if true>a</#if>'?interpret /]";
+    private static final String FTL_S_A_S = "[#ftl][@'<#if true>a</#if>[#if true]s[/#if]'?interpret /]";
+    private static final String OUT_S_A_WHEN_SYNTAX_IS_S = "s<#if true>a</#if>";
+    private static final String OUT_S_A_WHEN_SYNTAX_IS_A = "[#if true]s[/#if]a";
+    private static final String OUT_A_S_WHEN_SYNTAX_IS_A = "a[#if true]s[/#if]";
+    private static final String OUT_A_S_WHEN_SYNTAX_IS_S = "<#if true>a</#if>s";
+
+    @Test
+    public void tagSyntaxTest() throws IOException, TemplateException {
+        setConfiguration(new TestConfigurationBuilder()
+                .tagSyntax(ParsingConfiguration.ANGLE_BRACKET_TAG_SYNTAX)
+                .build());
+        assertOutput(FTL_S_A_S, OUT_A_S_WHEN_SYNTAX_IS_A);
+        assertOutput(FTL_S_S_A, OUT_S_A_WHEN_SYNTAX_IS_A);
+        assertOutput(FTL_A_A_S, OUT_A_S_WHEN_SYNTAX_IS_A);
+        assertOutput(FTL_A_S_A, OUT_S_A_WHEN_SYNTAX_IS_A);
+
+        setConfiguration(new TestConfigurationBuilder()
+                .tagSyntax(ParsingConfiguration.SQUARE_BRACKET_TAG_SYNTAX)
+                .build());
+        assertOutput(FTL_S_A_S, OUT_A_S_WHEN_SYNTAX_IS_S);
+        assertOutput(FTL_S_S_A, OUT_S_A_WHEN_SYNTAX_IS_S);
+        assertOutput(FTL_A_A_S, OUT_A_S_WHEN_SYNTAX_IS_S);
+        assertOutput(FTL_A_S_A, OUT_S_A_WHEN_SYNTAX_IS_S);
+
+        setConfiguration(new TestConfigurationBuilder()
+                .tagSyntax(ParsingConfiguration.AUTO_DETECT_TAG_SYNTAX)
+                .build());
+        assertOutput(FTL_S_A_S, OUT_A_S_WHEN_SYNTAX_IS_A);
+        assertOutput(FTL_S_S_A, OUT_S_A_WHEN_SYNTAX_IS_S);
+        assertOutput(FTL_A_A_S, OUT_A_S_WHEN_SYNTAX_IS_A);
+        assertOutput(FTL_A_S_A, OUT_S_A_WHEN_SYNTAX_IS_S);
+        assertOutput("<@'[#ftl]x'?interpret />[#if true]y[/#if]", "x[#if true]y[/#if]");
+    }
+
+    @Test
+    public void whitespaceStrippingTest() throws IOException, TemplateException {
+        Configuration cfg = getConfiguration();
+
+        setConfiguration(new TestConfigurationBuilder()
+                .whitespaceStripping(true)
+                .build());
+        assertOutput("<#assign x = 1>\nX<@'<#assign x = 1>\\nY'?interpret />", "XY");
+        assertOutput("<#ftl stripWhitespace=false><#assign x = 1>\nX<@'<#assign x = 1>\\nY'?interpret />", "\nXY");
+        assertOutput("<#assign x = 1>\nX<@'<#ftl stripWhitespace=false><#assign x = 1>\\nY'?interpret />", "X\nY");
+
+        setConfiguration(new TestConfigurationBuilder()
+                .whitespaceStripping(false)
+                .build());
+        assertOutput("<#assign x = 1>\nX<@'<#assign x = 1>\\nY'?interpret />", "\nX\nY");
+        assertOutput("<#ftl stripWhitespace=true><#assign x = 1>\nX<@'<#assign x = 1>\\nY'?interpret />", "X\nY");
+        assertOutput("<#assign x = 1>\nX<@'<#ftl stripWhitespace=true><#assign x = 1>\\nY'?interpret />", "\nXY");
+    }
+
+    @Test
+    public void evalTest() throws IOException, TemplateException {
+        setConfiguration(new TestConfigurationBuilder()
+                .tagSyntax(ParsingConfiguration.ANGLE_BRACKET_TAG_SYNTAX)
+                .build());
+        assertOutput("<@'\"[#if true]s[/#if]<#if true>a</#if>\"?interpret'?eval />", OUT_S_A_WHEN_SYNTAX_IS_A);
+        assertOutput("[#ftl][@'\"[#if true]s[/#if]<#if true>a</#if>\"?interpret'?eval /]", OUT_S_A_WHEN_SYNTAX_IS_A);
+
+        setConfiguration(new TestConfigurationBuilder()
+                .tagSyntax(ParsingConfiguration.SQUARE_BRACKET_TAG_SYNTAX)
+                .build());
+        assertOutput("[@'\"[#if true]s[/#if]<#if true>a</#if>\"?interpret'?eval /]", OUT_S_A_WHEN_SYNTAX_IS_S);
+        assertOutput("<#ftl><@'\"[#if true]s[/#if]<#if true>a</#if>\"?interpret'?eval />", OUT_S_A_WHEN_SYNTAX_IS_S);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/IteratorIssuesTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/IteratorIssuesTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/IteratorIssuesTest.java
new file mode 100644
index 0000000..08fcee2
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/IteratorIssuesTest.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 java.util.Arrays;
+import java.util.Iterator;
+
+import org.apache.freemarker.core.model.impl.DefaultObjectWrapper;
+import org.apache.freemarker.test.TemplateTest;
+import org.junit.Test;
+
+public class IteratorIssuesTest extends TemplateTest {
+
+    private static final DefaultObjectWrapper OW = new DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
+
+    private static final String FTL_HAS_CONTENT_AND_LIST
+            = "<#if it?hasContent><#list it as i>${i}</#list><#else>empty</#if>";
+    private static final String OUT_HAS_CONTENT_AND_LIST_ABC = "abc";
+    private static final String OUT_HAS_CONTENT_AND_LIST_EMPTY = "empty";
+
+    private static final String FTL_LIST_AND_HAS_CONTENT
+            = "<#list it as i>${i}${it?hasContent?then('+', '-')}</#list>";
+    private static final String OUT_LIST_AND_HAS_CONTENT_BW_GOOD = "a+b+c-";
+
+    @Test
+    public void testHasContentAndList() throws Exception {
+        addToDataModel("it", OW.wrap(getAbcIt()));
+        assertOutput(FTL_HAS_CONTENT_AND_LIST, OUT_HAS_CONTENT_AND_LIST_ABC);
+
+        addToDataModel("it", OW.wrap(getEmptyIt()));
+        assertOutput(FTL_HAS_CONTENT_AND_LIST, OUT_HAS_CONTENT_AND_LIST_EMPTY);
+    }
+
+    @Test
+    public void testListAndHasContent() throws Exception {
+        addToDataModel("it", OW.wrap(getAbcIt()));
+        assertErrorContains(FTL_LIST_AND_HAS_CONTENT, "can be listed only once");
+    }
+
+    private Iterator getAbcIt() {
+        return Arrays.asList(new String[] { "a", "b", "c" }).iterator();
+    }
+
+    private Iterator getEmptyIt() {
+        return Arrays.asList(new String[] {  }).iterator();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/JavaCCExceptionAsEOFFixTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/JavaCCExceptionAsEOFFixTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/JavaCCExceptionAsEOFFixTest.java
new file mode 100644
index 0000000..0fa3f79
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/JavaCCExceptionAsEOFFixTest.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;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.Reader;
+
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * JavaCC suppresses exceptions thrown by the Reader, silently treating them as EOF. To be precise, JavaCC 3.2 only does
+ * that with {@link IOException}-s, while JavaCC 6 does that for all {@link Exception}-s. This tests FreeMarker's
+ * workaround for this problem.
+ */
+public class JavaCCExceptionAsEOFFixTest {
+
+    public static class FailingReader extends Reader {
+
+        private static final String CONTENT = "abc";
+
+        private final Throwable exceptionToThrow;
+        private int readSoFar;
+
+        protected FailingReader(Throwable exceptionToThrow) {
+            this.exceptionToThrow = exceptionToThrow;
+        }
+
+        @Override
+        public int read() throws IOException {
+            if (readSoFar == CONTENT.length()) {
+                if (exceptionToThrow != null) {
+                    throwException();
+                } else {
+                    return -1;
+                }
+            }
+            return CONTENT.charAt(readSoFar++);
+        }
+
+        private void throwException() throws IOException {
+            if (exceptionToThrow instanceof IOException) {
+                throw (IOException) exceptionToThrow;
+            }
+            if (exceptionToThrow instanceof RuntimeException) {
+                throw (RuntimeException) exceptionToThrow;
+            }
+            if (exceptionToThrow instanceof Error) {
+                throw (Error) exceptionToThrow;
+            }
+            Assert.fail();
+        }
+
+        @Override
+        public void close() throws IOException {
+            // nop
+        }
+
+        @Override
+        public int read(char[] cbuf, int off, int len) throws IOException {
+            for (int i = 0; i < len; i++) {
+                int c = read();
+                if (c == -1) return i == 0 ? -1 : i;
+                cbuf[off + i] = (char) c;
+            }
+            return len;
+        }
+
+    }
+
+    @Test
+    public void testIOException() throws IOException {
+        try {
+            new Template(null, new FailingReader(new IOException("test")), new TestConfigurationBuilder().build());
+            fail();
+        } catch (IOException e) {
+            assertEquals("test", e.getMessage());
+        }
+    }
+
+    @Test
+    public void testRuntimeException() throws IOException {
+        try {
+            new Template(null, new FailingReader(new NullPointerException("test")), new TestConfigurationBuilder().build());
+            fail();
+        } catch (NullPointerException e) {
+            assertEquals("test", e.getMessage());
+        }
+    }
+
+    @Test
+    public void testError() throws IOException {
+        try {
+            new Template(null, new FailingReader(new OutOfMemoryError("test")), new TestConfigurationBuilder().build());
+            fail();
+        } catch (OutOfMemoryError e) {
+            assertEquals("test", e.getMessage());
+        }
+    }
+
+    @Test
+    public void testNoException() throws IOException {
+        Template t = new Template(null, new FailingReader(null), new TestConfigurationBuilder().build());
+        assertEquals("abc", t.toString());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/ListErrorsTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/ListErrorsTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/ListErrorsTest.java
new file mode 100644
index 0000000..05bac4f
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/ListErrorsTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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.core.model.impl.DefaultObjectWrapper;
+import org.apache.freemarker.test.TemplateTest;
+import org.apache.freemarker.test.templatesuite.models.Listables;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableMap;
+
+public class ListErrorsTest extends TemplateTest {
+    
+    @Test
+    public void testValid() throws IOException, TemplateException {
+        assertOutput("<#list 1..2 as x><#list 3..4>${x}:<#items as x>${x}</#items></#list>;</#list>", "1:34;2:34;");
+        assertOutput("<#list [] as x>${x}<#else><#list 1..2 as x>${x}<#sep>, </#list></#list>", "1, 2");
+        assertOutput("<#macro m>[<#nested 3>]</#macro>"
+                + "<#list 1..2 as x>"
+                + "${x}@${x?index}"
+                + "<@m ; x>"
+                + "${x},"
+                + "<#list 4..4 as x>${x}@${x?index}</#list>"
+                + "</@>"
+                + "${x}@${x?index}; "
+                + "</#list>",
+                "1@0[3,4@0]1@0; 2@1[3,4@0]2@1; ");
+    }
+
+    @Test
+    public void testInvalidItemsParseTime() throws IOException, TemplateException {
+        assertErrorContains("<#items as x>${x}</#items>",
+                "#items", "must be inside", "#list");
+        assertErrorContains("<#list xs><#macro m><#items as x></#items></#macro></#list>",
+                "#items", "must be inside", "#list");
+        assertErrorContains("<#list xs as x><#items as x>${x}</#items></#list>",
+                "#list", "must not have", "#items", "as loopVar");
+        assertErrorContains("<#list xs><#list xs as x><#items as x>${x}</#items></#list></#list>",
+                "#list", "must not have", "#items", "as loopVar");
+        assertErrorContains("<#list xs></#list>",
+                "#list", "must have", "#items", "as loopVar");
+    }
+
+    @Test
+    public void testInvalidSepParseTime() throws IOException, TemplateException {
+        assertErrorContains("<#sep>, </#sep>",
+                "#sep", "must be inside", "#list");
+        assertErrorContains("<#sep>, ",
+                "#sep", "must be inside", "#list");
+        assertErrorContains("<#list xs as x><#else><#sep>, </#list>",
+                "#sep", "must be inside", "#list");
+        assertErrorContains("<#list xs as x><#macro m><#sep>, </#macro></#list>",
+                "#sep", "must be inside", "#list");
+    }
+
+    @Test
+    public void testInvalidItemsRuntime() throws IOException, TemplateException {
+        assertErrorContains("<#list 1..1><#items as x></#items><#items as x></#items></#list>",
+                "#items", "already entered earlier");
+        assertErrorContains("<#list 1..1><#items as x><#items as y>${x}/${y}</#items></#items></#list>",
+                "#items", "Can't nest #items into each other");
+    }
+    
+    @Test
+    public void testInvalidLoopVarBuiltinLHO() {
+        assertErrorContains("<#list foos>${foo?index}</#list>",
+                "?index", "foo", "no loop variable");
+        assertErrorContains("<#list foos as foo></#list>${foo?index}",
+                "?index", "foo" , "no loop variable");
+        assertErrorContains("<#list foos as foo><#macro m>${foo?index}</#macro></#list>",
+                "?index", "foo" , "no loop variable");
+        assertErrorContains("<#list foos as foo><#function f>${foo?index}</#function></#list>",
+                "?index", "foo" , "no loop variable");
+        assertErrorContains("<#list xs as x>${foo?index}</#list>",
+                "?index", "foo" , "no loop variable");
+        assertErrorContains("<#list foos as foo><@m; foo>${foo?index}</@></#list>",
+                "?index", "foo" , "user defined directive");
+        assertErrorContains(
+                "<#list foos as foo><@m; foo><@m; foo>${foo?index}</@></@></#list>",
+                "?index", "foo" , "user defined directive");
+        assertErrorContains(
+                "<#list foos as foo><@m; foo>"
+                + "<#list foos as foo><@m; foo>${foo?index}</@></#list>"
+                + "</@></#list>",
+                "?index", "foo" , "user defined directive");
+    }
+
+    @Test
+    public void testKeyValueSameName() {
+        assertErrorContains("<#list {} as foo, foo></#list>",
+                "key", "value", "both" , "foo");
+    }
+
+    @Test
+    public void testCollectionVersusHash() {
+        assertErrorContains("<#list {} as i></#list>",
+                "as k, v");
+        assertErrorContains("<#list [] as k, v></#list>",
+                "only one loop variable");
+    }
+
+    @Test
+    public void testNonEx2NonStringKey() throws IOException, TemplateException {
+        addToDataModel("m", new Listables.NonEx2MapAdapter(ImmutableMap.of("k1", "v1", 2, "v2"),
+                new DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0).build()));
+        assertOutput("<#list m?keys as k>${k};</#list>", "k1;2;");
+        assertErrorContains("<#list m as k, v></#list>",
+                "string", "number", ".TemplateHashModelEx2");
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/MiscErrorMessagesTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/MiscErrorMessagesTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/MiscErrorMessagesTest.java
new file mode 100644
index 0000000..1903e05
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/MiscErrorMessagesTest.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateNameFormat;
+import org.apache.freemarker.test.TemplateTest;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Test;
+
+public class MiscErrorMessagesTest extends TemplateTest {
+
+    @Test
+    public void stringIndexOutOfBounds() {
+        assertErrorContains("${'foo'[10]}", "length", "3", "10", "String index out of");
+    }
+    
+    @Test
+    public void wrongTemplateNameFormat() {
+        setConfiguration(new TestConfigurationBuilder().templateNameFormat(DefaultTemplateNameFormat.INSTANCE).build());
+
+        assertErrorContains("<#include 'foo:/bar:baaz'>", "Malformed template name", "':'");
+        assertErrorContains("<#include '../baaz'>", "Malformed template name", "root");
+        assertErrorContains("<#include '\u0000'>", "Malformed template name", "\\u0000");
+    }
+
+    @Test
+    public void numericalKeyHint() {
+        assertErrorContains("${{}[10]}", "[]", "?api");
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/MistakenlyPublicImportAPIsTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/MistakenlyPublicImportAPIsTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/MistakenlyPublicImportAPIsTest.java
new file mode 100644
index 0000000..5fcee97
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/MistakenlyPublicImportAPIsTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.io.StringWriter;
+import java.util.List;
+
+import org.apache.freemarker.core.Environment.Namespace;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.templateresolver.impl.StringTemplateLoader;
+import org.apache.freemarker.core.util._NullWriter;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Test;
+
+/**
+ * These are things that users shouldn't do, but we shouldn't break backward compatibility without knowing about it.
+ */
+public class MistakenlyPublicImportAPIsTest {
+
+    @Test
+    public void testImportCopying() throws IOException, TemplateException {
+        StringTemplateLoader tl = new StringTemplateLoader();
+        tl.putTemplate("imp1", "<#macro m>1</#macro>");
+        tl.putTemplate("imp2", "<#assign x = 2><#macro m>${x}</#macro>");
+
+        Configuration cfg = new TestConfigurationBuilder().templateLoader(tl).build();
+        
+        Template t1 = new Template(null, "<#import 'imp1' as i1><#import 'imp2' as i2>", cfg);
+        List<ASTDirImport> imports = t1.getImports();
+        assertEquals(2, imports.size());
+        
+        {
+            Template t2 = new Template(null, "<@i1.m/><@i2.m/>", cfg);
+            for (ASTDirImport libLoad : imports) {
+                t2.addImport(libLoad);
+            }
+            
+            try {
+                t2.process(null, _NullWriter.INSTANCE);
+                fail();
+            } catch (InvalidReferenceException e) {
+                // Apparenly, it has never worked like this...
+                assertEquals("i1", e.getBlamedExpressionString());
+            }
+        }
+        
+        // It works this way, though it has nothing to do with the problematic API-s: 
+        Environment env = t1.createProcessingEnvironment(null, _NullWriter.INSTANCE);
+        env.process();
+        TemplateModel i1 = env.getVariable("i1");
+        assertThat(i1, instanceOf(Namespace.class));
+        TemplateModel i2 = env.getVariable("i2");
+        assertThat(i2, instanceOf(Namespace.class));
+
+        {
+            Template t2 = new Template(null, "<@i1.m/>", cfg);
+            
+            StringWriter sw = new StringWriter();
+            env = t2.createProcessingEnvironment(null, sw);
+            env.setVariable("i1", i1);
+            
+            env.process();
+            assertEquals("1", sw.toString());
+        }
+
+        {
+            Template t2 = new Template(null, "<@i2.m/>", cfg);
+            
+            StringWriter sw = new StringWriter();
+            env = t2.createProcessingEnvironment(null, sw);
+            env.setVariable("i2", i2);
+            
+            try {
+                env.process();
+                assertEquals("2", sw.toString());
+            } catch (NullPointerException e) {
+                // Expected on 2.3.x, because it won't find the namespace for the macro
+                // [2.4] Fix this "bug"
+            }
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/MistakenlyPublicMacroAPIsTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/MistakenlyPublicMacroAPIsTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/MistakenlyPublicMacroAPIsTest.java
new file mode 100644
index 0000000..9c87e61
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/MistakenlyPublicMacroAPIsTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.io.StringWriter;
+import java.util.Map;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.util._NullWriter;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Test;
+
+/**
+ * These are things that users shouldn't do, but we shouldn't break backward compatibility without knowing about it.
+ */
+public class MistakenlyPublicMacroAPIsTest {
+
+    private final Configuration cfg = new TestConfigurationBuilder().build();
+    
+    /**
+     * Getting the macros from one template, and adding them to another.
+     */
+    @Test
+    public void testMacroCopyingExploit() throws IOException, TemplateException {
+        Template tMacros = new Template(null, "<#macro m1>1</#macro><#macro m2>2</#macro>", cfg);
+        Map<String, ASTDirMacro> macros = tMacros.getMacros();
+        
+        Template t = new Template(null,
+                "<@m1/><@m2/><@m3/>"
+                + "<#macro m1>1b</#macro><#macro m3>3b</#macro> "
+                + "<@m1/><@m2/><@m3/>", cfg);
+        t.addMacro(macros.get("m1"));
+        t.addMacro(macros.get("m2"));
+        
+        assertEquals("123b 1b23b", getTemplateOutput(t));
+    }
+
+    @Test
+    public void testMacroCopyingExploitAndNamespaces() throws IOException, TemplateException {
+        Template tMacros = new Template(null, "<#assign x = 0><#macro m1>${x}</#macro>", cfg);
+        Template t = new Template(null, "<#assign x = 1><@m1/>", cfg);
+        t.addMacro((ASTDirMacro) tMacros.getMacros().get("m1"));
+        
+        assertEquals("1", getTemplateOutput(t));
+    }
+
+    @Test
+    public void testMacroCopyingFromFTLVariable() throws IOException, TemplateException {
+        Template tMacros = new Template(null, "<#assign x = 0><#macro m1>${x}</#macro>", cfg);
+        Environment env = tMacros.createProcessingEnvironment(null, _NullWriter.INSTANCE);
+        env.process();
+        TemplateModel m1 = env.getVariable("m1");
+        assertThat(m1, instanceOf(ASTDirMacro.class));
+        
+        Template t = new Template(null, "<#assign x = 1><@m1/>", cfg);
+        t.addMacro((ASTDirMacro) m1);
+        
+        assertEquals("1", getTemplateOutput(t));
+    }
+    
+    private String getTemplateOutput(Template t) throws TemplateException, IOException {
+        StringWriter sw = new StringWriter();
+        t.process(null, sw);
+        return sw.toString();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/NewBiObjectWrapperRestrictionTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/NewBiObjectWrapperRestrictionTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/NewBiObjectWrapperRestrictionTest.java
new file mode 100644
index 0000000..d865deb
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/NewBiObjectWrapperRestrictionTest.java
@@ -0,0 +1,50 @@
+/*
+ * 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.apache.freemarker.test.util.EntirelyCustomObjectWrapper;
+import org.junit.Test;
+
+public class NewBiObjectWrapperRestrictionTest extends TemplateTest {
+
+    @Override
+    protected Configuration createDefaultConfiguration() throws Exception {
+        return new TestConfigurationBuilder().objectWrapper(new EntirelyCustomObjectWrapper()).build();
+    }
+
+    @Test
+    public void testPositive() throws IOException, TemplateException {
+        assertOutput(
+                "${'org.apache.freemarker.test.templatesuite.models.NewTestModel'?new()}",
+                "default constructor");
+    }
+
+    @Test
+    public void testNegative() {
+        assertErrorContains(
+                "${'org.apache.freemarker.test.templatesuite.models.NewTestModel'?new('s')}",
+                "only supports 0 argument");
+    }
+
+}


[19/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/PrimtiveArrayBackedReadOnlyList.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/PrimtiveArrayBackedReadOnlyList.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/PrimtiveArrayBackedReadOnlyList.java
new file mode 100644
index 0000000..0cf7d80
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/PrimtiveArrayBackedReadOnlyList.java
@@ -0,0 +1,47 @@
+/*
+ * 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.lang.reflect.Array;
+import java.util.AbstractList;
+
+/**
+ * Similar to {@link NonPrimitiveArrayBackedReadOnlyList}, but uses reflection so that it works with primitive arrays
+ * too. 
+ */
+class PrimtiveArrayBackedReadOnlyList extends AbstractList {
+    
+    private final Object array;
+    
+    PrimtiveArrayBackedReadOnlyList(Object array) {
+        this.array = array;
+    }
+
+    @Override
+    public Object get(int index) {
+        return Array.get(array, index);
+    }
+
+    @Override
+    public int size() {
+        return Array.getLength(array);
+    }
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ReflectionCallableMemberDescriptor.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ReflectionCallableMemberDescriptor.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ReflectionCallableMemberDescriptor.java
new file mode 100644
index 0000000..eb2ade5
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ReflectionCallableMemberDescriptor.java
@@ -0,0 +1,95 @@
+/*
+ * 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.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Member;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+
+/**
+ * The most commonly used {@link CallableMemberDescriptor} implementation. 
+ */
+final class ReflectionCallableMemberDescriptor extends CallableMemberDescriptor {
+
+    private final Member/*Method|Constructor*/ member;
+    
+    /**
+     * Don't modify this array!
+     */
+    final Class[] paramTypes;
+    
+    ReflectionCallableMemberDescriptor(Method member, Class[] paramTypes) {
+        this.member = member;
+        this.paramTypes = paramTypes;
+    }
+
+    ReflectionCallableMemberDescriptor(Constructor member, Class[] paramTypes) {
+        this.member = member;
+        this.paramTypes = paramTypes;
+    }
+
+    @Override
+    TemplateModel invokeMethod(DefaultObjectWrapper ow, Object obj, Object[] args)
+            throws TemplateModelException, InvocationTargetException, IllegalAccessException {
+        return ow.invokeMethod(obj, (Method) member, args);
+    }
+
+    @Override
+    Object invokeConstructor(DefaultObjectWrapper ow, Object[] args)
+            throws IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException {
+        return ((Constructor) member).newInstance(args);
+    }
+
+    @Override
+    String getDeclaration() {
+        return _MethodUtil.toString(member);
+    }
+    
+    @Override
+    boolean isConstructor() {
+        return member instanceof Constructor;
+    }
+    
+    @Override
+    boolean isStatic() {
+        return (member.getModifiers() & Modifier.STATIC) != 0;
+    }
+
+    @Override
+    boolean isVarargs() {
+        return _MethodUtil.isVarargs(member);
+    }
+
+    @Override
+    Class[] getParamTypes() {
+        return paramTypes;
+    }
+
+    @Override
+    String getName() {
+        return member.getName();
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ResourceBundleModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ResourceBundleModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ResourceBundleModel.java
new file mode 100644
index 0000000..31af451
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ResourceBundleModel.java
@@ -0,0 +1,181 @@
+/*
+ * 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.text.MessageFormat;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.MissingResourceException;
+import java.util.ResourceBundle;
+import java.util.Set;
+
+import org.apache.freemarker.core._DelayedJQuote;
+import org.apache.freemarker.core._TemplateModelException;
+import org.apache.freemarker.core.model.TemplateMethodModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+
+/**
+ * <p>A hash model that wraps a resource bundle. Makes it convenient to store
+ * localized content in the data model. It also acts as a method model that will
+ * take a resource key and arbitrary number of arguments and will apply
+ * {@link MessageFormat} with arguments on the string represented by the key.</p>
+ *
+ * <p>Typical usages:</p>
+ * <ul>
+ * <li><tt>bundle.resourceKey</tt> will retrieve the object from resource bundle
+ * with key <tt>resourceKey</tt></li>
+ * <li><tt>bundle("patternKey", arg1, arg2, arg3)</tt> will retrieve the string
+ * from resource bundle with key <tt>patternKey</tt>, and will use it as a pattern
+ * for MessageFormat with arguments arg1, arg2 and arg3</li>
+ * </ul>
+ */
+public class ResourceBundleModel
+    extends
+    BeanModel
+    implements
+    TemplateMethodModelEx {
+
+    private Hashtable formats = null;
+
+    public ResourceBundleModel(ResourceBundle bundle, DefaultObjectWrapper wrapper) {
+        super(bundle, wrapper);
+    }
+
+    /**
+     * Overridden to invoke the get method of the resource bundle.
+     */
+    @Override
+    protected TemplateModel invokeGenericGet(Map keyMap, Class clazz, String key)
+    throws TemplateModelException {
+        try {
+            return wrap(((ResourceBundle) object).getObject(key));
+        } catch (MissingResourceException e) {
+            throw new _TemplateModelException(e,
+                    "No ", new _DelayedJQuote(key), " key in the ResourceBundle. "
+                    + "Note that conforming to the ResourceBundle Java API, this is an error and not just "
+                    + "a missing sub-variable (a null).");
+        }
+    }
+
+    /**
+     * Returns true if this bundle contains no objects.
+     */
+    @Override
+    public boolean isEmpty() {
+        return !((ResourceBundle) object).getKeys().hasMoreElements() &&
+            super.isEmpty();
+    }
+
+    @Override
+    public int size() {
+        return keySet().size();
+    }
+
+    @Override
+    protected Set keySet() {
+        Set set = super.keySet();
+        Enumeration e = ((ResourceBundle) object).getKeys();
+        while (e.hasMoreElements()) {
+            set.add(e.nextElement());
+        }
+        return set;
+    }
+
+    /**
+     * Takes first argument as a resource key, looks up a string in resource bundle
+     * with this key, then applies a MessageFormat.format on the string with the
+     * rest of the arguments. The created MessageFormats are cached for later reuse.
+     */
+    @Override
+    public Object exec(List arguments)
+        throws TemplateModelException {
+        // Must have at least one argument - the key
+        if (arguments.size() < 1)
+            throw new TemplateModelException("No message key was specified");
+        // Read it
+        Iterator it = arguments.iterator();
+        String key = unwrap((TemplateModel) it.next()).toString();
+        try {
+            if (!it.hasNext()) {
+                return wrap(((ResourceBundle) object).getObject(key));
+            }
+    
+            // Copy remaining arguments into an Object[]
+            int args = arguments.size() - 1;
+            Object[] params = new Object[args];
+            for (int i = 0; i < args; ++i)
+                params[i] = unwrap((TemplateModel) it.next());
+    
+            // Invoke format
+            return new BeanAndStringModel(format(key, params), wrapper);
+        } catch (MissingResourceException e) {
+            throw new TemplateModelException("No such key: " + key);
+        } catch (Exception e) {
+            throw new TemplateModelException(e.getMessage());
+        }
+    }
+
+    /**
+     * Provides direct access to caching format engine from code (instead of from script).
+     */
+    public String format(String key, Object[] params)
+        throws MissingResourceException {
+        // Check to see if we already have a cache for message formats
+        // and construct it if we don't
+        // NOTE: this block statement should be synchronized. However
+        // concurrent creation of two caches will have no harmful
+        // consequences, and we avoid a performance hit.
+        /* synchronized(this) */
+        {
+            if (formats == null)
+                formats = new Hashtable();
+        }
+
+        MessageFormat format = null;
+        // Check to see if we already have a requested MessageFormat cached
+        // and construct it if we don't
+        // NOTE: this block statement should be synchronized. However
+        // concurrent creation of two formats will have no harmful
+        // consequences, and we avoid a performance hit.
+        /* synchronized(formats) */
+        {
+            format = (MessageFormat) formats.get(key);
+            if (format == null) {
+                format = new MessageFormat(((ResourceBundle) object).getString(key));
+                format.setLocale(getBundle().getLocale());
+                formats.put(key, format);
+            }
+        }
+
+        // Perform the formatting. We synchronize on it in case it
+        // contains date formatting, which is not thread-safe.
+        synchronized (format) {
+            return format.format(params);
+        }
+    }
+
+    public ResourceBundle getBundle() {
+        return (ResourceBundle) object;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/RestrictedObjectWrapper.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/RestrictedObjectWrapper.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/RestrictedObjectWrapper.java
new file mode 100644
index 0000000..e456dc6
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/RestrictedObjectWrapper.java
@@ -0,0 +1,98 @@
+/*
+ * 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.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+import org.apache.freemarker.core.Version;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+
+/**
+ * A restricted version of {@link DefaultObjectWrapper} that doesn't expose arbitrary object, just those that directly
+ * correspond to the {@link TemplateModel} sub-interfaces ({@code String}, {@code Map} and such). If it had to wrap
+ * other kind of objects, it will throw exception. It will also block {@code ?api} calls on the values it wraps.
+ */
+public class RestrictedObjectWrapper extends DefaultObjectWrapper {
+
+    protected RestrictedObjectWrapper(Builder builder, boolean finalizeConstruction) {
+        super(builder, finalizeConstruction);
+    }
+
+    /**
+     * Called if a type other than the simple ones we know about is passed in. 
+     * In this implementation, this just throws an exception.
+     */
+    @Override
+    protected TemplateModel handleNonBasicTypes(Object obj) throws TemplateModelException {
+        throw new TemplateModelException("RestrictedObjectWrapper deliberately won't wrap this type: "
+                + obj.getClass().getName());
+    }
+
+    @Override
+    public TemplateHashModel wrapAsAPI(Object obj) throws TemplateModelException {
+        throw new TemplateModelException("RestrictedObjectWrapper deliberately doesn't allow ?api.");
+    }
+
+    protected static abstract class ExtendableBuilder<
+            ProductT extends RestrictedObjectWrapper, SelfT extends ExtendableBuilder<ProductT,
+            SelfT>> extends DefaultObjectWrapper.ExtendableBuilder<ProductT, SelfT> {
+
+        protected ExtendableBuilder(Version incompatibleImprovements, boolean isIncompImprsAlreadyNormalized) {
+            super(incompatibleImprovements, isIncompImprsAlreadyNormalized);
+        }
+
+    }
+
+    public static final class Builder extends ExtendableBuilder<RestrictedObjectWrapper, Builder> {
+
+        private final static Map<ClassLoader, Map<Builder, WeakReference<RestrictedObjectWrapper>>>
+                INSTANCE_CACHE = new WeakHashMap<>();
+
+        private final static ReferenceQueue<RestrictedObjectWrapper> INSTANCE_CACHE_REF_QUEUE = new ReferenceQueue<>();
+
+        public Builder(Version incompatibleImprovements) {
+            super(incompatibleImprovements, false);
+        }
+
+        @Override
+        public RestrictedObjectWrapper build() {
+            return DefaultObjectWrapperTCCLSingletonUtil.getSingleton(
+                    this, INSTANCE_CACHE, INSTANCE_CACHE_REF_QUEUE, ConstructorInvoker.INSTANCE);
+        }
+
+        private static class ConstructorInvoker
+                implements DefaultObjectWrapperTCCLSingletonUtil._ConstructorInvoker<RestrictedObjectWrapper, Builder> {
+
+            private static final ConstructorInvoker INSTANCE = new ConstructorInvoker();
+
+            @Override
+            public RestrictedObjectWrapper invoke(Builder builder) {
+                return new RestrictedObjectWrapper(builder, true);
+            }
+        }
+
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SequenceAdapter.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SequenceAdapter.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SequenceAdapter.java
new file mode 100644
index 0000000..e9b6156
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SequenceAdapter.java
@@ -0,0 +1,68 @@
+/*
+ * 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.util.AbstractList;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelAdapter;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.util.UndeclaredThrowableException;
+
+/**
+ */
+class SequenceAdapter extends AbstractList implements TemplateModelAdapter {
+    private final DefaultObjectWrapper wrapper;
+    private final TemplateSequenceModel model;
+    
+    SequenceAdapter(TemplateSequenceModel model, DefaultObjectWrapper wrapper) {
+        this.model = model;
+        this.wrapper = wrapper;
+    }
+    
+    @Override
+    public TemplateModel getTemplateModel() {
+        return model;
+    }
+    
+    @Override
+    public int size() {
+        try {
+            return model.size();
+        } catch (TemplateModelException e) {
+            throw new UndeclaredThrowableException(e);
+        }
+    }
+    
+    @Override
+    public Object get(int index) {
+        try {
+            return wrapper.unwrap(model.get(index));
+        } catch (TemplateModelException e) {
+            throw new UndeclaredThrowableException(e);
+        }
+    }
+    
+    public TemplateSequenceModel getTemplateSequenceModel() {
+        return model;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SetAdapter.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SetAdapter.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SetAdapter.java
new file mode 100644
index 0000000..0975ac4
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SetAdapter.java
@@ -0,0 +1,32 @@
+/*
+ * 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.util.Set;
+
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+
+/**
+ */
+class SetAdapter extends CollectionAdapter implements Set {
+    SetAdapter(TemplateCollectionModel model, DefaultObjectWrapper wrapper) {
+        super(model, wrapper);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SimpleCollection.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SimpleCollection.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SimpleCollection.java
new file mode 100644
index 0000000..cf399ab
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SimpleCollection.java
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.model.impl;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Iterator;
+
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+import org.apache.freemarker.core.model.WrappingTemplateModel;
+
+/**
+ * A simple implementation of {@link TemplateCollectionModel}.
+ * It's able to wrap <tt>java.util.Iterator</tt>-s and <tt>java.util.Collection</tt>-s.
+ * If you wrap an <tt>Iterator</tt>, the variable can be &lt;#list&gt;-ed only once!
+ *
+ * <p>Consider using {@link SimpleSequence} instead of this class if you want to wrap <tt>Iterator</tt>s.
+ * <tt>SimpleSequence</tt> will read all elements of the <tt>Iterator</tt>, and store them in a <tt>List</tt>
+ * (this may cause too high resource consumption in some applications), so you can list the variable
+ * for unlimited times. Also, if you want to wrap <tt>Collection</tt>s, and then list the resulting
+ * variable for many times, <tt>SimpleSequence</tt> may gives better performance, as the
+ * wrapping of non-<tt>TemplateModel</tt> objects happens only once.
+ *
+ * <p>This class is thread-safe. The returned {@link TemplateModelIterator}-s
+ * are <em>not</em> thread-safe.
+ */
+public class SimpleCollection extends WrappingTemplateModel
+implements TemplateCollectionModel, Serializable {
+    
+    private boolean iteratorOwned;
+    private final Iterator iterator;
+    private final Collection collection;
+
+    public SimpleCollection(Iterator iterator, ObjectWrapper wrapper) {
+        super(wrapper);
+        this.iterator = iterator;
+        collection = null;
+    }
+
+    public SimpleCollection(Collection collection, ObjectWrapper wrapper) {
+        super(wrapper);
+        this.collection = collection;
+        iterator = null;
+    }
+
+    /**
+     * Retrieves a template model iterator that is used to iterate over the elements in this collection.
+     *  
+     * <p>When you wrap an <tt>Iterator</tt> and you get <tt>TemplateModelIterator</tt> for multiple times,
+     * only one of the returned <tt>TemplateModelIterator</tt> instances can be really used. When you have called a
+     * method of a <tt>TemplateModelIterator</tt> instance, all other instance will throw a
+     * <tt>TemplateModelException</tt> when you try to call their methods, since the wrapped <tt>Iterator</tt>
+     * can't return the first element anymore.
+     */
+    @Override
+    public TemplateModelIterator iterator() {
+        return iterator != null
+                ? new SimpleTemplateModelIterator(iterator, false)
+                : new SimpleTemplateModelIterator(collection.iterator(), true);
+    }
+    
+    /**
+     * Wraps an {@link Iterator}; not thread-safe. The encapsulated {@link Iterator} may be accessible from multiple
+     * threads (as multiple {@link SimpleTemplateModelIterator} instance can wrap the same {@link Iterator} instance),
+     * but if the {@link Iterator} was marked in the constructor as shared, the first thread which uses the
+     * {@link Iterator} will monopolize that.
+     */
+    private class SimpleTemplateModelIterator implements TemplateModelIterator {
+        
+        private final Iterator iterator;
+        private boolean iteratorOwnedByMe;
+            
+        SimpleTemplateModelIterator(Iterator iterator, boolean iteratorOwnedByMe) {
+            this.iterator = iterator;
+            this.iteratorOwnedByMe = iteratorOwnedByMe;
+        }
+
+        @Override
+        public TemplateModel next() throws TemplateModelException {
+            if (!iteratorOwnedByMe) { 
+                synchronized (SimpleCollection.this) {
+                    checkIteratorNotOwned();
+                    iteratorOwned = true;
+                    iteratorOwnedByMe = true;
+                }
+            }
+            
+            if (!iterator.hasNext()) {
+                throw new TemplateModelException("The collection has no more items.");
+            }
+            
+            Object value  = iterator.next();
+            return value instanceof TemplateModel ? (TemplateModel) value : wrap(value);
+        }
+
+        @Override
+        public boolean hasNext() throws TemplateModelException {
+            // Calling hasNext may looks safe, but I have met sync. problems.
+            if (!iteratorOwnedByMe) {
+                synchronized (SimpleCollection.this) {
+                    checkIteratorNotOwned();
+                }
+            }
+            
+            return iterator.hasNext();
+        }
+        
+        private void checkIteratorNotOwned() throws TemplateModelException {
+            if (iteratorOwned) {
+                throw new TemplateModelException(
+                        "This collection value wraps a java.util.Iterator, thus it can be listed only once.");
+            }
+        }
+        
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SimpleDate.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SimpleDate.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SimpleDate.java
new file mode 100644
index 0000000..cf6e753
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SimpleDate.java
@@ -0,0 +1,85 @@
+/*
+ * 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.io.Serializable;
+
+import org.apache.freemarker.core.model.TemplateDateModel;
+
+/**
+ * A simple implementation of the <tt>TemplateDateModel</tt>
+ * interface. Note that this class is immutable.
+ * <p>This class is thread-safe.
+ */
+public class SimpleDate implements TemplateDateModel, Serializable {
+    private final java.util.Date date;
+    private final int type;
+    
+    /**
+     * Creates a new date model wrapping the specified date object and
+     * having DATE type.
+     */
+    public SimpleDate(java.sql.Date date) {
+        this(date, DATE);
+    }
+    
+    /**
+     * Creates a new date model wrapping the specified time object and
+     * having TIME type.
+     */
+    public SimpleDate(java.sql.Time time) {
+        this(time, TIME);
+    }
+    
+    /**
+     * Creates a new date model wrapping the specified time object and
+     * having DATETIME type.
+     */
+    public SimpleDate(java.sql.Timestamp datetime) {
+        this(datetime, DATETIME);
+    }
+    
+    /**
+     * Creates a new date model wrapping the specified date object and
+     * having the specified type.
+     */
+    public SimpleDate(java.util.Date date, int type) {
+        if (date == null) {
+            throw new IllegalArgumentException("date == null");
+        }
+        this.date = date;
+        this.type = type;
+    }
+    
+    @Override
+    public java.util.Date getAsDate() {
+        return date;
+    }
+
+    @Override
+    public int getDateType() {
+        return type;
+    }
+    
+    @Override
+    public String toString() {
+        return date.toString();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SimpleHash.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SimpleHash.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SimpleHash.java
new file mode 100644
index 0000000..f520c3d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SimpleHash.java
@@ -0,0 +1,296 @@
+/*
+ * 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.io.Serializable;
+import java.util.ConcurrentModificationException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import org.apache.freemarker.core._DelayedJQuote;
+import org.apache.freemarker.core._TemplateModelException;
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateHashModelEx2;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.WrappingTemplateModel;
+
+/**
+ * A simple implementation of the {@link TemplateHashModelEx} interface, using its own underlying {@link Map} or
+ * {@link SortedMap} for storing the hash entries. If you are wrapping an already existing {@link Map}, you should
+ * certainly use {@link DefaultMapAdapter} instead (see comparison below).
+ *
+ * <p>
+ * This class is thread-safe if you don't call modifying methods (like {@link #put(String, Object)},
+ * {@link #remove(String)}, etc.) after you have made the object available for multiple threads (assuming you have
+ * published it safely to the other threads; see JSR-133 Java Memory Model). These methods aren't called by FreeMarker,
+ * so it's usually not a concern.
+ * 
+ * <p>
+ * <b>{@link SimpleHash} VS {@link DefaultMapAdapter} - Which to use when?</b>
+ * 
+ * <p>
+ * For a {@link Map} that exists regardless of FreeMarker, only you need to access it from templates,
+ * {@link DefaultMapAdapter} should be the default choice, as it reflects the exact behavior of the underlying
+ * {@link Map} (no surprises), can be unwrapped to the originally wrapped object (important when passing it to Java
+ * methods from the template), and has more predictable performance (no spikes).
+ * 
+ * <p>
+ * For a hash that's made specifically to be used from templates, creating an empty {@link SimpleHash} then filling it
+ * with {@link SimpleHash#put(String, Object)} is usually the way to go, as the resulting hash is significantly faster
+ * to read from templates than a {@link DefaultMapAdapter} (though it's somewhat slower to read from a plain Java method
+ * to which it had to be passed adapted to a {@link Map}).
+ * 
+ * <p>
+ * It also matters if for how many times will the <em>same</em> {@link Map} entry be read from the template(s) later, on
+ * average. If, on average, you read each entry for more than 4 times, {@link SimpleHash} will be most certainly faster,
+ * but if for 2 times or less (and especially if not at all) then {@link DefaultMapAdapter} will be faster. Before
+ * choosing based on performance though, pay attention to the behavioral differences; {@link SimpleHash} will
+ * shallow-copy the original {@link Map} at construction time, so key order will be lost in some cases, and it won't
+ * reflect {@link Map} content changes after the {@link SimpleHash} construction, also {@link SimpleHash} can't be
+ * unwrapped to the original {@link Map} instance.
+ *
+ * @see DefaultMapAdapter
+ * @see TemplateHashModelEx
+ */
+public class SimpleHash extends WrappingTemplateModel implements TemplateHashModelEx2, Serializable {
+
+    private final Map map;
+    private boolean putFailed;
+    private Map unwrappedMap;
+
+    /**
+     * Creates an empty simple hash using the specified object wrapper.
+     * @param wrapper The object wrapper to use to wrap objects into
+     * {@link TemplateModel} instances. Not {@code null}.
+     */
+    public SimpleHash(ObjectWrapper wrapper) {
+        super(wrapper);
+        map = new HashMap();
+    }
+
+    /**
+     * Creates a new hash by shallow-coping (possibly cloning) the underlying map; in many applications you should use
+     * {@link DefaultMapAdapter} instead.
+     *
+     * @param map
+     *            The Map to use for the key/value pairs. It makes a copy for internal use. If the map implements the
+     *            {@link SortedMap} interface, the internal copy will be a {@link TreeMap}, otherwise it will be a
+     * @param wrapper
+     *            The object wrapper to use to wrap contained objects into {@link TemplateModel} instances. Not
+     *            {@code null}.
+     */
+    public SimpleHash(Map map, ObjectWrapper wrapper) {
+        super(wrapper);
+        Map mapCopy;
+        try {
+            mapCopy = copyMap(map);
+        } catch (ConcurrentModificationException cme) {
+            //This will occur extremely rarely.
+            //If it does, we just wait 5 ms and try again. If 
+            // the ConcurrentModificationException
+            // is thrown again, we just let it bubble up this time.
+            // TODO: Maybe we should log here.
+            try {
+                Thread.sleep(5);
+            } catch (InterruptedException ie) {
+                // Ignored
+            }
+            synchronized (map) {
+                mapCopy = copyMap(map);
+            }
+        }
+        this.map = mapCopy;
+    }
+
+    protected Map copyMap(Map map) {
+        if (map instanceof HashMap) {
+            return (Map) ((HashMap) map).clone();
+        }
+        if (map instanceof SortedMap) {
+            if (map instanceof TreeMap) {
+                return (Map) ((TreeMap) map).clone();
+            } else {
+                return new TreeMap((SortedMap) map);
+            }
+        } 
+        return new HashMap(map);
+    }
+
+    /**
+     * Adds a key-value entry to this hash.
+     *
+     * @param key
+     *            The name by which the object is identified in the template.
+     * @param value
+     *            The value to which the name will be associated. This will only be wrapped to {@link TemplateModel}
+     *            lazily when it's first read.
+     */
+    public void put(String key, Object value) {
+        map.put(key, value);
+        unwrappedMap = null;
+    }
+
+    /**
+     * Puts a boolean in the map
+     *
+     * @param key the name by which the resulting <tt>TemplateModel</tt>
+     * is identified in the template.
+     * @param b the boolean to store.
+     */
+    public void put(String key, boolean b) {
+        put(key, b ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE);
+    }
+
+    @Override
+    public TemplateModel get(String key) throws TemplateModelException {
+        Object result;
+        try {
+            result = map.get(key);
+        } catch (ClassCastException e) {
+            throw new _TemplateModelException(e,
+                    "ClassCastException while getting Map entry with String key ",
+                    new _DelayedJQuote(key));
+        } catch (NullPointerException e) {
+            throw new _TemplateModelException(e,
+                    "NullPointerException while getting Map entry with String key ",
+                    new _DelayedJQuote(key));
+        }
+        // The key to use for putting -- it's the key that already exists in
+        // the map (either key or charKey below). This way, we'll never put a 
+        // new key in the map, avoiding spurious ConcurrentModificationException
+        // from another thread iterating over the map, see bug #1939742 in 
+        // SourceForge tracker.
+        Object putKey = null;
+        if (result == null) {
+            // Check for Character key if this is a single-character string.
+            // In SortedMap-s, however, we can't do that safely, as it can cause ClassCastException.
+            if (key.length() == 1 && !(map instanceof SortedMap)) {
+                Character charKey = Character.valueOf(key.charAt(0));
+                try {
+                    result = map.get(charKey);
+                    if (result != null || map.containsKey(charKey)) {
+                        putKey = charKey;
+                    }
+                } catch (ClassCastException e) {
+                    throw new _TemplateModelException(e,
+                            "ClassCastException while getting Map entry with Character key ",
+                            new _DelayedJQuote(key));
+                } catch (NullPointerException e) {
+                    throw new _TemplateModelException(e,
+                            "NullPointerException while getting Map entry with Character key ",
+                            new _DelayedJQuote(key));
+                }
+            }
+            if (putKey == null) {
+                if (!map.containsKey(key)) {
+                    return null;
+                } else {
+                    putKey = key;
+                }
+            }
+        } else {
+            putKey = key;
+        }
+        
+        if (result instanceof TemplateModel) {
+            return (TemplateModel) result;
+        }
+        
+        TemplateModel tm = wrap(result);
+        if (!putFailed) {
+            try {
+                map.put(putKey, tm);
+            } catch (Exception e) {
+                // If it's immutable or something, we just keep going.
+                putFailed = true;
+            }
+        }
+        return tm;
+    }
+
+    /**
+     * Tells if the map contains a key or not, regardless if the associated value is {@code null} or not.
+     * @since 2.3.20
+     */
+    public boolean containsKey(String key) {
+        return map.containsKey(key);
+    }
+
+    /**
+     * Removes the given key from the underlying map.
+     *
+     * @param key the key to be removed
+     */
+    public void remove(String key) {
+        map.remove(key);
+    }
+
+    /**
+     * Adds all the key/value entries in the map
+     * @param m the map with the entries to add, the keys are assumed to be strings.
+     */
+
+    public void putAll(Map m) {
+        for (Iterator it = m.entrySet().iterator(); it.hasNext(); ) {
+            Map.Entry entry = (Map.Entry) it.next();
+            put((String) entry.getKey(), entry.getValue());
+        }
+    }
+
+    /**
+     * Returns the {@code toString()} of the underlying {@link Map}.
+     */
+    @Override
+    public String toString() {
+        return map.toString();
+    }
+
+    @Override
+    public int size() {
+        return map.size();
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return map == null || map.isEmpty();
+    }
+
+    @Override
+    public TemplateCollectionModel keys() {
+        return new SimpleCollection(map.keySet(), getObjectWrapper());
+    }
+
+    @Override
+    public TemplateCollectionModel values() {
+        return new SimpleCollection(map.values(), getObjectWrapper());
+    }
+
+    @Override
+    public KeyValuePairIterator keyValuePairIterator() {
+        return new MapKeyValuePairIterator(map, getObjectWrapper());
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SimpleMethod.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SimpleMethod.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SimpleMethod.java
new file mode 100644
index 0000000..ae5c531
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SimpleMethod.java
@@ -0,0 +1,174 @@
+/*
+ * 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.lang.reflect.Array;
+import java.lang.reflect.Member;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.freemarker.core._DelayedFTLTypeDescription;
+import org.apache.freemarker.core._DelayedOrdinal;
+import org.apache.freemarker.core._ErrorDescriptionBuilder;
+import org.apache.freemarker.core._TemplateModelException;
+import org.apache.freemarker.core.model.ObjectWrapperAndUnwrapper;
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.util._ClassUtil;
+
+/**
+ * This class is used for as a base for non-overloaded method models and for constructors.
+ * (For overloaded methods and constructors see {@link OverloadedMethods}.)
+ */
+class SimpleMethod {
+    
+    static final String MARKUP_OUTPUT_TO_STRING_TIP
+            = "A markup output value can be converted to markup string like value?markup_string. "
+              + "But consider if the Java method whose argument it will be can handle markup strings properly.";
+    
+    private final Member member;
+    private final Class[] argTypes;
+    
+    protected SimpleMethod(Member member, Class[] argTypes) {
+        this.member = member;
+        this.argTypes = argTypes;
+    }
+    
+    Object[] unwrapArguments(List arguments, DefaultObjectWrapper wrapper) throws TemplateModelException {
+        if (arguments == null) {
+            arguments = Collections.EMPTY_LIST;
+        }
+        boolean isVarArg = _MethodUtil.isVarargs(member);
+        int typesLen = argTypes.length;
+        if (isVarArg) {
+            if (typesLen - 1 > arguments.size()) {
+                throw new _TemplateModelException(
+                        _MethodUtil.invocationErrorMessageStart(member),
+                        " takes at least ", Integer.valueOf(typesLen - 1),
+                        typesLen - 1 == 1 ? " argument" : " arguments", ", but ",
+                        Integer.valueOf(arguments.size()), " was given.");
+            }
+        } else if (typesLen != arguments.size()) {
+            throw new _TemplateModelException(
+                    _MethodUtil.invocationErrorMessageStart(member), 
+                    " takes ", Integer.valueOf(typesLen), typesLen == 1 ? " argument" : " arguments", ", but ",
+                    Integer.valueOf(arguments.size()), " was given.");
+        }
+         
+        return unwrapArguments(arguments, argTypes, isVarArg, wrapper);
+    }
+
+    private Object[] unwrapArguments(List args, Class[] argTypes, boolean isVarargs,
+            DefaultObjectWrapper w)
+    throws TemplateModelException {
+        if (args == null) return null;
+        
+        int typesLen = argTypes.length;
+        int argsLen = args.size();
+        
+        Object[] unwrappedArgs = new Object[typesLen];
+        
+        // Unwrap arguments:
+        Iterator it = args.iterator();
+        int normalArgCnt = isVarargs ? typesLen - 1 : typesLen; 
+        int argIdx = 0;
+        while (argIdx < normalArgCnt) {
+            Class argType = argTypes[argIdx];
+            TemplateModel argVal = (TemplateModel) it.next();
+            Object unwrappedArgVal = w.tryUnwrapTo(argVal, argType);
+            if (unwrappedArgVal == ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS) {
+                throw createArgumentTypeMismarchException(argIdx, argVal, argType);
+            }
+            if (unwrappedArgVal == null && argType.isPrimitive()) {
+                throw createNullToPrimitiveArgumentException(argIdx, argType); 
+            }
+            
+            unwrappedArgs[argIdx++] = unwrappedArgVal;
+        }
+        if (isVarargs) {
+            // The last argType, which is the vararg type, wasn't processed yet.
+            
+            Class varargType = argTypes[typesLen - 1];
+            Class varargItemType = varargType.getComponentType();
+            if (!it.hasNext()) {
+                unwrappedArgs[argIdx++] = Array.newInstance(varargItemType, 0);
+            } else {
+                TemplateModel argVal = (TemplateModel) it.next();
+                
+                Object unwrappedArgVal;
+                // We first try to treat the last argument as a vararg *array*.
+                // This is consistent to what OverloadedVarArgMethod does.
+                if (argsLen - argIdx == 1
+                        && (unwrappedArgVal = w.tryUnwrapTo(argVal, varargType))
+                            != ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS) {
+                    // It was a vararg array.
+                    unwrappedArgs[argIdx++] = unwrappedArgVal;
+                } else {
+                    // It wasn't a vararg array, so we assume it's a vararg
+                    // array *item*, possibly followed by further ones.
+                    int varargArrayLen = argsLen - argIdx;
+                    Object varargArray = Array.newInstance(varargItemType, varargArrayLen);
+                    for (int varargIdx = 0; varargIdx < varargArrayLen; varargIdx++) {
+                        TemplateModel varargVal = (TemplateModel) (varargIdx == 0 ? argVal : it.next());
+                        Object unwrappedVarargVal = w.tryUnwrapTo(varargVal, varargItemType);
+                        if (unwrappedVarargVal == ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS) {
+                            throw createArgumentTypeMismarchException(
+                                    argIdx + varargIdx, varargVal, varargItemType);
+                        }
+                        
+                        if (unwrappedVarargVal == null && varargItemType.isPrimitive()) {
+                            throw createNullToPrimitiveArgumentException(argIdx + varargIdx, varargItemType); 
+                        }
+                        Array.set(varargArray, varargIdx, unwrappedVarargVal);
+                    }
+                    unwrappedArgs[argIdx++] = varargArray;
+                }
+            }
+        }
+        
+        return unwrappedArgs;
+    }
+
+    private TemplateModelException createArgumentTypeMismarchException(
+            int argIdx, TemplateModel argVal, Class targetType) {
+        _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder(
+                _MethodUtil.invocationErrorMessageStart(member), " couldn't be called: Can't convert the ",
+                new _DelayedOrdinal(Integer.valueOf(argIdx + 1)),
+                " argument's value to the target Java type, ", _ClassUtil.getShortClassName(targetType),
+                ". The type of the actual value was: ", new _DelayedFTLTypeDescription(argVal));
+        if (argVal instanceof TemplateMarkupOutputModel && (targetType.isAssignableFrom(String.class))) {
+            desc.tip(MARKUP_OUTPUT_TO_STRING_TIP);
+        }
+        return new _TemplateModelException(desc);
+    }
+
+    private TemplateModelException createNullToPrimitiveArgumentException(int argIdx, Class targetType) {
+        return new _TemplateModelException(
+                _MethodUtil.invocationErrorMessageStart(member), " couldn't be called: The value of the ",
+                new _DelayedOrdinal(Integer.valueOf(argIdx + 1)),
+                " argument was null, but the target Java parameter type (", _ClassUtil.getShortClassName(targetType),
+                ") is primitive and so can't store null.");
+    }
+    
+    protected Member getMember() {
+        return member;
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SimpleNumber.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SimpleNumber.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SimpleNumber.java
new file mode 100644
index 0000000..6f588b6
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SimpleNumber.java
@@ -0,0 +1,77 @@
+/*
+ * 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.io.Serializable;
+
+import org.apache.freemarker.core.model.TemplateNumberModel;
+
+
+/**
+ * A simple implementation of the <tt>TemplateNumberModel</tt>
+ * interface. Note that this class is immutable.
+ *
+ * <p>This class is thread-safe.
+ */
+public final class SimpleNumber implements TemplateNumberModel, Serializable {
+
+    /**
+     * @serial the value of this <tt>SimpleNumber</tt> 
+     */
+    private final Number value;
+
+    public SimpleNumber(Number value) {
+        this.value = value;
+    }
+
+    public SimpleNumber(byte val) {
+        value = Byte.valueOf(val);
+    }
+
+    public SimpleNumber(short val) {
+        value = Short.valueOf(val);
+    }
+
+    public SimpleNumber(int val) {
+        value = Integer.valueOf(val);
+    }
+
+    public SimpleNumber(long val) {
+        value = Long.valueOf(val);
+    }
+
+    public SimpleNumber(float val) {
+        value = Float.valueOf(val);
+    }
+    
+    public SimpleNumber(double val) {
+        value = Double.valueOf(val);
+    }
+
+    @Override
+    public Number getAsNumber() {
+        return value;
+    }
+
+    @Override
+    public String toString() {
+        return value.toString();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SimpleScalar.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SimpleScalar.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SimpleScalar.java
new file mode 100644
index 0000000..79a8820
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SimpleScalar.java
@@ -0,0 +1,73 @@
+/*
+ * 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.io.Serializable;
+
+import org.apache.freemarker.core.model.TemplateScalarModel;
+
+/**
+ * A simple implementation of the <tt>TemplateScalarModel</tt>
+ * interface, using a <tt>String</tt>.
+ * As of version 2.0 this object is immutable.
+ *
+ * <p>This class is thread-safe.
+ *
+ * @see SimpleSequence
+ * @see SimpleHash
+ */
+public final class SimpleScalar 
+implements TemplateScalarModel, Serializable {
+    
+    /**
+     * @serial the value of this <tt>SimpleScalar</tt> if it wraps a
+     * <tt>String</tt>.
+     */
+    private final String value;
+
+    /**
+     * Constructs a <tt>SimpleScalar</tt> containing a string value.
+     * @param value the string value. If this is {@code null}, its value in FTL will be {@code ""}.
+     */
+    public SimpleScalar(String value) {
+        this.value = value;
+    }
+
+    @Override
+    public String getAsString() {
+        return (value == null) ? "" : value;
+    }
+
+    @Override
+    public String toString() {
+        // [2.4] Shouldn't return null
+        return value;
+    }
+    
+    /**
+     * Same as calling the constructor, except that for a {@code null} parameter it returns null. 
+     * 
+     * @since 2.3.23
+     */
+    public static SimpleScalar newInstanceOrNull(String s) {
+        return s != null ? new SimpleScalar(s) : null;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SimpleSequence.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SimpleSequence.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SimpleSequence.java
new file mode 100644
index 0000000..1b949f1
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SimpleSequence.java
@@ -0,0 +1,162 @@
+/*
+ * 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.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.WrappingTemplateModel;
+
+/**
+ * A simple implementation of the {@link TemplateSequenceModel} interface, using its own underlying {@link List} for
+ * storing the list items. If you are wrapping an already existing {@link List} or {@code array}, you should certainly
+ * use {@link DefaultMapAdapter} or {@link DefaultArrayAdapter} (see comparison below).
+ * 
+ * <p>
+ * This class is thread-safe if you don't call modifying methods (like {@link #add(Object)}) after you have made the
+ * object available for multiple threads (assuming you have published it safely to the other threads; see JSR-133 Java
+ * Memory Model). These methods aren't called by FreeMarker, so it's usually not a concern.
+ * 
+ * <p>
+ * <b>{@link SimpleSequence} VS {@link DefaultListAdapter}/{@link DefaultArrayAdapter} - Which to use when?</b>
+ * </p>
+ * 
+ * <p>
+ * For a {@link List} or {@code array} that exists regardless of FreeMarker, only you need to access it from templates,
+ * {@link DefaultMapAdapter} should be the default choice, as it can be unwrapped to the originally wrapped object
+ * (important when passing it to Java methods from the template). It also has more predictable performance (no spikes).
+ * 
+ * <p>
+ * For a sequence that's made specifically to be used from templates, creating an empty {@link SimpleSequence} then
+ * filling it with {@link SimpleSequence#add(Object)} is usually the way to go, as the resulting sequence is
+ * significantly faster to read from templates than a {@link DefaultListAdapter} (though it's somewhat slower to read
+ * from a plain Java method to which it had to be passed adapted to a {@link List}).
+ * 
+ * <p>
+ * It also matters if for how many times will the <em>same</em> {@link List} entry be read from the template(s) later,
+ * on average. If, on average, you read each entry for more than 4 times, {@link SimpleSequence} will be most
+ * certainly faster, but if for 2 times or less (and especially if not at all) then {@link DefaultMapAdapter} will
+ * be faster. Before choosing based on performance though, pay attention to the behavioral differences;
+ * {@link SimpleSequence} will shallow-copy the original {@link List} at construction time, so it won't reflect
+ * {@link List} content changes after the {@link SimpleSequence} construction, also {@link SimpleSequence} can't be
+ * unwrapped to the original wrapped instance.
+ *
+ * @see DefaultListAdapter
+ * @see DefaultArrayAdapter
+ * @see TemplateSequenceModel
+ */
+public class SimpleSequence extends WrappingTemplateModel implements TemplateSequenceModel, Serializable {
+
+    /**
+     * The {@link List} that stored the elements of this sequence. It might contains both {@link TemplateModel} elements
+     * and non-{@link TemplateModel} elements.
+     */
+    protected final List list;
+
+    /**
+     * Constructs an empty sequence using the specified object wrapper.
+     * 
+     * @param wrapper
+     *            The object wrapper to use to wrap the list items into {@link TemplateModel} instances. Not
+     *            {@code null}.
+     */
+    public SimpleSequence(ObjectWrapper wrapper) {
+        super(wrapper);
+        list = new ArrayList();
+    }
+    
+    /**
+     * Constructs an empty simple sequence with preallocated capacity.
+     * 
+     * @param wrapper
+     *            See the similar parameter of {@link SimpleSequence#SimpleSequence(ObjectWrapper)}.
+     * 
+     * @since 2.3.21
+     */
+    public SimpleSequence(int capacity, ObjectWrapper wrapper) {
+        super(wrapper);
+        list = new ArrayList(capacity);
+    }    
+    
+    /**
+     * Constructs a simple sequence that will contain the elements from the specified {@link Collection}; consider
+     * using {@link DefaultListAdapter} instead.
+     * 
+     * @param collection
+     *            The collection containing the initial items of the sequence. A shallow copy of this collection is made
+     *            immediately for internal use (thus, later modification on the parameter collection won't be visible in
+     *            the resulting sequence). The items however, will be only wrapped with the {@link ObjectWrapper}
+     *            lazily, when first needed.
+     * @param wrapper
+     *            See the similar parameter of {@link SimpleSequence#SimpleSequence(ObjectWrapper)}.
+     */
+    public SimpleSequence(Collection collection, ObjectWrapper wrapper) {
+        super(wrapper);
+        list = new ArrayList(collection);
+    }
+
+    /**
+     * Adds an arbitrary object to the end of this sequence. If the newly added object does not implement the
+     * {@link TemplateModel} interface, it will be wrapped into the appropriate {@link TemplateModel} interface when
+     * it's first read (lazily).
+     *
+     * @param obj
+     *            The object to be added.
+     */
+    public void add(Object obj) {
+        list.add(obj);
+    }
+
+    /**
+     * Returns the item at the specified index of the list. If the item isn't yet an {@link TemplateModel}, it will wrap
+     * it to one now, and writes it back into the backing list.
+     */
+    @Override
+    public TemplateModel get(int index) throws TemplateModelException {
+        try {
+            Object value = list.get(index);
+            if (value instanceof TemplateModel) {
+                return (TemplateModel) value;
+            }
+            TemplateModel tm = wrap(value);
+            list.set(index, tm);
+            return tm;
+        } catch (IndexOutOfBoundsException e) {
+            return null;
+        }
+    }
+
+    @Override
+    public int size() {
+        return list.size();
+    }
+
+    @Override
+    public String toString() {
+        return list.toString();
+    }
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SingletonCustomizer.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SingletonCustomizer.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SingletonCustomizer.java
new file mode 100644
index 0000000..e4a0e5a
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/SingletonCustomizer.java
@@ -0,0 +1,51 @@
+/*
+ * 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.model.ObjectWrapper;
+
+/**
+ * Marker interface useful when used together with {@link MethodAppearanceFineTuner} and such customizer objects, to
+ * indicate that it <b>doesn't contain reference to the {@link ObjectWrapper}</b> (so beware with non-static inner
+ * classes) and can be and should be used in call introspection cache keys. This also implies that you won't
+ * invoke many instances of the class, rather just reuse the same (or same few) instances over and over. Furthermore,
+ * the instances must be thread-safe. The typical pattern in which this instance should be used is like this:
+ * 
+ * <pre>static class MyMethodAppearanceFineTuner implements MethodAppearanceFineTuner, SingletonCustomizer {
+ *      
+ *    // This is the singleton:
+ *    static final MyMethodAppearanceFineTuner INSTANCE = new MyMethodAppearanceFineTuner();
+ *     
+ *    // Private, so it can't be constructed from outside this class.
+ *    private MyMethodAppearanceFineTuner() { }
+ *
+ *    &#64;Override
+ *    public void fineTuneMethodAppearance(...) {
+ *       // Do something here, only using the parameters and maybe some other singletons. 
+ *       ...
+ *    }
+ *     
+ * }</pre>
+ *
+ * @since 2.3.21
+ */
+public interface SingletonCustomizer {
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/StaticModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/StaticModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/StaticModel.java
new file mode 100644
index 0000000..fbee788
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/StaticModel.java
@@ -0,0 +1,177 @@
+/*
+ * 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.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.apache.freemarker.core._CoreLogs;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.slf4j.Logger;
+
+/**
+ * Wraps the static fields and methods of a class in a
+ * {@link org.apache.freemarker.core.model.TemplateHashModel}.
+ * Fields are wrapped using {@link DefaultObjectWrapper#wrap(Object)}, and
+ * methods are wrapped into an appropriate {@link org.apache.freemarker.core.model.TemplateMethodModelEx} instance.
+ * Unfortunately, there is currently no support for bean property-style
+ * calls of static methods, similar to that in {@link BeanModel}.
+ */
+final class StaticModel implements TemplateHashModelEx {
+    
+    private static final Logger LOG = _CoreLogs.OBJECT_WRAPPER;
+    
+    private final Class clazz;
+    private final DefaultObjectWrapper wrapper;
+    private final Map map = new HashMap();
+
+    StaticModel(Class clazz, DefaultObjectWrapper wrapper) throws TemplateModelException {
+        this.clazz = clazz;
+        this.wrapper = wrapper;
+        populate();
+    }
+
+    /**
+     * Returns the field or method named by the <tt>key</tt>
+     * parameter.
+     */
+    @Override
+    public TemplateModel get(String key) throws TemplateModelException {
+        Object model = map.get(key);
+        // Simple method, overloaded method or final field -- these have cached 
+        // template models
+        if (model instanceof TemplateModel)
+            return (TemplateModel) model;
+        // Non-final field; this must be evaluated on each call.
+        if (model instanceof Field) {
+            try {
+                return wrapper.getOuterIdentity().wrap(((Field) model).get(null));
+            } catch (IllegalAccessException e) {
+                throw new TemplateModelException(
+                    "Illegal access for field " + key + " of class " + clazz.getName());
+            }
+        }
+
+        throw new TemplateModelException(
+            "No such key: " + key + " in class " + clazz.getName());
+    }
+
+    /**
+     * Returns true if there is at least one public static
+     * field or method in the underlying class.
+     */
+    @Override
+    public boolean isEmpty() {
+        return map.isEmpty();
+    }
+
+    @Override
+    public int size() {
+        return map.size();
+    }
+    
+    @Override
+    public TemplateCollectionModel keys() throws TemplateModelException {
+        return (TemplateCollectionModel) wrapper.getOuterIdentity().wrap(map.keySet());
+    }
+    
+    @Override
+    public TemplateCollectionModel values() throws TemplateModelException {
+        return (TemplateCollectionModel) wrapper.getOuterIdentity().wrap(map.values());
+    }
+
+    private void populate() throws TemplateModelException {
+        if (!Modifier.isPublic(clazz.getModifiers())) {
+            throw new TemplateModelException(
+                "Can't wrap the non-public class " + clazz.getName());
+        }
+        
+        if (wrapper.getExposureLevel() == DefaultObjectWrapper.EXPOSE_NOTHING) {
+            return;
+        }
+
+        Field[] fields = clazz.getFields();
+        for (Field field : fields) {
+            int mod = field.getModifiers();
+            if (Modifier.isPublic(mod) && Modifier.isStatic(mod)) {
+                if (Modifier.isFinal(mod))
+                    try {
+                        // public static final fields are evaluated once and
+                        // stored in the map
+                        map.put(field.getName(), wrapper.getOuterIdentity().wrap(field.get(null)));
+                    } catch (IllegalAccessException e) {
+                        // Intentionally ignored
+                    }
+                else
+                    // This is a special flagging value: Field in the map means
+                    // that this is a non-final field, and it must be evaluated
+                    // on each get() call.
+                    map.put(field.getName(), field);
+            }
+        }
+        if (wrapper.getExposureLevel() < DefaultObjectWrapper.EXPOSE_PROPERTIES_ONLY) {
+            Method[] methods = clazz.getMethods();
+            for (Method method : methods) {
+                int mod = method.getModifiers();
+                if (Modifier.isPublic(mod) && Modifier.isStatic(mod)
+                        && wrapper.getClassIntrospector().isAllowedToExpose(method)) {
+                    String name = method.getName();
+                    Object obj = map.get(name);
+                    if (obj instanceof Method) {
+                        OverloadedMethods overloadedMethods = new OverloadedMethods();
+                        overloadedMethods.addMethod((Method) obj);
+                        overloadedMethods.addMethod(method);
+                        map.put(name, overloadedMethods);
+                    } else if (obj instanceof OverloadedMethods) {
+                        OverloadedMethods overloadedMethods = (OverloadedMethods) obj;
+                        overloadedMethods.addMethod(method);
+                    } else {
+                        if (obj != null) {
+                            if (LOG.isInfoEnabled()) {
+                                LOG.info("Overwriting value [" + obj + "] for " +
+                                        " key '" + name + "' with [" + method +
+                                        "] in static model for " + clazz.getName());
+                            }
+                        }
+                        map.put(name, method);
+                    }
+                }
+            }
+            for (Iterator entries = map.entrySet().iterator(); entries.hasNext(); ) {
+                Map.Entry entry = (Map.Entry) entries.next();
+                Object value = entry.getValue();
+                if (value instanceof Method) {
+                    Method method = (Method) value;
+                    entry.setValue(new JavaMethodModel(null, method,
+                            method.getParameterTypes(), wrapper));
+                } else if (value instanceof OverloadedMethods) {
+                    entry.setValue(new OverloadedMethodsModel(null, (OverloadedMethods) value, wrapper));
+                }
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/StaticModels.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/StaticModels.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/StaticModels.java
new file mode 100644
index 0000000..15b45bc
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/StaticModels.java
@@ -0,0 +1,43 @@
+/*
+ * 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.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+
+/**
+ * Utility class for instantiating {@link StaticModel} instances from
+ * templates. If your template's data model contains an instance of
+ * StaticModels (named, say <tt>StaticModels</tt>), then you can
+ * instantiate an arbitrary StaticModel using get syntax (i.e.
+ * <tt>StaticModels["java.lang.System"].currentTimeMillis()</tt>).
+ */
+class StaticModels extends ClassBasedModelFactory {
+    
+    StaticModels(DefaultObjectWrapper wrapper) {
+        super(wrapper);
+    }
+
+    @Override
+    protected TemplateModel createModel(Class clazz) 
+    throws TemplateModelException {
+        return new StaticModel(clazz, getWrapper());
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/TemplateModelListSequence.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/TemplateModelListSequence.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/TemplateModelListSequence.java
new file mode 100644
index 0000000..b049c63
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/TemplateModelListSequence.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.model.impl;
+
+import java.util.List;
+
+import org.apache.freemarker.core.model.TemplateMethodModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+
+/**
+ * A sequence that wraps a {@link List} of {@link TemplateModel}-s. It does not copy the original
+ * list. It's mostly useful when implementing {@link TemplateMethodModelEx}-es that collect items from other
+ * {@link TemplateModel}-s.
+ */
+public class TemplateModelListSequence implements TemplateSequenceModel {
+    
+    private List/*<TemplateModel>*/ list;
+
+    public TemplateModelListSequence(List list) {
+        this.list = list;
+    }
+
+    @Override
+    public TemplateModel get(int index) {
+        return (TemplateModel) list.get(index);
+    }
+
+    @Override
+    public int size() {
+        return list.size();
+    }
+
+    /**
+     * Returns the original {@link List} of {@link TemplateModel}-s, so it's not a fully unwrapped value.
+     */
+    public Object getWrappedObject() {
+        return list;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/TypeFlags.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/TypeFlags.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/TypeFlags.java
new file mode 100644
index 0000000..6a5579b
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/TypeFlags.java
@@ -0,0 +1,130 @@
+/*
+ * 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.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Flag values and masks for "type flags". "Type flags" is a set of bits that store information about the possible
+ * destination types at a parameter position of overloaded methods. 
+ */
+class TypeFlags {
+
+    /**
+     * Indicates that the unwrapping hint will not be a specific numerical type; it must not be set if there's no
+     * numerical type at the given parameter index.
+     */
+    static final int WIDENED_NUMERICAL_UNWRAPPING_HINT = 1;
+    
+    static final int BYTE = 4;
+    static final int SHORT = 8;
+    static final int INTEGER = 16;
+    static final int LONG = 32;
+    static final int FLOAT = 64;
+    static final int DOUBLE = 128;
+    static final int BIG_INTEGER = 256;
+    static final int BIG_DECIMAL = 512;
+    static final int UNKNOWN_NUMERICAL_TYPE = 1024;
+
+    static final int ACCEPTS_NUMBER = 0x800;
+    static final int ACCEPTS_DATE = 0x1000;
+    static final int ACCEPTS_STRING = 0x2000;
+    static final int ACCEPTS_BOOLEAN = 0x4000;
+    static final int ACCEPTS_MAP = 0x8000;
+    static final int ACCEPTS_LIST = 0x10000;
+    static final int ACCEPTS_SET = 0x20000;
+    static final int ACCEPTS_ARRAY = 0x40000;
+    
+    /**
+     * Indicates the presence of the char or Character type
+     */
+    static final int CHARACTER = 0x80000;
+    
+    static final int ACCEPTS_ANY_OBJECT = ACCEPTS_NUMBER | ACCEPTS_DATE | ACCEPTS_STRING | ACCEPTS_BOOLEAN
+            | ACCEPTS_MAP | ACCEPTS_LIST | ACCEPTS_SET | ACCEPTS_ARRAY;
+    
+    static final int MASK_KNOWN_INTEGERS = BYTE | SHORT | INTEGER | LONG | BIG_INTEGER;
+    static final int MASK_KNOWN_NONINTEGERS = FLOAT | DOUBLE | BIG_DECIMAL;
+    static final int MASK_ALL_KNOWN_NUMERICALS = MASK_KNOWN_INTEGERS | MASK_KNOWN_NONINTEGERS;
+    static final int MASK_ALL_NUMERICALS = MASK_ALL_KNOWN_NUMERICALS | UNKNOWN_NUMERICAL_TYPE;
+    
+    static int classToTypeFlags(Class pClass) {
+        // We start with the most frequent cases  
+        if (pClass == Object.class) {
+            return ACCEPTS_ANY_OBJECT;
+        } else if (pClass == String.class) {
+            return ACCEPTS_STRING;
+        } else if (pClass.isPrimitive()) {
+            if (pClass == Integer.TYPE) return INTEGER | ACCEPTS_NUMBER;
+            else if (pClass == Long.TYPE) return LONG | ACCEPTS_NUMBER;
+            else if (pClass == Double.TYPE) return DOUBLE | ACCEPTS_NUMBER;
+            else if (pClass == Float.TYPE) return FLOAT | ACCEPTS_NUMBER;
+            else if (pClass == Byte.TYPE) return BYTE | ACCEPTS_NUMBER;
+            else if (pClass == Short.TYPE) return SHORT | ACCEPTS_NUMBER;
+            else if (pClass == Character.TYPE) return CHARACTER;
+            else if (pClass == Boolean.TYPE) return ACCEPTS_BOOLEAN;
+            else return 0;
+        } else if (Number.class.isAssignableFrom(pClass)) {
+            if (pClass == Integer.class) return INTEGER | ACCEPTS_NUMBER;
+            else if (pClass == Long.class) return LONG | ACCEPTS_NUMBER;
+            else if (pClass == Double.class) return DOUBLE | ACCEPTS_NUMBER;
+            else if (pClass == Float.class) return FLOAT | ACCEPTS_NUMBER;
+            else if (pClass == Byte.class) return BYTE | ACCEPTS_NUMBER;
+            else if (pClass == Short.class) return SHORT | ACCEPTS_NUMBER;
+            else if (BigDecimal.class.isAssignableFrom(pClass)) return BIG_DECIMAL | ACCEPTS_NUMBER;
+            else if (BigInteger.class.isAssignableFrom(pClass)) return BIG_INTEGER | ACCEPTS_NUMBER;
+            else return UNKNOWN_NUMERICAL_TYPE | ACCEPTS_NUMBER;
+        } else if (pClass.isArray()) {
+            return ACCEPTS_ARRAY;
+        } else {
+            int flags = 0;
+            if (pClass.isAssignableFrom(String.class)) {
+                flags |= ACCEPTS_STRING;
+            }
+            if (pClass.isAssignableFrom(Date.class)) {
+                flags |= ACCEPTS_DATE;
+            }
+            if (pClass.isAssignableFrom(Boolean.class)) {
+                flags |= ACCEPTS_BOOLEAN;
+            }
+            if (pClass.isAssignableFrom(Map.class)) {
+                flags |= ACCEPTS_MAP;
+            }
+            if (pClass.isAssignableFrom(List.class)) {
+                flags |= ACCEPTS_LIST;
+            }
+            if (pClass.isAssignableFrom(Set.class)) {
+                flags |= ACCEPTS_SET;
+            }
+            
+            if (pClass == Character.class) {
+                flags |= CHARACTER;
+            }
+            
+            return flags;
+        } 
+    }
+
+}



[09/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormat.java
new file mode 100644
index 0000000..e30c2e4
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormat.java
@@ -0,0 +1,75 @@
+/*
+ * 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.valueformat.impl;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.valueformat.TemplateDateFormat;
+import org.apache.freemarker.core.valueformat.TemplateFormatUtil;
+import org.apache.freemarker.core.valueformat.UnparsableValueException;
+
+/**
+ * Java {@link DateFormat}-based format.
+ */
+class JavaTemplateDateFormat extends TemplateDateFormat {
+    
+    private final DateFormat javaDateFormat;
+
+    public JavaTemplateDateFormat(DateFormat javaDateFormat) {
+        this.javaDateFormat = javaDateFormat;
+    }
+    
+    @Override
+    public String formatToPlainText(TemplateDateModel dateModel) throws TemplateModelException {
+        return javaDateFormat.format(TemplateFormatUtil.getNonNullDate(dateModel));
+    }
+
+    @Override
+    public Date parse(String s, int dateType) throws UnparsableValueException {
+        try {
+            return javaDateFormat.parse(s);
+        } catch (ParseException e) {
+            throw new UnparsableValueException(e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public String getDescription() {
+        return javaDateFormat instanceof SimpleDateFormat
+                ? ((SimpleDateFormat) javaDateFormat).toPattern()
+                : javaDateFormat.toString();
+    }
+
+    @Override
+    public boolean isLocaleBound() {
+        return true;
+    }
+
+    @Override
+    public boolean isTimeZoneBound() {
+        return true;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormatFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormatFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormatFactory.java
new file mode 100644
index 0000000..093e110
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormatFactory.java
@@ -0,0 +1,187 @@
+/*
+ * 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.valueformat.impl;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+import java.util.StringTokenizer;
+import java.util.TimeZone;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core._CoreLogs;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.valueformat.InvalidFormatParametersException;
+import org.apache.freemarker.core.valueformat.TemplateDateFormat;
+import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory;
+import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException;
+import org.slf4j.Logger;
+
+/**
+ * Deals with {@link TemplateDateFormat}-s that wrap a Java {@link DateFormat}. The parameter string is usually a
+ * {@link java.text.SimpleDateFormat} pattern, but it also recognized the names "short", "medium", "long"
+ * and "full", which correspond to formats defined by {@link DateFormat} with similar names.
+ *
+ * <p>Note that the resulting {@link java.text.SimpleDateFormat}-s are globally cached, and threading issues are
+ * addressed by cloning the cached instance before returning it. So it just makes object creation faster, but doesn't
+ * eliminate it.
+ */
+public class JavaTemplateDateFormatFactory extends TemplateDateFormatFactory {
+    
+    public static final JavaTemplateDateFormatFactory INSTANCE = new JavaTemplateDateFormatFactory();
+    
+    private static final Logger LOG = _CoreLogs.RUNTIME;
+
+    private static final ConcurrentHashMap<CacheKey, DateFormat> GLOBAL_FORMAT_CACHE = new ConcurrentHashMap<>();
+    private static final int LEAK_ALERT_DATE_FORMAT_CACHE_SIZE = 1024;
+    
+    private JavaTemplateDateFormatFactory() {
+        // Can't be instantiated
+    }
+    
+    /**
+     * @param zonelessInput
+     *            Has no effect in this implementation.
+     */
+    @Override
+    public TemplateDateFormat get(String params, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput,
+                                  Environment env) throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatParametersException {
+        return new JavaTemplateDateFormat(getJavaDateFormat(dateType, params, locale, timeZone));
+    }
+
+    /**
+     * Returns a "private" copy (not in the global cache) for the given format.  
+     */
+    private DateFormat getJavaDateFormat(int dateType, String nameOrPattern, Locale locale, TimeZone timeZone)
+            throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatParametersException {
+
+        // Get DateFormat from global cache:
+        CacheKey cacheKey = new CacheKey(dateType, nameOrPattern, locale, timeZone);
+        DateFormat jFormat;
+        
+        jFormat = GLOBAL_FORMAT_CACHE.get(cacheKey);
+        if (jFormat == null) {
+            // Add format to global format cache.
+            StringTokenizer tok = new StringTokenizer(nameOrPattern, "_");
+            int tok1Style = tok.hasMoreTokens() ? parseDateStyleToken(tok.nextToken()) : DateFormat.DEFAULT;
+            if (tok1Style != -1) {
+                switch (dateType) {
+                    case TemplateDateModel.UNKNOWN: {
+                        throw new UnknownDateTypeFormattingUnsupportedException();
+                    }
+                    case TemplateDateModel.TIME: {
+                        jFormat = DateFormat.getTimeInstance(tok1Style, cacheKey.locale);
+                        break;
+                    }
+                    case TemplateDateModel.DATE: {
+                        jFormat = DateFormat.getDateInstance(tok1Style, cacheKey.locale);
+                        break;
+                    }
+                    case TemplateDateModel.DATETIME: {
+                        int tok2Style = tok.hasMoreTokens() ? parseDateStyleToken(tok.nextToken()) : tok1Style;
+                        if (tok2Style != -1) {
+                            jFormat = DateFormat.getDateTimeInstance(tok1Style, tok2Style, cacheKey.locale);
+                        }
+                        break;
+                    }
+                }
+            }
+            if (jFormat == null) {
+                try {
+                    jFormat = new SimpleDateFormat(nameOrPattern, cacheKey.locale);
+                } catch (IllegalArgumentException e) {
+                    final String msg = e.getMessage();
+                    throw new InvalidFormatParametersException(
+                            msg != null ? msg : "Invalid SimpleDateFormat pattern", e);
+                }
+            }
+            jFormat.setTimeZone(cacheKey.timeZone);
+            
+            if (GLOBAL_FORMAT_CACHE.size() >= LEAK_ALERT_DATE_FORMAT_CACHE_SIZE) {
+                boolean triggered = false;
+                synchronized (JavaTemplateDateFormatFactory.class) {
+                    if (GLOBAL_FORMAT_CACHE.size() >= LEAK_ALERT_DATE_FORMAT_CACHE_SIZE) {
+                        triggered = true;
+                        GLOBAL_FORMAT_CACHE.clear();
+                    }
+                }
+                if (triggered) {
+                    LOG.warn("Global Java DateFormat cache has exceeded {} entries => cache flushed. "
+                            + "Typical cause: Some template generates high variety of format pattern strings.",
+                            LEAK_ALERT_DATE_FORMAT_CACHE_SIZE);
+                }
+            }
+            
+            DateFormat prevJFormat = GLOBAL_FORMAT_CACHE.putIfAbsent(cacheKey, jFormat);
+            if (prevJFormat != null) {
+                jFormat = prevJFormat;
+            }
+        }  // if cache miss
+        
+        return (DateFormat) jFormat.clone();  // For thread safety
+    }
+
+    private static final class CacheKey {
+        private final int dateType;
+        private final String pattern;
+        private final Locale locale;
+        private final TimeZone timeZone;
+
+        CacheKey(int dateType, String pattern, Locale locale, TimeZone timeZone) {
+            this.dateType = dateType;
+            this.pattern = pattern;
+            this.locale = locale;
+            this.timeZone = timeZone;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o instanceof CacheKey) {
+                CacheKey fk = (CacheKey) o;
+                return dateType == fk.dateType && fk.pattern.equals(pattern) && fk.locale.equals(locale)
+                        && fk.timeZone.equals(timeZone);
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return dateType ^ pattern.hashCode() ^ locale.hashCode() ^ timeZone.hashCode();
+        }
+    }
+
+    private int parseDateStyleToken(String token) {
+        if ("short".equals(token)) {
+            return DateFormat.SHORT;
+        }
+        if ("medium".equals(token)) {
+            return DateFormat.MEDIUM;
+        }
+        if ("long".equals(token)) {
+            return DateFormat.LONG;
+        }
+        if ("full".equals(token)) {
+            return DateFormat.FULL;
+        }
+        return -1;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormat.java
new file mode 100644
index 0000000..e3cdea0
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormat.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.valueformat.impl;
+
+import java.text.NumberFormat;
+
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.valueformat.TemplateFormatUtil;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormat;
+import org.apache.freemarker.core.valueformat.UnformattableValueException;
+
+final class JavaTemplateNumberFormat extends TemplateNumberFormat {
+    
+    private final String formatString;
+    private final NumberFormat javaNumberFormat;
+
+    public JavaTemplateNumberFormat(NumberFormat javaNumberFormat, String formatString) {
+        this.formatString = formatString;
+        this.javaNumberFormat = javaNumberFormat;
+    }
+
+    @Override
+    public String formatToPlainText(TemplateNumberModel numberModel) throws UnformattableValueException, TemplateModelException {
+        Number number = TemplateFormatUtil.getNonNullNumber(numberModel);
+        try {
+            return javaNumberFormat.format(number);
+        } catch (ArithmeticException e) {
+            throw new UnformattableValueException(
+                    "This format can't format the " + number + " number. Reason: " + e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public boolean isLocaleBound() {
+        return true;
+    }
+
+    public NumberFormat getJavaNumberFormat() {
+        return javaNumberFormat;
+    }
+
+    @Override
+    public String getDescription() {
+        return formatString;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormatFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormatFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormatFactory.java
new file mode 100644
index 0000000..cf292df
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormatFactory.java
@@ -0,0 +1,133 @@
+/*
+ * 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.valueformat.impl;
+
+import java.text.NumberFormat;
+import java.text.ParseException;
+import java.util.Locale;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core._CoreLogs;
+import org.apache.freemarker.core.valueformat.InvalidFormatParametersException;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormat;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory;
+import org.slf4j.Logger;
+
+/**
+ * Deals with {@link TemplateNumberFormat}-s that wrap a Java {@link NumberFormat}. The parameter string is usually
+ * a {@link java.text.DecimalFormat} pattern, with the extensions described in the Manual (see "Extended Jav decimal
+ * format"). There are some names that aren't parsed as patterns: "number", "currency", "percent", which
+ * corresponds to the predefined formats with similar name in {@link NumberFormat}-s, and also "computer" that
+ * behaves like {@code someNumber?c} in templates.
+ *
+ * <p>Note that the resulting {@link java.text.DecimalFormat}-s are globally cached, and threading issues are
+ * addressed by cloning the cached instance before returning it. So it just makes object creation faster, but doesn't
+ * eliminate it.
+ */
+public class JavaTemplateNumberFormatFactory extends TemplateNumberFormatFactory {
+    
+    public static final JavaTemplateNumberFormatFactory INSTANCE = new JavaTemplateNumberFormatFactory();
+    
+    private static final Logger LOG = _CoreLogs.RUNTIME;
+
+    private static final ConcurrentHashMap<CacheKey, NumberFormat> GLOBAL_FORMAT_CACHE
+            = new ConcurrentHashMap<>();
+    private static final int LEAK_ALERT_NUMBER_FORMAT_CACHE_SIZE = 1024;
+    
+    private JavaTemplateNumberFormatFactory() {
+        // Not meant to be instantiated
+    }
+
+    @Override
+    public TemplateNumberFormat get(String params, Locale locale, Environment env)
+            throws InvalidFormatParametersException {
+        CacheKey cacheKey = new CacheKey(params, locale);
+        NumberFormat jFormat = GLOBAL_FORMAT_CACHE.get(cacheKey);
+        if (jFormat == null) {
+            if ("number".equals(params)) {
+                jFormat = NumberFormat.getNumberInstance(locale);
+            } else if ("currency".equals(params)) {
+                jFormat = NumberFormat.getCurrencyInstance(locale);
+            } else if ("percent".equals(params)) {
+                jFormat = NumberFormat.getPercentInstance(locale);
+            } else if ("computer".equals(params)) {
+                jFormat = env.getCNumberFormat();
+            } else {
+                try {
+                    jFormat = ExtendedDecimalFormatParser.parse(params, locale);
+                } catch (ParseException e) {
+                    String msg = e.getMessage();
+                    throw new InvalidFormatParametersException(
+                            msg != null ? msg : "Invalid DecimalFormat pattern", e);
+                }
+            }
+
+            if (GLOBAL_FORMAT_CACHE.size() >= LEAK_ALERT_NUMBER_FORMAT_CACHE_SIZE) {
+                boolean triggered = false;
+                synchronized (JavaTemplateNumberFormatFactory.class) {
+                    if (GLOBAL_FORMAT_CACHE.size() >= LEAK_ALERT_NUMBER_FORMAT_CACHE_SIZE) {
+                        triggered = true;
+                        GLOBAL_FORMAT_CACHE.clear();
+                    }
+                }
+                if (triggered) {
+                    LOG.warn("Global Java NumberFormat cache has exceeded {} entries => cache flushed. "
+                            + "Typical cause: Some template generates high variety of format pattern strings.",
+                            LEAK_ALERT_NUMBER_FORMAT_CACHE_SIZE);
+                }
+            }
+            
+            NumberFormat prevJFormat = GLOBAL_FORMAT_CACHE.putIfAbsent(cacheKey, jFormat);
+            if (prevJFormat != null) {
+                jFormat = prevJFormat;
+            }
+        }  // if cache miss
+        
+        // JFormat-s aren't thread-safe; must deepClone it
+        jFormat = (NumberFormat) jFormat.clone();
+        
+        return new JavaTemplateNumberFormat(jFormat, params);
+    }
+
+    private static final class CacheKey {
+        private final String pattern;
+        private final Locale locale;
+
+        CacheKey(String pattern, Locale locale) {
+            this.pattern = pattern;
+            this.locale = locale;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o instanceof CacheKey) {
+                CacheKey fk = (CacheKey) o;
+                return fk.pattern.equals(pattern) && fk.locale.equals(locale);
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return pattern.hashCode() ^ locale.hashCode();
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormat.java
new file mode 100644
index 0000000..37d64dc
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormat.java
@@ -0,0 +1,94 @@
+/*
+ * 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.valueformat.impl;
+
+import java.util.Date;
+import java.util.TimeZone;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.util._DateUtil;
+import org.apache.freemarker.core.util._DateUtil.CalendarFieldsToDateConverter;
+import org.apache.freemarker.core.util._DateUtil.DateParseException;
+import org.apache.freemarker.core.util._DateUtil.DateToISO8601CalendarFactory;
+import org.apache.freemarker.core.valueformat.InvalidFormatParametersException;
+import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException;
+
+/**
+ * XML Schema format.
+ */
+class XSTemplateDateFormat extends ISOLikeTemplateDateFormat {
+
+    XSTemplateDateFormat(
+            String settingValue, int parsingStart,
+            int dateType,
+            boolean zonelessInput,
+            TimeZone timeZone,
+            ISOLikeTemplateDateFormatFactory factory,
+            Environment env)
+            throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatParametersException {
+        super(settingValue, parsingStart, dateType, zonelessInput, timeZone, factory, env);
+    }
+    
+    @Override
+    protected String format(Date date, boolean datePart, boolean timePart, boolean offsetPart, int accuracy,
+            TimeZone timeZone, DateToISO8601CalendarFactory calendarFactory) {
+        return _DateUtil.dateToXSString(
+                date, datePart, timePart, offsetPart, accuracy, timeZone, calendarFactory);
+    }
+
+    @Override
+    protected Date parseDate(String s, TimeZone tz, CalendarFieldsToDateConverter calToDateConverter)
+            throws DateParseException {
+        return _DateUtil.parseXSDate(s, tz, calToDateConverter);
+    }
+
+    @Override
+    protected Date parseTime(String s, TimeZone tz, CalendarFieldsToDateConverter calToDateConverter)
+            throws DateParseException {
+        return _DateUtil.parseXSTime(s, tz, calToDateConverter);
+    }
+
+    @Override
+    protected Date parseDateTime(String s, TimeZone tz,
+            CalendarFieldsToDateConverter calToDateConverter) throws DateParseException {
+        return _DateUtil.parseXSDateTime(s, tz, calToDateConverter);
+    }
+
+    @Override
+    protected String getDateDescription() {
+        return "W3C XML Schema date";
+    }
+
+    @Override
+    protected String getTimeDescription() {
+        return "W3C XML Schema time";
+    }
+
+    @Override
+    protected String getDateTimeDescription() {
+        return "W3C XML Schema dateTime";
+    }
+
+    @Override
+    protected boolean isXSMode() {
+        return true;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormatFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormatFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormatFactory.java
new file mode 100644
index 0000000..352b353
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormatFactory.java
@@ -0,0 +1,51 @@
+/*
+ * 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.valueformat.impl;
+
+import java.util.Locale;
+import java.util.TimeZone;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.valueformat.InvalidFormatParametersException;
+import org.apache.freemarker.core.valueformat.TemplateDateFormat;
+import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException;
+
+/**
+ * Creates {@link TemplateDateFormat}-s that follows the W3C XML Schema date, time and dateTime syntax.
+ */
+public final class XSTemplateDateFormatFactory extends ISOLikeTemplateDateFormatFactory {
+    
+    public static final XSTemplateDateFormatFactory INSTANCE = new XSTemplateDateFormatFactory();
+
+    private XSTemplateDateFormatFactory() {
+        // Not meant to be instantiated
+    }
+
+    @Override
+    public TemplateDateFormat get(String params, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput,
+                                  Environment env) throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatParametersException {
+        // We don't cache these as creating them is cheap (only 10% speedup of ${d?string.xs} with caching)
+        return new XSTemplateDateFormat(
+                params, 2,
+                dateType, zonelessInput,
+                timeZone, this, env);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/package.html
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/package.html b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/package.html
new file mode 100644
index 0000000..ecfd725
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/package.html
@@ -0,0 +1,26 @@
+<!--
+  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.
+-->
+<html>
+<head>
+</head>
+<body>
+<p>Formatting values shown in templates: Standard implementations. This package is part of the published API, that
+is, user code can safely depend on it.</p>
+</body>
+</html>

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/package.html
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/package.html b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/package.html
new file mode 100644
index 0000000..21d4c1b
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/package.html
@@ -0,0 +1,25 @@
+<!--
+  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.
+-->
+<html>
+<head>
+</head>
+<body>
+<p>Formatting values shown in templates: Base classes/interfaces</p>
+</body>
+</html>

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/dom/AtAtKey.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/dom/AtAtKey.java b/freemarker-core/src/main/java/org/apache/freemarker/dom/AtAtKey.java
new file mode 100644
index 0000000..ca6ac6b
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/dom/AtAtKey.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.dom;
+
+/**
+ * The special hash keys that start with "@@".
+ */
+enum AtAtKey {
+    
+    MARKUP("@@markup"),
+    NESTED_MARKUP("@@nested_markup"),
+    ATTRIBUTES_MARKUP("@@attributes_markup"),
+    TEXT("@@text"),
+    START_TAG("@@start_tag"),
+    END_TAG("@@end_tag"),
+    QNAME("@@qname"),
+    NAMESPACE("@@namespace"),
+    LOCAL_NAME("@@local_name"),
+    ATTRIBUTES("@@"),
+    PREVIOUS_SIBLING_ELEMENT("@@previous_sibling_element"),
+    NEXT_SIBLING_ELEMENT("@@next_sibling_element");
+
+    private final String key;
+
+    public String getKey() {
+        return key;
+    }
+
+    AtAtKey(String key) {
+        this.key = key;
+    }
+    
+    public static boolean containsKey(String key) {
+        for (AtAtKey item : AtAtKey.values()) {
+            if (item.getKey().equals(key)) {
+                return true;
+            }
+        }
+        return false;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/dom/AttributeNodeModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/dom/AttributeNodeModel.java b/freemarker-core/src/main/java/org/apache/freemarker/dom/AttributeNodeModel.java
new file mode 100644
index 0000000..cc510c4
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/dom/AttributeNodeModel.java
@@ -0,0 +1,69 @@
+/*
+ * 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.dom;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.w3c.dom.Attr;
+
+class AttributeNodeModel extends NodeModel implements TemplateScalarModel {
+    
+    public AttributeNodeModel(Attr att) {
+        super(att);
+    }
+    
+    @Override
+    public String getAsString() {
+        return ((Attr) node).getValue();
+    }
+    
+    @Override
+    public String getNodeName() {
+        String result = node.getLocalName();
+        if (result == null || result.equals("")) {
+            result = node.getNodeName();
+        }
+        return result;
+    }
+    
+    @Override
+    public boolean isEmpty() {
+        return true;
+    }
+    
+    @Override
+    String getQualifiedName() {
+        String nsURI = node.getNamespaceURI();
+        if (nsURI == null || nsURI.equals(""))
+            return node.getNodeName();
+        Environment env = Environment.getCurrentEnvironment();
+        String defaultNS = env.getDefaultNS();
+        String prefix = null;
+        if (nsURI.equals(defaultNS)) {
+            prefix = "D";
+        } else {
+            prefix = env.getPrefixForNamespace(nsURI);
+        }
+        if (prefix == null) {
+            return null;
+        }
+        return prefix + ":" + node.getLocalName();
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/dom/CharacterDataNodeModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/dom/CharacterDataNodeModel.java b/freemarker-core/src/main/java/org/apache/freemarker/dom/CharacterDataNodeModel.java
new file mode 100644
index 0000000..264c0db
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/dom/CharacterDataNodeModel.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+ 
+package org.apache.freemarker.dom;
+
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.w3c.dom.CharacterData;
+import org.w3c.dom.Comment;
+
+class CharacterDataNodeModel extends NodeModel implements TemplateScalarModel {
+    
+    public CharacterDataNodeModel(CharacterData text) {
+        super(text);
+    }
+    
+    @Override
+    public String getAsString() {
+        return ((org.w3c.dom.CharacterData) node).getData();
+    }
+    
+    @Override
+    public String getNodeName() {
+        return (node instanceof Comment) ? "@comment" : "@text";
+    }
+    
+    @Override
+    public boolean isEmpty() {
+        return true;
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/dom/DocumentModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/dom/DocumentModel.java b/freemarker-core/src/main/java/org/apache/freemarker/dom/DocumentModel.java
new file mode 100644
index 0000000..876b3cf
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/dom/DocumentModel.java
@@ -0,0 +1,76 @@
+/*
+ * 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.dom;
+ 
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.w3c.dom.Document;
+import org.w3c.dom.NodeList;
+
+/**
+ * A class that wraps the root node of a parsed XML document, using
+ * the W3C DOM_WRAPPER API.
+ */
+
+class DocumentModel extends NodeModel implements TemplateHashModel {
+    
+    private ElementModel rootElement;
+    
+    DocumentModel(Document doc) {
+        super(doc);
+    }
+    
+    @Override
+    public String getNodeName() {
+        return "@document";
+    }
+    
+    @Override
+    public TemplateModel get(String key) throws TemplateModelException {
+        if (key.equals("*")) {
+            return getRootElement();
+        } else if (key.equals("**")) {
+            NodeList nl = ((Document) node).getElementsByTagName("*");
+            return new NodeListModel(nl, this);
+        } else if (DomStringUtil.isXMLNameLike(key)) {
+            ElementModel em = (ElementModel) NodeModel.wrap(((Document) node).getDocumentElement());
+            if (em.matchesName(key, Environment.getCurrentEnvironment())) {
+                return em;
+            } else {
+                return new NodeListModel(this);
+            }
+        }
+        return super.get(key);
+    }
+    
+    ElementModel getRootElement() {
+        if (rootElement == null) {
+            rootElement = (ElementModel) wrap(((Document) node).getDocumentElement());
+        }
+        return rootElement;
+    }
+    
+    @Override
+    public boolean isEmpty() {
+        return false;
+    }
+} 
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/dom/DocumentTypeModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/dom/DocumentTypeModel.java b/freemarker-core/src/main/java/org/apache/freemarker/dom/DocumentTypeModel.java
new file mode 100644
index 0000000..3448f77
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/dom/DocumentTypeModel.java
@@ -0,0 +1,56 @@
+/*
+ * 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.dom;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.w3c.dom.DocumentType;
+import org.w3c.dom.ProcessingInstruction;
+
+class DocumentTypeModel extends NodeModel {
+    
+    public DocumentTypeModel(DocumentType docType) {
+        super(docType);
+    }
+    
+    public String getAsString() {
+        return ((ProcessingInstruction) node).getData();
+    }
+    
+    public TemplateSequenceModel getChildren() throws TemplateModelException {
+        throw new TemplateModelException("entering the child nodes of a DTD node is not currently supported");
+    }
+    
+    @Override
+    public TemplateModel get(String key) throws TemplateModelException {
+        throw new TemplateModelException("accessing properties of a DTD is not currently supported");
+    }
+    
+    @Override
+    public String getNodeName() {
+        return "@document_type$" + node.getNodeName();
+    }
+    
+    @Override
+    public boolean isEmpty() {
+        return true;
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/dom/DomLog.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/dom/DomLog.java b/freemarker-core/src/main/java/org/apache/freemarker/dom/DomLog.java
new file mode 100644
index 0000000..a1f6f0c
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/dom/DomLog.java
@@ -0,0 +1,32 @@
+/*
+ * 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.dom;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+final class DomLog {
+
+    private DomLog() {
+        //
+    }
+
+    public static final Logger LOG = LoggerFactory.getLogger("org.apache.freemarker.dom");
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/dom/DomStringUtil.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/dom/DomStringUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/dom/DomStringUtil.java
new file mode 100644
index 0000000..f5b58f8
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/dom/DomStringUtil.java
@@ -0,0 +1,67 @@
+/*
+ * 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.dom;
+
+/**
+ * For internal use only; don't depend on this, there's no backward compatibility guarantee at all!
+ * This class is to work around the lack of module system in Java, i.e., so that other FreeMarker packages can
+ * access things inside this package that users shouldn't. 
+ */
+final class DomStringUtil {
+
+    private DomStringUtil() {
+        // Not meant to be instantiated
+    }
+
+    static boolean isXMLNameLike(String name) {
+        return isXMLNameLike(name, 0);
+    }
+    
+    /**
+     * Check if the name looks like an XML element name.
+     * 
+     * @param firstCharIdx The index of the character in the string parameter that we treat as the beginning of the
+     *      string to check. This is to spare substringing that has become more expensive in Java 7.  
+     * 
+     * @return whether the name is a valid XML element name. (This routine might only be 99% accurate. REVISIT)
+     */
+    static boolean isXMLNameLike(String name, int firstCharIdx) {
+        int ln = name.length();
+        for (int i = firstCharIdx; i < ln; i++) {
+            char c = name.charAt(i);
+            if (i == firstCharIdx && (c == '-' || c == '.' || Character.isDigit(c))) {
+                return false;
+            }
+            if (!Character.isLetterOrDigit(c) && c != '_' && c != '-' && c != '.') {
+                if (c == ':') {
+                    if (i + 1 < ln && name.charAt(i + 1) == ':') {
+                        // "::" is used in XPath
+                        return false;
+                    }
+                    // We don't return here, as a lonely ":" is allowed.
+                } else {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }    
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/dom/ElementModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/dom/ElementModel.java b/freemarker-core/src/main/java/org/apache/freemarker/dom/ElementModel.java
new file mode 100644
index 0000000..220f414
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/dom/ElementModel.java
@@ -0,0 +1,234 @@
+/*
+ * 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.dom;
+
+import java.util.Collections;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.Template;
+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.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.apache.freemarker.core.util._StringUtil;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+class ElementModel extends NodeModel implements TemplateScalarModel {
+
+    public ElementModel(Element element) {
+        super(element);
+    }
+    
+    @Override
+    public boolean isEmpty() {
+        return false;
+    }
+    
+    /**
+     * An Element node supports various hash keys.
+     * Any key that corresponds to the tag name of any child elements
+     * returns a sequence of those elements. The special key "*" returns 
+     * all the element's direct children.
+     * The "**" key return all the element's descendants in the order they
+     * occur in the document.
+     * Any key starting with '@' is taken to be the name of an element attribute.
+     * The special key "@@" returns a hash of all the element's attributes.
+     * The special key "/" returns the root document node associated with this element.
+     */
+    @Override
+    public TemplateModel get(String key) throws TemplateModelException {
+        if (key.equals("*")) {
+            NodeListModel ns = new NodeListModel(this);
+            TemplateSequenceModel children = getChildNodes();
+            for (int i = 0; i < children.size(); i++) {
+                NodeModel child = (NodeModel) children.get(i);
+                if (child.node.getNodeType() == Node.ELEMENT_NODE) {
+                    ns.add(child);
+                }
+            }
+            return ns;
+        } else if (key.equals("**")) {
+            return new NodeListModel(((Element) node).getElementsByTagName("*"), this);    
+        } else if (key.startsWith("@")) {
+            if (key.startsWith("@@")) {
+                if (key.equals(AtAtKey.ATTRIBUTES.getKey())) {
+                    return new NodeListModel(node.getAttributes(), this);
+                } else if (key.equals(AtAtKey.START_TAG.getKey())) {
+                    NodeOutputter nodeOutputter = new NodeOutputter(node);
+                    return new SimpleScalar(nodeOutputter.getOpeningTag((Element) node));
+                } else if (key.equals(AtAtKey.END_TAG.getKey())) {
+                    NodeOutputter nodeOutputter = new NodeOutputter(node);
+                    return new SimpleScalar(nodeOutputter.getClosingTag((Element) node));
+                } else if (key.equals(AtAtKey.ATTRIBUTES_MARKUP.getKey())) {
+                    StringBuilder buf = new StringBuilder();
+                    NodeOutputter nu = new NodeOutputter(node);
+                    nu.outputContent(node.getAttributes(), buf);
+                    return new SimpleScalar(buf.toString().trim());
+                } else if (key.equals(AtAtKey.PREVIOUS_SIBLING_ELEMENT.getKey())) {
+                    Node previousSibling = node.getPreviousSibling();
+                    while (previousSibling != null && !isSignificantNode(previousSibling)) {
+                        previousSibling = previousSibling.getPreviousSibling();
+                    }
+                    return previousSibling != null && previousSibling.getNodeType() == Node.ELEMENT_NODE
+                            ? wrap(previousSibling) : new NodeListModel(Collections.emptyList(), null);  
+                } else if (key.equals(AtAtKey.NEXT_SIBLING_ELEMENT.getKey())) {
+                    Node nextSibling = node.getNextSibling();
+                    while (nextSibling != null && !isSignificantNode(nextSibling)) {
+                        nextSibling = nextSibling.getNextSibling();
+                    }
+                    return nextSibling != null && nextSibling.getNodeType() == Node.ELEMENT_NODE
+                            ? wrap(nextSibling) : new NodeListModel(Collections.emptyList(), null);  
+                } else {
+                    // We don't know anything like this that's element-specific; fall back 
+                    return super.get(key);
+                }
+            } else { // Starts with "@", but not with "@@"
+                if (DomStringUtil.isXMLNameLike(key, 1)) {
+                    Attr att = getAttribute(key.substring(1));
+                    if (att == null) { 
+                        return new NodeListModel(this);
+                    }
+                    return wrap(att);
+                } else if (key.equals("@*")) {
+                    return new NodeListModel(node.getAttributes(), this);
+                } else {
+                    // We don't know anything like this that's element-specific; fall back 
+                    return super.get(key);
+                }
+            }
+        } else if (DomStringUtil.isXMLNameLike(key)) {
+            // We interpret key as an element name
+            NodeListModel result = ((NodeListModel) getChildNodes()).filterByName(key);
+            return result.size() != 1 ? result : result.get(0);
+        } else {
+            // We don't anything like this that's element-specific; fall back 
+            return super.get(key);
+        }
+    }
+
+    @Override
+    public String getAsString() throws TemplateModelException {
+        NodeList nl = node.getChildNodes();
+        String result = "";
+        for (int i = 0; i < nl.getLength(); i++) {
+            Node child = nl.item(i);
+            int nodeType = child.getNodeType();
+            if (nodeType == Node.ELEMENT_NODE) {
+                String msg = "Only elements with no child elements can be processed as text."
+                             + "\nThis element with name \""
+                             + node.getNodeName()
+                             + "\" has a child element named: " + child.getNodeName();
+                throw new TemplateModelException(msg);
+            } else if (nodeType == Node.TEXT_NODE || nodeType == Node.CDATA_SECTION_NODE) {
+                result += child.getNodeValue();
+            }
+        }
+        return result;
+    }
+    
+    @Override
+    public String getNodeName() {
+        String result = node.getLocalName();
+        if (result == null || result.equals("")) {
+            result = node.getNodeName();
+        }
+        return result;
+    }
+    
+    @Override
+    String getQualifiedName() {
+        String nodeName = getNodeName();
+        String nsURI = getNodeNamespace();
+        if (nsURI == null || nsURI.length() == 0) {
+            return nodeName;
+        }
+        Environment env = Environment.getCurrentEnvironment();
+        String defaultNS = env.getDefaultNS();
+        String prefix;
+        if (defaultNS != null && defaultNS.equals(nsURI)) {
+            prefix = "";
+        } else {
+            prefix = env.getPrefixForNamespace(nsURI);
+            
+        }
+        if (prefix == null) {
+            return null; // We have no qualified name, because there is no prefix mapping
+        }
+        if (prefix.length() > 0) {
+            prefix += ":";
+        }
+        return prefix + nodeName;
+    }
+    
+    private Attr getAttribute(String qname) {
+        Element element = (Element) node;
+        Attr result = element.getAttributeNode(qname);
+        if (result != null)
+            return result;
+        int colonIndex = qname.indexOf(':');
+        if (colonIndex > 0) {
+            String prefix = qname.substring(0, colonIndex);
+            String uri;
+            if (prefix.equals(Template.DEFAULT_NAMESPACE_PREFIX)) {
+                uri = Environment.getCurrentEnvironment().getDefaultNS();
+            } else {
+                uri = Environment.getCurrentEnvironment().getNamespaceForPrefix(prefix);
+            }
+            String localName = qname.substring(1 + colonIndex);
+            if (uri != null) {
+                result = element.getAttributeNodeNS(uri, localName);
+            }
+        }
+        return result;
+    }
+    
+    private boolean isSignificantNode(Node node) throws TemplateModelException {
+        return (node.getNodeType() == Node.TEXT_NODE || node.getNodeType() == Node.CDATA_SECTION_NODE)
+                ? !isBlankXMLText(node.getTextContent())
+                : node.getNodeType() != Node.PROCESSING_INSTRUCTION_NODE && node.getNodeType() != Node.COMMENT_NODE;
+    }
+    
+    private boolean isBlankXMLText(String s) {
+        if (s == null) {
+            return true;
+        }
+        for (int i = 0; i < s.length(); i++) {
+            if (!isXMLWhiteSpace(s.charAt(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * White space according the XML spec. 
+     */
+    private boolean isXMLWhiteSpace(char c) {
+        return c == ' ' || c == '\t' || c == '\n' | c == '\r';
+    }
+
+    boolean matchesName(String name, Environment env) {
+        return _StringUtil.matchesQName(name, getNodeName(), getNodeNamespace(), env);
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/dom/JaxenXPathSupport.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/dom/JaxenXPathSupport.java b/freemarker-core/src/main/java/org/apache/freemarker/dom/JaxenXPathSupport.java
new file mode 100644
index 0000000..3e52836
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/dom/JaxenXPathSupport.java
@@ -0,0 +1,243 @@
+/*
+ * 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.dom;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.apache.freemarker.core.CustomStateKey;
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.util.UndeclaredThrowableException;
+import org.apache.freemarker.core.util._ObjectHolder;
+import org.jaxen.BaseXPath;
+import org.jaxen.Function;
+import org.jaxen.FunctionCallException;
+import org.jaxen.FunctionContext;
+import org.jaxen.JaxenException;
+import org.jaxen.NamespaceContext;
+import org.jaxen.Navigator;
+import org.jaxen.UnresolvableException;
+import org.jaxen.VariableContext;
+import org.jaxen.XPathFunctionContext;
+import org.jaxen.dom.DocumentNavigator;
+import org.w3c.dom.Document;
+import org.xml.sax.EntityResolver;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+
+/**
+ */
+class JaxenXPathSupport implements XPathSupport {
+
+    private static final CustomStateKey<Map<String, BaseXPath>> XPATH_CACHE_ATTR
+            = new CustomStateKey<Map<String, BaseXPath>>() {
+        @Override
+        protected Map<String, BaseXPath> create() {
+            return new HashMap<String, BaseXPath>();
+        }
+    };
+
+        // [2.4] Can't we just use Collections.emptyList()? 
+    private final static ArrayList EMPTY_ARRAYLIST = new ArrayList();
+
+    @Override
+    public TemplateModel executeQuery(Object context, String xpathQuery) throws TemplateModelException {
+        try {
+            BaseXPath xpath;
+            Map<String, BaseXPath> xpathCache = Environment.getCurrentEnvironmentNotNull().getCurrentTemplateNotNull()
+                    .getCustomState(XPATH_CACHE_ATTR);
+            synchronized (xpathCache) {
+                xpath = xpathCache.get(xpathQuery);
+                if (xpath == null) {
+                    xpath = new BaseXPath(xpathQuery, FM_DOM_NAVIGATOR);
+                    xpath.setNamespaceContext(customNamespaceContext);
+                    xpath.setFunctionContext(FM_FUNCTION_CONTEXT);
+                    xpath.setVariableContext(FM_VARIABLE_CONTEXT);
+                    xpathCache.put(xpathQuery, xpath);
+                }
+            }
+            List result = xpath.selectNodes(context != null ? context : EMPTY_ARRAYLIST);
+            if (result.size() == 1) {
+                return NodeQueryResultItemObjectWrapper.INSTANCE.wrap(result.get(0));
+            }
+            NodeListModel nlm = new NodeListModel(result, null);
+            nlm.xpathSupport = this;
+            return nlm;
+        } catch (UndeclaredThrowableException e) {
+            Throwable t  = e.getUndeclaredThrowable();
+            if (t instanceof TemplateModelException) {
+                throw (TemplateModelException) t;
+            }
+            throw e;
+        } catch (JaxenException je) {
+            throw new TemplateModelException(je);
+        }
+    }
+
+    static private final NamespaceContext customNamespaceContext = new NamespaceContext() {
+        
+        @Override
+        public String translateNamespacePrefixToUri(String prefix) {
+            if (prefix.equals(Template.DEFAULT_NAMESPACE_PREFIX)) {
+                return Environment.getCurrentEnvironment().getDefaultNS();
+            }
+            return Environment.getCurrentEnvironment().getNamespaceForPrefix(prefix);
+        }
+    };
+
+    private static final VariableContext FM_VARIABLE_CONTEXT = new VariableContext() {
+        @Override
+        public Object getVariableValue(String namespaceURI, String prefix, String localName)
+        throws UnresolvableException {
+            try {
+                TemplateModel model = Environment.getCurrentEnvironment().getVariable(localName);
+                if (model == null) {
+                    throw new UnresolvableException("Variable \"" + localName + "\" not found.");
+                }
+                if (model instanceof TemplateScalarModel) {
+                    return ((TemplateScalarModel) model).getAsString();
+                }
+                if (model instanceof TemplateNumberModel) {
+                    return ((TemplateNumberModel) model).getAsNumber();
+                }
+                if (model instanceof TemplateDateModel) {
+                    return ((TemplateDateModel) model).getAsDate();
+                }
+                if (model instanceof TemplateBooleanModel) {
+                    return Boolean.valueOf(((TemplateBooleanModel) model).getAsBoolean());
+                }
+            } catch (TemplateModelException e) {
+                throw new UndeclaredThrowableException(e);
+            }
+            throw new UnresolvableException(
+                    "Variable \"" + localName + "\" exists, but it's not a string, number, date, or boolean");
+        }
+    };
+     
+    private static final FunctionContext FM_FUNCTION_CONTEXT = new XPathFunctionContext() {
+        @Override
+        public Function getFunction(String namespaceURI, String prefix, String localName)
+        throws UnresolvableException {
+            try {
+                return super.getFunction(namespaceURI, prefix, localName);
+            } catch (UnresolvableException e) {
+                return super.getFunction(null, null, localName);
+            }
+        }
+    };
+    
+    /**
+     * Stores the the template parsed as {@link Document} in the template itself.
+     */
+    private static final CustomStateKey<_ObjectHolder<Document>> FM_DOM_NAVIAGOTOR_CACHED_DOM
+            = new CustomStateKey<_ObjectHolder<Document>>() {
+        @Override
+        protected _ObjectHolder<Document> create() {
+            return new _ObjectHolder<>(null);
+        }
+    };
+     
+    private static final Navigator FM_DOM_NAVIGATOR = new DocumentNavigator() {
+        @Override
+        public Object getDocument(String uri) throws FunctionCallException {
+            try {
+                Template raw = getTemplate(uri);
+                _ObjectHolder<Document> docHolder = Environment.getCurrentEnvironmentNotNull()
+                        .getCurrentTemplateNotNull().getCustomState(FM_DOM_NAVIAGOTOR_CACHED_DOM);
+                synchronized (docHolder) {
+                    Document doc = docHolder.get();
+                    if (doc == null) {
+                        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+                        factory.setNamespaceAware(true);
+                        DocumentBuilder builder = factory.newDocumentBuilder();
+                        FmEntityResolver er = new FmEntityResolver();
+                        builder.setEntityResolver(er);
+                        doc = builder.parse(createInputSource(null, raw));
+                        // If the entity resolver got called 0 times, the document
+                        // is standalone, so we can safely cache it
+                        if (er.getCallCount() == 0) {
+                            docHolder.set(doc);
+                        }
+                    }
+                    return doc;
+                }
+            } catch (Exception e) {
+                throw new FunctionCallException("Failed to parse document for URI: " + uri, e);
+            }
+        }
+    };
+
+    // [FM3] Look into this "hidden" feature
+    static Template getTemplate(String systemId) throws IOException {
+        Environment env = Environment.getCurrentEnvironment();
+        String templatePath = env.getCurrentTemplate().getLookupName();
+        int lastSlash = templatePath.lastIndexOf('/');
+        templatePath = lastSlash == -1 ? "" : templatePath.substring(0, lastSlash + 1);
+        systemId = env.toFullTemplateName(templatePath, systemId);
+        return env.getConfiguration().getTemplate(systemId, env.getLocale());
+    }
+
+    private static InputSource createInputSource(String publicId, Template raw) throws IOException, SAXException {
+        StringWriter sw = new StringWriter();
+        try {
+            raw.process(Collections.EMPTY_MAP, sw);
+        } catch (TemplateException e) {
+            throw new SAXException(e);
+        }
+        InputSource is = new InputSource();
+        is.setPublicId(publicId);
+        is.setSystemId(raw.getLookupName());
+        is.setCharacterStream(new StringReader(sw.toString()));
+        return is;
+    }
+
+    private static class FmEntityResolver implements EntityResolver {
+        private int callCount = 0;
+        
+        @Override
+        public InputSource resolveEntity(String publicId, String systemId)
+        throws SAXException, IOException {
+            ++callCount;
+            return createInputSource(publicId, getTemplate(systemId));
+        }
+        
+        int getCallCount() {
+            return callCount;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/dom/NodeListModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/dom/NodeListModel.java b/freemarker-core/src/main/java/org/apache/freemarker/dom/NodeListModel.java
new file mode 100644
index 0000000..333bb5c
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/dom/NodeListModel.java
@@ -0,0 +1,219 @@
+/*
+ * 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.dom;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core._UnexpectedTypeErrorExplainerTemplateModel;
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNodeModel;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.apache.freemarker.core.model.impl.SimpleSequence;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+/**
+ * Used when the result set contains 0 or multiple nodes; shouldn't be used when you have exactly 1 node. For exactly 1
+ * node, use {@link NodeModel#wrap(Node)}, because {@link NodeModel} subclasses can have extra features building on that
+ * restriction, like single elements with text content can be used as FTL string-s.
+ * <p>
+ * This class is not guaranteed to be thread safe, so instances of this shouldn't be used as
+ * {@linkplain Configuration#getSharedVariables() shared variable}.
+ */
+class NodeListModel extends SimpleSequence implements TemplateHashModel, _UnexpectedTypeErrorExplainerTemplateModel {
+    
+    // [2.4] make these private
+    NodeModel contextNode;
+    XPathSupport xpathSupport;
+    
+    NodeListModel(Node contextNode) {
+        this(NodeModel.wrap(contextNode));
+    }
+    
+    NodeListModel(NodeModel contextNode) {
+        super(NodeQueryResultItemObjectWrapper.INSTANCE);
+        this.contextNode = contextNode;
+    }
+    
+    NodeListModel(NodeList nodeList, NodeModel contextNode) {
+        super(NodeQueryResultItemObjectWrapper.INSTANCE);
+        for (int i = 0; i < nodeList.getLength(); i++) {
+            list.add(nodeList.item(i));
+        }
+        this.contextNode = contextNode;
+    }
+    
+    NodeListModel(NamedNodeMap nodeList, NodeModel contextNode) {
+        super(NodeQueryResultItemObjectWrapper.INSTANCE);
+        for (int i = 0; i < nodeList.getLength(); i++) {
+            list.add(nodeList.item(i));
+        }
+        this.contextNode = contextNode;
+    }
+    
+    NodeListModel(List list, NodeModel contextNode) {
+        super(list, NodeQueryResultItemObjectWrapper.INSTANCE);
+        this.contextNode = contextNode;
+    }
+    
+    NodeListModel filterByName(String name) throws TemplateModelException {
+        NodeListModel result = new NodeListModel(contextNode);
+        int size = size();
+        if (size == 0) {
+            return result;
+        }
+        Environment env = Environment.getCurrentEnvironment();
+        for (int i = 0; i < size; i++) {
+            NodeModel nm = (NodeModel) get(i);
+            if (nm instanceof ElementModel) {
+                if (((ElementModel) nm).matchesName(name, env)) {
+                    result.add(nm);
+                }
+            }
+        }
+        return result;
+    }
+    
+    @Override
+    public boolean isEmpty() {
+        return size() == 0;
+    }
+    
+    @Override
+    public TemplateModel get(String key) throws TemplateModelException {
+        if (size() == 1) {
+            NodeModel nm = (NodeModel) get(0);
+            return nm.get(key);
+        }
+        if (key.startsWith("@@")) {
+            if (key.equals(AtAtKey.MARKUP.getKey()) 
+                    || key.equals(AtAtKey.NESTED_MARKUP.getKey()) 
+                    || key.equals(AtAtKey.TEXT.getKey())) {
+                StringBuilder result = new StringBuilder();
+                for (int i = 0; i < size(); i++) {
+                    NodeModel nm = (NodeModel) get(i);
+                    TemplateScalarModel textModel = (TemplateScalarModel) nm.get(key);
+                    result.append(textModel.getAsString());
+                }
+                return new SimpleScalar(result.toString());
+            } else if (key.length() != 2 /* to allow "@@" to fall through */) {
+                // As @@... would cause exception in the XPath engine, we throw a nicer exception now. 
+                if (AtAtKey.containsKey(key)) {
+                    throw new TemplateModelException(
+                            "\"" + key + "\" is only applicable to a single XML node, but it was applied on "
+                            + (size() != 0
+                                    ? size() + " XML nodes (multiple matches)."
+                                    : "an empty list of XML nodes (no matches)."));
+                } else {
+                    throw new TemplateModelException("Unsupported @@ key: " + key);
+                }
+            }
+        }
+        if (DomStringUtil.isXMLNameLike(key) 
+                || ((key.startsWith("@")
+                        && (DomStringUtil.isXMLNameLike(key, 1)  || key.equals("@@") || key.equals("@*"))))
+                || key.equals("*") || key.equals("**")) {
+            NodeListModel result = new NodeListModel(contextNode);
+            for (int i = 0; i < size(); i++) {
+                NodeModel nm = (NodeModel) get(i);
+                if (nm instanceof ElementModel) {
+                    TemplateSequenceModel tsm = (TemplateSequenceModel) nm.get(key);
+                    if (tsm != null) {
+                        int size = tsm.size();
+                        for (int j = 0; j < size; j++) {
+                            result.add(tsm.get(j));
+                        }
+                    }
+                }
+            }
+            if (result.size() == 1) {
+                return result.get(0);
+            }
+            return result;
+        }
+        XPathSupport xps = getXPathSupport();
+        if (xps != null) {
+            Object context = (size() == 0) ? null : rawNodeList(); 
+            return xps.executeQuery(context, key);
+        } else {
+            throw new TemplateModelException(
+                    "Can't try to resolve the XML query key, because no XPath support is available. "
+                    + "This is either malformed or an XPath expression: " + key);
+        }
+    }
+    
+    private List rawNodeList() throws TemplateModelException {
+        int size = size();
+        ArrayList al = new ArrayList(size);
+        for (int i = 0; i < size; i++) {
+            al.add(((NodeModel) get(i)).node);
+        }
+        return al;
+    }
+    
+    XPathSupport getXPathSupport() throws TemplateModelException {
+        if (xpathSupport == null) {
+            if (contextNode != null) {
+                xpathSupport = contextNode.getXPathSupport();
+            } else if (size() > 0) {
+                xpathSupport = ((NodeModel) get(0)).getXPathSupport();
+            }
+        }
+        return xpathSupport;
+    }
+
+    @Override
+    public Object[] explainTypeError(Class[] expectedClasses) {
+        for (Class expectedClass : expectedClasses) {
+            if (TemplateScalarModel.class.isAssignableFrom(expectedClass)
+                    || TemplateDateModel.class.isAssignableFrom(expectedClass)
+                    || TemplateNumberModel.class.isAssignableFrom(expectedClass)
+                    || TemplateBooleanModel.class.isAssignableFrom(expectedClass)) {
+                return newTypeErrorExplanation("string");
+            } else if (TemplateNodeModel.class.isAssignableFrom(expectedClass)) {
+                return newTypeErrorExplanation("node");
+            }
+        }
+        return null;
+    }
+
+    private Object[] newTypeErrorExplanation(String type) {
+        return new Object[] {
+                "This XML query result can't be used as ", type, " because for that it had to contain exactly "
+                + "1 XML node, but it contains ", Integer.valueOf(size()), " nodes. "
+                + "That is, the constructing XML query has found ",
+                isEmpty()
+                    ? "no matches."
+                    : "multiple matches."
+                };
+    }
+
+}
\ No newline at end of file


[45/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpAddOrConcat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpAddOrConcat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpAddOrConcat.java
new file mode 100644
index 0000000..15e632a
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpAddOrConcat.java
@@ -0,0 +1,313 @@
+/*
+ * 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.util.HashSet;
+import java.util.Set;
+
+import org.apache.freemarker.core.arithmetic.ArithmeticEngine;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.impl.CollectionAndSequence;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+
+/**
+ * AST expression node: binary {@code +} operator. Note that this is treated separately from the other 4 arithmetic
+ * operators, since it's overloaded to mean concatenation of string-s, sequences and hash-es too.
+ */
+final class ASTExpAddOrConcat extends ASTExpression {
+
+    private final ASTExpression left;
+    private final ASTExpression right;
+
+    ASTExpAddOrConcat(ASTExpression left, ASTExpression right) {
+        this.left = left;
+        this.right = right;
+    }
+
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        return _eval(env, this, left, left.eval(env), right, right.eval(env));
+    }
+
+    /**
+     * @param leftExp
+     *            Used for error messages only; can be {@code null}
+     * @param rightExp
+     *            Used for error messages only; can be {@code null}
+     */
+    static TemplateModel _eval(Environment env,
+            ASTNode parent,
+            ASTExpression leftExp, TemplateModel leftModel,
+            ASTExpression rightExp, TemplateModel rightModel)
+            throws TemplateException {
+        if (leftModel instanceof TemplateNumberModel && rightModel instanceof TemplateNumberModel) {
+            Number first = _EvalUtil.modelToNumber((TemplateNumberModel) leftModel, leftExp);
+            Number second = _EvalUtil.modelToNumber((TemplateNumberModel) rightModel, rightExp);
+            return _evalOnNumbers(env, parent, first, second);
+        } else if (leftModel instanceof TemplateSequenceModel && rightModel instanceof TemplateSequenceModel) {
+            return new ConcatenatedSequence((TemplateSequenceModel) leftModel, (TemplateSequenceModel) rightModel);
+        } else {
+            boolean hashConcatPossible
+                    = leftModel instanceof TemplateHashModel && rightModel instanceof TemplateHashModel;
+            try {
+                // We try string addition first. If hash addition is possible, then instead of throwing exception
+                // we return null and do hash addition instead. (We can't simply give hash addition a priority, like
+                // with sequence addition above, as FTL strings are often also FTL hashes.)
+                Object leftOMOrStr = _EvalUtil.coerceModelToStringOrMarkup(
+                        leftModel, leftExp, /* returnNullOnNonCoercableType = */ hashConcatPossible, null,
+                        env);
+                if (leftOMOrStr == null) {
+                    return _eval_concatenateHashes(leftModel, rightModel);
+                }
+
+                // Same trick with null return as above.
+                Object rightOMOrStr = _EvalUtil.coerceModelToStringOrMarkup(
+                        rightModel, rightExp, /* returnNullOnNonCoercableType = */ hashConcatPossible, null,
+                        env);
+                if (rightOMOrStr == null) {
+                    return _eval_concatenateHashes(leftModel, rightModel);
+                }
+
+                if (leftOMOrStr instanceof String) {
+                    if (rightOMOrStr instanceof String) {
+                        return new SimpleScalar(((String) leftOMOrStr).concat((String) rightOMOrStr));
+                    } else { // rightOMOrStr instanceof TemplateMarkupOutputModel
+                        TemplateMarkupOutputModel<?> rightMO = (TemplateMarkupOutputModel<?>) rightOMOrStr; 
+                        return _EvalUtil.concatMarkupOutputs(parent,
+                                rightMO.getOutputFormat().fromPlainTextByEscaping((String) leftOMOrStr),
+                                rightMO);
+                    }                    
+                } else { // leftOMOrStr instanceof TemplateMarkupOutputModel 
+                    TemplateMarkupOutputModel<?> leftMO = (TemplateMarkupOutputModel<?>) leftOMOrStr; 
+                    if (rightOMOrStr instanceof String) {  // markup output
+                        return _EvalUtil.concatMarkupOutputs(parent,
+                                leftMO,
+                                leftMO.getOutputFormat().fromPlainTextByEscaping((String) rightOMOrStr));
+                    } else { // rightOMOrStr instanceof TemplateMarkupOutputModel
+                        return _EvalUtil.concatMarkupOutputs(parent,
+                                leftMO,
+                                (TemplateMarkupOutputModel<?>) rightOMOrStr);
+                    }
+                }
+            } catch (NonStringOrTemplateOutputException e) {
+                // 2.4: Remove this catch; it's for BC, after reworking hash addition so it doesn't rely on this. But
+                // user code might throws this (very unlikely), and then in 2.3.x we did catch that too, incorrectly.
+                if (hashConcatPossible) {
+                    return _eval_concatenateHashes(leftModel, rightModel);
+                } else {
+                    throw e;
+                }
+            }
+        }
+    }
+
+    private static TemplateModel _eval_concatenateHashes(TemplateModel leftModel, TemplateModel rightModel)
+            throws TemplateModelException {
+        if (leftModel instanceof TemplateHashModelEx && rightModel instanceof TemplateHashModelEx) {
+            TemplateHashModelEx leftModelEx = (TemplateHashModelEx) leftModel;
+            TemplateHashModelEx rightModelEx = (TemplateHashModelEx) rightModel;
+            if (leftModelEx.size() == 0) {
+                return rightModelEx;
+            } else if (rightModelEx.size() == 0) {
+                return leftModelEx;
+            } else {
+                return new ConcatenatedHashEx(leftModelEx, rightModelEx);
+            }
+        } else {
+            return new ConcatenatedHash((TemplateHashModel) leftModel,
+                                        (TemplateHashModel) rightModel);
+        }
+    }
+
+    static TemplateModel _evalOnNumbers(Environment env, ASTNode parent, Number first, Number second)
+            throws TemplateException {
+        ArithmeticEngine ae = _EvalUtil.getArithmeticEngine(env, parent);
+        return new SimpleNumber(ae.add(first, second));
+    }
+
+    @Override
+    boolean isLiteral() {
+        return constantValue != null || (left.isLiteral() && right.isLiteral());
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+    	return new ASTExpAddOrConcat(
+    	left.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
+    	right.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState));
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return left.getCanonicalForm() + " + " + right.getCanonicalForm();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "+";
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        return idx == 0 ? left : right;
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        return ParameterRole.forBinaryOperatorOperand(idx);
+    }
+
+    private static final class ConcatenatedSequence
+    implements
+        TemplateSequenceModel {
+        private final TemplateSequenceModel left;
+        private final TemplateSequenceModel right;
+
+        ConcatenatedSequence(TemplateSequenceModel left, TemplateSequenceModel right) {
+            this.left = left;
+            this.right = right;
+        }
+
+        @Override
+        public int size()
+        throws TemplateModelException {
+            return left.size() + right.size();
+        }
+
+        @Override
+        public TemplateModel get(int i)
+        throws TemplateModelException {
+            int ls = left.size();
+            return i < ls ? left.get(i) : right.get(i - ls);
+        }
+    }
+
+    private static class ConcatenatedHash
+    implements TemplateHashModel {
+        protected final TemplateHashModel left;
+        protected final TemplateHashModel right;
+
+        ConcatenatedHash(TemplateHashModel left, TemplateHashModel right) {
+            this.left = left;
+            this.right = right;
+        }
+        
+        @Override
+        public TemplateModel get(String key)
+        throws TemplateModelException {
+            TemplateModel model = right.get(key);
+            return (model != null) ? model : left.get(key);
+        }
+
+        @Override
+        public boolean isEmpty()
+        throws TemplateModelException {
+            return left.isEmpty() && right.isEmpty();
+        }
+    }
+
+    private static final class ConcatenatedHashEx
+    extends ConcatenatedHash
+    implements TemplateHashModelEx {
+        private CollectionAndSequence keys;
+        private CollectionAndSequence values;
+        private int size;
+
+        ConcatenatedHashEx(TemplateHashModelEx left, TemplateHashModelEx right) {
+            super(left, right);
+        }
+        
+        @Override
+        public int size() throws TemplateModelException {
+            initKeys();
+            return size;
+        }
+
+        @Override
+        public TemplateCollectionModel keys()
+        throws TemplateModelException {
+            initKeys();
+            return keys;
+        }
+
+        @Override
+        public TemplateCollectionModel values()
+        throws TemplateModelException {
+            initValues();
+            return values;
+        }
+
+        private void initKeys()
+        throws TemplateModelException {
+            if (keys == null) {
+                HashSet keySet = new HashSet();
+                NativeSequence keySeq = new NativeSequence(32);
+                addKeys(keySet, keySeq, (TemplateHashModelEx) left);
+                addKeys(keySet, keySeq, (TemplateHashModelEx) right);
+                size = keySet.size();
+                keys = new CollectionAndSequence(keySeq);
+            }
+        }
+
+        private static void addKeys(Set set, NativeSequence keySeq, TemplateHashModelEx hash)
+        throws TemplateModelException {
+            TemplateModelIterator it = hash.keys().iterator();
+            while (it.hasNext()) {
+                TemplateScalarModel tsm = (TemplateScalarModel) it.next();
+                if (set.add(tsm.getAsString())) {
+                    // The first occurence of the key decides the index;
+                    // this is consisten with stuff like java.util.LinkedHashSet.
+                    keySeq.add(tsm);
+                }
+            }
+        }        
+
+        private void initValues()
+        throws TemplateModelException {
+            if (values == null) {
+                NativeSequence seq = new NativeSequence(size());
+                // Note: size() invokes initKeys() if needed.
+            
+                int ln = keys.size();
+                for (int i  = 0; i < ln; i++) {
+                    seq.add(get(((TemplateScalarModel) keys.get(i)).getAsString()));
+                }
+                values = new CollectionAndSequence(seq);
+            }
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpAnd.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpAnd.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpAnd.java
new file mode 100644
index 0000000..346d526
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpAnd.java
@@ -0,0 +1,82 @@
+/*
+ * 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;
+
+/**
+ * AST expression node: {@code &&} operator
+ */
+final class ASTExpAnd extends ASTExpBoolean {
+
+    private final ASTExpression lho;
+    private final ASTExpression rho;
+
+    ASTExpAnd(ASTExpression lho, ASTExpression rho) {
+        this.lho = lho;
+        this.rho = rho;
+    }
+
+    @Override
+    boolean evalToBoolean(Environment env) throws TemplateException {
+        return lho.evalToBoolean(env) && rho.evalToBoolean(env);
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return lho.getCanonicalForm() + " && " + rho.getCanonicalForm();
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return "&&";
+    }
+    
+    @Override
+    boolean isLiteral() {
+        return constantValue != null || (lho.isLiteral() && rho.isLiteral());
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+    	return new ASTExpAnd(
+    	        lho.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
+    	        rho.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState));
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return lho;
+        case 1: return rho;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        return ParameterRole.forBinaryOperatorOperand(idx);
+    }
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBoolean.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBoolean.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBoolean.java
new file mode 100644
index 0000000..d580372
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBoolean.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateModel;
+
+/**
+ * AST expression node superclass for expressions returning a boolean.
+ */
+abstract class ASTExpBoolean extends ASTExpression {
+
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        return evalToBoolean(env) ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBooleanLiteral.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBooleanLiteral.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBooleanLiteral.java
new file mode 100644
index 0000000..e38578b
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBooleanLiteral.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateModel;
+
+/**
+ * AST expression node: boolean literal 
+ */
+final class ASTExpBooleanLiteral extends ASTExpression {
+
+    private final boolean val;
+
+    public ASTExpBooleanLiteral(boolean val) {
+        this.val = val;
+    }
+
+    static TemplateBooleanModel getTemplateModel(boolean b) {
+        return b? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+    }
+
+    @Override
+    boolean evalToBoolean(Environment env) {
+        return val;
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return val ? MiscUtil.C_TRUE : MiscUtil.C_FALSE;
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return getCanonicalForm();
+    }
+    
+    @Override
+    public String toString() {
+        return val ? MiscUtil.C_TRUE : MiscUtil.C_FALSE;
+    }
+
+    @Override
+    TemplateModel _eval(Environment env) {
+        return val ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+    }
+    
+    @Override
+    boolean isLiteral() {
+        return true;
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+    	return new ASTExpBooleanLiteral(val);
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 0;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltIn.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltIn.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltIn.java
new file mode 100644
index 0000000..be559f6
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltIn.java
@@ -0,0 +1,485 @@
+/*
+ * 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.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.apache.freemarker.core.BuiltInsForDates.iso_BI;
+import org.apache.freemarker.core.BuiltInsForDates.iso_utc_or_local_BI;
+import org.apache.freemarker.core.BuiltInsForMarkupOutputs.markup_stringBI;
+import org.apache.freemarker.core.BuiltInsForMultipleTypes.is_dateLikeBI;
+import org.apache.freemarker.core.BuiltInsForNodes.ancestorsBI;
+import org.apache.freemarker.core.BuiltInsForNodes.childrenBI;
+import org.apache.freemarker.core.BuiltInsForNodes.nextSiblingBI;
+import org.apache.freemarker.core.BuiltInsForNodes.node_nameBI;
+import org.apache.freemarker.core.BuiltInsForNodes.node_namespaceBI;
+import org.apache.freemarker.core.BuiltInsForNodes.node_typeBI;
+import org.apache.freemarker.core.BuiltInsForNodes.parentBI;
+import org.apache.freemarker.core.BuiltInsForNodes.previousSiblingBI;
+import org.apache.freemarker.core.BuiltInsForNodes.rootBI;
+import org.apache.freemarker.core.BuiltInsForNumbers.absBI;
+import org.apache.freemarker.core.BuiltInsForNumbers.byteBI;
+import org.apache.freemarker.core.BuiltInsForNumbers.ceilingBI;
+import org.apache.freemarker.core.BuiltInsForNumbers.doubleBI;
+import org.apache.freemarker.core.BuiltInsForNumbers.floatBI;
+import org.apache.freemarker.core.BuiltInsForNumbers.floorBI;
+import org.apache.freemarker.core.BuiltInsForNumbers.intBI;
+import org.apache.freemarker.core.BuiltInsForNumbers.is_infiniteBI;
+import org.apache.freemarker.core.BuiltInsForNumbers.is_nanBI;
+import org.apache.freemarker.core.BuiltInsForNumbers.longBI;
+import org.apache.freemarker.core.BuiltInsForNumbers.number_to_dateBI;
+import org.apache.freemarker.core.BuiltInsForNumbers.roundBI;
+import org.apache.freemarker.core.BuiltInsForNumbers.shortBI;
+import org.apache.freemarker.core.BuiltInsForOutputFormatRelated.escBI;
+import org.apache.freemarker.core.BuiltInsForOutputFormatRelated.no_escBI;
+import org.apache.freemarker.core.BuiltInsForSequences.chunkBI;
+import org.apache.freemarker.core.BuiltInsForSequences.firstBI;
+import org.apache.freemarker.core.BuiltInsForSequences.lastBI;
+import org.apache.freemarker.core.BuiltInsForSequences.reverseBI;
+import org.apache.freemarker.core.BuiltInsForSequences.seq_containsBI;
+import org.apache.freemarker.core.BuiltInsForSequences.seq_index_ofBI;
+import org.apache.freemarker.core.BuiltInsForSequences.sortBI;
+import org.apache.freemarker.core.BuiltInsForSequences.sort_byBI;
+import org.apache.freemarker.core.BuiltInsForStringsMisc.evalBI;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.util._DateUtil;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * AST expression node: {@code exp?name}
+ */
+abstract class ASTExpBuiltIn extends ASTExpression implements Cloneable {
+    
+    protected ASTExpression target;
+    protected String key;
+
+    static final Set<String> CAMEL_CASE_NAMES = new TreeSet<>();
+    static final Set<String> SNAKE_CASE_NAMES = new TreeSet<>();
+    static final int NUMBER_OF_BIS = 263;
+    static final HashMap<String, ASTExpBuiltIn> BUILT_INS_BY_NAME = new HashMap(NUMBER_OF_BIS * 3 / 2 + 1, 1f);
+
+    static {
+        // Note that you must update NUMBER_OF_BIS if you add new items here!
+        
+        putBI("abs", new absBI());
+        putBI("ancestors", new ancestorsBI());
+        putBI("api", new BuiltInsForMultipleTypes.apiBI());
+        putBI("boolean", new BuiltInsForStringsMisc.booleanBI());
+        putBI("byte", new byteBI());
+        putBI("c", new BuiltInsForMultipleTypes.cBI());
+        putBI("cap_first", "capFirst", new BuiltInsForStringsBasic.cap_firstBI());
+        putBI("capitalize", new BuiltInsForStringsBasic.capitalizeBI());
+        putBI("ceiling", new ceilingBI());
+        putBI("children", new childrenBI());
+        putBI("chop_linebreak", "chopLinebreak", new BuiltInsForStringsBasic.chop_linebreakBI());
+        putBI("contains", new BuiltInsForStringsBasic.containsBI());        
+        putBI("date", new BuiltInsForMultipleTypes.dateBI(TemplateDateModel.DATE));
+        putBI("date_if_unknown", "dateIfUnknown", new BuiltInsForDates.dateType_if_unknownBI(TemplateDateModel.DATE));
+        putBI("datetime", new BuiltInsForMultipleTypes.dateBI(TemplateDateModel.DATETIME));
+        putBI("datetime_if_unknown", "datetimeIfUnknown", new BuiltInsForDates.dateType_if_unknownBI(TemplateDateModel.DATETIME));
+        putBI("default", new BuiltInsForExistenceHandling.defaultBI());
+        putBI("double", new doubleBI());
+        putBI("ends_with", "endsWith", new BuiltInsForStringsBasic.ends_withBI());
+        putBI("ensure_ends_with", "ensureEndsWith", new BuiltInsForStringsBasic.ensure_ends_withBI());
+        putBI("ensure_starts_with", "ensureStartsWith", new BuiltInsForStringsBasic.ensure_starts_withBI());
+        putBI("esc", new escBI());
+        putBI("eval", new evalBI());
+        putBI("exists", new BuiltInsForExistenceHandling.existsBI());
+        putBI("first", new firstBI());
+        putBI("float", new floatBI());
+        putBI("floor", new floorBI());
+        putBI("chunk", new chunkBI());
+        putBI("counter", new BuiltInsForLoopVariables.counterBI());
+        putBI("item_cycle", "itemCycle", new BuiltInsForLoopVariables.item_cycleBI());
+        putBI("has_api", "hasApi", new BuiltInsForMultipleTypes.has_apiBI());
+        putBI("has_content", "hasContent", new BuiltInsForExistenceHandling.has_contentBI());
+        putBI("has_next", "hasNext", new BuiltInsForLoopVariables.has_nextBI());
+        putBI("html", new BuiltInsForStringsEncoding.htmlBI());
+        putBI("if_exists", "ifExists", new BuiltInsForExistenceHandling.if_existsBI());
+        putBI("index", new BuiltInsForLoopVariables.indexBI());
+        putBI("index_of", "indexOf", new BuiltInsForStringsBasic.index_ofBI(false));
+        putBI("int", new intBI());
+        putBI("interpret", new BuiltInsForStringsMisc.interpretBI());
+        putBI("is_boolean", "isBoolean", new BuiltInsForMultipleTypes.is_booleanBI());
+        putBI("is_collection", "isCollection", new BuiltInsForMultipleTypes.is_collectionBI());
+        putBI("is_collection_ex", "isCollectionEx", new BuiltInsForMultipleTypes.is_collection_exBI());
+        is_dateLikeBI bi = new BuiltInsForMultipleTypes.is_dateLikeBI();
+        putBI("is_date", "isDate", bi);  // misnomer
+        putBI("is_date_like", "isDateLike", bi);
+        putBI("is_date_only", "isDateOnly", new BuiltInsForMultipleTypes.is_dateOfTypeBI(TemplateDateModel.DATE));
+        putBI("is_even_item", "isEvenItem", new BuiltInsForLoopVariables.is_even_itemBI());
+        putBI("is_first", "isFirst", new BuiltInsForLoopVariables.is_firstBI());
+        putBI("is_last", "isLast", new BuiltInsForLoopVariables.is_lastBI());
+        putBI("is_unknown_date_like", "isUnknownDateLike", new BuiltInsForMultipleTypes.is_dateOfTypeBI(TemplateDateModel.UNKNOWN));
+        putBI("is_datetime", "isDatetime", new BuiltInsForMultipleTypes.is_dateOfTypeBI(TemplateDateModel.DATETIME));
+        putBI("is_directive", "isDirective", new BuiltInsForMultipleTypes.is_directiveBI());
+        putBI("is_enumerable", "isEnumerable", new BuiltInsForMultipleTypes.is_enumerableBI());
+        putBI("is_hash_ex", "isHashEx", new BuiltInsForMultipleTypes.is_hash_exBI());
+        putBI("is_hash", "isHash", new BuiltInsForMultipleTypes.is_hashBI());
+        putBI("is_infinite", "isInfinite", new is_infiniteBI());
+        putBI("is_indexable", "isIndexable", new BuiltInsForMultipleTypes.is_indexableBI());
+        putBI("is_macro", "isMacro", new BuiltInsForMultipleTypes.is_macroBI());
+        putBI("is_markup_output", "isMarkupOutput", new BuiltInsForMultipleTypes.is_markup_outputBI());
+        putBI("is_method", "isMethod", new BuiltInsForMultipleTypes.is_methodBI());
+        putBI("is_nan", "isNan", new is_nanBI());
+        putBI("is_node", "isNode", new BuiltInsForMultipleTypes.is_nodeBI());
+        putBI("is_number", "isNumber", new BuiltInsForMultipleTypes.is_numberBI());
+        putBI("is_odd_item", "isOddItem", new BuiltInsForLoopVariables.is_odd_itemBI());
+        putBI("is_sequence", "isSequence", new BuiltInsForMultipleTypes.is_sequenceBI());
+        putBI("is_string", "isString", new BuiltInsForMultipleTypes.is_stringBI());
+        putBI("is_time", "isTime", new BuiltInsForMultipleTypes.is_dateOfTypeBI(TemplateDateModel.TIME));
+        putBI("is_transform", "isTransform", new BuiltInsForMultipleTypes.is_transformBI());
+        
+        putBI("iso_utc", "isoUtc", new iso_utc_or_local_BI(
+                /* showOffset = */ null, _DateUtil.ACCURACY_SECONDS, /* useUTC = */ true));
+        putBI("iso_utc_fz", "isoUtcFZ", new iso_utc_or_local_BI(
+                /* showOffset = */ Boolean.TRUE, _DateUtil.ACCURACY_SECONDS, /* useUTC = */ true));
+        putBI("iso_utc_nz", "isoUtcNZ", new iso_utc_or_local_BI(
+                /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_SECONDS, /* useUTC = */ true));
+        
+        putBI("iso_utc_ms", "isoUtcMs", new iso_utc_or_local_BI(
+                /* showOffset = */ null, _DateUtil.ACCURACY_MILLISECONDS, /* useUTC = */ true));
+        putBI("iso_utc_ms_nz", "isoUtcMsNZ", new iso_utc_or_local_BI(
+                /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_MILLISECONDS, /* useUTC = */ true));
+        
+        putBI("iso_utc_m", "isoUtcM", new iso_utc_or_local_BI(
+                /* showOffset = */ null, _DateUtil.ACCURACY_MINUTES, /* useUTC = */ true));
+        putBI("iso_utc_m_nz", "isoUtcMNZ", new iso_utc_or_local_BI(
+                /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_MINUTES, /* useUTC = */ true));
+        
+        putBI("iso_utc_h", "isoUtcH", new iso_utc_or_local_BI(
+                /* showOffset = */ null, _DateUtil.ACCURACY_HOURS, /* useUTC = */ true));
+        putBI("iso_utc_h_nz", "isoUtcHNZ", new iso_utc_or_local_BI(
+                /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_HOURS, /* useUTC = */ true));
+        
+        putBI("iso_local", "isoLocal", new iso_utc_or_local_BI(
+                /* showOffset = */ null, _DateUtil.ACCURACY_SECONDS, /* useUTC = */ false));
+        putBI("iso_local_nz", "isoLocalNZ", new iso_utc_or_local_BI(
+                /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_SECONDS, /* useUTC = */ false));
+        
+        putBI("iso_local_ms", "isoLocalMs", new iso_utc_or_local_BI(
+                /* showOffset = */ null, _DateUtil.ACCURACY_MILLISECONDS, /* useUTC = */ false));
+        putBI("iso_local_ms_nz", "isoLocalMsNZ", new iso_utc_or_local_BI(
+                /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_MILLISECONDS, /* useUTC = */ false));
+        
+        putBI("iso_local_m", "isoLocalM", new iso_utc_or_local_BI(
+                /* showOffset = */ null, _DateUtil.ACCURACY_MINUTES, /* useUTC = */ false));
+        putBI("iso_local_m_nz", "isoLocalMNZ", new iso_utc_or_local_BI(
+                /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_MINUTES, /* useUTC = */ false));
+        
+        putBI("iso_local_h", "isoLocalH", new iso_utc_or_local_BI(
+                /* showOffset = */ null, _DateUtil.ACCURACY_HOURS, /* useUTC = */ false));
+        putBI("iso_local_h_nz", "isoLocalHNZ", new iso_utc_or_local_BI(
+                /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_HOURS, /* useUTC = */ false));
+        
+        putBI("iso", new iso_BI(
+                /* showOffset = */ null, _DateUtil.ACCURACY_SECONDS));
+        putBI("iso_nz", "isoNZ", new iso_BI(
+                /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_SECONDS));
+        
+        putBI("iso_ms", "isoMs", new iso_BI(
+                /* showOffset = */ null, _DateUtil.ACCURACY_MILLISECONDS));
+        putBI("iso_ms_nz", "isoMsNZ", new iso_BI(
+                /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_MILLISECONDS));
+        
+        putBI("iso_m", "isoM", new iso_BI(
+                /* showOffset = */ null, _DateUtil.ACCURACY_MINUTES));
+        putBI("iso_m_nz", "isoMNZ", new iso_BI(
+                /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_MINUTES));
+        
+        putBI("iso_h", "isoH", new iso_BI(
+                /* showOffset = */ null, _DateUtil.ACCURACY_HOURS));
+        putBI("iso_h_nz", "isoHNZ", new iso_BI(
+                /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_HOURS));
+        
+        putBI("j_string", "jString", new BuiltInsForStringsEncoding.j_stringBI());
+        putBI("join", new BuiltInsForSequences.joinBI());
+        putBI("js_string", "jsString", new BuiltInsForStringsEncoding.js_stringBI());
+        putBI("json_string", "jsonString", new BuiltInsForStringsEncoding.json_stringBI());
+        putBI("keep_after", "keepAfter", new BuiltInsForStringsBasic.keep_afterBI());
+        putBI("keep_before", "keepBefore", new BuiltInsForStringsBasic.keep_beforeBI());
+        putBI("keep_after_last", "keepAfterLast", new BuiltInsForStringsBasic.keep_after_lastBI());
+        putBI("keep_before_last", "keepBeforeLast", new BuiltInsForStringsBasic.keep_before_lastBI());
+        putBI("keys", new BuiltInsForHashes.keysBI());
+        putBI("last_index_of", "lastIndexOf", new BuiltInsForStringsBasic.index_ofBI(true));
+        putBI("last", new lastBI());
+        putBI("left_pad", "leftPad", new BuiltInsForStringsBasic.padBI(true));
+        putBI("length", new BuiltInsForStringsBasic.lengthBI());
+        putBI("long", new longBI());
+        putBI("lower_abc", "lowerAbc", new BuiltInsForNumbers.lower_abcBI());
+        putBI("lower_case", "lowerCase", new BuiltInsForStringsBasic.lower_caseBI());
+        putBI("namespace", new BuiltInsForMultipleTypes.namespaceBI());
+        putBI("new", new BuiltInsForStringsMisc.newBI());
+        putBI("markup_string", "markupString", new markup_stringBI());
+        putBI("node_name", "nodeName", new node_nameBI());
+        putBI("node_namespace", "nodeNamespace", new node_namespaceBI());
+        putBI("node_type", "nodeType", new node_typeBI());
+        putBI("no_esc", "noEsc", new no_escBI());
+        putBI("number", new BuiltInsForStringsMisc.numberBI());
+        putBI("number_to_date", "numberToDate", new number_to_dateBI(TemplateDateModel.DATE));
+        putBI("number_to_time", "numberToTime", new number_to_dateBI(TemplateDateModel.TIME));
+        putBI("number_to_datetime", "numberToDatetime", new number_to_dateBI(TemplateDateModel.DATETIME));
+        putBI("parent", new parentBI());
+        putBI("previous_sibling", "previousSibling", new previousSiblingBI());
+        putBI("next_sibling", "nextSibling", new nextSiblingBI());
+        putBI("item_parity", "itemParity", new BuiltInsForLoopVariables.item_parityBI());
+        putBI("item_parity_cap", "itemParityCap", new BuiltInsForLoopVariables.item_parity_capBI());
+        putBI("reverse", new reverseBI());
+        putBI("right_pad", "rightPad", new BuiltInsForStringsBasic.padBI(false));
+        putBI("root", new rootBI());
+        putBI("round", new roundBI());
+        putBI("remove_ending", "removeEnding", new BuiltInsForStringsBasic.remove_endingBI());
+        putBI("remove_beginning", "removeBeginning", new BuiltInsForStringsBasic.remove_beginningBI());
+        putBI("rtf", new BuiltInsForStringsEncoding.rtfBI());
+        putBI("seq_contains", "seqContains", new seq_containsBI());
+        putBI("seq_index_of", "seqIndexOf", new seq_index_ofBI(1));
+        putBI("seq_last_index_of", "seqLastIndexOf", new seq_index_ofBI(-1));
+        putBI("short", new shortBI());
+        putBI("size", new BuiltInsForMultipleTypes.sizeBI());
+        putBI("sort_by", "sortBy", new sort_byBI());
+        putBI("sort", new sortBI());
+        putBI("split", new BuiltInsForStringsBasic.split_BI());
+        putBI("switch", new BuiltInsWithParseTimeParameters.switch_BI());
+        putBI("starts_with", "startsWith", new BuiltInsForStringsBasic.starts_withBI());
+        putBI("string", new BuiltInsForMultipleTypes.stringBI());
+        putBI("substring", new BuiltInsForStringsBasic.substringBI());
+        putBI("then", new BuiltInsWithParseTimeParameters.then_BI());
+        putBI("time", new BuiltInsForMultipleTypes.dateBI(TemplateDateModel.TIME));
+        putBI("time_if_unknown", "timeIfUnknown", new BuiltInsForDates.dateType_if_unknownBI(TemplateDateModel.TIME));
+        putBI("trim", new BuiltInsForStringsBasic.trimBI());
+        putBI("uncap_first", "uncapFirst", new BuiltInsForStringsBasic.uncap_firstBI());
+        putBI("upper_abc", "upperAbc", new BuiltInsForNumbers.upper_abcBI());
+        putBI("upper_case", "upperCase", new BuiltInsForStringsBasic.upper_caseBI());
+        putBI("url", new BuiltInsForStringsEncoding.urlBI());
+        putBI("url_path", "urlPath", new BuiltInsForStringsEncoding.urlPathBI());
+        putBI("values", new BuiltInsForHashes.valuesBI());
+        putBI("web_safe", "webSafe", BUILT_INS_BY_NAME.get("html"));  // deprecated; use ?html instead
+        putBI("word_list", "wordList", new BuiltInsForStringsBasic.word_listBI());
+        putBI("xhtml", new BuiltInsForStringsEncoding.xhtmlBI());
+        putBI("xml", new BuiltInsForStringsEncoding.xmlBI());
+        putBI("matches", new BuiltInsForStringsRegexp.matchesBI());
+        putBI("groups", new BuiltInsForStringsRegexp.groupsBI());
+        putBI("replace", new BuiltInsForStringsRegexp.replace_reBI());
+
+        
+        if (NUMBER_OF_BIS < BUILT_INS_BY_NAME.size()) {
+            throw new AssertionError("Update NUMBER_OF_BIS! Should be: " + BUILT_INS_BY_NAME.size());
+        }
+    }
+    
+    private static void putBI(String name, ASTExpBuiltIn bi) {
+        BUILT_INS_BY_NAME.put(name, bi);
+        SNAKE_CASE_NAMES.add(name);
+        CAMEL_CASE_NAMES.add(name);
+    }
+
+    private static void putBI(String nameSnakeCase, String nameCamelCase, ASTExpBuiltIn bi) {
+        BUILT_INS_BY_NAME.put(nameSnakeCase, bi);
+        BUILT_INS_BY_NAME.put(nameCamelCase, bi);
+        SNAKE_CASE_NAMES.add(nameSnakeCase);
+        CAMEL_CASE_NAMES.add(nameCamelCase);
+    }
+    
+    /**
+     * @param target
+     *            Left-hand-operand expression
+     * @param keyTk
+     *            Built-in name token
+     */
+    static ASTExpBuiltIn newBuiltIn(int incompatibleImprovements, ASTExpression target, Token keyTk,
+            FMParserTokenManager tokenManager) throws ParseException {
+        String key = keyTk.image;
+        ASTExpBuiltIn bi = BUILT_INS_BY_NAME.get(key);
+        if (bi == null) {
+            StringBuilder buf = new StringBuilder("Unknown built-in: ").append(_StringUtil.jQuote(key)).append(". ");
+            
+            buf.append(
+                    "Help (latest version): http://freemarker.org/docs/ref_builtins.html; "
+                    + "you're using FreeMarker ").append(Configuration.getVersion()).append(".\n" 
+                    + "The alphabetical list of built-ins:");
+            List<String> names = new ArrayList<>(BUILT_INS_BY_NAME.keySet().size());
+            names.addAll(BUILT_INS_BY_NAME.keySet());
+            Collections.sort(names);
+            char lastLetter = 0;
+            
+            int shownNamingConvention;
+            {
+                int namingConvention = tokenManager.namingConvention;
+                shownNamingConvention = namingConvention != ParsingConfiguration.AUTO_DETECT_NAMING_CONVENTION
+                        ? namingConvention : ParsingConfiguration.LEGACY_NAMING_CONVENTION /* [2.4] CAMEL_CASE */;
+            }
+            
+            boolean first = true;
+            for (String correctName : names) {
+                int correctNameNamingConvetion = _StringUtil.getIdentifierNamingConvention(correctName);
+                if (shownNamingConvention == ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION
+                        ? correctNameNamingConvetion != ParsingConfiguration.LEGACY_NAMING_CONVENTION
+                        : correctNameNamingConvetion != ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION) {
+                    if (first) {
+                        first = false;
+                    } else {
+                        buf.append(", ");
+                    }
+
+                    char firstChar = correctName.charAt(0);
+                    if (firstChar != lastLetter) {
+                        lastLetter = firstChar;
+                        buf.append('\n');
+                    }
+                    buf.append(correctName);
+                }
+            }
+                
+            throw new ParseException(buf.toString(), null, keyTk);
+        }
+        
+        try {
+            bi = (ASTExpBuiltIn) bi.clone();
+        } catch (CloneNotSupportedException e) {
+            throw new InternalError();
+        }
+        bi.key = key;
+        bi.target = target;
+        return bi;
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return target.getCanonicalForm() + "?" + key;
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "?" + key;
+    }
+
+    @Override
+    boolean isLiteral() {
+        return false; // be on the safe side.
+    }
+    
+    protected final void checkMethodArgCount(List args, int expectedCnt) throws TemplateModelException {
+        checkMethodArgCount(args.size(), expectedCnt);
+    }
+    
+    protected final void checkMethodArgCount(int argCnt, int expectedCnt) throws TemplateModelException {
+        if (argCnt != expectedCnt) {
+            throw MessageUtil.newArgCntError("?" + key, argCnt, expectedCnt);
+        }
+    }
+
+    protected final void checkMethodArgCount(List args, int minCnt, int maxCnt) throws TemplateModelException {
+        checkMethodArgCount(args.size(), minCnt, maxCnt);
+    }
+    
+    protected final void checkMethodArgCount(int argCnt, int minCnt, int maxCnt) throws TemplateModelException {
+        if (argCnt < minCnt || argCnt > maxCnt) {
+            throw MessageUtil.newArgCntError("?" + key, argCnt, minCnt, maxCnt);
+        }
+    }
+
+    /**
+     * Same as {@link #getStringMethodArg}, but checks if {@code args} is big enough, and returns {@code null} if it
+     * isn't.
+     */
+    protected final String getOptStringMethodArg(List args, int argIdx)
+            throws TemplateModelException {
+        return args.size() > argIdx ? getStringMethodArg(args, argIdx) : null;
+    }
+    
+    /**
+     * Gets a method argument and checks if it's a string; it does NOT check if {@code args} is big enough.
+     */
+    protected final String getStringMethodArg(List args, int argIdx)
+            throws TemplateModelException {
+        TemplateModel arg = (TemplateModel) args.get(argIdx);
+        if (!(arg instanceof TemplateScalarModel)) {
+            throw MessageUtil.newMethodArgMustBeStringException("?" + key, argIdx, arg);
+        } else {
+            return _EvalUtil.modelToString((TemplateScalarModel) arg, null, null);
+        }
+    }
+
+    /**
+     * Gets a method argument and checks if it's a number; it does NOT check if {@code args} is big enough.
+     */
+    protected final Number getNumberMethodArg(List args, int argIdx)
+            throws TemplateModelException {
+        TemplateModel arg = (TemplateModel) args.get(argIdx);
+        if (!(arg instanceof TemplateNumberModel)) {
+            throw MessageUtil.newMethodArgMustBeNumberException("?" + key, argIdx, arg);
+        } else {
+            return _EvalUtil.modelToNumber((TemplateNumberModel) arg, null);
+        }
+    }
+    
+    protected final TemplateModelException newMethodArgInvalidValueException(int argIdx, Object[] details) {
+        return MessageUtil.newMethodArgInvalidValueException("?" + key, argIdx, details);
+    }
+
+    protected final TemplateModelException newMethodArgsInvalidValueException(Object[] details) {
+        return MessageUtil.newMethodArgsInvalidValueException("?" + key, details);
+    }
+    
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+    	try {
+	    	ASTExpBuiltIn clone = (ASTExpBuiltIn) clone();
+	    	clone.target = target.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState);
+	    	return clone;
+        } catch (CloneNotSupportedException e) {
+            throw new RuntimeException("Internal error: " + e);
+        }
+    }
+
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return target;
+        case 1: return key;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        switch (idx) {
+        case 0: return ParameterRole.LEFT_HAND_OPERAND;
+        case 1: return ParameterRole.RIGHT_HAND_OPERAND;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltInVariable.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltInVariable.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltInVariable.java
new file mode 100644
index 0000000..ece2099
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltInVariable.java
@@ -0,0 +1,298 @@
+/*
+ * 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.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.Date;
+
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.impl.SimpleDate;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * AST expression node: {@code .name}
+ */
+final class ASTExpBuiltInVariable extends ASTExpression {
+
+    static final String TEMPLATE_NAME_CC = "templateName";
+    static final String TEMPLATE_NAME = "template_name";
+    static final String MAIN_TEMPLATE_NAME_CC = "mainTemplateName";
+    static final String MAIN_TEMPLATE_NAME = "main_template_name";
+    static final String CURRENT_TEMPLATE_NAME_CC = "currentTemplateName";
+    static final String CURRENT_TEMPLATE_NAME = "current_template_name";
+    static final String NAMESPACE = "namespace";
+    static final String MAIN = "main";
+    static final String GLOBALS = "globals";
+    static final String LOCALS = "locals";
+    static final String DATA_MODEL_CC = "dataModel";
+    static final String DATA_MODEL = "data_model";
+    static final String LANG = "lang";
+    static final String LOCALE = "locale";
+    static final String LOCALE_OBJECT_CC = "localeObject";
+    static final String LOCALE_OBJECT = "locale_object";
+    static final String CURRENT_NODE_CC = "currentNode";
+    static final String CURRENT_NODE = "current_node";
+    static final String NODE = "node";
+    static final String PASS = "pass";
+    static final String VARS = "vars";
+    static final String VERSION = "version";
+    static final String INCOMPATIBLE_IMPROVEMENTS_CC = "incompatibleImprovements";
+    static final String INCOMPATIBLE_IMPROVEMENTS = "incompatible_improvements";
+    static final String ERROR = "error";
+    static final String OUTPUT_ENCODING_CC = "outputEncoding";
+    static final String OUTPUT_ENCODING = "output_encoding";
+    static final String OUTPUT_FORMAT_CC = "outputFormat";
+    static final String OUTPUT_FORMAT = "output_format";
+    static final String AUTO_ESC_CC = "autoEsc";
+    static final String AUTO_ESC = "auto_esc";
+    static final String URL_ESCAPING_CHARSET_CC = "urlEscapingCharset";
+    static final String URL_ESCAPING_CHARSET = "url_escaping_charset";
+    static final String NOW = "now";
+    
+    static final String[] SPEC_VAR_NAMES = new String[] {
+        AUTO_ESC_CC,
+        AUTO_ESC,
+        CURRENT_NODE_CC,
+        CURRENT_TEMPLATE_NAME_CC,
+        CURRENT_NODE,
+        CURRENT_TEMPLATE_NAME,
+        DATA_MODEL_CC,
+        DATA_MODEL,
+        ERROR,
+        GLOBALS,
+        INCOMPATIBLE_IMPROVEMENTS_CC,
+        INCOMPATIBLE_IMPROVEMENTS,
+        LANG,
+        LOCALE,
+        LOCALE_OBJECT_CC,
+        LOCALE_OBJECT,
+        LOCALS,
+        MAIN,
+        MAIN_TEMPLATE_NAME_CC,
+        MAIN_TEMPLATE_NAME,
+        NAMESPACE,
+        NODE,
+        NOW,
+        OUTPUT_ENCODING_CC,
+        OUTPUT_FORMAT_CC,
+        OUTPUT_ENCODING,
+        OUTPUT_FORMAT,
+        PASS,
+        TEMPLATE_NAME_CC,
+        TEMPLATE_NAME,
+        URL_ESCAPING_CHARSET_CC,
+        URL_ESCAPING_CHARSET,
+        VARS,
+        VERSION
+    };
+
+    private final String name;
+    private final TemplateModel parseTimeValue;
+
+    ASTExpBuiltInVariable(Token nameTk, FMParserTokenManager tokenManager, TemplateModel parseTimeValue)
+            throws ParseException {
+        String name = nameTk.image;
+        this.parseTimeValue = parseTimeValue;
+        if (Arrays.binarySearch(SPEC_VAR_NAMES, name) < 0) {
+            StringBuilder sb = new StringBuilder();
+            sb.append("Unknown special variable name: ");
+            sb.append(_StringUtil.jQuote(name)).append(".");
+            
+            int shownNamingConvention;
+            {
+                int namingConvention = tokenManager.namingConvention;
+                shownNamingConvention = namingConvention != ParsingConfiguration.AUTO_DETECT_NAMING_CONVENTION
+                        ? namingConvention : ParsingConfiguration.LEGACY_NAMING_CONVENTION /* [2.4] CAMEL_CASE */;
+            }
+            
+            {
+                String correctName;
+                if (name.equals("auto_escape") || name.equals("auto_escaping") || name.equals("autoesc")) {
+                    correctName = "auto_esc";
+                } else if (name.equals("autoEscape") || name.equals("autoEscaping")) {
+                    correctName = "autoEsc";
+                } else {
+                    correctName = null;
+                }
+                if (correctName != null) {
+                    sb.append(" You may meant: ");
+                    sb.append(_StringUtil.jQuote(correctName)).append(".");
+                }
+            }
+            
+            sb.append("\nThe allowed special variable names are: ");
+            boolean first = true;
+            for (final String correctName : SPEC_VAR_NAMES) {
+                int correctNameNamingConvention = _StringUtil.getIdentifierNamingConvention(correctName);
+                if (shownNamingConvention == ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION
+                        ? correctNameNamingConvention != ParsingConfiguration.LEGACY_NAMING_CONVENTION
+                        : correctNameNamingConvention != ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION) {
+                    if (first) {
+                        first = false;
+                    } else {
+                        sb.append(", ");
+                    }
+                    sb.append(correctName);
+                }
+            }
+            throw new ParseException(sb.toString(), null, nameTk);
+        }
+        
+        this.name = name.intern();
+    }
+
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        if (parseTimeValue != null) {
+            return parseTimeValue;
+        }
+        if (name == NAMESPACE) {
+            return env.getCurrentNamespace();
+        }
+        if (name == MAIN) {
+            return env.getMainNamespace();
+        }
+        if (name == GLOBALS) {
+            return env.getGlobalVariables();
+        }
+        if (name == LOCALS) {
+            ASTDirMacro.Context ctx = env.getCurrentMacroContext();
+            return ctx == null ? null : ctx.getLocals();
+        }
+        if (name == DATA_MODEL || name == DATA_MODEL_CC) {
+            return env.getDataModel();
+        }
+        if (name == VARS) {
+            return new VarsHash(env);
+        }
+        if (name == LOCALE) {
+            return new SimpleScalar(env.getLocale().toString());
+        }
+        if (name == LOCALE_OBJECT || name == LOCALE_OBJECT_CC) {
+            return env.getObjectWrapper().wrap(env.getLocale());
+        }
+        if (name == LANG) {
+            return new SimpleScalar(env.getLocale().getLanguage());
+        }
+        if (name == CURRENT_NODE || name == NODE || name == CURRENT_NODE_CC) {
+            return env.getCurrentVisitorNode();
+        }
+        if (name == MAIN_TEMPLATE_NAME || name == MAIN_TEMPLATE_NAME_CC) {
+            return SimpleScalar.newInstanceOrNull(env.getMainTemplate().getLookupName());
+        }
+        // [FM3] Some of these two should be removed.
+        if (name == CURRENT_TEMPLATE_NAME || name == CURRENT_TEMPLATE_NAME_CC
+                || name == TEMPLATE_NAME || name == TEMPLATE_NAME_CC) {
+            return SimpleScalar.newInstanceOrNull(env.getCurrentTemplate().getLookupName());
+        }
+        if (name == PASS) {
+            return ASTDirMacro.DO_NOTHING_MACRO;
+        }
+        if (name == OUTPUT_ENCODING || name == OUTPUT_ENCODING_CC) {
+            Charset encoding = env.getOutputEncoding();
+            return encoding != null ? new SimpleScalar(encoding.name()) : null;
+        }
+        if (name == URL_ESCAPING_CHARSET || name == URL_ESCAPING_CHARSET_CC) {
+            Charset charset = env.getURLEscapingCharset();
+            return charset != null ? new SimpleScalar(charset.name()) : null;
+        }
+        if (name == ERROR) {
+            return new SimpleScalar(env.getCurrentRecoveredErrorMessage());
+        }
+        if (name == NOW) {
+            return new SimpleDate(new Date(), TemplateDateModel.DATETIME);
+        }
+        if (name == VERSION) {
+            return new SimpleScalar(Configuration.getVersion().toString());
+        }
+        if (name == INCOMPATIBLE_IMPROVEMENTS || name == INCOMPATIBLE_IMPROVEMENTS_CC) {
+            return new SimpleScalar(env.getConfiguration().getIncompatibleImprovements().toString());
+        }
+        
+        throw new _MiscTemplateException(this,
+                "Invalid special variable: ", name);
+    }
+
+    @Override
+    public String toString() {
+        return "." + name;
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return "." + name;
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return getCanonicalForm();
+    }
+
+    @Override
+    boolean isLiteral() {
+        return false;
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+        return this;
+    }
+
+    static class VarsHash implements TemplateHashModel {
+        
+        Environment env;
+        
+        VarsHash(Environment env) {
+            this.env = env;
+        }
+        
+        @Override
+        public TemplateModel get(String key) throws TemplateModelException {
+            return env.getVariable(key);
+        }
+        
+        @Override
+        public boolean isEmpty() {
+            return false;
+        }
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 0;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpComparison.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpComparison.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpComparison.java
new file mode 100644
index 0000000..4e3559f
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpComparison.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.util.BugException;
+
+/**
+ * AST expression node: Comparison operators, like {@code ==}, {@code !=}, {@code <}, etc.
+ */
+final class ASTExpComparison extends ASTExpBoolean {
+
+    private final ASTExpression left;
+    private final ASTExpression right;
+    private final int operation;
+    private final String opString;
+
+    ASTExpComparison(ASTExpression left, ASTExpression right, String opString) {
+        this.left = left;
+        this.right = right;
+        opString = opString.intern();
+        this.opString = opString;
+        if (opString == "==" || opString == "=") {
+            operation = _EvalUtil.CMP_OP_EQUALS;
+        } else if (opString == "!=") {
+            operation = _EvalUtil.CMP_OP_NOT_EQUALS;
+        } else if (opString == "gt" || opString == "\\gt" || opString == ">" || opString == "&gt;") {
+            operation = _EvalUtil.CMP_OP_GREATER_THAN;
+        } else if (opString == "gte" || opString == "\\gte" || opString == ">=" || opString == "&gt;=") {
+            operation = _EvalUtil.CMP_OP_GREATER_THAN_EQUALS;
+        } else if (opString == "lt" || opString == "\\lt" || opString == "<" || opString == "&lt;") {
+            operation = _EvalUtil.CMP_OP_LESS_THAN;
+        } else if (opString == "lte" || opString == "\\lte" || opString == "<=" || opString == "&lt;=") {
+            operation = _EvalUtil.CMP_OP_LESS_THAN_EQUALS;
+        } else {
+            throw new BugException("Unknown comparison operator " + opString);
+        }
+    }
+
+    /*
+     * WARNING! This algorithm is duplicated in SequenceBuiltins.modelsEqual.
+     * Thus, if you update this method, then you have to update that too!
+     */
+    @Override
+    boolean evalToBoolean(Environment env) throws TemplateException {
+        return _EvalUtil.compare(left, operation, opString, right, this, env);
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return left.getCanonicalForm() + ' ' + opString + ' ' + right.getCanonicalForm();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return opString;
+    }
+
+    @Override
+    boolean isLiteral() {
+        return constantValue != null || (left.isLiteral() && right.isLiteral());
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+    	return new ASTExpComparison(
+    	        left.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
+    	        right.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
+    	        opString);
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        return idx == 0 ? left : right;
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        return ParameterRole.forBinaryOperatorOperand(idx);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDefault.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDefault.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDefault.java
new file mode 100644
index 0000000..b891374
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDefault.java
@@ -0,0 +1,142 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+
+import org.apache.freemarker.core.model.Constants;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+
+/** {@code exp!defExp}, {@code (exp)!defExp} and the same two with {@code (exp)!}. */
+class ASTExpDefault extends ASTExpression {
+	
+	static private class EmptyStringAndSequence
+	  implements TemplateScalarModel, TemplateSequenceModel, TemplateHashModelEx {
+		@Override
+        public String getAsString() {
+			return "";
+		}
+		@Override
+        public TemplateModel get(int i) {
+			return null;
+		}
+		@Override
+        public TemplateModel get(String s) {
+			return null;
+		}
+		@Override
+        public int size() {
+			return 0;
+		}
+		@Override
+        public boolean isEmpty() {
+			return true;
+		}
+		@Override
+        public TemplateCollectionModel keys() {
+			return Constants.EMPTY_COLLECTION;
+		}
+		@Override
+        public TemplateCollectionModel values() {
+			return Constants.EMPTY_COLLECTION;
+		}
+		
+	}
+	
+	static final TemplateModel EMPTY_STRING_AND_SEQUENCE = new EmptyStringAndSequence();
+	
+	private final ASTExpression lho, rho;
+	
+	ASTExpDefault(ASTExpression lho, ASTExpression rho) {
+		this.lho = lho;
+		this.rho = rho;
+	}
+
+	@Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+		TemplateModel left;
+		if (lho instanceof ASTExpParenthesis) {
+            boolean lastFIRE = env.setFastInvalidReferenceExceptions(true);
+	        try {
+                left = lho.eval(env);
+	        } catch (InvalidReferenceException ire) {
+	            left = null;
+            } finally {
+                env.setFastInvalidReferenceExceptions(lastFIRE);
+	        }
+		} else {
+            left = lho.eval(env);
+		}
+		
+		if (left != null) return left;
+		else if (rho == null) return EMPTY_STRING_AND_SEQUENCE;
+		else return rho.eval(env);
+	}
+
+	@Override
+    boolean isLiteral() {
+		return false;
+	}
+
+	@Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+        return new ASTExpDefault(
+                lho.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
+                rho != null
+                        ? rho.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState)
+                        : null);
+	}
+
+	@Override
+    public String getCanonicalForm() {
+		if (rho == null) {
+			return lho.getCanonicalForm() + '!';
+		}
+		return lho.getCanonicalForm() + '!' + rho.getCanonicalForm();
+	}
+	
+	@Override
+    String getNodeTypeSymbol() {
+        return "...!...";
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return lho;
+        case 1: return rho;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        return ParameterRole.forBinaryOperatorOperand(idx);
+    }
+        
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDot.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDot.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDot.java
new file mode 100644
index 0000000..1e6a742
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDot.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * AST expression node: {@code .} operator.
+ */
+final class ASTExpDot extends ASTExpression {
+    private final ASTExpression target;
+    private final String key;
+
+    ASTExpDot(ASTExpression target, String key) {
+        this.target = target;
+        this.key = key;
+    }
+
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        TemplateModel leftModel = target.eval(env);
+        if (leftModel instanceof TemplateHashModel) {
+            return ((TemplateHashModel) leftModel).get(key);
+        }
+        throw new NonHashException(target, leftModel, env);
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return target.getCanonicalForm() + getNodeTypeSymbol() + _StringUtil.toFTLIdentifierReferenceAfterDot(key);
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return ".";
+    }
+    
+    @Override
+    boolean isLiteral() {
+        return target.isLiteral();
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+    	return new ASTExpDot(
+    	        target.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
+    	        key);
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        return idx == 0 ? target : key;
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        return ParameterRole.forBinaryOperatorOperand(idx);
+    }
+    
+    String getRHO() {
+        return key;
+    }
+
+    boolean onlyHasIdentifiers() {
+        return (target instanceof ASTExpVariable) || ((target instanceof ASTExpDot) && ((ASTExpDot) target).onlyHasIdentifiers());
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDynamicKeyName.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDynamicKeyName.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDynamicKeyName.java
new file mode 100644
index 0000000..b904ce4
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDynamicKeyName.java
@@ -0,0 +1,284 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.Constants;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+
+/**
+ * AST expression node: {@code target[keyExpression]}, where, in FM 2.3, {@code keyExpression} can be string, a number
+ * or a range, and {@code target} can be a hash or a sequence.
+ */
+final class ASTExpDynamicKeyName extends ASTExpression {
+
+    private final ASTExpression keyExpression;
+    private final ASTExpression target;
+
+    ASTExpDynamicKeyName(ASTExpression target, ASTExpression keyExpression) {
+        this.target = target; 
+        this.keyExpression = keyExpression;
+    }
+
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        TemplateModel targetModel = target.eval(env);
+        target.assertNonNull(targetModel, env);
+        
+        TemplateModel keyModel = keyExpression.eval(env);
+        keyExpression.assertNonNull(keyModel, env);
+        if (keyModel instanceof TemplateNumberModel) {
+            int index = keyExpression.modelToNumber(keyModel, env).intValue();
+            return dealWithNumericalKey(targetModel, index, env);
+        }
+        if (keyModel instanceof TemplateScalarModel) {
+            String key = _EvalUtil.modelToString((TemplateScalarModel) keyModel, keyExpression, env);
+            return dealWithStringKey(targetModel, key, env);
+        }
+        if (keyModel instanceof RangeModel) {
+            return dealWithRangeKey(targetModel, (RangeModel) keyModel, env);
+        }
+        throw new UnexpectedTypeException(keyExpression, keyModel, "number, range, or string",
+                new Class[] { TemplateNumberModel.class, TemplateScalarModel.class, ASTExpRange.class }, env);
+    }
+
+    static private Class[] NUMERICAL_KEY_LHO_EXPECTED_TYPES;
+    static {
+        NUMERICAL_KEY_LHO_EXPECTED_TYPES = new Class[1 + NonStringException.STRING_COERCABLE_TYPES.length];
+        NUMERICAL_KEY_LHO_EXPECTED_TYPES[0] = TemplateSequenceModel.class;
+        for (int i = 0; i < NonStringException.STRING_COERCABLE_TYPES.length; i++) {
+            NUMERICAL_KEY_LHO_EXPECTED_TYPES[i + 1] = NonStringException.STRING_COERCABLE_TYPES[i];
+        }
+    }
+    
+    private TemplateModel dealWithNumericalKey(TemplateModel targetModel, 
+                                               int index, 
+                                               Environment env)
+        throws TemplateException {
+        if (targetModel instanceof TemplateSequenceModel) {
+            TemplateSequenceModel tsm = (TemplateSequenceModel) targetModel;
+            int size;
+            try {
+                size = tsm.size();
+            } catch (Exception e) {
+                size = Integer.MAX_VALUE;
+            }
+            return index < size ? tsm.get(index) : null;
+        } 
+        
+        try {
+            String s = target.evalAndCoerceToPlainText(env);
+            try {
+                return new SimpleScalar(s.substring(index, index + 1));
+            } catch (IndexOutOfBoundsException e) {
+                if (index < 0) {
+                    throw new _MiscTemplateException("Negative index not allowed: ", Integer.valueOf(index));
+                }
+                if (index >= s.length()) {
+                    throw new _MiscTemplateException(
+                            "String index out of range: The index was ", Integer.valueOf(index),
+                            " (0-based), but the length of the string is only ", Integer.valueOf(s.length()) , ".");
+                }
+                throw new RuntimeException("Can't explain exception", e);
+            }
+        } catch (NonStringException e) {
+            throw new UnexpectedTypeException(
+                    target, targetModel,
+                    "sequence or " + NonStringException.STRING_COERCABLE_TYPES_DESC,
+                    NUMERICAL_KEY_LHO_EXPECTED_TYPES,
+                    (targetModel instanceof TemplateHashModel
+                            ? "You had a numberical value inside the []. Currently that's only supported for "
+                                    + "sequences (lists) and strings. To get a Map item with a non-string key, "
+                                    + "use myMap?api.get(myKey)."
+                            : null),
+                    env);
+        }
+    }
+
+    private TemplateModel dealWithStringKey(TemplateModel targetModel, String key, Environment env)
+        throws TemplateException {
+        if (targetModel instanceof TemplateHashModel) {
+            return((TemplateHashModel) targetModel).get(key);
+        }
+        throw new NonHashException(target, targetModel, env);
+    }
+
+    private TemplateModel dealWithRangeKey(TemplateModel targetModel, RangeModel range, Environment env)
+    throws TemplateException {
+        final TemplateSequenceModel targetSeq;
+        final String targetStr;
+        if (targetModel instanceof TemplateSequenceModel) {
+            targetSeq = (TemplateSequenceModel) targetModel;
+            targetStr = null;
+        } else {
+            targetSeq = null;
+            try {
+                targetStr = target.evalAndCoerceToPlainText(env);
+            } catch (NonStringException e) {
+                throw new UnexpectedTypeException(
+                        target, target.eval(env),
+                        "sequence or " + NonStringException.STRING_COERCABLE_TYPES_DESC,
+                        NUMERICAL_KEY_LHO_EXPECTED_TYPES, env);
+            }
+        }
+        
+        final int size = range.size();
+        final boolean rightUnbounded = range.isRightUnbounded();
+        final boolean rightAdaptive = range.isRightAdaptive();
+        
+        // Right bounded empty ranges are accepted even if the begin index is out of bounds. That's because a such range
+        // produces an empty sequence, which thus doesn't contain any illegal indexes.
+        if (!rightUnbounded && size == 0) {
+            return emptyResult(targetSeq != null);
+        }
+
+        final int firstIdx = range.getBegining();
+        if (firstIdx < 0) {
+            throw new _MiscTemplateException(keyExpression,
+                    "Negative range start index (", Integer.valueOf(firstIdx),
+                    ") isn't allowed for a range used for slicing.");
+        }
+        
+        final int targetSize = targetStr != null ? targetStr.length() : targetSeq.size();
+        final int step = range.getStep();
+        
+        // Right-adaptive increasing ranges can start 1 after the last element of the target, because they are like
+        // ranges with exclusive end index of at most targetSize. Thence a such range is just an empty list of indexes,
+        // and thus it isn't out-of-bounds.
+        // Right-adaptive decreasing ranges has exclusive end -1, so it can't help on a  to high firstIndex. 
+        // Right-bounded ranges at this point aren't empty, so the right index surely can't reach targetSize. 
+        if (rightAdaptive && step == 1 ? firstIdx > targetSize : firstIdx >= targetSize) {
+            throw new _MiscTemplateException(keyExpression,
+                    "Range start index ", Integer.valueOf(firstIdx), " is out of bounds, because the sliced ",
+                    (targetStr != null ? "string" : "sequence"),
+                    " has only ", Integer.valueOf(targetSize), " ", (targetStr != null ? "character(s)" : "element(s)"),
+                    ". ", "(Note that indices are 0-based).");
+        }
+        
+        final int resultSize;
+        if (!rightUnbounded) {
+            final int lastIdx = firstIdx + (size - 1) * step;
+            if (lastIdx < 0) {
+                if (!rightAdaptive) {
+                    throw new _MiscTemplateException(keyExpression,
+                            "Negative range end index (", Integer.valueOf(lastIdx),
+                            ") isn't allowed for a range used for slicing.");
+                } else {
+                    resultSize = firstIdx + 1;
+                }
+            } else if (lastIdx >= targetSize) {
+                if (!rightAdaptive) {
+                    throw new _MiscTemplateException(keyExpression,
+                            "Range end index ", Integer.valueOf(lastIdx), " is out of bounds, because the sliced ",
+                            (targetStr != null ? "string" : "sequence"),
+                            " has only ", Integer.valueOf(targetSize), " ", (targetStr != null ? "character(s)" : "element(s)"),
+                            ". (Note that indices are 0-based).");
+                } else {
+                    resultSize = Math.abs(targetSize - firstIdx);
+                }
+            } else {
+                resultSize = size;
+            }
+        } else {
+            resultSize = targetSize - firstIdx;
+        }
+        
+        if (resultSize == 0) {
+            return emptyResult(targetSeq != null);
+        }
+        if (targetSeq != null) {
+            NativeSequence resultSeq = new NativeSequence(resultSize);
+            int srcIdx = firstIdx;
+            for (int i = 0; i < resultSize; i++) {
+                resultSeq.add(targetSeq.get(srcIdx));
+                srcIdx += step;
+            }
+            // List items are already wrapped, so the wrapper will be null:
+            return resultSeq;
+        } else {
+            final int exclEndIdx;
+            if (step < 0 && resultSize > 1) {
+                if (!(range.isAffactedByStringSlicingBug() && resultSize == 2)) {
+                    throw new _MiscTemplateException(keyExpression,
+                            "Decreasing ranges aren't allowed for slicing strings (as it would give reversed text). "
+                            + "The index range was: first = ", Integer.valueOf(firstIdx),
+                            ", last = ", Integer.valueOf(firstIdx + (resultSize - 1) * step));
+                } else {
+                    // Emulate the legacy bug, where "foo"[n .. n-1] gives "" instead of an error (if n >= 1).  
+                    // Fix this in FTL [2.4]
+                    exclEndIdx = firstIdx;
+                }
+            } else {
+                exclEndIdx = firstIdx + resultSize;
+            }
+            
+            return new SimpleScalar(targetStr.substring(firstIdx, exclEndIdx));
+        }
+    }
+
+    private TemplateModel emptyResult(boolean seq) {
+        return seq ? Constants.EMPTY_SEQUENCE : TemplateScalarModel.EMPTY_STRING;
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return target.getCanonicalForm() 
+               + "[" 
+               + keyExpression.getCanonicalForm() 
+               + "]";
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "...[...]";
+    }
+    
+    @Override
+    boolean isLiteral() {
+        return constantValue != null || (target.isLiteral() && keyExpression.isLiteral());
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        return idx == 0 ? target : keyExpression;
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        return idx == 0 ? ParameterRole.LEFT_HAND_OPERAND : ParameterRole.ENCLOSED_OPERAND;
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+    	return new ASTExpDynamicKeyName(
+    	        target.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
+    	        keyExpression.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState));
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpExists.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpExists.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpExists.java
new file mode 100644
index 0000000..72b8182
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpExists.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateModel;
+
+/**
+ * AST expression node: {@code ??} operator.
+ */
+class ASTExpExists extends ASTExpression {
+	
+	protected final ASTExpression exp;
+	
+	ASTExpExists(ASTExpression exp) {
+		this.exp = exp;
+	}
+
+	@Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        TemplateModel tm;
+	    if (exp instanceof ASTExpParenthesis) {
+            boolean lastFIRE = env.setFastInvalidReferenceExceptions(true);
+            try {
+                tm = exp.eval(env);
+            } catch (InvalidReferenceException ire) {
+                tm = null;
+            } finally {
+                env.setFastInvalidReferenceExceptions(lastFIRE);
+            }
+	    } else {
+            tm = exp.eval(env);
+	    }
+		return tm == null ? TemplateBooleanModel.FALSE : TemplateBooleanModel.TRUE;
+	}
+
+	@Override
+    boolean isLiteral() {
+		return false;
+	}
+
+	@Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+		return new ASTExpExists(
+		        exp.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState));
+	}
+
+	@Override
+    public String getCanonicalForm() {
+		return exp.getCanonicalForm() + getNodeTypeSymbol();
+	}
+	
+	@Override
+    String getNodeTypeSymbol() {
+        return "??";
+    }
+
+    @Override
+    int getParameterCount() {
+        return 1;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        return exp;
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        return ParameterRole.LEFT_HAND_OPERAND;
+    }
+	
+}



[29/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_ErrorDescriptionBuilder.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_ErrorDescriptionBuilder.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_ErrorDescriptionBuilder.java
new file mode 100644
index 0000000..2d09062
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_ErrorDescriptionBuilder.java
@@ -0,0 +1,356 @@
+/*
+ * 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.lang.reflect.Constructor;
+import java.lang.reflect.Member;
+import java.lang.reflect.Method;
+
+import org.apache.freemarker.core.model.impl._MethodUtil;
+import org.apache.freemarker.core.util._ClassUtil;
+import org.apache.freemarker.core.util._StringUtil;
+import org.slf4j.Logger;
+
+/**
+ * Used internally only, might changes without notice!
+ * Packs a structured from of the error description from which the error message can be rendered on-demand.
+ * Note that this class isn't serializable, thus the containing exception should render the message before it's
+ * serialized.
+ */
+public class _ErrorDescriptionBuilder {
+
+    private static final Logger LOG = _CoreLogs.RUNTIME;
+
+    private final String description;
+    private final Object[] descriptionParts;
+    private ASTExpression blamed;
+    private boolean showBlamer;
+    private Object/*String|Object[]*/ tip;
+    private Object[]/*String[]|Object[][]*/ tips;
+    private Template template;
+
+    public _ErrorDescriptionBuilder(String description) {
+        this.description = description;
+        descriptionParts = null;
+    }
+
+    /**
+     * @param descriptionParts These will be concatenated to a single {@link String} in {@link #toString()}.
+     *      {@link String} array items that look like FTL tag (must start with {@code "&lt;"} and end with {@code ">"})
+     *      will be converted to the actual template syntax if {@link #blamed} or {@link #template} was set.
+     */
+    public _ErrorDescriptionBuilder(Object... descriptionParts) {
+        this.descriptionParts = descriptionParts;
+        description = null;
+    }
+
+    @Override
+    public String toString() {
+        return toString(null, true);
+    }
+    
+    public String toString(ASTElement parentElement, boolean showTips) {
+        if (blamed == null && tips == null && tip == null && descriptionParts == null) return description;
+
+        StringBuilder sb = new StringBuilder(200);
+        
+        if (parentElement != null && blamed != null && showBlamer) {
+            try {
+                Blaming blaming = findBlaming(parentElement, blamed, 0);
+                if (blaming != null) {
+                    sb.append("For ");
+                    String nss = blaming.blamer.getNodeTypeSymbol();
+                    char q = nss.indexOf('"') == -1 ? '\"' : '`';
+                    sb.append(q).append(nss).append(q);
+                    sb.append(" ").append(blaming.roleOfblamed).append(": ");
+                }
+            } catch (Throwable e) {
+                // Should not happen. But we rather give a not-so-good error message than replace it with another...
+                // So we ignore this.
+                LOG.error("Error when searching blamer for better error message.", e);
+            }
+        }
+        
+        if (description != null) {
+            sb.append(description);
+        } else {
+            appendParts(sb, descriptionParts);
+        }
+
+        String extraTip = null;
+        if (blamed != null) {
+            // Right-trim:
+            for (int idx = sb.length() - 1; idx >= 0 && Character.isWhitespace(sb.charAt(idx)); idx --) {
+                sb.deleteCharAt(idx);
+            }
+            
+            char lastChar = sb.length() > 0 ? (sb.charAt(sb.length() - 1)) : 0;
+            if (lastChar != 0) {
+                sb.append('\n');
+            }
+            if (lastChar != ':') {
+                sb.append("The blamed expression:\n");
+            }
+            
+            String[] lines = splitToLines(blamed.toString());
+            for (int i = 0; i < lines.length; i++) {
+                sb.append(i == 0 ? "==> " : "\n    ");
+                sb.append(lines[i]);
+            }
+            
+            sb.append("  [");
+            sb.append(blamed.getStartLocation());
+            sb.append(']');
+            
+            
+            if (containsSingleInterpolatoinLiteral(blamed, 0)) {
+                extraTip = "It has been noticed that you are using ${...} as the sole content of a quoted string. That "
+                        + "does nothing but forcably converts the value inside ${...} to string (as it inserts it into "
+                        + "the enclosing string). "
+                        + "If that's not what you meant, just remove the quotation marks, ${ and }; you don't need "
+                        + "them. If you indeed wanted to convert to string, use myExpression?string instead.";
+            }
+        }
+        
+        if (showTips) {
+            int allTipsLen = (tips != null ? tips.length : 0) + (tip != null ? 1 : 0) + (extraTip != null ? 1 : 0);
+            Object[] allTips;
+            if (tips != null && allTipsLen == tips.length) {
+                allTips = tips;
+            } else {
+                allTips = new Object[allTipsLen];
+                int dst = 0;
+                if (tip != null) allTips[dst++] = tip; 
+                if (tips != null) {
+                    for (Object t : tips) {
+                        allTips[dst++] = t;
+                    }
+                }
+                if (extraTip != null) allTips[dst++] = extraTip; 
+            }
+            if (allTips != null && allTips.length > 0) {
+                sb.append("\n\n");
+                for (int i = 0; i < allTips.length; i++) {
+                    if (i != 0) sb.append('\n');
+                    sb.append(MessageUtil.ERROR_MESSAGE_HR).append('\n');
+                    sb.append("Tip: ");
+                    Object tip = allTips[i];
+                    if (!(tip instanceof Object[])) {
+                        sb.append(allTips[i]);
+                    } else {
+                        appendParts(sb, (Object[]) tip);
+                    }
+                }
+                sb.append('\n').append(MessageUtil.ERROR_MESSAGE_HR);
+            }
+        }
+        
+        return sb.toString();
+    }
+
+    private boolean containsSingleInterpolatoinLiteral(ASTExpression exp, int recursionDepth) {
+        if (exp == null) return false;
+        
+        // Just in case a loop ever gets into the AST somehow, try not fill the stack and such: 
+        if (recursionDepth > 20) return false;
+        
+        if (exp instanceof ASTExpStringLiteral && ((ASTExpStringLiteral) exp).isSingleInterpolationLiteral()) return true;
+        
+        int paramCnt = exp.getParameterCount();
+        for (int i = 0; i < paramCnt; i++) {
+            Object paramValue = exp.getParameterValue(i);
+            if (paramValue instanceof ASTExpression) {
+                boolean result = containsSingleInterpolatoinLiteral((ASTExpression) paramValue, recursionDepth + 1);
+                if (result) return true;
+            }
+        }
+        
+        return false;
+    }
+
+    private Blaming findBlaming(ASTNode parent, ASTExpression blamed, int recursionDepth) {
+        // Just in case a loop ever gets into the AST somehow, try not fill the stack and such: 
+        if (recursionDepth > 50) return null;
+        
+        int paramCnt = parent.getParameterCount();
+        for (int i = 0; i < paramCnt; i++) {
+            Object paramValue = parent.getParameterValue(i);
+            if (paramValue == blamed) {
+                Blaming blaming = new Blaming();
+                blaming.blamer = parent;
+                blaming.roleOfblamed = parent.getParameterRole(i);
+                return blaming;
+            } else if (paramValue instanceof ASTNode) {
+                Blaming blaming = findBlaming((ASTNode) paramValue, blamed, recursionDepth + 1);
+                if (blaming != null) return blaming;
+            }
+        }
+        return null;
+    }
+
+    private void appendParts(StringBuilder sb, Object[] parts) {
+        Template template = this.template != null ? this.template : (blamed != null ? blamed.getTemplate() : null);
+        for (Object partObj : parts) {
+            if (partObj instanceof Object[]) {
+                appendParts(sb, (Object[]) partObj);
+            } else {
+                String partStr;
+                partStr = tryToString(partObj);
+                if (partStr == null) {
+                    partStr = "null";
+                }
+
+                if (template != null) {
+                    if (partStr.length() > 4
+                            && partStr.charAt(0) == '<'
+                            && (
+                            (partStr.charAt(1) == '#' || partStr.charAt(1) == '@')
+                                    || (partStr.charAt(1) == '/') && (partStr.charAt(2) == '#' || partStr.charAt(2) == '@')
+                    )
+                            && partStr.charAt(partStr.length() - 1) == '>') {
+                        if (template.getActualTagSyntax() == ParsingConfiguration.SQUARE_BRACKET_TAG_SYNTAX) {
+                            sb.append('[');
+                            sb.append(partStr.substring(1, partStr.length() - 1));
+                            sb.append(']');
+                        } else {
+                            sb.append(partStr);
+                        }
+                    } else {
+                        sb.append(partStr);
+                    }
+                } else {
+                    sb.append(partStr);
+                }
+            }
+        }
+    }
+
+    /**
+     * A twist on Java's toString that generates more appropriate results for generating error messages.
+     */
+    public static String toString(Object partObj) {
+        return toString(partObj, false);
+    }
+
+    public static String tryToString(Object partObj) {
+        return toString(partObj, true);
+    }
+    
+    private static String toString(Object partObj, boolean suppressToStringException) {
+        final String partStr;
+        if (partObj == null) {
+            return null;
+        } else if (partObj instanceof Class) {
+            partStr = _ClassUtil.getShortClassName((Class) partObj);
+        } else if (partObj instanceof Method || partObj instanceof Constructor) {
+            partStr = _MethodUtil.toString((Member) partObj);
+        } else {
+            partStr = suppressToStringException ? _StringUtil.tryToString(partObj) : partObj.toString();
+        }
+        return partStr;
+    }
+
+    private String[] splitToLines(String s) {
+        s = _StringUtil.replace(s, "\r\n", "\n");
+        s = _StringUtil.replace(s, "\r", "\n");
+        return _StringUtil.split(s, '\n');
+    }
+    
+    /**
+     * Needed for description <em>parts</em> that look like an FTL tag to be converted, if there's no {@link #blamed}.
+     */
+    public _ErrorDescriptionBuilder template(Template template) {
+        this.template = template;
+        return this;
+    }
+
+    public _ErrorDescriptionBuilder blame(ASTExpression blamed) {
+        this.blamed = blamed;
+        return this;
+    }
+    
+    public _ErrorDescriptionBuilder showBlamer(boolean showBlamer) {
+        this.showBlamer = showBlamer;
+        return this;
+    }
+    
+    public _ErrorDescriptionBuilder tip(String tip) {
+        tip((Object) tip);
+        return this;
+    }
+    
+    public _ErrorDescriptionBuilder tip(Object... tip) {
+        tip((Object) tip);
+        return this;
+    }
+    
+    private _ErrorDescriptionBuilder tip(Object tip) {
+        if (tip == null) {
+            return this;
+        }
+        
+        if (this.tip == null) {
+            this.tip = tip;
+        } else {
+            if (tips == null) {
+                tips = new Object[] { tip };
+            } else {
+                final int origTipsLen = tips.length;
+                
+                Object[] newTips = new Object[origTipsLen + 1];
+                for (int i = 0; i < origTipsLen; i++) {
+                    newTips[i] = tips[i];
+                }
+                newTips[origTipsLen] = tip;
+                tips = newTips;
+            }
+        }
+        return this;
+    }
+    
+    public _ErrorDescriptionBuilder tips(Object... tips) {
+        if (tips == null || tips.length == 0) {
+            return this;
+        }
+        
+        if (this.tips == null) {
+            this.tips = tips;
+        } else {
+            final int origTipsLen = this.tips.length;
+            final int additionalTipsLen = tips.length;
+            
+            Object[] newTips = new Object[origTipsLen + additionalTipsLen];
+            for (int i = 0; i < origTipsLen; i++) {
+                newTips[i] = this.tips[i];
+            }
+            for (int i = 0; i < additionalTipsLen; i++) {
+                newTips[origTipsLen + i] = tips[i];
+            }
+            this.tips = newTips;
+        }
+        return this;
+    }
+    
+    private static class Blaming {
+        ASTNode blamer;
+        ParameterRole roleOfblamed;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_EvalUtil.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_EvalUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_EvalUtil.java
new file mode 100644
index 0000000..727085f
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_EvalUtil.java
@@ -0,0 +1,545 @@
+/*
+ * 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.util.Date;
+
+import org.apache.freemarker.core.arithmetic.ArithmeticEngine;
+import org.apache.freemarker.core.arithmetic.impl.BigDecimalArithmeticEngine;
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.outputformat.MarkupOutputFormat;
+import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.valueformat.TemplateDateFormat;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormat;
+import org.apache.freemarker.core.valueformat.TemplateValueFormat;
+import org.apache.freemarker.core.valueformat.TemplateValueFormatException;
+
+/**
+ * Internally used static utilities for evaluation expressions.
+ */
+public class _EvalUtil {
+    static final int CMP_OP_EQUALS = 1;
+    static final int CMP_OP_NOT_EQUALS = 2;
+    static final int CMP_OP_LESS_THAN = 3;
+    static final int CMP_OP_GREATER_THAN = 4;
+    static final int CMP_OP_LESS_THAN_EQUALS = 5;
+    static final int CMP_OP_GREATER_THAN_EQUALS = 6;
+    // If you add a new operator here, update the "compare" and "cmpOpToString" methods!
+    
+    // Prevents instantination.
+    private _EvalUtil() { }
+    
+    /**
+     * @param expr {@code null} is allowed, but may results in less helpful error messages
+     * @param env {@code null} is allowed
+     */
+    static String modelToString(TemplateScalarModel model, ASTExpression expr, Environment env)
+    throws TemplateModelException {
+        String value = model.getAsString();
+        if (value == null) {
+            throw newModelHasStoredNullException(String.class, model, expr);
+        }
+        return value;
+    }
+    
+    /**
+     * @param expr {@code null} is allowed, but may results in less helpful error messages
+     */
+    static Number modelToNumber(TemplateNumberModel model, ASTExpression expr)
+        throws TemplateModelException {
+        Number value = model.getAsNumber();
+        if (value == null) throw newModelHasStoredNullException(Number.class, model, expr);
+        return value;
+    }
+
+    /**
+     * @param expr {@code null} is allowed, but may results in less helpful error messages
+     */
+    static Date modelToDate(TemplateDateModel model, ASTExpression expr)
+        throws TemplateModelException {
+        Date value = model.getAsDate();
+        if (value == null) throw newModelHasStoredNullException(Date.class, model, expr);
+        return value;
+    }
+    
+    /** Signals the buggy case where we have a non-null model, but it wraps a null. */
+    public static TemplateModelException newModelHasStoredNullException(
+            Class expected, TemplateModel model, ASTExpression expr) {
+        return new _TemplateModelException(expr,
+                _TemplateModelException.modelHasStoredNullDescription(expected, model));
+    }
+
+    /**
+     * Compares two expressions according the rules of the FTL comparator operators.
+     * 
+     * @param leftExp not {@code null}
+     * @param operator one of the {@code COMP_OP_...} constants, like {@link #CMP_OP_EQUALS}.
+     * @param operatorString can be null {@code null}; the actual operator used, used for more accurate error message.
+     * @param rightExp not {@code null}
+     * @param env {@code null} is tolerated, but should be avoided
+     */
+    static boolean compare(
+            ASTExpression leftExp,
+            int operator, String  operatorString,
+            ASTExpression rightExp,
+            ASTExpression defaultBlamed,
+            Environment env) throws TemplateException {
+        TemplateModel ltm = leftExp.eval(env);
+        TemplateModel rtm = rightExp.eval(env);
+        return compare(
+                ltm, leftExp,
+                operator, operatorString,
+                rtm, rightExp,
+                defaultBlamed, false,
+                false, false, false,
+                env);
+    }
+    
+    /**
+     * Compares values according the rules of the FTL comparator operators; if the {@link ASTExpression}-s are
+     * accessible, use {@link #compare(ASTExpression, int, String, ASTExpression, ASTExpression, Environment)} instead,
+     * as that gives better error messages.
+     * 
+     * @param leftValue maybe {@code null}, which will usually cause the appropriate {@link TemplateException}. 
+     * @param operator one of the {@code COMP_OP_...} constants, like {@link #CMP_OP_EQUALS}.
+     * @param rightValue maybe {@code null}, which will usually cause the appropriate {@link TemplateException}.
+     * @param env {@code null} is tolerated, but should be avoided
+     */
+    static boolean compare(
+            TemplateModel leftValue, int operator, TemplateModel rightValue,
+            Environment env) throws TemplateException {
+        return compare(
+                leftValue, null,
+                operator, null,
+                rightValue, null,
+                null, false,
+                false, false, false,
+                env);
+    }
+
+    /**
+     * Same as {@link #compare(TemplateModel, int, TemplateModel, Environment)}, but if the two types are incompatible,
+     *     they are treated as non-equal instead of throwing an exception. Comparing dates of different types will
+     *     still throw an exception, however.
+     */
+    static boolean compareLenient(
+            TemplateModel leftValue, int operator, TemplateModel rightValue,
+            Environment env) throws TemplateException {
+        return compare(
+                leftValue, null,
+                operator, null,
+                rightValue, null,
+                null, false,
+                true, false, false,
+                env);
+    }
+    
+    private static final String VALUE_OF_THE_COMPARISON_IS_UNKNOWN_DATE_LIKE
+            = "value of the comparison is a date-like value where "
+              + "it's not known if it's a date (no time part), time, or date-time, "
+              + "and thus can't be used in a comparison.";
+    
+    /**
+     * @param leftExp {@code null} is allowed, but may results in less helpful error messages
+     * @param operator one of the {@code COMP_OP_...} constants, like {@link #CMP_OP_EQUALS}.
+     * @param operatorString can be null {@code null}; the actual operator used, used for more accurate error message.
+     * @param rightExp {@code null} is allowed, but may results in less helpful error messages
+     * @param defaultBlamed {@code null} allowed; the expression to which the error will point to if something goes
+     *        wrong that is not specific to the left or right side expression, or if that expression is {@code null}.
+     * @param typeMismatchMeansNotEqual If the two types are incompatible, they are treated as non-equal instead
+     *     of throwing an exception. Comparing dates of different types will still throw an exception, however. 
+     * @param leftNullReturnsFalse if {@code true}, a {@code null} left value will not cause exception, but make the
+     *     expression {@code false}.  
+     * @param rightNullReturnsFalse if {@code true}, a {@code null} right value will not cause exception, but make the
+     *     expression {@code false}.  
+     */
+    static boolean compare(
+            TemplateModel leftValue, ASTExpression leftExp,
+            int operator, String operatorString,
+            TemplateModel rightValue, ASTExpression rightExp,
+            ASTExpression defaultBlamed, boolean quoteOperandsInErrors,
+            boolean typeMismatchMeansNotEqual,
+            boolean leftNullReturnsFalse, boolean rightNullReturnsFalse,
+            Environment env) throws TemplateException {
+        if (leftValue == null) {
+            if (leftNullReturnsFalse) { 
+                return false;
+            } else {
+                if (leftExp != null) {
+                    throw InvalidReferenceException.getInstance(leftExp, env);
+                } else {
+                    throw new _MiscTemplateException(defaultBlamed, env, 
+                                "The left operand of the comparison was undefined or null.");
+                }
+            }
+        }
+
+        if (rightValue == null) {
+            if (rightNullReturnsFalse) { 
+                return false;
+            } else {
+                if (rightExp != null) {
+                    throw InvalidReferenceException.getInstance(rightExp, env);
+                } else {
+                    throw new _MiscTemplateException(defaultBlamed, env,
+                                "The right operand of the comparison was undefined or null.");
+                }
+            }
+        }
+
+        final int cmpResult;
+        if (leftValue instanceof TemplateNumberModel && rightValue instanceof TemplateNumberModel) {
+            Number leftNum = _EvalUtil.modelToNumber((TemplateNumberModel) leftValue, leftExp);
+            Number rightNum = _EvalUtil.modelToNumber((TemplateNumberModel) rightValue, rightExp);
+            ArithmeticEngine ae =
+                    env != null
+                        ? env.getArithmeticEngine()
+                        : (leftExp != null
+                            ? leftExp.getTemplate().getArithmeticEngine()
+                            : BigDecimalArithmeticEngine.INSTANCE);
+            try {
+                cmpResult = ae.compareNumbers(leftNum, rightNum);
+            } catch (RuntimeException e) {
+                throw new _MiscTemplateException(defaultBlamed, e, env,
+                        "Unexpected error while comparing two numbers: ", e);
+            }
+        } else if (leftValue instanceof TemplateDateModel && rightValue instanceof TemplateDateModel) {
+            TemplateDateModel leftDateModel = (TemplateDateModel) leftValue;
+            TemplateDateModel rightDateModel = (TemplateDateModel) rightValue;
+            
+            int leftDateType = leftDateModel.getDateType();
+            int rightDateType = rightDateModel.getDateType();
+            
+            if (leftDateType == TemplateDateModel.UNKNOWN || rightDateType == TemplateDateModel.UNKNOWN) {
+                String sideName;
+                ASTExpression sideExp;
+                if (leftDateType == TemplateDateModel.UNKNOWN) {
+                    sideName = "left";
+                    sideExp = leftExp;
+                } else {
+                    sideName = "right";
+                    sideExp = rightExp;
+                }
+                
+                throw new _MiscTemplateException(sideExp != null ? sideExp : defaultBlamed, env,
+                        "The ", sideName, " ", VALUE_OF_THE_COMPARISON_IS_UNKNOWN_DATE_LIKE);
+            }
+            
+            if (leftDateType != rightDateType) {
+                throw new _MiscTemplateException(defaultBlamed, env,
+                        "Can't compare dates of different types. Left date type is ",
+                        TemplateDateModel.TYPE_NAMES.get(leftDateType), ", right date type is ",
+                        TemplateDateModel.TYPE_NAMES.get(rightDateType), ".");
+            }
+
+            Date leftDate = _EvalUtil.modelToDate(leftDateModel, leftExp);
+            Date rightDate = _EvalUtil.modelToDate(rightDateModel, rightExp);
+            cmpResult = leftDate.compareTo(rightDate);
+        } else if (leftValue instanceof TemplateScalarModel && rightValue instanceof TemplateScalarModel) {
+            if (operator != CMP_OP_EQUALS && operator != CMP_OP_NOT_EQUALS) {
+                throw new _MiscTemplateException(defaultBlamed, env,
+                        "Can't use operator \"", cmpOpToString(operator, operatorString), "\" on string values.");
+            }
+            String leftString = _EvalUtil.modelToString((TemplateScalarModel) leftValue, leftExp, env);
+            String rightString = _EvalUtil.modelToString((TemplateScalarModel) rightValue, rightExp, env);
+            // FIXME NBC: Don't use the Collator here. That's locale-specific, but ==/!= should not be.
+            cmpResult = env.getCollator().compare(leftString, rightString);
+        } else if (leftValue instanceof TemplateBooleanModel && rightValue instanceof TemplateBooleanModel) {
+            if (operator != CMP_OP_EQUALS && operator != CMP_OP_NOT_EQUALS) {
+                throw new _MiscTemplateException(defaultBlamed, env,
+                        "Can't use operator \"", cmpOpToString(operator, operatorString), "\" on boolean values.");
+            }
+            boolean leftBool = ((TemplateBooleanModel) leftValue).getAsBoolean();
+            boolean rightBool = ((TemplateBooleanModel) rightValue).getAsBoolean();
+            cmpResult = (leftBool ? 1 : 0) - (rightBool ? 1 : 0);
+        } else {
+            if (typeMismatchMeansNotEqual) {
+                if (operator == CMP_OP_EQUALS) {
+                    return false;
+                } else if (operator == CMP_OP_NOT_EQUALS) {
+                    return true;
+                }
+                // Falls through
+            }
+            throw new _MiscTemplateException(defaultBlamed, env,
+                    "Can't compare values of these types. ",
+                    "Allowed comparisons are between two numbers, two strings, two dates, or two booleans.\n",
+                    "Left hand operand ",
+                    (quoteOperandsInErrors && leftExp != null
+                            ? new Object[] { "(", new _DelayedGetCanonicalForm(leftExp), ") value " }
+                            : ""),
+                    "is ", new _DelayedAOrAn(new _DelayedFTLTypeDescription(leftValue)), ".\n",
+                    "Right hand operand ",
+                    (quoteOperandsInErrors && rightExp != null
+                            ? new Object[] { "(", new _DelayedGetCanonicalForm(rightExp), ") value " }
+                            : ""),
+                    "is ", new _DelayedAOrAn(new _DelayedFTLTypeDescription(rightValue)),
+                    ".");
+        }
+
+        switch (operator) {
+            case CMP_OP_EQUALS: return cmpResult == 0;
+            case CMP_OP_NOT_EQUALS: return cmpResult != 0;
+            case CMP_OP_LESS_THAN: return cmpResult < 0;
+            case CMP_OP_GREATER_THAN: return cmpResult > 0;
+            case CMP_OP_LESS_THAN_EQUALS: return cmpResult <= 0;
+            case CMP_OP_GREATER_THAN_EQUALS: return cmpResult >= 0;
+            default: throw new BugException("Unsupported comparator operator code: " + operator);
+        }
+    }
+
+    private static String cmpOpToString(int operator, String operatorString) {
+        if (operatorString != null) {
+            return operatorString;
+        } else {
+            switch (operator) {
+                case CMP_OP_EQUALS: return "equals";
+                case CMP_OP_NOT_EQUALS: return "not-equals";
+                case CMP_OP_LESS_THAN: return "less-than";
+                case CMP_OP_GREATER_THAN: return "greater-than";
+                case CMP_OP_LESS_THAN_EQUALS: return "less-than-equals";
+                case CMP_OP_GREATER_THAN_EQUALS: return "greater-than-equals";
+                default: return "???";
+            }
+        }
+    }
+
+    /**
+     * Converts a value to plain text {@link String}, or a {@link TemplateMarkupOutputModel} if that's what the
+     * {@link TemplateValueFormat} involved produces.
+     * 
+     * @param seqTip
+     *            Tip to display if the value type is not coercable, but it's sequence or collection.
+     * 
+     * @return Never {@code null}
+     */
+    static Object coerceModelToStringOrMarkup(TemplateModel tm, ASTExpression exp, String seqTip, Environment env)
+            throws TemplateException {
+        return coerceModelToStringOrMarkup(tm, exp, false, seqTip, env);
+    }
+    
+    /**
+     * @return {@code null} if the {@code returnNullOnNonCoercableType} parameter is {@code true}, and the coercion is
+     *         not possible, because of the type is not right for it.
+     * 
+     * @see #coerceModelToStringOrMarkup(TemplateModel, ASTExpression, String, Environment)
+     */
+    static Object coerceModelToStringOrMarkup(
+            TemplateModel tm, ASTExpression exp, boolean returnNullOnNonCoercableType, String seqTip, Environment env)
+            throws TemplateException {
+        if (tm instanceof TemplateNumberModel) {
+            TemplateNumberModel tnm = (TemplateNumberModel) tm; 
+            TemplateNumberFormat format = env.getTemplateNumberFormat(exp, false);
+            try {
+                return assertFormatResultNotNull(format.format(tnm));
+            } catch (TemplateValueFormatException e) {
+                throw MessageUtil.newCantFormatNumberException(format, exp, e, false);
+            }
+        } else if (tm instanceof TemplateDateModel) {
+            TemplateDateModel tdm = (TemplateDateModel) tm;
+            TemplateDateFormat format = env.getTemplateDateFormat(tdm, exp, false);
+            try {
+                return assertFormatResultNotNull(format.format(tdm));
+            } catch (TemplateValueFormatException e) {
+                throw MessageUtil.newCantFormatDateException(format, exp, e, false);
+            }
+        } else if (tm instanceof TemplateMarkupOutputModel) {
+            return tm;
+        } else { 
+            return coerceModelToTextualCommon(tm, exp, seqTip, true, returnNullOnNonCoercableType, env);
+        }
+    }
+
+    /**
+     * Like {@link #coerceModelToStringOrMarkup(TemplateModel, ASTExpression, String, Environment)}, but gives error
+     * if the result is markup. This is what you normally use where markup results can't be used.
+     *
+     * @param seqTip
+     *            Tip to display if the value type is not coercable, but it's sequence or collection.
+     * 
+     * @return Never {@code null}
+     */
+    static String coerceModelToStringOrUnsupportedMarkup(
+            TemplateModel tm, ASTExpression exp, String seqTip, Environment env)
+            throws TemplateException {
+        if (tm instanceof TemplateNumberModel) {
+            TemplateNumberModel tnm = (TemplateNumberModel) tm; 
+            TemplateNumberFormat format = env.getTemplateNumberFormat(exp, false);
+            try {
+                return ensureFormatResultString(format.format(tnm), exp, env);
+            } catch (TemplateValueFormatException e) {
+                throw MessageUtil.newCantFormatNumberException(format, exp, e, false);
+            }
+        } else if (tm instanceof TemplateDateModel) {
+            TemplateDateModel tdm = (TemplateDateModel) tm;
+            TemplateDateFormat format = env.getTemplateDateFormat(tdm, exp, false);
+            try {
+                return ensureFormatResultString(format.format(tdm), exp, env);
+            } catch (TemplateValueFormatException e) {
+                throw MessageUtil.newCantFormatDateException(format, exp, e, false);
+            }
+        } else { 
+            return coerceModelToTextualCommon(tm, exp, seqTip, false, false, env);
+        }
+    }
+
+    /**
+     * Converts a value to plain text {@link String}, even if the {@link TemplateValueFormat} involved normally produces
+     * markup. This should be used rarely, where the user clearly intend to use the plain text variant of the format.
+     * 
+     * @param seqTip
+     *            Tip to display if the value type is not coercable, but it's sequence or collection.
+     * 
+     * @return Never {@code null}
+     */
+    static String coerceModelToPlainText(TemplateModel tm, ASTExpression exp, String seqTip,
+            Environment env) throws TemplateException {
+        if (tm instanceof TemplateNumberModel) {
+            return assertFormatResultNotNull(env.formatNumberToPlainText((TemplateNumberModel) tm, exp, false));
+        } else if (tm instanceof TemplateDateModel) {
+            return assertFormatResultNotNull(env.formatDateToPlainText((TemplateDateModel) tm, exp, false));
+        } else {
+            return coerceModelToTextualCommon(tm, exp, seqTip, false, false, env);
+        }
+    }
+
+    /**
+     * @param tm
+     *            If {@code null} that's an exception
+     * 
+     * @param supportsTOM
+     *            Whether the caller {@code coerceModelTo...} method could handle a {@link TemplateMarkupOutputModel}.
+     *            
+     * @return Never {@code null}
+     */
+    private static String coerceModelToTextualCommon(
+            TemplateModel tm, ASTExpression exp, String seqHint, boolean supportsTOM, boolean returnNullOnNonCoercableType,
+            Environment env)
+            throws TemplateException {
+        if (tm instanceof TemplateScalarModel) {
+            return modelToString((TemplateScalarModel) tm, exp, env);
+        } else if (tm == null) {
+            if (exp != null) {
+                throw InvalidReferenceException.getInstance(exp, env);
+            } else {
+                throw new InvalidReferenceException(
+                        "Null/missing value (no more informatoin avilable)",
+                        env);
+            }
+        } else if (tm instanceof TemplateBooleanModel) {
+            // [FM3] This should be before TemplateScalarModel, but automatic boolean-to-string is only non-error since
+            // 2.3.20, so to keep backward compatibility we couldn't insert this before TemplateScalarModel.
+            boolean booleanValue = ((TemplateBooleanModel) tm).getAsBoolean();
+            return env.formatBoolean(booleanValue, false);
+        } else {
+            if (returnNullOnNonCoercableType) {
+                return null;
+            }
+            if (seqHint != null && (tm instanceof TemplateSequenceModel || tm instanceof TemplateCollectionModel)) {
+                if (supportsTOM) {
+                    throw new NonStringOrTemplateOutputException(exp, tm, seqHint, env);
+                } else {
+                    throw new NonStringException(exp, tm, seqHint, env);
+                }
+            } else {
+                if (supportsTOM) {
+                    throw new NonStringOrTemplateOutputException(exp, tm, env);
+                } else {
+                    throw new NonStringException(exp, tm, env);
+                }
+            }
+        }
+    }
+
+    private static String ensureFormatResultString(Object formatResult, ASTExpression exp, Environment env)
+            throws NonStringException {
+        if (formatResult instanceof String) { 
+            return (String) formatResult;
+        }
+        
+        assertFormatResultNotNull(formatResult);
+        
+        TemplateMarkupOutputModel mo = (TemplateMarkupOutputModel) formatResult;
+        _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder(
+                "Value was formatted to convert it to string, but the result was markup of ouput format ",
+                new _DelayedJQuote(mo.getOutputFormat()), ".")
+                .tip("Use value?string to force formatting to plain text.")
+                .blame(exp);
+        throw new NonStringException(null, desc);
+    }
+
+    static String assertFormatResultNotNull(String r) {
+        if (r != null) {
+            return r;
+        }
+        throw new NullPointerException("TemplateValueFormatter result can't be null");
+    }
+
+    static Object assertFormatResultNotNull(Object r) {
+        if (r != null) {
+            return r;
+        }
+        throw new NullPointerException("TemplateValueFormatter result can't be null");
+    }
+
+    static TemplateMarkupOutputModel concatMarkupOutputs(ASTNode parent, TemplateMarkupOutputModel leftMO,
+            TemplateMarkupOutputModel rightMO) throws TemplateException {
+        MarkupOutputFormat leftOF = leftMO.getOutputFormat();
+        MarkupOutputFormat rightOF = rightMO.getOutputFormat();
+        if (rightOF != leftOF) {
+            String rightPT;
+            String leftPT;
+            if ((rightPT = rightOF.getSourcePlainText(rightMO)) != null) {
+                return leftOF.concat(leftMO, leftOF.fromPlainTextByEscaping(rightPT));
+            } else if ((leftPT = leftOF.getSourcePlainText(leftMO)) != null) {
+                return rightOF.concat(rightOF.fromPlainTextByEscaping(leftPT), rightMO);
+            } else {
+                Object[] message = { "Concatenation left hand operand is in ", new _DelayedToString(leftOF),
+                        " format, while the right hand operand is in ", new _DelayedToString(rightOF),
+                        ". Conversion to common format wasn't possible." };
+                if (parent instanceof ASTExpression) {
+                    throw new _MiscTemplateException((ASTExpression) parent, message);
+                } else {
+                    throw new _MiscTemplateException(message);
+                }
+            }
+        } else {
+            return leftOF.concat(leftMO, rightMO);
+        }
+    }
+
+    /**
+     * Returns an {@link ArithmeticEngine} even if {@code env} is {@code null}, because we are in parsing phase.
+     */
+    static ArithmeticEngine getArithmeticEngine(Environment env, ASTNode tObj) {
+        return env != null
+                ? env.getArithmeticEngine()
+                : tObj.getTemplate().getParsingConfiguration().getArithmeticEngine();
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_Java8.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_Java8.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_Java8.java
new file mode 100644
index 0000000..037ef9a
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_Java8.java
@@ -0,0 +1,34 @@
+/*
+ * 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.lang.reflect.Method;
+
+/**
+ * Used internally only, might changes without notice!
+ * Used for accessing functionality that's only present in Java 6 or later.
+ */
+public interface _Java8 {
+
+    /**
+     * Returns if it's a Java 8 "default method".
+     */
+    boolean isDefaultMethod(Method method);
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_Java8Impl.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_Java8Impl.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_Java8Impl.java
new file mode 100644
index 0000000..527a180
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_Java8Impl.java
@@ -0,0 +1,54 @@
+/*
+ * 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.lang.reflect.Method;
+
+/**
+ * Used internally only, might changes without notice!
+ * Used for accessing functionality that's only present in Java 8 or later.
+ */
+public final class _Java8Impl implements _Java8 {
+    
+    public static final _Java8 INSTANCE = new _Java8Impl();
+
+    private final Method isDefaultMethodMethod;
+
+    private _Java8Impl() {
+        // Not meant to be instantiated
+        try {
+            isDefaultMethodMethod = Method.class.getMethod("isDefault");
+        } catch (NoSuchMethodException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    @Override
+    public boolean isDefaultMethod(Method method) {
+        try {
+            // In FM2 this was compiled against Java 8 and this was a direct call. Doing that in a way that fits
+            // IDE-s would be an overkill (would need introducing two new modules), so we fell back to reflection.
+            return ((Boolean) isDefaultMethodMethod.invoke(method)).booleanValue();
+        } catch (Exception e) {
+            throw new IllegalStateException("Failed to call Method.isDefaultMethod()", e);
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_MiscTemplateException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_MiscTemplateException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_MiscTemplateException.java
new file mode 100644
index 0000000..1c8abfe
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_MiscTemplateException.java
@@ -0,0 +1,124 @@
+/*
+ * 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;
+
+/**
+ * For internal use only; don't depend on this, there's no backward compatibility guarantee at all!
+ * {@link TemplateException}-s that don't fit into any category that warrant its own class. In fact, this was added
+ * because the API of {@link TemplateException} is too simple for the purposes of the core, but it can't be
+ * extended without breaking backward compatibility and exposing internals.  
+ */
+public class _MiscTemplateException extends TemplateException {
+
+    // -----------------------------------------------------------------------------------------------------------------
+    // Permutation group:
+    
+    public _MiscTemplateException(String description) {
+        super(description, null);
+    }
+
+    public _MiscTemplateException(Environment env, String description) {
+        super(description, env);
+    }
+    
+    // -----------------------------------------------------------------------------------------------------------------
+    // Permutation group:
+
+    public _MiscTemplateException(Throwable cause, String description) {
+        this(cause, null, description);
+    }
+
+    public _MiscTemplateException(Throwable cause, Environment env) {
+        this(cause, env, (String) null);
+    }
+
+    public _MiscTemplateException(Throwable cause) {
+        this(cause, null, (String) null);
+    }
+    
+    public _MiscTemplateException(Throwable cause, Environment env, String description) {
+        super(description, cause, env);
+    }
+
+    // -----------------------------------------------------------------------------------------------------------------
+    // Permutation group:
+    
+    public _MiscTemplateException(_ErrorDescriptionBuilder description) {
+        this(null, description);
+    }
+
+    public _MiscTemplateException(Environment env, _ErrorDescriptionBuilder description) {
+        this(null, env, description);
+    }
+
+    public _MiscTemplateException(Throwable cause, Environment env, _ErrorDescriptionBuilder description) {
+        super(cause, env, null, description);
+    }
+
+    // -----------------------------------------------------------------------------------------------------------------
+    // Permutation group:
+    
+    public _MiscTemplateException(Object... descriptionParts) {
+        this((Environment) null, descriptionParts);
+    }
+
+    public _MiscTemplateException(Environment env, Object... descriptionParts) {
+        this((Throwable) null, env, descriptionParts);
+    }
+
+    public _MiscTemplateException(Throwable cause, Object... descriptionParts) {
+        this(cause, null, descriptionParts);
+    }
+
+    public _MiscTemplateException(Throwable cause, Environment env, Object... descriptionParts) {
+        super(cause, env, null, new _ErrorDescriptionBuilder(descriptionParts));
+    }
+
+    // -----------------------------------------------------------------------------------------------------------------
+    // Permutation group:
+    
+    public _MiscTemplateException(ASTExpression blamed, Object... descriptionParts) {
+        this(blamed, null, descriptionParts);
+    }
+
+    public _MiscTemplateException(ASTExpression blamed, Environment env, Object... descriptionParts) {
+        this(blamed, null, env, descriptionParts);
+    }
+
+    public _MiscTemplateException(ASTExpression blamed, Throwable cause, Environment env, Object... descriptionParts) {
+        super(cause, env, blamed, new _ErrorDescriptionBuilder(descriptionParts).blame(blamed));
+    }
+
+    // -----------------------------------------------------------------------------------------------------------------
+    // Permutation group:
+    
+    public _MiscTemplateException(ASTExpression blamed, String description) {
+        this(blamed, null, description);
+    }
+
+    public _MiscTemplateException(ASTExpression blamed, Environment env, String description) {
+        this(blamed, null, env, description);
+    }
+
+    public _MiscTemplateException(ASTExpression blamed, Throwable cause, Environment env, String description) {
+        super(cause, env, blamed, new _ErrorDescriptionBuilder(description).blame(blamed));
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_ObjectBuilderSettingEvaluationException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_ObjectBuilderSettingEvaluationException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_ObjectBuilderSettingEvaluationException.java
new file mode 100644
index 0000000..620399a
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_ObjectBuilderSettingEvaluationException.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * Don't use this; used internally by FreeMarker, might changes without notice.
+ * Thrown by {@link _ObjectBuilderSettingEvaluator}.
+ */
+public class _ObjectBuilderSettingEvaluationException extends Exception {
+    
+    public _ObjectBuilderSettingEvaluationException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public _ObjectBuilderSettingEvaluationException(String message) {
+        super(message);
+    }
+
+    public _ObjectBuilderSettingEvaluationException(String expected, String src, int location) {
+        super("Expression syntax error: Expected a(n) " + expected + ", but "
+                + (location < src.length()
+                        ? "found character " + _StringUtil.jQuote("" + src.charAt(location)) + " at position "
+                            + (location + 1) + "."
+                        : "the end of the parsed string was reached.") );
+    }
+    
+}


[07/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

Posted by dd...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/javacc/FTL.jj
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/javacc/FTL.jj b/freemarker-core/src/main/javacc/FTL.jj
new file mode 100644
index 0000000..dc6079f
--- /dev/null
+++ b/freemarker-core/src/main/javacc/FTL.jj
@@ -0,0 +1,4132 @@
+/*
+ * 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.
+ */
+
+options
+{
+    STATIC = false;
+    UNICODE_INPUT = true;
+    // DEBUG_TOKEN_MANAGER = true;
+    // DEBUG_PARSER = true;
+}
+
+PARSER_BEGIN(FMParser)
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.*;
+import org.apache.freemarker.core.outputformat.*;
+import org.apache.freemarker.core.outputformat.impl.*;
+import org.apache.freemarker.core.model.*;
+import org.apache.freemarker.core.model.impl.*;
+import org.apache.freemarker.core.util.*;
+import java.io.*;
+import java.util.*;
+import java.nio.charset.Charset;
+import java.nio.charset.UnsupportedCharsetException;
+
+/**
+ * This class is generated by JavaCC from a grammar file.
+ */
+public class FMParser {
+
+    private static final int ITERATOR_BLOCK_KIND_LIST = 0; 
+    private static final int ITERATOR_BLOCK_KIND_ITEMS = 1;
+    private static final int ITERATOR_BLOCK_KIND_USER_DIRECTIVE = 2;
+
+    private static class ParserIteratorBlockContext {
+        /**
+         * loopVarName in <#list ... as loopVarName> or <#items as loopVarName>; null after we left the nested
+         * block of #list or #items, respectively.
+         */
+        private String loopVarName;
+        
+        /**
+         * loopVar1Name in <#list ... as k, loopVar2Name> or <#items as k, loopVar2Name>; null after we left the nested
+         * block of #list or #items, respectively.
+         */
+        private String loopVar2Name;
+        
+        /**
+         * See the ITERATOR_BLOCK_KIND_... costants.
+         */
+        private int kind;
+        
+        /**
+         * Is this a key-value pair listing? When there's a nested #items, it's only set there. 
+         */
+        private boolean hashListing;
+    }
+
+    private Template template;
+
+    private boolean stripWhitespace, stripText;
+    private int incompatibleImprovements;
+    private OutputFormat outputFormat;
+    private int autoEscapingPolicy;
+    private boolean autoEscaping;
+    private ParsingConfiguration pCfg;
+    private InputStream streamToUnmarkWhenEncEstabd;
+
+    /** Keeps track of #list nesting. */
+    private List/*<ParserIteratorBlockContext>*/ iteratorBlockContexts;
+    
+    /**
+     * Keeps track of the nesting depth of directives that support #break.
+     */
+    private int breakableDirectiveNesting;
+
+    private boolean inMacro, inFunction;
+    private LinkedList escapes = new LinkedList();
+    private int mixedContentNesting; // for stripText
+
+    FMParser(Template template, Reader reader,
+            ParsingConfiguration pCfg, OutputFormat outputFormat, Integer autoEscapingPolicy,
+            InputStream streamToUnmarkWhenEncEstabd) {
+        this(template, true, readerToTokenManager(reader, pCfg),
+                pCfg, outputFormat, autoEscapingPolicy,
+                streamToUnmarkWhenEncEstabd);
+    }
+
+    private static FMParserTokenManager readerToTokenManager(Reader reader, ParsingConfiguration pCfg) {
+        SimpleCharStream simpleCharStream = new SimpleCharStream(reader, 1, 1);
+        simpleCharStream.setTabSize(pCfg.getTabSize());
+        return new FMParserTokenManager(simpleCharStream);
+    }
+
+    FMParser(Template template, boolean newTemplate, FMParserTokenManager tkMan,
+            ParsingConfiguration pCfg, OutputFormat contextOutputFormat, Integer contextAutoEscapingPolicy,
+    		InputStream streamToUnmarkWhenEncEstabd) {
+        this(tkMan);
+
+        _NullArgumentException.check(pCfg);
+        this.pCfg = pCfg;
+        
+        this.streamToUnmarkWhenEncEstabd = streamToUnmarkWhenEncEstabd;
+
+        _NullArgumentException.check(template);
+        this.template = template;
+
+        int incompatibleImprovements = pCfg.getIncompatibleImprovements().intValue();
+        token_source.incompatibleImprovements = incompatibleImprovements;
+        this.incompatibleImprovements = incompatibleImprovements;
+
+        {
+            OutputFormat outputFormatFromExt = pCfg.getRecognizeStandardFileExtensions() ? getFormatFromStdFileExt()
+                    : null;
+            outputFormat = contextOutputFormat != null ? contextOutputFormat
+                    : outputFormatFromExt != null ? outputFormatFromExt
+                    : pCfg.getOutputFormat();
+            autoEscapingPolicy = contextAutoEscapingPolicy != null ? contextAutoEscapingPolicy
+                    : outputFormatFromExt != null ? Configuration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY
+                    : pCfg.getAutoEscapingPolicy();
+        }
+        recalculateAutoEscapingField();
+
+        token_source.setParser(this);
+
+        int tagSyntax = pCfg.getTagSyntax();
+        switch (tagSyntax) {
+        case Configuration.AUTO_DETECT_TAG_SYNTAX:
+            token_source.autodetectTagSyntax = true;
+            break;
+        case Configuration.ANGLE_BRACKET_TAG_SYNTAX:
+            token_source.squBracTagSyntax = false;
+            break;
+        case Configuration.SQUARE_BRACKET_TAG_SYNTAX:
+            token_source.squBracTagSyntax = true;
+            break;
+        default:
+            throw new IllegalArgumentException("Illegal argument for tagSyntax: " + tagSyntax);
+        }
+
+        int namingConvention = pCfg.getNamingConvention();
+        switch (namingConvention) {
+        case Configuration.AUTO_DETECT_NAMING_CONVENTION:
+        case Configuration.CAMEL_CASE_NAMING_CONVENTION:
+        case Configuration.LEGACY_NAMING_CONVENTION:
+            token_source.initialNamingConvention = namingConvention;
+            token_source.namingConvention = namingConvention;
+            break;
+        default:
+            throw new IllegalArgumentException("Illegal argument for namingConvention: " + namingConvention);
+        }
+
+        this.stripWhitespace = pCfg.getWhitespaceStripping();
+
+        // If this is a Template under construction, we do the below.
+        // If this is just the enclosing Template for ?eval or such, we must not modify it.
+        if (newTemplate) {
+            template.setAutoEscapingPolicy(autoEscapingPolicy);
+            template.setOutputFormat(outputFormat);
+        }
+    }
+    
+    void setupStringLiteralMode(FMParserTokenManager parentTokenSource, OutputFormat outputFormat) {
+        token_source.initialNamingConvention = parentTokenSource.initialNamingConvention;
+        token_source.namingConvention = parentTokenSource.namingConvention;
+        token_source.namingConventionEstabilisher = parentTokenSource.namingConventionEstabilisher;
+        token_source.SwitchTo(NODIRECTIVE);
+        
+        this.outputFormat = outputFormat;
+        recalculateAutoEscapingField();                                
+    }
+
+    void tearDownStringLiteralMode(FMParserTokenManager parentTokenSource) {
+        parentTokenSource.namingConvention = token_source.namingConvention;
+        parentTokenSource.namingConventionEstabilisher = token_source.namingConventionEstabilisher;
+    }
+
+    private OutputFormat getFormatFromStdFileExt() {
+        String name = template.getSourceOrLookupName();
+        if (name == null) {
+            return null;
+        }
+
+        int ln = name.length();
+        if (ln < 5) return null;
+
+        char c = name.charAt(ln - 5);
+        if (c != '.') return null;
+
+        c = name.charAt(ln - 4);
+        if (c != 'f' && c != 'F') return null;
+
+        c = name.charAt(ln - 3);
+        if (c != 't' && c != 'T') return null;
+
+        c = name.charAt(ln - 2);
+        if (c != 'l' && c != 'L') return null;
+
+        c = name.charAt(ln - 1);
+        try {
+            // Note: We get the output formats by name, so that custom overrides take effect.
+            if (c == 'h' || c == 'H') {
+                return template.getConfiguration().getOutputFormat(HTMLOutputFormat.INSTANCE.getName());
+                }
+            if (c == 'x' || c == 'X') {
+                return template.getConfiguration().getOutputFormat(XMLOutputFormat.INSTANCE.getName());
+            }
+        } catch (UnregisteredOutputFormatException e) {
+            throw new BugException("Unregistered std format", e);
+        }
+        return null;
+    }
+    
+    /**
+     * Updates the {@link #autoEscaping} field based on the {@link #autoEscapingPolicy} and {@link #outputFormat} fields.
+     */
+    private void recalculateAutoEscapingField() {
+        if (outputFormat instanceof MarkupOutputFormat) {
+            if (autoEscapingPolicy == Configuration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY) {
+                autoEscaping = ((MarkupOutputFormat) outputFormat).isAutoEscapedByDefault();
+            } else if (autoEscapingPolicy == Configuration.ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY) {
+                autoEscaping = true;
+            } else if (autoEscapingPolicy == Configuration.DISABLE_AUTO_ESCAPING_POLICY) {
+                autoEscaping = false;
+            } else {
+                throw new IllegalStateException("Unhandled autoEscaping enum: " + autoEscapingPolicy);
+            }
+        } else {
+            autoEscaping = false;
+        }
+    }
+    
+    MarkupOutputFormat getMarkupOutputFormat() {
+        return outputFormat instanceof MarkupOutputFormat ? (MarkupOutputFormat) outputFormat : null;
+    }
+
+    /**
+     * Don't use it, unless you are developing FreeMarker itself.
+     */
+    public int _getLastTagSyntax() {
+        return token_source.squBracTagSyntax
+                ? Configuration.SQUARE_BRACKET_TAG_SYNTAX
+                : Configuration.ANGLE_BRACKET_TAG_SYNTAX;
+    }
+    
+    /**
+     * Don't use it, unless you are developing FreeMarker itself.
+     * The naming convention used by this template; if it couldn't be detected so far, it will be the most probable one.
+     * This could be used for formatting error messages, but not for anything serious.
+     */
+    public int _getLastNamingConvention() {
+        return token_source.namingConvention;
+    }
+
+    /**
+     * Throw an exception if the expression passed in is a String Literal
+     */
+    private void notStringLiteral(ASTExpression exp, String expected) throws ParseException {
+        if (exp instanceof ASTExpStringLiteral) {
+            throw new ParseException(
+                    "Found string literal: " + exp + ". Expecting: " + expected,
+                    exp);
+        }
+    }
+
+    /**
+     * Throw an exception if the expression passed in is a Number Literal
+     */
+    private void notNumberLiteral(ASTExpression exp, String expected) throws ParseException {
+        if (exp instanceof ASTExpNumberLiteral) {
+            throw new ParseException(
+                    "Found number literal: " + exp.getCanonicalForm() + ". Expecting " + expected,
+                    exp);
+        }
+    }
+
+    /**
+     * Throw an exception if the expression passed in is a boolean Literal
+     */
+    private void notBooleanLiteral(ASTExpression exp, String expected) throws ParseException {
+        if (exp instanceof ASTExpBooleanLiteral) {
+            throw new ParseException("Found: " + exp.getCanonicalForm() + ". Expecting " + expected, exp);
+        }
+    }
+
+    /**
+     * Throw an exception if the expression passed in is a Hash Literal
+     */
+    private void notHashLiteral(ASTExpression exp, String expected) throws ParseException {
+        if (exp instanceof ASTExpHashLiteral) {
+            throw new ParseException(
+                    "Found hash literal: " + exp.getCanonicalForm() + ". Expecting " + expected,
+                    exp);
+        }
+    }
+
+    /**
+     * Throw an exception if the expression passed in is a List Literal
+     */
+    private void notListLiteral(ASTExpression exp, String expected)
+            throws ParseException
+    {
+        if (exp instanceof ASTExpListLiteral) {
+            throw new ParseException(
+                    "Found list literal: " + exp.getCanonicalForm() + ". Expecting " + expected,
+                    exp);
+        }
+    }
+
+    /**
+     * Throw an exception if the expression passed in is a literal other than of the numerical type
+     */
+    private void numberLiteralOnly(ASTExpression exp) throws ParseException {
+        notStringLiteral(exp, "number");
+        notListLiteral(exp, "number");
+        notHashLiteral(exp, "number");
+        notBooleanLiteral(exp, "number");
+    }
+
+    /**
+     * Throw an exception if the expression passed in is not a string.
+     */
+    private void stringLiteralOnly(ASTExpression exp) throws ParseException {
+        notNumberLiteral(exp, "string");
+        notListLiteral(exp, "string");
+        notHashLiteral(exp, "string");
+        notBooleanLiteral(exp, "string");
+    }
+
+    /**
+     * Throw an exception if the expression passed in is a literal other than of the boolean type
+     */
+    private void booleanLiteralOnly(ASTExpression exp) throws ParseException {
+        notStringLiteral(exp, "boolean (true/false)");
+        notListLiteral(exp, "boolean (true/false)");
+        notHashLiteral(exp, "boolean (true/false)");
+        notNumberLiteral(exp, "boolean (true/false)");
+    }
+
+    private ASTExpression escapedExpression(ASTExpression exp) {
+        if (!escapes.isEmpty()) {
+            return ((ASTDirEscape) escapes.getFirst()).doEscape(exp);
+        } else {
+            return exp;
+        }
+    }
+
+    private boolean getBoolean(ASTExpression exp, boolean legacyCompat) throws ParseException {
+        TemplateModel tm = null;
+        try {
+            tm = exp.eval(null);
+        } catch (Exception e) {
+            throw new ParseException(e.getMessage()
+                    + "\nCould not evaluate expression: "
+                    + exp.getCanonicalForm(),
+                    exp,
+                    e);
+        }
+        if (tm instanceof TemplateBooleanModel) {
+            try {
+                return ((TemplateBooleanModel) tm).getAsBoolean();
+            } catch (TemplateModelException tme) {
+            }
+        }
+        if (legacyCompat && tm instanceof TemplateScalarModel) {
+            try {
+                return _StringUtil.getYesNo(((TemplateScalarModel) tm).getAsString());
+            } catch (Exception e) {
+                throw new ParseException(e.getMessage()
+                        + "\nExpecting boolean (true/false), found: " + exp.getCanonicalForm(),
+                        exp);
+            }
+        }
+        throw new ParseException("Expecting boolean (true/false) parameter", exp);
+    }
+    
+    void checkCurrentOutputFormatCanEscape(Token start) throws ParseException {
+        if (!(outputFormat instanceof MarkupOutputFormat)) {
+            throw new ParseException("The current output format can't do escaping: " + outputFormat,
+                    template, start);
+        }
+    }    
+    
+    private ParserIteratorBlockContext pushIteratorBlockContext() {
+        if (iteratorBlockContexts == null) {
+            iteratorBlockContexts = new ArrayList(4);
+        }
+        ParserIteratorBlockContext newCtx = new ParserIteratorBlockContext();
+        iteratorBlockContexts.add(newCtx);
+        return newCtx;
+    }
+    
+    private void popIteratorBlockContext() {
+        iteratorBlockContexts.remove(iteratorBlockContexts.size() - 1);
+    }
+    
+    private ParserIteratorBlockContext peekIteratorBlockContext() {
+        int size = iteratorBlockContexts != null ? iteratorBlockContexts.size() : 0;
+        return size != 0 ? (ParserIteratorBlockContext) iteratorBlockContexts.get(size - 1) : null; 
+    }
+    
+    private void checkLoopVariableBuiltInLHO(String loopVarName, ASTExpression lhoExp, Token biName)
+            throws ParseException {
+        int size = iteratorBlockContexts != null ? iteratorBlockContexts.size() : 0;
+        for (int i = size - 1; i >= 0; i--) {
+            ParserIteratorBlockContext ctx = (ParserIteratorBlockContext) iteratorBlockContexts.get(i);
+            if (loopVarName.equals(ctx.loopVarName) || loopVarName.equals(ctx.loopVar2Name)) {
+                if (ctx.kind == ITERATOR_BLOCK_KIND_USER_DIRECTIVE) {
+			        throw new ParseException(
+			                "The left hand operand of ?" + biName.image
+			                + " can't be the loop variable of an user defined directive: "
+			                +  loopVarName,
+			                lhoExp);
+                }
+                return;  // success
+            }
+        }
+        throw new ParseException(
+                "The left hand operand of ?" + biName.image + " must be a loop variable, "
+                + "but there's no loop variable in scope with this name: " + loopVarName,
+                lhoExp);
+    }
+
+}
+
+PARSER_END(FMParser)
+
+/**
+ * The lexer portion defines 5 lexical states:
+ * DEFAULT, FM_EXPRESSION, IN_PAREN, NO_PARSE, and EXPRESSION_COMMENT.
+ * The DEFAULT state is when you are parsing
+ * text but are not inside a FreeMarker expression.
+ * FM_EXPRESSION is the state you are in
+ * when the parser wants a FreeMarker expression.
+ * IN_PAREN is almost identical really. The difference
+ * is that you are in this state when you are within
+ * FreeMarker expression and also within (...).
+ * This is a necessary subtlety because the
+ * ">" and ">=" symbols can only be used
+ * within parentheses because otherwise, it would
+ * be ambiguous with the end of a directive.
+ * So, for example, you enter the FM_EXPRESSION state
+ * right after a ${ and leave it after the matching }.
+ * Or, you enter the FM_EXPRESSION state right after
+ * an "<if" and then, when you hit the matching ">"
+ * that ends the if directive,
+ * you go back to DEFAULT lexical state.
+ * If, within the FM_EXPRESSION state, you enter a
+ * parenthetical expression, you enter the IN_PAREN
+ * state.
+ * Note that whitespace is ignored in the
+ * FM_EXPRESSION and IN_PAREN states
+ * but is passed through to the parser as PCDATA in the DEFAULT state.
+ * NO_PARSE and EXPRESSION_COMMENT are extremely simple
+ * lexical states. NO_PARSE is when you are in a comment
+ * block and EXPRESSION_COMMENT is when you are in a comment
+ * that is within an FTL expression.
+ */
+TOKEN_MGR_DECLS:
+{
+
+    private static final String PLANNED_DIRECTIVE_HINT
+            = "(If you have seen this directive in use elsewhere, this was a planned directive, "
+                + "so maybe you need to upgrade FreeMarker.)";
+
+    /**
+     * The noparseTag is set when we enter a block of text that the parser more or less ignores. These are <noparse> and
+     * <#-- ... --->. This variable tells us what the closing tag should be, and when we hit that, we resume parsing.
+     * Note that with this scheme, <noparse> tags and comments cannot nest recursively.
+     */
+    String noparseTag;
+
+    /**
+     * Keeps track of how deeply nested we have the hash literals. This is necessary since we need to be able to
+     * distinguish the } used to close a hash literal and the one used to close a ${
+     */
+    private FMParser parser;
+    private int postInterpolationLexState = -1;
+    private int hashLiteralNesting;
+    private int parenthesisNesting;
+    private int bracketNesting;
+    private boolean inFTLHeader;
+    boolean squBracTagSyntax,
+            autodetectTagSyntax,
+            directiveSyntaxEstablished,
+            inInvocation;
+    int initialNamingConvention;
+    int namingConvention;
+    Token namingConventionEstabilisher;
+    int incompatibleImprovements;
+
+    void setParser(FMParser parser) {
+        this.parser = parser;
+    }
+
+    /**
+     * This method handles tag syntax ('<' VS '['), and also participates in naming convention detection.
+     * If you update this logic, take a look at the UNKNOWN_DIRECTIVE token too. 
+     */
+    private void handleTagSyntaxAndSwitch(Token tok, int tokenNamingConvention, int newLexState) {
+        final String image = tok.image;
+        
+        char firstChar = image.charAt(0);
+        if (autodetectTagSyntax && !directiveSyntaxEstablished) {
+            squBracTagSyntax = (firstChar == '[');
+        }
+        if ((firstChar == '[' && !squBracTagSyntax) || (firstChar == '<' && squBracTagSyntax)) {
+            tok.kind = STATIC_TEXT_NON_WS;
+            return;
+        }
+        
+        directiveSyntaxEstablished = true;
+        
+        checkNamingConvention(tok, tokenNamingConvention);
+        
+        SwitchTo(newLexState);
+    }
+
+    /**
+     * Used for tags whose name isn't affected by naming convention.
+     */
+    private void handleTagSyntaxAndSwitch(Token tok, int newLexState) {
+        handleTagSyntaxAndSwitch(tok, Configuration.AUTO_DETECT_NAMING_CONVENTION, newLexState);
+    }
+
+    void checkNamingConvention(Token tok) {
+        checkNamingConvention(tok, _StringUtil.getIdentifierNamingConvention(tok.image)); 
+    }
+    
+    void checkNamingConvention(Token tok, int tokenNamingConvention) {
+        if (tokenNamingConvention != Configuration.AUTO_DETECT_NAMING_CONVENTION) {
+	        if (namingConvention == Configuration.AUTO_DETECT_NAMING_CONVENTION) {
+	            namingConvention = tokenNamingConvention;
+	            namingConventionEstabilisher = tok;
+	        } else if (namingConvention != tokenNamingConvention) {
+                throw newNameConventionMismatchException(tok);
+	        }
+        }
+    }
+    
+    private TokenMgrError newNameConventionMismatchException(Token tok) {
+        return new TokenMgrError(
+                "Naming convention mismatch. "
+                + "Identifiers that are part of the template language (not the user specified ones) "
+                + (initialNamingConvention == Configuration.AUTO_DETECT_NAMING_CONVENTION
+                    ? "must consistently use the same naming convention within the same template. This template uses "
+                    : "must use the configured naming convention, which is the ")
+                + (namingConvention == Configuration.CAMEL_CASE_NAMING_CONVENTION
+                            ? "camel case naming convention (like: exampleName) "
+                            : (namingConvention == Configuration.LEGACY_NAMING_CONVENTION
+                                    ? "legacy naming convention (directive (tag) names are like examplename, " 
+                                      + "everything else is like example_name) "
+                                    : "??? (internal error)"
+                                    ))
+                + (namingConventionEstabilisher != null
+                        ? "estabilished by auto-detection at "
+                            + MessageUtil.formatPosition(
+                                    namingConventionEstabilisher.beginLine, namingConventionEstabilisher.beginColumn)
+                            + " by token " + _StringUtil.jQuote(namingConventionEstabilisher.image.trim())
+                        : "")
+                + ", but the problematic token, " + _StringUtil.jQuote(tok.image.trim())
+                + ", uses a different convention.",
+                TokenMgrError.LEXICAL_ERROR,
+                tok.beginLine, tok.beginColumn, tok.endLine, tok.endColumn);
+    }
+
+    /**
+     * Detects the naming convention used, both in start- and end-tag tokens.
+     *
+     * @param charIdxInName
+     *         The index of the deciding character relatively to the first letter of the name.
+     */
+    private static int getTagNamingConvention(Token tok, int charIdxInName) {
+        return _StringUtil.isUpperUSASCII(getTagNameCharAt(tok, charIdxInName))
+                ? Configuration.CAMEL_CASE_NAMING_CONVENTION : Configuration.LEGACY_NAMING_CONVENTION;
+    }
+
+    static char getTagNameCharAt(Token tok, int charIdxInName) {
+        final String image = tok.image;
+        
+        // Skip tag delimiter:
+        int idx = 0;
+        for (;;) {
+            final char c = image.charAt(idx);
+            if (c != '<' && c != '[' && c != '/' && c != '#') {
+                break;
+            }
+            idx++;
+        }
+
+        return image.charAt(idx + charIdxInName);
+    }
+
+    private void unifiedCall(Token tok) {
+        char firstChar = tok.image.charAt(0);
+        if (autodetectTagSyntax && !directiveSyntaxEstablished) {
+            squBracTagSyntax = (firstChar == '[');
+        }
+        if (squBracTagSyntax && firstChar == '<') {
+            tok.kind = STATIC_TEXT_NON_WS;
+            return;
+        }
+        if (!squBracTagSyntax && firstChar == '[') {
+            tok.kind = STATIC_TEXT_NON_WS;
+            return;
+        }
+        directiveSyntaxEstablished = true;
+        SwitchTo(NO_SPACE_EXPRESSION);
+    }
+
+    private void unifiedCallEnd(Token tok) {
+        char firstChar = tok.image.charAt(0);
+        if (squBracTagSyntax && firstChar == '<') {
+            tok.kind = STATIC_TEXT_NON_WS;
+            return;
+        }
+        if (!squBracTagSyntax && firstChar == '[') {
+            tok.kind = STATIC_TEXT_NON_WS;
+            return;
+        }
+    }
+
+    private void closeBracket(Token tok) {
+        if (bracketNesting > 0) {
+            --bracketNesting;
+        } else {
+            tok.kind = DIRECTIVE_END;
+            if (inFTLHeader) {
+                eatNewline();
+                inFTLHeader = false;
+            }
+            SwitchTo(DEFAULT);
+        }
+    }
+    
+    private void startInterpolation(Token tok) {
+        if (postInterpolationLexState != -1) {
+            char c = tok.image.charAt(0);
+            throw new TokenMgrError(
+                    "You can't start an interpolation (" + c + "{...}) here "
+                    + "as you are inside another interpolation.)",
+                    TokenMgrError.LEXICAL_ERROR,
+                    tok.beginLine, tok.beginColumn,
+                    tok.endLine, tok.endColumn);
+        }
+        postInterpolationLexState = curLexState;
+        SwitchTo(FM_EXPRESSION);
+    }
+
+    /**
+     * @param tok
+     *         Assumed to be an '}', or something that is the closing pair of another "mirror image" character.
+     */
+    private void endInterpolation(Token tok) {
+        if (postInterpolationLexState == -1) {
+            char c = tok.image.charAt(0);
+            throw new TokenMgrError(
+                    "You can't have an \"" + c + "\" here, as there's nothing open that it could close.",
+                    TokenMgrError.LEXICAL_ERROR,
+                    tok.beginLine, tok.beginColumn,
+                    tok.endLine, tok.endColumn);
+        }
+        SwitchTo(postInterpolationLexState);
+        postInterpolationLexState = -1;
+    }
+
+    private void eatNewline() {
+        int charsRead = 0;
+        try {
+            while (true) {
+                char c = input_stream.readChar();
+                ++charsRead;
+                if (!Character.isWhitespace(c)) {
+                    input_stream.backup(charsRead);
+                    return;
+                } else if (c == '\r') {
+                    char next = input_stream.readChar();
+                    ++charsRead;
+                    if (next != '\n') {
+                        input_stream.backup(1);
+                    }
+                    return;
+                } else if (c == '\n') {
+                    return;
+                }
+            }
+        } catch (IOException ioe) {
+            input_stream.backup(charsRead);
+        }
+    }
+
+    private void ftlHeader(Token matchedToken) {
+        if (!directiveSyntaxEstablished) {
+            squBracTagSyntax = matchedToken.image.charAt(0) == '[';
+            directiveSyntaxEstablished = true;
+            autodetectTagSyntax = false;
+        }
+        String img = matchedToken.image;
+        char firstChar = img.charAt(0);
+        char lastChar = img.charAt(img.length() - 1);
+        if ((firstChar == '[' && !squBracTagSyntax) || (firstChar == '<' && squBracTagSyntax)) {
+            matchedToken.kind = STATIC_TEXT_NON_WS;
+        }
+        if (matchedToken.kind != STATIC_TEXT_NON_WS) {
+            if (lastChar != '>' && lastChar != ']') {
+                SwitchTo(FM_EXPRESSION);
+                inFTLHeader = true;
+            } else {
+                eatNewline();
+            }
+        }
+    }
+}
+
+TOKEN:
+{
+    <#BLANK : " " | "\t" | "\n" | "\r">
+    |
+    <#START_TAG : "<#" | "[#">
+    |
+    <#END_TAG : "</#" | "[/#">
+    |
+    <#CLOSE_TAG1 : (<BLANK>)* (">" | "]")>
+    |
+    <#CLOSE_TAG2 : (<BLANK>)* ("/")? (">" | "]")>
+    |
+    /*
+     * ATTENTION: Update _CoreAPI.*_BUILT_IN_DIRECTIVE_NAMES if you add new directives!
+     */
+    <ATTEMPT : <START_TAG> "attempt" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <RECOVER : <START_TAG> "recover" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); } 
+    |
+    <IF : <START_TAG> "if" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <ELSE_IF : <START_TAG> "else" ("i" | "I") "f" <BLANK>> {
+        handleTagSyntaxAndSwitch(matchedToken, getTagNamingConvention(matchedToken, 4), FM_EXPRESSION);
+    }
+    |
+    <LIST : <START_TAG> "list" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <ITEMS : <START_TAG> "items" (<BLANK>)+ <AS> <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <SEP : <START_TAG> "sep" <CLOSE_TAG1>>
+    |
+    <SWITCH : <START_TAG> "switch" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <CASE : <START_TAG> "case" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <ASSIGN : <START_TAG> "assign" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <GLOBALASSIGN : <START_TAG> "global" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <LOCALASSIGN : <START_TAG> "local" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <_INCLUDE : <START_TAG> "include" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <IMPORT : <START_TAG> "import" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <FUNCTION : <START_TAG> "function" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <MACRO : <START_TAG> "macro" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <VISIT : <START_TAG> "visit" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <STOP : <START_TAG> "stop" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <RETURN : <START_TAG> "return" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <SETTING : <START_TAG> "setting" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <OUTPUTFORMAT : <START_TAG> "output" ("f"|"F") "ormat" <BLANK>> {
+        handleTagSyntaxAndSwitch(matchedToken, getTagNamingConvention(matchedToken, 6), FM_EXPRESSION);
+    }
+    |
+    <AUTOESC : <START_TAG> "auto" ("e"|"E") "sc" <CLOSE_TAG1>> {
+        handleTagSyntaxAndSwitch(matchedToken, getTagNamingConvention(matchedToken, 4), DEFAULT);
+    }
+    |
+    <NOAUTOESC : <START_TAG> "no" ("autoe"|"AutoE") "sc" <CLOSE_TAG1>> {
+        handleTagSyntaxAndSwitch(matchedToken, getTagNamingConvention(matchedToken, 2), DEFAULT);
+    }
+    |
+    <COMPRESS : <START_TAG> "compress" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <TERSE_COMMENT : ("<" | "[") "#--" > { noparseTag = "-->"; handleTagSyntaxAndSwitch(matchedToken, NO_PARSE); }
+    |
+    <NOPARSE: <START_TAG> "no" ("p" | "P") "arse" <CLOSE_TAG1>> {
+        int tagNamingConvention = getTagNamingConvention(matchedToken, 2);
+        handleTagSyntaxAndSwitch(matchedToken, tagNamingConvention, NO_PARSE);
+        noparseTag = tagNamingConvention == Configuration.CAMEL_CASE_NAMING_CONVENTION ? "noParse" : "noparse";
+    }
+    |
+    <END_IF : <END_TAG> "if" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <END_LIST : <END_TAG> "list" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <END_ITEMS : <END_TAG> "items" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <END_SEP : <END_TAG> "sep" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <END_RECOVER : <END_TAG> "recover" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <END_ATTEMPT : <END_TAG> "attempt" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <END_LOCAL : <END_TAG> "local" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <END_GLOBAL : <END_TAG> "global" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <END_ASSIGN : <END_TAG> "assign" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <END_FUNCTION : <END_TAG> "function" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <END_MACRO : <END_TAG> "macro" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <END_OUTPUTFORMAT : <END_TAG> "output" ("f" | "F") "ormat" <CLOSE_TAG1>> {
+        handleTagSyntaxAndSwitch(matchedToken, getTagNamingConvention(matchedToken, 6), DEFAULT);
+    }
+    |
+    <END_AUTOESC : <END_TAG> "auto" ("e" | "E") "sc" <CLOSE_TAG1>> {
+        handleTagSyntaxAndSwitch(matchedToken, getTagNamingConvention(matchedToken, 4), DEFAULT);
+    }
+    |
+    <END_NOAUTOESC : <END_TAG> "no" ("autoe"|"AutoE") "sc" <CLOSE_TAG1>> {
+        handleTagSyntaxAndSwitch(matchedToken, getTagNamingConvention(matchedToken, 2), DEFAULT);
+    }
+    |
+    <END_COMPRESS : <END_TAG> "compress" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <END_SWITCH : <END_TAG> "switch" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <ELSE : <START_TAG> "else" <CLOSE_TAG2>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <BREAK : <START_TAG> "break" <CLOSE_TAG2>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <SIMPLE_RETURN : <START_TAG> "return" <CLOSE_TAG2>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <HALT : <START_TAG> "stop" <CLOSE_TAG2>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <FLUSH : <START_TAG> "flush" <CLOSE_TAG2>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <TRIM : <START_TAG> "t" <CLOSE_TAG2>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <LTRIM : <START_TAG> "lt" <CLOSE_TAG2>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <RTRIM : <START_TAG> "rt" <CLOSE_TAG2>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <NOTRIM : <START_TAG> "nt" <CLOSE_TAG2>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <DEFAUL : <START_TAG> "default" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <SIMPLE_NESTED : <START_TAG> "nested" <CLOSE_TAG2>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <NESTED : <START_TAG> "nested" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <SIMPLE_RECURSE : <START_TAG> "recurse" <CLOSE_TAG2>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <RECURSE : <START_TAG> "recurse" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <FALLBACK : <START_TAG> "fallback" <CLOSE_TAG2>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <ESCAPE : <START_TAG> "escape" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <END_ESCAPE : <END_TAG> "escape" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <NOESCAPE : <START_TAG> "no" ("e" | "E") "scape" <CLOSE_TAG1>> {
+        handleTagSyntaxAndSwitch(matchedToken, getTagNamingConvention(matchedToken, 2), DEFAULT);
+    }
+    |
+    <END_NOESCAPE : <END_TAG> "no" ("e" | "E") "scape" <CLOSE_TAG1>> {
+        handleTagSyntaxAndSwitch(matchedToken, getTagNamingConvention(matchedToken, 2), DEFAULT);
+    }
+    |
+    <UNIFIED_CALL : "<@" | "[@" > { unifiedCall(matchedToken); }
+    |
+    <UNIFIED_CALL_END : ("<" | "[") "/@" ((<ID>) ("."<ID>)*)? <CLOSE_TAG1>> { unifiedCallEnd(matchedToken); }
+    |
+    <FTL_HEADER : ("<#ftl" | "[#ftl") <BLANK>> { ftlHeader(matchedToken); }
+    |
+    <TRIVIAL_FTL_HEADER : ("<#ftl" | "[#ftl") ("/")? (">" | "]")> { ftlHeader(matchedToken); }
+    |
+    /*
+     * ATTENTION: Update _CoreAPI.*_BUILT_IN_DIRECTIVE_NAMES if you add new directives!
+     */
+    <UNKNOWN_DIRECTIVE : ("[#" | "[/#" | "<#" | "</#") (["a"-"z", "A"-"Z", "_"])+>
+    {
+        char firstChar = matchedToken.image.charAt(0);
+
+        if (!directiveSyntaxEstablished && autodetectTagSyntax) {
+            squBracTagSyntax = (firstChar == '[');
+            directiveSyntaxEstablished = true;
+        }
+
+        if (firstChar == '<' && squBracTagSyntax) {
+            matchedToken.kind = STATIC_TEXT_NON_WS;
+        } else if (firstChar == '[' && !squBracTagSyntax) {
+            matchedToken.kind = STATIC_TEXT_NON_WS;
+        } else {
+            String dn = matchedToken.image;
+            int index = dn.indexOf('#');
+            dn = dn.substring(index + 1);
+
+            // Until the tokenizer/parser is reworked, we have this quirk where something like <#list>
+            // doesn't match any directive starter tokens, because that token requires whitespace after the
+            // name as it should be followed by parameters. For now we work this around so we don't report
+            // unknown directive:
+            if (ASTDirective.ALL_BUILT_IN_DIRECTIVE_NAMES.contains(dn)) {
+                throw new TokenMgrError(
+                        "#" + dn + " is an existing directive, but the tag is malformed. " 
+                        + " (See FreeMarker Manual / Directive Reference.)",
+                        TokenMgrError.LEXICAL_ERROR,
+                        matchedToken.beginLine, matchedToken.beginColumn + 1,
+                        matchedToken.endLine, matchedToken.endColumn);
+            }
+
+            String tip = null;
+            if (dn.equals("set") || dn.equals("var")) {
+                tip = "Use #assign or #local or #global, depending on the intented scope "
+                      + "(#assign is template-scope). " + PLANNED_DIRECTIVE_HINT;
+            } else if (dn.equals("else_if") || dn.equals("elif")) {
+            	tip = "Use #elseif.";
+            } else if (dn.equals("no_escape")) {
+            	tip = "Use #noescape instead.";
+            } else if (dn.equals("method")) {
+            	tip = "Use #function instead.";
+            } else if (dn.equals("head") || dn.equals("template") || dn.equals("fm")) {
+            	tip = "You may meant #ftl.";
+            } else if (dn.equals("try") || dn.equals("atempt")) {
+            	tip = "You may meant #attempt.";
+            } else if (dn.equals("for") || dn.equals("each") || dn.equals("iterate") || dn.equals("iterator")) {
+                tip = "You may meant #list (http://freemarker.org/docs/ref_directive_list.html).";
+            } else if (dn.equals("prefix")) {
+                tip = "You may meant #import. " + PLANNED_DIRECTIVE_HINT;
+            } else if (dn.equals("item") || dn.equals("row") || dn.equals("rows")) {
+                tip = "You may meant #items.";
+            } else if (dn.equals("separator") || dn.equals("separate") || dn.equals("separ")) {
+                tip = "You may meant #sep.";
+            } else {
+                tip = "Help (latest version): http://freemarker.org/docs/ref_directive_alphaidx.html; "
+                        + "you're using FreeMarker " + Configuration.getVersion() + ".";
+            }
+            throw new TokenMgrError(
+                    "Unknown directive: #" + dn + (tip != null ? ". " + tip : ""),
+                    TokenMgrError.LEXICAL_ERROR,
+                    matchedToken.beginLine, matchedToken.beginColumn + 1,
+                    matchedToken.endLine, matchedToken.endColumn);
+        }
+    }
+}
+
+<DEFAULT, NODIRECTIVE> TOKEN :
+{
+    <STATIC_TEXT_WS : ("\n" | "\r" | "\t" | " ")+>
+    |
+    <STATIC_TEXT_NON_WS : (~["$", "<", "#", "[", "{", "\n", "\r", "\t", " "])+>
+    |
+    <STATIC_TEXT_FALSE_ALARM : "$" | "#" | "<" | "[" | "{"> // to handle a lone dollar sign or "<" or "# or <@ with whitespace after"
+    |
+    <DOLLAR_INTERPOLATION_OPENING : "${"> { startInterpolation(matchedToken); }
+    |
+    <HASH_INTERPOLATION_OPENING : "#{"> { startInterpolation(matchedToken); }
+}
+
+<FM_EXPRESSION, IN_PAREN, NAMED_PARAMETER_EXPRESSION> SKIP :
+{
+    < ( " " | "\t" | "\n" | "\r" )+ >
+    |
+    < ("<" | "[") ("#" | "!") "--"> : EXPRESSION_COMMENT
+}
+
+<EXPRESSION_COMMENT> SKIP:
+{
+    < (~["-", ">", "]"])+ >
+    |
+    < ">">
+    |
+    < "]">
+    |
+    < "-">
+    |
+    < "-->" | "--]">
+    {
+        if (parenthesisNesting > 0) SwitchTo(IN_PAREN);
+        else if (inInvocation) SwitchTo(NAMED_PARAMETER_EXPRESSION);
+        else SwitchTo(FM_EXPRESSION);
+    }
+}
+
+<FM_EXPRESSION, IN_PAREN, NO_SPACE_EXPRESSION, NAMED_PARAMETER_EXPRESSION> TOKEN :
+{
+    <#ESCAPED_CHAR :
+        "\\"
+        (
+            ("n" | "t" | "r" | "f" | "b" | "g" | "l" | "a" | "\\" | "'" | "\"" | "$" | "{")
+            |
+            ("x" ["0"-"9", "A"-"F", "a"-"f"])
+        )
+    >
+    | 
+    <STRING_LITERAL :
+        (
+            "\""
+            ((~["\"", "\\"]) | <ESCAPED_CHAR>)*
+            "\""
+        )
+        |
+        (
+            "'"
+            ((~["'", "\\"]) | <ESCAPED_CHAR>)*
+            "'"
+        )
+    >
+    |
+    <RAW_STRING : "r" (("\"" (~["\""])* "\"") | ("'" (~["'"])* "'"))>
+    |
+    <FALSE : "false">
+    |
+    <TRUE : "true">
+    |
+    <INTEGER : (["0"-"9"])+>
+    |
+    <DECIMAL : <INTEGER> "." <INTEGER>>
+    |
+    <DOT : ".">
+    |
+    <DOT_DOT : "..">
+    |
+    <DOT_DOT_LESS : "..<" | "..!" >
+    |
+    <DOT_DOT_ASTERISK : "..*" >
+    |
+    <BUILT_IN : "?">
+    |
+    <EXISTS : "??">
+    |
+    <EQUALS : "=">
+    |
+    <DOUBLE_EQUALS : "==">
+    |
+    <NOT_EQUALS : "!=">
+    |
+    <PLUS_EQUALS : "+=">
+    |
+    <MINUS_EQUALS : "-=">
+    |
+    <TIMES_EQUALS : "*=">
+    |
+    <DIV_EQUALS : "/=">
+    |
+    <MOD_EQUALS : "%=">
+    |
+    <PLUS_PLUS : "++">
+    |
+    <MINUS_MINUS : "--">
+    |
+    <LESS_THAN : "lt" | "\\lt" | "<" | "&lt;">
+    |
+    <LESS_THAN_EQUALS : "lte" | "\\lte" | "<=" | "&lt;=">
+    |
+    <ESCAPED_GT: "gt" | "\\gt" |  "&gt;">
+    |
+    <ESCAPED_GTE : "gte" | "\\gte" | "&gt;=">
+    |
+    <PLUS : "+">
+    |
+    <MINUS : "-">
+    |
+    <TIMES : "*">
+    |
+    <DOUBLE_STAR : "**">
+    |
+    <ELLIPSIS : "...">
+    |
+    <DIVIDE : "/">
+    |
+    <PERCENT : "%">
+    |
+    <AND : "&" | "&&" >
+    |
+    <OR : "|" | "||">
+    |
+    <EXCLAM : "!">
+    |
+    <COMMA : ",">
+    |
+    <SEMICOLON : ";">
+    |
+    <COLON : ":">
+    |
+    <OPEN_BRACKET : "[">
+    {
+        ++bracketNesting;
+    }
+    |
+    <CLOSE_BRACKET : "]">
+    {
+        closeBracket(matchedToken);
+    }
+    |
+    <OPEN_PAREN : "(">
+    {
+        ++parenthesisNesting;
+        if (parenthesisNesting == 1) SwitchTo(IN_PAREN);
+    }
+    |
+    <CLOSE_PAREN : ")">
+    {
+        --parenthesisNesting;
+        if (parenthesisNesting == 0) {
+            if (inInvocation) SwitchTo(NAMED_PARAMETER_EXPRESSION);
+            else SwitchTo(FM_EXPRESSION);
+        }
+    }
+    |
+    <OPENING_CURLY_BRACKET : "{">
+    {
+        ++hashLiteralNesting;
+    }
+    |
+    <CLOSING_CURLY_BRACKET : "}">
+    {
+        if (hashLiteralNesting == 0) endInterpolation(matchedToken);
+        else --hashLiteralNesting;
+    }
+    |
+    <IN : "in">
+    |
+    <AS : "as">
+    |
+    <USING : "using">
+    |
+    <ID: <ID_START_CHAR> (<ID_START_CHAR>|<ASCII_DIGIT>)*> {
+        // Remove backslashes from Token.image:
+        final String s = matchedToken.image;
+        if (s.indexOf('\\') != -1) {
+            final int srcLn = s.length(); 
+            final char[] newS = new char[srcLn - 1];
+            int dstIdx = 0;
+            for (int srcIdx = 0; srcIdx < srcLn; srcIdx++) {
+                final char c = s.charAt(srcIdx);
+                if (c != '\\') {
+                    newS[dstIdx++] = c;
+                }
+            }
+            matchedToken.image = new String(newS, 0, dstIdx);
+        }
+    }
+    |
+    <OPEN_MISPLACED_INTERPOLATION : "${" | "#{">
+    {
+        if ("".length() == 0) {  // prevents unreachabe "break" compilation error in generated Java
+            char c = matchedToken.image.charAt(0);
+            throw new TokenMgrError(
+                    "You can't use \"" + c + "{\" here as you are already in FreeMarker-expression-mode. Thus, instead "
+                    + "of " + c + "{myExpression}, just write myExpression. "
+                    + "(" + c + "{...} is only needed where otherwise static text is expected, i.e, outside " 
+                    + "FreeMarker tags and ${...}-s.)",
+                    TokenMgrError.LEXICAL_ERROR,
+                    matchedToken.beginLine, matchedToken.beginColumn,
+                    matchedToken.endLine, matchedToken.endColumn);
+        }
+    }
+    |
+    <#NON_ESCAPED_ID_START_CHAR:
+        [
+            // This was generated on JDK 1.8.0_20 Win64 with src/main/misc/identifierChars/IdentifierCharGenerator.java
+			"$", 
+			"@" - "Z", 
+			"_", 
+			"a" - "z", 
+			"\u00AA", 
+			"\u00B5", 
+			"\u00BA", 
+			"\u00C0" - "\u00D6", 
+			"\u00D8" - "\u00F6", 
+			"\u00F8" - "\u1FFF", 
+			"\u2071", 
+			"\u207F", 
+			"\u2090" - "\u209C", 
+			"\u2102", 
+			"\u2107", 
+			"\u210A" - "\u2113", 
+			"\u2115", 
+			"\u2119" - "\u211D", 
+			"\u2124", 
+			"\u2126", 
+			"\u2128", 
+			"\u212A" - "\u212D", 
+			"\u212F" - "\u2139", 
+			"\u213C" - "\u213F", 
+			"\u2145" - "\u2149", 
+			"\u214E", 
+			"\u2183" - "\u2184", 
+			"\u2C00" - "\u2C2E", 
+			"\u2C30" - "\u2C5E", 
+			"\u2C60" - "\u2CE4", 
+			"\u2CEB" - "\u2CEE", 
+			"\u2CF2" - "\u2CF3", 
+			"\u2D00" - "\u2D25", 
+			"\u2D27", 
+			"\u2D2D", 
+			"\u2D30" - "\u2D67", 
+			"\u2D6F", 
+			"\u2D80" - "\u2D96", 
+			"\u2DA0" - "\u2DA6", 
+			"\u2DA8" - "\u2DAE", 
+			"\u2DB0" - "\u2DB6", 
+			"\u2DB8" - "\u2DBE", 
+			"\u2DC0" - "\u2DC6", 
+			"\u2DC8" - "\u2DCE", 
+			"\u2DD0" - "\u2DD6", 
+			"\u2DD8" - "\u2DDE", 
+			"\u2E2F", 
+			"\u3005" - "\u3006", 
+			"\u3031" - "\u3035", 
+			"\u303B" - "\u303C", 
+			"\u3040" - "\u318F", 
+			"\u31A0" - "\u31BA", 
+			"\u31F0" - "\u31FF", 
+			"\u3300" - "\u337F", 
+			"\u3400" - "\u4DB5", 
+			"\u4E00" - "\uA48C", 
+			"\uA4D0" - "\uA4FD", 
+			"\uA500" - "\uA60C", 
+			"\uA610" - "\uA62B", 
+			"\uA640" - "\uA66E", 
+			"\uA67F" - "\uA697", 
+			"\uA6A0" - "\uA6E5", 
+			"\uA717" - "\uA71F", 
+			"\uA722" - "\uA788", 
+			"\uA78B" - "\uA78E", 
+			"\uA790" - "\uA793", 
+			"\uA7A0" - "\uA7AA", 
+			"\uA7F8" - "\uA801", 
+			"\uA803" - "\uA805", 
+			"\uA807" - "\uA80A", 
+			"\uA80C" - "\uA822", 
+			"\uA840" - "\uA873", 
+			"\uA882" - "\uA8B3", 
+			"\uA8D0" - "\uA8D9", 
+			"\uA8F2" - "\uA8F7", 
+			"\uA8FB", 
+			"\uA900" - "\uA925", 
+			"\uA930" - "\uA946", 
+			"\uA960" - "\uA97C", 
+			"\uA984" - "\uA9B2", 
+			"\uA9CF" - "\uA9D9", 
+			"\uAA00" - "\uAA28", 
+			"\uAA40" - "\uAA42", 
+			"\uAA44" - "\uAA4B", 
+			"\uAA50" - "\uAA59", 
+			"\uAA60" - "\uAA76", 
+			"\uAA7A", 
+			"\uAA80" - "\uAAAF", 
+			"\uAAB1", 
+			"\uAAB5" - "\uAAB6", 
+			"\uAAB9" - "\uAABD", 
+			"\uAAC0", 
+			"\uAAC2", 
+			"\uAADB" - "\uAADD", 
+			"\uAAE0" - "\uAAEA", 
+			"\uAAF2" - "\uAAF4", 
+			"\uAB01" - "\uAB06", 
+			"\uAB09" - "\uAB0E", 
+			"\uAB11" - "\uAB16", 
+			"\uAB20" - "\uAB26", 
+			"\uAB28" - "\uAB2E", 
+			"\uABC0" - "\uABE2", 
+			"\uABF0" - "\uABF9", 
+			"\uAC00" - "\uD7A3", 
+			"\uD7B0" - "\uD7C6", 
+			"\uD7CB" - "\uD7FB", 
+			"\uF900" - "\uFB06", 
+			"\uFB13" - "\uFB17", 
+			"\uFB1D", 
+			"\uFB1F" - "\uFB28", 
+			"\uFB2A" - "\uFB36", 
+			"\uFB38" - "\uFB3C", 
+			"\uFB3E", 
+			"\uFB40" - "\uFB41", 
+			"\uFB43" - "\uFB44", 
+			"\uFB46" - "\uFBB1", 
+			"\uFBD3" - "\uFD3D", 
+			"\uFD50" - "\uFD8F", 
+			"\uFD92" - "\uFDC7", 
+			"\uFDF0" - "\uFDFB", 
+			"\uFE70" - "\uFE74", 
+			"\uFE76" - "\uFEFC", 
+			"\uFF10" - "\uFF19", 
+			"\uFF21" - "\uFF3A", 
+			"\uFF41" - "\uFF5A", 
+			"\uFF66" - "\uFFBE", 
+			"\uFFC2" - "\uFFC7", 
+			"\uFFCA" - "\uFFCF", 
+			"\uFFD2" - "\uFFD7", 
+			"\uFFDA" - "\uFFDC" 
+        ]
+    >
+    |
+    <#ESCAPED_ID_CHAR: "\\" ("-" | "." | ":")>
+    |
+    <#ID_START_CHAR: <NON_ESCAPED_ID_START_CHAR>|<ESCAPED_ID_CHAR>>
+    |
+    <#ASCII_DIGIT: ["0" - "9"]>
+}
+
+<FM_EXPRESSION, NO_SPACE_EXPRESSION, NAMED_PARAMETER_EXPRESSION> TOKEN :
+{
+    <DIRECTIVE_END : ">">
+    {
+        if (inFTLHeader) eatNewline();
+        inFTLHeader = false;
+        if (squBracTagSyntax) {
+            matchedToken.kind = NATURAL_GT;
+        } else {
+            SwitchTo(DEFAULT);
+        }
+    }
+    |
+    <EMPTY_DIRECTIVE_END : "/>" | "/]">
+    {
+        if (inFTLHeader) eatNewline();
+        inFTLHeader = false;
+        SwitchTo(DEFAULT);
+    }
+}
+
+<IN_PAREN> TOKEN :
+{
+    <NATURAL_GT : ">">
+    |
+    <NATURAL_GTE : ">=">
+}
+
+<NO_SPACE_EXPRESSION> TOKEN :
+{
+    <TERMINATING_WHITESPACE :  (["\n", "\r", "\t", " "])+> : FM_EXPRESSION
+}
+
+<NAMED_PARAMETER_EXPRESSION> TOKEN :
+{
+    <TERMINATING_EXCLAM : "!" (["\n", "\r", "\t", " "])+> : FM_EXPRESSION
+}
+
+<NO_PARSE> TOKEN :
+{
+    <TERSE_COMMENT_END : "-->" | "--]">
+    {
+        if (noparseTag.equals("-->")) {
+            boolean squareBracket = matchedToken.image.endsWith("]");
+            if ((squBracTagSyntax && squareBracket) || (!squBracTagSyntax && !squareBracket)) {
+                matchedToken.image = matchedToken.image + ";"; 
+                SwitchTo(DEFAULT);
+            }
+        }
+    }
+    |
+    <MAYBE_END :
+        ("<" | "[")
+        "/#"
+        (["a"-"z", "A"-"Z"])+
+        ( " " | "\t" | "\n" | "\r" )*
+        (">" | "]")
+    >
+    {
+        StringTokenizer st = new StringTokenizer(image.toString(), " \t\n\r<>[]/#", false);
+        if (st.nextToken().equals(noparseTag)) {
+            matchedToken.image = matchedToken.image + ";"; 
+            SwitchTo(DEFAULT);
+        }
+    }
+    |
+    <KEEP_GOING : (~["<", "[", "-"])+>
+    |
+    <LONE_LESS_THAN_OR_DASH : ["<", "[", "-"]>
+}
+
+// Now the actual parsing code, starting
+// with the productions for FreeMarker's
+// expression syntax.
+
+/**
+ * This is the same as ASTExpOr, since
+ * OR is the operator with the lowest
+ * precedence.
+ */
+ASTExpression ASTExpression() :
+{
+    ASTExpression exp;
+}
+{
+    exp = ASTExpOr()
+    {
+        return exp;
+    }
+}
+
+/**
+ * Lowest level expression, a literal, a variable,
+ * or a possibly more complex expression bounded
+ * by parentheses.
+ */
+ASTExpression PrimaryExpression() :
+{
+    ASTExpression exp;
+}
+{
+    (
+        exp = ASTExpNumberLiteral()
+        |   
+        exp = ASTExpHashLiteral()
+        |   
+        exp = ASTExpStringLiteral(true)
+        |   
+        exp = ASTExpBooleanLiteral()
+        |   
+        exp = ASTExpListLiteral()
+        |   
+        exp = ASTExpVariable()
+        |   
+        exp = Parenthesis()
+        |   
+        exp = ASTExpBuiltInVariable()
+    )
+    (
+        LOOKAHEAD(<DOT> | <OPEN_BRACKET> |<OPEN_PAREN> | <BUILT_IN> | <EXCLAM> | <TERMINATING_EXCLAM> | <EXISTS>)
+        exp = AddSubExpression(exp)
+    )*
+    {
+        return exp;
+    }
+}
+
+ASTExpression Parenthesis() :
+{
+    ASTExpression exp, result;
+    Token start, end;
+}
+{
+    start = <OPEN_PAREN>
+    exp = ASTExpression()
+    end = <CLOSE_PAREN>
+    {
+        result = new ASTExpParenthesis(exp);
+        result.setLocation(template, start, end);
+        return result;
+    }
+}
+
+/**
+ * A primary expression preceded by zero or
+ * more unary operators. (The only unary operator we
+ * currently have is the NOT.)
+ */
+ASTExpression UnaryExpression() :
+{
+    ASTExpression exp, result;
+    boolean haveNot = false;
+    Token t = null, start = null;
+}
+{
+    (
+        result = ASTExpNegateOrPlus()
+        |
+        result = ASTExpNot()
+        |
+        result = PrimaryExpression()
+    )
+    {
+        return result;
+    }
+}
+
+ASTExpression ASTExpNot() : 
+{
+    Token t;
+    ASTExpression exp, result = null;
+    ArrayList nots = new ArrayList();
+}
+{
+    (
+        t = <EXCLAM> { nots.add(t); }
+    )+
+    exp = PrimaryExpression()
+    {
+        for (int i = 0; i < nots.size(); i++) {
+            result = new ASTExpNot(exp);
+            Token tok = (Token) nots.get(nots.size() -i -1);
+            result.setLocation(template, tok, exp);
+            exp = result;
+        }
+        return result;
+    }
+}
+
+ASTExpression ASTExpNegateOrPlus() :
+{
+    ASTExpression exp, result;
+    boolean isMinus = false;
+    Token t;
+}
+{
+    (
+        t = <PLUS>
+        |
+        t = <MINUS> { isMinus = true; }
+    )
+    exp = PrimaryExpression()
+    {
+        result = new ASTExpNegateOrPlus(exp, isMinus);  
+        result.setLocation(template, t, exp);
+        return result;
+    }
+}
+
+ASTExpression AdditiveExpression() :
+{
+    ASTExpression lhs, rhs, result;
+    boolean plus;
+}
+{
+    lhs = MultiplicativeExpression() { result = lhs; }
+    (
+        LOOKAHEAD(<PLUS>|<MINUS>)
+        (
+            (
+                <PLUS> { plus = true; }
+                |
+                <MINUS> { plus = false; }
+            )
+        )
+        rhs = MultiplicativeExpression()
+        {
+            if (plus) {
+	            // plus is treated separately, since it is also
+	            // used for concatenation.
+                result = new ASTExpAddOrConcat(lhs, rhs);
+            } else {
+                numberLiteralOnly(lhs);
+                numberLiteralOnly(rhs);
+                result = new ArithmeticExpression(lhs, rhs, ArithmeticExpression.TYPE_SUBSTRACTION);
+            }
+            result.setLocation(template, lhs, rhs);
+            lhs = result;
+        }
+    )*
+    {
+        return result;
+    }
+}
+
+/**
+ * A unary expression followed by zero or more
+ * unary expressions with operators in between.
+ */
+ASTExpression MultiplicativeExpression() :
+{
+    ASTExpression lhs, rhs, result;
+    int operation = ArithmeticExpression.TYPE_MULTIPLICATION;
+}
+{
+    lhs = UnaryExpression() { result = lhs; }
+    (
+        LOOKAHEAD(<TIMES>|<DIVIDE>|<PERCENT>)
+        (
+            (
+                <TIMES> { operation = ArithmeticExpression.TYPE_MULTIPLICATION; }
+                |
+                <DIVIDE> { operation = ArithmeticExpression.TYPE_DIVISION; }
+                |
+                <PERCENT> {operation = ArithmeticExpression.TYPE_MODULO; }
+            )
+        )
+        rhs = UnaryExpression()
+        {
+            numberLiteralOnly(lhs);
+            numberLiteralOnly(rhs);
+            result = new ArithmeticExpression(lhs, rhs, operation);
+            result.setLocation(template, lhs, rhs);
+            lhs = result;
+        }
+    )*
+    {
+        return result;
+    }
+}
+
+
+ASTExpression EqualityExpression() :
+{
+    ASTExpression lhs, rhs, result;
+    Token t;
+}
+{
+    lhs = RelationalExpression() { result = lhs; }
+    [
+        LOOKAHEAD(<NOT_EQUALS>|<EQUALS>|<DOUBLE_EQUALS>)
+        (
+            t = <NOT_EQUALS> 
+            |
+            t = <EQUALS> 
+            |
+            t = <DOUBLE_EQUALS>
+        )
+        rhs = RelationalExpression()
+        {
+	        notHashLiteral(lhs, "scalar");
+	        notHashLiteral(rhs, "scalar");
+	        notListLiteral(lhs, "scalar");
+	        notListLiteral(rhs, "scalar");
+	        result = new ASTExpComparison(lhs, rhs, t.image);
+	        result.setLocation(template, lhs, rhs);
+        }
+    ]
+    {
+        return result;
+    }
+}
+
+ASTExpression RelationalExpression() :
+{
+    ASTExpression lhs, rhs, result;
+    Token t;
+}
+{
+    lhs = RangeExpression() { result = lhs; }
+    [
+        LOOKAHEAD(<NATURAL_GTE>|<ESCAPED_GTE>|<NATURAL_GT>|<ESCAPED_GT>|<LESS_THAN_EQUALS>|<LESS_THAN_EQUALS>|<LESS_THAN>)
+        (
+            t = <NATURAL_GTE>
+            |
+            t = <ESCAPED_GTE>
+            |
+            t = <NATURAL_GT>
+            |
+            t = <ESCAPED_GT>
+            |
+            t = <LESS_THAN_EQUALS>
+            |
+            t = <LESS_THAN>
+        )
+        rhs = RangeExpression()
+        {
+            notHashLiteral(lhs, "scalar");
+            notHashLiteral(rhs, "scalar");
+            notListLiteral(lhs, "scalar");
+            notListLiteral(rhs, "scalar");
+            notStringLiteral(lhs, "number");
+            notStringLiteral(rhs, "number");
+            result = new ASTExpComparison(lhs, rhs, t.image);
+            result.setLocation(template, lhs, rhs);
+        }
+    ]
+    {
+        return result;
+    }
+}
+
+ASTExpression RangeExpression() :
+{
+    ASTExpression lhs, rhs = null, result;
+    int endType;
+    Token dotDot = null;
+}
+{
+    lhs = AdditiveExpression() { result = lhs; }
+    [
+        LOOKAHEAD(1)  // To suppress warning
+        (
+            (
+                (
+                    <DOT_DOT_LESS> { endType = ASTExpRange.END_EXCLUSIVE; }
+                    |
+                    <DOT_DOT_ASTERISK> { endType = ASTExpRange.END_SIZE_LIMITED; }
+                )
+                rhs = AdditiveExpression()
+            )
+            | 
+            (
+                dotDot = <DOT_DOT> { endType = ASTExpRange.END_UNBOUND; }
+                [
+                    LOOKAHEAD(AdditiveExpression())
+                    rhs = AdditiveExpression()
+                    {
+                        endType = ASTExpRange.END_INCLUSIVE;
+                    }
+                ]
+            )
+        )
+        {
+            numberLiteralOnly(lhs);
+            if (rhs != null) {
+                numberLiteralOnly(rhs);
+            }
+           
+            ASTExpRange range = new ASTExpRange(lhs, rhs, endType);
+            if (rhs != null) {
+                range.setLocation(template, lhs, rhs);
+            } else {
+                range.setLocation(template, lhs, dotDot);
+            }
+            result = range;
+        }
+    ]
+    {
+        return result;
+    }
+}
+
+
+
+
+ASTExpression ASTExpAnd() :
+{
+    ASTExpression lhs, rhs, result;
+}
+{
+    lhs = EqualityExpression() { result = lhs; }
+    (
+        LOOKAHEAD(<AND>)
+        <AND>
+        rhs = EqualityExpression()
+        {
+            booleanLiteralOnly(lhs);
+            booleanLiteralOnly(rhs);
+            result = new ASTExpAnd(lhs, rhs);
+            result.setLocation(template, lhs, rhs);
+            lhs = result;
+        }
+    )*
+    {
+        return result;
+    }
+}
+
+ASTExpression ASTExpOr() :
+{
+    ASTExpression lhs, rhs, result;
+}
+{
+    lhs = ASTExpAnd() { result = lhs; }
+    (
+        LOOKAHEAD(<OR>)
+        <OR>
+        rhs = ASTExpAnd()
+        {
+            booleanLiteralOnly(lhs);
+            booleanLiteralOnly(rhs);
+            result = new ASTExpOr(lhs, rhs);
+            result.setLocation(template, lhs, rhs);
+            lhs = result;
+        }
+    )*
+    {
+        return result;
+    }
+}
+
+ASTExpListLiteral ASTExpListLiteral() :
+{
+    ArrayList values = new ArrayList();
+    Token begin, end;
+}
+{
+    begin = <OPEN_BRACKET>
+    values = PositionalArgs()
+    end = <CLOSE_BRACKET>
+    {
+        ASTExpListLiteral result = new ASTExpListLiteral(values);
+        result.setLocation(template, begin, end);
+        return result;
+    }
+}
+
+ASTExpression ASTExpNumberLiteral() :
+{
+    Token op = null, t;
+}
+{
+    (
+        t = <INTEGER>
+        |
+        t = <DECIMAL>
+    )
+    {
+        String s = t.image;
+        ASTExpression result = new ASTExpNumberLiteral(pCfg.getArithmeticEngine().toNumber(s));
+        Token startToken = (op != null) ? op : t;
+        result.setLocation(template, startToken, t);
+        return result;
+    }
+}
+
+ASTExpVariable ASTExpVariable() :
+{
+    Token t;
+}
+{
+    t = <ID>
+    {
+        ASTExpVariable id = new ASTExpVariable(t.image);
+        id.setLocation(template, t, t);
+        return id;
+    }
+}
+
+ASTExpression IdentifierOrStringLiteral() :
+{
+    ASTExpression exp;
+}
+{
+    (
+        exp = ASTExpVariable()
+        |
+        exp = ASTExpStringLiteral(false)
+    )
+    {
+        return exp;
+    }   
+}
+
+ASTExpBuiltInVariable ASTExpBuiltInVariable() :
+{
+    Token dot, name;
+}
+{
+    dot = <DOT>
+    name = <ID>
+    {
+        ASTExpBuiltInVariable result = null;
+        token_source.checkNamingConvention(name);
+
+        TemplateModel parseTimeValue;
+        String nameStr = name.image;
+        if (nameStr.equals(ASTExpBuiltInVariable.OUTPUT_FORMAT) || nameStr.equals(ASTExpBuiltInVariable.OUTPUT_FORMAT_CC)) {
+            parseTimeValue = new SimpleScalar(outputFormat.getName());
+        } else if (nameStr.equals(ASTExpBuiltInVariable.AUTO_ESC) || nameStr.equals(ASTExpBuiltInVariable.AUTO_ESC_CC)) {
+            parseTimeValue = autoEscaping ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        } else {
+            parseTimeValue = null;
+        }
+        
+        result = new ASTExpBuiltInVariable(name, token_source, parseTimeValue);
+        
+        result.setLocation(template, dot, name);
+        return result;
+    }
+}
+
+/**
+ * Production that builds up an expression
+ * using the dot or dynamic key name
+ * or the args list if this is a method invocation.
+ */
+ASTExpression AddSubExpression(ASTExpression exp) :
+{
+    ASTExpression result = null;
+}
+{
+    (
+        result = DotVariable(exp)
+        |
+        result = DynamicKey(exp)
+        |
+        result = MethodArgs(exp)
+        |
+        result = ASTExpBuiltIn(exp)
+        |
+        result = DefaultTo(exp)
+        |
+        result = Exists(exp)
+    )
+    {
+        return result;
+    }
+}
+
+ASTExpression DefaultTo(ASTExpression exp) :
+{
+    ASTExpression rhs = null;
+    Token t;
+}
+{
+    (
+        t = <TERMINATING_EXCLAM>
+        |
+        (
+            t = <EXCLAM>
+            [
+                LOOKAHEAD(ASTExpression())
+                rhs = ASTExpression()
+            ]
+        )
+    )
+    {
+        ASTExpDefault result = new ASTExpDefault(exp, rhs);
+        if (rhs == null) {
+            result.setLocation(template, exp, t);
+        } else {
+            result.setLocation(template, exp, rhs);
+        }
+        return result;
+    }
+}
+
+ASTExpression Exists(ASTExpression exp) :
+{
+    Token t;
+}
+{
+    t = <EXISTS>
+    {
+        ASTExpExists result = new ASTExpExists(exp);
+        result.setLocation(template, exp, t);
+        return result;
+    }
+}
+
+ASTExpression ASTExpBuiltIn(ASTExpression lhoExp) :
+{
+    Token t = null;
+    ASTExpBuiltIn result;
+    ArrayList/*<ASTExpression>*/ args = null;
+    Token openParen;
+    Token closeParen;
+}
+{
+    <BUILT_IN>
+    t = <ID>
+    {
+        token_source.checkNamingConvention(t);
+        result = ASTExpBuiltIn.newBuiltIn(incompatibleImprovements, lhoExp, t, token_source);
+        result.setLocation(template, lhoExp, t);
+        
+        if (!(result instanceof SpecialBuiltIn)) {
+            return result;
+        }
+
+        if (result instanceof BuiltInForLoopVariable) {
+            if (!(lhoExp instanceof ASTExpVariable)) {
+                throw new ParseException(
+                        "Expression used as the left hand operand of ?" + t.image
+                        + " must be a simple loop variable name.", lhoExp);
+            }
+            String loopVarName = ((ASTExpVariable) lhoExp).getName();
+            checkLoopVariableBuiltInLHO(loopVarName, lhoExp, t);
+            ((BuiltInForLoopVariable) result).bindToLoopVariable(loopVarName);
+            
+            return result;
+        }
+        
+        if (result instanceof BuiltInBannedWhenAutoEscaping) {
+	        if (outputFormat instanceof MarkupOutputFormat && autoEscaping) {
+	            throw new ParseException(
+	                    "Using ?" + t.image + " (legacy escaping) is not allowed when auto-escaping is on with "
+	                    + "a markup output format (" + outputFormat.getName() + "), to avoid double-escaping mistakes.",
+	                    template, t);
+	        }
+            
+            return result;
+        }
+
+        if (result instanceof MarkupOutputFormatBoundBuiltIn) {
+            if (!(outputFormat instanceof MarkupOutputFormat)) {
+                throw new ParseException(
+                        "?" + t.image + " can't be used here, as the current output format isn't a markup (escaping) "
+                        + "format: " + outputFormat, template, t);
+            }
+            ((MarkupOutputFormatBoundBuiltIn) result).bindToMarkupOutputFormat((MarkupOutputFormat) outputFormat);
+            
+            return result;
+        }
+
+        if (result instanceof OutputFormatBoundBuiltIn) {
+            ((OutputFormatBoundBuiltIn) result).bindToOutputFormat(outputFormat, autoEscapingPolicy);
+            
+            return result;
+        }
+    }
+    [
+        LOOKAHEAD({ result instanceof BuiltInWithParseTimeParameters  })
+        openParen = <OPEN_PAREN>
+        args = PositionalArgs()
+        closeParen = <CLOSE_PAREN> {
+            result.setLocation(template, lhoExp, closeParen);
+            ((BuiltInWithParseTimeParameters) result).bindToParameters(args, openParen, closeParen);
+            
+            return result;
+        }
+    ]
+    {
+        // Should have already return-ed
+        throw new AssertionError("Unhandled " + SpecialBuiltIn.class.getName() + " subclass: " + result.getClass());
+    }
+}
+
+
+/**
+ * production for when a key is specified by <DOT> + keyname
+ */
+ASTExpression DotVariable(ASTExpression exp) :
+{
+    Token t;
+}
+{
+        <DOT>
+        (
+            t = <ID> | t = <TIMES> | t = <DOUBLE_STAR> 
+            |
+            (
+                t = <LESS_THAN>
+                |
+                t = <LESS_THAN_EQUALS>
+                |
+                t = <ESCAPED_GT>
+                |
+                t = <ESCAPED_GTE>
+                |
+                t = <FALSE>
+                |
+                t = <TRUE>
+                |
+                t = <IN>
+                |
+                t = <AS>
+                |
+                t = <USING>
+            )
+            {
+                if (!Character.isLetter(t.image.charAt(0))) {
+                    throw new ParseException(t.image + " is not a valid identifier.", template, t);
+                }
+            }
+        )
+        {
+            notListLiteral(exp, "hash");
+            notStringLiteral(exp, "hash");
+            notBooleanLiteral(exp, "hash");
+            ASTExpDot dot = new ASTExpDot(exp, t.image);
+            dot.setLocation(template, exp, t);
+            return dot;
+        }
+}
+
+/**
+ * production for when the key is specified
+ * in brackets.
+ */
+ASTExpression DynamicKey(ASTExpression exp) :
+{
+    ASTExpression arg;
+    Token t;
+}
+{
+    <OPEN_BRACKET>
+    arg = ASTExpression()
+    t = <CLOSE_BRACKET>
+    {
+        notBooleanLiteral(exp, "list or hash");
+        notNumberLiteral(exp, "list or hash");
+        ASTExpDynamicKeyName dkn = new ASTExpDynamicKeyName(exp, arg);
+        dkn.setLocation(template, exp, t);
+        return dkn;
+    }
+}
+
+/**
+ * production for an arglist part of a method invocation.
+ */
+ASTExpMethodCall MethodArgs(ASTExpression exp) :
+{
+        ArrayList args = new ArrayList();
+        Token end;
+}
+{
+        <OPEN_PAREN>
+        args = PositionalArgs()
+        end = <CLOSE_PAREN>
+        {
+            args.trimToSize();
+            ASTExpMethodCall result = new ASTExpMethodCall(exp, args);
+            result.setLocation(template, exp, end);
+            return result;
+        }
+}
+
+ASTExpStringLiteral ASTExpStringLiteral(boolean interpolate) :
+{
+    Token t;
+    boolean raw = false;
+}
+{
+    (
+        t = <STRING_LITERAL>
+        |
+        t = <RAW_STRING> { raw = true; }
+    )
+    {
+        String s;
+        // Get rid of the quotes.
+        if (raw) {
+            s = t.image.substring(2, t.image.length() -1);
+        } else {
+	        try {
+	            s = FTLUtil.unescapeStringLiteralPart(t.image.substring(1, t.image.length() -1));
+            } catch (GenericParseException e) {
+                throw new ParseException(e.getMessage(), template, t);
+            }
+        }
+        ASTExpStringLiteral result = new ASTExpStringLiteral(s);
+        result.setLocation(template, t, t);
+        if (interpolate && !raw) {
+            // TODO: This logic is broken. It can't handle literals that contains both ${...} and $\{...}. 
+            if (t.image.indexOf("${") >= 0 || t.image.indexOf("#{") >= 0) result.parseValue(token_source, outputFormat);
+        }
+        return result;
+    }
+}
+
+ASTExpression ASTExpBooleanLiteral() :
+{
+    Token t;
+    ASTExpression result;
+}
+{
+    (
+        t = <FALSE> { result = new ASTExpBooleanLiteral(false); }
+        |
+        t = <TRUE> { result = new ASTExpBooleanLiteral(true); }
+    )
+    {
+        result.setLocation(template, t, t);
+        return result;
+    }
+}
+
+
+ASTExpHashLiteral ASTExpHashLiteral() :
+{
+    Token begin, end;
+    ASTExpression key, value;
+    ArrayList keys = new ArrayList();
+    ArrayList values = new ArrayList();
+}
+{
+    begin = <OPENING_CURLY_BRACKET>
+    [
+        key = ASTExpression()
+        (<COMMA>|<COLON>)
+        value = ASTExpression()
+        {
+            stringLiteralOnly(key);
+            keys.add(key);
+            values.add(value);
+        }
+        (
+            <COMMA>
+            key = ASTExpression()
+            (<COMMA>|<COLON>)
+            value = ASTExpression()
+            {
+                stringLiteralOnly(key);
+                keys.add(key);
+                values.add(value);
+            }
+        )*
+    ]
+    end = <CLOSING_CURLY_BRACKET>
+    {
+        ASTExpHashLiteral result = new ASTExpHashLiteral(keys, values);
+        result.setLocation(template, begin, end);
+        return result;
+    }
+}
+
+/**
+ * A production representing the ${...}
+ * that outputs a variable.
+ */
+ASTDollarInterpolation StringOutput() :
+{
+    ASTExpression exp;
+    Token begin, end;
+}
+{
+    begin = <DOLLAR_INTERPOLATION_OPENING>
+    exp = ASTExpression()
+    {
+        notHashLiteral(exp, NonStringException.STRING_COERCABLE_TYPES_DESC);
+        notListLiteral(exp, NonStringException.STRING_COERCABLE_TYPES_DESC);
+    }
+    end = <CLOSING_CURLY_BRACKET>
+    {
+        ASTDollarInterpolation result = new ASTDollarInterpolation(
+                exp, escapedExpression(exp),
+                outputFormat,
+                autoEscaping);
+        result.setLocation(template, begin, end);
+        return result;
+    }
+}
+
+ASTHashInterpolation ASTHashInterpolation() :
+{
+    ASTExpression exp;
+    Token fmt = null, begin, end;
+}
+{
+    begin = <HASH_INTERPOLATION_OPENING>
+    exp = ASTExpression() { numberLiteralOnly(exp); }
+    [
+        <SEMICOLON>
+        fmt = <ID>
+    ]
+    end = <CLOSING_CURLY_BRACKET>
+    {
+        MarkupOutputFormat<?> autoEscOF = autoEscaping && outputFormat instanceof MarkupOutputFormat
+                ? (MarkupOutputFormat<?>) outputFormat : null;
+    
+        ASTHashInterpolation result;
+        if (fmt != null) {
+            int minFrac = -1;  // -1 indicates that the value has not been set
+            int maxFrac = -1;
+
+            StringTokenizer st = new StringTokenizer(fmt.image, "mM", true);
+            char type = '-';
+            while (st.hasMoreTokens()) {
+                String token = st.nextToken();
+                try {
+	                if (type != '-') {
+	                    switch (type) {
+	                    case 'm':
+	                        if (minFrac != -1) throw new ParseException("Invalid formatting string", template, fmt);
+	                        minFrac = Integer.parseInt(token);
+	                        break;
+	                    case 'M':
+	                        if (maxFrac != -1) throw new ParseException("Invalid formatting string", template, fmt);
+	                        maxFrac = Integer.parseInt(token);
+	                        break;
+	                    default:
+	                        throw new ParseException("Invalid formatting string", template, fmt);
+	                    }
+	                    type = '-';
+	                } else if (token.equals("m")) {
+	                    type = 'm';
+	                } else if (token.equals("M")) {
+	                    type = 'M';
+	                } else {
+	                    throw new ParseException();
+	                }
+                } catch (ParseException e) {
+                	throw new ParseException("Invalid format specifier " + fmt.image, template, fmt);
+                } catch (NumberFormatException e) {
+                	throw new ParseException("Invalid number in the format specifier " + fmt.image, template, fmt);
+                }
+            }
+
+            if (maxFrac == -1) {
+	            if (minFrac == -1) {
+	                throw new ParseException(
+	                		"Invalid format specification, at least one of m and M must be specified!", template, fmt);
+	            }
+            	maxFrac = minFrac;
+            } else if (minFrac == -1) {
+            	minFrac = 0;
+            }
+            if (minFrac > maxFrac) {
+            	throw new ParseException(
+            			"Invalid format specification, min cannot be greater than max!", template, fmt);
+            }
+            if (minFrac > 50 || maxFrac > 50) {// sanity check
+                throw new ParseException("Cannot specify more than 50 fraction digits", template, fmt);
+            }
+            result = new ASTHashInterpolation(exp, minFrac, maxFrac, autoEscOF);
+        } else {  // if format != null
+            result = new ASTHashInterpolation(exp, autoEscOF);
+        }
+        result.setLocation(template, begin, end);
+        return result;
+    }
+}
+
+ASTElement If() :
+{
+    Token start, end, t;
+    ASTExpression condition;
+    TemplateElements children;
+    ASTDirIfElseIfElseContainer ifBlock;
+    ASTDirIfOrElseOrElseIf cblock;
+}
+{
+    start = <IF>
+    condition = ASTExpression()
+    end = <DIRECTIVE_END>
+    children = MixedContentElements()
+    {
+        cblock = new ASTDirIfOrElseOrElseIf(condition, children, ASTDirIfOrElseOrElseIf.TYPE_IF);
+        cblock.setLocation(template, start, end, children);
+        ifBlock = new ASTDirIfElseIfElseContainer(cblock);
+    }
+    (
+        t = <ELSE_IF>
+        condition = ASTExpression()
+        end = LooseDirectiveEnd()
+        children = MixedContentElements()
+        {
+            cblock = new ASTDirIfOrElseOrElseIf(condition, children, ASTDirIfOrElseOrElseIf.TYPE_ELSE_IF);
+            cblock.setLocation(template, t, end, children);
+            ifBlock.addBlock(cblock);
+        }
+    )*
+    [
+            t = <ELSE>
+            children = MixedContentElements()
+            {
+                cblock = new ASTDirIfOrElseOrElseIf(null, children, ASTDirIfOrElseOrElseIf.TYPE_ELSE);
+                cblock.setLocation(template, t, t, children);
+                ifBlock.addBlock(cblock);
+            }
+    ]
+    end = <END_IF>
+    {
+        ifBlock.setLocation(template, start, end);
+        return ifBlock;
+    }
+}
+
+ASTDirAttemptRecoverContainer Attempt() :
+{
+    Token start, end;
+    TemplateElements children;
+    ASTDirRecover recoveryBlock;
+}
+{
+    start = <ATTEMPT>
+    children = MixedContentElements()
+    recoveryBlock = Recover()
+    (
+        end = <END_RECOVER>
+        |
+        end = <END_ATTEMPT>
+    )
+    {
+        ASTDirAttemptRecoverContainer result = new ASTDirAttemptRecoverContainer(children, recoveryBlock);
+        result.setLocation(template, start, end);
+        return result;
+    }
+}
+
+ASTDirRecover Recover() : 
+{
+    Token start;
+    TemplateElements children;
+}
+{
+    start = <RECOVER>
+    children = MixedContentElements()
+    {
+        ASTDirRecover result = new ASTDirRecover(children);
+        result.setLocation(template, start, start, children);
+        return result;
+    }
+}
+
+ASTElement List() :
+{
+    ASTExpression exp;
+    Token loopVar = null, loopVar2 = null, start, end;
+    TemplateElements childrendBeforeElse;
+    ASTDirElseOfList elseOfList = null;
+    ParserIteratorBlockContext iterCtx;
+}
+{
+    start = <LIST>
+    exp = ASTExpression()
+    [
+        <AS>
+        loopVar = <ID>
+        [
+            <COMMA>
+            loopVar2 = <ID>
+        ]
+    ]
+    <DIRECTIVE_END>
+    {
+        iterCtx = pushIteratorBlockContext();
+        if (loopVar != null) {
+            iterCtx.loopVarName = loopVar.image;
+            breakableDirectiveNesting++;
+            if (loopVar2 != null) {
+                iterCtx.loopVar2Name = loopVar2.image;
+                iterCtx.hashListing = true;
+                if (iterCtx.loopVar2Name.equals(iterCtx.loopVarName)) {
+                    throw new ParseException(
+                            "The key and value loop variable names must differ, but both were: " + iterCtx.loopVarName,
+                            template, start);
+                }
+            }
+        }
+    }
+    
+    childrendBeforeElse = MixedContentElements()
+    {
+        if (loopVar != null) {
+            breakableDirectiveNesting--;
+        } else if (iterCtx.kind != ITERATOR_BLOCK_KIND_ITEMS) {
+            throw new ParseException(
+                    "#list must have either \"as loopVar\" parameter or nested #items that belongs to it.",
+                    template, start);
+        }
+        popIteratorBlockContext();
+    }
+    
+    [
+        elseOfList = ASTDirElseOfList()
+    ]
+    
+    end = <END_LIST>
+    {
+        ASTDirList list = new ASTDirList(
+                exp,
+                loopVar != null ? loopVar.image : null,  // null when we have a nested #items
+                loopVar2 != null ? loopVar2.image : null,
+                childrendBeforeElse, iterCtx.hashListing);
+        list.setLocation(template, start, end);
+
+        ASTElement result;
+        if (elseOfList == null) {
+            result = list;
+        } else {
+            result = new ASTDirListElseContainer(list, elseOfList);
+            result.setLocation(template, start, end);
+        }
+        return result;
+    }
+}
+
+ASTDirElseOfList ASTDirElseOfList() :
+{
+    Token start;
+    TemplateElements children;
+}
+{
+        start = <ELSE>
+        children = MixedContentElements()
+        {
+            ASTDirElseOfList result = new ASTDirElseOfList(children);
+	        result.setLocation(template, start, start, children);
+	        return result;
+        }
+}
+
+ASTDirItems Items() :
+{
+    Token loopVar, loopVar2 = null, start, end;
+    TemplateElements children;
+    ParserIteratorBlockContext iterCtx;
+}
+{
+    start = <ITEMS>
+    loopVar = <ID>
+    [
+        <COMMA>
+        loopVar2 = <ID>
+    ]
+    <DIRECTIVE_END>
+    {
+        iterCtx = peekIteratorBlockContext();
+        if (iterCtx == null) {
+            throw new ParseException("#items must be inside a #list block.", template, start);
+        }
+        if (iterCtx.loopVarName != null) {
+            String msg;
+	        if (iterCtx.kind == ITERATOR_BLOCK_KIND_ITEMS) {
+                msg = "Can't nest #items into each other when they belong to the same #list.";
+	        } else {
+	            msg = "The parent #list of the #items must not have \"as loopVar\" parameter.";
+            }
+            throw new ParseException(msg, template, start);
+        }
+        iterCtx.kind = ITERATOR_BLOCK_KIND_ITEMS;
+        iterCtx.loopVarName = loopVar.image;
+        if (loopVar2 != null) {
+            iterCtx.loopVar2Name = loopVar2.image;
+            iterCtx.hashListing = true;
+            if (iterCtx.loopVar2Name.equals(iterCtx.loopVarName)) {
+                throw new ParseException(
+                        "The key and value loop variable names must differ, but both were: " + iterCtx.loopVarName,
+                        template, start);
+            }
+        }
+    
+        breakableDirectiveNesting++;
+    }
+    
+    children = MixedContentElements()
+    
+    end = <END_ITEMS>
+    {
+        breakableDirectiveNesting--;
+        iterCtx.loopVarName = null;
+        iterCtx.loopVar2Name = null;
+        
+        ASTDirItems result = new ASTDirItems(loopVar.image, loopVar2 != null ? loopVar2.image : null, children);
+        result.setLocation(template, start, end);
+        return result;
+    }
+}
+
+ASTDirSep Sep() :
+{
+    Token loopVar, start, end = null;
+    TemplateElements children;
+}
+{
+    start = <SEP>
+    {
+        if (peekIteratorBlockContext() == null) {
+            throw new ParseException(
+                    "#sep must be inside a #list block.",
+                    template, start);
+        }
+    }
+    children = MixedContentElements()
+    [
+        LOOKAHEAD(1)
+        end = <END_SEP>
+    ]
+    {
+        ASTDirSep result = new ASTDirSep(children);
+        if (end != null) {
+            result.setLocation(template, start, end);
+        } else {
+            result.setLocation(template, start, start, children);
+        }
+        return result;
+    }
+}
+
+ASTDirVisit Visit() :
+{
+    Token start, end;
+    ASTExpression targetNode, namespaces = null;
+}
+{
+    start = <VISIT>
+    targetNode = ASTExpression()
+    [
+        <USING>
+        namespaces = ASTExpression()
+    ]
+    end = LooseDirectiveEnd()
+    {
+        ASTDirVisit result = new ASTDirVisit(targetNode, namespaces);
+        result.setLocation(template, start, end);
+        return result;
+    }
+}
+
+ASTDirRecurse Recurse() :
+{
+    Token start, end = null;
+    ASTExpression node = null, namespaces = null;
+}
+{
+    (
+        start = <SIMPLE_RECURSE>
+        |
+        (
+            start = <RECURSE>
+            [
+                node = ASTExpression()
+            ]
+            [
+                <USING>
+                namespaces = ASTExpression()
+            ]
+            end = LooseDirectiveEnd()
+        )
+    )
+    {
+        if (end == null) end = start;
+        ASTDirRecurse result = new ASTDirRecurse(node, namespaces);
+        result.setLocation(template, start, end);
+        return result;
+    }
+}
+
+ASTDirFallback FallBack() :
+{
+    Token tok;
+}
+{
+    tok = <FALLBACK>
+    {
+        if (!inMacro) {
+            throw new ParseException("Cannot fall back outside a macro.", template, tok);
+        }
+        ASTDirFallback result = new ASTDirFallback();
+        result.setLocation(template, tok, tok);
+        return result;
+    }
+}
+
+/**
+ * Production used to break out of a loop or a switch block.
+ */
+ASTDirBreak Break() :
+{
+    Token start;
+}
+{
+    start = <BREAK>
+    {
+        if (breakableDirectiveNesting < 1) {
+            throw new ParseException(start.image + " must be nested inside a directive that supports it: " 
+                    + " #list with \"as\", #items, #switch",
+                    template, start);
+        }
+        ASTDirBreak result = new ASTDirBreak();
+        result.setLocation(template, start, start);
+        return result;
+    }
+}
+
+/**
+ * Production used to jump out of a macro.
+ * The stop instruction terminates the rendering of the template.
+ */
+ASTDirReturn Return() :
+{
+    Token start, end = null;
+    ASTExpression exp = null;
+}
+{
+    (
+        start = <SIMPLE_RETURN> { end = start; }
+        |
+        start = <RETURN> exp = ASTExpression() end = LooseDirectiveEnd()
+    )
+    {
+        if (inMacro) {
+            if (exp != null) {
+            	throw new ParseException("A macro cannot return a value", template, start);
+            }
+        } else if (inFunction) {
+            if (exp == null) {
+            	throw new ParseException("A function must return a value", template, start);
+            }
+        } else {
+            if (exp == null) {
+            	throw new ParseException(
+            			"A return instruction can only occur inside a macro or function", template, start);
+            }
+        }
+        ASTDirReturn result = new ASTDirReturn(exp);
+        result.setLocation(template, start, end);
+        return result;
+    }
+}
+
+ASTDirStop Stop() :
+{
+    Token start = null;
+    ASTExpression exp = null;
+}
+{
+    (
+        start = <HALT>
+        |
+        start = <STOP> exp = ASTExpression() LooseDirectiveEnd()
+    )
+    {
+        ASTDirStop result = new ASTDirStop(exp);
+        result.setLocation(template, start, start);
+        return result;
+    }
+}
+
+ASTElement Nested() :
+{
+    Token t, end;
+    ArrayList bodyParameters;
+    ASTDirNested result = null;
+}
+{
+    (
+        (
+            t = <SIMPLE_NESTED>
+            {
+                result = new ASTDirNested(null);
+                result.setLocation(template, t, t);
+            }
+        )
+        |
+        (
+            t = <NESTED>
+            bodyParameters = PositionalArgs()
+            end = LooseDirectiveEnd()
+            {
+                result = new ASTDirNested(bodyParameters);
+                result.setLocation(template, t, end);
+            }
+        )
+    )
+    {
+        if (!inMacro) {
+            throw new ParseException("Cannot use a " + t.image + " instruction outside a macro.", template, t);
+        }
+        return result;
+    }
+}
+
+ASTElement Flush() :
+{
+    Token t;
+}
+{
+    t = <FLUSH>
+    {
+        ASTDirFlush result = new ASTDirFlush();
+        result.setLocation(template, t, t);
+        return result;
+    }
+}
+
+ASTElement Trim() :
+{
+    Token t;
+    ASTDirTOrTrOrTl result = null;
+}
+{
+    (
+        t = <TRIM> { result = new ASTDirTOrTrOrTl(true, true); }
+        |
+        t = <LTRIM> { result = new ASTDirTOrTrOrTl(true, false); }
+        |
+        t = <RTRIM> { result = new ASTDirTOrTrOrTl(false, true); }
+        |
+        t = <NOTRIM> { result = new ASTDirTOrTrOrTl(false, false); }
+    )
+    {
+        result.setLocation(template, t, t);
+        return result;
+    }
+}
+
+
+ASTElement Assign() :
+{
+    Token start, end;
+    int scope;
+    Token id = null;
+    Token equalsOp;
+    ASTExpression nameExp, exp, nsExp = null;
+    String varName;
+    ArrayList assignments = new ArrayList();
+    ASTDirAssignment ass;
+    TemplateElements children;
+}
+{
+    (
+        start = <ASSIGN> { scope = ASTDirAssignment.NAMESPACE; }
+        |
+        start = <GLOBALASSIGN> { scope = ASTDirAssignment.GLOBAL; }
+        |
+        start = <LOCALASSIGN> { scope = ASTDirAssignment.LOCAL; }
+        {
+            scope = ASTDirAssignment.LOCAL;
+            if (!inMacro && !inFunction) {
+                throw new ParseException("Local variable assigned outside a macro.", template, start);
+            }
+        }
+    )
+    nameExp = IdentifierOrStringLiteral()
+    {
+        varName = (nameExp instanceof ASTExpStringLiteral)
+                ? ((ASTExpStringLiteral) nameExp).getAsString()
+                : ((ASTExpVariable) nameExp).getName();
+    }
+    (
+    	(
+            (
+	    	    (
+			        (<EQUALS>|<PLUS_EQUALS>|<MINUS_EQUALS>|<TIMES_EQUALS>|<DIV_EQUALS>|<MOD_EQUALS>)
+			        {
+			           equalsOp = token;
+

<TRUNCATED>