Cleaned up customAttribute related API-s. Most notably, a new 
getCustomAttribute overload was added, `getCustomAttribute(Serializable key, 
Object default)`, where the default value is used if the attribute wasn't set 
(not even to null). (There's a special value, 
`ProcessingConfiguration.MISSING_VALUE_MARKER`, which can be used as default 
but not as the value of a custom attribute.) `getCustomAttribute(Serializable 
key)` now throws `MissingCustomAttributeValue` exception if the attribute isn't 
set. Also, `getCustomAttributesSnapshot(includeInherited)` was added, which is 
mostly useful for debugging purposes. Also note that the


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

Branch: refs/heads/3
Commit: dc689993a6407a0307b4710c4a06272d45488a5f
Parents: eae2708
Author: ddekany <ddek...@apache.org>
Authored: Fri Jun 2 00:13:10 2017 +0200
Committer: ddekany <ddek...@apache.org>
Committed: Fri Jun 2 00:13:10 2017 +0200

----------------------------------------------------------------------
 FM3-CHANGE-LOG.txt                              |   6 +-
 .../freemarker/core/CustomAttributeTest.java    | 240 ++++++++++++++-----
 .../core/TemplateConfigurationTest.java         |  37 ++-
 ...gurationWithDefaultTemplateResolverTest.java |  10 +-
 .../TemplateConfigurationFactoryTest.java       |  11 +-
 .../core/util/CollectionUtilTest.java           |  43 ++++
 .../apache/freemarker/core/Configuration.java   | 182 +++++++++-----
 .../core/CustomAttributeNotSetException.java    |  48 ++++
 .../org/apache/freemarker/core/Environment.java |  16 +-
 .../core/MutableProcessingConfiguration.java    | 221 ++++++++++-------
 .../core/ProcessingConfiguration.java           | 101 ++++++--
 .../core/SettingValueNotSetException.java       |  12 +-
 .../org/apache/freemarker/core/Template.java    | 167 +++++++++----
 .../freemarker/core/TemplateConfiguration.java  |  88 ++++---
 .../freemarker/core/util/_CollectionUtil.java   |  23 ++
 freemarker-core/src/main/javacc/FTL.jj          |   2 +-
 .../freemarker/servlet/FreemarkerServlet.java   |   2 +-
 17 files changed, 837 insertions(+), 372 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dc689993/FM3-CHANGE-LOG.txt
----------------------------------------------------------------------
diff --git a/FM3-CHANGE-LOG.txt b/FM3-CHANGE-LOG.txt
index f0823ab..1ca0250 100644
--- a/FM3-CHANGE-LOG.txt
+++ b/FM3-CHANGE-LOG.txt
@@ -179,7 +179,11 @@ the FreeMarer 3 changelog here:
     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.)
+  - Cleaned up customAttribute related API-s. Most notably, a new 
getCustomAttribute overload was added, `getCustomAttribute(Serializable key, 
Object default)`,
+    where the default value is used if the attribute wasn't set (not even to 
null). (There's a reserved ProcessingConfiguration.MISSING_VALUE_MARKER that
+    can be used as default but not as the value of a custom attribute). 
`getCustomAttribute(Serializable key)` now throws MissingCustomAttributeValue 
exception
+    if the attribute isn't set. Also, 
getCustomAttributesSnapshot(includeInherited) was added, which is mostly useful 
for debugging purposes. Also note that the
+    key of custom attributes now must be Serializable.
   - 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.

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dc689993/freemarker-core-test/src/test/java/org/apache/freemarker/core/CustomAttributeTest.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/CustomAttributeTest.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/CustomAttributeTest.java
index 726a20c..d714380 100644
--- 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/CustomAttributeTest.java
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/CustomAttributeTest.java
@@ -19,10 +19,14 @@
 
 package org.apache.freemarker.core;
 
+import static 
org.apache.freemarker.core.ProcessingConfiguration.MISSING_VALUE_MARKER;
+import static org.hamcrest.Matchers.containsString;
 import static org.junit.Assert.*;
 
+import java.io.Serializable;
 import java.math.BigDecimal;
-import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
 
 import org.junit.Test;
 
@@ -42,82 +46,121 @@ public class CustomAttributeTest {
     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();
+    public void testMutableProcessingConfiguration() throws Exception {
+        testMutableProcessingConfiguration(new 
Configuration.Builder(Configuration.VERSION_3_0_0));
+
+        testMutableProcessingConfiguration(new 
TemplateConfiguration.Builder());
+
+        Configuration cfg = new 
Configuration.Builder(Configuration.VERSION_3_0_0).build();
+        Environment env = new Template(null, "", 
cfg).createProcessingEnvironment(null, null);
+        testMutableProcessingConfiguration(env);
+    }
+
+    private void 
testMutableProcessingConfiguration(MutableProcessingConfiguration<?> mpc) {
+        assertTrue(mpc.getCustomAttributesSnapshot(true).isEmpty());
+        assertTrue(mpc.getCustomAttributesSnapshot(false).isEmpty());
+        testMissingCustomAttributeAccess(mpc, 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));
-        
         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));
+        testMissingCustomAttributeAccess(mpc, KEY_3);
+        assertEquals(ImmutableMap.of(KEY_1, VALUE_1, KEY_2, VALUE_2), 
mpc.getCustomAttributesSnapshot(true));
+        assertEquals(ImmutableMap.of(KEY_1, VALUE_1, KEY_2, VALUE_2), 
mpc.getCustomAttributesSnapshot(false));
 
-        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));
+        assertSame(VALUE_2, mpc.getCustomAttribute(KEY_2, "default"));
+        mpc.unsetCustomAttribute(KEY_2);
+        assertEquals("default", mpc.getCustomAttribute(KEY_2, "default"));
+        assertEquals(ImmutableMap.of(KEY_1, VALUE_1), 
mpc.getCustomAttributesSnapshot(true));
+        assertEquals(ImmutableMap.of(KEY_1, VALUE_1), 
mpc.getCustomAttributesSnapshot(false));
 
-        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.unsetAllCustomAttributes();
+        assertTrue(mpc.getCustomAttributesSnapshot(true).isEmpty());
 
-        mpc.removeCustomAttribute(KEY_1);
-        assertArrayEquals(new String[] { KEY_2 }, 
mpc.getCustomAttributeNames());
-        assertNull(mpc.getCustomAttribute(KEY_1));
-        assertSame(VALUE_2, mpc.getCustomAttribute(KEY_2));
+        testCustomAttributesSnapshotIsUnmodifiable(mpc);
+        mpc.setCustomAttribute(KEY_1, VALUE_1);
+        testCustomAttributesSnapshotIsUnmodifiable(mpc);
+
+        // Test no aliasing
+        Map<Serializable, Object> attrMap1 = 
mpc.getCustomAttributesSnapshot(false);
+        mpc.setCustomAttribute(KEY_2, VALUE_2);
+        assertNull(attrMap1.get(KEY_2));
+
+        mpc.unsetAllCustomAttributes();
+        mpc.setCustomAttribute(KEY_1, VALUE_1);
+        mpc.setCustomAttributes(ImmutableMap.of(KEY_2, VALUE_2, KEY_3, 
VALUE_3));
+        assertEquals(
+                ImmutableMap.of(KEY_1, VALUE_1, KEY_2, VALUE_2, KEY_3, 
VALUE_3),
+                mpc.getCustomAttributesSnapshot(false));
+
+        try {
+            mpc.setCustomAttribute(KEY_1, MISSING_VALUE_MARKER);
+            fail();
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), containsString("MISSING_VALUE_MARKER"));
+        }
+        try {
+            mpc.setCustomAttributes(ImmutableMap.of(KEY_1, 
MISSING_VALUE_MARKER));
+            fail();
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), containsString("MISSING_VALUE_MARKER"));
+        }
     }
 
