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
+ * <@myDirective /> or <@myDirective></@myDirective>), 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 <#visit...> and <#recurse...>
+ * 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>
+ * <@capture_output var="captured">
+ * ...
+ * </@capture_output>
+ * </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 <#assign>), the second creates a macro-local variable (as <#local>),
+ * and the last creates a global variable (as <#global>).
+ * </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><assign x>...</assign></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() {
+ * @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: <result of getMessage>
+ */
+ 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><#ftl sourceEncoding="..."></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><#ftl attributes={...}></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 <, >, & 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,
+ * < > " and & 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:
+ * <transform htmlEscape>
+ * <p>This paragraph has all HTML special characters escaped.</p>
+ * </transform>
+ *
+ * ...
+ * </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 = "<".toCharArray();
+ private static final char[] GT = ">".toCharArray();
+ private static final char[] AMP = "&".toCharArray();
+ private static final char[] 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>
+ * <transform normalizeNewlines>
+ * <html>
+ * <head>
+ * ...
+ * <p>This template has all newlines normalized to the current platform's
+ * default.</p>
+ * ...
+ * </body>
+ * </html>
+ * </transform>
+ * </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>
+ * <transform standardCompress>
+ * <p>This paragraph will have
+ * extraneous
+ *
+ * whitespace removed.</p>
+ * </transform>
+ * </pre>
+ *
+ * <p>Output:</p>
+ * <pre>
+ * <p>This paragraph will have
+ * extraneous
+ * whitespace removed.</p>
+ * </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><</tt> <tt>></tt> <tt>"</tt> <tt>'</tt> and <tt>&</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 = "<".toCharArray();
+ private static final char[] GT = ">".toCharArray();
+ private static final char[] AMP = "&".toCharArray();
+ private static final char[] QUOT = """.toCharArray();
+ private static final char[] 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 "<" 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 "<" 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 ">" 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 ">=" 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<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}", "<y>");
+ 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>&");
+ 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 testConcatenation2() 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>");
+ }
+
+ @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="
" />
+ <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 '>' '<' '&', "'" 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 '>' '<' '&', "'" and '"' with entity reference
+ * suitable for XHTML decoding in common user agents (including legacy
+ * user agents, which do not decode "&apos;" to "'", so "&#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>'<'</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 = "</${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><</tt>
+ * <td><tt>\/</tt>
+ * <tr>
+ * <td><tt>></tt> if the method can't know that it won't be directly after <tt>]]</tt> or <tt>--</tt>
+ * <td>JavaScript: <tt>\></tt>; JSON: <tt>\</tt><tt>u003E</tt>
+ * <tr>
+ * <td><tt><</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><transform></tt> block will process the generated template
+ * just as if it had been <tt><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><#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 <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 <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="['"]DejaVu Sans['"]" replace="Arial"
- />
- <replaceregexp
- file="${file}" flags="gsi" encoding="utf-8"
- match="['"]DejaVu Sans Mono['"]" replace="'Courier New'"
- />
- <replaceregexp
- file="${file}" flags="gsi" encoding="utf-8"
- match="['"]DejaVu Serif['"]" replace="Arial"
- />
- <replaceregexp
- file="${file}" flags="gsi" encoding="utf-8"
- match="(?<=[\s,:])serif\b" replace="sans-serif"
- />
- <replaceregexp
- file="${file}" flags="gsi" encoding="utf-8"
- match="(?<=[\s,:])Georgia,\s*" replace=""
- />
- <replaceregexp
- file="${file}" flags="gsi" encoding="utf-8"
- match="['"]Times New Roman['"],\s*" replace=""
- />
- <replaceregexp
- file="${file}" flags="gsi" encoding="utf-8"
- match="(?<=[\s,:])Times,\s*" replace=""
- />
- <replaceregexp
- file="${file}" flags="gsi" encoding="utf-8"
- match="(?<=[\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
<!--
- -->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><#assign s = "foo${bar}"></code>, {@code bar} will always get into {@code s}
+ * without escaping, but with <code><#assign s>foo${bar}<#assign></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><#directiveName ...></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<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 <#list>-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() { }
+ *
+ * @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 == ">") {
+ operation = _EvalUtil.CMP_OP_GREATER_THAN;
+ } else if (opString == "gte" || opString == "\\gte" || opString == ">=" || opString == ">=") {
+ operation = _EvalUtil.CMP_OP_GREATER_THAN_EQUALS;
+ } else if (opString == "lt" || opString == "\\lt" || opString == "<" || opString == "<") {
+ operation = _EvalUtil.CMP_OP_LESS_THAN;
+ } else if (opString == "lte" || opString == "\\lte" || opString == "<=" || opString == "<=") {
+ 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 "<"} 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" | "<" | "<">
+ |
+ <LESS_THAN_EQUALS : "lte" | "\\lte" | "<=" | "<=">
+ |
+ <ESCAPED_GT: "gt" | "\\gt" | ">">
+ |
+ <ESCAPED_GTE : "gte" | "\\gte" | ">=">
+ |
+ <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>