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.

Features

  • 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.)

Limitations

  • Only tested on Ubuntu Server 10.04 and 12.04 (Lucid and Precise LTS releases)
  • The script directly calls Java, bypassing alfresco.sh and even catalina.sh. 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.

Installation

  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 @abstractive.ca.

The Script

Here is a link with proper indentation and easier copying:

http://dl.dropbox.com/u/7658190/util/alfresco.sh

Eclipse Run As Java Application – NoClassDefFound

I started having a problem in Eclipse trying to run a Java class with a main() method. (Run As… Java Application).

It kept giving me a NoClassDefFoundException on the class I was trying to run. I swear this worked for this project before. It works fine if I create a new Java project.

I finally managed to fix it by adding a missing buildCommand to my .project file.

This was so frustrating that now that I finally figured it out I decided to share it. I’m using Eclipse Indigo on 64-bit Windows 7 by the way.

Before:

<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
  <name>MyJavaProject</name>
    <comment></comment>
    <projects>
    </projects>
    <buildSpec>
    </buildSpec>
    <natures>
      <nature>org.eclipse.jdt.core.javanature</nature>
    </natures>
</projectDescription>

After:

<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
  <name>MyJavaProject</name>
  <comment></comment>
  <projects>
  </projects>
  <buildSpec>
    <buildCommand>
      <name>org.eclipse.jdt.core.javabuilder</name>
      <arguments>
      </arguments>
    </buildCommand>
  </buildSpec>
  <natures>
    <nature>org.eclipse.jdt.core.javanature</nature>
  </natures>
</projectDescription>

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: http://logging.apache.org/log4j/1.2/manual.html

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.

HOWEVER

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:

log4j.logger.org.alfresco.util.log.NDC=debug

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

NOTE

Usually you set your log4j overrides in a file in tomcat/shared/classes/alfresco/extension called custom-log4j.properties or dev-log4j.properties 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 https://issues.alfresco.com/jira/browse/ALF-13742). Instead you must change the conversion pattern in tomcat/webapps/alfresco/WEB-INF/classes/log4j.properties.

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 module.properties file. This is where we read the module version. It’s probably already defined in your build.properties under a property called dir.module.MYPROJECT.property.
  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 module.properties
  • pulls the extension from the amp file name
  • gets the “module.version” from module.properties
  • 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.

abstractive-macros.xml

<project name="abstractive-macros">

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

      <!-- modpropsdir is the VALUE of the dir.module.XXXXX.property -->
      <attribute name="modpropsdir" />

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

      <sequential>
        <!-- append module version to amp file name -->
        <property file="@{modpropsdir}/module.properties"/>
        <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>
      </sequential>
   </macrodef>
</project>

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

build.xml

...
  <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.

projects.xml

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

    <versionamp modpropsdir="${dir.module.MYMODULE.property}"
      ampnameproperty="file.name.amp.MYMODULE" />

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

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

  • your module.properties contains module.version=1.10
  • in your build.properties your file.name.amp.MYPROJECT=MYPROJECT.amp

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:

  • DEBUG
  • INFO
  • WARN
  • ERROR
  • FATAL

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

