Apache FreeMarker is a template engine: a Java library to generate text output based on templates and changing data.
—
FreeMarker's output format system provides automatic escaping and content-type-aware processing to prevent security vulnerabilities like XSS attacks while ensuring proper content formatting.
abstract class OutputFormat {
// Format identification
abstract String getName();
abstract String getMimeType();
abstract boolean isOutputFormatMixingAllowed(OutputFormat otherOutputFormat);
// Escaping capabilities
abstract boolean isAutoEscapedByDefault();
// Format compatibility
boolean isMarkupFormat();
boolean isSourceCodeFormat();
}abstract class MarkupOutputFormat<MO extends TemplateMarkupOutputModel<MO>> extends OutputFormat {
// Markup-specific methods
abstract MO fromPlainTextByEscaping(String textToEsc) throws TemplateModelException;
abstract MO fromMarkup(String markupText) throws TemplateModelException;
abstract MO getEmpty();
// Markup concatenation
abstract MO concat(MO... outputModels) throws TemplateModelException;
// Format information
boolean isAutoEscapedByDefault();
boolean isMarkupFormat();
}
// Common markup format base
abstract class CommonMarkupOutputFormat<MO extends CommonTemplateMarkupOutputModel<MO>>
extends MarkupOutputFormat<MO> {
// Common escaping functionality
abstract String escapePlainText(String plainTextContent);
abstract boolean isLegacyBuiltInBypassed(String builtInName);
}class HTMLOutputFormat extends CommonMarkupOutputFormat<TemplateHTMLOutputModel> {
// Singleton instance
static final HTMLOutputFormat INSTANCE = new HTMLOutputFormat();
// Format identification
String getName();
String getMimeType();
// HTML-specific escaping
String escapePlainText(String plainTextContent);
TemplateHTMLOutputModel fromPlainTextByEscaping(String textToEsc) throws TemplateModelException;
TemplateHTMLOutputModel fromMarkup(String markupText) throws TemplateModelException;
TemplateHTMLOutputModel getEmpty();
// HTML model creation
TemplateHTMLOutputModel fromPlainTextByEscaping(String textToEsc);
TemplateHTMLOutputModel fromMarkup(String markupText);
}
// HTML output model
interface TemplateHTMLOutputModel extends CommonTemplateMarkupOutputModel<TemplateHTMLOutputModel> {
String getMarkupString() throws TemplateModelException;
}class XMLOutputFormat extends CommonMarkupOutputFormat<TemplateXMLOutputModel> {
// Singleton instance
static final XMLOutputFormat INSTANCE = new XMLOutputFormat();
// Format identification
String getName();
String getMimeType();
// XML-specific escaping
String escapePlainText(String plainTextContent);
TemplateXMLOutputModel fromPlainTextByEscaping(String textToEsc) throws TemplateModelException;
TemplateXMLOutputModel fromMarkup(String markupText) throws TemplateModelException;
TemplateXMLOutputModel getEmpty();
}
// XML output model
interface TemplateXMLOutputModel extends CommonTemplateMarkupOutputModel<TemplateXMLOutputModel> {
String getMarkupString() throws TemplateModelException;
}class XHTMLOutputFormat extends HTMLOutputFormat {
// Singleton instance
static final XHTMLOutputFormat INSTANCE = new XHTMLOutputFormat();
// Format identification (inherits most from HTMLOutputFormat)
String getName();
String getMimeType();
}class RTFOutputFormat extends CommonMarkupOutputFormat<TemplateRTFOutputModel> {
// Singleton instance
static final RTFOutputFormat INSTANCE = new RTFOutputFormat();
// Format identification
String getName();
String getMimeType();
// RTF-specific escaping
String escapePlainText(String plainTextContent);
TemplateRTFOutputModel fromPlainTextByEscaping(String textToEsc) throws TemplateModelException;
TemplateRTFOutputModel fromMarkup(String markupText) throws TemplateModelException;
TemplateRTFOutputModel getEmpty();
}class PlainTextOutputFormat extends OutputFormat {
// Singleton instance
static final PlainTextOutputFormat INSTANCE = new PlainTextOutputFormat();
// Format identification
String getName();
String getMimeType();
boolean isAutoEscapedByDefault();
boolean isOutputFormatMixingAllowed(OutputFormat otherOutputFormat);
}class UndefinedOutputFormat extends OutputFormat {
// Singleton instance
static final UndefinedOutputFormat INSTANCE = new UndefinedOutputFormat();
// Format identification
String getName();
String getMimeType();
boolean isAutoEscapedByDefault();
boolean isOutputFormatMixingAllowed(OutputFormat otherOutputFormat);
}Configuration cfg = new Configuration(Configuration.VERSION_2_3_34);
// Set default output format
cfg.setOutputFormat(HTMLOutputFormat.INSTANCE);
// Configure auto-escaping policy
cfg.setAutoEscapingPolicy(Configuration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY);
// Register custom output formats
Set<OutputFormat> customFormats = new HashSet<>();
customFormats.add(new CustomJSONOutputFormat());
cfg.setRegisteredCustomOutputFormats(customFormats);
// Recognize standard file extensions for automatic format selection
cfg.setRecognizeStandardFileExtensions(true);// Configure specific output format for certain templates
TemplateConfiguration htmlConfig = new TemplateConfiguration();
htmlConfig.setOutputFormat(HTMLOutputFormat.INSTANCE);
htmlConfig.setAutoEscapingPolicy(Configuration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY);
cfg.setTemplateConfigurations(
new ConditionalTemplateConfigurationFactory(
new FileExtensionMatcher("html"),
new FirstMatchTemplateConfigurationFactory(htmlConfig)
)
);
// Plain text for email templates
TemplateConfiguration textConfig = new TemplateConfiguration();
textConfig.setOutputFormat(PlainTextOutputFormat.INSTANCE);
cfg.setTemplateConfigurations(
new ConditionalTemplateConfigurationFactory(
new PathGlobMatcher("email/**"),
new FirstMatchTemplateConfigurationFactory(textConfig)
)
);// Auto-escaping policy constants in Configuration
static final int ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY = 0;
static final int ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY = 1;
static final int DISABLE_AUTO_ESCAPING_POLICY = 2;Configuration cfg = new Configuration(Configuration.VERSION_2_3_34);
// Enable auto-escaping only for formats where it's default (like HTML)
cfg.setAutoEscapingPolicy(Configuration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY);
// Enable auto-escaping for all markup formats that support it
cfg.setAutoEscapingPolicy(Configuration.ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY);
// Disable auto-escaping completely (not recommended for security)
cfg.setAutoEscapingPolicy(Configuration.DISABLE_AUTO_ESCAPING_POLICY);interface CommonTemplateMarkupOutputModel<MO extends CommonTemplateMarkupOutputModel<MO>>
extends TemplateMarkupOutputModel<MO> {
String getMarkupString() throws TemplateModelException;
MO concat(MO... outputModels) throws TemplateModelException;
}
// Base markup output model
interface TemplateMarkupOutputModel<MO extends TemplateMarkupOutputModel<MO>>
extends TemplateModel {
MO getOutputFormat();
}// HTML output model implementation
class TemplateHTMLOutputModelImpl implements TemplateHTMLOutputModel {
TemplateHTMLOutputModelImpl(String markupContent, HTMLOutputFormat outputFormat);
String getMarkupString() throws TemplateModelException;
TemplateHTMLOutputModel concat(TemplateHTMLOutputModel... outputModels) throws TemplateModelException;
HTMLOutputFormat getOutputFormat();
}
// XML output model implementation
class TemplateXMLOutputModelImpl implements TemplateXMLOutputModel {
TemplateXMLOutputModelImpl(String markupContent, XMLOutputFormat outputFormat);
String getMarkupString() throws TemplateModelException;
TemplateXMLOutputModel concat(TemplateXMLOutputModel... outputModels) throws TemplateModelException;
XMLOutputFormat getOutputFormat();
}public class JSONOutputFormat extends OutputFormat {
public static final JSONOutputFormat INSTANCE = new JSONOutputFormat();
private JSONOutputFormat() {
// Private constructor for singleton
}
@Override
public String getName() {
return "JSON";
}
@Override
public String getMimeType() {
return "application/json";
}
@Override
public boolean isAutoEscapedByDefault() {
return true;
}
@Override
public boolean isOutputFormatMixingAllowed(OutputFormat otherOutputFormat) {
return otherOutputFormat instanceof JSONOutputFormat ||
otherOutputFormat instanceof PlainTextOutputFormat;
}
}
// JSON markup output format with escaping
public class JSONMarkupOutputFormat extends MarkupOutputFormat<TemplateJSONOutputModel> {
public static final JSONMarkupOutputFormat INSTANCE = new JSONMarkupOutputFormat();
@Override
public TemplateJSONOutputModel fromPlainTextByEscaping(String textToEsc)
throws TemplateModelException {
return new TemplateJSONOutputModelImpl(escapeJSON(textToEsc), this);
}
@Override
public TemplateJSONOutputModel fromMarkup(String markupText)
throws TemplateModelException {
return new TemplateJSONOutputModelImpl(markupText, this);
}
@Override
public TemplateJSONOutputModel getEmpty() {
return new TemplateJSONOutputModelImpl("", this);
}
@Override
public TemplateJSONOutputModel concat(TemplateJSONOutputModel... outputModels)
throws TemplateModelException {
StringBuilder sb = new StringBuilder();
for (TemplateJSONOutputModel model : outputModels) {
sb.append(model.getMarkupString());
}
return new TemplateJSONOutputModelImpl(sb.toString(), this);
}
private String escapeJSON(String text) {
return text.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t")
.replace("\b", "\\b")
.replace("\f", "\\f");
}
@Override
public String getName() {
return "JSON";
}
@Override
public String getMimeType() {
return "application/json";
}
@Override
public boolean isAutoEscapedByDefault() {
return true;
}
@Override
public boolean isOutputFormatMixingAllowed(OutputFormat otherOutputFormat) {
return otherOutputFormat instanceof JSONMarkupOutputFormat ||
otherOutputFormat instanceof PlainTextOutputFormat;
}
}<!-- Template with HTML output format -->
<html>
<head>
<title>${pageTitle}</title>
</head>
<body>
<h1>${heading}</h1>
<p>${userComment}</p> <!-- Automatically escaped for HTML -->
<p>${safeHtmlContent?no_esc}</p> <!-- Bypass escaping when safe -->
</body>
</html>// Data model with potentially dangerous content
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("userInput", "<script>alert('XSS')</script>");
dataModel.put("xmlContent", "<item>Value & \"quoted\"</item>");
// HTML template - userInput will be escaped
// Output: <script>alert('XSS')</script>
// XML template - xmlContent will be escaped for XML
// Output: <item>Value & "quoted"</item>
// Plain text template - no escaping applied
// Output: <script>alert('XSS')</script><!-- Main HTML template -->
<html>
<body>
<script type="application/json">
<#-- Switch to JSON output format for this section -->
<#outputformat "JSON">
{
"userData": "${userData}", <!-- JSON-escaped -->
"message": "${message}" <!-- JSON-escaped -->
}
</#outputformat>
</script>
<div class="content">
${htmlContent} <!-- HTML-escaped -->
</div>
</body>
</html><!-- Check current output format -->
<#if .output_format == "HTML">
<!-- HTML-specific content -->
</#if>
<!-- Get output format name -->
Current format: ${.output_format}
<!-- Force escaping regardless of auto-escaping setting -->
${dangerousContent?esc}
<!-- Bypass escaping (use with caution) -->
${safeMarkup?no_esc}
<!-- Convert to different format -->
${htmlContent?markup_string} <!-- Get raw markup string -->
<!-- Check if content is already escaped -->
<#if content?is_markup_output>
Content is already escaped
</#if><!-- Conditional escaping based on format -->
<#if .output_format?starts_with("HTML")>
${content?html}
<#elseif .output_format == "XML">
${content?xml}
<#elseif .output_format == "RTF">
${content?rtf}
<#else>
${content}
</#if>
<!-- Format-aware concatenation -->
<#assign combined = markup1 + markup2> <!-- Properly combines markup -->
<!-- Convert between formats -->
<#outputformat "XML">
<#assign xmlEscaped = htmlContent?string>
</#outputformat>// Proper configuration for web applications
Configuration cfg = new Configuration(Configuration.VERSION_2_3_34);
cfg.setOutputFormat(HTMLOutputFormat.INSTANCE);
cfg.setAutoEscapingPolicy(Configuration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY);
// Template usage - automatic escaping prevents XSS
// ${userInput} -> automatically HTML-escaped
// ${trustedHtml?no_esc} -> bypasses escaping (use with extreme caution)// Custom output format with CSP-aware escaping
public class CSPAwareHTMLOutputFormat extends HTMLOutputFormat {
@Override
public String escapePlainText(String plainTextContent) {
// Enhanced escaping for Content Security Policy compliance
return super.escapePlainText(plainTextContent)
.replace("javascript:", "blocked:")
.replace("data:", "blocked:")
.replace("vbscript:", "blocked:");
}
}// Wrapper for trusted content that should not be escaped
public class TrustedContent implements TemplateScalarModel, TemplateMarkupOutputModel {
private final String content;
private final OutputFormat outputFormat;
public TrustedContent(String content, OutputFormat outputFormat) {
this.content = content;
this.outputFormat = outputFormat;
}
@Override
public String getAsString() throws TemplateModelException {
return content;
}
@Override
public OutputFormat getOutputFormat() {
return outputFormat;
}
}
// Usage
dataModel.put("trustedHtml", new TrustedContent("<b>Safe HTML</b>", HTMLOutputFormat.INSTANCE));// Pre-configure output formats for better performance
Configuration cfg = new Configuration(Configuration.VERSION_2_3_34);
// Use template configurations for automatic format selection
cfg.setRecognizeStandardFileExtensions(true); // Automatic based on file extension
// Pre-register custom formats
Set<OutputFormat> customFormats = new HashSet<>();
customFormats.add(CustomJSONOutputFormat.INSTANCE);
customFormats.add(CustomXMLOutputFormat.INSTANCE);
cfg.setRegisteredCustomOutputFormats(customFormats);// Efficient escaping for high-throughput applications
public class OptimizedHTMLOutputFormat extends HTMLOutputFormat {
private static final char[] HTML_ESCAPE_CHARS = {'&', '<', '>', '"', '\''};
@Override
public String escapePlainText(String plainTextContent) {
// Optimized escaping implementation
if (plainTextContent == null || plainTextContent.isEmpty()) {
return plainTextContent;
}
// Quick check if escaping is needed
boolean needsEscaping = false;
for (char c : HTML_ESCAPE_CHARS) {
if (plainTextContent.indexOf(c) != -1) {
needsEscaping = true;
break;
}
}
if (!needsEscaping) {
return plainTextContent;
}
// Perform escaping only when necessary
return super.escapePlainText(plainTextContent);
}
}Install with Tessl CLI
npx tessl i tessl/maven-org-freemarker--freemarker