AEM With Freemarker Templating Language

AEM With Freemarker Templating Language

Not every developer in the AEM space is overwhelmed by enthusiasm when choosing between sightly or jsp. While the masses argue about the advantages of one over the other, everyone can agree neither is perfect. Whether turned off by the adolescence of Sightly or the rampant use of scriptlet that can make JSPs unreadable (and difficult to maintain), you might be looking for something new to implement. Fortunately, AEM gives us the power to leverage different templating languages with just a few custom java classes.

Freemarker?

While these steps can be replicated for any type of script rendering, I will be making references specifically the Freemarker Templating Language. If it is something you are unfamiliar with, I highly recommended reading about what it has to offer. I think you will be pleasantly surprised by how robust of a templating language it is.

Part I: Creating a Custom Script Engine

Step 1 - Maven Dependency

This first step should be the easiest. You'll need to add the Freemarker dependency to your POM file. Something along the lines of:

<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.23</version>
</dependency>

Congratulations! You are well on your way to creating templates with Freemarker within AEM!

Step 2 - The Script Engine

Before we can play with our fun new templating language, the second step in the process is to create a script engine and a script engine factory. To create the engine we need to create a class that extends the org.apache.sling.scripting.api.AbstractSlingScriptEngine. Following the class creation, we will need to override a constructor, and a single method, eval(). Here is what a simple instance of the class would look like:

public class FreemarkerScriptEngine extends AbstractSlingScriptEngine {
private static final Configuration CONFIG = new Configuration(null);

    public FreemarkerScriptEngine(ScriptEngineFactory factory) {
        super(factory);
    }

    public Object eval(Reader reader, ScriptContext scriptContext) throws ScriptException {
        Bindings bindings = scriptContext.getBindings(ScriptContext.ENGINE_SCOPE);
        SlingScriptHelper helper = (SlingScriptHelper) bindings.get(SlingBindings.SLING);
        String scriptName = helper.getScript().getScriptResource().getPath();
        try {
            Template tmpl = new Template(scriptName, reader, CONFIG);
            tmpl.process(bindings, scriptContext.getWriter());
        } catch (Exception e) {
            throw new ScriptException(e);
        }
        return null;
    }
}

if you took the time to review Freemarker's documentation, you'll notice that you can invoke the processing of the template directly (as demonstrated above), or you can utilize (and extend) the FreemarkerServlet to process the template for you. I'll discuss WHY you should extend the FreemarkerServlet a bit later in this post. Now that the script engine is created, you need a way to register that engine with AEM so it recognizes how to process the template file requests.

Step 3 - The Script Engine Factory

We need to create a class that is an OSGi service and will extend the org.apache.sling.scripting.api.AbstractSlingScriptEngineFactory to get the job done. Let's take this class step by step. First, the component declaration and the constructor:

@Component
@Service({ScriptEngineFactory.class})
public class FreemarkerScriptEngineFactory extends AbstractScriptEngineFactory {
    public FreemarkerScriptEngineFactory() {
        setExtensions("ftl");
        setMimeTypes("text/x-freemarker");
        setNames("freemarker");
    }

First we need to set the file extension that we will be using for our freemarker components. This will generally be ftl (though note that this extension is arbitrary. Whatever you deem appropriate for your freemarker file extension can be used so long as the extension in the script engine factory mirrors your freemarker components). Next the mimetype and short name will need to be set. While the short name is a vanity property, it should still be configured as above. We now have a few methods to override. Again, here is an example:

@Override
public ScriptEngine getScriptEngine() {
    return new FreemarkerScriptEngine(this);
}

@Override
public String getLanguageName() {
    return "Freemarker";
}

@Override
public String getLanguageVersion() {
    return "2.3.23";
}

The getScriptEngine() method is notably the most important. This is where we return an instance of the Script Engine that we created in Step 2. The other two methods just provide some metadata. Once you have the Script Engine and the Script Engine Factory classes created, you can install these into your AEM instance however you see fit.

Note that when building your script engine factory, I recommend leveraging the OSGi lifecycle methods and configurations for managing data within the class, as you would for any other service. I would NOT copy this example verbatim.

Now lets verify script engine has been successfully installed. The system console provides a convenient way to tell if our script engine has been registered. Go to /system/console/status-slingscripting and you should see the Freemarker script engine defined:

Well that's it! You can go ahead and start creating (painfully simple) components using Freemarker! Unfortunately, you won't be able to build anything useful at the moment. You want to setup an extension of the FreemarkerServlet along with some other configurations. Here's why:

