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());