¿Qué es y cómo se utiliza FreeMarker?

¿Qué es y cómo se utiliza FreeMarker?

Esta semana inicié un nuevo reto, medianamente complejo, pero algo nuevo y es que necesitaba clonar y personalizar un tema de FusionAuth. Para muchos, FusionAuth es simplemente una solución de identidad y acceso. Sin embargo, en nuestra organización, se ha convertido en una herramienta fundamental, siendo nuestro proveedor interno de SSO (Single Sign-On), el cual se utiliza como puerta de acceso a diferentes aplicaciones. El objetivo era simple en teoría: modificar el look & feel del sistema para reflejar una estética más alineada con nuestra identidad corporativa. Lo que no anticipé fue el interesante viaje que emprendería con FreeMarker, el motor de plantillas detrás de FusionAuth.

Introducción a FusionAuth y FreeMarker

Antes de profundizar en la práctica, es importante comprender qué es FusionAuth. Es una plataforma de autenticación, autorización y gestión de usuarios. Pero, ¿qué papel juega FreeMarker aquí? Bueno, FusionAuth utiliza FreeMarker para sus temas, lo que permite a los desarrolladores como yo, personalizar "fácilmente" la interfaz de usuario y he dicho que es fácil porque aparentemente lo es y en este artículo aunque la idea inicial era describir como es el proceso de modificacion de plantillas en FusionAuth, pues decidí mejor enfocarme sobre FreeMarker, especialmente después de mi intento fallido de portar las plantillas de FusionAuth a un sistema menos complejo y más fácil de modificar.

Estructura de archivos de los temas de FusionAuth

Dentro de FusionAuth, los temas tienen una estructura específica y uno de los archivos más importantes es _helpers.ftl. Este archivo actúa como una biblioteca, conteniendo diversas macros que se utilizan a través de las diferentes plantillas en toda la aplicación. Esencialmente, actúa como un conjunto de funciones reutilizables que proporcionan funcionalidad a las distintas partes del tema.

El archivo _helpers.ftl se ve algo como esto y es un poco más complejo y extenso:

