Archive for the ‘web script’ tag
Alfresco: Permissions Web Scripts
A couple of months back I was asked to write a couple of web scripts to help one of our customers to be able to check and modify permissions for content/spaces in the Alfresco repository. I’ve finally had the chance to spend sometime testing and now writing about them.
The core of the web scripts was quick to write. The fun (more time consuming) part was working with exception handling in javascipt. I know tons of fun right! There are few different ways to use exception handling based on which version of Alfresco you are using. The customer is on Enterprise 3.1 and I wanted to make sure that the web scripts also worked on the more current releases of Alfresco as well. A change (re: addition) was made in Enterprise 3.2.1 and Community 3.3 to help simplify exception handeling. I’ll talk about exception handling and these differences in a follow up post. For now let’s talk about these new web scripts.
permissions GET
The first web script returns all of the permissions for a specified node.
The URL used is /alfresco/service/permissions/{store_type}/{store_id}/{id}
Where
store_type: The type of store you want to query, ex: workspace
store_id: The ID of the store you want to query, ex: SpacesStore
id: The UUID of the node, ex: aed218e8-df44-4865-84cd-0105252f4993
The above values are joined together to form the nodeRef.
If the node is not found a 404 error will be returned, any missing URI parameters will result in a 400 error and if you don’t have permission to view the node you will get a 401 error.
The web script will return a JSON object that looks like the following:
{ "permissions": [
"ALLOWED;user1;Coordinator",
"ALLOWED;user2;Coordinator"
] ,
"inherit": false }
The return object lists the permissions in a triplet for that node. The permissions triplet follow this format:
[ALLOWED|DENIED];[USERNAME|GROUPNAME];PERMISSION
It also returns a boolean value indicating if some permissions are inherited from the parent node.
The above example shows two permissions are assigned to the node: the Coordinator permission is given to user1 and user2 on this node. Permissions are not inherited from the parent node.
permissions POST
This web script enables you to modify the permissions for a given node
It is called through the same URL as the above web script but as a POST instead of a get: /alfresco/service/permissions/{store_type}/{store_id}/{id}
Again, if the node is not found a 404 error will be returned, any missing URI parameters will result in a 400 error and if you don’t have permission to modify the node, you will get a 401 error.
You must also pass a JSON object containing the permissions that are being changed, deleted or added.
{ permissions: [
"REMOVE;user3;All",
"REMOVE;user2;All",
"ADD;user4;Coordinator",
"ADD;GROUP_usergroup1;Consumer"
] ,
"inherit": false }
The above example uses the following triplet to define a permission
[ADD|REMOVE];[USERNAME|GROUPNAME];PERMISSION
Where the values are defined as:
ADD | REMOVE: Do you want to add or remove the permission for this user/group? Any other value passed will result in a 400 error.
USERNAME | GROUPNAME: The user or group you want the permission to be added or removed for. Group names must be prefixed by GROUP_. Unknown users or groups will result in a 400 error.
PERMISSION: The supported permissions options are defined in
org.alfresco.service.cmr.security.PermissionService or through custom extension to the permission model. Unknown permissions will result in a 400 error.
The object can also contain an optional inherit permission value to specifying if the permissions for this node should be inherited from the parent node. Without the inherit option, the current value for the node is maintained. Inherited permissions can not be removed from a node.
The return format is the same as the return format of the permissions GET web script above.
This web script is also transactional: any errors will result in the node being returned to the state before the call was made. (The exception handling in the controller was added for these conditions.)
These scripts can be installed as an AMP. The code and AMPs are hosted in the alfresco-permissions-webscripts project on Google Code. The code is available for either pre-3.2.1 (starting with 3.1) or 3.2.1 to 3.3.1. These are all Enterprise releases numbers. The web scripts have been tested against these releases. There may not be any need to modify the web scripts for Community releases (except for the min and max version numbers in the module.properties file). Pre community 3.3 should use the pre 3.2.1 release. No community releases have been tested. (If you try these on a community releases, please comment either here or in the Google Code Project.)
In a follow up post, I’ll cover exception handling with JavaScript.
Web Script: Modified Content List
For a recent customer engagement I was asked to write a web script which they’ve agreed to let me share.
The script was written to give them:
- A list of content that had been modified from a point in time until “now” (The time of execution).
- Specify a specific space or recurse the child spaces for the named space.
- return a specified XML structure
In preparing for this post I’ve added some modifications/fixes:
- I’ve added two new return formats: JSON and HTML
- fixed an issue that allows it to run on the 3.2.x release of Alfresco.
Here are some insights to a few areas of the script:
Lucene Date Queries
There are two specific things to remember about Date queries:
1 – Date Format. Date queries with Lucene are looking for the date in an ISO8601 format using combined date and time in UTC. (Even if you are looking just for the date it will complain if you don’t pass the time ).
To do this I had originally used the JavaScript Date prototype from http://delete.me.uk/2005/03/iso8601.html. This works fine in pre 3.2 releases. But due to a change in the 3.2 code line top level objects are now sealed which means you can’t make these type of prototype change to Root level objects in the JavaScript libraries. So I’ve made simple modification to Date prototype code allowing me to pass a Date Object into the code which then does the coversion and returns a properly formated ISO8601 string.
function toISO8601String(date) { // based on http://delete.me.uk/2005/03/iso8601.html
/*
* accepted values for the format [1-6]: 1 Year: YYYY (eg 1997) 2 Year and
* month: YYYY-MM (eg 1997-07) 3 Complete date: YYYY-MM-DD (eg 1997-07-16) 4
* Complete date plus hours and minutes: YYYY-MM-DDThh:mmTZD (eg
* 1997-07-16T19:20+01:00) 5 Complete date plus hours, minutes and seconds:
* YYYY-MM-DDThh:mm:ssTZD (eg 1997-07-16T19:20:30+01:00) 6 Complete date
* plus hours, minutes, seconds and a decimal fraction of a second
* YYYY-MM-DDThh:mm:ss.sTZD (eg 1997-07-16T19:20:30.45+01:00)
*/
if (!format) {
var format = 6;
}
if (!offset) {
var offset = 'Z';
var date = date;
} else {
var d = offset.match(/([-+])([0-9]{2}):([0-9]{2})/);
var offsetnum = (Number(d[2]) * 60) + Number(d[3]);
offsetnum *= ((d[1] == '-') ? -1 : 1);
var date = new Date(Number(Number(date) + (offsetnum * 60000)));
}
var zeropad = function(num) {
return ((num < 10) ? '0' : '') + num; }
var str = "";
str += date.getUTCFullYear(); if (format > 1) {
str += "-" + zeropad(date.getUTCMonth() + 1);
}
if (format > 2) {
str += "-" + zeropad(date.getUTCDate());
}
if (format > 3) {
str += "T" + zeropad(date.getUTCHours()) + ":"
+ zeropad(date.getUTCMinutes());
}
if (format > 5) {
var secs = Number(date.getUTCSeconds() + "."
+ ((date.getUTCMilliseconds() < 100) ? '0' : '')
+ zeropad(date.getUTCMilliseconds()));
str += ":" + zeropad(secs);
} else if (format > 4) {
str += ":" + zeropad(date.getUTCSeconds());
}
if (format > 3) {
str += offset;
}
return str;
}
I’ve turned the original prototyping into a function, where I pass a Date object, with the date I want to work with, into this function. I also replaced any reference to ‘this’ with that data parameter. I will admit there is some excess code here that may never be called. But this was the quickest change to accomplish what I needed.
2 – Only dates are indexed. By default, only the date portion of the property is indexed. In general, most use cases only require the date . But in this case, the customer was interested in what may have changed over the period of an hour.
There are two options for this: Code around or modify how Alfresco indexes these DateTime properties. In this case I choose to code around it. This means that I am not modifying default behavior in Alfresco…always a plus.
Code Around. We have all the pieces we need to do this: We know the datetime we use for the start and end our range query. We also have access to the full datetime property (even if it wasn’t indexed).
I chose to pull this into a function that takes the collection (array) of documents found in our range query and then test to see if the datetime property of the node (in this case the modified datetime property) is between our the beginning and end of our range, if so then add it to our new filtered results array.
function filterResults(unfilteredResults) {
var now = new Date();
var filteredResults = new Array();
for each (node in unfilteredResults) {
var testDate = new Date(node.properties.modified);
//if the nodes modified date is between the passed date/time and now add
//to filteredResult Array
if ((filterDate <= testDate) && (testDate <= now)){
filteredResults.push(node);
}
}
return filteredResults;
}
This is clean and simple.
Modify Alfresco. While this sounds heavy, it actually isn’t. Our support team pointed me at this forum post in which Andy Hind from our engineering team shows us how to change the default behaviour:
In dataTypeAnalyzers.properties (located inside the war file in WEB-INF/classes/alfresco/model) change:
d_dictionary.datatype.d_datetime.analyzer=org.alfresco.repo.search.impl.lucene.analysis.DateAnalyser
to
d_dictionary.datatype.d_datetime.analyzer=org.alfresco.repo.search.impl.lucene.analysis.DateTimeAnalyser
Now rebuild your indexes. Depending on the amount of content in your repository it this may take some time. It may also not be possible to make this immediate because the SLA you have with your customers won’t permit it until you have scheduled maintenance downtime. (HA Clustering is helpful in these situations.)
System Folders
One thing that may show up in these queries are system folders. These folders contain any of the rules that may be associated with a space. In most queries these folders and their content has no value, so we want to exclude them.
This string can be appended to your query string to exclude them:
var systemFolder = "-TYPE:\"cm:systemfolder\" " +
"-TYPE:\"{http://www.alfresco.org/model/rule/1.0}rule\" " +
"-TYPE:\"{http://www.alfresco.org/model/action/1.0}compositeaction\" " +
"-TYPE:\"{http://www.alfresco.org/model/action/1.0}actioncondition\" " +
"-TYPE:\"{http://www.alfresco.org/model/action/1.0}action\" " +
"-TYPE:\"{http://www.alfresco.org/model/action/1.0}actionparameter\"";
PARENT vs PATH
When we are performing queries for content within a space we have a couple of options. Two of the most common are PARENT and PATH.
PARENT queries work directly on a space. No recursion. It will return all of the nodes (spaces and content) in that space. In other words, PARENT queries work directly on a space without recursion. It will return all of the nodes (spaces and content) in that space alone, but will not return any nodes (spaces and content) that are in sub-spaces of that space.
PATH queries are a subset of XPATH. They are eagerly evaluated. Thus they can be memory (Caching) and CPU intensive. They are useful if you want more than what is just in the space you are working, may not know the location of the space, or if a space of that name may exist in multiple locations.
Be smart in your choice. Use PARENT as often as possible.
Note For Java extensions: unless the other clauses in your query are complex, it’s likely more efficient to enumerate (list / ls / dir) a space using the FileFolderService rather than a Lucene query. ie. a simple query of the form “PARENT:[noderef]” would be better implemented using FileFolderService.list()
OK, enough about some of our design decisions let’s talk about how to use the web script.
Install
The web script is packaged in an amp file. It will work on Alfresco 3.1.1 and newer versions. (I tested up to Alfresco 3.2r) The alfresco-modified-content-list amp and the source can be found on the google code project site. Use the apply_amps script appropriate for your OS.
How to use it
The web script can be called using:
http://localhost:8080/alfresco/service/updated/in/{path}/since?date=01/01/2010 13:10:10
http://localhost:8080/alfresco/service/updated/{path}/since?date=01/01/2010 13:10:10
There is a slight difference in the two URLs: ‘in’
The ‘in’ allows you to tell the web script to just list the content in the space passed in the path parameter. Without the ‘in’ the web script will recurse the space structure from the passed path parameter down.
Next, the DateTime structure is not fixed: Possible (tested) formats are
mm/dd/yyyy
yyyy/mm/dd
mm/dd/yyyy hh:mm:ss
yyyy/mm/dd hh:mm:ss
The hh is expecting a 24 hour clock format. And millisecond and timezone tests are not supported
The query tests against the modified datetime property of the content. (Fairly easy to change it and test against the created property or any other custom metadata DataTime property.)
There are three return formats: XML (the default), JSON and HTML.
XML The XML format is a simple structure that returns back the name of the file, the path to the file and the modification date. It is also the default format returned.
<contentItems> <contentItem name='simple.doc' path='/Company Home/test/Portal' modDate='2010-04-26T16:21:20.612-06:00'/> <contentItem name='simpledraft.docx' path='/Company Home/test/Portal' modDate='2010-04-26T16:28:44.569-06:00'/> </contentItems>
If there is no modified content a simple <contentItems/> is returned.
JSON The JSON object is somewhat similar, a modified object containing a collection of node objects is returned
{ "modified": [
{ "node": {
"name": "test3.txt", "path": "some/path","modified": "2010-03-17T00:05:43.311-06:00"
}
},
{ "node": {
"name": "test2.txt", "path": "some/path","modified": "2010-03-17T15:07:30.301-06:00"
}
}
]
}
HTML This final format is more for testing than actual production use. It returns a simple html page with a list of modified items. It displays a simple unordered list, where each item is a csv list with key and values seperated by colons.
In all of these return formats, the freemaker template processes the returned scriptNodes for a simple collection. This makes it easy to extend the return with the returned nodes properties to include things like nodeRef, creation date, node icons, download url, etc.
If you have suggestions for improvements or questions please feel free to comment here or on the google project. As always I’m willing to open access to the project to those will to help improve the code.
Alfresco WCM Forms: Unique Identifier
From time to time I’ve seen the need to provide a unique identifier in content created through the Alfresco WCM Forms Service. The reason why has varied from needing an unique identifier across renditions to needing a way to tie the content to User Generated Content (UGC) or some other database managed application. A simple solution is to leverage Java generated Universally Unique Identifiers (UUID) imported into the Schema Definition of your form via a Java Backed Web Script. Note: These are not the UUIDs used by Alfresco DM. UUIDs are not available/part of the AVM/WCM model used by Alfresco.
Java Backed Web Scripts provide a powerful way to include complex business process logic, enabling integrations through the Alfresco Web Script framework using a simple HTTP based API. In this case, we are using a simple Java class to generate a UUID and then returning that UUID wrapped in a XML Scheme Definition imported through a <xs:include> into a Schema Definition used to generate a form used to capture content in Alfresco’s WCM service.
First let’s tackle the Java portion. This is a simple Java class that extends the Alfresco DeclarativeWebScript class. A Declarative Web Script allows you to mix Java classes, Freemarker templates (FTL) and Javascript. In this case we a want to leverage a FTL, which generates the XSD for the return. Here is the meat of our class:
public class UUIDWebScript extends DeclarativeWebScript
{
@Override
protected Map executeImpl(WebScriptRequest req,
Status status, Cache cache)
{
Map model = new HashMap();
UUID uuid = UUID.randomUUID();
model.put("uuid", uuid.toString());
return model;
}
}
Basically,
- Create a
mapof properties you want to return (by coding conventions this is called a model). - Perform your business logic, in our case generate a UUID.
- Add the values you want to return to the template with a unique name to identify it in the template
- Return the map of properties
Next we want to format our return. This is done using a Freemarker Template . (Freemarker is a Java Based markup language.) Here is ours:
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:alfresco="http://www.alfresco.org/alfresco"
elementFormDefault="qualified">
<xs:complexType name="uuid">
<xs:sequence>
<xs:element name="uuid" fixed="${uuid}" type="xs:normalizedString"/>
</xs:sequence>
</xs:complexType>
</xs:schema>
The FTL, as you can see , is a simple XSD. The value returned from our Java class, in the model, named uuid, is declared as ${uuid}.
Now let’s declare the web script so that it can be registered in Alfresco:
<webscript>
<shortname>Generate UUID</shortname>
<description>Returns a UUID</description>
<url>/util/uuid</url>
<authentication>none</authentication>
<format default="xml">argument</format>
</webscript>
And since we are using a Java class in our Web Script, we also need to declare it so that the Web Script framework can use it:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="webscript.org.alfresco.extension.uuid.uuid.get"
class="org.alfresco.extension.uuid.UUIDWebScript"
parent="webscript">
</bean>
</beans>
It is important to note here that the bean id for a Java backed Web Script requires a very specific format:
- Start by declaring this bean a webscript with: webscript.
- Next add the full Java package declaration: org.alfresco.extension.uuid
- Followed by the name used in your FTL and XML declaration with the method used to access the web script: uuid.get
- When packaged, the FTL and XML declaration must be in a folder structure that matches the Java package declaration.
This puts together all of the pieces on the Web Script side. It is easiest to manage this through an AMP file, which we won’t discuss here how to create or install, but code and amp file are available at the project page: http://code.google.com/p/alfresco-uuid-webscript/
Once installed we can verify that it works by going to http://localhost:8080/alfresco/service/util/uuid in your browser (replace localhost and port as needed for your install). What you should see returned is an XSD that looks something like this:
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:alfresco="http://www.alfresco.org/alfresco"
elementFormDefault="qualified">
<xs:complexType name="uuid">
<xs:sequence>
<xs:element name="uuid" fixed="9c2fce39-3351-47c9-bb05-4b002967a00d" type="xs:normalizedString"/>
</xs:sequence>
</xs:complexType>
</xs:schema>
It is our template but with are declaration of ${uuid} replaced with aUUID: 9c2fce39-3351-47c9-bb05-4b002967a00d. With each call to this Web Script you will see a uniquely generated ID.
Now let’s integrate this with a simple XSD:
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema
xmlns:alf="http://www.alfresco.org"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:sample="http://www.alfresco.org/alfresco/sample"
elementFormDefault="qualified"
targetNamespace="http://www.alfresco.org/alfresco/sample">
<xs:include schemaLocation="webscript://util/uuid" />
<xs:element name="sample">
<xs:complexType>
<xs:sequence>
<xs:element name="sample" type="xs:normalizedString" minOccurs="1" maxOccurs="1"/>
<xs:element name="key" type="sample:uuid"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
The important thing here is how we reference the Web Script, and you can do this with any custom Web Script designed to render XML Schema to be included in a form, by referencing the call with the declaration of webscript:
<xs:include schemaLocation="webscript://util/uuid" />
Add this XSD to your Web Project and give it a run! In the form you will see the UUID, as a read-only field.

Now in your XML output you will have a unique key that you can reference in your templates.