or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

caching-loading.mdcore-processing.mdexception-handling.mdextensions.mdindex.mdobject-wrapping.mdoutput-formats.mdtemplate-models.md

extensions.mddocs/

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("&", "&amp;")

289

.replace("<", "&lt;")

290

.replace(">", "&gt;")

291

.replace("\"", "&quot;")

292

.replace("'", "&#39;");

293

}

294

};

295

}

296

}

297

298

// Usage in templates:

299

// <@upperCase>hello world</@upperCase> -> HELLO WORLD

300

// <@htmlEscape><script>alert('xss')</script></@htmlEscape> -> &lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;

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

```