Freemarker?
While these steps can be replicated for any type of script rendering, I will be making references specifically to 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.
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.
While these steps can be replicated for any type of script rendering, I will be making references specifically to 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
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
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
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 set up an extension of the FreemarkerServlet along with some other configurations. Here's why:
- 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.
- 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.
- The FreemarkerServlet inserts the JspTaglibs hash into the data-model, which you can use to access JSP taglibs.
- The ContextLoader alongside the FreemarkerServlet implements a means to cache template files, which is very beneficial for performance.
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.
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 into 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 bound 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 the 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 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 data 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!
No comments:
Post a Comment
If you have any doubts or questions, please let us know.