freemarker-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ddek...@apache.org
Subject incubator-freemarker git commit: Tested/fixed the setters of configuration settings that has type Map and List. Other minor adjustments.
Date Thu, 08 Jun 2017 21:32:57 GMT
Repository: incubator-freemarker
Updated Branches:
  refs/heads/3 c42014cc0 -> db64ebc97


Tested/fixed the setters of configuration settings that has type Map and List. Other minor adjustments.


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

Branch: refs/heads/3
Commit: db64ebc976a82ec1b6594fa3045e902084a229cb
Parents: c42014c
Author: ddekany <ddekany@apache.org>
Authored: Thu Jun 8 23:32:01 2017 +0200
Committer: ddekany <ddekany@apache.org>
Committed: Thu Jun 8 23:32:51 2017 +0200

----------------------------------------------------------------------
 .../freemarker/core/ConfigurableTest.java       | 176 --------------
 .../freemarker/core/ConfigurationTest.java      |  36 ++-
 .../MutableProcessingConfigurationTest.java     | 242 +++++++++++++++++++
 .../core/RestrictedObjectWrapperTest.java       |  72 ------
 .../core/RestrictedObjetWrapperTest.java        | 112 ---------
 .../core/TemplateConfigurationTest.java         |  12 +
 .../model/impl/DefaultObjectWrapperTest.java    |  12 +
 .../model/impl/RestrictedObjectWrapperTest.java | 155 ++++++++++++
 .../apache/freemarker/core/Configuration.java   |  34 +--
 .../core/MutableProcessingConfiguration.java    |  18 +-
 .../freemarker/core/TemplateConfiguration.java  |  22 +-
 .../core/util/ProductWrappingBuilder.java       |  43 ----
 .../freemarker/core/util/_CollectionUtil.java   |  12 +
 13 files changed, 501 insertions(+), 445 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/db64ebc9/freemarker-core-test/src/test/java/org/apache/freemarker/core/ConfigurableTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/ConfigurableTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/ConfigurableTest.java
