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: {http://www.alfresco.org/model/content/1.0}person, Node Aspects: [...]",
"Node Type: {http://www.alfresco.org/model/content/1.0}person, Node Aspects: [...]",
"Node Type: {http://www.alfresco.org/model/content/1.0}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 {http://www.alfresco.org/model/content/1.0}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;
        json.append(node.toJSON(true));

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

          json.append(",");
          Object value = array.get(i, array);
          node = (ScriptNode) value;
          json.append(node.toJSON(true));
        }
        json.append("]");
        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": [
 "app:configurable",
 "cm:ownable",
 "sys:referenceable",
 "sys:localized"
 ],
 "nodeRef": "workspace://SpacesStore/770df4a0-78ff-4c46-aca9-0de5ff12345",
 "properties": {
 "cm:name": "770df4a0-78ff-4c46-aca9-0de5ff512345",
 "sys:node-dbid": 27,
 "cm:email": "admin@alfresco.com",
 "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": [
 "cm:ownable",
 "sys:referenceable",
 "sys:localized",
 "cm:personDisabled",
 "cm:preferences"
 ],
 "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": "jane.doe@example.com",
 "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": [
 "app:configurable",
 "cm:ownable",
 "sys:referenceable",
 "sys:localized"
 ],
 "nodeRef": "workspace://SpacesStore/c530d479-9a78-4c14-94c7-933b46412345",
 "properties": {
 "cm:name": "c530d479-9a78-4c14-94c7-933b46412345",
 "sys:node-dbid": 6939,
 "cm:organizationId": "",
 "cm:email": "john.doe@example.com",
 "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"
 }
 ]

Custom Share role breaks existing sites solution

ADDED: Your new group is added to the ACL of both site and document library spaces with the role taken from your new group name i.e. new group is  GROUP_site_SOMESITEID_SiteCollaboratorPlus, SiteCollaboratorPlus is the role.

When you create a custom role in Share you must know up front what roles you need – before your project begins because it breaks your existing sites if you deploy the role after the fact. This isn’t always how things work out so here is a quick little webscript that will fix existing sites when deploying a new custom role.

This is a java backed webscript that will search all existing sites that do not have your custom role security group. It will then take your new role name via Spring and dynamically add it to all sites in share (I put it in spring because we don’t want people creating new roles to ALL sites via a URL parameter).

If you are un-familiar with how the files talk to eachother or where to put things please see the Alfresco Wiki on Java backed webscripts. Once the webscript is installed, simply go to http://localhost/alfresco/wcs/ecm/sites/fixrole (Make sure you change the spring context to add YOUR custom role name)

The context file (webscript-context.xml):

This descriptor file is where you want to put your custom group name that its complaining about. Just replace your siteid with the :SITE param you see below.

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN 2.0//EN' 'http://www.springframework.org/dtd/spring-beans-2.0.dtd'>

<beans>

    <bean id="webscript.ca.abstractive.ecm.alfresco.sitefix.fix-role.get" 

      parent="webscript">

        <property name="serviceRegistry" ref="ServiceRegistry" />
        <property name="searchService" ref="SearchService" /> 
        <property name="nodeService" ref="NodeService" />
        <property name="authorityService" ref="AuthorityService" /> 

        <!-- Custom role SiteCollaboratorPlus -->
        <property name="authorityName">
            <value>GROUP_site_:SITE_SiteCollaboratorPlus</value>
        </property>  
    </bean>
</beans>

The webscript descriptor (fix-role.get.desc.xml):

<webscript>
  <shortname>Fix Share Roles</shortname>
  <description>Adds new role to all existing sites</description>
  <url>/ecm/sites/fixrole</url>
  <authentication>user</authentication>
  <format default="">argument</format>
  <family>Toolkit</family> 
</webscript>

The Java (UtilityRoleCreation.java):

package ca.abstractive.ecm.alfresco.sites;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.alfresco.model.ContentModel;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.search.ResultSet;
import org.alfresco.service.cmr.search.ResultSetRow;
import org.alfresco.service.cmr.search.SearchParameters;
import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.security.AuthorityType;
import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.web.bean.repository.Repository;
import org.json.JSONException;
import org.json.JSONObject;
import org.springframework.extensions.webscripts.AbstractWebScript;
import org.springframework.extensions.webscripts.WebScriptException;
import org.springframework.extensions.webscripts.WebScriptRequest;
import org.springframework.extensions.webscripts.WebScriptResponse;

public class UtilityRoleCreation extends AbstractWebScript {
 