public void debug(Object message) {
    logger.debug(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">
        <value>atcLogger</value>
    </property>
</bean>

ATCLogger.java:

package ca.abstractive.ecm.alfresco.jscript.io;

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) {
        logger.debug(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) {
        logger.info(message);
    }

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

    /**
     * @see org.apache.log4j.Logger#warn(Object)
     */
    public void warn(Object message) {
        logger.warn(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) {
        logger.error(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) {
        logger.fatal(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

Alfresco List Installed Modules in Explorer Template

UPDATE: I realized that if Alfresco is started with alf_start.bat then this works fine. However if you start it as a service then ALF_HOME and CATALINA_HOME environment variables are not available and it fails. So I’ve updated getCommonArgs() to use hard-coded paths as a work around for now.

Did you ever want to see which modules (AMPs) are currently installed but don’t want to have to log into the server to call the MMT (module management tool) for a list? Or you don’t have access to the MMT on the server. Or maybe you just want to see module version or install dates.

I decided to make this information available in the Alfresco Explorer to make my life easier.

Since the MMT already has a list command I decided just to call it with Java’s ProcessBuilder and parse the output.

Originally I planned to do this as a JavaScript extension that would return a JSON object with the module details. But then I thought this would work nicely in a presentation template that I could tack onto my Alfresco home space or wherever I wanted. So I ended up creating a template extension for Freemarker. The ModuleInfo class is shown below and extends BaseTemplateProcessorExtension.

If you aren’t familiar with extending Freemarker in Alfresco or using custom templates, see the Alfresco Template Guide.

Here is the command we need to run to have the MMT list installed Alfresco modules:

%JAVA_HOME%/bin/java -jar %ALF_HOME%bin/alfresco-mmt.jar list %CATALINA_HOME%/webapps/alfresco.war

And for Share modules we just change the war file it points to:

%JAVA_HOME%/bin/java -jar %ALF_HOME%bin/alfresco-mmt.jar list %CATALINA_HOME%/webapps/share.war

I don’t use ProcessBuilder often at all, but I figured since it has JAVA_HOME, ALF_HOME and CATALINA_HOME in its environment map that it should substitute the values as it’s making the call. Apparently not. Or I wasn’t calling it correctly. In any case I didn’t want to spend much time on that so I “manually” substituted the environment variable values before making the call. This is in the getCommonArgs() method (the full source is included below).

I split the Alfresco module list from the Share module list, having one call for each.

I put the module properties into a map using the names from the MMT output as the keys and I added the Name. So the map includes these properties:

  • Name
  • Title
  • Version
  • Install Date
  • Desription (this isn’t my spelling mistake! The MMT outputs it this way)

Once the template extension is deployed, we just need to make a presentation template that calls it. In the repository in Company Home/Data Dictionary/PresentationTemplates I created ModuleInfo.ftl.

It just calls the Java class to get the module details and throws it into a plain table. Here’s how one of the calls looks:

<#assign alfMods = moduleInfo.getAlfrescoModules()>

Finally, I chose a repository space and set it to use my new custom view.

Here’s what it looks like:

Here is the spring bean for the template extension:

<bean id="moduleInfoTemplate" parent="baseTemplateImplementation">
    <property name="extensionName">
        <value>moduleInfo</value>
    </property>
</bean>

ModuleInfo.java:

package ca.abstractive.ecm.alfresco.template;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.template.BaseTemplateProcessorExtension;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Extends Alfresco's FreeMarker API with functions for retrieving
 * information about installed modules.
 * 
 * Refer to this class in FreeMarker templates using the alias "moduleInfo".
 *
 * @author Tim.Frith
 */
public class ModuleInfo extends BaseTemplateProcessorExtension {

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

    /* ------------------------------------------------------------ */
    /* Public methods - available to FreeMarker */
    /* ------------------------------------------------------------ */

    /**
     * Returns a list of modules currently installed in the Alfresco war.
     *
     * @return a List of module property maps (Name, Title, Version, Install Date, Desription)
     * @throws AlfrescoRuntimeException
     */
    public List<Map<String,String>> getAlfrescoModules() throws AlfrescoRuntimeException {
    	ProcessBuilder processBuilder = new ProcessBuilder();

	String[] args = this.getCommonArgs(processBuilder);
	args[4] += "alfresco.war";

    	return this.getInstalledModules(processBuilder, args);
    }

    /**
     * Returns a list of modules currently installed in the Share war.
     *
     * @return a List of module property maps (Name, Title, Version, Install Date, Desription)
     * @throws AlfrescoRuntimeException
     */
    public List<Map<String,String>> getShareModules() throws AlfrescoRuntimeException {
    	ProcessBuilder processBuilder = new ProcessBuilder();

	String[] args = this.getCommonArgs(processBuilder);
	args[4] += "share.war";

    	return this.getInstalledModules(processBuilder, args);
    }

    /**
     * Gets detailed information about installed modules.
     *
     * @param processBuilder the process builder to be used to execute the command
     * @param args an array containing the command to execute and its args
     * @return a List with a Map for each module
     * @throws AlfrescoRuntimeException if errors retrieving module info
     */
    public List<Map<String,String>> getInstalledModules(ProcessBuilder processBuilder, String[] args) throws AlfrescoRuntimeException {

    	List<Map<String,String>> results = new ArrayList<Map<String,String>>();

    	try {
            processBuilder.command(args);
            Process process = processBuilder.start();

	    InputStream is = process.getInputStream();

	    String output = IOUtils.toString(is);
	    String[] lines = output.split("\n");

	    Map<String,String> workingMap = null;
	    for (String line : lines) {

	        if (line.startsWith("Module")) {
		    if (workingMap != null) {
	  	        Map<String,String> moduleMap = new HashMap<String,String>();
  		        moduleMap.putAll(workingMap);
		        results.add(moduleMap);
 		    }
		    workingMap = new HashMap<String,String>();
		    workingMap.put("Name", this.getModuleName(line));

	        } else {

	    	    String[] prop = this.getModuleProperty(line);
		    workingMap.put(prop[0], prop[1]);
	        }
	    }

	} catch (IOException ioe) {
	    logger.error("Error retrieving module details", ioe);
	    throw new AlfrescoRuntimeException("Error retrieving module details");
	}

	return results;
    }

    /* ------------------------------------------------------------ */
    /* Utility methods - unavailable to FreeMarker */
    /* ------------------------------------------------------------ */

    /**
     * Builds the list of arguments that are common to both Alfresco and Share calls.
     *
     * @param processBuilder the process builder to be used to execute the command
     * @return command and args WITHOUT the war file specified
     */
    protected String[] getCommonArgs(ProcessBuilder processBuilder) {
	String[] args = new String[5];
	args[0] = processBuilder.environment().get("JAVA_HOME") + "/bin/java";
	args[1] = "-jar";
	args[2] = "C:/Alfresco/bin/alfresco-mmt.jar";
	args[3] = "list";
	args[4] = "C:/Alfresco/tomcat/webapps/";

	return args;
    }

    /**
     * Extracts the module name from a line of alfresco_mmt.jar output.
     *
     * @param line a line of output
     * @return the module name
     */
    protected String getModuleName(String line) {
    	// Sample:
    	// Module 'org_alfresco_module_dod5015' installed in 'C:/ALFRES~1/tomcat/webapps/alfresco.war'

    	int startIdx = line.indexOf("'") + 1;
    	int endIdx = line.indexOf("'", startIdx + 1);

    	return line.substring(startIdx, endIdx).trim();
    }

    /**
     * Extracts the property name and value from a line of alfresco_mmt.jar output.
     *
     * @param line a line of output
     * @return an array where [0] = property name, [1] = propery value
     */
    protected String[] getModuleProperty(String line) {
    	// Sample:
    	//   -    Title:        Alfresco DOD 5015 Record Management Extension

    	String[] pair = new String[2];
    	pair[0] = line.substring("   -    ".length(), line.indexOf(":"));

    	int startIdx = line.indexOf(":") + 1;
    	pair[1] = line.substring(startIdx).trim();

    	return pair;
    }
}

ModuleInfo.ftl:

<h3>Installed Alfresco Modules:</h3>

<#assign alfMods = moduleInfo.getAlfrescoModules()>

<#if alfMods?size == 0>
    There are curently no Alfresco modules installed.
<#else>
    <table cellpadding="2">
	<tr>
   	    <th>Name</th>
	    <th>Title</th>
	    <th>Version</th>
	    <th>Install Date</th>
	    <th>Description</th>
	</tr>
	<#list alfMods as module>
	    <tr>
		<td>${module.Name}</td>
		<td>${module.Title}</td>
		<td>${module.Version}</td>
		<#-- convert install date to date so then we can format it back into the string we want -->
		<td>${module['Install Date']?datetime("EEE MMM dd hh:mm:ss zzz yyyy")?string.medium_short}</td>
		<td>${module.Desription}</td>	<#-- the MMT spells it wrong, so we must too -->
	    </tr>
	</#list>
    </table>
</#if>

<h3>Installed Share Modules:</h3>

<#assign shareMods = moduleInfo.getShareModules()>

<#if shareMods?size == 0>
    There are curently no Share modules installed.
<#else>
    <table cellpadding="2">
	<tr>
	    <th>Name</th>
	    <th>Title</th>
	    <th>Version</th>
	    <th>Install Date</th>
	    <th>Description</th>
	</tr>
	<#list shareMods as module>
	    <tr>
		<td>${module.Name}</td>
		<td>${module.Title}</td>
		<td>${module.Version}</td>
		<#-- convert install date to date so then we can format it back into the string we want -->
		<td>${module['Install Date']?datetime("EEE MMM dd hh:mm:ss zzz yyyy")?string.medium_short}</td>
		<td>${module.Desription}</td>	<#-- the MMT spells it wrong, so we must too -->
	    </tr>
	</#list>
    </table>
</#if>

Suggested improvements:

  • change the Java to use regular expressions to extract the details from the MMT output
  • make the presentation template pretty, mine is very plain

Alfresco Process Template on Classpath

Processing a template that is on the classpath instead of in the repository:

I’m deploying an Alfresco module (AMP) with a scheduled script that processes a FreeMarker template. The script resides on the classpath instead of in the repository and I thought it would be cleaner to deploy if the template was also on the classpath. I was sure that processing a FreeMarker template from the classpath must be functionality available in Alfresco out-of-the-box. However, after searching tirelessly for as long as my attention span would allow (admittedly not long) I couldn’t find such a call. So here is what I did…

All work was done on Alfresco 3.2r Enterprise.

The standard JavaScript API allows you to call processTemplate(template, args) on a node. “template” is either a string containing the FreeMarker or a node whose contents contains the FreeMarker. So I looked at org.alfresco.repo.jscript.ScriptNode and in particular at:

public String processTemplate(String template, Object args)

Then I looked at org.alfresco.repo.jscript.ClasspathScriptLocation since I knew the scheduled script bean uses it for the script which is on the classpath.

As described in the Alfresco Wiki (http://wiki.alfresco.com/wiki/3.2_JavaScript_API#Adding_Custom_Script_APIs) I extended the JavaScript API. I’ve already got several handy extensions in a NodeUtilScript Java class with an extension name of “nodeUtil” so I just added to it as follows:

/**
 * Process a FreeMarker Template against the given node.
 *
 * @param templateLocation the classpath of the template to execute
 * @param args Scriptable object (generally an associative array) containing the name/value pairs 
 *			   of arguments to be passed to the template
 * @param node the node to process the template against
 * @return output of the template execution
 * @throws AlfrescoRuntimeException
 */
public String processClasspathTemplate(String templateLocation, Object args, ScriptNode node) {

   ClasspathScriptLocation location = new ClasspathScriptLocation(templateLocation);

   try {
      // retrieve template content
      String template = IOUtils.toString(location.getInputStream());

      // process template
      return node.processTemplate(template, args);

   } catch (IOException ioe) {
      throw new AlfrescoRuntimeException("Error retrieving template", ioe);
   }
}

There’s not much to it:

  • create a ClasspathScriptLocation with a path to the template
  • get the template contents as an InputStream
  • use IOUtils.toString(InputStream) from Apache Commons IO to pull the template into a string
  • call processTemplate() on the given node using the retrieved template string, returning the result

It can then be called from my scheduled script (or from a web script).

Calling Script:

// get a node

var myNode = search.findNode("workspace://SpacesStore/c7e27390-12f0-44dd-b89b-ef63a8320d6b");

// prepare some data to pass to the template
var args = new Array();
args["arg1"] = "Hello";
args["arg2"] = "The World";

// process the template agains the node
var result = nodeUtil.processClasspathTemplate("alfresco/templates/email/MyTemplate.ftl", args, myNode);

MyTemplate.ftl:

I say, ${args["arg1"]!} to ${args["arg2"]!}.

Node-uuid is ${document.properties["sys:node-uuid"]}

So after executing the above script “result” contains “I say, Hello to The World. Node-uuid is c7e27390-12f0-44dd-b89b-ef63a8320d6b”.

Having my template deployable within my AMP really simplifies moving from development to UAT to production repositories. I don’t have to remember to actually upload it to the repository separately or to export it as an ACP and bootstrap it.

Of course, in many cases the template is in the repository for a very good reason – so that changes can be made to it by less technical users and those changes can be seen without restarting Alfresco. But it’s still nice to have the choice.

References:

Alfresco Wiki, 3.2 JavaScript API, Adding Custom Script APIs

http://wiki.alfresco.com/wiki/3.2_JavaScript_API#Adding_Custom_Script_APIs

Apache Commons IO (http://commons.apache.org/io/)

IOUtils (http://commons.apache.org/io/api-release/org/apache/commons/io/IOUtils.html)

 

Alfresco Dynamic Content Rules

I needed a way to create rules dynamically in script. You can do this in Java but the API is not the most pleasant thing to use. What if you’ve just created a space in a script and you want to add rules to it? I couldn’t find any way of doing this but it wasn’t that difficult to piece together a solution.

All work was done on Alfresco 3.2r Enterprise.

Disclaimer: There’s always a chance that I’ve missed some other way of doing this in Alfresco that doesn’t involve coding. But sometimes it’s more interesting and you learn more when you dig in and do it yourself.

Getting Started

So I did some Googling and found this post in the Alfresco Wiki that let me know it was possible:

However, I don’t have the option of enabling “remote” on the server.

But the idea of describing a rule in JSON in a script then passing it to a Java class to do the work fits well with what I need to in my project.

I poked around the Alfresco code a bit and found org.alfresco.web.bean.rules.CreateRuleWizard, the class used for the Alfresco Explorer’s rule wizard.

It seemed to have the functionality I needed, I just had to figure out a way to pull the rule from JSON rather that from the UI.

The JSON

I used the JSON format in this page as a starting point:

I modified it based on what the various action and condition handlers expect for parameter names.

See the “Using It” section below for some sample JSON.

TIP: JSONLint is a very handy tool.

The Java

I decided to build a class that would read the rule configuration from JSON objects. I called this JsonRuleCreator and it extends CreateRuleWizard mostly to use its condition and action handlers.

Unlike a lot of other Alfresco code that I’ve seen, CreateRuleWizard seems to have been coded with the possibility of extension in mind. It’s nice to see “protected” used instead the “private” I’ve come to expect in their classes (but I won’t get started on that rant).

The constructor takes a couple of helper classes. They important thing here is the call to CreateRuleWizard.init(). This initializes the maps of condition handlers and action handlers, key on “conditionName” and “actionName” respectively in the JSON.

public JsonRuleCreator(ServiceRegistry serviceRegistry, Repository repository) {
  this.serviceRegistry = serviceRegistry;
  this.repository = repository;

  super.init(null);
}

The createRule() method sets some of the simple rule properties such as title, description, etc in the parent class, calls setupRule() to set the more complex rule properties, then attaches the rule to the space.

public void createRule(NodeRef targetSpace, JSONObject ruleJson) throws JSONException {
  if (logger.isDebugEnabled()) {
    logger.debug("creating rule");
  }
  this.ruleJson = ruleJson;

  this.setTitle(this.ruleJson.getString("title"));
  this.setDescription(this.ruleJson.getString("description"));
  this.setType(this.ruleJson.getString("ruleType"));
  this.setRunInBackground(this.ruleJson.getBoolean("executeAsynchronously"));
  this.setApplyToSubSpaces(this.ruleJson.getBoolean("applyToChildren"));
  this.setRuleDisabled(this.ruleJson.getBoolean("disabled"));

  // create the new rule
  Rule rule = new Rule();
  rule.setRuleType(this.getType());

  // setup the rule
  this.setupRule(rule);

  // save the rule
  this.getRuleService().saveRule(targetSpace, rule);
}

The setupRule() and createCondition() methods do most of the real work. These are basically CreateRuleWizard.setupRule() and createCondition() modified to read from JSON objects.

The other method of note in convertJsonToMap() which converts a JSON object into a Map that CompositeCondition and CompositeActionCondition expect. For scripts and destination locations for things like move and copy, the Alfresco code expects a node reference. However I need to be able to have my script work on several different servers (DEV, UAT, PROD) that will all have different node references. So to allow the JSON to be more portable, I had this method convert repository paths to node references for “script” and “destinationLocation” parameters.

Gotcha 1: The handlers that use the “script” parameter assume the node reference is a string. The handlers that use the “destinationLocation” parameter assume the node reference is a NodeRef. So this method converts node references to the correct object type based on the parameter name.

Gotcha 2: Boolean parameter values pose a similar problem. So this method converts any “true” or “false” values to a Boolean object. And so on for Integer, Long and Double.

protected Map convertJsonToMap(JSONObject jsonObj) throws JSONException {

  // convert JSON to Map
  Map propMap = new HashMap();
  Iterator keyIt = jsonObj.keys();
  while (keyIt.hasNext()) {
    String key = (String) keyIt.next();
    String value = jsonObj.getString(key);
    Serializable propValue = null;

    // convert repository paths to node references
    if (PROP_SCRIPT.equals(key) || PROP_DESTINATION.equals(key)) {

      NodeService nodeService = this.serviceRegistry.getNodeService();
      NodeRef nodeRef = null;
      String storeProtocol = null;
      String storeIdentifier = null;
      String nodeUUID = null;

      // is value a node reference?
      if (NodeRef.isNodeRef(value)) {
        nodeRef = new NodeRef(value);

        storeProtocol = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_STORE_PROTOCOL);
        storeIdentifier = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_STORE_IDENTIFIER);
        nodeUUID = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_NODE_UUID);

      } else {

        // assume we have a repository path relative to Company Home
        // search for the path within Alfresco content repository
        String nodePath = "workspace/SpacesStore/" + value;
        nodeRef = this.repository.findNodeRef("path", nodePath.split("/"));

        if (nodeRef == null) {
          throw new JSONException("Path " + nodePath + " not found.");

        } else {
          storeProtocol = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_STORE_PROTOCOL);
          storeIdentifier = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_STORE_IDENTIFIER);
          nodeUUID = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_NODE_UUID);

          logger.debug("Original parameter value: " + value);
          value = storeProtocol.concat("://").concat(storeIdentifier).concat("/").concat(nodeUUID);
          logger.debug("Modified parameter value: " + value);
        }
      }
      if (PROP_SCRIPT.equals(key)) {
        propValue = value;

      } else if (PROP_DESTINATION.equals(key)) {
        propValue = nodeRef;
      }
    }
    if (propValue == null) {
      if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) {
        propValue = jsonObj.getBoolean(key);
      }
    }
    if (propValue == null) {
      try {
        propValue = jsonObj.getInt(key);

      } catch(JSONException jsone) {
        // not an integer, continue
      }
    }
    if (propValue == null) {
      try {
        propValue = jsonObj.getDouble(key);

      } catch (JSONException jsone) {
        // not a double, continue
      }
    }
    if (propValue == null) {
      try {
        propValue = jsonObj.getLong(key);

      } catch (JSONException jsone) {
        // not a long, continue
      }
    }
    if (propValue == null) {
      // default to String
      propValue = value;
    }
    propMap.put(key, propValue);
  }
  return propMap;
}

Now I just need to expose this to JavaScript. I created a RuleUtilScript class that extends org.alfresco.repo.jscript.BaseScopableProcessorExtension to handle this.

The createRule() method takes a space to add the rule to and a JSON string describing the rule. All it does is parse the JSON string into an object and call the JsonRuleCreator to do the work.

public void createRule(ScriptNode targetSpace, String ruleJson) throws JSONException {
  JSONObject json = new JSONObject(ruleJson);

  JsonRuleCreator jrc = new JsonRuleCreator(this.serviceRegistry, this.repository);
  jrc.createRule(targetSpace.getNodeRef(), json);
}

And we need to define this bean:

<bean id="ruleUtilScript" parent="baseJavaScriptExtension">
  <property name="extensionName">
    <value>ruleUtil</value>
  </property>
  <property name="serviceRegistry">
    <ref bean="ServiceRegistry">
  </ref></property>
  <property name="repository" ref="repositoryHelper">
</property></bean>

Using It

This script creates a new space under Company Home and attaches a rule to it called “Make Uniform”.
This rule:

  • is triggered on inbound TIFF and PDF files (based on mime type).
  • is applied to child spaces and runs in the background.
  • executes a script found in the repository **that must exist before you create the rule**.
var myspace = companyhome.createFolder("My Space");

var ruleJson = '';
ruleJson += '{';
ruleJson += '  "title": "Make Uniform",';
ruleJson += '  "description": "Makes submitted TIFF and PDF files uniform",';
ruleJson += '  "ruleType": "inbound",';
ruleJson += '  "applyToChildren": "true",';
ruleJson += '  "executeAsynchronously": "true",';
ruleJson += '  "disabled": "false",';
ruleJson += '  "action" : {';
ruleJson += '    "actionName": "composite-action",';
ruleJson += '    "actions": [';
ruleJson += '      {';
ruleJson += '        "actionName": "script",';
ruleJson += '        "parameterValues": {';
ruleJson += '          "script": "CompanyHome/Data Dictionary/Scripts/MakeUniform.js"';
// WE COULD HAVE USED A NODE REFERENCE HERE INSTEAD...
//          			 "script": "workspace://SpacesStore/75f01003-46eb-479e-a492-beae589aa5d4"
ruleJson += '        }';
ruleJson += '      }';
ruleJson += '    ],';
ruleJson += '    "conditions": [';
ruleJson += '      {';
ruleJson += '        "conditionName": "composite-condition",';
ruleJson += '        "orconditions": "true",';
ruleJson += '        "notcondition": "false",';
ruleJson += '        "conditions": [';
ruleJson += '          {';
ruleJson += '            "conditionName": "compare-mime-type",';
ruleJson += '            "mimetype":  "image/tiff",';
ruleJson += '            "notcondition": "false"';
ruleJson += '          },';
ruleJson += '          {';
ruleJson += '            "conditionName": "compare-mime-type",';
ruleJson += '            "mimetype": "application/pdf",';
ruleJson += '            "notcondition": "false"';
ruleJson += '          }';
ruleJson += '        ]';
ruleJson += '      }';
ruleJson += '    ]';
ruleJson += '  }';
ruleJson += '}';
logger.log("RULE JSON:\n" + ruleJson);
ruleUtil.createRule(myspace, ruleJson);

Action/Condition/Parameter Names

Here are lists of actions and condition names and thier handler classes. See the prepareForSave() methods on the handlers to figure out the expected parameter names and types. I found these by setting a breakpoint in CreateRuleWizard.initialiseActionHandlers().

TIP: It’s also handy to run Alfresco in debug mode and put a breakpoint in CreateRuleWizard.finishImpl() and step through rules you create in the UI. This helped me discover less obvious parameters like the “All Items” and “Items which contain a specific value in its name” conditions.

Valid values for “actionName” composite-action

  • transform-image (org.alfresco.web.bean.actions.handlers.TransformImageHandler)
  • mail (org.alfresco.web.bean.actions.handlers.MailHandler)
  • copy-to-web-project (org.alfresco.web.bean.actions.handlers.CopyToWebProjectHandler)
  • check-in (org.alfresco.web.bean.actions.handlers.CheckInHandler)
  • simple-workflow (org.alfresco.web.bean.actions.handlers.SimpleWorkflowHandler)
  • script (org.alfresco.web.bean.actions.handlers.ScriptHandler)
  • transform (org.alfresco.web.bean.actions.handlers.TransformHandler)
  • remove-features (org.alfresco.web.bean.actions.handlers.RemoveFeaturesHandler)
  • specialise-type (org.alfresco.web.bean.actions.handlers.SpecialiseTypeHandler)
  • link-category (org.alfresco.web.bean.actions.handlers.LinkCategoryHandler)
  • import (org.alfresco.web.bean.actions.handlers.ImportHandler)
  • add-features (org.alfresco.web.bean.actions.handlers.AddFeaturesHandler)
  • move (org.alfresco.web.bean.actions.handlers.MoveHandler)
  • copy (org.alfresco.web.bean.actions.handlers.CopyHandler)
  • check-out (org.alfresco.web.bean.actions.handlers.CheckOutHandler)

Valid values for “conditionName”

  • composite-condition (org.alfresco.web.bean.rules.handlers.CompositeConditionHandler)
  • compare-mime-type (org.alfresco.web.bean.rules.handlers.CompareMimeTypeHandler)
  • compare-property-value (org.alfresco.web.bean.rules.handlers.PropertyValueHandler)
  • has-aspect (org.alfresco.web.bean.rules.handlers.HasAspectHandler)
  • in-category (org.alfresco.web.bean.rules.handlers.InCategoryHandler)
  • is-subtype (org.alfresco.web.bean.rules.handlers.IsSubTypeHandler)
  • compare-date-property (org.alfresco.web.bean.rules.handlers.property.DatePropertyValueConditionHandler)
  • compare-integer-property (org.alfresco.web.bean.rules.handlers.property.IntegerPropertyValueConditionHandler)
  • compare-text-property (org.alfresco.web.bean.rules.handlers.property.TextPropertyValueConditionHandler)

Other than a little trial-and-error on the parameter names, that’s it. I find myself using very similar rules repeatedly so there was just a bit of pain the first time to figure out a rule’s JSON. After that…well I’m an excellent cutter and paster.

Full Java Source for JsonRuleCreator

package ca.abstractive.ecm.alfresco.rules;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.alfresco.model.ContentModel;
import org.alfresco.repo.model.Repository;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.action.Action;
import org.alfresco.service.cmr.action.ActionCondition;
import org.alfresco.service.cmr.action.CompositeAction;
import org.alfresco.service.cmr.action.CompositeActionCondition;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.rule.Rule;
import org.alfresco.web.bean.actions.IHandler;
import org.alfresco.web.bean.rules.CreateRuleWizard;
import org.alfresco.web.bean.rules.handlers.BaseConditionHandler;
import org.alfresco.web.bean.rules.handlers.CompositeConditionHandler;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

public class JsonRuleCreator extends CreateRuleWizard {

  /** the value of this parameter must be a node ref as a String */
  protected static final String PROP_SCRIPT = "script";

  /** the value of this parameter must be a node ref as a NodeRef */
  protected static final String PROP_DESTINATION = "destinationLocation";

  protected JSONObject ruleJson = null;

  protected ServiceRegistry serviceRegistry = null;

  protected Repository repository = null;

  public JsonRuleCreator(ServiceRegistry serviceRegistry, Repository repository) {
    this.serviceRegistry = serviceRegistry;
    this.repository = repository;

    super.init(null);
  }

  /**
   * Creates a rule on the given space using rule properties
   * provided via JSON in the constructor.
   *
   * @param targetSpace the space the rule will apply to
   * @param ruleJson a JSON object describing the rule to create
   * @throws JSONException if error parsing JSON string
   */
  public void createRule(NodeRef targetSpace, JSONObject ruleJson) throws JSONException {
    if (logger.isDebugEnabled()) {
      logger.debug("creating rule");
    }
    this.ruleJson = ruleJson;

    this.setTitle(this.ruleJson.getString("title"));
    this.setDescription(this.ruleJson.getString("description"));
    this.setType(this.ruleJson.getString("ruleType"));
    this.setRunInBackground(this.ruleJson.getBoolean("executeAsynchronously"));
    this.setApplyToSubSpaces(this.ruleJson.getBoolean("applyToChildren"));
    this.setRuleDisabled(this.ruleJson.getBoolean("disabled"));

    // create the new rule
    Rule rule = new Rule();
    rule.setRuleType(this.getType());

    // setup the rule
    this.setupRule(rule);

    // save the rule
    this.getRuleService().saveRule(targetSpace, rule);
  }

  /**
   * Sets up the given rule using rule properties provided via JSON in the constructor.
   *
   * @param rule the rule to setup
   * @throws JSONException if error parsing JSON
   */
  protected void setupRule(Rule rule) throws JSONException {

    // setup the rule 
    rule.setTitle(this.title);
    rule.setDescription(this.description);
    rule.applyToChildren(this.applyToSubSpaces);
    rule.setExecuteAsynchronously(this.runInBackground);
    rule.setRuleDisabled(this.ruleDisabled);

    JSONObject ruleAction = this.ruleJson.getJSONObject("action");

    // assume composite action
    CompositeAction compositeAction = this.getActionService().createCompositeAction();
    rule.setAction(compositeAction);

    JSONArray conditions = ruleAction.getJSONArray("conditions");
    for (int cIdx = 0; cIdx < conditions.length(); cIdx++) {
      JSONObject jsonCond = conditions.getJSONObject(cIdx);

      ActionCondition condition = createCondition(jsonCond);

      if (condition instanceof CompositeActionCondition) {
        CompositeActionCondition compositeCondition = (CompositeActionCondition) condition;

        compositeCondition.setORCondition(jsonCond.getBoolean(CompositeConditionHandler.PROP_CONDITION_OR));
        compositeCondition.setInvertCondition(jsonCond.getBoolean(CompositeConditionHandler.PROP_CONDITION_NOT));

        JSONArray subconditions = jsonCond.getJSONArray("conditions");

        for (int sIdx = 0; sIdx < subconditions.length(); sIdx++) {
          JSONObject jsonSubCond = subconditions.getJSONObject(sIdx);

          compositeCondition.addActionCondition(createCondition(jsonSubCond));
        }
      }
      compositeAction.addActionCondition(condition);
    }

    // add all the actions to the rule
    JSONArray actions = ruleAction.getJSONArray("actions");
    for (int aIdx = 0; aIdx < actions.length(); aIdx++) {
      JSONObject jsonAction = actions.getJSONObject(aIdx);

      String actionName = jsonAction.getString(PROP_ACTION_NAME);
      this.action = actionName;

      JSONObject jsonParams = jsonAction.getJSONObject("parameterValues");
      Map actionProps = this.convertJsonToMap(jsonParams);

      // get the action handler to prepare for the save
      Map repoActionParams = new HashMap();
      IHandler handler = this.actionHandlers.get(this.action);
      if (handler != null) {
        handler.prepareForSave(actionProps, repoActionParams);
      }

      // add the action to the rule
      Action action = this.getActionService().createAction(actionName);
      action.setParameterValues(repoActionParams);
      compositeAction.addAction(action);
    }
  }

  /**
   * Creates an action condition based on a JSON object
   *
   * @param jsonCond JSON object containing condition properties
   * @return an action condition
   * @throws JSONException if error parsing JSON object
   */
  private ActionCondition createCondition(JSONObject jsonCond) throws JSONException {
    String conditionName = jsonCond.getString(PROP_CONDITION_NAME);

    Map repoCondParams = new HashMap();

    Map conditionProps = this.convertJsonToMap(jsonCond);

    // get the condition handler to prepare for the save
    IHandler handler = this.conditionHandlers.get(conditionName);
    if (handler != null) {
      handler.prepareForSave(conditionProps, repoCondParams);
    }

    // add the condition to the rule
    ActionCondition condition = this.getActionService().createActionCondition(conditionName);
    condition.setParameterValues(repoCondParams);

    // specify whether the condition result should be inverted
    Boolean not = (Boolean) conditionProps.get(BaseConditionHandler.PROP_CONDITION_NOT);
    if (not == null) {
      not = Boolean.TRUE;
    }
    condition.setInvertCondition(((Boolean) not).booleanValue());
    return condition;
  }

  /**
   * Converts a JSON object to a Map
   *
   * @param jsonObj JSON object to convert
   * @return a Map where key is a String and value is a Serializable object
   * @throws JSONException if error parsing JSON object
   */
  @SuppressWarnings("rawtypes")
  protected Map convertJsonToMap(JSONObject jsonObj) throws JSONException {

    // convert JSON to Map
    Map propMap = new HashMap();
    Iterator keyIt = jsonObj.keys();
    while (keyIt.hasNext()) {
      String key = (String) keyIt.next();
      String value = jsonObj.getString(key);
      Serializable propValue = null;

      // convert repository paths to node references
      if (PROP_SCRIPT.equals(key) || PROP_DESTINATION.equals(key)) {

        NodeService nodeService = this.serviceRegistry.getNodeService();
        NodeRef nodeRef = null;
        String storeProtocol = null;
        String storeIdentifier = null;
        String nodeUUID = null;

        // is value a node reference?
        if (NodeRef.isNodeRef(value)) {
          nodeRef = new NodeRef(value);

          storeProtocol = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_STORE_PROTOCOL);
          storeIdentifier = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_STORE_IDENTIFIER);
          nodeUUID = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_NODE_UUID);

        } else {

          // assume we have a repository path relative to Company Home
          // search for the path within Alfresco content repository
          String nodePath = "workspace/SpacesStore/" + value;
          nodeRef = this.repository.findNodeRef("path", nodePath.split("/"));

          if (nodeRef == null) {
            throw new JSONException("Path " + nodePath + " not found.");

          } else {
            storeProtocol = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_STORE_PROTOCOL);
            storeIdentifier = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_STORE_IDENTIFIER);
            nodeUUID = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_NODE_UUID);

            logger.debug("Original parameter value: " + value);
            value = storeProtocol.concat("://").concat(storeIdentifier).concat("/").concat(nodeUUID);
            logger.debug("Modified parameter value: " + value);
          }
        }
        if (PROP_SCRIPT.equals(key)) {
          propValue = value;

        } else if (PROP_DESTINATION.equals(key)) {
          propValue = nodeRef;
        }
      }
      if (propValue == null) {
        if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) {
          propValue = jsonObj.getBoolean(key);
        }
      }
      if (propValue == null) {
        try {
          propValue = jsonObj.getInt(key);

        } catch(JSONException jsone) {
          // not an integer, continue
        }
      }
      if (propValue == null) {
        try {
          propValue = jsonObj.getDouble(key);

        } catch (JSONException jsone) {
          // not a double, continue
        }
      }
      if (propValue == null) {
        try {
          propValue = jsonObj.getLong(key);

        } catch (JSONException jsone) {
          // not a long, continue
        }
      }
      if (propValue == null) {
        // default to String
        propValue = value;
      }
      propMap.put(key, propValue);
    }
    return propMap;
  }
}

