You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2017/06/01 22:13:29 UTC

[4/5] incubator-freemarker git commit: 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 se

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dc689993/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java b/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java
index e653254..f165990 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java
@@ -55,6 +55,7 @@ import org.apache.freemarker.core.templateresolver.TemplateLookupStrategy;
 import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateResolver;
 import org.apache.freemarker.core.templateresolver.impl.FileTemplateLoader;
 import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.util._CollectionUtil;
 import org.apache.freemarker.core.util._NullArgumentException;
 import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory;
 import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory;
@@ -65,7 +66,6 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
  * <p>
  * Stores an already parsed template, ready to be processed (rendered) for unlimited times, possibly from multiple
  * threads.
- * 
  * <p>
  * Typically, you will use {@link Configuration#getTemplate(String)} to invoke/get {@link Template} objects, so you
  * don't construct them directly. But you can also construct a template from a {@link Reader} or a {@link String} that
@@ -73,13 +73,11 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
  * efficient for later processing, creating a new {@link Template} itself is relatively expensive. So try to re-use
  * {@link Template} objects if possible. {@link Configuration#getTemplate(String)} (and its overloads) does that
  * (caching {@link Template}-s) for you, but the constructor of course doesn't, so it's up to you to solve then.
- * 
  * <p>
- * Objects of this class meant to be handled as immutable and thus thread-safe. However, it has some setter methods for
- * changing FreeMarker settings. Those must not be used while the template is being processed, or if the template object
- * is already accessible from multiple threads. If some templates need different settings that those coming from the
- * shared {@link Configuration}, and you are using {@link Configuration#getTemplate(String)} (or its overloads), then
- * use the {@link Configuration#getTemplateConfigurations() templateConfigurations} setting to achieve that.
+ * The {@link ProcessingConfiguration} reader methods of this class don't throw {@link SettingValueNotSetException}
+ * because unset settings are ultimately inherited from {@link Configuration}.
+ * <p>
+ * Objects of this class are immutable and thus thread-safe.
  */
 // TODO [FM3] Try to make Template serializable for distributed caching. Transient fields will have to be restored.
 public class Template implements ProcessingConfiguration, CustomStateScope {
@@ -96,7 +94,7 @@ public class Template implements ProcessingConfiguration, CustomStateScope {
     private final String sourceName;
     private final ArrayList lines = new ArrayList();
 
-    // TODO [FM3] We want to get rid of these, thenthe same Template object could be reused for different lookups.
+    // TODO [FM3] We want to get rid of these, then the same Template object could be reused for different lookups.
     // Template lookup parameters:
     private final String lookupName;
     private Locale lookupLocale;
@@ -112,8 +110,13 @@ public class Template implements ProcessingConfiguration, CustomStateScope {
     private String defaultNS;
     private Map prefixToNamespaceURILookup = new HashMap();
     private Map namespaceURIToPrefixLookup = new HashMap();
-    private Map<String, Serializable> customAttributes;
-    private transient Map<Object, Object> mergedCustomAttributes;
+    /** Custom attributes specified inside the template with the #ftl directive. Maybe {@code null}. */
+    private Map<String, Serializable> headerCustomAttributes;
+    /**
+     * In case {@link #headerCustomAttributes} is not {@code null} and the {@link TemplateConfiguration} also specifies
+     * custom attributes, this is the two set of custom attributes merged. Otherwise it's {@code null}.
+     */
+    private transient Map<Serializable, Object> tcAndHeaderCustomAttributes;
 
     private AutoEscapingPolicy autoEscapingPolicy;
     // Values from template content that are detected automatically:
@@ -126,6 +129,11 @@ public class Template implements ProcessingConfiguration, CustomStateScope {
     private final ConcurrentHashMap<CustomStateKey, Object> customStateMap = new ConcurrentHashMap<>(0);
 
     /**
+     * Indicates that the Template constructor has run completely.
+     */
+    private boolean writeProtected;
+
+    /**
      * Same as {@link #Template(String, String, Reader, Configuration)} with {@code null} {@code sourceName} parameter.
      */
     public Template(String lookupName, Reader reader, Configuration cfg) throws IOException {
@@ -326,8 +334,26 @@ public class Template implements ProcessingConfiguration, CustomStateScope {
         ltbReader.throwFailure();
         
         _DebuggerService.registerTemplate(this);
-        namespaceURIToPrefixLookup = Collections.unmodifiableMap(namespaceURIToPrefixLookup);
-        prefixToNamespaceURILookup = Collections.unmodifiableMap(prefixToNamespaceURILookup);
+        namespaceURIToPrefixLookup = _CollectionUtil.unmodifiableMap(namespaceURIToPrefixLookup);
+        prefixToNamespaceURILookup = _CollectionUtil.unmodifiableMap(prefixToNamespaceURILookup);
+
+       finishConstruction();
+   }
+
+    /**
+     * {@link Template} is technically mutable (to simplify internals), but it has to be finalized and then write
+     * protected when construction is done.
+     */
+    private void finishConstruction() {
+        headerCustomAttributes = _CollectionUtil.unmodifiableMap(headerCustomAttributes);
+        tcAndHeaderCustomAttributes = _CollectionUtil.unmodifiableMap(tcAndHeaderCustomAttributes);
+        writeProtected = true;
+    }
+
+    private void checkWritable() {
+        if (writeProtected) {
+            throw new IllegalStateException("Template can't be modified anymore");
+        }
     }
 
     /**
@@ -358,12 +384,11 @@ public class Template implements ProcessingConfiguration, CustomStateScope {
                Charset sourceEncoding) {
         Template template;
         try {
-            template = new Template(lookupName, sourceName, new StringReader("X"), config);
+            template = new Template(lookupName, sourceName, new StringReader(""), config, sourceEncoding);
         } catch (IOException e) {
             throw new BugException("Plain text template creation failed", e);
         }
         ((ASTStaticText) template.rootElement).replaceText(content);
-        template.setActualSourceEncoding(sourceEncoding);
 
         _DebuggerService.registerTemplate(template);
 
@@ -612,6 +637,7 @@ public class Template implements ProcessingConfiguration, CustomStateScope {
      *            already gives back text (as opposed to binary data), so no decoding with a charset was needed.
      */
     void setActualSourceEncoding(Charset actualSourceEncoding) {
+        checkWritable();
         this.actualSourceEncoding = actualSourceEncoding;
     }
 
@@ -635,6 +661,7 @@ public class Template implements ProcessingConfiguration, CustomStateScope {
         return customLookupCondition;
     }
 
+    // TODO [FM3] Should not be public, should be final field
     /**
      * Mostly only used internally; setter pair of {@link #getCustomLookupCondition()}. This meant to be called directly
      * after instantiating the template with its constructor, after a successfull lookup that used this condition. So
@@ -682,6 +709,7 @@ public class Template implements ProcessingConfiguration, CustomStateScope {
      * Should be called by the parser, for example to apply the output format specified in the #ftl header.
      */
     void setOutputFormat(OutputFormat outputFormat) {
+        checkWritable();
         this.outputFormat = outputFormat;
     }
     
@@ -701,6 +729,7 @@ public class Template implements ProcessingConfiguration, CustomStateScope {
      * Should be called by the parser, for example to apply the auto escaping policy specified in the #ftl header.
      */
     void setAutoEscapingPolicy(AutoEscapingPolicy autoEscapingPolicy) {
+        checkWritable();
         this.autoEscapingPolicy = autoEscapingPolicy;
     }
     
@@ -1037,65 +1066,103 @@ public class Template implements ProcessingConfiguration, CustomStateScope {
         return tCfg != null && tCfg.isAutoIncludesSet();
     }
 
-    /**
-     * This exists to provide the functionality required by {@link ProcessingConfiguration}, but try not call it
-     * too frequently as it has some overhead compared to an usual getter.
-     */
     @SuppressWarnings({ "unchecked", "rawtypes" })
     @Override
-    public Map<Object, Object> getCustomAttributes() {
-        if (mergedCustomAttributes != null) {
-            return Collections.unmodifiableMap(mergedCustomAttributes);
-        } else if (customAttributes != null) {
-            return (Map) Collections.unmodifiableMap(customAttributes);
-        } else if (tCfg != null && tCfg.isCustomAttributesSet()) {
-            return tCfg.getCustomAttributes();
+    public Map<Serializable, Object> getCustomAttributesSnapshot(boolean includeInherited) {
+        boolean nonInheritedAttrsFinal;
+        Map<? extends Serializable, ? extends Object> nonInheritedAttrs;
+        if (tcAndHeaderCustomAttributes != null) {
+            nonInheritedAttrs = tcAndHeaderCustomAttributes;
+            nonInheritedAttrsFinal = writeProtected;
+        } else if (headerCustomAttributes != null) {
+            nonInheritedAttrs = headerCustomAttributes;
+            nonInheritedAttrsFinal = writeProtected;
+        } else if (tCfg != null) {
+            nonInheritedAttrs =  tCfg.getCustomAttributesSnapshot(false);
+            nonInheritedAttrsFinal = true;
         } else {
-            return cfg.getCustomAttributes();
+            nonInheritedAttrs = Collections.emptyMap();
+            nonInheritedAttrsFinal = true;
         }
+
+        Map<Serializable, Object> inheritedAttrs = includeInherited ? cfg.getCustomAttributesSnapshot(true)
+                : Collections.<Serializable, Object>emptyMap();
+
+        LinkedHashMap<Serializable, Object> mergedAttrs;
+        if (nonInheritedAttrs.isEmpty()) {
+            return inheritedAttrs;
+        } else if (inheritedAttrs.isEmpty() && nonInheritedAttrsFinal) {
+            return (Map) nonInheritedAttrs;
+        } else {
+            LinkedHashMap<Serializable, Object> result = new LinkedHashMap<>(
+                    (inheritedAttrs.size() + nonInheritedAttrs.size()) * 4 / 3 + 1, 0.75f);
+            result.putAll(inheritedAttrs);
+            result.putAll(nonInheritedAttrs);
+            return Collections.unmodifiableMap(result);
+        }
+    }
+
+    @Override
+    public boolean isCustomAttributeSet(Serializable key) {
+        if (tcAndHeaderCustomAttributes != null) {
+            return tcAndHeaderCustomAttributes.containsKey(key);
+        }
+        return headerCustomAttributes != null && headerCustomAttributes.containsKey(key)
+                || tCfg != null && tCfg.isCustomAttributeSet(key);
     }
 
     @Override
-    public boolean isCustomAttributesSet() {
-        return customAttributes != null || tCfg != null && tCfg.isCustomAttributesSet();
+    public Object getCustomAttribute(Serializable key) {
+        return getCustomAttribute(key, null, false);
     }
 
     @Override
-    public Object getCustomAttribute(Object name) {
-        // Extra step for custom attributes specified in the #ftl header:
-        if (mergedCustomAttributes != null) {
-            Object value = mergedCustomAttributes.get(name);
-            if (value != null || mergedCustomAttributes.containsKey(name)) {
+    public Object getCustomAttribute(Serializable key, Object defaultValue) {
+        return getCustomAttribute(key, defaultValue, true);
+    }
+
+    private Object getCustomAttribute(Serializable key, Object defaultValue, boolean useDefaultValue) {
+        if (tcAndHeaderCustomAttributes != null) {
+            Object value = tcAndHeaderCustomAttributes.get(key);
+            if (value != null || tcAndHeaderCustomAttributes.containsKey(key)) {
                 return value;
             }
-        } else if (customAttributes != null) {
-            Object value = customAttributes.get(name);
-            if (value != null || customAttributes.containsKey(name)) {
-                return value;
+        } else {
+            if (headerCustomAttributes != null) {
+                Object value = headerCustomAttributes.get(key);
+                if (value != null || headerCustomAttributes.containsKey(key)) {
+                    return value;
+                }
             }
-        } else if (tCfg != null && tCfg.isCustomAttributesSet()) {
-            Object value = tCfg.getCustomAttributes().get(name);
-            if (value != null || tCfg.getCustomAttributes().containsKey(name)) {
-                return value;
+            if (tCfg != null) {
+                Object value = tCfg.getCustomAttribute(key, MISSING_VALUE_MARKER);
+                if (value != MISSING_VALUE_MARKER) {
+                    return value;
+                }
             }
         }
-        return cfg.getCustomAttribute(name);
+        return useDefaultValue ? cfg.getCustomAttribute(key, defaultValue) : cfg.getCustomAttribute(key);
     }
 
     /**
      * Should be called by the parser, for example to add the attributes specified in the #ftl header.
      */
-    void setCustomAttribute(String attName, Serializable attValue) {
-        if (customAttributes == null) {
-            customAttributes = new LinkedHashMap<>();
+    void setHeaderCustomAttribute(String attName, Serializable attValue) {
+        checkWritable();
+
+        if (headerCustomAttributes == null) {
+            headerCustomAttributes = new LinkedHashMap<>();
         }
-        customAttributes.put(attName, attValue);
+        headerCustomAttributes.put(attName, attValue);
 
-        if (tCfg != null && tCfg.isCustomAttributesSet()) {
-            if (mergedCustomAttributes == null) {
-                mergedCustomAttributes = new LinkedHashMap<>(tCfg.getCustomAttributes());
+        if (tCfg != null) {
+            Map<Serializable, Object> tcCustAttrs = tCfg.getCustomAttributesSnapshot(false);
+            if (!tcCustAttrs.isEmpty()) {
+                if (tcAndHeaderCustomAttributes == null) {
+                    tcAndHeaderCustomAttributes = new LinkedHashMap<>(tcCustAttrs);
+                }
+                tcAndHeaderCustomAttributes.put(attName, attValue);
             }
-            mergedCustomAttributes.put(attName, attValue);
         }
     }
 

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dc689993/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 8a6ccc3..f727002 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
@@ -19,6 +19,7 @@
 package org.apache.freemarker.core;
 
 import java.io.Reader;
+import java.io.Serializable;
 import java.nio.charset.Charset;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -39,9 +40,9 @@ import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory;
  * A partial set of configuration settings used for customizing the {@link Configuration}-level settings for individual
  * {@link Template}-s (or rather, for a group of templates). That it's partial means that you should call the
  * corresponding {@code isXxxSet()} before getting a settings, or else you may cause
- * {@link SettingValueNotSetException}. (The fallback to the {@link Configuration} setting isn't automatic to keep
- * the dependency graph of configuration related beans non-cyclic. As user code seldom reads settings from here anyway,
- * this compromise was chosen.)
+ * {@link SettingValueNotSetException}. (There's no fallback to the {@link Configuration}-level settings to keep the
+ * dependency graph of configuration related beans non-cyclic. As user code seldom reads settings directly from
+ * {@link TemplateConfiguration}-s anyway, this compromise was chosen.)
  * <p>
  * Note on the {@code locale} setting: When used with the standard template loading/caching mechanism ({@link
  * Configuration#getTemplate(String)} and its overloads), localized lookup happens before the {@code locale} specified
@@ -82,7 +83,7 @@ public final class TemplateConfiguration implements ParsingAndProcessingConfigur
     private final Boolean lazyImports;
     private final Boolean lazyAutoImports;
     private final boolean lazyAutoImportsSet;
-    private final Map<Object, Object> customAttributes;
+    private final Map<Serializable, Object> customAttributes;
     
     private final TemplateLanguage templateLanguage;
     private final TagSyntax tagSyntax;
@@ -123,7 +124,7 @@ public final class TemplateConfiguration implements ParsingAndProcessingConfigur
         lazyImports = builder.isLazyImportsSet() ? builder.getLazyImports() : null;
         lazyAutoImportsSet = builder.isLazyAutoImportsSet();
         lazyAutoImports = lazyAutoImportsSet ? builder.getLazyAutoImports() : null;
-        customAttributes = builder.isCustomAttributesSet() ? builder.getCustomAttributes() : null;
+        customAttributes = builder.getCustomAttributesSnapshot(false);
 
         templateLanguage = builder.isTemplateLanguageSet() ? builder.getTemplateLanguage() : null;
         tagSyntax = builder.isTagSyntaxSet() ? builder.getTagSyntax() : null;
@@ -173,24 +174,6 @@ public final class TemplateConfiguration implements ParsingAndProcessingConfigur
         return Collections.unmodifiableList(mergedList);
     }
 
-    /**
-     * For internal usage only, copies the custom attributes set directly on this objects into another
-     * {@link MutableProcessingConfiguration}. The target {@link MutableProcessingConfiguration} is assumed to be not seen be other thread than the current
-     * one yet. (That is, the operation is not synchronized on the target {@link MutableProcessingConfiguration}, only on the source
-     * {@link MutableProcessingConfiguration})
-     */
-    private void copyDirectCustomAttributes(MutableProcessingConfiguration<?> target, boolean overwriteExisting) {
-        if (customAttributes == null) {
-            return;
-        }
-        for (Map.Entry<?, ?> custAttrEnt : customAttributes.entrySet()) {
-            Object custAttrKey = custAttrEnt.getKey();
-            if (overwriteExisting || !target.isCustomAttributeSet(custAttrKey)) {
-                target.setCustomAttribute(custAttrKey, custAttrEnt.getValue());
-            }
-        }
-    }
-
     @Override
     public TagSyntax getTagSyntax() {
         if (!isTagSyntaxSet()) {
@@ -645,29 +628,37 @@ public final class TemplateConfiguration implements ParsingAndProcessingConfigur
         return autoIncludes != null;
     }
 
+    /**
+     * {@inheritDoc}
+     * <p>
+     * Note that the {@code includeInherited} has no effect here, as {@link TemplateConfiguration}-s has no parent.
+     */
     @Override
-    public Map<Object, Object> getCustomAttributes() {
-        if (!isCustomAttributesSet()) {
-            throw new SettingValueNotSetException("customAttributes");
-        }
+    public Map<Serializable, Object> getCustomAttributesSnapshot(boolean includeInherited) {
         return customAttributes;
     }
 
     @Override
-    public boolean isCustomAttributesSet() {
-        return customAttributes != null;
+    public boolean isCustomAttributeSet(Serializable key) {
+        return customAttributes.containsKey(key);
     }
 
     @Override
-    public Object getCustomAttribute(Object name) {
-        Object attValue;
-        if (isCustomAttributesSet()) {
-            attValue = customAttributes.get(name);
-            if (attValue != null || customAttributes.containsKey(name)) {
-                return attValue;
-            }
+    public Object getCustomAttribute(Serializable key) {
+        Object result = getCustomAttribute(key, MISSING_VALUE_MARKER);
+        if (result == MISSING_VALUE_MARKER) {
+            throw new CustomAttributeNotSetException(key);
         }
-        return null;
+        return result;
+    }
+
+    @Override
+    public Object getCustomAttribute(Serializable key, Object defaultValue) {
+        Object attValue = customAttributes.get(key);
+        if (attValue != null || customAttributes.containsKey(key)) {
+            return attValue;
+        }
+        return defaultValue;
     }
 
     public static final class Builder extends MutableParsingAndProcessingConfiguration<Builder>
@@ -813,13 +804,17 @@ public final class TemplateConfiguration implements ParsingAndProcessingConfigur
         }
 
         @Override
-        protected Object getDefaultCustomAttribute(Object name) {
-            return null;
+        protected Object getDefaultCustomAttribute(Serializable key, Object defaultValue, boolean useDefaultValue) {
+            // We don't inherit from anything.
+            if (useDefaultValue) {
+                return defaultValue;
+            }
+            throw new CustomAttributeNotSetException(key);
         }
 
         @Override
-        protected Map<Object, Object> getDefaultCustomAttributes() {
-            throw new SettingValueNotSetException("customAttributes");
+        protected void collectDefaultCustomAttributesSnapshot(Map<Serializable, Object> target) {
+            // We don't inherit from anything.
         }
 
         /**
@@ -939,13 +934,10 @@ public final class TemplateConfiguration implements ParsingAndProcessingConfigur
                         tc.isAutoIncludesSet() ? tc.getAutoIncludes() : null));
             }
 
-            if (tc.isCustomAttributesSet()) {
-                setCustomAttributes(mergeMaps(
-                        isCustomAttributesSet() ? getCustomAttributes() : null,
-                        tc.isCustomAttributesSet() ? tc.getCustomAttributes() : null,
-                        true),
-                        true);
-            }
+            setCustomAttributesMap(mergeMaps(
+                    getCustomAttributesSnapshot(false),
+                    tc.getCustomAttributesSnapshot(false),
+                    true));
         }
 
         @Override

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dc689993/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 bd703af..275f64c 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
@@ -20,6 +20,8 @@
 package org.apache.freemarker.core.util;
 
 import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -109,4 +111,25 @@ public class _CollectionUtil {
         return map;
     }
 
+    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();
+
+    public static boolean isMapKnownToBeUnmodifiable(Map<?, ?> map) {
+        if (map == null) {
+            return true;
+        }
+        Class<? extends Map> mapClass = map.getClass();
+        return mapClass == UNMODIFIABLE_MAP_CLASS_1 || mapClass == UNMODIFIABLE_MAP_CLASS_2;
+    }
+
+    /**
+     * Optimized version of {@link Collections#unmodifiableMap(Map)} (avoids needless wrapping).
+     *
+     * @param map The map to return or wrap if not already unmodifiable, or {@code null} which is silently bypassed.
+     */
+    public static <K, V> Map<K, V> unmodifiableMap(Map<K, V> map) {
+        return isMapKnownToBeUnmodifiable(map) ? map : Collections.unmodifiableMap(map);
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dc689993/freemarker-core/src/main/javacc/FTL.jj
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/javacc/FTL.jj b/freemarker-core/src/main/javacc/FTL.jj
index 0df4035..0be9d81 100644
--- a/freemarker-core/src/main/javacc/FTL.jj
+++ b/freemarker-core/src/main/javacc/FTL.jj
@@ -3971,7 +3971,7 @@ void HeaderElement() :
                                                     + " should implement java.io.Serializable.",
                                                     exp);
                                         }
-                                        template.setCustomAttribute(attName, (Serializable) attValue);
+                                        template.setHeaderCustomAttribute(attName, (Serializable) attValue);
                                 }
                             } catch (TemplateModelException tme) {
                             }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dc689993/freemarker-servlet/src/main/java/org/apache/freemarker/servlet/FreemarkerServlet.java
----------------------------------------------------------------------
diff --git a/freemarker-servlet/src/main/java/org/apache/freemarker/servlet/FreemarkerServlet.java b/freemarker-servlet/src/main/java/org/apache/freemarker/servlet/FreemarkerServlet.java
index 570fe17..f4a4895 100644
--- a/freemarker-servlet/src/main/java/org/apache/freemarker/servlet/FreemarkerServlet.java
+++ b/freemarker-servlet/src/main/java/org/apache/freemarker/servlet/FreemarkerServlet.java
@@ -902,7 +902,7 @@ public class FreemarkerServlet extends HttpServlet {
     }
 
     private ContentType getTemplateSpecificContentType(final Template template) {
-        Object contentTypeAttr = template.getCustomAttribute("content_type");
+        Object contentTypeAttr = template.getCustomAttribute("content_type", null);
         if (contentTypeAttr != null) {
             // Converted with toString() for backward compatibility.
             return new ContentType(contentTypeAttr.toString());