  1. The only implicit objects you can access from the component .ftl are the ones already existing in the Bindings object. That's not really enough to do anything useful.

  2. The FreemarkerServlet is required to reference and include .ftl files within other .ftl files. (which will require a custom ContextLoader). Achieving this will allow us to craft components along the same lines we are used to with jsp's.

  3. The FreemarkerServlet inserts the JspTaglibs hash into the data-model, which you can use to access JSP taglibs.

  4. The ContextLoader alongside the FreemarkerServlet implements a means to cache template files, which is very beneficial for performance.

Check out the Freemarker Template Language Reference. The built in's and directives for Freemarker if you haven't. They will quickly make you forget about JSTL and EL functions.

Part II: Leveraging the FreemarkerServlet

Now that our script engine is registered we are ready to implement something that is actually useful. It would be beneficial to read up on the FreemarkerServlet before continuing.

The implementation I am about to provide is meant to give insight on how all of the pieces communicate together. It is a purposefully minimalist approach, and as such, has intentional flaws that should be redesigned.

Step 1 - Extending the FreemarkerServlet

Since createTemplateLoader() is called from the initialize() method of the servlet, the easiest approach is to create both the AEMFreemarkerServlet and the AEMTemplateLoader as OSGi services so we can easily reference the instance of template loader within the servlet. Thus, our code would look similar to:

@Reference
TemplateLoader aemTemplateLoader;

@Override
protected TemplateLoader createTemplateLoader(String templatePath) throws IOException {
    return aemTemplateLoader;
}

We also want to override the preTemplateProcess() method. This is the last chance we have to make any changes before the template gets processed (go figure). What we want to do here is set some objects as request attributes so they can be accessed within our .ftl files:

@Override
protected boolean preTemplateProcess(HttpServletRequest request, HttpServletResponse response, Template template, TemplateModel data) throws ServletException, IOException {
    if (request instanceof SlingHttpServletRequest) {
        SlingHttpServletRequest req = (SlingHttpServletRequest) request;
        Resource resource = req.getResource();
        request.setAttribute("resource", resource);
        request.setAttribute("properties", resource.adaptTo(ValueMap.class));
        request.setAttribute("slingResponse", response);
        request.setAttribute("slingRequest", req);
    }
    return super.preTemplateProcess(request, response, template, data);
}

Anything set as a request attribute becomes an implicit object that gets binded to the template. Now your .ftl file can reference the requested resource like ${resource} or a property like ${properties.key}. On to the template loader!

Step 2 - Implementing a Template Loader

This class will be critical for loading .ftl files within AEM. While Freemarker provides several template loaders (file system, ClassLoader, and the ServletContext) none are useful for the JCR. This is where we will define the logic for determining which script file to load. Further down the road I would advise handling component inheritance here for determining which .ftl to load in overlayed or extended components. That's beyond scope here though.

There are four methods to implement here in order for our template loader to work properly. First, here is a description of the those methods:

public Object findTemplateSource(String path):
It takes a String to the location of the script, and returns any ol' Object. The fact that an Object is returned is excellent for us as we can take the path to the resource, use a ResourceResolverFactory (we created this as an OSGi service, remember?), and return the resource. This resource will be the object that gets passed into some of the other methods in the Template Loader.

In a real implementation you would NOT want to use ResourceResolverFactory. The ResourceResolver will need to come from the request, which would require a bit of re-engineering. This approach was taken just to provide a working example.

public long getLastModified(Object templateSource): One nice thing about Freemarker is that it manages its own caching of scripts. This modified date is collected each time the script is loaded. If the long returned here matches the last modified as maintained in the cache, FreemarkerServlet will use the cached version of the script. Since the templateSource object is our script resource, we can easily get the last modified date (jcr:lastModified property).

public Reader getReader(Object templateSource, String encoding): This Reader is an instance of the script that the Freemarker engine can understand and process. Thankfully Resources can be adapted to InputStreams. All we need to do is get the jcr:content node of the resource, adapt it to an InputStream, and return a new InputStreamReader. We won't concern ourselves with the encoding parameter here.

public void closeTemplateSource(Object templateSource): This is the end of the line for our template loader. In this example, we can go ahead and get the ResourceResolver from the templateSource and close it.

And here is a sample TemplateLoader (javadocs and comments mostly removed for readability). Again, use the Session/ResourceResolver from the request in your implementation. Do not use the SlingRepository or ResourceResolverFactory outside of testing

@Properties({
    @Property(name = "service.description", value = "This an AEM implementation of a TemplateLoader for Freemarker")    })
@Component(immediate = true)
@Service(AEMTemplateLoader.class)
public class AEMTemplateLoader implements TemplateLoader {
    private static final Logger LOG = Logger.getLogger(AEMFreemarkerServlet.class.getName());

