FreeMarker is a Java-based template engine designed to generate text output (e.g., HTML, XML, emails) by combining templates with dynamic data. It is widely used in web applications, enterprise systems like NetSuite, and other platforms requiring dynamic content generation. FreeMarker’s strength lies in its flexibility, allowing developers to embed logic, perform calculations, and format data within templates.
FreeMarker Syntax Overview
FreeMarker templates are text files (e.g., .ftl or embedded in HTML) containing static text, placeholders for dynamic data, and directives for logic. The syntax is intuitive, using delimiters like ${…} for expressions and <#…> for directives.
1. Basic Interpolation
The ${expression} syntax outputs the value of an expression. For example:
<p>Order Number: ${transaction.tranid}</p>
- Use Case: In a NetSuite email template, ${transaction.tranid} displays the transaction ID (e.g., SO12345).
- Note: The expression must evaluate to a non-null value, or FreeMarker throws an error unless handled (e.g., with ! for defaults).
2. Directives
Directives control template logic, enclosed in <#…> (start) and </#…> (end) for block directives, or single <#…> for standalone ones. Common directives include:
- If/Else:
<#if processingFee != 0>
<p>Processing Fee: ${processingFee?string.currency}</p>
<#else>
<p>No processing fee applied.</p>
</#if>
- List (Loop):
<#list transaction.item as item>
<p>Item: ${item.item}, Amount: ${item.amount}</p>
</#list>
- Iterates over a collection (e.g., transaction.item in NetSuite) to display line items.
- Assign:
<#assign total = 100.50>
<p>Total: ${total}</p>
3. Comments
Comments are ignored during rendering, enclosed in <#– … –>:
<#-- Calculate adjusted subtotal --> <#assign adjustedSubtotal = subtotal - processingFee>
4. Built-ins
Built-ins are functions applied to variables using ? (e.g., variable?built_in). Examples include:
- String Manipulation:
${transaction.tranid?replace("Sales Order ", "")} <#-- Removes "Sales Order " prefix -->
${item.custcol1?lower_case} <#-- Converts to lowercase -->
${item.custcol1?trim} <#-- Removes leading/trailing whitespace -->
- Number Formatting:
${adjustedSubtotal?string.currency} <#-- Formats as $974.75 -->
${adjustedSubtotal?string["#,##0.00"]} <#-- Formats as 974.75 -->
- Null Handling:
${transaction.subtotal!0} <#-- Defaults to 0 if null -->
${transaction.otherrefnum?has_content?then("PO: " + transaction.otherrefnum, "No PO")} <#-- Conditional output -->
5. Null Handling
FreeMarker is strict about null values. To avoid errors, use:
- Default Values: ${variable!default} (e.g., ${transaction.subtotal!0}).
- Has Content Check: variable?has_content (e.g., <#if transaction.subtotal?has_content>…</#if>).
- Null Check: variable?? (e.g., <#if transaction.subtotal??>…</#if>).
Example:
<#assign subtotal = transaction.subtotal?has_content?then(transaction.subtotal?number, 0)>
This assigns 0 if transaction.subtotal is null, preventing arithmetic errors.
Arithmetic Operations in FreeMarker
FreeMarker supports standard arithmetic operations for numbers, which are crucial for calculations like subtotals or tax adjustments in templates. Arithmetic is performed within expressions, typically in <#assign> directives or ${…} interpolations.
1. Supported Operators
- Addition: + (e.g., total + tax)
- Subtraction: – (e.g., subtotal – processingFee)
- Multiplication: * (e.g., quantity * rate)
- Division: / (e.g., total / 2)
- Modulus: % (e.g., index % 2)
Example:
<#assign subtotal = 1000.50>
<#assign processingFee = 25.75>
<#assign adjustedSubtotal = subtotal - processingFee>
<p>Subtotal: ${adjustedSubtotal?string.currency}</p> <#-- Outputs: $974.75 -->
2. Number Conversion
NetSuite fields like transaction.subtotal or item.amount may be strings or complex objects. Use ?number to convert to a number:
<#assign processingFee = item.amount?has_content?then(item.amount?number, 0)>
- Issue: If item.amount is a currency string (e.g., “$25.75”), ?number may fail. Strip currency symbols first:
<#assign amount = item.amount?replace("[^d.]", "", "r")?number>
3. Type Safety
Arithmetic operations require numbers. If a variable is not a number, FreeMarker throws an error. Use checks:
<#assign subtotal = 0>
<#if transaction.subtotal?has_content && transaction.subtotal?string?matches("d*.?d+")>
<#assign subtotal = transaction.subtotal?number>
</#if>
This ensures subtotal is a valid number or defaults to 0.
4. Rounding and Formatting
Use ?round, ?floor, or ?ceiling for rounding, and ?string for formatting:
<#assign price = 123.456>
${price?round} <#-- 123 -->
${price?string["0.00"]} <#-- 123.46 -->
${price?string.currency} <#-- $123.46 -->
5. Example: Arithmetic in NetSuite
<#assign processingFee = 0>
<#list transaction.item as item>
<#if item.custcol1?has_content && item.custcol1?trim?lower_case == "processing fee" && item.amount?has_content>
<#assign processingFee = item.amount?number>
<#break>
</#if>
</#list>
<#assign subtotal = transaction.subtotal?has_content?then(transaction.subtotal?number, 0)>
<#assign adjustedSubtotal = subtotal - processingFee>
<td>${adjustedSubtotal?string.currency}</td>
- Issue: If transaction.subtotal is a complex object, ?number fails. The corrected version (as provided) uses regex to validate strings.
Practical Example: NetSuite Email Template
<html>
<body>
<p>Dear ${transaction.entityname!""},</p>
<p>Thank you for your order (${transaction.tranid?replace("Sales Order ", "")}).</p>
<table>
<#list transaction.item as item>
<#if item_index == 0>
<thead>
<tr>
<th>SKU</th>
<th>Item</th>
<th>Qty</th>
<th>Price</th>
<th>Amount</th>
</tr>
</thead>
</#if>
<#if item.itemtype != "Discount" && item.custcol1?trim?lower_case != "processing fee">
<tr>
<td>${item.custcol1!""}</td>
<td>${item.item!""}</td>
<td>${item.quantity!0}</td>
<td>${item.rate?string.currency}</td>
<td>${item.amount?string.currency}</td>
</tr>
</#if>
</#list>
</table>
<#-- Calculate adjusted subtotal -->
<#assign processingFee = 0>
<#list transaction.item as item>
<#if item.custcol1?has_content && item.custcol1?trim?lower_case == "processing fee" && item.amount?has_content>
<#assign processingFee = item.amount?number>
<#break>
</#if>
</#list>
<#assign subtotal = 0>
<#if transaction.subtotal?has_content>
<#if transaction.subtotal?is_number>
<#assign subtotal = transaction.subtotal>
<#elseif transaction.subtotal?string?matches("d*.?d+")>
<#assign subtotal = transaction.subtotal?number>
</#if>
</#if>
<#assign adjustedSubtotal = subtotal - processingFee>
<table>
<tr>
<td>Subtotal</td>
<td>${adjustedSubtotal?string.currency}</td>
</tr>
<#if processingFee != 0>
<tr>
<td>Processing Fee</td>
<td>${processingFee?string.currency}</td>
</tr>
</#if>
<tr>
<td>Total</td>
<td>${transaction.total?has_content?then(transaction.total?string.currency, "$0.00")}</td>
</tr>
</table>
</body>
</html>