Introducción a FreeMarker el Motor de Plantillas en Java

1. Introducción

FreeMarker es un motor de plantillas, escrito en Java y mantenido por la Fundación Apache. Se puede utilizar el Lenguaje de Plantillas de FreeMarker, también conocido como FTL, para generar muchos formatos de texto, como páginas web, correos electrónicos o archivos XML. En este tutorial, veremos qué podemos hacer con FreeMarker de forma predeterminada; sin embargo, es importante señalar que es muy configurable e incluso se integra muy bien con Spring. ¡Vamos a comenzar!

2. Visión General

Para inyectar contenido dinámico en nuestras páginas, necesitamos utilizar una sintaxis que FreeMarker entienda:

  • ${…} dentro de la plantilla se reemplazará en la salida generada con el valor real de la expresión dentro de las llaves. Esto se llama interpolación; algunos ejemplos son ${1 + 2} y ${variableName}.
  • Las etiquetas FTL son como las etiquetas HTML (pero contienen # o @) y FreeMarker las interpreta, por ejemplo, <#if…></#if>.
  • Los comentarios en FreeMarker comienzan con <#– y terminan con –>.

3. La Etiqueta Include

La directiva FTL include es una forma de seguir el principio DRY (Don’t Repeat Yourself) en nuestra aplicación. Definiremos el contenido repetitivo en un archivo y lo reutilizaremos en diferentes plantillas FreeMarker con una única etiqueta include.

Un caso de uso sería incluir la sección del menú en múltiples páginas. Primero, definimos la sección del menú dentro de un archivo llamado menu.ftl con el siguiente contenido:

<a href="#dashboard">Dashboard</a>
<a href="#newEndpoint">Agregar nuevo endpoint</a>

En nuestra página HTML, incluimos el archivo menu.ftl creado:

<!DOCTYPE html>
<html>
<body>
<#include 'fragments/menu.ftl'>
    <h6>Página de Dashboard</h6>
</body>
</html>

También podemos incluir FTL en nuestros fragmentos, lo cual es genial.

4. Manejo de la Existencia de Valores

El FTL considerará cualquier valor null como una ausencia de valor. Por lo tanto, necesitamos tener cuidado y agregar lógica para manejar valores null dentro de nuestra plantilla.

Podemos usar el operador ?? para verificar si un atributo, o propiedad anidada, existe. El resultado es un booleano:

${attribute??}

Así pues, hemos probado el atributo por si es null, pero eso no siempre es suficiente. Ahora definimos un valor predeterminado como una solución para este valor ausente. Para hacer esto, necesitamos colocar el operador ! después del nombre de la variable:

${attribute!'default value'}

Usando paréntesis, podemos envolver muchos atributos anidados.

Por ejemplo, para verificar si el atributo existe y tiene una propiedad anidada con otra propiedad anidada, la envolvemos:

${(attribute.nestedProperty.nestedProperty)??}

Finalmente, poniendo todo juntos, podemos incrustar estos entre contenido estático:

<p>Verificando si la propiedad del estudiante existe: ${student??}</p>
<p>Usando valor por defecto para estudiante ausente: ${student!'John Doe'}</p>
<p>Envolviendo propiedades anidadas del estudiante: ${(student.address.street)??}</p>

Y si el estudiante estuviera null, veríamos:

<p>Verificando si la propiedad del estudiante existe: false</p>
<p>Usando valor por defecto para estudiante ausente: John Doe</p>
<p>Envolviendo propiedades anidadas del estudiante: false</p>

Tenga en cuenta el adicional ?c utilizado después de ??. Lo hicimos para convertir el valor booleano en una cadena legible para humanos.

5. La Etiqueta If-Else

Las estructuras de control están presentes en FreeMarker, y el tradicional if-else probablemente será familiar:

<#if condition>
    <!-- bloque a ejecutar si la condición es verdadera -->
<#elseif condition2>
    <!-- bloque a ejecutar si condition2 es la primera condición verdadera -->
<#elseif condition3>
    <!-- bloque a ejecutar si condition3 es la primera condición verdadera -->
<#else>
    <!-- bloque a ejecutar si ninguna condición es verdadera -->
</#if>

Mientras que las ramas elseif y else son opcionales, las condiciones deben resolverse a un valor booleano.

Para ayudarnos con nuestras evaluaciones, probablemente usaremos uno de los siguientes:

  • x == y para verificar si x es igual a y
  • x != y para devolver true solo si x es diferente de y
  • x lt y significa que x debe ser estrictamente menor que y; también podemos usar < en lugar de lt
  • x gt y se evalúa como true solo si x es estrictamente mayor que y; podemos usar > en lugar de gt
  • x lte y prueba si x es menor o igual que y; la alternativa a lte es <=
  • x gte y prueba si x es mayor o igual que y; la alternativa de gte es >=
  • x?? para verificar la existencia de x
  • sequence?seqContains(x) valida la existencia de x dentro de una secuencia

Es muy importante tener en cuenta que FreeMarker considera `>=` y `>` como caracteres de cierre para una etiqueta FTL. La solución es envolver su uso entre paréntesis o usar gte o gt en su lugar.

Poniendo todo junto, para la siguiente plantilla:

<#if status??>
    <p>${status.reason}</p>
<#else>
    <p>¡Falta el estado!</p>
</#if>

Obtenemos el siguiente HTML:

<!-- Cuando el estado existe -->
<p>404 Not Found</p>

<!-- Cuando el estado está ausente -->
<p>¡Falta el estado!</p>

6. Contenedores de Sub-Variables

En FreeMarker, tenemos tres tipos de contenedores para subvariables:

  • Hashes son una secuencia de pares clave-valor — la clave debe ser única dentro del hash y no tenemos un orden.
  • Secuencias son listas en las que tenemos un índice asociado con cada valor; un hecho notable es que las subvariables pueden ser de diferentes tipos.
  • Colecciones son un caso especial de secuencias donde no podemos acceder al tamaño ni recuperar valores por índice; aún podemos iterarlas con la etiqueta list.

6.1. Iteración de Artículos

Podemos iterar sobre un contenedor de dos maneras básicas. La primera es donde iteramos sobre cada valor y realizamos lógica para cada uno de ellos:

<#list sequence as item>
    <!-- hacer algo con ${item} -->
</#list>

O bien, al iterar un Hash, accediendo tanto a la clave como al valor:

<#list hash as key, value>
    <!-- hacer algo con ${key} y ${value} -->
</#list>

La segunda forma es más poderosa porque también permite definir la lógica que debe suceder en varios pasos en la iteración:

<#list sequence>
    <!-- lógica de una sola vez si la secuencia no está vacía -->
    <#items as item>
        <!-- lógica repetida para cada elemento en la secuencia -->
    </#items>
    <!-- lógica de una sola vez si la secuencia no está vacía -->
<#else>
    <!-- lógica de una sola vez si la secuencia está vacía -->
</#list>

El item representa el nombre de la variable iterada, pero podemos renombrarlo como queramos.

Para un ejemplo práctico, definiremos una plantilla donde listamos algunos estados:

<#list statuses>
    <ul>
    <#items as status>
        <li>${status}</li>
    </#items>
    </ul>
<#else>
    <p>No hay estados disponibles</p>
</#list>

Esto nos devolverá el siguiente HTML cuando nuestro contenedor sea [“200 OK”, “404 Not Found”, “500 Internal Server Error”]:

<ul>
<li>200 OK</li>
<li>404 Not Found</li>
<li>500 Internal Server Error</li>
</ul>

6.2. Manejo de Artículos

Un hash permite dos funciones simples: keys para recuperar solo las claves contenidas y values para recuperar solo los valores.

Una secuencia es más compleja; podemos agrupar las funciones más útiles:

  • chunk y join para obtener una sub-secuencia o combinar dos secuencias.
  • reverse, sort, y sortBy para modificar el orden de los elementos.
  • first y last recuperarán el primer o el último elemento, respectivamente.
  • size representa el número de elementos en la secuencia.
  • seqContains, seqIndexOf, o seqLastIndexOf para buscar un elemento.

7. Manejo de Tipos

FreeMarker viene con una amplia variedad de funciones (built-ins) disponibles para trabajar con objetos. Veamos algunas funciones utilizadas frecuentemente.

7.1. Manejo de Cadenas

  • url y urlPath escaparán la cadena, excepto que urlPath no escapará la barra /.
  • jString, jsString, y jsonString aplicarán las reglas de escapado para Java, Javascript y JSON, respectivamente.
  • capFirst, uncapFirst, upperCase, lowerCase, y capitalize son útiles para cambiar el caso de nuestras cadenas, como implican sus nombres.
  • boolean, date, time, datetime y number son funciones para convertir de cadena a otros tipos.

Ahora usemos algunas de estas funciones:

<p>${'http://myurl.com/?search=Hello World'?urlPath}</p>
<p>${'Usando " en el texto'?jsString}</p>
<p>${'mi valor?upperCase}</p>
<p>${'2019-01-12'?date('yyyy-MM-dd')}</p>

Y la salida para la plantilla anterior será:

<p>http%3A//myurl.com/%3Fsearch%3DHello%20World</p>
<p>MI VALOR</p>
<p>Usando \" en el texto</p>
<p>12.01.2019</p>

Al usar la función date, también hemos pasado el patrón a utilizar para analizar el objeto String. FreeMarker utiliza el formato local a menos que se indique lo contrario, por ejemplo, en la función string disponible para objetos de fecha.

7.2. Manejo de Números

  • round, floor y ceiling pueden ayudar con el redondeo de números.
  • abs devolverá el valor absoluto de un número.
  • string convertirá un número en una cadena. También podemos pasar cuatro formatos de número predefinidos: computer, currency, number, o percent o definir nuestro propio formato, como [“0.###”].

Hagamos una cadena de algunas operaciones matemáticas:

<p>${(7.3?round + 3.4?ceiling + 0.1234)?string('0.##')}</p>
<!-- (7 + 4 + 0.1234) con 2 decimales -->

Y como se esperaba, el valor resultante es 11.12.

7.3. Manejo de Fechas

  • .now representa la fecha y hora actuales.
  • date, time y datetime pueden devolver las secciones de fecha y hora del objeto de fecha-hora.
  • string convertirá fechas-horas a cadenas; también podemos pasar el formato deseado o usar uno predefinido.

Ahora obtendremos el tiempo actual y formatearemos la salida a una cadena que contenga solo las horas y minutos:

<p>${.now?time?string('HH:mm')}</p>

La salida resultante será:

<p>15:39</p>

8. Manejo de Excepciones

Veremos dos formas de manejar excepciones para una plantilla FreeMarker.

La primera forma es usar etiquetas attempt-recover para definir lo que deberíamos intentar ejecutar y un bloque de código que debería ejecutarse en caso de error.

La sintaxis es la siguiente:

<#
attempt>
    <!-- bloque a intentar -->
<#recover>
    <!-- bloque a ejecutar en caso de excepción -->
</#attempt>

Tanto attempt como recover son obligatorias. En caso de un error, se deshace el bloque intentado y se ejecutará solo el código en la sección recover.

Teniendo en cuenta esta sintaxis, definamos nuestra plantilla como:

<p>Preparando la evaluación</p>
<#attempt>
    <p>Atributo es ${attributeWithPossibleValue??}</p>
<#recover>
    <p>Atributo ausente</p>
</#attempt>
<p>Hecho con la evaluación</p>

Cuando attributeWithPossibleValue está ausente, veremos:

<p>Preparando la evaluación</p>
    <p>Atributo ausente</p>
<p>Hecho con la evaluación</p>

Y la salida cuando attributeWithPossibleValue existe es:

<p>Preparando la evaluación</p>
    <p>Atributo es 200 OK</p>
<p>Hecho con la evaluación</p>

La segunda manera es configurar FreeMarker sobre lo que debería suceder en caso de excepciones.

Con Spring Boot, podemos configurar esto fácilmente a través de un archivo de propiedades; aquí hay algunas configuraciones disponibles:

  • spring.freemarker.setting.template_exception_handler=rethrow vuelve a lanzar la excepción.
  • spring.freemarker.setting.template_exception_handler=debug genera la información de la traza de pila al cliente y luego vuelve a lanzar la excepción.
  • spring.freemarker.setting.template_exception_handler=html_debug genera la información de la traza de pila al cliente, formateándola para que sea bien legible en el navegador y luego vuelve a lanzar la excepción.
  • spring.freemarker.setting.template_exception_handler=ignore omite las instrucciones que fallan, permitiendo que la plantilla continúe ejecutándose.
  • spring.freemarker.setting.template_exception_handler=default.

9. Llamando Métodos

A veces queremos llamar a métodos de Java desde nuestras plantillas FreeMarker. Ahora veremos cómo hacerlo.

9.1. Miembros Estáticos

Para comenzar a acceder a miembros estáticos, podríamos actualizar nuestra configuración Global FreeMarker o añadir un tipo de atributo StaticModels al modelo, bajo el nombre de atributo statics:

model.addAttribute("statics", new DefaultObjectWrapperBuilder(new Version("2.3.28"))
    .build().getStaticModels());

Acceder a elementos estáticos es directo.

Primero, importamos los elementos estáticos de nuestra clase usando la etiqueta de asignación, luego decidimos sobre un nombre y, finalmente, la ruta de clase de Java.

Aquí está cómo importaremos la clase Math en nuestra plantilla, mostramos el valor del campo estático PI, y utilizamos el método estático pow:

<#assign MathUtils=statics['java.lang.Math']>
<p>Valor de PI: ${MathUtils.PI}</p>
<p>2*10 es: ${MathUtils.pow(2, 10)}</p>

La salida resultante es:

<p>Valor de PI: 3.142</p>
<p>2*10 es: 1,024</p>

9.2. Miembros de Beans

Los miembros de beans son muy fáciles de acceder: usa el punto (.) y eso es todo.

Para nuestro siguiente ejemplo, añadiremos un objeto Random a nuestro modelo:

model.addAttribute("random", new Random());

En nuestra plantilla FreeMarker, generemos un número aleatorio:

<p>Valor aleatorio: ${random.nextInt()}</p>

Esto provocará una salida similar a:

<p>Valor aleatorio: 1,329,970,768</p>

9.3. Métodos Personalizados

El primer paso para agregar un método personalizado es tener una clase que implemente la interfaz TemplateMethodModelEx de FreeMarker y defina nuestra lógica dentro del método exec:

public class LastCharMethod implements TemplateMethodModelEx {
    public Object exec(List arguments) throws TemplateModelException {
        if (arguments.size() != 1 || StringUtils.isEmpty(arguments.get(0)))
            throw new TemplateModelException("¡Argumentos incorrectos!");
        String argument = arguments.get(0).toString();
        return argument.charAt(argument.length() - 1);
    }
}

Añadiremos una instancia de nuestra nueva clase como atributo en el modelo:

model.addAttribute("lastChar", new LastCharMethod());

El paso siguiente es usar nuestro nuevo método en nuestra plantilla:

<p>Ejemplo de último carácter: ${lastChar('mystring')}</p>

Finalmente, la salida resultante es:

<p>Ejemplo de último carácter: g</p>

10. Conclusión

En este artículo, hemos visto cómo utilizar el motor de plantillas FreeMarker dentro de nuestro proyecto. Nos hemos centrado en operaciones comunes, cómo manipular diferentes objetos y algunos temas más avanzados.

Además de lo que hemos aprendido, aquí hay algunos consejos prácticos para programadores que usan Java y FreeMarker:

  1. Usa el principio DRY: Al definir contenido repetitivo en archivos separados, puedes fácilmente mantener y actualizar tu código sin duplicación innecesaria.
  2. Gestión de errores: No subestimes la importancia de manejar excepciones. Asegúrate de incluir bloques attempt-recover en tu plantilla para una experiencia de usuario más fluida.
  3. Optimiza rendimiento: Presta atención a cómo cargues y proceses las plantillas. Para aplicaciones más grandes, considera usar cachés para mejorar el rendimiento.
  4. Mantén la documentación actualizada: Ten siempre en mente la documentación oficial de FreeMarker. Es un recurso invaluable para resolver dudas y explorar nuevas funcionalidades. Accede a ella aquí.

Con esto, hemos terminado este recorrido por FreeMarker. ¡Feliz codificación!