deleted file mode 100644
index 5301f14..0000000
--- a/freemarker-core-test/src/test/java/org/apache/freemarker/core/ConfigurableTest.java
+++ /dev/null
@@ -1,176 +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.
- */
-
-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(NamingConvention.CAMEL_CASE,
-                        _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(NamingConvention.LEGACY,
-                            _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/db64ebc9/freemarker-core-test/src/test/java/org/apache/freemarker/core/ConfigurationTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/ConfigurationTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/ConfigurationTest.java
index 064e34a..fa1c4d2 100644
--- a/freemarker-core-test/src/test/java/org/apache/freemarker/core/ConfigurationTest.java
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/ConfigurationTest.java
@@ -35,6 +35,7 @@ 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;
 
@@ -73,6 +74,7 @@ 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._CollectionUtil;
 import org.apache.freemarker.core.util._DateUtil;
 import org.apache.freemarker.core.util._NullArgumentException;
 import org.apache.freemarker.core.util._NullWriter;
@@ -1279,7 +1281,7 @@ public class ConfigurationTest extends TestCase {
     @SuppressFBWarnings("DLS_DEAD_LOCAL_STORE")
     public void testGetSettingNamesNameConventionsContainTheSame() throws Exception {
         Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0);
-        ConfigurableTest.testGetSettingNamesNameConventionsContainTheSame(
+        MutableProcessingConfigurationTest.testGetSettingNamesNameConventionsContainTheSame(
                 new ArrayList<>(cfgB.getSettingNames(false)),
                 new ArrayList<>(cfgB.getSettingNames(true)));
     }
@@ -1314,7 +1316,7 @@ public class ConfigurationTest extends TestCase {
     
     @Test
     public void testKeyStaticFieldsHasAllVariationsAndCorrectFormat() throws IllegalArgumentException, IllegalAccessException {
-        ConfigurableTest.testKeyStaticFieldsHasAllVariationsAndCorrectFormat(Configuration.ExtendableBuilder.class);
+        MutableProcessingConfigurationTest.testKeyStaticFieldsHasAllVariationsAndCorrectFormat(Configuration.ExtendableBuilder.class);
     }
 
     @Test
@@ -1423,7 +1425,35 @@ public class ConfigurationTest extends TestCase {
             assertThat(e.getMessage(), allOf(containsString("removed"), containsString("3.0.0")));
         }
     }
-    
+
+    @Test
+    public void testCanBeBuiltOnlyOnce() {
+        Configuration.Builder builder = new Configuration.Builder(Configuration.VERSION_3_0_0);
+        builder.build();
+        try {
+            builder.build();
+            fail();
+        } catch (IllegalStateException e) {
+            // Expected
+        }
+    }
+
+    @Test
+    public void testCollectionSettingMutability() throws IOException {
+        Configuration.Builder cb = new Configuration.Builder(Configuration.VERSION_3_0_0);
+
+        assertTrue(_CollectionUtil.isMapKnownToBeUnmodifiable(cb.getSharedVariables()));
+        Map<String, Object> mutableValue = new HashMap<>();
+        mutableValue.put("x", "v1");
+        cb.setSharedVariables(mutableValue);
+        Map<String, Object> immutableValue = cb.getSharedVariables();
+        assertNotSame(mutableValue, immutableValue); // Must be a copy
+        assertTrue(_CollectionUtil.isMapKnownToBeUnmodifiable(immutableValue));
+        assertEquals(mutableValue, immutableValue);
+        mutableValue.put("y", "v2");
+        assertNotEquals(mutableValue, immutableValue); // No aliasing
+    }
+
     @SuppressWarnings("boxing")
     private void assertStartsWith(List<String> list, List<String> headList) {
         int index = 0;

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/db64ebc9/freemarker-core-test/src/test/java/org/apache/freemarker/core/MutableProcessingConfigurationTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/MutableProcessingConfigurationTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/MutableProcessingConfigurationTest.java
new file mode 100644
index 0000000..f780e9d
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/MutableProcessingConfigurationTest.java
@@ -0,0 +1,242 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, 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.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.freemarker.core.userpkg.BaseNTemplateNumberFormatFactory;
+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._CollectionUtil;
+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;
+
+public class MutableProcessingConfigurationTest {
+
+    @Test
+    public void testGetSettingNamesAreSorted() throws Exception {
+        MutableProcessingConfiguration mpc = createMutableProcessingConfiguration();
+        for (boolean camelCase : new boolean[] { false, true }) {
+            Collection<String> names = mpc.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 mpc = createMutableProcessingConfiguration();
+        Collection<String> names = mpc.getSettingNames(false);
+        for (String name : names) {
+                assertTrue("No field was found for " + name, keyFieldExists(name));
+        }
+    }
+    
+    @Test
+    public void testGetSettingNamesCoversAllStaticKeyFields() throws Exception {
+        MutableProcessingConfiguration mpc = createMutableProcessingConfiguration();
+        Collection<String> names = mpc.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 {
+        MutableProcessingConfigurationTest.testKeyStaticFieldsHasAllVariationsAndCorrectFormat(MutableProcessingConfiguration.class);
+    }
+    
+    @Test
+    public void testGetSettingNamesNameConventionsContainTheSame() throws Exception {
+        MutableProcessingConfiguration mpc = createMutableProcessingConfiguration();
+        MutableProcessingConfigurationTest.testGetSettingNamesNameConventionsContainTheSame(
+                new ArrayList<>(mpc.getSettingNames(false)),
+                new ArrayList<>(mpc.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(NamingConvention.CAMEL_CASE,
+                        _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(NamingConvention.LEGACY,
+                            _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 createMutableProcessingConfiguration() 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;
+    }
+
+    @Test
+    public void testCollectionSettingMutability() throws IOException {
+        MutableProcessingConfiguration<?> mpc = new Configuration.Builder(Configuration.VERSION_3_0_0);
+
+        {
+            assertTrue(_CollectionUtil.isListKnownToBeUnmodifiable(mpc.getAutoIncludes()));
+            List<String> mutableValue = new ArrayList<>();
+            mutableValue.add("x");
+            mpc.setAutoIncludes(mutableValue);
+            List<String> immutableValue = mpc.getAutoIncludes();
+            assertNotSame(mutableValue, immutableValue); // Must be a copy
+            assertTrue(_CollectionUtil.isListKnownToBeUnmodifiable(immutableValue));
+            assertEquals(mutableValue, immutableValue);
+            mutableValue.add("y");
+            assertNotEquals(mutableValue, immutableValue); // No aliasing
+        }
+
+        {
+            assertTrue(_CollectionUtil.isMapKnownToBeUnmodifiable(mpc.getAutoImports()));
+            Map<String, String> mutableValue = new HashMap<>();
+            mutableValue.put("x", "x.ftl");
+            mpc.setAutoImports(mutableValue);
+            Map<String, String> immutableValue = mpc.getAutoImports();
+            assertNotSame(mutableValue, immutableValue); // Must be a copy
+            assertTrue(_CollectionUtil.isMapKnownToBeUnmodifiable(immutableValue));
+            assertEquals(mutableValue, immutableValue);
+            mutableValue.put("y", "y.ftl");
+            assertNotEquals(mutableValue, immutableValue); // No aliasing
+        }
+
+        {
+            assertTrue(_CollectionUtil.isMapKnownToBeUnmodifiable(mpc.getCustomDateFormats()));
+            Map<String, TemplateDateFormatFactory> mutableValue = new HashMap<>();
+            mutableValue.put("x", EpochMillisTemplateDateFormatFactory.INSTANCE);
+            mpc.setCustomDateFormats(mutableValue);
+            Map<String, TemplateDateFormatFactory> immutableValue = mpc.getCustomDateFormats();
+            assertNotSame(mutableValue, immutableValue); // Must be a copy
+            assertTrue(_CollectionUtil.isMapKnownToBeUnmodifiable(immutableValue));
+            assertEquals(mutableValue, immutableValue);
+            mutableValue.put("y", EpochMillisDivTemplateDateFormatFactory.INSTANCE);
+            assertNotEquals(mutableValue, immutableValue); // No aliasing
+        }
+
+        {
+            assertTrue(_CollectionUtil.isMapKnownToBeUnmodifiable(mpc.getCustomNumberFormats()));
+            Map<String, TemplateNumberFormatFactory> mutableValue = new HashMap<>();
+            mutableValue.put("x", BaseNTemplateNumberFormatFactory.INSTANCE);
+            mpc.setCustomNumberFormats(mutableValue);
+            Map<String, TemplateNumberFormatFactory> immutableValue = mpc.getCustomNumberFormats();
+            assertNotSame(mutableValue, immutableValue); // Must be a copy
+            assertTrue(_CollectionUtil.isMapKnownToBeUnmodifiable(immutableValue));
+            assertEquals(mutableValue, immutableValue);
+            mutableValue.put("y", HexTemplateNumberFormatFactory.INSTANCE);
+            assertNotEquals(mutableValue, immutableValue); // No aliasing
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/db64ebc9/freemarker-core-test/src/test/java/org/apache/freemarker/core/RestrictedObjectWrapperTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/RestrictedObjectWrapperTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/RestrictedObjectWrapperTest.java
deleted file mode 100644
index 702a254..0000000
--- a/freemarker-core-test/src/test/java/org/apache/freemarker/core/RestrictedObjectWrapperTest.java
+++ /dev/null
@@ -1,72 +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.
- */
-
-package org.apache.freemarker.core;
-
-import static org.apache.freemarker.test.hamcerst.Matchers.*;
-import static org.junit.Assert.*;
-
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashSet;
-
-import javax.annotation.PostConstruct;
-
-import org.apache.freemarker.core.model.TemplateBooleanModel;
-import org.apache.freemarker.core.model.TemplateModelException;
-import org.apache.freemarker.core.model.impl.DefaultArrayAdapter;
-import org.apache.freemarker.core.model.impl.DefaultListAdapter;
-import org.apache.freemarker.core.model.impl.DefaultMapAdapter;
-import org.apache.freemarker.core.model.impl.DefaultNonListCollectionAdapter;
-import org.apache.freemarker.core.model.impl.DefaultObjectWrapperTest.TestBean;
-import org.apache.freemarker.core.model.impl.RestrictedObjectWrapper;
-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.junit.Test;
-
-public class RestrictedObjectWrapperTest {
-
-    @Test
-    public void testBasics() throws TemplateModelException {
-        PostConstruct.class.toString();
-        RestrictedObjectWrapper ow = new RestrictedObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
-        testCustomizationCommonPart(ow);
-        assertTrue(ow.wrap(Collections.emptyMap()) instanceof DefaultMapAdapter);
-        assertTrue(ow.wrap(Collections.emptyList()) instanceof DefaultListAdapter);
-        assertTrue(ow.wrap(new boolean[] { }) instanceof DefaultArrayAdapter);
-        assertTrue(ow.wrap(new HashSet()) instanceof DefaultNonListCollectionAdapter);
-    }
-
-    @SuppressWarnings("boxing")
-    private void testCustomizationCommonPart(RestrictedObjectWrapper ow) throws TemplateModelException {
-        assertTrue(ow.wrap("x") instanceof SimpleScalar);
-        assertTrue(ow.wrap(1.5) instanceof SimpleNumber);
-        assertTrue(ow.wrap(new Date()) instanceof SimpleDate);
-        assertEquals(TemplateBooleanModel.TRUE, ow.wrap(true));
-        
-        try {
-            ow.wrap(new TestBean());
-            fail();
-        } catch (TemplateModelException e) {
-            assertThat(e.getMessage(), containsStringIgnoringCase("type"));
-        }
-    }
-    
-}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/db64ebc9/freemarker-core-test/src/test/java/org/apache/freemarker/core/RestrictedObjetWrapperTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/RestrictedObjetWrapperTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/RestrictedObjetWrapperTest.java
deleted file mode 100644
index 43ff3bf..0000000
--- a/freemarker-core-test/src/test/java/org/apache/freemarker/core/RestrictedObjetWrapperTest.java
+++ /dev/null
@@ -1,112 +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.
- */
-
-package org.apache.freemarker.core;
-
-import static org.hamcrest.Matchers.*;
-import static org.junit.Assert.*;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.StringReader;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.parsers.ParserConfigurationException;
-
-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.TemplateHashModelEx2;
-import org.apache.freemarker.core.model.TemplateModelException;
-import org.apache.freemarker.core.model.TemplateModelWithAPISupport;
-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.RestrictedObjectWrapper;
-import org.junit.Test;
-import org.w3c.dom.Document;
-import org.xml.sax.InputSource;
-import org.xml.sax.SAXException;
-
-public class RestrictedObjetWrapperTest {
-    
-    @Test
-    public void testDoesNotAllowAPIBuiltin() throws TemplateModelException {
-        RestrictedObjectWrapper sow = new RestrictedObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
-        
-        TemplateModelWithAPISupport map = (TemplateModelWithAPISupport) sow.wrap(new HashMap());
-        try {
-            map.getAPI();
-            fail();
-        } catch (TemplateException e) {
-            assertThat(e.getMessage(), containsString("?api"));
-        }
-    }
-
-    @SuppressWarnings("boxing")
-    @Test
-    public void testCanWrapBasicTypes() throws TemplateModelException {
-        RestrictedObjectWrapper sow = new RestrictedObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
-        assertTrue(sow.wrap("s") instanceof TemplateScalarModel);
-        assertTrue(sow.wrap(1) instanceof TemplateNumberModel);
-        assertTrue(sow.wrap(true) instanceof TemplateBooleanModel);
-        assertTrue(sow.wrap(new Date()) instanceof TemplateDateModel);
-        assertTrue(sow.wrap(new ArrayList()) instanceof TemplateSequenceModel);
-        assertTrue(sow.wrap(new String[0]) instanceof TemplateSequenceModel);
-        assertTrue(sow.wrap(new ArrayList().iterator()) instanceof TemplateCollectionModel);
-        assertTrue(sow.wrap(new HashSet()) instanceof TemplateCollectionModelEx);
-        assertTrue(sow.wrap(new HashMap()) instanceof TemplateHashModelEx2);
-        assertNull(sow.wrap(null));
-    }
-    
-    @Test
-    public void testWontWrapDOM() throws SAXException, IOException, ParserConfigurationException,
-            TemplateModelException {
-        DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
-        InputSource is = new InputSource();
-        is.setCharacterStream(new StringReader("<doc><sub a='1' /></doc>"));
-        Document doc = db.parse(is);
-        
-        RestrictedObjectWrapper sow = new RestrictedObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
-        try {
-            sow.wrap(doc);
-            fail();
-        } catch (TemplateModelException e) {
-            assertThat(e.getMessage(), containsString("won't wrap"));
-        }
-    }
-    
-    @Test
-    public void testWontWrapGenericObjects() {
-        RestrictedObjectWrapper sow = new RestrictedObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
-        try {
-            sow.wrap(new File("/x"));
-            fail();
-        } catch (TemplateModelException e) {
-            assertThat(e.getMessage(), containsString("won't wrap"));
-        }
-    }
-    
-}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/db64ebc9/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateConfigurationTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateConfigurationTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateConfigurationTest.java
index a3fe6dc..5cf5811 100644
--- a/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateConfigurationTest.java
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateConfigurationTest.java
@@ -898,5 +898,17 @@ public class TemplateConfigurationTest {
             }
         }
     }
+
+    @Test
+    public void testCanBeBuiltOnlyOnce() {
+        TemplateConfiguration.Builder builder = new TemplateConfiguration.Builder();
+        builder.build();
+        try {
+            builder.build();
+            fail();
+        } catch (IllegalStateException e) {
+            // Expected
+        }
+    }
     
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/db64ebc9/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperTest.java
index 6581b10..0a1e16c 100644
--- a/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperTest.java
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperTest.java
@@ -707,6 +707,18 @@ public class DefaultObjectWrapperTest {
         }
     }
 
+    @Test
+    public void testCanBeBuiltOnlyOnce() {
+        DefaultObjectWrapper.Builder tcb = new DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0);
+        tcb.build();
+        try {
+            tcb.build();
+            fail();
+        } catch (IllegalStateException e) {
+            // Expected
+        }
+    }
+
     private TemplateHashModel wrapWithExposureLevel(Object bean, int exposureLevel) throws TemplateModelException {
         return (TemplateHashModel) new DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0)
                 .exposureLevel(exposureLevel).build()

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/db64ebc9/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/RestrictedObjectWrapperTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/RestrictedObjectWrapperTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/RestrictedObjectWrapperTest.java
new file mode 100644
index 0000000..1033340
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/RestrictedObjectWrapperTest.java
@@ -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.
+ */
+
+package org.apache.freemarker.core.model.impl;
+
+import static org.apache.freemarker.test.hamcerst.Matchers.*;
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+
+import javax.annotation.PostConstruct;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.TemplateException;
+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.TemplateHashModelEx2;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelWithAPISupport;
+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.DefaultObjectWrapperTest.TestBean;
+import org.junit.Test;
+import org.w3c.dom.Document;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+public class RestrictedObjectWrapperTest {
+
+    @Test
+    public void testBasics() throws TemplateModelException {
+        PostConstruct.class.toString();
+        RestrictedObjectWrapper ow = new RestrictedObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
+        testCustomizationCommonPart(ow);
+        assertTrue(ow.wrap(Collections.emptyMap()) instanceof DefaultMapAdapter);
+        assertTrue(ow.wrap(Collections.emptyList()) instanceof DefaultListAdapter);
+        assertTrue(ow.wrap(new boolean[] { }) instanceof DefaultArrayAdapter);
+        assertTrue(ow.wrap(new HashSet()) instanceof DefaultNonListCollectionAdapter);
+    }
+
+    @SuppressWarnings("boxing")
+    private void testCustomizationCommonPart(RestrictedObjectWrapper ow) throws TemplateModelException {
+        assertTrue(ow.wrap("x") instanceof SimpleScalar);
+        assertTrue(ow.wrap(1.5) instanceof SimpleNumber);
+        assertTrue(ow.wrap(new Date()) instanceof SimpleDate);
+        assertEquals(TemplateBooleanModel.TRUE, ow.wrap(true));
+        
+        try {
+            ow.wrap(new TestBean());
+            fail();
+        } catch (TemplateModelException e) {
+            assertThat(e.getMessage(), containsStringIgnoringCase("type"));
+        }
+    }
+
+    @Test
+    public void testDoesNotAllowAPIBuiltin() throws TemplateModelException {
+        RestrictedObjectWrapper sow = new RestrictedObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
+
+        TemplateModelWithAPISupport map = (TemplateModelWithAPISupport) sow.wrap(new HashMap());
+        try {
+            map.getAPI();
+            fail();
+        } catch (TemplateException e) {
+            assertThat(e.getMessage(), containsString("?api"));
+        }
+    }
+
+    @SuppressWarnings("boxing")
+    @Test
+    public void testCanWrapBasicTypes() throws TemplateModelException {
+        RestrictedObjectWrapper sow = new RestrictedObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
+        assertTrue(sow.wrap("s") instanceof TemplateScalarModel);
+        assertTrue(sow.wrap(1) instanceof TemplateNumberModel);
+        assertTrue(sow.wrap(true) instanceof TemplateBooleanModel);
+        assertTrue(sow.wrap(new Date()) instanceof TemplateDateModel);
+        assertTrue(sow.wrap(new ArrayList()) instanceof TemplateSequenceModel);
+        assertTrue(sow.wrap(new String[0]) instanceof TemplateSequenceModel);
+        assertTrue(sow.wrap(new ArrayList().iterator()) instanceof TemplateCollectionModel);
+        assertTrue(sow.wrap(new HashSet()) instanceof TemplateCollectionModelEx);
+        assertTrue(sow.wrap(new HashMap()) instanceof TemplateHashModelEx2);
+        assertNull(sow.wrap(null));
+    }
+
+    @Test
+    public void testWontWrapDOM() throws SAXException, IOException, ParserConfigurationException,
+            TemplateModelException {
+        DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+        InputSource is = new InputSource();
+        is.setCharacterStream(new StringReader("<doc><sub a='1' /></doc>"));
+        Document doc = db.parse(is);
+
+        RestrictedObjectWrapper sow = new RestrictedObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
+        try {
+            sow.wrap(doc);
+            fail();
+        } catch (TemplateModelException e) {
+            assertThat(e.getMessage(), containsString("won't wrap"));
+        }
+    }
+
+    @Test
+    public void testWontWrapGenericObjects() {
+        RestrictedObjectWrapper sow = new RestrictedObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
+        try {
+            sow.wrap(new File("/x"));
+            fail();
+        } catch (TemplateModelException e) {
+            assertThat(e.getMessage(), containsString("won't wrap"));
+        }
+    }
+
+    @Test
+    public void testCanBeBuiltOnlyOnce() {
+        RestrictedObjectWrapper.Builder builder = new RestrictedObjectWrapper.Builder(Configuration.VERSION_3_0_0);
+        builder.build();
+        try {
+            builder.build();
+            fail();
+        } catch (IllegalStateException e) {
+            // Expected
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/db64ebc9/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
index 98ae7a9..e7e5947 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/Configuration.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/Configuration.java
@@ -1550,30 +1550,9 @@ public final class Configuration implements TopLevelConfiguration, CustomStateSc
     }
 
     /**
-     * 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>
+     * Returns the FreeMarker version information, most importantly the major.minor.micro version numbers; do not use
+     * this for {@link #getIncompatibleImprovements() #incompatibleImprovements} value, use constants like
+     * {@link Configuration#VERSION_3_0_0} for that.
      */
     public static Version getVersion() {
         return VERSION;
@@ -2551,7 +2530,7 @@ public final class Configuration implements TopLevelConfiguration, CustomStateSc
             _NullArgumentException.check("sharedVariables", sharedVariables);
             _CollectionUtil.safeCastMap(
                     "sharedVariables", sharedVariables, String.class, false, Object.class,true);
-            this.sharedVariables = new HashMap<>(sharedVariables);
+            this.sharedVariables = Collections.unmodifiableMap(new HashMap<>(sharedVariables));
         }
 
         /**
@@ -2834,8 +2813,9 @@ public final class Configuration implements TopLevelConfiguration, CustomStateSc
 
         /**
          * @param incompatibleImprovements
-         *         Specifies the value of the {@linkplain Configuration#getIncompatibleImprovements()} incompatible
-         *         improvements setting}. This setting can't be changed later.
+         *         Specifies the value of the {@link Configuration#getIncompatibleImprovements()}
+         *         incompatibleImprovements} setting, such as {@link Configuration#VERSION_3_0_0}. This setting can't be
+         *         changed later.
          */
         public Builder(Version incompatibleImprovements) {
             super(incompatibleImprovements);

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/db64ebc9/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
index eb28dac..75eac65 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/MutableProcessingConfiguration.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/MutableProcessingConfiguration.java
@@ -525,7 +525,7 @@ public abstract class MutableProcessingConfiguration<SelfT extends MutableProces
      *         Not {@code null}; will be copied (to prevent aliasing effect); keys must conform to format name
      *         syntactical restrictions  (see in {@link #getCustomNumberFormats()})
      */
-    public void setCustomNumberFormats(Map<String, TemplateNumberFormatFactory> customNumberFormats) {
+    public void setCustomNumberFormats(Map<String, ? extends TemplateNumberFormatFactory> customNumberFormats) {
         setCustomNumberFormats(customNumberFormats, false);
     }
 
@@ -534,7 +534,8 @@ public abstract class MutableProcessingConfiguration<SelfT extends MutableProces
      *         {@code true} if we know that the 1st argument is already validated, immutable, and unchanging (means,
      *         won't change later because of aliasing).
      */
-    void setCustomNumberFormats(Map<String, TemplateNumberFormatFactory> customNumberFormats,
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    void setCustomNumberFormats(Map<String, ? extends TemplateNumberFormatFactory> customNumberFormats,
             boolean validatedImmutableUnchanging) {
         _NullArgumentException.check("customNumberFormats", customNumberFormats);
         if (!validatedImmutableUnchanging) {
@@ -547,7 +548,7 @@ public abstract class MutableProcessingConfiguration<SelfT extends MutableProces
             validateFormatNames(customNumberFormats.keySet());
             this.customNumberFormats = Collections.unmodifiableMap(new HashMap<>(customNumberFormats));
         } else {
-            this.customNumberFormats = customNumberFormats;
+            this.customNumberFormats = (Map) customNumberFormats;
         }
     }
 
@@ -794,7 +795,7 @@ public abstract class MutableProcessingConfiguration<SelfT extends MutableProces
      *         Not {@code null}; will be copied (to prevent aliasing effect); keys must conform to format name
      *         syntactical restrictions (see in {@link #getCustomDateFormats()})
      */
-    public void setCustomDateFormats(Map<String, TemplateDateFormatFactory> customDateFormats) {
+    public void setCustomDateFormats(Map<String, ? extends TemplateDateFormatFactory> customDateFormats) {
         setCustomDateFormats(customDateFormats, false);
     }
 
@@ -803,8 +804,9 @@ public abstract class MutableProcessingConfiguration<SelfT extends MutableProces
      *         {@code true} if we know that the 1st argument is already validated, immutable, and unchanging (means,
      *         won't change later because of aliasing).
      */
+    @SuppressWarnings({ "unchecked", "rawtypes" })
     void setCustomDateFormats(
-            Map<String, TemplateDateFormatFactory> customDateFormats,
+            Map<String, ? extends TemplateDateFormatFactory> customDateFormats,
             boolean validatedImmutableUnchanging) {
         _NullArgumentException.check("customDateFormats", customDateFormats);
         if (!validatedImmutableUnchanging) {
@@ -815,9 +817,9 @@ public abstract class MutableProcessingConfiguration<SelfT extends MutableProces
                     String.class, false,
                     TemplateDateFormatFactory.class, false);
             validateFormatNames(customDateFormats.keySet());
-            this.customDateFormats = Collections.unmodifiableMap(new HashMap(customDateFormats));
+            this.customDateFormats = Collections.unmodifiableMap(new HashMap<>(customDateFormats));
         } else {
-            this.customDateFormats = customDateFormats;
+            this.customDateFormats = (Map) customDateFormats;
         }
     }
 
@@ -1363,7 +1365,7 @@ public abstract class MutableProcessingConfiguration<SelfT extends MutableProces
                 return;
             }
             _CollectionUtil.safeCastMap("autoImports", autoImports, String.class, false, String.class, false);
-            this.autoImports = new LinkedHashMap<>(autoImports);
+            this.autoImports = Collections.unmodifiableMap(new LinkedHashMap<>(autoImports));
         } else {
             this.autoImports = autoImports;
         }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/db64ebc9/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
index 40542a6..4aaa7c2 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateConfiguration.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateConfiguration.java
@@ -23,10 +23,12 @@ import java.io.Serializable;
 import java.nio.charset.Charset;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 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;
@@ -162,14 +164,23 @@ public final class TemplateConfiguration implements ParsingAndProcessingConfigur
      * Adds two {@link List}-s; assuming the inputs are already unmodifiable and unchanging, it returns an
      * unmodifiable and unchanging {@link List} itself.
      */
-    private static List<String> mergeLists(List<String> list1, List<String> list2) {
+    private static List<String> mergeLists(List<String> list1, List<String> list2, boolean skipDuplicatesInList1) {
         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);
+        if (skipDuplicatesInList1) {
+            Set<String> list2Set = new HashSet<>(list2);
+            for (String it : list1) {
+                if (!list2Set.contains(it)) {
+                    mergedList.add(it);
+                }
+            }
+        } else {
+            mergedList.addAll(list1);
+        }
         mergedList.addAll(list2);
         return Collections.unmodifiableList(mergedList);
     }
@@ -933,12 +944,15 @@ public final class TemplateConfiguration implements ParsingAndProcessingConfigur
                 setAutoImports(mergeMaps(
                         isAutoImportsSet() ? getAutoImports() : null,
                         tc.isAutoImportsSet() ? tc.getAutoImports() : null,
-                        true));
+                        true),
+                        true);
             }
             if (tc.isAutoIncludesSet()) {
                 setAutoIncludes(mergeLists(
                         isAutoIncludesSet() ? getAutoIncludes() : null,
-                        tc.isAutoIncludesSet() ? tc.getAutoIncludes() : null));
+                        tc.isAutoIncludesSet() ? tc.getAutoIncludes() : null,
+                        true),
+                        true);
             }
 
             setCustomSettingsMap(mergeMaps(

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/db64ebc9/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
deleted file mode 100644
index 86d0b3e..0000000
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/util/ProductWrappingBuilder.java
+++ /dev/null
@@ -1,43 +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.
- */
-
-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;
-    private boolean alreadyBuilt;
-
-    public ProductWrappingBuilder(ProductT product) {
-        _NullArgumentException.check("product", product);
-        this.product = product;
-    }
-
-    @Override
-    public ProductT build() {
-        if (alreadyBuilt) {
-            throw new IllegalStateException("build() can only be executed once.");
-        }
-        alreadyBuilt = true;
-        return product;
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/db64ebc9/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
index 275f64c..1f91821 100644
--- 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
@@ -19,6 +19,7 @@
 
 package org.apache.freemarker.core.util;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -114,6 +115,9 @@ public class _CollectionUtil {
     private static final Class<?> UNMODIFIABLE_MAP_CLASS_1 = Collections.emptyMap().getClass();
     private static final Class<?> UNMODIFIABLE_MAP_CLASS_2 = Collections.unmodifiableMap(
             new HashMap<Object, Object> (1)).getClass();
+    private static final Class<?> UNMODIFIABLE_LIST_CLASS_1 = Collections.emptyList().getClass();
+    private static final Class<?> UNMODIFIABLE_LIST_CLASS_2 = Collections.unmodifiableList(
+            new ArrayList<Object>(1)).getClass();
 
     public static boolean isMapKnownToBeUnmodifiable(Map<?, ?> map) {
         if (map == null) {
@@ -123,6 +127,14 @@ public class _CollectionUtil {
         return mapClass == UNMODIFIABLE_MAP_CLASS_1 || mapClass == UNMODIFIABLE_MAP_CLASS_2;
     }
 
+    public static boolean isListKnownToBeUnmodifiable(List<?> list) {
+        if (list == null) {
+            return true;
+        }
+        Class<? extends List> listClass = list.getClass();
+        return listClass == UNMODIFIABLE_LIST_CLASS_1 || listClass == UNMODIFIABLE_LIST_CLASS_2;
+    }
+
     /**
      * Optimized version of {@link Collections#unmodifiableMap(Map)} (avoids needless wrapping).
      *


Mime
View raw message