-    @Test
-    public void testRemoveFromEmptySet() throws Exception {
-        // Need some MutableProcessingConfiguration:
-        TemplateConfiguration.Builder mpc = new 
TemplateConfiguration.Builder();
+    private void testMissingCustomAttributeAccess(ProcessingConfiguration pc) {
+        testMissingCustomAttributeAccess(pc, "noSuchKey");
+    }
+
+    private void 
testCustomAttributesSnapshotIsUnmodifiable(ProcessingConfiguration pc) {
+        for (boolean includeInherited : new boolean[] { false,  true }) {
+            Map<Serializable, Object> map = 
pc.getCustomAttributesSnapshot(includeInherited);
+            try {
+                map.put("aNewKey", 123);
+                fail();
+            } catch (UnsupportedOperationException e) {
+                // Expected
+            }
+        }
+    }
 
-        mpc.removeCustomAttribute(KEY_1);
-        assertEquals(0, mpc.getCustomAttributeNames().length);
-        assertNull(mpc.getCustomAttribute(KEY_1));
+    private void testMissingCustomAttributeAccess(ProcessingConfiguration pc, 
Serializable key) {
+        try {
+            pc.getCustomAttribute(key);
+            fail();
+        } catch (CustomAttributeNotSetException e) {
+            assertSame(key, e.getKey());
+        }
 
-        mpc.setCustomAttribute(KEY_1, VALUE_1);
-        assertArrayEquals(new String[] { KEY_1 }, 
mpc.getCustomAttributeNames());
-        assertSame(VALUE_1, mpc.getCustomAttribute(KEY_1));
+        assertNull(pc.getCustomAttribute(key, null));
+        assertEquals("default", pc.getCustomAttribute(key, "default"));
+        assertSame(MISSING_VALUE_MARKER,
+                pc.getCustomAttribute(key, MISSING_VALUE_MARKER));
     }
 
     @Test
-    public void testAttrsFromFtlHeaderOnly() throws Exception {
+    public void testTemplateAttrsFromFtlHeaderOnly() throws Exception {
         Template t = new Template(null, "<#ftl attributes={"
                 + "'" + KEY_1 + "': [ 's', 2, true, {  'a': 'A' } ], "
-                + "'" + KEY_2 + "': " + VALUE_BIGDECIMAL + " "
+                + "'" + KEY_2 + "': 22 "
                 + "}>",
                 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));
+        assertEquals(ImmutableSet.of(KEY_1, KEY_2), 
t.getCustomAttributesSnapshot(true).keySet());
+        assertEquals(
+                ImmutableList.<Object>of("s", BigDecimal.valueOf(2), 
Boolean.TRUE, ImmutableMap.of("a", "A")),
+                t.getCustomAttribute(KEY_1));
+        assertEquals(BigDecimal.valueOf(22), 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));
+        testMissingCustomAttributeAccess(t);
+        testCustomAttributesSnapshotIsUnmodifiable(t);
     }
 
     @Test
-    public void testAttrsFromFtlHeaderAndFromTemplateConfiguration() throws 
Exception {
+    public void testTemplateAttrsFromFtlHeaderAndFromTemplateConfiguration() 
throws Exception {
         TemplateConfiguration.Builder tcb = new 
TemplateConfiguration.Builder();
         tcb.setCustomAttribute(KEY_3, VALUE_3);
         tcb.setCustomAttribute(KEY_4, VALUE_4);
@@ -129,20 +172,19 @@ public class CustomAttributeTest {
                 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(ImmutableMap.of(KEY_1, "a", KEY_2, "b", KEY_3, "c", 
KEY_4, VALUE_4),
+                t.getCustomAttributesSnapshot(true));
         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));
+        testMissingCustomAttributeAccess(t);
+        testCustomAttributesSnapshotIsUnmodifiable(t);
     }
 
-
     @Test