[#ftl/]
[#setting url_escaping_charset="UTF-8"]
[#-- Below are the main blocks for all of the themeable pages --]
[#-- @ftlvariable name="application" type="io.fusionauth.domain.Application" --]
[#-- @ftlvariable name="bypassTheme" type="boolean" --]
[#-- @ftlvariable name="client_id" type="java.lang.String" --]
[#-- @ftlvariable name="code_challenge" type="java.lang.String" --]
[#-- @ftlvariable name="code_challenge_method" type="java.lang.String" --]
[#-- @ftlvariable name="tenant" type="io.fusionauth.domain.Tenant" --]
[#-- @ftlvariable name="theme" type="io.fusionauth.domain.Theme" --]
[#-- @ftlvariable name="version" type="java.lang.String" --]

[#macro html]
    <!DOCTYPE html>
    <html lang="en">
    [#nested/]
    </html>
[/#macro]

[#macro head title="SSO" description="Single Sign On Service"]
    <head>
        <title>${title}</title>
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <meta name="description" content="${description}">
        <meta name="robots" content="index, follow">
    </head>
[/#macro]

y se inyecta en en un archivo index.ftl de la forma:

[#ftl/]
[#import "_helpers.ftl" as helpers/]

FreeMarker

FreeMarker es una biblioteca y motor de plantillas (template engine) para Java. Se utiliza principalmente en aplicaciones web para generar contenido basado en plantillas y datos proporcionados, aunque también se puede usar en aplicaciones no web.

Las características principales de FreeMarker incluyen:

  1. Separación de lógica y presentación: Al igual que otros motores de plantillas, FreeMarker permite separar la lógica de la aplicación (como el acceso a la base de datos o a la lógica empresarial) de la presentación (como la estructura y diseño de la página).

  2. Flexibilidad: FreeMarker tiene un lenguaje de plantillas poderoso que permite realizar bucles, condicionales, operaciones con textos, números, fechas, entre otros.

  3. Integración: Se integra fácilmente con frameworks populares como Spring MVC, Apache Struts y muchos otros.

  4. Rendimiento: Diseñado para ser rápido y con un bajo consumo de memoria.

  5. Sin dependencias externas: A diferencia de otros motores de plantillas, FreeMarker no tiene dependencias externas (por ejemplo, no necesita JSP o Servlets para funcionar).

Conceptos básicos de FreeMarker

  1. Macros: Estas son similares a las funciones en programación. Permiten reutilizar una serie de instrucciones.

Ejemplo:

<#macro saludo nombre>
  ¡Hola, ${nombre}!
</#macro>

Para usarla:

<@saludo nombre="Mundo"/>
  1. Directivas: Son estructuras de control en FreeMarker. Ejemplos incluyen listas y condicionales.

Ejemplo:

<#list items as item>
  - ${item}
</#list>
  1. Interpolación: Es la forma en que insertamos variables en el texto.

Ejemplo:

La capital de Francia es ${capital}.
  1. Inclusión: Permite incluir el contenido de otra plantilla.

Ejemplo:

<#include "header.ftl">
  1. Comentarios: Sirven para añadir notas que no se renderizan en la salida final.

Ejemplo:

<#-- Este es un comentario y no se mostrará en el HTML final. -->

Montando una aplicación con Java, FreeMarker y Jetty

He sido un poco ingenuo al intentar hacer esto, pero fue un buen experimento, puesto que creí que podría portar las plantillas de FusionAuth a un nuevo sistema, algo más ligero que me permitiera realizar y ver los cambios en tiempo real, y a partir de esta idea es que decidí crear una pequeña aplicacion en Java, usando Maven, FreeMarker y Jetty.

Estructura del proyecto

/proyecto
|-- src/main/
|   |-- java/com/example
|   `-- resources/
|-- pom.xml

Paso 1: Configura un nuevo proyecto Maven

Crea un nuevo directorio para tu proyecto y en él un archivo pom.xml con el siguiente contenido:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>freemarker-live-reload</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <!-- Dependencia para FreeMarker -->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.31</version>
        </dependency>
        <!-- Dependencia para Jetty -->
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-server</artifactId>
            <version>9.4.44.v20210927</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-servlet</artifactId>
            <version>9.4.44.v20210927</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>1.6.0</version>
                <configuration>
                    <mainClass>com.example.Main</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Paso 2: Configura FreeMarker y Jetty

En src/main/java/com/example/Main.java:

package com.example;

import freemarker.template.Configuration;
import freemarker.template.Template;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) throws Exception {
        Server server = new Server(8080);
        ServletContextHandler context = new ServletContextHandler();
        context.setContextPath("/");
        context.addServlet(new ServletHolder(new HelloServlet()), "/");
        server.setHandler(context);
        server.start();
        server.join();
    }

    public static class HelloServlet extends HttpServlet {
        private Configuration cfg;

        public HelloServlet() {
            cfg = new Configuration(Configuration.VERSION_2_3_31);
            cfg.setClassForTemplateLoading(this.getClass(), "/");
            cfg.setCacheStorage(new freemarker.cache.NullCacheStorage());
        }

        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
            try {
                Template template = cfg.getTemplate("index.ftl");
                Map<String, String> data = new HashMap<>();
                data.put("message", "Hola, FreeMarker con recarga en tiempo real!");

                resp.setContentType("text/html");
                PrintWriter writer = resp.getWriter();
                template.process(data, writer);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

Paso 3: Crea la plantilla

En src/main/resources/index.ftl:

<html>
<head>
    <title>Recarga en Tiempo Real</title>
</head>
<body>
<h1>${message}</h1>
</body>
</html>

Paso 4: Ejecutando la aplicación

En la raíz de tu proyecto, ejecuta:

mvn clean compile exec:java

Visita http://localhost:8080 en tu navegador y deberías ver la plantilla. Si realizas cambios en index.ftl y recargas tu navegador, verás los cambios de inmediato.

Pasando parámetros del código a la plantilla

FreeMarker es especialmente potente cuando se trata de manejar datos. Aunque el ejemplo anterior mostraba un simple par de clave-valor, FreeMarker puede manejar estructuras mucho más complejas.

Manejando Listas: Imagina que deseas mostrar una lista de productos en tu plantilla. En Java, podrías tener algo como:

List<String> productos = Arrays.asList("Manzana", "Banana", "Cereza");
Map<String, Object> data = new HashMap<>();
data.put("productos", productos);

En tu plantilla FreeMarker, podrías iterar sobre esta lista de la siguiente manera:

<ul>
<#list productos as producto>
    <li>${producto}</li>
</#list>
</ul>

Mapas Anidados: Ahora, imagina que cada producto tiene un precio. Podrías manejar esto con un mapa anidado:

Map<String, Double> productosConPrecio = new HashMap<>();
productosConPrecio.put("Manzana", 0.50);
productosConPrecio.put("Banana", 0.30);
productosConPrecio.put("Cereza", 0.70);

data.put("productosConPrecio", productosConPrecio);

Y en tu plantilla:

<ul>
<#list productosConPrecio?keys as producto>
    <li>${producto}: $${productosConPrecio[producto]}</li>
</#list>
</ul>

Esta flexibilidad en el manejo de datos es una de las razones por las que FreeMarker es tan popular. Te permite diseñar plantillas que pueden adaptarse a estructuras de datos de cualquier complejidad.

Conclusión

Modificar el aspecto de FusionAuth fue más que un simple reto de diseño; pues hice un deep dive en FreeMarker y aunque no completé totalmente el proyecto debido a la complejidad inherente de las plantillas de FusionAuth, este intento me brindó una nueva apreciación por la potencia y versatilidad de FreeMarker. El viaje fue tanto educativo como inspirador, y aunque hubo momentos de frustración, al final, el conocimiento adquirido fue invaluable.

El problema finalmente fue resuelto, pero no de la forma en que lo había planteado, sino que tuve que usar el modo preview en FusionAuth que permite hacer cambios, guardarlos y ver el resultado en un iframe dentro de su sistema, definitivamente el proceso de desarrollo es mucho más lento ya que es mucho prueba y error, pero nada que un par de días dedicado a esto no me hubiesen permitido lograrlo.

Happy coding! :D


Photo by Dmitriy Demidov on Unsplash

Jack Fiallos

Jack Fiallos

Te gustó este artículo?