0
# Extensions and Integration
1
2
FreeMarker provides extensive extension capabilities through custom directives, methods, transforms, and integration with external systems like XML/DOM processing and servlet containers.
3
4
## Custom Directives
5
6
Custom directives allow you to extend the FreeMarker template language with new functionality.
7
8
```java { .api }
9
interface TemplateDirectiveModel extends TemplateModel {
10
void execute(Environment env, Map params, TemplateModel[] loopVars,
11
TemplateDirectiveBody body) throws TemplateException, IOException;
12
}
13
14
// Body interface for directive content
15
interface TemplateDirectiveBody {
16
void render(Writer out) throws TemplateException, IOException;
17
}
18
```
19
20
### Custom Directive Example
21
22
```java
23
public class RepeatDirective implements TemplateDirectiveModel {
24
25
@Override
26
public void execute(Environment env, Map params, TemplateModel[] loopVars,
27
TemplateDirectiveBody body) throws TemplateException, IOException {
28
29
// Get the 'count' parameter
30
TemplateNumberModel countModel = (TemplateNumberModel) params.get("count");
31
if (countModel == null) {
32
throw new TemplateModelException("Missing required parameter 'count'");
33
}
34
35
int count = countModel.getAsNumber().intValue();
36
37
// Execute the body 'count' times
38
for (int i = 0; i < count; i++) {
39
// Set loop variable if provided
40
if (loopVars.length > 0) {
41
loopVars[0] = new SimpleNumber(i);
42
}
43
if (loopVars.length > 1) {
44
loopVars[1] = new SimpleScalar(i % 2 == 0 ? "even" : "odd");
45
}
46
47
// Render the body
48
body.render(env.getOut());
49
}
50
}
51
}
52
53
// Usage in template:
54
// <@repeat count=3 ; index, parity>
55
// Item ${index} (${parity})
56
// </@repeat>
57
```
58
59
### Advanced Directive Features
60
61
```java
62
public class ConditionalIncludeDirective implements TemplateDirectiveModel {
63
64
@Override
65
public void execute(Environment env, Map params, TemplateModel[] loopVars,
66
TemplateDirectiveBody body) throws TemplateException, IOException {
67
68
// Get parameters
69
TemplateScalarModel templateName = (TemplateScalarModel) params.get("template");
70
TemplateBooleanModel condition = (TemplateBooleanModel) params.get("if");
71
72
if (templateName == null) {
73
throw new TemplateModelException("Missing required parameter 'template'");
74
}
75
76
// Check condition (default to true if not specified)
77
boolean shouldInclude = condition == null || condition.getAsBoolean();
78
79
if (shouldInclude) {
80
try {
81
// Include the specified template
82
Template template = env.getConfiguration().getTemplate(templateName.getAsString());
83
env.include(template);
84
} catch (IOException e) {
85
throw new TemplateException("Failed to include template: " + templateName.getAsString(), env, e);
86
}
87
} else if (body != null) {
88
// Render alternative content if condition is false
89
body.render(env.getOut());
90
}
91
}
92
}
93
94
// Usage in template:
95
// <@conditionalInclude template="admin-menu.ftl" if=user.isAdmin />
96
// <@conditionalInclude template="special-offer.ftl" if=user.isPremium>
97
// <p>Upgrade to premium to see special offers!</p>
98
// </@conditionalInclude>
99
```
100
101
## Custom Methods
102
103
Implement callable methods that can be invoked from templates.
104
105
```java { .api }
106
interface TemplateMethodModel extends TemplateModel {
107
Object exec(List arguments) throws TemplateModelException;
108
}
109
110
// Enhanced method model with TemplateModel arguments
111
interface TemplateMethodModelEx extends TemplateMethodModel {
112
Object exec(List arguments) throws TemplateModelException;
113
}
114
```
115
116
### Custom Method Examples
117
118
```java
119
// String manipulation method
120
public class CapitalizeMethod implements TemplateMethodModelEx {
121
@Override
122
public Object exec(List arguments) throws TemplateModelException {
123
if (arguments.size() != 1) {
124
throw new TemplateModelException("capitalize method expects exactly 1 argument");
125
}
126
127
TemplateScalarModel arg = (TemplateScalarModel) arguments.get(0);
128
String str = arg.getAsString();
129
130
if (str == null || str.isEmpty()) {
131
return new SimpleScalar("");
132
}
133
134
return new SimpleScalar(Character.toUpperCase(str.charAt(0)) + str.substring(1).toLowerCase());
135
}
136
}
137
138
// Math utility method
139
public class MaxMethod implements TemplateMethodModelEx {
140
@Override
141
public Object exec(List arguments) throws TemplateModelException {
142
if (arguments.size() < 2) {
143
throw new TemplateModelException("max method expects at least 2 arguments");
144
}
145
146
double max = Double.NEGATIVE_INFINITY;
147
for (Object arg : arguments) {
148
TemplateNumberModel num = (TemplateNumberModel) arg;
149
double value = num.getAsNumber().doubleValue();
150
if (value > max) {
151
max = value;
152
}
153
}
154
155
return new SimpleNumber(max);
156
}
157
}
158
159
// Date formatting method
160
public class FormatDateMethod implements TemplateMethodModelEx {
161
@Override
162
public Object exec(List arguments) throws TemplateModelException {
163
if (arguments.size() != 2) {
164
throw new TemplateModelException("formatDate method expects exactly 2 arguments: date and pattern");
165
}
166
167
TemplateDateModel dateModel = (TemplateDateModel) arguments.get(0);
168
TemplateScalarModel patternModel = (TemplateScalarModel) arguments.get(1);
169
170
Date date = dateModel.getAsDate();
171
String pattern = patternModel.getAsString();
172
173
SimpleDateFormat formatter = new SimpleDateFormat(pattern);
174
return new SimpleScalar(formatter.format(date));
175
}
176
}
177
178
// Usage in templates:
179
// ${capitalize("hello world")} -> "Hello world"
180
// ${max(10, 20, 15, 30)} -> 30
181
// ${formatDate(currentDate, "yyyy-MM-dd")} -> "2024-03-15"
182
```
183
184
## Text Transforms
185
186
Transform template content through custom processing.
187
188
```java { .api }
189
interface TemplateTransformModel extends TemplateModel {
190
Writer getWriter(Writer out, Map args) throws TemplateModelException, IOException;
191
}
192
```
193
194
### Built-in Utility Transforms
195
196
FreeMarker provides several built-in utility transforms for common text processing tasks:
197
198
```java { .api }
199
// HTML escaping transform
200
class HtmlEscape implements TemplateTransformModel {
201
HtmlEscape();
202
Writer getWriter(Writer out, Map args) throws TemplateModelException, IOException;
203
}
204
205
// XML escaping transform
206
class XmlEscape implements TemplateTransformModel {
207
XmlEscape();
208
Writer getWriter(Writer out, Map args) throws TemplateModelException, IOException;
209
}
210
211
// Normalize newlines transform
212
class NormalizeNewlines implements TemplateTransformModel {
213
NormalizeNewlines();
214
Writer getWriter(Writer out, Map args) throws TemplateModelException, IOException;
215
}
216
217
// Capture output transform
218
class CaptureOutput implements TemplateTransformModel {
219
CaptureOutput();
220
Writer getWriter(Writer out, Map args) throws TemplateModelException, IOException;
221
}
222
```
223
224
**Usage:**
225
226
```java
227
Configuration cfg = new Configuration(Configuration.VERSION_2_3_34);
228
229
// Register built-in transforms
230
cfg.setSharedVariable("htmlEscape", new HtmlEscape());
231
cfg.setSharedVariable("xmlEscape", new XmlEscape());
232
cfg.setSharedVariable("normalizeNewlines", new NormalizeNewlines());
233
cfg.setSharedVariable("captureOutput", new CaptureOutput());
234
```
235
236
Template usage:
237
```html
238
<#-- HTML escaping -->
239
<@htmlEscape>
240
<p>This & that will be escaped: <b>bold</b></p>
241
</@htmlEscape>
242
243
<#-- XML escaping -->
244
<@xmlEscape>
245
<data value="quotes & brackets < > will be escaped"/>
246
</@xmlEscape>
247
248
<#-- Normalize line endings -->
249
<@normalizeNewlines>
250
Text with mixed
251
line endings
252
</@normalizeNewlines>
253
```
254
255
### Transform Examples
256
257
```java
258
// Upper case transform
259
public class UpperCaseTransform implements TemplateTransformModel {
260
@Override
261
public Writer getWriter(Writer out, Map args) throws TemplateModelException, IOException {
262
return new FilterWriter(out) {
263
@Override
264
public void write(char[] cbuf, int off, int len) throws IOException {
265
String str = new String(cbuf, off, len);
266
out.write(str.toUpperCase());
267
}
268
269
@Override
270
public void write(String str) throws IOException {
271
out.write(str.toUpperCase());
272
}
273
};
274
}
275
}
276
277
// HTML escaping transform
278
public class HtmlEscapeTransform implements TemplateTransformModel {
279
@Override
280
public Writer getWriter(Writer out, Map args) throws TemplateModelException, IOException {
281
return new FilterWriter(out) {
282
@Override
283
public void write(String str) throws IOException {
284
out.write(escapeHtml(str));
285
}
286
287
private String escapeHtml(String str) {
288
return str.replace("&", "&")
289
.replace("<", "<")
290
.replace(">", ">")
291
.replace("\"", """)
292
.replace("'", "'");
293
}
294
};
295
}
296
}
297
298
// Usage in templates:
299
// <@upperCase>hello world</@upperCase> -> HELLO WORLD
300
// <@htmlEscape><script>alert('xss')</script></@htmlEscape> -> <script>alert('xss')</script>
301
```
302
303
## XML and DOM Integration
304
305
### Node Model for DOM Processing
306
307
```java { .api }
308
class NodeModel implements TemplateNodeModel, TemplateHashModel, TemplateSequenceModel, TemplateScalarModel {
309
// Factory methods
310
static NodeModel wrap(Node node);
311
static NodeModel parse(InputSource is) throws SAXException, IOException, ParserConfigurationException;
312
static NodeModel parse(File f) throws SAXException, IOException, ParserConfigurationException;
313
static NodeModel parse(String xml) throws SAXException, IOException, ParserConfigurationException;
314
315
// Node access
316
Node getNode();
317
String getNodeName() throws TemplateModelException;
318
String getNodeType() throws TemplateModelException;
319
TemplateNodeModel getParentNode() throws TemplateModelException;
320
TemplateSequenceModel getChildNodes() throws TemplateModelException;
321
322
// Hash model implementation
323
TemplateModel get(String key) throws TemplateModelException;
324
boolean isEmpty() throws TemplateModelException;
325
326
// Sequence model implementation
327
TemplateModel get(int index) throws TemplateModelException;
328
int size() throws TemplateModelException;
329
330
// Scalar model implementation
331
String getAsString() throws TemplateModelException;
332
333
// XPath support
334
TemplateModel exec(List arguments) throws TemplateModelException;
335
}
336
```
337
338
### DOM Processing Examples
339
340
```java
341
// Parse XML and make available to template
342
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
343
DocumentBuilder builder = factory.newDocumentBuilder();
344
Document doc = builder.parse(new File("data.xml"));
345
346
Map<String, Object> dataModel = new HashMap<>();
347
dataModel.put("doc", NodeModel.wrap(doc));
348
349
// Template usage:
350
// ${doc.documentElement.nodeName} -> root element name
351
// <#list doc.getElementsByTagName("item") as item>
352
// ${item.textContent}
353
// </#list>
354
355
// XPath queries:
356
// ${doc["//item[@id='123']"].textContent}
357
// <#assign items = doc["//item[position() <= 5]"]>
358
```
359
360
### XML Namespace Support
361
362
```java
363
public class NamespaceAwareNodeModel extends NodeModel {
364
private final Map<String, String> namespaces;
365
366
public NamespaceAwareNodeModel(Node node, Map<String, String> namespaces) {
367
super(node);
368
this.namespaces = namespaces;
369
}
370
371
// XPath with namespace support
372
@Override
373
public TemplateModel exec(List arguments) throws TemplateModelException {
374
// Configure XPath with namespace context
375
XPathFactory xpathFactory = XPathFactory.newInstance();
376
XPath xpath = xpathFactory.newXPath();
377
xpath.setNamespaceContext(new SimpleNamespaceContext(namespaces));
378
379
// Execute XPath query
380
String expression = ((TemplateScalarModel) arguments.get(0)).getAsString();
381
// ... XPath execution logic
382
383
return super.exec(arguments);
384
}
385
}
386
```
387
388
## Bean Integration Extensions
389
390
### Static Models Access
391
392
Access static methods and fields from templates:
393
394
```java { .api }
395
class StaticModels implements TemplateHashModel {
396
StaticModels(BeansWrapper wrapper);
397
TemplateModel get(String key) throws TemplateModelException;
398
boolean isEmpty() throws TemplateModelException;
399
}
400
401
// Individual static model for a class
402
class StaticModel implements TemplateHashModel, TemplateScalarModel {
403
StaticModel(Class clazz, BeansWrapper wrapper);
404
TemplateModel get(String key) throws TemplateModelException;
405
boolean isEmpty() throws TemplateModelException;
406
String getAsString() throws TemplateModelException;
407
}
408
```
409
410
Usage example:
411
```java
412
BeansWrapper wrapper = new BeansWrapper(Configuration.VERSION_2_3_34);
413
Map<String, Object> dataModel = new HashMap<>();
414
dataModel.put("statics", wrapper.getStaticModels());
415
416
// Template usage:
417
// ${statics["java.lang.Math"].PI} -> 3.141592653589793
418
// ${statics["java.lang.System"].currentTimeMillis()} -> current timestamp
419
// ${statics["java.util.UUID"].randomUUID()} -> random UUID
420
```
421
422
### Enum Models Access
423
424
Access enum constants from templates:
425
426
```java { .api }
427
class EnumModels implements TemplateHashModel {
428
EnumModels(BeansWrapper wrapper);
429
TemplateModel get(String key) throws TemplateModelException;
430
boolean isEmpty() throws TemplateModelException;
431
}
432
```
433
434
Usage example:
435
```java
436
Map<String, Object> dataModel = new HashMap<>();
437
dataModel.put("enums", wrapper.getEnumModels());
438
439
// Template usage:
440
// ${enums["java.time.DayOfWeek"].MONDAY} -> MONDAY
441
// ${enums["java.util.concurrent.TimeUnit"].SECONDS} -> SECONDS
442
```
443
444
## Servlet Integration
445
446
### Servlet-specific Extensions
447
448
```java { .api }
449
// Servlet context integration
450
class ServletContextHashModel implements TemplateHashModel {
451
ServletContextHashModel(ServletContext sc, ObjectWrapper wrapper);
452
TemplateModel get(String key) throws TemplateModelException;
453
boolean isEmpty() throws TemplateModelException;
454
}
455
456
// HTTP request integration
457
class HttpRequestHashModel implements TemplateHashModel {
458
HttpRequestHashModel(HttpServletRequest request, ObjectWrapper wrapper);
459
TemplateModel get(String key) throws TemplateModelException;
460
boolean isEmpty() throws TemplateModelException;
461
}
462
463
// HTTP session integration
464
class HttpSessionHashModel implements TemplateHashModel {
465
HttpSessionHashModel(HttpSession session, ObjectWrapper wrapper);
466
TemplateModel get(String key) throws TemplateModel;
467
boolean isEmpty() throws TemplateModelException;
468
}
469
```
470
471
### Servlet Usage Example
472
473
```java
474
// In a servlet
475
protected void doGet(HttpServletRequest request, HttpServletResponse response)
476
throws ServletException, IOException {
477
478
Configuration cfg = getConfiguration();
479
480
Map<String, Object> dataModel = new HashMap<>();
481
dataModel.put("request", new HttpRequestHashModel(request, cfg.getObjectWrapper()));
482
dataModel.put("session", new HttpSessionHashModel(request.getSession(), cfg.getObjectWrapper()));
483
dataModel.put("application", new ServletContextHashModel(getServletContext(), cfg.getObjectWrapper()));
484
485
Template template = cfg.getTemplate("page.ftl");
486
response.setContentType("text/html");
487
template.process(dataModel, response.getWriter());
488
}
489
490
// Template usage:
491
// ${request.requestURI} -> current request URI
492
// ${session.id} -> session ID
493
// ${application.serverInfo} -> server information
494
```
495
496
## JSR-223 Scripting Integration
497
498
```java { .api }
499
class FreeMarkerScriptEngine extends AbstractScriptEngine {
500
FreeMarkerScriptEngine();
501
FreeMarkerScriptEngine(Configuration config);
502
503
Object eval(String script, ScriptContext context) throws ScriptException;
504
Object eval(Reader reader, ScriptContext context) throws ScriptException;
505
Bindings createBindings();
506
ScriptEngineFactory getFactory();
507
}
508
509
class FreeMarkerScriptEngineFactory implements ScriptEngineFactory {
510
String getEngineName();
511
String getEngineVersion();
512
List<String> getExtensions();
513
List<String> getMimeTypes();
514
List<String> getNames();
515
String getLanguageName();
516
String getLanguageVersion();
517
ScriptEngine getScriptEngine();
518
}
519
```
520
521
## Extension Registration
522
523
### Global Extension Registration
524
525
```java
526
Configuration cfg = new Configuration(Configuration.VERSION_2_3_34);
527
528
// Register custom directives
529
cfg.setSharedVariable("repeat", new RepeatDirective());
530
cfg.setSharedVariable("conditionalInclude", new ConditionalIncludeDirective());
531
532
// Register custom methods
533
cfg.setSharedVariable("capitalize", new CapitalizeMethod());
534
cfg.setSharedVariable("max", new MaxMethod());
535
cfg.setSharedVariable("formatDate", new FormatDateMethod());
536
537
// Register transforms
538
cfg.setSharedVariable("upperCase", new UpperCaseTransform());
539
cfg.setSharedVariable("htmlEscape", new HtmlEscapeTransform());
540
541
// Register utility objects
542
cfg.setSharedVariable("statics", wrapper.getStaticModels());
543
cfg.setSharedVariable("enums", wrapper.getEnumModels());
544
```
545
546
### Per-Template Extension Registration
547
548
```java
549
Map<String, Object> dataModel = new HashMap<>();
550
551
// Add custom extensions for specific templates
552
dataModel.put("xmlUtils", new XmlUtilityMethods());
553
dataModel.put("dateUtils", new DateUtilityMethods());
554
dataModel.put("stringUtils", new StringUtilityMethods());
555
556
Template template = cfg.getTemplate("advanced.ftl");
557
template.process(dataModel, out);
558
```
559
560
## Extension Best Practices
561
562
### Error Handling in Extensions
563
564
```java
565
public class SafeDirective implements TemplateDirectiveModel {
566
@Override
567
public void execute(Environment env, Map params, TemplateModel[] loopVars,
568
TemplateDirectiveBody body) throws TemplateException, IOException {
569
try {
570
// Directive implementation
571
doExecute(env, params, loopVars, body);
572
} catch (Exception e) {
573
// Log error for debugging
574
logger.error("Error in SafeDirective", e);
575
576
// Provide meaningful error message
577
throw new TemplateException("SafeDirective failed: " + e.getMessage(), env, e);
578
}
579
}
580
}
581
```
582
583
### Parameter Validation
584
585
```java
586
public class ValidatedMethod implements TemplateMethodModelEx {
587
@Override
588
public Object exec(List arguments) throws TemplateModelException {
589
// Validate argument count
590
if (arguments.size() != 2) {
591
throw new TemplateModelException(
592
String.format("Expected 2 arguments, got %d", arguments.size()));
593
}
594
595
// Validate argument types
596
if (!(arguments.get(0) instanceof TemplateScalarModel)) {
597
throw new TemplateModelException("First argument must be a string");
598
}
599
600
if (!(arguments.get(1) instanceof TemplateNumberModel)) {
601
throw new TemplateModelException("Second argument must be a number");
602
}
603
604
// Proceed with implementation
605
return doExec(arguments);
606
}
607
}
608
```
609
610
### Thread Safety Considerations
611
612
```java
613
// Thread-safe extension (no mutable state)
614
public class ThreadSafeMethod implements TemplateMethodModelEx {
615
@Override
616
public Object exec(List arguments) throws TemplateModelException {
617
// Use only local variables and immutable objects
618
String input = ((TemplateScalarModel) arguments.get(0)).getAsString();
619
return new SimpleScalar(processInput(input));
620
}
621
622
private String processInput(String input) {
623
// Thread-safe processing logic
624
return input.toUpperCase();
625
}
626
}
627
628
// For stateful extensions, use ThreadLocal or synchronization
629
public class StatefulMethod implements TemplateMethodModelEx {
630
private final ThreadLocal<SomeState> state = new ThreadLocal<SomeState>() {
631
@Override
632
protected SomeState initialValue() {
633
return new SomeState();
634
}
635
};
636
637
@Override
638
public Object exec(List arguments) throws TemplateModelException {
639
SomeState currentState = state.get();
640
// Use currentState safely
641
return processWithState(currentState, arguments);
642
}
643
}
644
```