Full Java Source for RuleUtilScript

package ca.abstractive.ecm.alfresco.jscript.repo;

import java.util.List;

import org.alfresco.model.ContentModel;
import org.alfresco.repo.jscript.BaseScopableProcessorExtension;
import org.alfresco.repo.jscript.ScriptNode;
import org.alfresco.repo.model.Repository;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.rule.RuleType;
import org.alfresco.service.namespace.QName;
import org.apache.log4j.Logger;
import org.json.JSONException;
import org.json.JSONObject;

import ca.abstractive.ecm.alfresco.rules.JsonRuleCreator;

public class RuleUtilScript extends BaseScopableProcessorExtension {

  protected ServiceRegistry serviceRegistry = null;

  protected Repository repository = null;

  public RuleUtilScript() {
  }

  /**
   * Creates a rule (described by the given JSON) on the given space.
   * See {@link ca.abstractive.ecm.alfresco.rules.AtkJsonRuleCreator} for sample JSON.
   * Assumes the current user has permission to create rules on the space.
   *
   * @param targetSpace the space to create the rule on
   * @param ruleJson a JSON string describing the rule
   * @throws JSONException if error parsing JSON
   */
  public void createRule(ScriptNode targetSpace, String ruleJson) throws JSONException {
    JSONObject json = new JSONObject(ruleJson);

    JsonRuleCreator jrc = new JsonRuleCreator(this.serviceRegistry, this.repository);
    jrc.createRule(targetSpace.getNodeRef(), json);
  }

  public ServiceRegistry getServiceRegistry() {
    return serviceRegistry;
  }

  public void setServiceRegistry(ServiceRegistry serviceRegistry) {
    this.serviceRegistry = serviceRegistry;
  }

  public Repository getRepository() {
    return repository;
  }

  public void setRepository(Repository repository) {
    this.repository = repository;
  }
}

References: