Alfresco ScriptNodes and jsonUtils

I’m using Alfresco Enterprise v4.1.1 for a project and I found a bit of a hole in the jsonUtils object.

Here’s what I tried to do in a webscript:

var users = people.getMembers(people.getGroup("GROUP_MyGroup"));
var json = jsonUtils.toJSONString(users);


The first line returns a javascript array of org.alfresco.repo.jscript.ScriptNodes (3 persons). These are the node objects you use in your webscripts.
The second line is supposed to provide a valid JSON string representing the user array.
The “jsonUtils” object available in webscripts uses org.springframework.extensions.webscripts.json.JSONUtils.

What I get for my array of 3 users looks something like this:

"Node Type: {}person, Node Aspects: [...]",
"Node Type: {}person, Node Aspects: [...]",
"Node Type: {}person, Node Aspects: [...]"


I’ve done some slight editing to save space here but it’s not valid JSON and not very useful at all. It turns out this is the output from ScriptNode.toString() for each user.

However ScriptNode also provides a handy toJSON() method.
Here’s some Java I wrote to add support for both a single ScriptNode and an array of ScriptNodes:

public String toJSONString(Object object) throws IOException {

  JSONUtils jsonUtils = new JSONUtils();

  if (object instanceof ScriptNode) {
    // the true tells it to use short QNames
    // eg. cm:name instead of {}name
    return ((ScriptNode) object).toJSON(true);

  } else {
    // is it an array of ScriptNodes?
    if (object instanceof NativeArray) {

      NativeArray array = (NativeArray) object;

      // get first item and see if it's a ScriptNode
      Object firstItem = array.get(0, array);

      if (firstItem instanceof ScriptNode) {
        StringBuffer json = new StringBuffer("[");
        ScriptNode node = (ScriptNode) firstItem;

        for (int i = 1; i < array.getLength(); i++) {

          Object value = array.get(i, array);
          node = (ScriptNode) value;
        return json.toString();

    // if it's not a ScriptNode or array of ScriptNodes let jsonUtils handle it
    return this.jsonUtils.toJSONString(object);


I expose this code to javascript with an extension name of ‘jsonUtilsTim’ and now the webscript looks like this:

var users = people.getMembers(people.getGroup("GROUP_MyGroup"));
var json = jsonUtilsTim.toJSONString(users);


Now I get the following which is valid JSON and actually useful:

 "mimetype": "application/octet-stream",
 "aspects": [
 "nodeRef": "workspace://SpacesStore/770df4a0-78ff-4c46-aca9-0de5ff12345",
 "properties": {
 "cm:name": "770df4a0-78ff-4c46-aca9-0de5ff512345",
 "sys:node-dbid": 27,
 "cm:email": "",
 "cm:organizationId": "",
 "sys:store-identifier": "SpacesStore",
 "sys:locale": "en_US",
 "cm:homeFolderProvider": "bootstrapHomeFolderProvider",
 "cm:userName": "admin",
 "cm:sizeCurrent": null,
 "cm:owner": "admin",
 "sys:node-uuid": "770df4a0-78ff-4c46-aca9-0de5ff512345",
 "cm:lastName": "",
 "sys:store-protocol": "workspace",
 "cm:homeFolder": "workspace://SpacesStore/4f4ff00d-c94f-4a99-a304-cbe2e2912345",
 "cm:firstName": "Administrator"
 "type": "cm:person"
 "mimetype": "application/octet-stream",
 "aspects": [
 "nodeRef": "workspace://SpacesStore/b6d80d49-21cc-4f04-9c92-e70630312345",
 "properties": {
 "sys:locale": "en_US",
 "cm:companyaddress3": "UK",
 "cm:homeFolderProvider": "userHomesHomeFolderProvider",
 "cm:companytelephone": "",
 "cm:owner": "admin",
 "cm:jobtitle": "Web Site Manager",
 "cm:preferenceValues": "contentUrl=store://2012/10/26/15/16/146c1e27-3980-4c21-b66c-397d97512345.bin|mimetype=text/plain|size=817|encoding=UTF-8|locale=en_US_|id=127",
 "cm:homeFolder": "workspace://SpacesStore/5da679f6-e7f3-4f8d-9506-554ed7312345",
 "cm:instantmsg": "",
 "cm:sizeQuota": -1,
 "cm:googleusername": "",
 "cm:firstName": "Jane",
 "cm:emailFeedId": 442,
 "cm:name": "b6d80d49-21cc-4f04-9c92-e70630312345",
 "cm:userStatusTime": "Tue Feb 15 13:13:09 MST 2011",
 "sys:node-dbid": 562,
 "cm:email": "",
 "sys:store-identifier": "SpacesStore",
 "cm:companyfax": "",
 "sys:node-uuid": "b6d80d49-21cc-4f04-9c92-e70630312345",
 "cm:lastName": "Doe",
 "cm:persondescription": "contentUrl=store://2012/10/26/15/16/8f13b84b-f797-4176-bcce-cdff90912345.bin|mimetype=application/octet-stream|size=54|encoding=UTF-8|locale=en_US_|id=128",
 "cm:companyemail": "",
 "cm:sizeCurrent": null,
 "cm:userName": "jane.doe",
 "sys:store-protocol": "workspace"
 "type": "cm:person"
 "mimetype": "application/octet-stream",
 "aspects": [
 "nodeRef": "workspace://SpacesStore/c530d479-9a78-4c14-94c7-933b46412345",
 "properties": {
 "cm:name": "c530d479-9a78-4c14-94c7-933b46412345",
 "sys:node-dbid": 6939,
 "cm:organizationId": "",
 "cm:email": "",
 "sys:store-identifier": "SpacesStore",
 "sys:locale": "en_US",
 "cm:presenceUsername": "",
 "cm:homeFolderProvider": "userHomesHomeFolderProvider",
 "cm:owner": "john.doe",
 "cm:organization": "",
 "cm:jobtitle": "",
 "sys:node-uuid": "c530d479-9a78-4c14-94c7-933b46412345",
 "cm:lastName": "Doe",
 "cm:homeFolder": "workspace://SpacesStore/15a8f7fb-87ee-4a7e-9b4d-c2f3a8f12345",
 "cm:presenceProvider": "",
 "cm:location": "",
 "cm:sizeQuota": -1,
 "cm:sizeCurrent": null,
 "cm:userName": "john.doe",
 "sys:store-protocol": "workspace",
 "cm:firstName": "John"
 "type": "cm:person"

Scheduling a “nag” email using Activiti Workflow and Alfresco

Sometimes users need to be reminded of outstanding tasks that have been neglected for a specified period of time. A helpful way to do this is to send them a nice email. For a recent project, I cobbled together an Activiti workflow that had various such “nag” steps that would fire off an email if a task became stagnant for too long, and I thought I’d share some code snippets on how I did it.


I’ve simplified the workflow to a single step, plus the “nag” task for brevity:

You could easily imagine this as a part of a much more complex workflow, perhaps with multiple steps requiring timed email notifications.

Classify Task

If the Classify task is idle for more than 14 days, we kick off a friendly reminder email. Here’s the Classify Article workflow step.

<userTask id=”classifyArticle” name=”Classify Article” activiti:candidateGroups= “${ATCWF_classifierGroup}” activiti:formKey=”ATCWF:classifyArticle”>
      <activiti:taskListener event=”complete”>
        <activiti:field name=”script”>
          <activiti:string>classification = task.getVariable(‘ATCWF_classification’);
 <boundaryEvent id=”classifyTimer” name=”Classify Nag Timer” cancelActivity=”false” attachedToRef=”classifyArticle”>

So when this task is completed, it grabs the ATCWF_classification variable chosen by the user on the workflow’s form, and stores it in an Activiti workflow variable for later use. Not overly useful, but good enough for demonstration.

I’ve attached a boundary event, “classifyTimer”, to this task which will fire after a timeCycle determined by the ATCWF_nagTimerDuration Activiti workflow variable.

For the required 14 days, the value will be R/P14D (“repeat every period of 14 days”, according to ISO 8601 format.) Make sure cancelActivity is set to false so that the workflow won’t be suspended while we’re waiting.

A Brief Aside

I’d like to make a brief aside to comment on a useful “trick” I’ve used in my code snippets that might be useful to others, although it is not necessary for our task of creating a “nag” email.

Both ${ATCWF_nagTimerDuration} and ${ATCWF_classifierGroup} are Activiti workflow variables that I declared and set in a step prior to this Classify Article task.

This let me store those values in a configuration file in the underlying Alfresco repository, allowing them to be changed without having to redeploy the workflow. Which is nice, since deploying a new workflow doesn’t impact in-flight workflows, but it is possible by altering the value of some workflow variables.

This “trick” could be applied to change what group a later workflow step is assigned to, depending on choices made by a user in an earlier step, cutting down on extraneous exclusive gateways and tasks needed to account for all possible choices.

Email Task

Now let’s look at the step that’ll send a reminder email every 14 days.

<serviceTask id=”classifyNagEmail” name=”Nag Email” activiti:class=”org.alfresco.repo.workflow.activiti.script.AlfrescoScriptDelegate” activiti:async=”true” activiti:exclusive=”false”>
      <activiti:field name=”script”>
            var article = bpm_package.children[0];

                   // construct notification email
                  var subject = “An article needs to be classified”;
                  var emailTemplatePath = “”Data Dictionary/Email Templates/Notify Email Templates/ATC”;
                  var emailTemplate = companyhome.childByNamePath(emailTemplatePath+”/notify_reminder.ftl”);
                  var emailFrom = “”;

                  var recipient = ATCWF_classifierGroup;
                  var mail = actions.create(“mail”);
                  mail.parameters.to_many  = recipient;
                  mail.parameters.subject   = subject;
                  mail.parameters.from = emailFrom;
                  mail.parameters.template = emailTemplate;
                  // placeholder body text; the template will have the message
                  mail.parameters.text = “Task requires action”;

           // pass article properties to the template layer for easy email substitution
           var templateArgs = new Array();
           var articleId =['sys:node-dbid'];
           templateArgs['articleId'] = articleId + “”;
           templateArgs['articleTitle']    =['cm:title'];
           templateArgs['reviewPeriod']    = ATCWF_nagTimerValue;

                  var templateModel = new Array();
                  templateModel['args'] = templateArgs;
                  mail.parameters.template_model = templateModel;


Here we see the construction of an Alfresco mail action, the retrieval of a FreeMarker template from the repository for the message body, and passing the Activiti workflow variables and package properties to the template so the email can give more specific information about the article needing attention.

I simplified things a bit in the above snippet, as I normally put static variables such as the email subject, email template path etc. as properties in a configuration file for the same reasons I mentioned earlier.

Hopefully it’s not too difficult to see how you could extend the concepts and “tricks” I’ve shown in these two steps to make an intelligent workflow capable of making decisions and notifying pesky users to take care of neglected tasks.

Alfresco Sub-groups

My goal: add a group as a sub-group of another group – ie. nest groups.

Alfresco Version: Enterprise – v3.4.7 (572)

My first try – the Alfresco Explorer UI

The problem with this is that although it will show you sub-groups it will only let you add users to groups, not other groups.

My second try – the Alfresco Share UI

I started by searching for my parent group by name.
Found it – great. Next I clicked the Edit icon.
Turns out this only lets you edit the display name of the group.
So I clicked the Browse button.
In the browse interface when you click on a group you can add sub-groups and users – perfect.
BUT…for some reason my parent group did not show up on the list. It’s not a system list or anything special, just a local Alfresco group I created.

My third try – script it

I took a look at my trusty Alfresco javascript API (
Adding a sub-group this way is pretty simple:

  1. Find the parent group
  2. Find the child group
  3. Add the child to the parent

Here is the javascript (NOTE the “GROUP_” prefix when finding a group):

var parentGrp = people.getGroup("GROUP_My Parent Group"); 
var childGrp = people.getGroup("GROUP_My Child Group"); 
people.addAuthority(parentGrp, childGrp);


I don’t really understand why, in Share, searching for a specific group and browsing groups offer different options on the results.

But scripting it is a quick, easy work-around.

Implementing a complete high availability Alfresco solution using open source technologies

As a proof of concept, I have done some research and experimenting to determine the best way of clustering Alfresco using completely open source components. I wanted a solution that offered load balancing as well as fault tolerance. There are three components outside of Alfresco that are needed to achieve this.

  1. Load balancer
  2. File system
  3. Database

The load balancer is the simplest component of the three, and the one with the most options available. We just need a load balancer that is able to handle sticky sessions. A dumb load balancer which round robins connections will not work for this scenario.

Alfresco stores all the content as regular files. (Unlike Sharepoint putting content in the database. Yikes!) In order to achieve HA on the content repository we need some sort of clustered or replicating file system. It was not long ago when clustered file systems were out of reach from the open source community. It is great that we now have some viable open source options now.

The last component needed, of course, is the database. Unfortunately, there is no viable multi-master open source option. There are many projects that are working towards this, such as Bucardo. But there is nothing currently that is a drop in replacement and/or production ready. The good news is we still have a master-slave(s) setup that can still achieve HA and some sort of load balancing.

Here is the complete solution I implemented:


Alfresco: Alfresco Enterprise 4.0.2

I used the latest version of Alfresco Enterprise at the time of writing this, just since it is what I deal with the most. I believe the Community Edition would work just fine as well in this scenario since the heart of Alfresco clustering is within Ehcache.


Load balancer: HAProxy

HAProxy is known to be very stable and currently used on some very high traffic web sites. It also gives us the functionality to keep track of sessions via the JSESSIONID cookie. Another great feature is we can take the fault detection further, and test a web script page in Alfresco to determine if Alfresco is currently running. (http://admin:passwd@server1/alfresco/wcs/s is a great page to check.)

There will be a small portion of people that were looking at this diagram and saying to themselves, “But there is a single point of failure!” HAProxy is a very simple component, and it would be easy to set up an active/passive automatic fail over. Also very stable physical and virtual options exist.

I should also note that we have tested HAProxy using single sign on authentication via Active Directory Kerberos. I assume NTLM would work just fine as well.


Clustered file system: GlusterFS

I have read good things about GlusterFS, but this was my first hands on experience with it. I was shocked how simple and quick this was to get up and running. A command to add the second server, and another to get the replication going. No messing with configuration files. You can even have 4 servers and enable replication and striping. Similar to the way RAID 10 (or 0+1) works, but across servers. This is a perfect fit for putting Alfresco’s content. Load balancing and seamless fault tolerance.


Replicating database: PostgreSQL + pgpool-II

MySQL is still an option, but I chose to go with Postgres here. I liked some of the HA features Postgres provided that seemed lacking in MySQL.  Unfortunately, either way we have to use a master-slave replication configuration.

In order to achieve load balancing and fault tolerance we need to put pgpool-II in front on the databases. It will take read only queries and load balance them between the master and slave(s). Commands that involve any kind of updates, or writes will be forwarded to the master which in turn get streamed to the slaves. This makes writes slower than a standalone database, but most Alfresco installs should be primarily reads for the average implementation. Pgpool can also be configured to use parallel queries. This means large queries can be split up amongst servers.

Pgpool will also detect any faults, so if any of the slaves go down it will just take them out of the pool. And if the master goes down, it will take one of the slaves and promote it to the new master. For the chance of a problem with Pgpool, a similar configuration with HAProxy, an active/passive configuration can be used to add some redundancy.


Enjoy your content management uptime! And feel free to drop me a comment.

Alfresco startup script for Ubuntu/Debian

If you have used the script that comes with Alfresco, you have most likely already made your own. I created one for Ubuntu, but it should work with other variants.


  • NEW! Added support for JPDA debugging and JMX console
  • NEW! Also support for status (/etc/init.d/alfresco status)
  • NEW! Precise doesn’t come with ‘bc’. (Really Canonical?) I removed that dependency.
  • NEW! Added support for Alfresco 4.X!
  • Can configure script to run Alfresco as root or a non-privileged user.
  • Firewall rules will be setup at system boot if configured to run as a non-privileged user.
  • Will attempt to shutdown all instances of Alfresco cleanly, will kill them after a set time if they are hung up with a default of 30 seconds.
  • Cleans up rogue processes such as OpenOffice
  • Able to change the umask that Alfresco creates files as. (Handy for multiuser environments.)


  • Only tested on Ubuntu Server 10.04 and 12.04 (Lucid and Precise LTS releases)
  • The script directly calls Java, bypassing and even It was the only way I could seem to get everything working smoothly including umasks. So the biggest downside of this, is that future versions of Alfresco may have different Tomcat start up parameters.


  1. Copy and paste the script into the file: /etc/init.d/alfresco
  2. Edit the “### Configurable variables” section in the script to suit your environment
  3. chmod +x /etc/init.d/alfresco
  4. update-rc.d alfresco start 99 2 3 4 5 . stop 99 0 1 6 .

Feel free to email myself any bugs, feedback ,or requests. My email address is the user name you see here at the top of the blog

The Script

Here is a link with proper indentation and easier copying:

Show Users In Logs – Alfresco NDC

I was trying to debug a user’s issue on one of our alfresco servers.  I looked through the alfresco log file but of course there are entries from many users there so it was tough to pinpoint the relevant ones.

One of our other alfresco servers adds the user name to the log statements and that would be very helpful in this case.  I couldn’t figure out why our 3.2 server included user names but our 3.4 server did not.

Here’s what I found out:Alfresco’s AuthenticationUtil class (unchanged from 3.2 to 3.4) has a logNDC() method that adds the user name.

NDC (Nested Diagnostic Contexts) is part of Log4j.  Look for “Nested Diagnostic Contexts” way down this page for an explanation:

Instead of directly using Apache’s NDC, Alfresco uses a delegate to avoid any dependancy. AuthenticationUtil uses org.alfresco.util.log.NDC instead of org.apache.log4j.NDC.

The first thing Alfresco’s NDC class does is to instantiate its delegate (org.alfresco.util.log.log4j.Log4JNDC) which then uses org.apache.log4j.NDC.


It only does this if org.alfresco.util.log.NDC’s logger has debug enabled.  If not then the delegate is null and any NDC calls do nothing at all.

How to Fix It

The solution is simple, set Alfresco’s NDC logger to debug in your log4j properties file like so:

That class doesn’t actually log any messages so this won’t add anything to your log files except the user name.

Now just make sure the File appender includes the NDC information by adding %x to the conversion pattern like so:

log4j.appender.File.layout.ConversionPattern=%d{ABSOLUTE} %x %-5p [%c] %m%n

You just get this:

12:25:00,018 User:System DEBUG [org.alfresco.repo.jscript.ScriptLogger] start clean user home script

instead of this:

12:25:00,018 DEBUG [org.alfresco.repo.jscript.ScriptLogger] start clean user home script


Usually you set your log4j overrides in a file in tomcat/shared/classes/alfresco/extension called or or something like that.  You can go ahead and put the NDC=debug line in this file.  However if you put your new conversion pattern in this file in 3.4 it will be ignored.  (See Instead you must change the conversion pattern in tomcat/webapps/alfresco/WEB-INF/classes/

About Alfresco Versions

This has been annoying me for a long time.  I looked at Alfresco’s NDC class in 3.2 and it does NOT check if debug is enabled – it just finds org.apache.log4j.NDC and uses it.  I’m not sure when this changed (I haven’t looked at 3.3) but I do know that you need to set NDC to debug in 3.4 and 4.0 as org.alfresco.util.log.NDC is the same in both of those.


Alfresco – Version Stamp Your AMP

I always find myself renaming AMP files after building them – I add the module version to the name. Being relatively lazy I decided to see if I could have Alfresco’s ANT build do this for me.

After a bit of trial and error I figured it out and created an ANT macro to do it for me.

I put my macro in a separate file called abstractive-macros.xml.

It has 2 parameters:

  1. modpropsdir is the directory containing the file. This is where we read the module version. It’s probably already defined in your under a property called
  2. ampnameproperty is the NAME of the property that contains the AMP file name. We just want the name of the property (not its value) because we want to update the property itself so anything else that uses it will have the new name.

The macro:

  • pulls in the contents of
  • pulls the extension from the amp file name
  • gets the “module.version” from
  • builds a new amp file name that includes the version

It turns out you can use javascript within an ANT script – I didn’t know that. You can also use all kinds of other scripts such as groovy, ruby, jython, judoscript, etc.

Here’s the complete macro xml.


<project name="abstractive-macros">

   <macrodef name="versionamp" description="Appends the module version to the amp file name">

      <!-- modpropsdir is the VALUE of the -->
      <attribute name="modpropsdir" />

      <!-- ampnameproperty is the NAME of the property
           and its value will be updated to include the module version -->
      <attribute name="ampnameproperty" />

        <!-- append module version to amp file name -->
        <property file="@{modpropsdir}/"/>
        <script language="javascript"> <![CDATA[
          amp = project.getProperty("@{ampnameproperty}");
          title = amp.substring(0, amp.indexOf(".amp"));

          version = project.getProperty("module.version");
          project.setProperty("@{ampnameproperty}", title + "_" + version + ".amp");
        ]]> </script>

The only change to build.xml is to include our new macro xml file.


  <import file="macros.xml" />
  <import file="abstractive-macros.xml" />
  <import file="projects.xml" />

And in projects.xml we make the call to the macro. This is done in the package-MYMODULE-extension target. I pass it the path to module properties and the NAME of the property that contains the amp file name.


  <target name="package-MYMODULE-extension" depends="package-MYMODULE-jar">

    <versionamp modpropsdir="${}"
      ampnameproperty="" />

    <zip destfile="${dir.module.MYMODULE.dist}/${}" update="true">
      <zipfileset file="${}/" />
      <zipfileset file="${dir.module.MYMODULE.dist}/${}" prefix="lib" />
      <zipfileset dir="${dir.module.MYMODULE}/${}" prefix="lib" />
      <zipfileset dir="${dir.module.MYMODULE.config}" prefix="config">
        <exclude name="**/" />

And that’s all there is to it.  So if:

  • your contains module.version=1.10
  • in your your

and you run the package-MYPROJECT-extension target it will generate an amp called MYPROJECT_1.10.amp.

Better Alfresco Script Logging

I was working on some web scripts recently and using log statements to help my development. Does it ever bother you that the only log level available to web scripts is DEBUG? It bugs me.

So I made a simple JavaScript extension that exposes the various log levels of the ScriptLogger to JavaScript.

If you are not familiar with creating Alfresco JavaScript extensions read the Alfresco JavaScript API wiki entry.

The Java class extends BaseScopableProcessorExtension. It provides two log methods for each of these log levels:

  • INFO
  • WARN

Here are the debug methods an example, the full source code is below:

public void debug(Object message) {

public void debug(Object message, Throwable t) {
    logger.debug(message, t);

As you will see below there is not much to the code.

I’ve called it atcLogger (ATC = Abstractive Technology Consulting)

This is nice if, for example, you have a server with the log level set to WARN and you want to quickly toss in a log statement. If you used logger.log(“Some message”) you wouldn’t see the output in the log since that uses DEBUG level. You could use atcLogger.warn(“Some message”) and this would appear in the log without having to change the log level on the server. Even if you change the log level through JMX it’s slightly more effort than lazy developers like me want to expend.

Here is the spring bean for the extension:

<bean id="atc_atcLogger" parent="baseJavaScriptExtension">
    <property name="extensionName">


import org.alfresco.repo.jscript.BaseScopableProcessorExtension;
import org.alfresco.repo.jscript.ScriptLogger;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

 * Extends Alfresco's JavaScript API to expose a wider range of
 * logging levels.
 * Refer to this class in JavaScript using the alias "atcLogger".
 * @author Tim.Frith
public class ATCLogger extends BaseScopableProcessorExtension {

    // Commons logger
    protected static final Log logger = LogFactory.getLog(ScriptLogger.class);

     * Standard constructor
    public ATCLogger() {

    /* ------------------------------------------------------------ */
    /* Public methods - available to JavaScript */
    /* ------------------------------------------------------------ */

     * @see org.apache.log4j.Logger#debug(Object)
    public void debug(Object message) {

     * @see org.apache.log4j.Logger#debug(Object, Throwable)
    public void debug(Object message, Throwable t) {
        logger.debug(message, t);

     * @see org.apache.log4j.Logger#info(Object)
    public void info(Object message) {;

     * @see org.apache.log4j.Logger#info(Object, Throwable)
    public void info(Object message, Throwable t) {, t);

     * @see org.apache.log4j.Logger#warn(Object)
    public void warn(Object message) {

     * @see org.apache.log4j.Logger#warn(Object, Throwable)
    public void warn(Object message, Throwable t) {
        logger.warn(message, t);

     * @see org.apache.log4j.Logger#error(Object)
    public void error(Object message) {

     * @see org.apache.log4j.Logger#error(Object, Throwable)
    public void error(Object message, Throwable t) {
        logger.error(message, t);

     * @see org.apache.log4j.Logger#fatal(Object)
    public void fatal(Object message) {

     * @see org.apache.log4j.Logger#fatal(Object, Throwable)
    public void fatal(Object message, Throwable t) {
        logger.fatal(message, t);

Possible improvements:

  • expose isDebugEnabled, isInfoEnabled(), etc. to JavaScript