    protected ServiceRegistry serviceRegistry = null;
    protected SearchService searchService = null; 
    protected NodeService nodeService = null;
    protected AuthorityService authorityService = null;
    protected String authorityName = null;
    protected PermissionService permissionService = null;
    
    
    /**
     * Returns a list of sites in Alfresco
     *   
     * @return a list of site nodes 
     */  
    protected List<NodeRef> getSites(){
    
        //Get all sites
        String nodeQuery = "TYPE:\"st:site\"";

        SearchParameters params = new SearchParameters();
        params.setLanguage(SearchService.LANGUAGE_LUCENE);
        params.addStore(Repository.getStoreRef());
        params.setQuery(nodeQuery);
        ResultSet results = null;
        List<NodeRef> siteNodeRefs = new ArrayList<NodeRef>();
        
        try {
            results = this.searchService.query(params);
            if (results == null) {
                return null;

            } else {

                Iterator<ResultSetRow> siteNodes = results.iterator();
                while(siteNodes.hasNext()){
                     
                     ResultSetRow siteRow = siteNodes.next();
                     NodeRef nodeRef = siteRow.getNodeRef();
                     
                     siteNodeRefs.add(nodeRef);
                }
                
                return siteNodeRefs;
            }
            
        } finally {
            if (results != null) {
                results.close();
            }
        } 
         
    }    
    
    /**
     *  Webscript execute method
     *  
     */
    public void execute(WebScriptRequest req, WebScriptResponse res) 
            throws IOException  {
         
        try
        { 
            
            // build a json object
            JSONObject obj = new JSONObject();
            
            List<NodeRef> sites = this.getSites();
              
            Iterator<NodeRef> siteNodes = sites.iterator();
            while(siteNodes.hasNext()){
                 
                NodeRef nodeRef = siteNodes.next();
                  
                 //Get the sitename
                 String siteid = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_NAME);  
                 String siteGroup = new StringBuilder().append("GROUP_site_").append(siteid).toString();
                       
                 if(this.authorityService.authorityExists(siteGroup)){
                    
                    Set<String> authorities = this.authorityService.getContainedAuthorities(AuthorityType.GROUP, siteGroup, true);
                    String customRoleAuthorityName = this.authorityName.replace(":SITE", siteid);
                    
                    if (authorities.contains(customRoleAuthorityName) == false)
                     { 
                        obj.put(siteGroup, customRoleAuthorityName);
                        
                        //create the authority
                        String newAuthName = this.authorityService.createAuthority(AuthorityType.GROUP, customRoleAuthorityName.replace("GROUP_", "")); 
                        this.authorityService.addAuthority(siteGroup, newAuthName);
                         
                        // Assign the group the relevant permission on the site and document library
                        String[] bits = customRoleAuthorityName.split("_");
                        String role = bits[bits.length-1];
                        
                        permissionService.setPermission(nodeRef, customRoleAuthorityName, role, true);                            
                        NodeRef libraryNodeRef = nodeService.getChildByName(nodeRef, ContentModel.ASSOC_CONTAINS, "documentLibrary");
                        permissionService.setPermission(libraryNodeRef, customRoleAuthorityName, role, true);
                     }
                 }
            }
             
            // build a JSON string and send it back
            String jsonString = obj.toString();
            res.getWriter().write(jsonString);
        }
        catch(JSONException e)
        {
            throw new WebScriptException("Unable to serialize JSON");
        } 
    }
    
     
    
    /* ------------------------------------------------------------ */
    /* Getter/Setter methods */
    /* ------------------------------------------------------------ */
    
    /**
     * @return the serviceRegistry
     */
    public ServiceRegistry getServiceRegistry() {
        return serviceRegistry;
    }

    /**
     * @param serviceRegistry the serviceRegistry to set
     */
    public void setServiceRegistry(ServiceRegistry serviceRegistry) {
        this.serviceRegistry = serviceRegistry;
        this.searchService = serviceRegistry.getSearchService(); 
        this.nodeService = serviceRegistry.getNodeService();
        this.permissionService = serviceRegistry.getPermissionService();
    }

    /**
     * @return the searchService
     */
    public SearchService getSearchService() {
        return searchService;
    }

    /**
     * @param searchService the searchService to set
     */
    public void setSearchService(SearchService searchService) {
        this.searchService = searchService;
    }
    
    /**
     * @return the authorityService
     */
    public AuthorityService getAuthorityService() {
        return authorityService;
    }

    /**
     * @param authorityService the searchService to set
     */
    public void setAuthorityService(AuthorityService authorityService) {
        this.authorityService = authorityService;
    }
      

    /**
     * @return the nodeService
     */
    public NodeService getNodeService() {
        return nodeService;
    }

    /**
     * @param nodeService the nodeService to set
     */
    public void setNodeService(NodeService nodeService) {
        this.nodeService = nodeService;
    }


    public String getAuthorityName() {
        return authorityName;
    }


    public void setAuthorityName(String authorityName) {
        this.authorityName = authorityName;
    }
    
    /**
     * Set permission service
     */
    public void setPermissionService(PermissionService permissionService)
    {
        this.permissionService = permissionService;
    }  
    public PermissionService getPermissionService()
    {
        return permissionService;
    }
}

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