Apache FreeMarker is a template engine: a Java library to generate text output based on templates and changing data.
—
FreeMarker provides extensive extension capabilities through custom directives, methods, transforms, and integration with external systems like XML/DOM processing and servlet containers.
Custom directives allow you to extend the FreeMarker template language with new functionality.
interface TemplateDirectiveModel extends TemplateModel {
void execute(Environment env, Map params, TemplateModel[] loopVars,
TemplateDirectiveBody body) throws TemplateException, IOException;
}
// Body interface for directive content
interface TemplateDirectiveBody {
void render(Writer out) throws TemplateException, IOException;
}public class RepeatDirective implements TemplateDirectiveModel {
@Override
public void execute(Environment env, Map params, TemplateModel[] loopVars,
TemplateDirectiveBody body) throws TemplateException, IOException {
// Get the 'count' parameter
TemplateNumberModel countModel = (TemplateNumberModel) params.get("count");
if (countModel == null) {
throw new TemplateModelException("Missing required parameter 'count'");
}
int count = countModel.getAsNumber().intValue();
// Execute the body 'count' times
for (int i = 0; i < count; i++) {
// Set loop variable if provided
if (loopVars.length > 0) {
loopVars[0] = new SimpleNumber(i);
}
if (loopVars.length > 1) {
loopVars[1] = new SimpleScalar(i % 2 == 0 ? "even" : "odd");
}
// Render the body
body.render(env.getOut());
}
}
}
// Usage in template:
// <@repeat count=3 ; index, parity>
// Item ${index} (${parity})
// </@repeat>public class ConditionalIncludeDirective implements TemplateDirectiveModel {
@Override
public void execute(Environment env, Map params, TemplateModel[] loopVars,
TemplateDirectiveBody body) throws TemplateException, IOException {
// Get parameters
TemplateScalarModel templateName = (TemplateScalarModel) params.get("template");
TemplateBooleanModel condition = (TemplateBooleanModel) params.get("if");
if (templateName == null) {
throw new TemplateModelException("Missing required parameter 'template'");
}
// Check condition (default to true if not specified)
boolean shouldInclude = condition == null || condition.getAsBoolean();
if (shouldInclude) {
try {
// Include the specified template
Template template = env.getConfiguration().getTemplate(templateName.getAsString());
env.include(template);
} catch (IOException e) {
throw new TemplateException("Failed to include template: " + templateName.getAsString(), env, e);
}
} else if (body != null) {
// Render alternative content if condition is false
body.render(env.getOut());
}
}
}
// Usage in template:
// <@conditionalInclude template="admin-menu.ftl" if=user.isAdmin />
// <@conditionalInclude template="special-offer.ftl" if=user.isPremium>
// <p>Upgrade to premium to see special offers!</p>
// </@conditionalInclude>Implement callable methods that can be invoked from templates.
interface TemplateMethodModel extends TemplateModel {
Object exec(List arguments) throws TemplateModelException;
}
// Enhanced method model with TemplateModel arguments
interface TemplateMethodModelEx extends TemplateMethodModel {
Object exec(List arguments) throws TemplateModelException;
}// String manipulation method
public class CapitalizeMethod implements TemplateMethodModelEx {
@Override
public Object exec(List arguments) throws TemplateModelException {
if (arguments.size() != 1) {
throw new TemplateModelException("capitalize method expects exactly 1 argument");
}
TemplateScalarModel arg = (TemplateScalarModel) arguments.get(0);
String str = arg.getAsString();
if (str == null || str.isEmpty()) {
return new SimpleScalar("");
}
return new SimpleScalar(Character.toUpperCase(str.charAt(0)) + str.substring(1).toLowerCase());
}
}
// Math utility method
public class MaxMethod implements TemplateMethodModelEx {
@Override
public Object exec(List arguments) throws TemplateModelException {
if (arguments.size() < 2) {
throw new TemplateModelException("max method expects at least 2 arguments");
}
double max = Double.NEGATIVE_INFINITY;
for (Object arg : arguments) {
TemplateNumberModel num = (TemplateNumberModel) arg;
double value = num.getAsNumber().doubleValue();
if (value > max) {
max = value;
}
}
return new SimpleNumber(max);
}
}
// Date formatting method
public class FormatDateMethod implements TemplateMethodModelEx {
@Override
public Object exec(List arguments) throws TemplateModelException {
if (arguments.size() != 2) {
throw new TemplateModelException("formatDate method expects exactly 2 arguments: date and pattern");
}
TemplateDateModel dateModel = (TemplateDateModel) arguments.get(0);
TemplateScalarModel patternModel = (TemplateScalarModel) arguments.get(1);
Date date = dateModel.getAsDate();
String pattern = patternModel.getAsString();
SimpleDateFormat formatter = new SimpleDateFormat(pattern);
return new SimpleScalar(formatter.format(date));
}
}
// Usage in templates:
// ${capitalize("hello world")} -> "Hello world"
// ${max(10, 20, 15, 30)} -> 30
// ${formatDate(currentDate, "yyyy-MM-dd")} -> "2024-03-15"Transform template content through custom processing.
interface TemplateTransformModel extends TemplateModel {
Writer getWriter(Writer out, Map args) throws TemplateModelException, IOException;
}FreeMarker provides several built-in utility transforms for common text processing tasks:
// HTML escaping transform
class HtmlEscape implements TemplateTransformModel {
HtmlEscape();
Writer getWriter(Writer out, Map args) throws TemplateModelException, IOException;
}
// XML escaping transform
class XmlEscape implements TemplateTransformModel {
XmlEscape();
Writer getWriter(Writer out, Map args) throws TemplateModelException, IOException;
}
// Normalize newlines transform
class NormalizeNewlines implements TemplateTransformModel {
NormalizeNewlines();
Writer getWriter(Writer out, Map args) throws TemplateModelException, IOException;
}
// Capture output transform
class CaptureOutput implements TemplateTransformModel {
CaptureOutput();
Writer getWriter(Writer out, Map args) throws TemplateModelException, IOException;
}Usage:
Configuration cfg = new Configuration(Configuration.VERSION_2_3_34);
// Register built-in transforms
cfg.setSharedVariable("htmlEscape", new HtmlEscape());
cfg.setSharedVariable("xmlEscape", new XmlEscape());
cfg.setSharedVariable("normalizeNewlines", new NormalizeNewlines());
cfg.setSharedVariable("captureOutput", new CaptureOutput());Template usage:
<#-- HTML escaping -->
<@htmlEscape>
<p>This & that will be escaped: <b>bold</b></p>
</@htmlEscape>
<#-- XML escaping -->
<@xmlEscape>
<data value="quotes & brackets < > will be escaped"/>
</@xmlEscape>
<#-- Normalize line endings -->
<@normalizeNewlines>
Text with mixed
line endings
</@normalizeNewlines>// Upper case transform
public class UpperCaseTransform implements TemplateTransformModel {
@Override
public Writer getWriter(Writer out, Map args) throws TemplateModelException, IOException {
return new FilterWriter(out) {
@Override
public void write(char[] cbuf, int off, int len) throws IOException {
String str = new String(cbuf, off, len);
out.write(str.toUpperCase());
}
@Override
public void write(String str) throws IOException {
out.write(str.toUpperCase());
}
};
}
}
// HTML escaping transform
public class HtmlEscapeTransform implements TemplateTransformModel {
@Override
public Writer getWriter(Writer out, Map args) throws TemplateModelException, IOException {
return new FilterWriter(out) {
@Override
public void write(String str) throws IOException {
out.write(escapeHtml(str));
}
private String escapeHtml(String str) {
return str.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'");
}
};
}
}
// Usage in templates:
// <@upperCase>hello world</@upperCase> -> HELLO WORLD
// <@htmlEscape><script>alert('xss')</script></@htmlEscape> -> <script>alert('xss')</script>class NodeModel implements TemplateNodeModel, TemplateHashModel, TemplateSequenceModel, TemplateScalarModel {
// Factory methods
static NodeModel wrap(Node node);
static NodeModel parse(InputSource is) throws SAXException, IOException, ParserConfigurationException;
static NodeModel parse(File f) throws SAXException, IOException, ParserConfigurationException;
static NodeModel parse(String xml) throws SAXException, IOException, ParserConfigurationException;
// Node access
Node getNode();
String getNodeName() throws TemplateModelException;
String getNodeType() throws TemplateModelException;
TemplateNodeModel getParentNode() throws TemplateModelException;
TemplateSequenceModel getChildNodes() throws TemplateModelException;
// Hash model implementation
TemplateModel get(String key) throws TemplateModelException;
boolean isEmpty() throws TemplateModelException;
// Sequence model implementation
TemplateModel get(int index) throws TemplateModelException;
int size() throws TemplateModelException;
// Scalar model implementation
String getAsString() throws TemplateModelException;
// XPath support
TemplateModel exec(List arguments) throws TemplateModelException;
}// Parse XML and make available to template
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new File("data.xml"));
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("doc", NodeModel.wrap(doc));
// Template usage:
// ${doc.documentElement.nodeName} -> root element name
// <#list doc.getElementsByTagName("item") as item>
// ${item.textContent}
// </#list>
// XPath queries:
// ${doc["//item[@id='123']"].textContent}
// <#assign items = doc["//item[position() <= 5]"]>public class NamespaceAwareNodeModel extends NodeModel {
private final Map<String, String> namespaces;
public NamespaceAwareNodeModel(Node node, Map<String, String> namespaces) {
super(node);
this.namespaces = namespaces;
}
// XPath with namespace support
@Override
public TemplateModel exec(List arguments) throws TemplateModelException {
// Configure XPath with namespace context
XPathFactory xpathFactory = XPathFactory.newInstance();
XPath xpath = xpathFactory.newXPath();
xpath.setNamespaceContext(new SimpleNamespaceContext(namespaces));
// Execute XPath query
String expression = ((TemplateScalarModel) arguments.get(0)).getAsString();
// ... XPath execution logic
return super.exec(arguments);
}
}Access static methods and fields from templates:
class StaticModels implements TemplateHashModel {
StaticModels(BeansWrapper wrapper);
TemplateModel get(String key) throws TemplateModelException;
boolean isEmpty() throws TemplateModelException;
}
// Individual static model for a class
class StaticModel implements TemplateHashModel, TemplateScalarModel {
StaticModel(Class clazz, BeansWrapper wrapper);
TemplateModel get(String key) throws TemplateModelException;
boolean isEmpty() throws TemplateModelException;
String getAsString() throws TemplateModelException;
}Usage example:
BeansWrapper wrapper = new BeansWrapper(Configuration.VERSION_2_3_34);
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("statics", wrapper.getStaticModels());
// Template usage:
// ${statics["java.lang.Math"].PI} -> 3.141592653589793
// ${statics["java.lang.System"].currentTimeMillis()} -> current timestamp
// ${statics["java.util.UUID"].randomUUID()} -> random UUIDAccess enum constants from templates:
class EnumModels implements TemplateHashModel {
EnumModels(BeansWrapper wrapper);
TemplateModel get(String key) throws TemplateModelException;
boolean isEmpty() throws TemplateModelException;
}Usage example:
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("enums", wrapper.getEnumModels());
// Template usage:
// ${enums["java.time.DayOfWeek"].MONDAY} -> MONDAY
// ${enums["java.util.concurrent.TimeUnit"].SECONDS} -> SECONDS// Servlet context integration
class ServletContextHashModel implements TemplateHashModel {
ServletContextHashModel(ServletContext sc, ObjectWrapper wrapper);
TemplateModel get(String key) throws TemplateModelException;
boolean isEmpty() throws TemplateModelException;
}
// HTTP request integration
class HttpRequestHashModel implements TemplateHashModel {
HttpRequestHashModel(HttpServletRequest request, ObjectWrapper wrapper);
TemplateModel get(String key) throws TemplateModelException;
boolean isEmpty() throws TemplateModelException;
}
// HTTP session integration
class HttpSessionHashModel implements TemplateHashModel {
HttpSessionHashModel(HttpSession session, ObjectWrapper wrapper);
TemplateModel get(String key) throws TemplateModel;
boolean isEmpty() throws TemplateModelException;
}// In a servlet
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Configuration cfg = getConfiguration();
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("request", new HttpRequestHashModel(request, cfg.getObjectWrapper()));
dataModel.put("session", new HttpSessionHashModel(request.getSession(), cfg.getObjectWrapper()));
dataModel.put("application", new ServletContextHashModel(getServletContext(), cfg.getObjectWrapper()));
Template template = cfg.getTemplate("page.ftl");
response.setContentType("text/html");
template.process(dataModel, response.getWriter());
}
// Template usage:
// ${request.requestURI} -> current request URI
// ${session.id} -> session ID
// ${application.serverInfo} -> server informationclass FreeMarkerScriptEngine extends AbstractScriptEngine {
FreeMarkerScriptEngine();
FreeMarkerScriptEngine(Configuration config);
Object eval(String script, ScriptContext context) throws ScriptException;
Object eval(Reader reader, ScriptContext context) throws ScriptException;
Bindings createBindings();
ScriptEngineFactory getFactory();
}
class FreeMarkerScriptEngineFactory implements ScriptEngineFactory {
String getEngineName();
String getEngineVersion();
List<String> getExtensions();
List<String> getMimeTypes();
List<String> getNames();
String getLanguageName();
String getLanguageVersion();
ScriptEngine getScriptEngine();
}Configuration cfg = new Configuration(Configuration.VERSION_2_3_34);
// Register custom directives
cfg.setSharedVariable("repeat", new RepeatDirective());
cfg.setSharedVariable("conditionalInclude", new ConditionalIncludeDirective());
// Register custom methods
cfg.setSharedVariable("capitalize", new CapitalizeMethod());
cfg.setSharedVariable("max", new MaxMethod());
cfg.setSharedVariable("formatDate", new FormatDateMethod());
// Register transforms
cfg.setSharedVariable("upperCase", new UpperCaseTransform());
cfg.setSharedVariable("htmlEscape", new HtmlEscapeTransform());
// Register utility objects
cfg.setSharedVariable("statics", wrapper.getStaticModels());
cfg.setSharedVariable("enums", wrapper.getEnumModels());Map<String, Object> dataModel = new HashMap<>();
// Add custom extensions for specific templates
dataModel.put("xmlUtils", new XmlUtilityMethods());
dataModel.put("dateUtils", new DateUtilityMethods());
dataModel.put("stringUtils", new StringUtilityMethods());
Template template = cfg.getTemplate("advanced.ftl");
template.process(dataModel, out);public class SafeDirective implements TemplateDirectiveModel {
@Override
public void execute(Environment env, Map params, TemplateModel[] loopVars,
TemplateDirectiveBody body) throws TemplateException, IOException {
try {
// Directive implementation
doExecute(env, params, loopVars, body);
} catch (Exception e) {
// Log error for debugging
logger.error("Error in SafeDirective", e);
// Provide meaningful error message
throw new TemplateException("SafeDirective failed: " + e.getMessage(), env, e);
}
}
}public class ValidatedMethod implements TemplateMethodModelEx {
@Override
public Object exec(List arguments) throws TemplateModelException {
// Validate argument count
if (arguments.size() != 2) {
throw new TemplateModelException(
String.format("Expected 2 arguments, got %d", arguments.size()));
}
// Validate argument types
if (!(arguments.get(0) instanceof TemplateScalarModel)) {
throw new TemplateModelException("First argument must be a string");
}
if (!(arguments.get(1) instanceof TemplateNumberModel)) {
throw new TemplateModelException("Second argument must be a number");
}
// Proceed with implementation
return doExec(arguments);
}
}// Thread-safe extension (no mutable state)
public class ThreadSafeMethod implements TemplateMethodModelEx {
@Override
public Object exec(List arguments) throws TemplateModelException {
// Use only local variables and immutable objects
String input = ((TemplateScalarModel) arguments.get(0)).getAsString();
return new SimpleScalar(processInput(input));
}
private String processInput(String input) {
// Thread-safe processing logic
return input.toUpperCase();
}
}
// For stateful extensions, use ThreadLocal or synchronization
public class StatefulMethod implements TemplateMethodModelEx {
private final ThreadLocal<SomeState> state = new ThreadLocal<SomeState>() {
@Override
protected SomeState initialValue() {
return new SomeState();
}
};
@Override
public Object exec(List arguments) throws TemplateModelException {
SomeState currentState = state.get();
// Use currentState safely
return processWithState(currentState, arguments);
}
}Install with Tessl CLI
npx tessl i tessl/maven-org-freemarker--freemarker