-    public void testAttrsFromTemplateConfigurationOnly() throws Exception {
+    public void testTemplateAttrsFromTemplateConfigurationOnly() throws 
Exception {
         TemplateConfiguration.Builder tcb = new 
TemplateConfiguration.Builder();
         tcb.setCustomAttribute(KEY_3, VALUE_3);
         tcb.setCustomAttribute(KEY_4, VALUE_4);
@@ -150,14 +192,90 @@ public class CustomAttributeTest {
                 new Configuration.Builder(Configuration.VERSION_3_0_0).build(),
                 tcb.build());
 
-        assertEquals(ImmutableSet.of(KEY_3, KEY_4), 
t.getCustomAttributes().keySet());
+        assertEquals(ImmutableSet.of(KEY_3, KEY_4), 
t.getCustomAttributesSnapshot(true).keySet());
         assertEquals(VALUE_3, t.getCustomAttribute(KEY_3));
         assertEquals(VALUE_4, t.getCustomAttribute(KEY_4));
+
+        testMissingCustomAttributeAccess(t);
+        testCustomAttributesSnapshotIsUnmodifiable(t);
     }
 
-    private Object[] sort(String[] customAttributeNames) {
-        Arrays.sort(customAttributeNames);
-        return customAttributeNames;
+    @Test
+    public void testTemplateAttrsFromConfigurationOnly() throws Exception {
+        Configuration cfg = new 
Configuration.Builder(Configuration.VERSION_3_0_0)
+                .customAttribute(KEY_1, VALUE_1)
+                .build();
+        Template t = new Template(null, "", cfg);
+
+        assertEquals(VALUE_1, t.getCustomAttribute(KEY_1));
+        assertEquals("default", t.getCustomAttribute(KEY_2, "default"));
+
+        assertEquals(ImmutableMap.of(KEY_1, VALUE_1), 
t.getCustomAttributesSnapshot(true));
+        assertTrue(t.getCustomAttributesSnapshot(false).isEmpty());
+
+        testMissingCustomAttributeAccess(t);
+        testCustomAttributesSnapshotIsUnmodifiable(t);
+    }
+
+    @Test
+    public void testTemplateAttrsFromTemplateAndConfiguration() throws 
Exception {
+        Configuration cfg = new 
Configuration.Builder(Configuration.VERSION_3_0_0)
+                .customAttribute(KEY_1, VALUE_1)
+                .build();
+        Template t = new Template(null, "<#ftl attributes={'k2':'v2'}>", cfg);
+
+        assertEquals(VALUE_1, t.getCustomAttribute(KEY_1));
+        assertEquals("v2", t.getCustomAttribute("k2"));
+
+        assertEquals(ImmutableMap.of(KEY_1, VALUE_1, "k2", "v2"), 
t.getCustomAttributesSnapshot(true));
+        assertEquals(ImmutableMap.of("k2", "v2"), 
t.getCustomAttributesSnapshot(false));
+
+        testMissingCustomAttributeAccess(t);
+        testCustomAttributesSnapshotIsUnmodifiable(t);
+    }
+
+    @Test
+    public void testAllLayers() throws Exception {
+        Configuration cfg = new 
Configuration.Builder(Configuration.VERSION_3_0_0)
+                .customAttribute(KEY_1, VALUE_1)
+                .build();
+        Template t = new Template(null, "<#ftl attributes={'k2':'v2'}>", cfg,
+                new TemplateConfiguration.Builder().customAttribute(KEY_3, 
VALUE_3).build());
+
+        assertEquals(VALUE_1, t.getCustomAttribute(KEY_1));
+        assertEquals("v2", t.getCustomAttribute("k2"));
+        assertEquals(VALUE_3, t.getCustomAttribute(KEY_3));
+
+        assertEquals(ImmutableMap.of(KEY_1, VALUE_1, "k2", "v2", KEY_3, 
VALUE_3),
+                t.getCustomAttributesSnapshot(true));
+        assertEquals(ImmutableMap.of("k2", "v2", KEY_3, VALUE_3),
+                t.getCustomAttributesSnapshot(false));
+
+        testMissingCustomAttributeAccess(t);
+        testCustomAttributesSnapshotIsUnmodifiable(t);
+
+        Environment env = t.createProcessingEnvironment(null, null);
+
+        assertEquals(VALUE_1, env.getCustomAttribute(KEY_1));
+        assertEquals("v2", env.getCustomAttribute("k2"));
+        assertEquals(VALUE_3, env.getCustomAttribute(KEY_3));
+        assertEquals(ImmutableMap.of(KEY_1, VALUE_1, "k2", "v2", KEY_3, 
VALUE_3),
+                env.getCustomAttributesSnapshot(true));
+        assertEquals(Collections.emptyMap(),
+                env.getCustomAttributesSnapshot(false));
+
+        env.setCustomAttribute(KEY_4, VALUE_4);
+        assertEquals(VALUE_1, env.getCustomAttribute(KEY_1));
+        assertEquals("v2", env.getCustomAttribute("k2"));
+        assertEquals(VALUE_3, env.getCustomAttribute(KEY_3));
+        assertEquals(VALUE_4, env.getCustomAttribute(KEY_4));
+        assertEquals(ImmutableMap.of(KEY_1, VALUE_1, "k2", "v2", KEY_3, 
VALUE_3, KEY_4, VALUE_4),
+                env.getCustomAttributesSnapshot(true));
+        assertEquals(ImmutableMap.of(KEY_4, VALUE_4),
+                env.getCustomAttributesSnapshot(false));
+
+        testMissingCustomAttributeAccess(env);
+        testCustomAttributesSnapshotIsUnmodifiable(env);
     }
 
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dc689993/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 c229655..92b5bfb 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
@@ -18,6 +18,7 @@
  */
 package org.apache.freemarker.core;
 
+import static 
org.apache.freemarker.core.ProcessingConfiguration.MISSING_VALUE_MARKER;
 import static org.junit.Assert.*;
 
 import java.beans.BeanInfo;
@@ -174,7 +175,6 @@ public class TemplateConfigurationTest {
                 ImmutableMap.of("dummy", 
HexTemplateNumberFormatFactory.INSTANCE));
         SETTING_ASSIGNMENTS.put("customDateFormats",
                 ImmutableMap.of("dummy", 
EpochMillisTemplateDateFormatFactory.INSTANCE));
-        SETTING_ASSIGNMENTS.put("customAttributes", ImmutableMap.of("dummy", 
123));
 
         // Parser-only settings:
         SETTING_ASSIGNMENTS.put("templateLanguage", 
TemplateLanguage.STATIC_TEXT);
@@ -237,6 +237,7 @@ public class TemplateConfigurationTest {
         IGNORED_PROP_NAMES.add("strictBeanModels");
         IGNORED_PROP_NAMES.add("parentConfiguration");
         IGNORED_PROP_NAMES.add("settings");
+        IGNORED_PROP_NAMES.add("customAttributes");
     }
 
     private static final Set<String> CONFIGURABLE_PROP_NAMES;
@@ -277,7 +278,7 @@ public class TemplateConfigurationTest {
         }
     }
 
-    private static final Object CA1 = new Object();
+    private static final Integer CA1 = Integer.valueOf(123);
     private static final String CA2 = "ca2";
     private static final String CA3 = "ca3";
     private static final String CA4 = "ca4";
@@ -434,10 +435,10 @@ public class TemplateConfigurationTest {
 
         assertEquals("v1", tcb1.getCustomAttribute("k1"));
         assertEquals("v1", tcb1.getCustomAttribute("k2"));
-        assertNull("v1", tcb1.getCustomAttribute("k3"));
+        assertEquals(MISSING_VALUE_MARKER, tcb1.getCustomAttribute("k3", 
MISSING_VALUE_MARKER));
         assertEquals("V1", tcb1.getCustomAttribute(CA1));
         assertEquals("V1", tcb1.getCustomAttribute(CA2));
-        assertNull(tcb1.getCustomAttribute(CA3));
+        assertEquals(MISSING_VALUE_MARKER, tcb1.getCustomAttribute(CA3, 
MISSING_VALUE_MARKER));
 
         TemplateConfiguration.Builder tcb2 = new 
TemplateConfiguration.Builder();
         tcb2.setCustomAttribute("k1", "v2");
@@ -454,10 +455,10 @@ public class TemplateConfigurationTest {
 
         assertNull(tcb1.getCustomAttribute("k1"));
         assertNull(tcb1.getCustomAttribute("k2"));
-        assertNull(tcb1.getCustomAttribute("k3"));
+        assertEquals(MISSING_VALUE_MARKER, tcb1.getCustomAttribute("k3", 
MISSING_VALUE_MARKER));
         assertNull(tcb1.getCustomAttribute(CA1));
         assertNull(tcb1.getCustomAttribute(CA2));
-        assertNull(tcb1.getCustomAttribute(CA3));
+        assertEquals(MISSING_VALUE_MARKER, tcb1.getCustomAttribute(CA3, 
MISSING_VALUE_MARKER));
 
         TemplateConfiguration.Builder tcb4 = new 
TemplateConfiguration.Builder();
         tcb4.setCustomAttribute("k1", "v4");
@@ -467,10 +468,10 @@ public class TemplateConfigurationTest {
 
         assertEquals("v4", tcb1.getCustomAttribute("k1"));
         assertNull(tcb1.getCustomAttribute("k2"));
-        assertNull(tcb1.getCustomAttribute("k3"));
+        assertEquals(MISSING_VALUE_MARKER, tcb1.getCustomAttribute("k3", 
MISSING_VALUE_MARKER));
         assertEquals("V4", tcb1.getCustomAttribute(CA1));
         assertNull(tcb1.getCustomAttribute(CA2));
-        assertNull(tcb1.getCustomAttribute(CA3));
+        assertEquals(MISSING_VALUE_MARKER, tcb1.getCustomAttribute(CA3, 
MISSING_VALUE_MARKER));
     }
 
     @Test
@@ -501,6 +502,7 @@ public class TemplateConfigurationTest {
                 .customAttribute("k1", "c")
                 .customAttribute("k2", "c")
                 .customAttribute("k3", "c")
+                .customAttribute("k8", "c")
                 .build();
 
         TemplateConfiguration.Builder tcb = new 
TemplateConfiguration.Builder();
@@ -509,30 +511,19 @@ public class TemplateConfigurationTest {
         tcb.setCustomAttribute("k4", "tc");
         tcb.setCustomAttribute("k5", "tc");
         tcb.setCustomAttribute("k6", "tc");
-        tcb.setCustomAttribute(CA1, "tc");
-        tcb.setCustomAttribute(CA2,"tc");
-        tcb.setCustomAttribute(CA3,"tc");
 
         TemplateConfiguration tc = tcb.build();
-        Template t = new Template(null, "", cfg, tc);
-        t.setCustomAttribute("k5", "t");
-        t.setCustomAttribute("k6", null);
-        t.setCustomAttribute("k7", "t");
-        t.setCustomAttribute(CA2, "t");
-        t.setCustomAttribute(CA3, null);
-        t.setCustomAttribute(CA4, "t");
+        Template t = new Template(null, "<#ftl attributes={'k5':'t', 'k7':'t', 
'k8':'t'}>", cfg, tc);
 
         assertEquals("c", t.getCustomAttribute("k1"));
         assertEquals("tc", t.getCustomAttribute("k2"));
         assertNull(t.getCustomAttribute("k3"));
         assertEquals("tc", t.getCustomAttribute("k4"));
         assertEquals("t", t.getCustomAttribute("k5"));
-        assertNull(t.getCustomAttribute("k6"));
+        // TODO [FM3] when { ... 'k6': null ... } works in FTL, put this back.
+        // assertNull(t.getCustomAttribute("k6"));
         assertEquals("t", t.getCustomAttribute("k7"));
-        assertEquals("tc", t.getCustomAttribute(CA1));
-        assertEquals("t", t.getCustomAttribute(CA2));
-        assertNull(t.getCustomAttribute(CA3));
-        assertEquals("t", t.getCustomAttribute(CA4));
+        assertEquals("t", t.getCustomAttribute("k8"));
     }
     
     @Test

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dc689993/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateConfigurationWithDefaultTemplateResolverTest.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateConfigurationWithDefaultTemplateResolverTest.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateConfigurationWithDefaultTemplateResolverTest.java
index 4cd50eb..f242b66 100644
--- 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateConfigurationWithDefaultTemplateResolverTest.java
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateConfigurationWithDefaultTemplateResolverTest.java
@@ -18,9 +18,11 @@
  */
 package org.apache.freemarker.core;
 
+import static 
org.apache.freemarker.core.ProcessingConfiguration.MISSING_VALUE_MARKER;
 import static org.junit.Assert.*;
 
 import java.io.IOException;
+import java.io.Serializable;
 import java.io.StringWriter;
 import java.io.UnsupportedEncodingException;
 import java.nio.charset.Charset;
@@ -40,8 +42,8 @@ public class 
TemplateConfigurationWithDefaultTemplateResolverTest {
 
     private static final String TEXT_WITH_ACCENTS = "pr\u00F3ba";
 
-    private static final Object CUST_ATT_1 = new Object();
-    private static final Object CUST_ATT_2 = new Object();
+    private static final Serializable CUST_ATT_1 = Integer.valueOf(111);
+    private static final Serializable CUST_ATT_2 = Integer.valueOf(222);
 
     private static final Charset ISO_8859_2 = Charset.forName("ISO-8859-2");
 
@@ -212,10 +214,10 @@ public class 
TemplateConfigurationWithDefaultTemplateResolverTest {
         {
             Template t = cfg.getTemplate("(tc2)");
             assertEquals("a1tc2", t.getCustomAttribute("a1"));
-            assertNull(t.getCustomAttribute("a2"));
+            assertEquals(MISSING_VALUE_MARKER, t.getCustomAttribute("a2", 
MISSING_VALUE_MARKER));
             assertEquals("a3temp", t.getCustomAttribute("a3"));
             assertEquals("ca1tc2", t.getCustomAttribute(CUST_ATT_1));
-            assertNull(t.getCustomAttribute(CUST_ATT_2));
+            assertEquals(MISSING_VALUE_MARKER, 
t.getCustomAttribute(CUST_ATT_2, MISSING_VALUE_MARKER));
         }
         {
             Template t = cfg.getTemplate("(tc1)(tc2)");

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dc689993/freemarker-core-test/src/test/java/org/apache/freemarker/core/templateresolver/TemplateConfigurationFactoryTest.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/templateresolver/TemplateConfigurationFactoryTest.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/templateresolver/TemplateConfigurationFactoryTest.java
index a7259d8..78ccf81 100644
--- 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/templateresolver/TemplateConfigurationFactoryTest.java
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/templateresolver/TemplateConfigurationFactoryTest.java
@@ -22,9 +22,11 @@ import static org.hamcrest.Matchers.*;
 import static org.junit.Assert.*;
 
 import java.io.IOException;
+import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
 
+import org.apache.freemarker.core.ProcessingConfiguration;
 import org.apache.freemarker.core.TemplateConfiguration;
 import org.junit.Test;
 
@@ -165,7 +167,7 @@ public class TemplateConfigurationFactoryTest {
     private void assertApplicable(TemplateConfigurationFactory tcf, String 
sourceName, TemplateConfiguration... expectedTCs)
             throws IOException, TemplateConfigurationFactoryException {
         TemplateConfiguration mergedTC = tcf.get(sourceName, 
DummyTemplateLoadingSource.INSTANCE);
-        List<Object> mergedTCAttNames = new 
ArrayList<>(mergedTC.getCustomAttributes().keySet());
+        List<Serializable> mergedTCAttNames = new 
ArrayList<>(mergedTC.getCustomAttributesSnapshot(false).keySet());
 
         for (TemplateConfiguration expectedTC : expectedTCs) {
             Integer tcId = (Integer) expectedTC.getCustomAttribute("id");
@@ -177,7 +179,7 @@ public class TemplateConfigurationFactoryTest {
             }
         }
         
-        for (Object attKey: mergedTCAttNames) {
+        for (Serializable attKey: mergedTCAttNames) {
             if (!containsCustomAttr(attKey, expectedTCs)) {
                 fail("The asserted TemplateConfiguration contains an 
unexpected custom attribute: " + attKey);
             }
@@ -186,9 +188,10 @@ public class TemplateConfigurationFactoryTest {
         assertEquals(expectedTCs[expectedTCs.length - 
1].getCustomAttribute("id"), mergedTC.getCustomAttribute("id"));
     }
 
-    private boolean containsCustomAttr(Object attKey, TemplateConfiguration... 
expectedTCs) {
+    private boolean containsCustomAttr(Serializable attKey, 
TemplateConfiguration... expectedTCs) {
         for (TemplateConfiguration expectedTC : expectedTCs) {
-            if (expectedTC.getCustomAttribute(attKey) != null) {
+            if (expectedTC.getCustomAttribute(attKey, 
ProcessingConfiguration.MISSING_VALUE_MARKER)
+                    != ProcessingConfiguration.MISSING_VALUE_MARKER) {
                 return true;
             }
         }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dc689993/freemarker-core-test/src/test/java/org/apache/freemarker/core/util/CollectionUtilTest.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/util/CollectionUtilTest.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/util/CollectionUtilTest.java
new file mode 100644
index 0000000..a18d585
--- /dev/null
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/util/CollectionUtilTest.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 static org.junit.Assert.*;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Test;
+
+public class CollectionUtilTest {
+
+    @Test
+    public void unmodifiableMap() {
+        Map<Object, Object> modifiableMap = new HashMap<>();
+        assertNotSame(modifiableMap, 
_CollectionUtil.unmodifiableMap(modifiableMap));
+
+        Map<Object, Object> wrappedModifiableMap = 
Collections.unmodifiableMap(modifiableMap);
+        assertSame(wrappedModifiableMap, 
_CollectionUtil.unmodifiableMap(wrappedModifiableMap));
+
+        assertSame(Collections.emptyMap(), 
_CollectionUtil.unmodifiableMap(Collections.emptyMap()));
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dc689993/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 721331e..6691053 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
@@ -133,6 +133,9 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
  * </ul>
  * 
  * <p>{@link Configuration} is thread-safe and (as of 3.0.0) immutable (apart 
from internal caches).
+ *
+ * <p>The setting reader methods of this class don't throw {@link 
SettingValueNotSetException}, because all settings
+ * are set on the {@link Configuration} level (even if they were just 
initialized to a default value).
  */
 public final class Configuration
         implements TopLevelConfiguration, CustomStateScope {
@@ -279,7 +282,7 @@ public final class Configuration
     private final List<String> autoIncludes;
     private final Boolean lazyImports;
     private final Boolean lazyAutoImports;
-    private final Map<Object, Object> customAttributes;
+    private final Map<Serializable, Object> customAttributes;
 
     // CustomStateScope:
 
@@ -434,7 +437,7 @@ public final class Configuration
         autoIncludes = builder.getAutoIncludes();
         lazyImports = builder.getLazyImports();
         lazyAutoImports = builder.getLazyAutoImports();
-        customAttributes = builder.getCustomAttributes();
+        customAttributes = builder.getCustomAttributesSnapshot(false);
     }
 
     private <SelfT extends ExtendableBuilder<SelfT>> void 
wrapAndPutSharedVariables(
@@ -462,7 +465,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isTemplateExceptionHandlerSet() {
@@ -482,7 +486,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isTemplateLoaderSet() {
@@ -498,7 +503,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isTemplateLookupStrategySet() {
@@ -514,7 +520,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isTemplateNameFormatSet() {
@@ -530,7 +537,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isTemplateConfigurationsSet() {
@@ -543,7 +551,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isCacheStorageSet() {
@@ -556,7 +565,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isTemplateUpdateDelayMillisecondsSet() {
@@ -574,7 +584,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isWhitespaceStrippingSet() {
@@ -627,7 +638,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isAutoEscapingPolicySet() {
@@ -640,7 +652,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isOutputFormatSet() {
@@ -760,7 +773,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isRecognizeStandardFileExtensionsSet() {
@@ -773,7 +787,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isTemplateLanguageSet() {
@@ -786,11 +801,13 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isTagSyntaxSet() {
@@ -803,7 +820,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isNamingConventionSet() {
@@ -816,7 +834,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isTabSizeSet() {
@@ -829,7 +848,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isLocaleSet() {
@@ -842,7 +862,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isTimeZoneSet() {
@@ -855,7 +876,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isSQLDateAndTimeTimeZoneSet() {
@@ -868,7 +890,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isArithmeticEngineSet() {
@@ -881,7 +904,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isNumberFormatSet() {
@@ -899,7 +923,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isCustomNumberFormatsSet() {
@@ -912,7 +937,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isBooleanFormatSet() {
@@ -925,7 +951,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isTimeFormatSet() {
@@ -938,7 +965,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isDateFormatSet() {
@@ -951,7 +979,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isDateTimeFormatSet() {
@@ -969,7 +998,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isCustomDateFormatsSet() {
@@ -982,7 +1012,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isObjectWrapperSet() {
@@ -995,7 +1026,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isOutputEncodingSet() {
@@ -1008,7 +1040,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isURLEscapingCharsetSet() {
@@ -1021,7 +1054,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isNewBuiltinClassResolverSet() {
@@ -1034,7 +1068,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isAPIBuiltinEnabledSet() {
@@ -1047,7 +1082,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isAutoFlushSet() {
@@ -1060,7 +1096,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isShowErrorTipsSet() {
@@ -1073,7 +1110,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isLogTemplateExceptionsSet() {
@@ -1086,7 +1124,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isLazyImportsSet() {
@@ -1099,7 +1138,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isLazyAutoImportsSet() {
@@ -1112,7 +1152,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isAutoImportsSet() {
@@ -1125,29 +1166,56 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isAutoIncludesSet() {
         return true;
     }
 
+    /**
+     * {@inheritDoc}
+     * <p>
+     * Because {@link Configuration} has on parent, the {@code 
includeInherited} parameter is ignored.
+     */
     @Override
-    public Map<Object, Object> getCustomAttributes() {
+    public Map<Serializable, Object> getCustomAttributesSnapshot(boolean 
includeInherited) {
         return customAttributes;
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * {@inheritDoc}
+     * <p>
+     * Unlike the other isXxxSet methods of {@link Configuration}, this can 
return {@code false}, as at least the
+     * builders in FreeMarker Core can't provide defaults for custom 
attributes. Note that since
+     * {@link #getCustomAttribute(Serializable)} just returns {@code null} for 
unset custom attributes, it's usually not a
+     * problem.
      */
     @Override
-    public boolean isCustomAttributesSet() {
-        return true;
+    public boolean isCustomAttributeSet(Serializable key) {
+        return customAttributes.containsKey(key);
     }
 
     @Override
-    public Object getCustomAttribute(Object key) {
-        return customAttributes.get(key);
+    public Object getCustomAttribute(Serializable key) {
+        return getCustomAttribute(key, null, false);
+    }
+
+    @Override
+    public Object getCustomAttribute(Serializable key, Object defaultValue) {
+        return getCustomAttribute(key, defaultValue, true);
+    }
+
+    private Object getCustomAttribute(Serializable key, Object defaultValue, 
boolean useDefaultValue) {
+        Object value = customAttributes.get(key);
+        if (value != null || customAttributes.containsKey(key)) {
+            return value;
+        }
+        if (useDefaultValue) {
+            return defaultValue;
+        }
+        throw new CustomAttributeNotSetException(key);
     }
 
     @Override
@@ -1174,11 +1242,9 @@ public final class Configuration
     /**
      * 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.
      */
@@ -1356,7 +1422,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isSourceEncodingSet() {
@@ -1414,7 +1481,8 @@ public final class Configuration
     }
 
     /**
-     * Always {@code true} in {@link Configuration}-s, so calling the 
corresponding getter is always safe.
+     * Always {@code true} in {@link Configuration}-s; even if this setting 
wasn't set in the builder, it gets a default
+     * value in the {@link Configuration}.
      */
     @Override
     public boolean isLocalizedLookupSet() {
@@ -2558,15 +2626,17 @@ public final class Configuration
         }
 
         @Override
-        protected Object getDefaultCustomAttribute(Object name) {
-            return Collections.emptyMap();
+        protected Object getDefaultCustomAttribute(Serializable key, Object 
defaultValue, boolean useDefaultValue) {
+            if (useDefaultValue) {
+                return defaultValue;
+            }
+            throw new CustomAttributeNotSetException(key);
         }
 
         @Override
-        protected Map<Object, Object> getDefaultCustomAttributes() {
-            return Collections.emptyMap();
+        protected void 
collectDefaultCustomAttributesSnapshot(Map<Serializable, Object> target) {
+            // Doesn't inherit anything
         }
-
     }
 
     /**

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dc689993/freemarker-core/src/main/java/org/apache/freemarker/core/CustomAttributeNotSetException.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/CustomAttributeNotSetException.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/CustomAttributeNotSetException.java
new file mode 100644
index 0000000..891f928
--- /dev/null
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/CustomAttributeNotSetException.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.io.Serializable;
+
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * Thrown by {@link ProcessingConfiguration#getCustomAttribute(Serializable)} 
if the custom attribute is not set.
+ */
+public class CustomAttributeNotSetException extends 
SettingValueNotSetException {
+
+    private final Serializable key;
+
+    /**
+     * @param key {@link 
ProcessingConfiguration#getCustomAttribute(Serializable)}
+     */
+    public CustomAttributeNotSetException(Serializable key) {
+        super("customAttributes[" + key instanceof String ? 
_StringUtil.jQuote(key) : _StringUtil.tryToString(key) +
+                        "]", false);
+        this.key = key;
+    }
+
+    /**
+     * The argument to {@link 
ProcessingConfiguration#getCustomAttribute(Serializable)}.
+     */
+    public Serializable getKey() {
+        return key;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dc689993/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
index 35ec25d..fa2d696 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java
@@ -92,14 +92,15 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
  * <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)}
+ * <p>
+ * The {@link ProcessingConfiguration} reader methods of this class don't 
throw {@link SettingValueNotSetException}
+ * because unset settings are ultimately inherited from {@link Configuration}.
  */
 public final class Environment extends 
MutableProcessingConfiguration<Environment> implements CustomStateScope {
     
@@ -1091,17 +1092,18 @@ public final class Environment extends 
MutableProcessingConfiguration<Environmen
     }
 
     @Override
-    protected Object getDefaultCustomAttribute(Object name) {
-        return getMainTemplate().getCustomAttribute(name);
+    protected Object getDefaultCustomAttribute(Serializable key, Object 
defaultValue, boolean useDefaultValue) {
+        return useDefaultValue ? getMainTemplate().getCustomAttribute(key, 
defaultValue)
+                : getMainTemplate().getCustomAttribute(key);
     }
 
     @Override
-    protected Map<Object, Object> getDefaultCustomAttributes() {
-        return getMainTemplate().getCustomAttributes();
+    protected void collectDefaultCustomAttributesSnapshot(Map<Serializable, 
Object> target) {
+        target.putAll(getMainTemplate().getCustomAttributesSnapshot(true));
     }
 
     /*
-     * Note that altough it's not allowed to set this setting with the 
<tt>setting</tt> directive, it still must be
+     * Note that although 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.
      */

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dc689993/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 dcf0714..25d6c62 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
@@ -19,6 +19,7 @@
 
 package org.apache.freemarker.core;
 
+import java.io.Serializable;
 import java.nio.charset.Charset;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -26,10 +27,8 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -335,7 +334,9 @@ public abstract class MutableProcessingConfiguration<SelfT 
extends MutableProces
     private Boolean lazyImports;
     private Boolean lazyAutoImports;
     private boolean lazyAutoImportsSet;
-    private Map<Object, Object> customAttributes;
+    private Map<Serializable, Object> customAttributes = 
Collections.emptyMap();
+    /** If {@code false}, we must use copy-on-write behavior for {@link 
#customAttributes}. */
+    private boolean customAttributesModifiable;
 
     /**
      * Creates a new instance. Normally you do not need to use this 
constructor,
@@ -2124,131 +2125,167 @@ public abstract class 
MutableProcessingConfiguration<SelfT extends MutableProces
         return self();
     }
 
-    @Override
-    public Map<Object, Object> getCustomAttributes() {
-        return isCustomAttributesSet() ? customAttributes : 
getDefaultCustomAttributes();
+    /**
+     * Setter pair of {@link #getCustomAttribute(Serializable)}.
+     *
+     * @param key
+     *         The identifier of the the custom attribute; not {@code null}. 
Usually an enum or a {@link String}. Must
+     *         be usable as {@link HashMap} key.
+     * @param value
+     *         The value of the custom attribute. {@code null} is a legal 
attribute value. Thus, setting the value to
+     *         {@code null} doesn't unset (remove) the attribute; use {@link 
#unsetCustomAttribute(Serializable)} for
+     *         that. Also, {@link #MISSING_VALUE_MARKER} is not an allowed 
value.
+     *         The content of the object shouldn't be changed after it was 
added as an attribute (ideally, it should
+     *         be a true immutable object); if you need to change the content, 
certainly you should use the
+     *         {@link CustomStateScope} API.
+     */
+    public void setCustomAttribute(Serializable key, Object value) {
+        _NullArgumentException.check("key", key);
+        if (value == MISSING_VALUE_MARKER) {
+            throw new IllegalArgumentException("MISSING_VALUE_MARKER can't be 
used as attribute value");
+        }
+        ensureCustomAttributesModifiable();
+        customAttributes.put(key, value);
     }
 
-    protected abstract Map<Object,Object> getDefaultCustomAttributes();
-
     /**
-     * Setter pair of {@link #getCustomAttributes()}
-     *
-     * @param customAttributes Not {@code null}. The {@link Map} is copied to 
prevent aliasing problems.
+     * Fluent API equivalent of {@link #setCustomAttribute(Serializable, 
Object)}
      */
-    public void setCustomAttributes(Map<Object, Object> customAttributes) {
-        setCustomAttributes(customAttributes, false);
+    public SelfT customAttribute(Serializable key, Object value) {
+        setCustomAttribute(key, value);
+        return self();
+    }
+
+    @Override
+    public boolean isCustomAttributeSet(Serializable key) {
+        return customAttributes.containsKey(key);
     }
 
     /**
-     * @param validatedImmutableUnchanging
-     *         {@code true} if we know that the 1st argument is already 
validated, immutable, and unchanging (means,
-     *         won't change later because of aliasing).
-     */
-    void setCustomAttributes(Map<Object, Object> customAttributes, boolean 
validatedImmutableUnchanging) {
-        _NullArgumentException.check("customAttributes", customAttributes);
-        if (!validatedImmutableUnchanging) {
-            this.customAttributes = new LinkedHashMap<>(customAttributes); // 
TODO mutable
-        } else {
-            this.customAttributes = customAttributes;
+     * Unset the custom attribute for this {@link ProcessingConfiguration} 
(but not from the parent
+     * {@link ProcessingConfiguration}, from where it will be possibly 
inherited after this), as if
+     * {@link #setCustomAttribute(Serializable, Object)} was never called for 
it on this
+     * {@link ProcessingConfiguration}. Note that this is different than 
setting the custom attribute value to {@code
+     * null}, as then {@link #getCustomAttribute(Serializable)} will just 
return that {@code null}, and won't look for the
+     * attribute in the parent {@link ProcessingConfiguration}.
+     *
+     * @param key As in {@link #getCustomAttribute(Serializable)}
+     */
+    public void unsetCustomAttribute(Serializable key) {
+        if (customAttributesModifiable) {
+            customAttributes.remove(key);
+        } else if (customAttributes.containsKey(key)) {
+            ensureCustomAttributesModifiable();
+            customAttributes.remove(key);
         }
     }
 
-    /**
-     * Fluent API equivalent of {@link #setCustomAttributes(Map)}
-     */
-    public SelfT customAttributes(Map<Object, Object> customAttributes) {
-        setCustomAttributes(customAttributes);
-        return self();
+    @Override
+    public Object getCustomAttribute(Serializable key) throws 
CustomAttributeNotSetException {
+        return getCustomAttribute(key, null, false);
     }
 
     @Override
-    public boolean isCustomAttributesSet() {
-        return customAttributes != null;
+    public Object getCustomAttribute(Serializable key, Object defaultValue) {
+        return getCustomAttribute(key, defaultValue, true);
     }
 
-    boolean isCustomAttributeSet(Object key) {
-         return isCustomAttributesSet() && customAttributes.containsKey(key);
+    private Object getCustomAttribute(Serializable key, Object defaultValue, 
boolean useDefaultValue) {
+        Object value = customAttributes.get(key);
+        if (value != null || customAttributes.containsKey(key)) {
+            return value;
+        }
+        return getDefaultCustomAttribute(key, defaultValue, useDefaultValue);
+    }
+
+    @Override
+    public Map<Serializable, Object> getCustomAttributesSnapshot(boolean 
includeInherited) {
+        if (includeInherited) {
+            LinkedHashMap<Serializable, Object> result = new LinkedHashMap<>();
+            collectDefaultCustomAttributesSnapshot(result);
+            if (!result.isEmpty()) {
+                if (customAttributes != null) {
+                    result.putAll(customAttributes);
+                }
+                return Collections.unmodifiableMap(result);
+            }
+        }
+
+        // When there's no need for inheritance:
+        customAttributesModifiable = false; // Copy-on-write on next 
modification
+        return _CollectionUtil.unmodifiableMap(customAttributes);
     }
 
     /**
-     * Sets a {@linkplain #getCustomAttributes() custom attribute} for this 
configurable.
-     *
-     * @param name
-     *         the name of the custom attribute
-     * @param value
-     *         the value of the custom attribute. You can set the value to 
{@code null}, however note that there is a
-     *         semantic difference between an attribute set to {@code null} 
and an attribute that is not present (see
-     *         {@link #removeCustomAttribute(Object)}).
+     * Called from {@link #getCustomAttributesSnapshot(boolean)}, adds the 
default (such as inherited) custom attributes
+     * to the argument {@link Map}.
      */
-    public void setCustomAttribute(Object name, Object value) {
-        if (customAttributes == null) {
-            customAttributes = new LinkedHashMap<>();
+    protected abstract void 
collectDefaultCustomAttributesSnapshot(Map<Serializable, Object> target);
+
+    private void ensureCustomAttributesModifiable() {
+        if (!customAttributesModifiable) {
+            customAttributes = new LinkedHashMap<>(customAttributes);
+            customAttributesModifiable = true;
         }
-        customAttributes.put(name, value);
     }
 
     /**
-     * Fluent API equivalent of {@link #setCustomAttribute(Object, Object)}
+     * Called be {@link #getCustomAttribute(Serializable)} and {@link 
#getCustomAttribute(Serializable, Object)} if the
+     * attribute wasn't set in the current {@link ProcessingConfiguration}.
+     *
+     * @param useDefaultValue
+     *         If {@code true}, and the attribute is missing, then return 
{@code defaultValue}, otherwise throw {@link
+     *         CustomAttributeNotSetException}.
+     *
+     * @throws CustomAttributeNotSetException
+     *         if the attribute wasn't set in the parents, or has no default 
otherwise, and {@code useDefaultValue} was
+     *         {@code false}.
      */
-    public SelfT customAttribute(Object name, Object value) {
-        setCustomAttribute(name, value);
-        return self();
-    }
+    protected abstract Object getDefaultCustomAttribute(
+            Serializable key, Object defaultValue, boolean useDefaultValue) 
throws CustomAttributeNotSetException;
 
     /**
-     * Returns an array with names of all custom attributes defined directly 
on this {@link ProcessingConfiguration}.
-     * (That is, it doesn't contain the names of custom attributes inherited 
from other {@link
-     * ProcessingConfiguration}-s.) The returned array is never {@code null}, 
but can be zero-length.
+     * Convenience method for calling {@link #setCustomAttribute(Serializable, 
Object)} for each {@link Map} entry.
+     * Note that it won't remove the already existing custom attributes.
      */
-    // TODO env only?
-    // TODO should return List<String>?
-    public String[] getCustomAttributeNames() {
-        if (customAttributes == null) {
-            return _CollectionUtil.EMPTY_STRING_ARRAY;
-        }
-        Collection names = new LinkedList(customAttributes.keySet());
-        for (Iterator iter = names.iterator(); iter.hasNext(); ) {
-            if (!(iter.next() instanceof String)) {
-                iter.remove();
+    public void setCustomAttributes(Map<? extends Serializable, ?> 
customAttributes) {
+        _NullArgumentException.check("customAttributes", customAttributes);
+        for (Object value : customAttributes.values()) {
+            if (value == MISSING_VALUE_MARKER) {
+                throw new IllegalArgumentException("MISSING_VALUE_MARKER can't 
be used as attribute value");
             }
         }
-        return (String[]) names.toArray(new String[names.size()]);
+
+        ensureCustomAttributesModifiable();
+        this.customAttributes.putAll(customAttributes);
+        customAttributesModifiable = true;
     }
-    
+
     /**
-     * Removes a named custom attribute for this configurable. Note that this
-     * is different than setting the custom attribute value to null. If you
-     * set the value to null, {@link #getCustomAttribute(Object)} will return
-     * null, while if you remove the attribute, it will return the value of
-     * the attribute in the parent configurable (if there is a parent 
-     * configurable, that is). 
-     *
-     * @param name the name of the custom attribute
+     * Fluent API equivalent of {@link #setCustomAttributes(Map)}
      */
-    // TODO doesn't work properly, remove?
-    public void removeCustomAttribute(Object name) {
-        if (customAttributes == null) {
-            return;
-        }
-        customAttributes.remove(name);
+    public SelfT customAttributes(Map<Serializable, Object> customAttributes) {
+        setCustomAttributes(customAttributes);
+        return self();
     }
 
-    @Override
-    public Object getCustomAttribute(Object key) {
-        Object value;
-        if (customAttributes != null) {
-            value = customAttributes.get(key);
-            if (value == null && customAttributes.containsKey(key)) {
-                return null;
-            }
-        } else {
-            value = null;
-        }
-        return value != null ? value : getDefaultCustomAttribute(key);
+    /**
+     * Used internally to avoid copying the {@link Map} when we know that its 
content won't change anymore.
+     */
+    void setCustomAttributesMap(Map<Serializable, Object> customAttributes) {
+        _NullArgumentException.check("customAttributes", customAttributes);
+        this.customAttributes = customAttributes;
+        this.customAttributesModifiable = false;
     }
 
-    protected abstract Object getDefaultCustomAttribute(Object name);
+    /**
+     * Unsets all custom attributes which were set in this {@link 
ProcessingConfiguration} (but doesn't unset
+     * those inherited from a parent {@link ProcessingConfiguration}).
+     */
+    public void unsetAllCustomAttributes() {
+        customAttributes = Collections.emptyMap();
+        customAttributesModifiable = false;
+    }
 
     protected final List<String> parseAsList(String text) throws 
GenericParseException {
         return new SettingStringParser(text).parseAsList();

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dc689993/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
index d68ab78..3756b34 100644
--- 
a/freemarker-core/src/main/java/org/apache/freemarker/core/ProcessingConfiguration.java
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ProcessingConfiguration.java
@@ -19,10 +19,12 @@
 
 package org.apache.freemarker.core;
 
+import java.io.Serializable;
 import java.io.Writer;
 import java.nio.charset.Charset;
 import java.text.NumberFormat;
 import java.text.SimpleDateFormat;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -48,6 +50,12 @@ import 
org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory;
 public interface ProcessingConfiguration {
 
     /**
+     * Useful as the default value parameter to 
{#getCustomAttribute(Serializable, Object)}, because this value is not
+     * allowed for custom attributes.
+     */
+    Object MISSING_VALUE_MARKER = new Object();
+
+    /**
      * 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.
      *
@@ -666,39 +674,86 @@ public interface ProcessingConfiguration {
     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.
+     * Retrieves the value of a custom attribute. Custom attributes are 
key-value pairs associated to a {@link
+     * ProcessingConfiguration} object, that the FreeMarker core doesn't try 
to interpret. They are like configuration
+     * settings added dynamically (as opposed to in compilation time), where 
each custom attribute is treated as an
+     * individual setting. So where predefined configuration settings used to 
have {@code isXxxSet}, {@code
+     * unsetXxx}, and {@code setXxx} methods, custom attributes have these 
too, with a key (the identifier of the
+     * custom attribute) as an extra argument (see {@link 
#isCustomAttributeSet(Serializable)},
+     * {@link MutableProcessingConfiguration#setCustomAttribute(Serializable, 
Object)},
+     * {@link 
MutableProcessingConfiguration#unsetCustomAttribute(Serializable)}).
      * <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.
+     * settings from the main {@link Template}, which inherits from the {@link 
Configuration}), this method will search
+     * the custom attribute in the whole inheritance chain, until it finds it.
+     * <p>
+     * To prevent key clashes (and for better performance), it's often a good 
idea to use enums as keys, rather than
+     * {@link String}-s. If {@link String}-s are used for keys (names) by 
components that will be reused on several
+     * places, then to avoid accidental name clashes, the names should use a 
prefix similar to a package name, like
+     * like "com.example.myframework.".
+     * <p>
+     * The values of custom attributes should be immutable, or at least not 
changed after they were added as a
+     * custom attribute value. To store custom state information (such as 
application or framework specific caches)
+     * you should use the methods provided by {@link CustomStateScope} instead.
+     * <p>
+     * The FreeMarker core doesn't provide any means for accessing custom 
attributes from the templates. If a framework
+     * or application needs such functionality, it has to add its own custom 
directives/methods for that. But its
+     * more typical that custom attributes just influence the behavior of 
custom directives/methods without the normal
+     * templates directly accessing them, or that they are just used by the 
framework code that invokes templates.
+     *
+     * @param key
+     *         The identifier (usually an enum or a {@link String}) of the 
custom attribute; not {@code null}; must be
+     *         usable as {@link HashMap} key
+     *
+     * @return The value of the custom attribute; possibly {@code null}, as 
that's a legal attribute value. The content
+     * of the value object shouldn't be changed after it was added as an 
attribute (ideally, it should be an
+     * immutable object); if you need to change the content, certainly you 
should use the {@link CustomStateScope}
+     * API. Note that if the custom attribute was created with 
<tt>&lt;#ftl&nbsp;attributes={...}&gt;</tt>, then this
+     * value is already unwrapped (i.e. it's a <code>String</code>, or a 
<code>List</code>, or a <code>Map</code>,
+     * ...etc., not a FreeMarker specific class).
+     *
+     * @throws CustomAttributeNotSetException if the custom attribute was not 
set (not even to {@code null}), nor in
+     * this {@link ProcessingConfiguration}, nor in another where we inherit 
settings from. Use
+     * {@link #getCustomAttribute(Serializable, Object)} to avoid this 
exception.
      */
-    Map<Object, Object> getCustomAttributes();
+    Object getCustomAttribute(Serializable key) throws 
CustomAttributeNotSetException;
 
     /**
-     * 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}.
+     * Same as {@link #getCustomAttribute(Serializable)}, but instead of 
throwing {@link CustomAttributeNotSetException}
+     * it returns the default value specified as the 2nd argument.
+     *
+     * @param defaultValue
+     *         The value to return if the attribute is not set. Note that an 
attribute that was explicitly set to
+     *         {@code null}, then {@code null} will be returned for it, not 
the default value specified here, since
+     *         the attribute was set. If you want to know if the value was 
set, {@link #MISSING_VALUE_MARKER} can
+     *         be used, as it's guaranteed that an attribute never has that 
value.
      */
-    boolean isCustomAttributesSet();
+    Object getCustomAttribute(Serializable key, Object defaultValue);
 
     /**
-     * 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.
+     * Tells if this custom attribute is set directly in this object (not in 
its parent
+     * {@link ProcessingConfiguration}). If not, then depending on the 
implementing class, reading the custom
+     * attribute might returns the value of the setting from a parent object, 
or returns {@code null}, or throws a
+     * {@link SettingValueNotSetException}. Note that if an attribute was set 
to {@code
+     * null} (as opposed to not set at all) then this method will return 
{@code true}.
+     */
+    boolean isCustomAttributeSet(Serializable key);
+
+    /**
+     * Collects all {@linkplain #getCustomAttribute(Serializable)} custom 
attributes} into a {@link Map}; mostly useful for
+     * debugging and tooling, and is possibly too slow to call very frequently.
      *
-     * @param key
-     *         the identifier (usually a name) of the custom attribute
+     * @param includeInherited
+     *         If {@code false}, only the custom attributes set in this {@link 
ProcessingConfiguration} will be
+     *         collected, otherwise the custom attributes inherited from the 
parent {@link ProcessingConfiguration}-s
+     *         will be too. Note that it's the last that matches the behavior 
of {@link
+     *         #getCustomAttribute(Serializable)}.
      *
-     * @return the value of the custom attribute. Note that if the custom 
attribute was created with
-     * <tt>&lt;#ftl&nbsp;attributes={...}&gt;</tt>, then this value is already 
unwrapped (i.e. it's a
-     * <code>String</code>, or a <code>List</code>, or a <code>Map</code>, 
...etc., not a FreeMarker specific class).
+     * @return An unmodifiable and unchanging {@link Map}; not {@code null}. 
The object identity of keys and values of
+     * this {@link Map} will not change when custom attributes are set/unset 
later (hence it's a snapshot). But, if
+     * a key or value objects are themselves mutable objects, FreeMarker can't 
prevent their content from changing.
+     * You shouldn't change the content of those objects.
      */
-    Object getCustomAttribute(Object key);
+    Map<Serializable, Object> getCustomAttributesSnapshot(boolean 
includeInherited);
 
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dc689993/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
index 1ce895d..c9817cf 100644
--- 
a/freemarker-core/src/main/java/org/apache/freemarker/core/SettingValueNotSetException.java
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/SettingValueNotSetException.java
@@ -21,13 +21,23 @@ package org.apache.freemarker.core;
 
 import org.apache.freemarker.core.util._StringUtil;
 
+/**
+ * Thrown when you try to read a configuration setting which wasn't set and 
isn't inherited from a parent object and has
+ * no default either. Because {@link Configuration} specifies a default value 
for all settings, objects that has a
+ * {@link Configuration} in their inheritance chain (like {@link Environment}, 
{@link Template}) won't throw this.
+ */
 public class SettingValueNotSetException extends IllegalStateException {
 
     private final String settingName;
 
     public SettingValueNotSetException(String settingName) {
-        super("The " + _StringUtil.jQuote(settingName)
+        this(settingName, true);
+    }
+
+    public SettingValueNotSetException(String settingName, boolean 
quoteSettingName) {
+        super("The " + (quoteSettingName ? _StringUtil.jQuote(settingName) : 
settingName)
                 + " setting is not set in this layer and has no default here 
either.");
         this.settingName = settingName;
     }
+
 }

Reply via email to