    @Reference
    SlingRepository repository;

    @Reference
    ResourceResolverFactory  rrf;

    //TODO: Refactor this class to use a session/resourceResolver from the request
    @Override
    public Object findTemplateSource(String path) throws IOException {
        Session session = null;
        try {
            session = repository.loginAdministrative(null);
            ResourceResolver resourceResolver = getResourceResolver(session);
            return resourceResolver.getResource(path);
        } catch (Exception e) {
            if (session != null) {
                session.logout();
                session = null;
            }
            throw new IOException(e);
        }
    }

    @Override
    public long getLastModified(Object templateSource) {
        if (templateSource instanceof Resource) {
            Resource resource = (Resource) templateSource;
            Resource jcrContent = resource.getChild("jcr:content");
            if (jcrContent != null) {
                try {
                    Calendar lastModified = JcrUtils.getLastModified(jcrContent.adaptTo(Node.class));
                    return lastModified.getTimeInMillis();
                } catch (RepositoryException e) {
                    LOG.error("Unable to determine last modified from resource" + jcrContent.getPath(), e);
                }
            }

        }
        return 0;
    }

    @Override
    public Reader getReader(Object templateSource, String encoding) throws IOException {
        if (templateSource instanceof Resource) {
            Resource resource = (Resource) templateSource;
            Resource jcrContent = resource.getChild("jcr:content");
            if (jcrContent != null) {
                InputStream is = resource.adaptTo(InputStream.class);
                LOG.debug("Returning Input Stream as reader");
                return new InputStreamReader(is);
            }
        }
        return null;
    }

    @Override
    public void closeTemplateSource(Object templateSource) throws IOException {
        if (templateSource instanceof Resource) {
            Resource resource = (Resource) templateSource;
            ResourceResolver rr = resource.getResourceResolver();
            if (rr != null) {
                if (rr.isLive()) {
                    rr.close();
                }
                rr = null;
            }
        }
    }

    //Just a convenience method for getting a resource resolver
    private ResourceResolver getResourceResolver(Session session) throws LoginException {
        final Map<String, Object> map = new HashMap<String, Object>();
        map.put("user.jcr.session", session);
        return this.rrf.getResourceResolver(map);
    }
}

Step 3 - Reconfigure the ScriptEngine & Factory

We have some updates we need to make to our original ScriptEngine in part I. First, a small change to the Factory. We want to give the factory a method to return the AEMFreemarkerServlet. We will use this shortly in the script engine:

@Reference
AEMFreemarkerServlet freemarkerServlet;

public AEMFreemarkerServlet getFreemarkerServlet() {
    return freemarkerServlet;
}

Simple, yet effective. Let's move on to the Script Engine. We want to configure our servlet instance by:

private final AEMFreemarkerServlet servlet;

protected FreemarkerScriptEngine(ScriptEngineFactory scriptEngineFactory) {
    super(scriptEngineFactory);
    FreemarkerScriptEngineFactory factory = (FreemarkerScriptEngineFactory) scriptEngineFactory;
    servlet = factory.getFreemarkerServlet();
}

public Object eval(Reader reader, ScriptContext scriptContext) throws ScriptException {
    Bindings bindings = scriptContext.getBindings(ScriptContext.ENGINE_SCOPE);
    SlingScriptHelper helper = (SlingScriptHelper) bindings.get(SlingBindings.SLING);   
    SlingHttpServletRequest request = helper.getRequest();
    String scriptPath = helper.getScript().getScriptResource().getPath());
    request.setAttribute("sling", helper);
    request.setAttribute("javax.servlet.include.servlet_path", scriptPath);
    try {
        servlet.service(request,  helper.getResponse());
    } catch (ServletException | IOException e) {
        throw new ScriptException(e);
    }
    return null;
}

Setting the sling script helper here instead of in the Servlet preProcessTemplate() method is necessary. Setting javax.servlet.include.servlet_path attribute is what the FreemarkerServlet uses to determine the script path. At this point, we are ready to start creating working components.

Now go forth! And create your components using Freemarker scripts!

Share this post

0 Comments

comments powered by Disqus