One of my teammates thoroughly drenched our under-the-desk development server with a giant cup of coffee once. Somehow, disaster was avoided, although the client’s carpet had a nice coffee-stain outline of the server’s footprint long after the app rolled out which afforded the rest of us endless opportunities to mercilessly haze the clumsy coder.
Keeping your Alfresco web scripts DRY is actually not about making your developers use spill-proof travel mugs, or better yet, virtual machines. DRY is a coding philosophy or principle that stands for Don’t Repeat Yourself. There are all kinds of resources available that go into more detail, and the principle is more broad than simply avoiding duplicate code, but that’s what I want to focus on here.
There are three techniques you should be using to avoid repeating yourself when writing web scripts: web script configuration, JavaScript libraries accessed via import, and FreeMarker macros accessed via import.
Web Script Configuration
Added in 3.0, a web script configuration file is an XML file that contains arbitrary settings for your web script. It’s accessible from both your controller and your view. Building on the hello world web script from the Alfresco Developer Guide, you could add a configuration script named “helloworld.get.config.xml” that contained:
<properties>
<title>Hello World</title>
</properties>
You could then access the “title” element from a JavaScript controller using the built-in E4X library:
var s = new XML(config.script);
logger.log(s.title);
And, you could also grab the title from the FreeMarker view:
<title>${config.script["properties"]["title"]}</title>
The web script configuration lets you separate your configuration from your controller logic. And because you can get to it from both the controller and the view, you don’t have to stick configuration info into your model.
This example showed a script-specific configuration, but global configuration is also possible. See the Alfresco Wiki Page on Web Scripts for more details.
JavaScript Import
If your web script controllers are written in JavaScript, at some point you will find yourself writing JavaScript functions that you’d like to share across multiple web scripts. A common example is logic that builds a query string, executes the query, and returns the results. You don’t want to repeat the code that does that across multiple controllers. That’s what the import statement is for.
The syntax is easy:
<import resource="classpath:alfresco/extension/scripts/status.js">
This needs to be the first line of your JavaScript controller file. This example shows a JavaScript library being imported from the classpath, but you can also import from the repository by name or by node reference. See the Alfresco JavaScript API Wiki Page for more details.
FreeMarker Import
Of the three this is the one I see ignored most often. Let’s take the Optaros-developed microblogging component for Share. Its basic data entity is called a “Status” object. So web scripts on the repository tier return JSON that might have a single Status object or a list of Status objects. That’s two different web scripts and two different views, but the difference between a list of objects and a single object is really just the list-related wrapper–in both cases, the individual Status object JSON is identical. I’ve seen people simply copy-and-paste the FreeMarker from the single-object template to the list template and then just wrap that with the list markup. That’s bad. If you ever change how a Status object is structured, you’ve got to change it in (at least) two places. (Or it’ll be someone that comes after you that has to do it which makes it even worse. If you aren’t following the analogy, your redundant code is the coffee stain).
Instead of duplicating the logic, use a FreeMarker import and define a macro that formats the object. Then you can call the macro any time you need your object formatted. Here’s how it works for Status.
A FreeMarker file called “status.lib.ftl” contains the macros that format Status objects. It lives with the rest of the web script files and looks like this:
<#assign datetimeformat="EEE, dd MMM yyyy HH:mm:ss zzz">
<#--
Renders a status node as a JSON object
-->
<#macro statusJSON status>
<#escape x as jsonUtils.encodeJSONString(x)>
{
"siteId" : "${status.properties["optStatus:siteId"]!''}",
"user" : "${status.properties["optStatus:user"]!''}",
"message" : "${status.properties["optStatus:message"]!''}",
"prefix" : "${status.properties["optStatus:prefix"]!''}",
"mood" : "${status.properties["optStatus:mood"]!''}",
"complete" : ${(status.properties["optStatus:complete"]!'false')?string},
"created" : "${status.properties["cm:created"]?string(datetimeformat)}",
"modified" : "${status.properties["cm:modified"]?string(datetimeformat)}"
}
</#escape>
</#macro>
<#--
Renders a status node as HTML
-->
<#macro statusHTML status>
SiteID: ${status.properties["optStatus:siteId"]!''}<br />
User: ${status.properties["optStatus:user"]!''}<br />
Message: ${status.properties["optStatus:message"]!''}<br />
Prefix: ${status.properties["optStatus:prefix"]!''}<br />
Mood: ${status.properties["optStatus:mood"]!''}<br />
Complete: ${(status.properties["optStatus:complete"]!'false')?string}<br />
Created: ${status.properties["cm:created"]?string(datetimeformat)}<br />
Modified: ${status.properties["cm:modified"]?string(datetimeformat)}<br />
</#macro>
There’s one macro for JSON and another for HTML. It still bothers me that the same basic structure is repeated, but at least they are both in the same file and it feels better knowing that this is the one and only place where the data structure for a Status object is defined.
The FreeMarker that returns Status objects as JSON resides in status.get.json.ftl and looks like this:
<#import "status.lib.ftl" as statusLib/>
{
"items" : [
<#list results as result>
<@statusLib.statusJSON status=result />
<#if result_has_next>,</#if>
</#list>
]
}
You can see the import statement followed by the JSON that sets up the list, and then the call to the FreeMarker macro to format each Status object in the list.
When someone posts a new Status, the new Status object is returned. Rather than repeat the JSON structure for a Status object, the status.post.json.ftl view simply calls the same macro that got called from the GET:
<#import "status.lib.ftl" as statusLib/>
{
"status" : <@statusLib.statusJSON status=result />
}
Now if the data structure for a Status object ever needs to change, it only has to be changed in one place.
Don’t Repeat Yourself
Take a look at your web scripts. Eliminate your duplicate code. And keep a lid on your mocha frappuccino.
Ok… my code didn’t show up previously, let’s try again:
Hi Jeff,
I have a question with this. I am trying to refactor a web script I wrote and I want to use FTL macros to do this. I started with a very simple one to see how it went.
I have a macro in a file called: getPageTitle.lib.ftl that contains the following:
<#macro getPageTitle currentPage>
${currentPage[“ra:pagetitle”]}
</#macro>
In my .html.ftl file for the web script, I have the following:
<#import “getPageTitle.lib.ftl” as pageTitleLib />
…
<#list layoutDom[“ra:layout/ra:page”] as page>
…
<@pageTitleLib.getPageTitle currentPage = page />
When I run the web script, I get an error saying, “Error on line 2, column 11 in Prototype 1/entryFormCreator/getPageTitle.lib.ftl Expecting a string, date or number here, Expression currentPage[“ra:pagetitle”] is instead a freemarker.ext.dom.NodeListModel’. Please contact your system administrator.”
I think that when I pass the “page” value, it is sending all the pages instead of just the single page I want.
When I put the macro inside the file and call it without importing, everything works as expected.
The web script files and the getPageTitle.lib.ftl all live in the same Space in the Alfresco repository under Company Home/ Data Dictionary/Web Scripts Extensions.
What do you think is causing the problem?
Any help would be great.
Thanks!
Krista