freemarker-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ddek...@apache.org
Subject [37/51] [abbrv] [partial] incubator-freemarker git commit: Restructured project so that freemarker-test-utils depends on freemarker-core (and hence can provide common classes for testing templates, and can use utility classes defined in the core). As a c
Date Mon, 15 May 2017 21:23:53 GMT
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/templateresolver/TemplateConfigurationFactoryTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/templateresolver/TemplateConfigurationFactoryTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/templateresolver/TemplateConfigurationFactoryTest.java
new file mode 100644
index 0000000..a7259d8
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/templateresolver/TemplateConfigurationFactoryTest.java
@@ -0,0 +1,203 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * 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 static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.freemarker.core.TemplateConfiguration;
+import org.junit.Test;
+
+public class TemplateConfigurationFactoryTest {
+    
+    @Test
+    public void testCondition1() throws IOException, TemplateConfigurationFactoryException {
+        TemplateConfiguration tc = newTemplateConfiguration(1);
+        
+        TemplateConfigurationFactory tcf = new ConditionalTemplateConfigurationFactory(new FileNameGlobMatcher("*.ftlx"), tc);
+
+        assertNotApplicable(tcf, "x.ftl");
+        assertApplicable(tcf, "x.ftlx", tc);
+    }
+
+    @Test
+    public void testCondition2() throws IOException, TemplateConfigurationFactoryException {
+        TemplateConfiguration tc = newTemplateConfiguration(1);
+        
+        TemplateConfigurationFactory tcf = new ConditionalTemplateConfigurationFactory(
+                new FileNameGlobMatcher("*.ftlx"),
+                new ConditionalTemplateConfigurationFactory(
+                        new FileNameGlobMatcher("x.*"), tc));
+
+        assertNotApplicable(tcf, "x.ftl");
+        assertNotApplicable(tcf, "y.ftlx");
+        assertApplicable(tcf, "x.ftlx", tc);
+    }
+
+    @Test
+    public void testMerging() throws IOException, TemplateConfigurationFactoryException {
+        TemplateConfiguration tc1 = newTemplateConfiguration(1);
+        TemplateConfiguration tc2 = newTemplateConfiguration(2);
+        TemplateConfiguration tc3 = newTemplateConfiguration(3);
+        
+        TemplateConfigurationFactory tcf = new MergingTemplateConfigurationFactory(
+                new ConditionalTemplateConfigurationFactory(new FileNameGlobMatcher("*.ftlx"), tc1),
+                new ConditionalTemplateConfigurationFactory(new FileNameGlobMatcher("*a*.*"), tc2),
+                new ConditionalTemplateConfigurationFactory(new FileNameGlobMatcher("*b*.*"), tc3));
+
+        assertNotApplicable(tcf, "x.ftl");
+        assertApplicable(tcf, "x.ftlx", tc1);
+        assertApplicable(tcf, "a.ftl", tc2);
+        assertApplicable(tcf, "b.ftl", tc3);
+        assertApplicable(tcf, "a.ftlx", tc1, tc2);
+        assertApplicable(tcf, "b.ftlx", tc1, tc3);
+        assertApplicable(tcf, "ab.ftl", tc2, tc3);
+        assertApplicable(tcf, "ab.ftlx", tc1, tc2, tc3);
+        
+        assertNotApplicable(new MergingTemplateConfigurationFactory(), "x.ftl");
+    }
+
+    @Test
+    public void testFirstMatch() throws IOException, TemplateConfigurationFactoryException {
+        TemplateConfiguration tc1 = newTemplateConfiguration(1);
+        TemplateConfiguration tc2 = newTemplateConfiguration(2);
+        TemplateConfiguration tc3 = newTemplateConfiguration(3);
+        
+        FirstMatchTemplateConfigurationFactory tcf = new FirstMatchTemplateConfigurationFactory(
+                new ConditionalTemplateConfigurationFactory(new FileNameGlobMatcher("*.ftlx"), tc1),
+                new ConditionalTemplateConfigurationFactory(new FileNameGlobMatcher("*a*.*"), tc2),
+                new ConditionalTemplateConfigurationFactory(new FileNameGlobMatcher("*b*.*"), tc3));
+
+        try {
+            assertNotApplicable(tcf, "x.ftl");
+        } catch (TemplateConfigurationFactoryException e) {
+            assertThat(e.getMessage(), containsString("x.ftl"));
+        }
+        tcf.setNoMatchErrorDetails("Test details");
+        try {
+            assertNotApplicable(tcf, "x.ftl");
+        } catch (TemplateConfigurationFactoryException e) {
+            assertThat(e.getMessage(), containsString("Test details"));
+        }
+        
+        tcf.setAllowNoMatch(true);
+        
+        assertNotApplicable(tcf, "x.ftl");
+        assertApplicable(tcf, "x.ftlx", tc1);
+        assertApplicable(tcf, "a.ftl", tc2);
+        assertApplicable(tcf, "b.ftl", tc3);
+        assertApplicable(tcf, "a.ftlx", tc1);
+        assertApplicable(tcf, "b.ftlx", tc1);
+        assertApplicable(tcf, "ab.ftl", tc2);
+        assertApplicable(tcf, "ab.ftlx", tc1);
+        
+        assertNotApplicable(new FirstMatchTemplateConfigurationFactory().allowNoMatch(true), "x.ftl");
+    }
+
+    @Test
+    public void testComplex() throws IOException, TemplateConfigurationFactoryException {
+        TemplateConfiguration tcA = newTemplateConfiguration(1);
+        TemplateConfiguration tcBSpec = newTemplateConfiguration(2);
+        TemplateConfiguration tcBCommon = newTemplateConfiguration(3);
+        TemplateConfiguration tcHH = newTemplateConfiguration(4);
+        TemplateConfiguration tcHtml = newTemplateConfiguration(5);
+        TemplateConfiguration tcXml = newTemplateConfiguration(6);
+        TemplateConfiguration tcNWS = newTemplateConfiguration(7);
+        
+        TemplateConfigurationFactory tcf = new MergingTemplateConfigurationFactory(
+                new FirstMatchTemplateConfigurationFactory(
+                        new ConditionalTemplateConfigurationFactory(new PathGlobMatcher("a/**"), tcA),
+                        new ConditionalTemplateConfigurationFactory(new PathGlobMatcher("b/**"),
+                                new MergingTemplateConfigurationFactory(
+                                    new ConditionalTemplateConfigurationFactory(new FileNameGlobMatcher("*"), tcBCommon),
+                                    new ConditionalTemplateConfigurationFactory(new FileNameGlobMatcher("*.s.*"), tcBSpec))))
+                        .allowNoMatch(true),
+                new FirstMatchTemplateConfigurationFactory(
+                        new ConditionalTemplateConfigurationFactory(new FileNameGlobMatcher("*.hh"), tcHH),
+                        new ConditionalTemplateConfigurationFactory(new FileNameGlobMatcher("*.*h"), tcHtml),
+                        new ConditionalTemplateConfigurationFactory(new FileNameGlobMatcher("*.*x"), tcXml))
+                        .allowNoMatch(true),
+                new ConditionalTemplateConfigurationFactory(new FileNameGlobMatcher("*.nws.*"), tcNWS));
+
+        assertNotApplicable(tcf, "x.ftl");
+        assertApplicable(tcf, "b/x.ftl", tcBCommon);
+        assertApplicable(tcf, "b/x.s.ftl", tcBCommon, tcBSpec);
+        assertApplicable(tcf, "b/x.s.ftlh", tcBCommon, tcBSpec, tcHtml);
+        assertApplicable(tcf, "b/x.s.nws.ftlx", tcBCommon, tcBSpec, tcXml, tcNWS);
+        assertApplicable(tcf, "a/x.s.nws.ftlx", tcA, tcXml, tcNWS);
+        assertApplicable(tcf, "a.hh", tcHH);
+        assertApplicable(tcf, "a.nws.hh", tcHH, tcNWS);
+    }
+
+    @SuppressWarnings("boxing")
+    private TemplateConfiguration newTemplateConfiguration(int id) {
+        TemplateConfiguration.Builder tcb = new TemplateConfiguration.Builder();
+        tcb.setCustomAttribute("id", id);
+        tcb.setCustomAttribute("contains" + id, true);
+        return tcb.build();
+    }
+
+    private void assertNotApplicable(TemplateConfigurationFactory tcf, String sourceName)
+            throws IOException, TemplateConfigurationFactoryException {
+        assertNull(tcf.get(sourceName, DummyTemplateLoadingSource.INSTANCE));
+    }
+
+    private void assertApplicable(TemplateConfigurationFactory tcf, String sourceName, TemplateConfiguration... expectedTCs)
+            throws IOException, TemplateConfigurationFactoryException {
+        TemplateConfiguration mergedTC = tcf.get(sourceName, DummyTemplateLoadingSource.INSTANCE);
+        List<Object> mergedTCAttNames = new ArrayList<>(mergedTC.getCustomAttributes().keySet());
+
+        for (TemplateConfiguration expectedTC : expectedTCs) {
+            Integer tcId = (Integer) expectedTC.getCustomAttribute("id");
+            if (tcId == null) {
+                fail("TemplateConfiguration-s must be created with newTemplateConfiguration(id) in this test");
+            }
+            if (!mergedTCAttNames.contains("contains" + tcId)) {
+                fail("TemplateConfiguration with ID " + tcId + " is missing from the asserted value");
+            }
+        }
+        
+        for (Object attKey: mergedTCAttNames) {
+            if (!containsCustomAttr(attKey, expectedTCs)) {
+                fail("The asserted TemplateConfiguration contains an unexpected custom attribute: " + attKey);
+            }
+        }
+        
+        assertEquals(expectedTCs[expectedTCs.length - 1].getCustomAttribute("id"), mergedTC.getCustomAttribute("id"));
+    }
+
+    private boolean containsCustomAttr(Object attKey, TemplateConfiguration... expectedTCs) {
+        for (TemplateConfiguration expectedTC : expectedTCs) {
+            if (expectedTC.getCustomAttribute(attKey) != null) {
+                return true;
+            }
+        }
+        return false;
+    }
+    
+    @SuppressWarnings("serial")
+    private static class DummyTemplateLoadingSource implements TemplateLoadingSource {
+        private static DummyTemplateLoadingSource INSTANCE = new DummyTemplateLoadingSource();
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/templateresolver/TemplateNameFormatTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/templateresolver/TemplateNameFormatTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/templateresolver/TemplateNameFormatTest.java
new file mode 100644
index 0000000..3a24bfc
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/templateresolver/TemplateNameFormatTest.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;
+
+import static org.apache.freemarker.test.hamcerst.Matchers.*;
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.util.Locale;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core.TemplateNotFoundException;
+import org.apache.freemarker.core.templateresolver.impl.ByteArrayTemplateLoader;
+import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateNameFormat;
+import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateNameFormatFM2;
+import org.apache.freemarker.test.MonitoredTemplateLoader;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+
+
+public class TemplateNameFormatTest {
+
+    @Test
+    public void testToRootBasedName() throws MalformedTemplateNameException {
+        // Path that are treated the same both in 2.3 and 2.4 format:
+        for (TemplateNameFormat tnf : new TemplateNameFormat[] {
+                DefaultTemplateNameFormatFM2.INSTANCE, DefaultTemplateNameFormat.INSTANCE }) {
+            // Relative paths:
+            // - No scheme:
+            assertEquals("a/b", tnf.toRootBasedName("a/", "b"));
+            assertEquals("/a/b", tnf.toRootBasedName("/a/", "b"));
+            assertEquals("a/b", tnf.toRootBasedName("a/f", "b"));
+            assertEquals("/a/b", tnf.toRootBasedName("/a/f", "b"));
+            // - Scheme:
+            assertEquals("s://a/b", tnf.toRootBasedName("s://a/", "b"));
+            assertEquals("s:///a/b", tnf.toRootBasedName("s:///a/", "b"));
+            assertEquals("s://a/b", tnf.toRootBasedName("s://a/f", "b"));
+            assertEquals("s:///a/b", tnf.toRootBasedName("s:///a/f", "b"));
+            assertEquals("s://b", tnf.toRootBasedName("s://f", "b"));
+            assertEquals("s:///b", tnf.toRootBasedName("s:///f", "b"));
+            
+            // Absolute paths:
+            // - No scheme:
+            assertEquals("b", tnf.toRootBasedName("a/", "/b"));
+            assertEquals("b", tnf.toRootBasedName("/a/", "/b"));
+            assertEquals("b", tnf.toRootBasedName("a/s:/f/", "/b"));
+            // - Scheme:
+            assertEquals("s://b", tnf.toRootBasedName("s://x/", "/b"));
+            assertEquals("s://b", tnf.toRootBasedName("s:///x/", "/b"));
+            
+            // Schemed absolute paths:
+            assertEquals("s://b", tnf.toRootBasedName("a/", "s://b"));
+            assertEquals("s://b", tnf.toRootBasedName("i://a/", "s://b"));
+        }
+        
+        // Scheme names in 2.4 format only:
+        {
+            final TemplateNameFormat tnf = DefaultTemplateNameFormat.INSTANCE;
+            assertEquals("s:b", tnf.toRootBasedName("s:f", "b"));
+            assertEquals("s:/b", tnf.toRootBasedName("s:/f", "b"));
+            assertEquals("s:b", tnf.toRootBasedName("s:f", "/b"));
+            assertEquals("s:b", tnf.toRootBasedName("s:/f", "/b"));
+            assertEquals("s:f/b", tnf.toRootBasedName("s:f/", "b"));
+            assertEquals("s:/f/b", tnf.toRootBasedName("s:/f/", "b"));
+            assertEquals("s:b", tnf.toRootBasedName("s:f/", "/b"));
+            assertEquals("s:b", tnf.toRootBasedName("s:/f/", "/b"));
+            assertEquals("s:b", tnf.toRootBasedName("s:/f/", "/b"));
+            assertEquals("b", tnf.toRootBasedName("a/s://f/", "/b"));
+        }
+        
+        // Scheme names in 2.3 format only:
+        {
+            final TemplateNameFormat tnf = DefaultTemplateNameFormatFM2.INSTANCE;
+            assertEquals("a/s://b", tnf.toRootBasedName("a/s://f/", "/b"));
+        }
+    }
+
+    @Test
+    public void testNormalizeRootBasedName() throws MalformedTemplateNameException {
+        // Normalizations that are the same in legacy and modern format:
+        for (TemplateNameFormat tnf : new TemplateNameFormat[] {
+                DefaultTemplateNameFormatFM2.INSTANCE, DefaultTemplateNameFormat.INSTANCE }) {
+            assertEquals("", tnf.normalizeRootBasedName(""));
+            for (String lead : new String[] { "", "/" }) {
+                assertEquals("foo", tnf.normalizeRootBasedName(lead + "foo"));
+                assertEquals("foo", tnf.normalizeRootBasedName(lead + "./foo"));
+                assertEquals("foo", tnf.normalizeRootBasedName(lead + "./././foo"));
+                assertEquals("foo", tnf.normalizeRootBasedName(lead + "bar/../foo"));
+                assertEquals("a/b/", tnf.normalizeRootBasedName("a/b/"));
+                assertEquals("a/", tnf.normalizeRootBasedName("a/b/../"));
+                assertEquals("a/c../..d/e*/*f", tnf.normalizeRootBasedName("a/c../..d/e*/*f"));
+                assertEquals("", tnf.normalizeRootBasedName(""));
+                assertEquals("foo/bar/*", tnf.normalizeRootBasedName("foo/bar/*"));
+                assertEquals("schema://", tnf.normalizeRootBasedName("schema://"));
+                
+                assertThrowsWithBackingOutException(lead + "bar/../../x/foo", tnf);
+                assertThrowsWithBackingOutException(lead + "../x", tnf);
+                assertThrowsWithBackingOutException(lead + "../../../x", tnf);
+                assertThrowsWithBackingOutException(lead + "../../../x", tnf);
+                assertThrowsWithBackingOutException("x://../../../foo", tnf);
+                
+                {
+                    final String name = lead + "foo\u0000";
+                    try {
+                        tnf.normalizeRootBasedName(name);
+                        fail();
+                    } catch (MalformedTemplateNameException e) {
+                        assertEquals(name, e.getTemplateName());
+
+                        assertThat(e.getMalformednessDescription(), containsStringIgnoringCase("null character"));
+                    }
+                }
+            }
+        }
+        
+        // ".." and "."
+        assertEqualsOn23AndOn24("bar/foo", "foo", "bar/./../foo");
+        
+        // Even number of leading ".."-s bug:
+        assertNormRBNameEqualsOn23ButThrowsBackOutExcOn24("foo", "../../foo");
+        assertNormRBNameEqualsOn23ButThrowsBackOutExcOn24("foo", "../../../../foo");
+        
+        // ".." and "*"
+        assertEqualsOn23AndOn24("a/b/foo", "a/*/foo", "a/b/*/../foo");
+        //
+        assertEqualsOn23AndOn24("a/foo", "foo", "a/b/*/../../foo");
+        //
+        assertNormRBNameEqualsOn23ButThrowsBackOutExcOn24("foo", "a/b/*/../../../foo");
+        //
+        assertEqualsOn23AndOn24("a/b/*/foo", "a/*/foo", "a/b/*/*/../foo");
+        //
+        assertEqualsOn23AndOn24("a/b/*/c/foo", "a/b/*/foo", "a/b/*/c/*/../foo");
+        //
+        assertEqualsOn23AndOn24("a/b/*/c/foo", "a/b/*/foo", "a/b/*/c/d/*/../../foo");
+        //
+        assertEqualsOn23AndOn24("a/*//b/*/c/foo", "a/*/b/*/foo", "a/*//b/*/c/d/*/../../foo");
+        //
+        assertEqualsOn23AndOn24("*", "", "a/../*");
+        //
+        assertEqualsOn23AndOn24("*/", "", "a/../*/");
+        
+        // ".." and "scheme"
+        assertNormRBNameEqualsOn23ButThrowsBackOutExcOn24("x:/foo", "x://../foo");
+        //
+        assertNormRBNameEqualsOn23ButThrowsBackOutExcOn24("foo", "x://../../foo");
+        //
+        assertNormRBNameEqualsOn23ButThrowsBackOutExcOn24("x:../foo", "x:../foo");
+        //
+        assertNormRBNameEqualsOn23ButThrowsBackOutExcOn24("foo", "x:../../foo");
+
+        // Tricky cases with terminating "/":
+        assertEqualsOn23AndOn24("/", "", "/");
+        // Terminating "/.." (produces terminating "/"):
+        assertEqualsOn23AndOn24("foo/bar/..", "foo/", "foo/bar/..");
+        // Terminating "/." (produces terminating "/"):
+        assertEqualsOn23AndOn24("foo/bar/.", "foo/bar/", "foo/bar/.");
+        
+        // Lonely "."
+        assertEqualsOn23AndOn24(".", "", ".");
+        // Lonely ".."
+        assertNormRBNameEqualsOn23ButThrowsBackOutExcOn24("..", "..");
+        // Lonely "*"
+        
+        // Eliminating redundant "//":
+        assertEqualsOn23AndOn24("foo//bar", "foo/bar", "foo//bar");
+        //
+        assertEqualsOn23AndOn24("///foo//bar///baaz////wombat", "foo/bar/baaz/wombat", "////foo//bar///baaz////wombat");
+        //
+        assertEqualsOn23AndOn24("scheme://foo", "scheme://foo", "scheme://foo");
+        //
+        assertEqualsOn23AndOn24("scheme://foo//x/y", "scheme://foo/x/y", "scheme://foo//x/y");
+        //
+        assertEqualsOn23AndOn24("scheme:///foo", "scheme://foo", "scheme:///foo");
+        //
+        assertEqualsOn23AndOn24("scheme:////foo", "scheme://foo", "scheme:////foo");
+        
+        // Eliminating redundant "*"-s:
+        assertEqualsOn23AndOn24("a/*/*/b", "a/*/b", "a/*/*/b");
+        //
+        assertEqualsOn23AndOn24("a/*/*/*/b", "a/*/b", "a/*/*/*/b");
+        //
+        assertEqualsOn23AndOn24("*/*/b", "b", "*/*/b");
+        //
+        assertEqualsOn23AndOn24("*/*/b", "b", "/*/*/b");
+        //
+        assertEqualsOn23AndOn24("b/*/*", "b/*", "b/*/*");
+        //
+        assertEqualsOn23AndOn24("b/*/*/*", "b/*", "b/*/*/*");
+        //
+        assertEqualsOn23AndOn24("*/a/*/b/*/*/c", "a/*/b/*/c", "*/a/*/b/*/*/c");
+        
+        // New kind of scheme handling:
+
+        assertEquals("s:a/b", DefaultTemplateNameFormat.INSTANCE.normalizeRootBasedName("s:a/b"));
+        assertEquals("s:a/b", DefaultTemplateNameFormat.INSTANCE.normalizeRootBasedName("s:/a/b"));
+        assertEquals("s://a/b", DefaultTemplateNameFormat.INSTANCE.normalizeRootBasedName("s://a/b"));
+        assertEquals("s://a/b", DefaultTemplateNameFormat.INSTANCE.normalizeRootBasedName("s:///a/b"));
+        assertEquals("s://a/b", DefaultTemplateNameFormat.INSTANCE.normalizeRootBasedName("s:////a/b"));
+        
+        // Illegal use a of ":":
+        assertNormRBNameThrowsColonExceptionOn24("a/b:c/d");
+        assertNormRBNameThrowsColonExceptionOn24("a/b:/..");
+    }
+    
+    @Test
+    public void assertBackslashNotSpecialWith23() throws IOException {
+        MonitoredTemplateLoader tl = new MonitoredTemplateLoader();
+        tl.putTextTemplate("foo\\bar.ftl", "");
+
+        Configuration cfg = new TestConfigurationBuilder().templateLoader(tl).build();
+
+        {
+            final String name = "foo\\bar.ftl";
+            
+            Template t = cfg.getTemplate(name, Locale.US);
+            assertEquals(name, t.getLookupName());
+            assertEquals(name, t.getSourceName());
+            assertEquals(
+                    ImmutableList.of(
+                            "foo\\bar_en_US.ftl",
+                            "foo\\bar_en.ftl",
+                            name),
+                    tl.getLoadNames());
+            tl.clearEvents();
+        }
+
+        try {
+            cfg.getTemplate("foo\\missing.ftl", Locale.US);
+            fail();
+        } catch (TemplateNotFoundException e) {
+            assertEquals("foo\\missing.ftl", e.getTemplateName());
+            assertEquals(
+                    ImmutableList.of(
+                            "foo\\missing_en_US.ftl",
+                            "foo\\missing_en.ftl",
+                            "foo\\missing.ftl"),
+                    tl.getLoadNames());
+            tl.clearEvents();
+            cfg.clearTemplateCache();
+        }
+        
+        {
+            final String name = "foo/bar\\..\\bar.ftl";
+            try {
+                cfg.getTemplate(name, Locale.US);
+                fail();
+            } catch (TemplateNotFoundException e) {
+                assertEquals(name, e.getTemplateName());
+            }
+        }
+        
+    }
+
+    @Test
+    public void assertBackslashNotAllowed() throws IOException {
+        Configuration cfg = new TestConfigurationBuilder()
+                .templateLoader(new ByteArrayTemplateLoader())
+                .templateNameFormat(DefaultTemplateNameFormat.INSTANCE)
+                .build();
+        try {
+            cfg.getTemplate("././foo\\bar.ftl", Locale.US);
+            fail();
+        } catch (MalformedTemplateNameException e) {
+            assertThat(e.getMessage(), containsStringIgnoringCase("backslash"));
+        }
+        
+    }
+    
+    private void assertEqualsOn23AndOn24(String expected23, String expected24, String name)
+            throws MalformedTemplateNameException {
+        assertEquals(expected23, DefaultTemplateNameFormatFM2.INSTANCE.normalizeRootBasedName(name));
+        assertEquals(expected24, DefaultTemplateNameFormat.INSTANCE.normalizeRootBasedName(name));
+    }
+
+    private void assertNormRBNameEqualsOn23ButThrowsBackOutExcOn24(final String expected23, final String name)
+            throws MalformedTemplateNameException {
+        assertEquals(expected23, DefaultTemplateNameFormatFM2.INSTANCE.normalizeRootBasedName(name));
+        assertThrowsWithBackingOutException(name, DefaultTemplateNameFormat.INSTANCE);
+    }
+
+    private void assertThrowsWithBackingOutException(final String name, final TemplateNameFormat tnf) {
+        try {
+            tnf.normalizeRootBasedName(name);
+            fail();
+        } catch (MalformedTemplateNameException e) {
+            assertEquals(name, e.getTemplateName());
+            assertBackingOutFromRootException(e);
+        }
+    }
+
+    private void assertNormRBNameThrowsColonExceptionOn24(final String name) throws MalformedTemplateNameException {
+        try {
+            DefaultTemplateNameFormat.INSTANCE.normalizeRootBasedName(name);
+            fail();
+        } catch (MalformedTemplateNameException e) {
+            assertEquals(name, e.getTemplateName());
+            assertColonException(e);
+        }
+    }
+    
+    private void assertBackingOutFromRootException(MalformedTemplateNameException e) {
+        assertThat(e.getMessage(), containsStringIgnoringCase("backing out"));
+    }
+
+    private void assertColonException(MalformedTemplateNameException e) {
+        assertThat(e.getMessage(), containsString("':'"));
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/templateresolver/TemplateSourceMatcherTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/templateresolver/TemplateSourceMatcherTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/templateresolver/TemplateSourceMatcherTest.java
new file mode 100644
index 0000000..51273c0
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/templateresolver/TemplateSourceMatcherTest.java
@@ -0,0 +1,188 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * 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 static org.junit.Assert.*;
+
+import java.io.IOException;
+
+import org.junit.Test;
+
+public class TemplateSourceMatcherTest {
+    
+    @Test
+    public void testPathGlobMatcher() throws IOException {
+        PathGlobMatcher m = new PathGlobMatcher("**/a/?.ftl");
+        assertTrue(m.matches("a/b.ftl", "dummy"));
+        assertTrue(m.matches("x/a/c.ftl", "dummy"));
+        assertFalse(m.matches("a/b.Ftl", "dummy"));
+        assertFalse(m.matches("b.ftl", "dummy"));
+        assertFalse(m.matches("a/bc.ftl", "dummy"));
+        
+        m = new PathGlobMatcher("**/a/?.ftl").caseInsensitive(true);
+        assertTrue(m.matches("A/B.FTL", "dummy"));
+        m.setCaseInsensitive(false);
+        assertFalse(m.matches("A/B.FTL", "dummy"));
+        
+        try {
+            new PathGlobMatcher("/b.ftl");
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+    }
+
+    @Test
+    public void testFileNameGlobMatcher() throws IOException {
+        FileNameGlobMatcher m = new FileNameGlobMatcher("a*.ftl");
+        assertTrue(m.matches("ab.ftl", "dummy"));
+        assertTrue(m.matches("dir/ab.ftl", "dummy"));
+        assertTrue(m.matches("/dir/dir/ab.ftl", "dummy"));
+        assertFalse(m.matches("Ab.ftl", "dummy"));
+        assertFalse(m.matches("bb.ftl", "dummy"));
+        assertFalse(m.matches("ab.ftl/x", "dummy"));
+
+        m = new FileNameGlobMatcher("a*.ftl").caseInsensitive(true);
+        assertTrue(m.matches("AB.FTL", "dummy"));
+        m.setCaseInsensitive(false);
+        assertFalse(m.matches("AB.FTL", "dummy"));
+        
+        m = new FileNameGlobMatcher("\u00E1*.ftl").caseInsensitive(true);
+        assertTrue(m.matches("\u00C1b.ftl", "dummy"));
+        
+        try {
+            new FileNameGlobMatcher("dir/a*.ftl");
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+    }
+
+    @Test
+    public void testFileExtensionMatcher() throws IOException {
+        FileExtensionMatcher m = new FileExtensionMatcher("ftlx");
+        assertTrue(m.matches("a.ftlx", "dummy"));
+        assertTrue(m.matches(".ftlx", "dummy"));
+        assertTrue(m.matches("b/a.b.ftlx", "dummy"));
+        assertTrue(m.matches("b/a.ftlx", "dummy"));
+        assertTrue(m.matches("c.b/a.ftlx", "dummy"));
+        assertFalse(m.matches("a.ftl", "dummy"));
+        assertFalse(m.matches("ftlx", "dummy"));
+        assertFalse(m.matches("b.ftlx/a.ftl", "dummy"));
+        
+        assertTrue(m.isCaseInsensitive());
+        assertTrue(m.matches("a.fTlX", "dummy"));
+        m.setCaseInsensitive(false);
+        assertFalse(m.matches("a.fTlX", "dummy"));
+        assertTrue(m.matches("A.ftlx", "dummy"));
+        
+        m = new FileExtensionMatcher("");
+        assertTrue(m.matches("a.", "dummy"));
+        assertTrue(m.matches(".", "dummy"));
+        assertFalse(m.matches("a", "dummy"));
+        assertFalse(m.matches("", "dummy"));
+        assertFalse(m.matches("a.x", "dummy"));
+        
+        m = new FileExtensionMatcher("html.t");
+        assertTrue(m.matches("a.html.t", "dummy"));
+        assertFalse(m.matches("a.xhtml.t", "dummy"));
+        assertFalse(m.matches("a.html", "dummy"));
+        assertFalse(m.matches("a.t", "dummy"));
+        
+        try {
+            new FileExtensionMatcher("*.ftlx");
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+        try {
+            new FileExtensionMatcher("ftl?");
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+        try {
+            new FileExtensionMatcher(".ftlx");
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+        try {
+            new FileExtensionMatcher("dir/a.ftl");
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+    }
+    
+    @Test
+    public void testPathRegexMatcher() throws IOException {
+        PathRegexMatcher m = new PathRegexMatcher("a/[a-z]+\\.ftl");
+        assertTrue(m.matches("a/b.ftl", "dummy"));
+        assertTrue(m.matches("a/abc.ftl", "dummy"));
+        assertFalse(m.matches("b.ftl", "dummy"));
+        assertFalse(m.matches("b/b.ftl", "dummy"));
+        
+        try {
+            new PathRegexMatcher("/b.ftl");
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+    }
+    
+    @Test
+    public void testAndMatcher() throws IOException {
+        AndMatcher m = new AndMatcher(new PathGlobMatcher("a*.*"), new PathGlobMatcher("*.t"));
+        assertTrue(m.matches("ab.t", "dummy"));
+        assertFalse(m.matches("bc.t", "dummy"));
+        assertFalse(m.matches("ab.ftl", "dummy"));
+        
+        try {
+            new AndMatcher();
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+    }
+    
+    @Test
+    public void testOrMatcher() throws IOException {
+        OrMatcher m = new OrMatcher(new PathGlobMatcher("a*.*"), new PathGlobMatcher("*.t"));
+        assertTrue(m.matches("ab.t", "dummy"));
+        assertTrue(m.matches("bc.t", "dummy"));
+        assertTrue(m.matches("ab.ftl", "dummy"));
+        assertFalse(m.matches("bc.ftl", "dummy"));
+        
+        try {
+            new OrMatcher();
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+    }
+    
+    @Test
+    public void testNotMatcher() throws IOException {
+        NotMatcher m = new NotMatcher(new PathGlobMatcher("a*.*"));
+        assertFalse(m.matches("ab.t", "dummy"));
+        assertTrue(m.matches("bc.t", "dummy"));
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/AppMetaTemplateDateFormatFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/AppMetaTemplateDateFormatFactory.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/AppMetaTemplateDateFormatFactory.java
new file mode 100644
index 0000000..1d69dd8
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/AppMetaTemplateDateFormatFactory.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.userpkg;
+
+import java.util.Date;
+import java.util.Locale;
+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.valueformat.InvalidFormatParametersException;
+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.UnformattableValueException;
+import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException;
+import org.apache.freemarker.core.valueformat.UnparsableValueException;
+
+public class AppMetaTemplateDateFormatFactory extends TemplateDateFormatFactory {
+
+    public static final AppMetaTemplateDateFormatFactory INSTANCE = new AppMetaTemplateDateFormatFactory();
+    
+    private AppMetaTemplateDateFormatFactory() {
+        // Defined to decrease visibility
+    }
+    
+    @Override
+    public TemplateDateFormat get(String params, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput,
+            Environment env) throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatParametersException {
+        TemplateFormatUtil.checkHasNoParameters(params);
+        return AppMetaTemplateDateFormat.INSTANCE;
+    }
+
+    private static class AppMetaTemplateDateFormat extends TemplateDateFormat {
+
+        private static final AppMetaTemplateDateFormat INSTANCE = new AppMetaTemplateDateFormat();
+        
+        private AppMetaTemplateDateFormat() { }
+        
+        @Override
+        public String formatToPlainText(TemplateDateModel dateModel)
+                throws UnformattableValueException, TemplateModelException {
+            String result = String.valueOf(TemplateFormatUtil.getNonNullDate(dateModel).getTime());
+            if (dateModel instanceof AppMetaTemplateDateModel) {
+                result += "/" + ((AppMetaTemplateDateModel) dateModel).getAppMeta(); 
+            }
+            return result;
+        }
+
+        @Override
+        public boolean isLocaleBound() {
+            return false;
+        }
+
+        @Override
+        public boolean isTimeZoneBound() {
+            return false;
+        }
+
+        @Override
+        public Object parse(String s, int dateType) throws UnparsableValueException {
+            int slashIdx = s.indexOf('/');
+            try {
+                if (slashIdx != -1) {
+                    return new AppMetaTemplateDateModel(
+                            new Date(Long.parseLong(s.substring(0,  slashIdx))),
+                            dateType,
+                            s.substring(slashIdx +1));
+                } else {
+                    return new Date(Long.parseLong(s));
+                }
+            } catch (NumberFormatException e) {
+                throw new UnparsableValueException("Malformed long");
+            }
+        }
+
+        @Override
+        public String getDescription() {
+            return "millis since the epoch";
+        }
+        
+    }
+    
+    public static class AppMetaTemplateDateModel implements TemplateDateModel {
+        
+        private final Date date;
+        private final int dateType;
+        private final String appMeta;
+
+        public AppMetaTemplateDateModel(Date date, int dateType, String appMeta) {
+            this.date = date;
+            this.dateType = dateType;
+            this.appMeta = appMeta;
+        }
+
+        @Override
+        public Date getAsDate() throws TemplateModelException {
+            return date;
+        }
+
+        @Override
+        public int getDateType() {
+            return dateType;
+        }
+
+        public String getAppMeta() {
+            return appMeta;
+        }
+        
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/BaseNTemplateNumberFormatFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/BaseNTemplateNumberFormatFactory.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/BaseNTemplateNumberFormatFactory.java
new file mode 100644
index 0000000..e5def77
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/BaseNTemplateNumberFormatFactory.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.userpkg;
+
+import java.util.Locale;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.util._NumberUtil;
+import org.apache.freemarker.core.util._StringUtil;
+import org.apache.freemarker.core.valueformat.InvalidFormatParametersException;
+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;
+import org.apache.freemarker.core.valueformat.UnformattableValueException;
+
+/**
+ * Shows a number in base N number system. Can only format numbers that fit into an {@code int},
+ * however, optionally you can specify a fallback format. This format has one required parameter,
+ * the numerical system base. That can be optionally followed by "|" and a fallback format.
+ */
+public class BaseNTemplateNumberFormatFactory extends TemplateNumberFormatFactory {
+
+    public static final BaseNTemplateNumberFormatFactory INSTANCE
+            = new BaseNTemplateNumberFormatFactory();
+    
+    private BaseNTemplateNumberFormatFactory() {
+        // Defined to decrease visibility
+    }
+    
+    @Override
+    public TemplateNumberFormat get(String params, Locale locale, Environment env)
+            throws InvalidFormatParametersException {
+        TemplateNumberFormat fallbackFormat;
+        {
+            int barIdx = params.indexOf('|');
+            if (barIdx != -1) {
+                String fallbackFormatStr = params.substring(barIdx + 1);
+                params = params.substring(0, barIdx);
+                try {
+                    fallbackFormat = env.getTemplateNumberFormat(fallbackFormatStr, locale);
+                } catch (TemplateValueFormatException e) {
+                    throw new InvalidFormatParametersException(
+                            "Couldn't get the fallback number format (specified after the \"|\"), "
+                            + _StringUtil.jQuote(fallbackFormatStr) + ". Reason: " + e.getMessage(),
+                            e);
+                }
+            } else {
+                fallbackFormat = null;
+            }
+        }
+        
+        int base;
+        try {
+            base = Integer.parseInt(params);
+        } catch (NumberFormatException e) {
+            if (params.length() == 0) {
+                throw new InvalidFormatParametersException(
+                        "A format parameter is required to specify the numerical system base.");
+            }
+            throw new InvalidFormatParametersException(
+                    "The format paramter must be an integer, but was (shown quoted): "
+                    + _StringUtil.jQuote(params));
+        }
+        if (base < 2) {
+            throw new InvalidFormatParametersException("A base must be at least 2.");
+        }
+        return new BaseNTemplateNumberFormat(base, fallbackFormat);
+    }
+
+    private static class BaseNTemplateNumberFormat extends TemplateNumberFormat {
+
+        private final int base;
+        private final TemplateNumberFormat fallbackFormat;
+        
+        private BaseNTemplateNumberFormat(int base, TemplateNumberFormat fallbackFormat) {
+            this.base = base;
+            this.fallbackFormat = fallbackFormat;
+        }
+        
+        @Override
+        public String formatToPlainText(TemplateNumberModel numberModel)
+                throws TemplateModelException, TemplateValueFormatException {
+            Number n = TemplateFormatUtil.getNonNullNumber(numberModel);
+            try {
+                return Integer.toString(_NumberUtil.toIntExact(n), base);
+            } catch (ArithmeticException e) {
+                if (fallbackFormat == null) {
+                    throw new UnformattableValueException(
+                            n + " doesn't fit into an int, and there was no fallback format "
+                            + "specified.");
+                } else {
+                    return fallbackFormat.formatToPlainText(numberModel);
+                }
+            }
+        }
+
+        @Override
+        public boolean isLocaleBound() {
+            return false;
+        }
+
+        @Override
+        public String getDescription() {
+            return "base " + base;
+        }
+        
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/CustomHTMLOutputFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/CustomHTMLOutputFormat.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/CustomHTMLOutputFormat.java
new file mode 100644
index 0000000..f570a66
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/CustomHTMLOutputFormat.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.freemarker.core.userpkg;
+
+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.util._StringUtil;
+
+/**
+ * Represents the HTML output format.
+ * 
+ * @since 2.3.24
+ */
+public final class CustomHTMLOutputFormat extends CommonMarkupOutputFormat<CustomTemplateHTMLModel> {
+
+    public static final CustomHTMLOutputFormat INSTANCE = new CustomHTMLOutputFormat();
+    
+    private CustomHTMLOutputFormat() {
+        // 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 {
+        // This is lazy - don't do it in reality.
+        out.write(escapePlainText(textToEsc));
+    }
+
+    @Override
+    public String escapePlainText(String plainTextContent) {
+        return _StringUtil.XHTMLEnc(plainTextContent.replace('x', 'X'));
+    }
+
+    @Override
+    public boolean isLegacyBuiltInBypassed(String builtInName) {
+        return builtInName.equals("html") || builtInName.equals("xml") || builtInName.equals("xhtml");
+    }
+
+    @Override
+    protected CustomTemplateHTMLModel newTemplateMarkupOutputModel(String plainTextContent, String markupContent) {
+        return new CustomTemplateHTMLModel(plainTextContent, markupContent);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/CustomTemplateHTMLModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/CustomTemplateHTMLModel.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/CustomTemplateHTMLModel.java
new file mode 100644
index 0000000..4cc67be
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/CustomTemplateHTMLModel.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.userpkg;
+
+import org.apache.freemarker.core.outputformat.CommonTemplateMarkupOutputModel;
+
+public final class CustomTemplateHTMLModel extends CommonTemplateMarkupOutputModel<CustomTemplateHTMLModel> {
+    
+    CustomTemplateHTMLModel(String plainTextContent, String markupContent) {
+        super(plainTextContent, markupContent);
+    }
+
+    @Override
+    public CustomHTMLOutputFormat getOutputFormat() {
+        return CustomHTMLOutputFormat.INSTANCE;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/DummyOutputFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/DummyOutputFormat.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/DummyOutputFormat.java
new file mode 100644
index 0000000..f368969
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/DummyOutputFormat.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.userpkg;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.outputformat.CommonMarkupOutputFormat;
+
+public class DummyOutputFormat extends CommonMarkupOutputFormat<TemplateDummyOutputModel> {
+    
+    public static final DummyOutputFormat INSTANCE = new DummyOutputFormat();
+    
+    private DummyOutputFormat() {
+        // hide
+    }
+
+    @Override
+    public String getName() {
+        return "dummy";
+    }
+
+    @Override
+    public String getMimeType() {
+        return "text/dummy";
+    }
+
+    @Override
+    public void output(String textToEsc, Writer out) throws IOException, TemplateModelException {
+        out.write(escapePlainText(textToEsc));
+    }
+
+    @Override
+    public String escapePlainText(String plainTextContent) {
+        return plainTextContent.replaceAll("(\\.|\\\\)", "\\\\$1");
+    }
+
+    @Override
+    public boolean isLegacyBuiltInBypassed(String builtInName) {
+        return false;
+    }
+
+    @Override
+    protected TemplateDummyOutputModel newTemplateMarkupOutputModel(String plainTextContent, String markupContent) {
+        return new TemplateDummyOutputModel(plainTextContent, markupContent);
+    }
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/EpochMillisDivTemplateDateFormatFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/EpochMillisDivTemplateDateFormatFactory.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/EpochMillisDivTemplateDateFormatFactory.java
new file mode 100644
index 0000000..8799eef
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/EpochMillisDivTemplateDateFormatFactory.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.userpkg;
+
+import java.util.Date;
+import java.util.Locale;
+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._StringUtil;
+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.TemplateFormatUtil;
+import org.apache.freemarker.core.valueformat.UnformattableValueException;
+import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException;
+import org.apache.freemarker.core.valueformat.UnparsableValueException;
+
+public class EpochMillisDivTemplateDateFormatFactory extends TemplateDateFormatFactory {
+
+    public static final EpochMillisDivTemplateDateFormatFactory INSTANCE = new EpochMillisDivTemplateDateFormatFactory();
+    
+    private EpochMillisDivTemplateDateFormatFactory() {
+        // Defined to decrease visibility
+    }
+    
+    @Override
+    public TemplateDateFormat get(String params, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput,
+            Environment env) throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatParametersException {
+        int divisor;
+        try {
+            divisor = Integer.parseInt(params);
+        } catch (NumberFormatException e) {
+            if (params.length() == 0) {
+                throw new InvalidFormatParametersException(
+                        "A format parameter is required, which specifies the divisor.");
+            }
+            throw new InvalidFormatParametersException(
+                    "The format paramter must be an integer, but was (shown quoted): " + _StringUtil.jQuote(params));
+        }
+        return new EpochMillisDivTemplateDateFormat(divisor);
+    }
+
+    private static class EpochMillisDivTemplateDateFormat extends TemplateDateFormat {
+
+        private final int divisor;
+        
+        private EpochMillisDivTemplateDateFormat(int divisor) {
+            this.divisor = divisor;
+        }
+        
+        @Override
+        public String formatToPlainText(TemplateDateModel dateModel)
+                throws UnformattableValueException, TemplateModelException {
+            return String.valueOf(TemplateFormatUtil.getNonNullDate(dateModel).getTime() / divisor);
+        }
+
+        @Override
+        public boolean isLocaleBound() {
+            return false;
+        }
+
+        @Override
+        public boolean isTimeZoneBound() {
+            return false;
+        }
+
+        @Override
+        public Date parse(String s, int dateType) throws UnparsableValueException {
+            try {
+                return new Date(Long.parseLong(s));
+            } catch (NumberFormatException e) {
+                throw new UnparsableValueException("Malformed long");
+            }
+        }
+
+        @Override
+        public String getDescription() {
+            return "millis since the epoch";
+        }
+        
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/EpochMillisTemplateDateFormatFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/EpochMillisTemplateDateFormatFactory.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/EpochMillisTemplateDateFormatFactory.java
new file mode 100644
index 0000000..9f27095
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/EpochMillisTemplateDateFormatFactory.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.userpkg;
+
+import java.util.Date;
+import java.util.Locale;
+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.valueformat.InvalidFormatParametersException;
+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.UnformattableValueException;
+import org.apache.freemarker.core.valueformat.UnparsableValueException;
+
+public class EpochMillisTemplateDateFormatFactory extends TemplateDateFormatFactory {
+
+    public static final EpochMillisTemplateDateFormatFactory INSTANCE
+            = new EpochMillisTemplateDateFormatFactory();
+    
+    private EpochMillisTemplateDateFormatFactory() {
+        // Defined to decrease visibility
+    }
+    
+    @Override
+    public TemplateDateFormat get(String params, int dateType,
+            Locale locale, TimeZone timeZone, boolean zonelessInput,
+            Environment env)
+            throws InvalidFormatParametersException {
+        TemplateFormatUtil.checkHasNoParameters(params);
+        return EpochMillisTemplateDateFormat.INSTANCE;
+    }
+
+    private static class EpochMillisTemplateDateFormat extends TemplateDateFormat {
+
+        private static final EpochMillisTemplateDateFormat INSTANCE
+                = new EpochMillisTemplateDateFormat();
+        
+        private EpochMillisTemplateDateFormat() { }
+        
+        @Override
+        public String formatToPlainText(TemplateDateModel dateModel)
+                throws UnformattableValueException, TemplateModelException {
+            return String.valueOf(TemplateFormatUtil.getNonNullDate(dateModel).getTime());
+        }
+
+        @Override
+        public boolean isLocaleBound() {
+            return false;
+        }
+
+        @Override
+        public boolean isTimeZoneBound() {
+            return false;
+        }
+
+        @Override
+        public Date parse(String s, int dateType) throws UnparsableValueException {
+            try {
+                return new Date(Long.parseLong(s));
+            } catch (NumberFormatException e) {
+                throw new UnparsableValueException("Malformed long");
+            }
+        }
+
+        @Override
+        public String getDescription() {
+            return "millis since the epoch";
+        }
+        
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/HTMLISOTemplateDateFormatFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/HTMLISOTemplateDateFormatFactory.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/HTMLISOTemplateDateFormatFactory.java
new file mode 100644
index 0000000..357034c
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/HTMLISOTemplateDateFormatFactory.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.userpkg;
+
+import java.util.Date;
+import java.util.Locale;
+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.outputformat.impl.HTMLOutputFormat;
+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.valueformat.InvalidFormatParametersException;
+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;
+import org.apache.freemarker.core.valueformat.UnformattableValueException;
+import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException;
+import org.apache.freemarker.core.valueformat.UnparsableValueException;
+
+public class HTMLISOTemplateDateFormatFactory extends TemplateDateFormatFactory {
+
+    public static final HTMLISOTemplateDateFormatFactory INSTANCE = new HTMLISOTemplateDateFormatFactory();
+    
+    private HTMLISOTemplateDateFormatFactory() {
+        // Defined to decrease visibility
+    }
+    
+    @Override
+    public TemplateDateFormat get(String params, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput,
+            Environment env) throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatParametersException {
+        TemplateFormatUtil.checkHasNoParameters(params);
+        return HTMLISOTemplateDateFormat.INSTANCE;
+    }
+
+    private static class HTMLISOTemplateDateFormat extends TemplateDateFormat {
+
+        private static final HTMLISOTemplateDateFormat INSTANCE = new HTMLISOTemplateDateFormat();
+
+        private _DateUtil.TrivialDateToISO8601CalendarFactory calendarFactory;
+
+        private CalendarFieldsToDateConverter calToDateConverter;
+        
+        private HTMLISOTemplateDateFormat() { }
+        
+        @Override
+        public String formatToPlainText(TemplateDateModel dateModel)
+                throws UnformattableValueException, TemplateModelException {
+            if (calendarFactory == null) {
+                calendarFactory = new _DateUtil.TrivialDateToISO8601CalendarFactory();
+            }
+            return _DateUtil.dateToISO8601String(
+                    TemplateFormatUtil.getNonNullDate(dateModel),
+                    true, true, true, _DateUtil.ACCURACY_SECONDS, _DateUtil.UTC,
+                    calendarFactory);
+        }
+
+        @Override
+        public boolean isLocaleBound() {
+            return false;
+        }
+
+        @Override
+        public boolean isTimeZoneBound() {
+            return false;
+        }
+
+        @Override
+        public Date parse(String s, int dateType) throws UnparsableValueException {
+            try {
+                if (calToDateConverter == null) {
+                    calToDateConverter = new _DateUtil.TrivialCalendarFieldsToDateConverter();
+                }
+                return _DateUtil.parseISO8601DateTime(s, _DateUtil.UTC, calToDateConverter);
+            } catch (DateParseException e) {
+                throw new UnparsableValueException("Malformed ISO date-time", e);
+            }
+        }
+
+        @Override
+        public Object format(TemplateDateModel dateModel) throws TemplateValueFormatException, TemplateModelException {
+            return HTMLOutputFormat.INSTANCE.fromMarkup(
+                    formatToPlainText(dateModel).replace("T", "<span class='T'>T</span>"));
+        }
+
+        @Override
+        public String getDescription() {
+            return "ISO UTC HTML";
+        }
+        
+    }
+
+}
+ 
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/HexTemplateNumberFormatFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/HexTemplateNumberFormatFactory.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/HexTemplateNumberFormatFactory.java
new file mode 100644
index 0000000..6d72c20
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/HexTemplateNumberFormatFactory.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.userpkg;
+
+import java.util.Locale;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.util._NumberUtil;
+import org.apache.freemarker.core.valueformat.InvalidFormatParametersException;
+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.UnformattableValueException;
+
+public class HexTemplateNumberFormatFactory extends TemplateNumberFormatFactory {
+
+    public static final HexTemplateNumberFormatFactory INSTANCE = new HexTemplateNumberFormatFactory();
+    
+    private HexTemplateNumberFormatFactory() {
+        // Defined to decrease visibility
+    }
+    
+    @Override
+    public TemplateNumberFormat get(String params, Locale locale, Environment env)
+            throws InvalidFormatParametersException {
+        TemplateFormatUtil.checkHasNoParameters(params);
+        return HexTemplateNumberFormat.INSTANCE;
+    }
+
+    private static class HexTemplateNumberFormat extends TemplateNumberFormat {
+
+        private static final HexTemplateNumberFormat INSTANCE = new HexTemplateNumberFormat();
+        
+        private HexTemplateNumberFormat() { }
+        
+        @Override
+        public String formatToPlainText(TemplateNumberModel numberModel)
+                throws UnformattableValueException, TemplateModelException {
+            Number n = TemplateFormatUtil.getNonNullNumber(numberModel);
+            try {
+                return Integer.toHexString(_NumberUtil.toIntExact(n));
+            } catch (ArithmeticException e) {
+                throw new UnformattableValueException(n + " doesn't fit into an int");
+            }
+        }
+
+        @Override
+        public boolean isLocaleBound() {
+            return false;
+        }
+
+        @Override
+        public String getDescription() {
+            return "hexadecimal int";
+        }
+        
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/LocAndTZSensitiveTemplateDateFormatFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/LocAndTZSensitiveTemplateDateFormatFactory.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/LocAndTZSensitiveTemplateDateFormatFactory.java
new file mode 100644
index 0000000..42b0401
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/LocAndTZSensitiveTemplateDateFormatFactory.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.userpkg;
+
+import java.util.Date;
+import java.util.Locale;
+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.valueformat.InvalidFormatParametersException;
+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.UnformattableValueException;
+import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException;
+import org.apache.freemarker.core.valueformat.UnparsableValueException;
+
+public class LocAndTZSensitiveTemplateDateFormatFactory extends TemplateDateFormatFactory {
+
+    public static final LocAndTZSensitiveTemplateDateFormatFactory INSTANCE = new LocAndTZSensitiveTemplateDateFormatFactory();
+    
+    private LocAndTZSensitiveTemplateDateFormatFactory() {
+        // Defined to decrease visibility
+    }
+    
+    @Override
+    public TemplateDateFormat get(String params, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput,
+            Environment env) throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatParametersException {
+        TemplateFormatUtil.checkHasNoParameters(params);
+        return new LocAndTZSensitiveTemplateDateFormat(locale, timeZone);
+    }
+
+    private static class LocAndTZSensitiveTemplateDateFormat extends TemplateDateFormat {
+
+        private final Locale locale;
+        private final TimeZone timeZone;
+        
+        public LocAndTZSensitiveTemplateDateFormat(Locale locale, TimeZone timeZone) {
+            this.locale = locale;
+            this.timeZone = timeZone;
+        }
+
+        @Override
+        public String formatToPlainText(TemplateDateModel dateModel)
+                throws UnformattableValueException, TemplateModelException {
+            return String.valueOf(TemplateFormatUtil.getNonNullDate(dateModel).getTime() + "@" + locale + ":" + timeZone.getID());
+        }
+
+        @Override
+        public boolean isLocaleBound() {
+            return true;
+        }
+
+        @Override
+        public boolean isTimeZoneBound() {
+            return true;
+        }
+
+        @Override
+        public Date parse(String s, int dateType) throws UnparsableValueException {
+            try {
+                int atIdx = s.indexOf("@");
+                if (atIdx == -1) {
+                    throw new UnparsableValueException("Missing @");
+                }
+                return new Date(Long.parseLong(s.substring(0, atIdx)));
+            } catch (NumberFormatException e) {
+                throw new UnparsableValueException("Malformed long");
+            }
+        }
+
+        @Override
+        public String getDescription() {
+            return "millis since the epoch";
+        }
+        
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/LocaleSensitiveTemplateNumberFormatFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/LocaleSensitiveTemplateNumberFormatFactory.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/LocaleSensitiveTemplateNumberFormatFactory.java
new file mode 100644
index 0000000..dc3dae0
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/LocaleSensitiveTemplateNumberFormatFactory.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.userpkg;
+
+import java.util.Locale;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.valueformat.InvalidFormatParametersException;
+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.UnformattableValueException;
+
+public class LocaleSensitiveTemplateNumberFormatFactory extends TemplateNumberFormatFactory {
+
+    public static final LocaleSensitiveTemplateNumberFormatFactory INSTANCE = new LocaleSensitiveTemplateNumberFormatFactory();
+    
+    private LocaleSensitiveTemplateNumberFormatFactory() {
+        // Defined to decrease visibility
+    }
+    
+    @Override
+    public TemplateNumberFormat get(String params, Locale locale, Environment env)
+            throws InvalidFormatParametersException {
+        TemplateFormatUtil.checkHasNoParameters(params);
+        return new LocaleSensitiveTemplateNumberFormat(locale);
+    }
+
+    private static class LocaleSensitiveTemplateNumberFormat extends TemplateNumberFormat {
+    
+        private final Locale locale;
+        
+        private LocaleSensitiveTemplateNumberFormat(Locale locale) {
+            this.locale = locale;
+        }
+        
+        @Override
+        public String formatToPlainText(TemplateNumberModel numberModel)
+                throws UnformattableValueException, TemplateModelException {
+            Number n = numberModel.getAsNumber();
+            try {
+                return n + "_" + locale;
+            } catch (ArithmeticException e) {
+                throw new UnformattableValueException(n + " doesn't fit into an int");
+            }
+        }
+    
+        @Override
+        public boolean isLocaleBound() {
+            return true;
+        }
+    
+        @Override
+        public String getDescription() {
+            return "test locale sensitive";
+        }
+        
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PackageVisibleAll.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PackageVisibleAll.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PackageVisibleAll.java
new file mode 100644
index 0000000..f3ebc72
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PackageVisibleAll.java
@@ -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.
+ */
+
+package org.apache.freemarker.core.userpkg;
+
+class PackageVisibleAll {
+    
+    PackageVisibleAll() {}
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PackageVisibleAllWithBuilder.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PackageVisibleAllWithBuilder.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PackageVisibleAllWithBuilder.java
new file mode 100644
index 0000000..53a89d4
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PackageVisibleAllWithBuilder.java
@@ -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.
+ */
+
+package org.apache.freemarker.core.userpkg;
+
+class PackageVisibleAllWithBuilder {
+
+    PackageVisibleAllWithBuilder() {}
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PackageVisibleAllWithBuilderBuilder.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PackageVisibleAllWithBuilderBuilder.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PackageVisibleAllWithBuilderBuilder.java
new file mode 100644
index 0000000..510ecc2
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PackageVisibleAllWithBuilderBuilder.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.userpkg;
+
+public class PackageVisibleAllWithBuilderBuilder {
+    
+    public PackageVisibleAllWithBuilder build() {
+        return new PackageVisibleAllWithBuilder();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PackageVisibleWithPublicConstructor.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PackageVisibleWithPublicConstructor.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PackageVisibleWithPublicConstructor.java
new file mode 100644
index 0000000..836e80d
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PackageVisibleWithPublicConstructor.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.userpkg;
+
+class PackageVisibleWithPublicConstructor {
+    
+    public PackageVisibleWithPublicConstructor() {
+    }
+
+}


Mime
View raw message