Jared Ottley

Technology | Fatherhood | Insanity

Archive for the ‘Alfresco’ Category

Alfresco: Permissions Web Scripts

with 2 comments

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.

Written by jared

August 26th, 2010 at 1:05 pm

Posted in Alfresco

Tagged with , ,

Alfresco: JMX from the command line

without comments

JMX is great. Alfresco and JMX is awesome. I’ve written before about configuring Alfresco to use tunneling to connect JMX & debuggers to servers that don’t allow access to the higher numbered ports (or to only a few ports).  Let’s add another cool tool. (JMX is an Enterprise only feature of Alfresco.)

The Alfresco wiki covers a few of the clients that are available out there.  Let’s add another type to the list:  JMX from the command line.  There are a couple of options for us to choose from.   I am partial to jmxterm from CyclopsGroups.org

Jmxterm is an opensource  JMX Client (download).  It supports auto completion, history browsing and scripting.  In a word: cool.

Let’s jump in…

First thing you need to do with the client is connect

With jmxterm you can pass a connection string via the jar to connect to Alfresco

java -jar jmxterm-1.0-alpha-4-uber.jar -l \ service:jmx:rmi:///jndi/rmi://<host>:50500/alfresco/jmxrmi -p <password> -u <user>

Or, you can connect via the interactive shell

java -jar jmxterm-1.0-alpha-4-uber.jar

Next, using the open command connect to Alfresco

$>open service:jmx:rmi:///jndi/rmi://localhost:50500/alfresco/jmxrmi -p change_asap -u \ controlRole
#Connection to service:jmx:rmi:///jndi/rmi://localhost:50500/alfresco/jmxrmi is opened

Or, using the jvms command (with the appropriate user permissions you can list the running java processes on the machine jmxterm is running on).

$>jvms
94822    ( ) - jmxterm-1.0-alpha-4-uber.jar
13157    (m) - org.apache.catalina.startup.Bootstrap start

And once again use the open command to connect

$>open 13157 -u controlRole -p change_asap
#Connection to 13157 is opened

Now that you are connected  let’s perform a simple operation, finding out who is logged into Alfresco and then disconnect one of the users.

First you’ll list the domains that are available

$>domains
#following domains are available
Alfresco
Catalina
JMImplementation
Users
axis
com.sun.management
connector
java.lang
java.util.logging
log4j

The bean you need is in the Alfresco domain

$>domain Alfresco
#domain is set to Alfresco

You can look up the beans within the domain by using the beans command.  There are quite a few beans in the Alfresco domain, so let’s connect directly to the one that you need: RepoServerMgmt. (Don’t forget jmxterm support auto completion.)

$>bean Alfresco:Name=RepoServerMgmt
#bean is set to Alfresco:Name=RepoServerMgmt

Now that you have the bean, you can now find out what it does by using the info command

$>info
#mbean = Alfresco:Name=RepoServerMgmt
#class name = org.alfresco.repo.admin.RepoServerMgmt
# attributes
%0   - LinkValidationDisabled (boolean, r)
%1   - MaxUsers (int, r)
%2   - ReadOnly (boolean, r)
%3   - TicketCountAll (int, r)
%4   - TicketCountNonExpired (int, r)
%5   - UserCountAll (int, r)
%6   - UserCountNonExpired (int, r)
# operations
%0   - int invalidateTicketsAll()
%1   - int invalidateTicketsExpired()
%2   - void invalidateUser(java.lang.String p1)
%3   - [Ljava.lang.String; listUserNamesAll()
%4   - [Ljava.lang.String; listUserNamesNonExpired()
#there's no notifications

There are lots of things you can do here, you can get attributes, you can set attributes, you can run opertaions.

Looking at the returned attributes you can tell a few things: the datatype of each attribute and if you can change it.  In this case none of our attributes can be modified, they are all read only (r).  ReadWrite would be signified by rw.

Operations are just like Java functions: they can have a return type and you might be able pass parameters to them.

It can also list notifcations, but there is no mechanism (that I have found yet) in jmxterm to subscribe or unsubscribe to them.

Let's do some more work:

First let's see how many users are logged in.

$>get UserCountNonExpired
#mbean = Alfresco:Name=RepoServerMgmt:
UserCountNonExpired = 2;

And now let's find out who these two users are

$>run listUserNamesNonExpired
#calling operation listUserNamesNonExpired of mbean Alfresco:Name=RepoServerMgmt
#operation returns:
[ System, admin ]

So you have 2 user currently in Alfresco: System and admin

Let’s log out the admin user.  (Look back at the list from the info command.) You can either invalidate all of the authenticated users tickets or we can invalidate named user.  Since you just want to remove one user let’s invalidate the admin users session.  (The System user is a special account, so you will ignore it.)

$>run invalidateUser admin
#calling operation invalidateUser of mbean Alfresco:Name=RepoServerMgmt
#operation returns:
null

Remember that this operation does not return anything so jmxterm returns null.

Let’s now check and see if the admin user is still logged in.

$>run listUserNamesNonExpired
#calling operation listUserNamesNonExpired of mbean Alfresco:Name=RepoServerMgmt
#operation returns:
[ System ]

You’ve now run through some basic operations: connecting to Alfresco, listing and selecting the domain, listing and selecting the bean, discovering what the bean can do, getting an attribute, running an operation.

The last things to cover are scripting/non-interactive mode and setting properties.

From time to time you might see the need, to lock Alfresco down by setting it to Read Only mode. The following two commands will help us do this.

First let’s check the current state:

$ echo get -s -b Alfresco:Name=RepoServerMgmt ReadOnly | java -jar jmxterm-1.0-alpha-4-uber.jar -l service:jmx:rmi:///jndi/rmi://localhost:50500/alfresco/jmxrmi -p change_asap -u controlRole -v silent -n

When passing commands to jmxterm, you need to echo the command into the interpreter. So the above command passes the jmxterm get command.  we pass it a -s since we just want the value and not the full expression (ReadOnly = <value>;) returned -b names the bean and finally the attribute we are querying. On the jmxterm side we pass the connection information (connection string or PID, username and password), set the verbosity of the execution, and tell it that it should run in non-interactive mode.

In this case the command returns the value false. So the value of ReadOnly is false — Alfresco is in Read/Write mode.

Now let’s put it in ReadOnly mode

$ echo set -b Alfresco:Category=sysAdmin,Type=Configuration,id1=default server.transaction.allow-writes false | java -jar jmxterm-1.0-alpha-4-uber.jar -l service:jmx:rmi:///jndi/rmi://localhost:50500/alfresco/jmxrmi -p change_asap -u controlRole -v silent -n

This follow pretty much the same pattern.  The ReadOnly attribute is a read only property, so to change Alfresco into Read Only mode you set the value of server.transaction.allow-writes to false. The set will not return any value.  You can then check the current state of the repository by running the previous command and then when you are ready to put the repository back in Read/Write mode change the value of the set command on server.transaction.allow-writes to true.

Written by jared

August 3rd, 2010 at 11:01 pm

Posted in Alfresco

Tagged with ,

Alfresco: Max Version Policy

with one comment

UPDATE:  Updated code examples to match updates to source code

As part of a POC for a customer I was asked to write an extension that allowed them to control the total number of versions allowed per versioned content. (Download links at bottom of page)

Alfresco has a strong versioning story, that gives you the ability to version any content stored in the repository, no matter what the file type.  Versions are full files and not diffs of the files.  Alfresco gives you the ability to have both major and minor versions of content.  Versions can be created/updated by checkout/checkin, by rule, through any interface or through script/APIs.

Alfresco also provides the ability to apply behaviors / policies to content/metadata within the repository.  You can think of these as event listeners, that allow you to take custom actions based on what is happening within the repository.  Jeff Potts has written an excellent tutorial on creating behaviors, with examples for both Java and javascript. (Other resources include: )

There are 4 versioning policies that we can work with:

  • beforeCreateVersion
  • afterCreateVersion
  • onCreateVersion
  • calculateVersionLabel

(For an example of a calculateVersionLabel policy look at this post and the accompanying code by Peter Monks.)

For this policy we are going to use afterCreateVersion:  after a version is created we want to remove any version that puts us past the max version value.

You start by implementing the policy interface for the policy you want to apply.

public class MaxVersionPolicy implements AfterCreateVersionPolicy

Adding the method implemented by the interface

public void afterCreateVersion(
				NodeRef versionableNode,
				Version version)

We also need to bind the behavior to the policy and we need to do this when the class is loaded by the springframework.  We wrap this registration in an init() method

	public void init() {
		this.afterCreateVersion = new JavaBehaviour(this, "afterCreateVersion",
				NotificationFrequency.TRANSACTION_COMMIT);

		this.policyComponent.bindClassBehaviour(QName.createQName(
				NamespaceService.ALFRESCO_URI, "afterCreateVersion"),
				MaxVersionPolicy.class, this.afterCreateVersion);
	}

The init method is then called when spring loads the bean

		<bean id="maxVersion"
                      class="org.alfresco.extension.versioning.MaxVersionPolicy"
                      init-method="init">
			<property name="policyComponent">
				<ref bean="policyComponent" />
			</property>
			<property name="versionService">
				<ref bean="versionService" />
			</property>
			<!-- The max number of versions per versioned node -->
			<property name="maxVersions">
				<value>10</value>
			</property>
		</bean>

Now to the meat of the policy, enforcing the removal of versions.

First we we have our maxVersion property.  This is set in the springbean (see above) and read when the bean is loaded. (You can overwrite the default value by copying the maxversionpolicy-context.xml file into the extensions directory and changing the value of the maxVersion property)

	public void setMaxVersions(int maxVersions) {
		this.maxVersions = maxVersions;
	}

Next we want to remove any version that puts us over the max

 	@Override
	public void afterCreateVersion(NodeRef nodeRef, Version version) {

		VersionHistory versionHistory = versionService
				.getVersionHistory(nodeRef);

		// If the current number of versions in the VersionHistory is greater
		// than the maxVersions limit, remove the root/least recent version
		if (versionHistory.getAllVersions().size() > maxVersions) {
			logger.debug("Removing Version: "
					+ versionHistory.getRootVersion().getVersionLabel());
			versionService.deleteVersion(nodeRef, versionHistory
					.getRootVersion());
		}
	}

We first get the versionHistory for the node and check it against the maxVersion property. If we have more versions than the limit we delete the least recent version from the version history.

10 Version Limt

Notice: 10 total versions; least recent version is 1.1; no pagination!

Q: Why are you using an if statement, instead of looping through all of the versions.  I have a few (many) more versions over the limit I want to impose?

A: A while statement would be more efficient in removing everything above the limit, but the versionHistory object isn’t being updated with each delete in a loop (I tried) until the policy has completely run.  Luckily, as you will find as you implement custom behaviors, they get called a lot!  So while a single update will result in a single version added, you will actually see that the behavior is called multiple times for that one action (In testing I saw the behavior being called a minimum of 7 times).  While you might not clear out every version over the limit with a single update, you will see a good many of them removed.  If you want to bring every down to the limit you could create a “cleaner” extension that could go in and remove all of the versions over the limit (this would be a very intensive/costly operation depending on the amount of content in your repository and the size of your version history  –  There several different strategies for this).  You could then use this extension to enforce that limit.

Q: Does this affect all versioned content in the repository?  What if I only want it to work on some content?

A: Yes, by default this policy is being applied to all versioned content, but you could add in checks to look for a specific aspect, parent space name, property, etc. before passing through the delete code.

Q: Compliance regulations/laws don’t allow me to delete the versions, but require me keep them for X years. How can I archive versions?

A: There are probably several different ways to handle this.  One way could be to use Alfresco’s content store selector.  The content store selector allows you to configure multiple underlying filesystem locations for your content.  To the end user all of the content appears to come from the same location, while the content itself lives in different disc systems. The idea would be to set up a secondary store that would act as the archive.  When your trigger is reached, be it a property (status [property or aspect], date, etc.), a version number/count, etc. have the policy move the content to the archive space structure (The archive will need to have the cm:storeSelector aspect set to mark it to use the secondary store). Because we are working from the least recent version, the 1.0 version of the content, it becomes the first version in the archive, each subsequent version, is added as a version for the content, version numbers are then maintained in the archive. I would also add an aspect to the original node that contains a pointer/aspect to the archived version, for reference. Then delete the least recent version of the original versioned node.

Google Code Project: http://code.google.com/p/alfresco-maxversion-policy/

Download Alfresco Max Version Policy AMP

Tested on Alfresco (Enterprise) 3.2 – 3.3.1

Written by jared

July 23rd, 2010 at 12:56 am

Posted in Alfresco

Tagged with , ,

UPDATED: Tunneling Debug and JMX for Alfresco

with one comment

Back in February I wrote a post on Tunneling Debug and JMX with Alfresco.  Here are a few updates to that post:

0/ For JMX: From Alfresco 3.2 sp1 (enterprise release) on you no longer need to add the custom-core-services-context.xml file.  Instead in the alfresco-global.properties file add monitor.rmi.services.port=50508 to set the static port.

1/ How can I get this work on Windows?  I’ve tested with two options: Cygwin or Putty.

Cygwin: this is option is the most straight forward.  Install Cygwin.  On the Select Packages screen do a search for openssh and select it for install.

The same ssh commands will work for creating tunnels under a Cygwin shell that you can use with Linux or Max OS X.

Putty: The only way I’ve been successful with connecting JMX with putty was by using the plink command line executable.  The graphical interface doesn’t allow you set the first hostname that is needed for the second tunnel.  plink gets this done.

Let’s take a look at some real examples…

For debug:

On the server:

Add the following to your alfresco.sh file:

export JAVA_OPTS="${JAVA_OPTS} -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8082"

The address parameter sets the port you want the debugger to listen on.  Adding this line, will require you to restart Alfresco/tomcat for it to become active.

On the client:

Using standard port forwarding with ssh will enable Java debugging using plink

plink <user>@<ip/host/dns address> -L 2000:localhost:8082 -N

The debugger is then set to connect to localhost using port 2000

For JMX:

On the server:

Modify alfresco.sh:  set -Dcom.sun.management.jmxremote equal to true.  Append -Djava.rmi.server.hostname=dummyhost to the JAVA_OPTS.

Add monitor.rmi.services.port=50508 to alfresco-global.properties

On the client:

Add 127.0.0.1 dummyhost to C:\Windows\System32\drivers\etc\hosts

Using plink from the command line, run the following two commands:

plink <user>@<ip/dns of server> -L 2001:localhost:50500 -N

plink <user>@<ip/dns of server> -L dummyhost:50508:localhost:50508 -N

Now you can connect to Alfresco using a jmx console, like jconsole (ships with jre/jdk).

the connection string will look like this:

service:jmx:rmi:///jndi/rmi://localhost:2001/alfresco/jmxrmi

Unless you’ve changed them, the default username and password are controlRole and change_asap

Written by jared

June 11th, 2010 at 5:07 pm

Posted in Alfresco

Tagged with , , , ,

Alfresco: SimpleGIS

with 2 comments

When I worked Pre-Sales here at Alfresco I had, on occasion, the chance to talk about integrating Alfresco with a GIS (Geographic Information System). I don’t have a lot of experience with these types of systems (actually none) but I always believed that integration was possible. (It falls into one of those categories where we believe that it is possible, but we have never actually seen it done.) Or even that a simple type of integration could be done with Google Maps to provide basic information.

I started playing with the Google Maps APIs almost 6 months ago and was able to throw together a rough POC, but felt that there was still more that I could do.  I’ve gotten closer to what I envisioned and a have put together a starting example, but there is still a lot of work to do, including working around issues from the underlying APIs. (ie one of my examples will not work in some versions of Firefox or IE…I’m working on these, but believe that the answer may actually be in the code of either the browser or the Maps API.)

You should note that my example is based off of US addresses.  If you are outside of the US you may need to customize these further to meet your needs, but the underlying code should work fine.

Content Map

My first attempt, used an aspect to contain the address. The fields I looked at are: address, city, state and zip code. Any of these, or any combination of these, can be passed to Google to get the coordinates. In this release, I’ve stuck with this design with a little modification.  The map that is rendered for the document (as a presentation template in the document details page) uses a static google map.  This is quick and easy, it is basically an image tag that has several parameter passed as part of the src.  This approach works find for our simple content item map, but has some limitations, for example you can’t design it provide more in-depth information like the little pop up information windows.

In both of these example maps (the document based map and the space map that we will discuss next) it requires some additional changes to the core Alfresco App.  I needed to do two things: make a call within my web script to an external resource, in this case Google Maps, to take an address turn it into map coordinates.  And have the presentation template call the web script.  With a little help from Yong Qu, I was able to address both of these.  For the first issue you can enable the ‘remote’ object in javaScript.  This is enabled by default for Alfresco Share, but not in Alfresco Explorer.  Yong has an easy to follow, example on his blog, on how to do this.

To use the remote object then, we make a simple set of calls like below:

uri = "http://maps.google.com/maps/geo?q="+encodeURI(fullAddress)+"&key="+key+"&sensor=false&output=json"

var connector = remote.connect("http");
var result = connector.call(uri);

var obj = eval("("+result+")");

coordinates = obj.Placemark[0].Point.coordinates[1]+","+obj.Placemark[0].Point.coordinates[0];

You should also note that the json that is returned to our call is returned as a string.  We will need to turn it into an javascript object to work with it by using the javaScript eval function.  Once we have done this, we can working on the json as a standard object.

This object, remote, however us not enabled for use in presentation templates. So again, borrowing from Yong, I used YUI (which we ship and use in Alfresco) to allow me to make the call out to get the map, via web script.

Our YUI code, again is pretty straight forward…make the call to the web script, set the returned code to be the html of a specified div tag:

<script src="/alfresco/yui/yahoo.js" type="text/javascript"></script>
<script src="/alfresco/yui/event.js" type="text/javascript"></script>
<script src="/alfresco/yui/connection.js" type="text/javascript"></script>

<script type="text/javascript">

 var handleSuccess = function(o){
    var div = document.getElementById('mapping');
    div.innerHTML = o.responseText; }

var handleFailure = function(o){
    var div = document.getElementById('mapping');
    div.innerHTML = "Map not available"; }

var callback = {
 	  success: handleSuccess,
 	  failure: handleFailure,
 	  argument: {} }

var request = YAHOO.util.Connect.asyncRequest('GET', '/alfresco/wcservice/map/workspace/SpacesStore/${document.id}', callback);
</script>

We want to incude the js references as the beginning. This enables us to make successful calls to this script without the wrapper of our presentation template.

Next, notice the url that we are calling, 1/ the call is relative to the application we are running. 2/We are using wcservice. This make it so that we are not prompted to login each time the web script is called.

Screen shot 2010-05-31 at 11.07.21 PM

As I mentioned above, this is the easer part especially since the hard parts were already thought out by Yong….

Space Map

For the second map, I initially wanted it to be a presentation template, with an amalgamation of spaces & content, on a single map.

The static maps API is a not a fit here.  I wanted to have a map that allowed me not to just specify the location for each child in a space, but also display a limited set of specific information about that piece of content. Note: This does mean that you may need to develop a specific “folder” based taxonomy to better organize content by location.

As I said above, my original attempts here were to try and do everything in a presentation template.  It would make multiple calls through YUI to get coordinates for the children of the space.  But YUI here becomes the bottleneck, especially since, I could not find a way to pass information out of the YUI Connection Managers callbacks.  This greatly limits the design…and ends up nesting calls to a depth of n as we loop through all of the children in a space.

One way to overcome this would be to include in the model/aspect the coordinates. This would reduce the YUI calls to zero.  One problem with this approach would be the changes to address need to result in updates to those coordinates.  This can be achieved through a behavior, watching the onUpdateProperties method for changes on those nodes. So for this first attempt, I’ve decided to skip this.  I’ll revisit this in an upcoming revision to the code.

I started my new web script by moving the logic that I worked on from the presentation template to this web script.  This was pretty straight forward.  We reused the remote object we enabled from before to pass our location (address string) back to Google.  I ignore 99% of what is returned in the json because what we are really after is the coordinates that we can pass to the Google Maps javaScript API.  I choose the javaScript API this time, because it allows me to create infoWindows where I can present the user with some basic metadata about the content along with links to either download or browse to the content.  I’m not much a UI designer so my poor attempts at layout are sure to make some squirm.

One of the things that was useful in this approach was to pass javaScript Associative Arrays to Freemarker.  This allowed me to specify the properties that I wanted/needed directly from witha larger sequence of objects without needing use multiple loops.

This was pretty straight forward:  I created an array in my controller.  At each element in the array, I created a new Array where I stored my values referenced by a key.  I could then in the FTL, start my list, and then reference each child by key name instead of looping again.

var big = new Array();

//some code that ends of looping over something
//take our properties/metadata stick them in an array

var _child = new Array();

_child["nodeID"] = child.id;
_child["location"] = _location;
_child["latitude"] = _latlong["latitude"];
_child["longitude"] = _latlong["longitude"];

.push(_child);
//end loop

model.big = big

Now in the ftl:

<#list big as b>

var _latlng${child_index} = new google.maps.LatLng(${child["latitude"]},${child["longitude"]});

</#list>

When we query for the specific node we want to work with where the value we want to pass to the query is in the Associative Array, we need to first clean the variable up, then it will work without throwing an error:

<#assign ref = child["nodeID"]>
<#list companyhome.childrenByLuceneSearch["ID:workspace\\:\\/\\/SpacesStore\\/${ref}"] as _child>

Screen shot 2010-06-01 at 1.26.00 AM

In my design I planned to reuse to the same pattern of using YUI within the presentation template to make the call to the web script, however when attempting to write the generated javaScript to the div tag where the map was to be displayed, I was reminded that the javaScript was never executed. It was treated as a string.  It is possible to get around this by using the javaScript eval function to execute the script, but this means turning the content retuned into a big string with lots of escapes…This would kill one of my design choices of making it possible to make direct calls to the web script and have it return a map that could be embedded in different things or be used stand alone. It could also be a big performance hit depending on how often the looip was made. Now, I could go back and write into the content branches to turn areas of the web script ftl on and off as a big sting, but this would over complicate the ftl.  This lead to the simplest fallback: have the presentation template reference the web script as part of an iFrame.  This worked nicely, but it helped to highlight what I thought was an issue with using the eval function:  there is a bug in the newest release of the maps API:  it was also happening in the iFrame on IE and Firefox.  For the moment I’ll let the script stand, it does work in Safari and Chrome, but it leaves out the major browsers.

Screen shot 2010-06-01 at 1.26.52 AM

How to use

We are still a bit early for heavy use of our plug.  We need to work out a few caching issues, the display issues are IE and Firefox, and modification of core Alfresco configs.

For this initial release I won’t provide an amp.  I’m want people to know that they will need to dig into this to learn it and extended it.  I’d also like to transfer this to be used in Share.

The source code is available at the Google Code Project — alfresco-simplegis.

If you want to call the web scripts directly:

For the document map use: /map/{store_type}/{store_id}/{id}

For the Space map use: /map/coordinates/{store_type}/{store_id}/{id}

Both of these will require that the space/content have the mappable aspect applied along with at least the zipcode applied.

Spaces can control the default zoom level for their maps.  By default that value is 8.  You should adjust it based on your need.

Document maps are static: there is no zoom or pan.  Space maps support both zoom and pan.

What next?

This project is still early in its development.  If you have ideas on how to overcome the Firefox or IE issues, your help would be appreciated.  I’m also going to be tackling the “caching” issue shortly.  This seems to me to be a useful feature.  Along with a good understanding of how you can script behaviors for properties when they are updated.

Written by jared

May 31st, 2010 at 10:50 pm

Posted in Alfresco

Tagged with , ,

Web Script: Modified Content List

without comments

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.

Written by jared

May 17th, 2010 at 1:37 pm

Posted in Alfresco

Tagged with ,

Search’d: How do I enable the Sharepoint Protocol

without comments

This is part of the Search’d series.  Topics are taken from Search Engine keyword searches.

This second post is coming a bit late, but it is finally here.  I’ve been getting ready for our World Wide Sales Kickoff. We will be kicking off our new fiscal year with great sessions for our worldwide group of partners. I’ll be presenting with two of my EMEA colleagues on Scaling Alfresco.  I’ve chosen to tackle scaling our WCM product.  It might even turn into some great information for a post or two here.

This weeks topic is one of the most popular searches that has lead to this blog: How do I enable the Sharepoint Protocol in Alfresco.  At the outset let me tell you: I’m not going to explain how to actually configure the Sharepoint Services in this post, rather I want to point you to resource that take you through all of the steps of enabling the Sharepoint Protocol in Alfresco and how to use it.  These are often over looked resources, but resources that can be really useful.

The first resource is the Installing and Configuring Alfresco Community Edition guide found on the Alfresco Community Download page (or on the Alfresco Content Community Site [registration required] in the Document Library under Documentation in the Installation and Configuration folder for your installed release of Alfresco Community.  This guide covers the basics of setting up and runningg Alfresco Community (It is also a preview of the documentation available as part of an Alfresco Enterprise Subscription in the Alfresco Network site [Alfresco Enterprise Subscription Required].)  You’ll find instructions for setting up and configuring Alfresco Sharepoint Services (on the server side) starting on the bottom of page 26.

The second resource covers using the Sharepoint Extension in Microsoft Office: Managing Alfresco Content from within MS Office Community Edition. (Requires Registration).  This is also found in the Alfresco Content Community Site [registration required] in the Document Library but in the Tutorials folder.

These documents as well as others for using Share, Web Content Management, Document Management and Records Management can be found in the Content Community Site. You’ll also find presentations from past Community Conferences, Case Studies, White Papers and Webinars.

Written by jared

March 8th, 2010 at 1:16 am

Posted in Alfresco,Search'd

Tagged with ,

Search’d: How do I disable Alfresco Share

with 3 comments

I’ve always found it fascinating what people searched for to find my blog.  And more fascinating why my blog even came up in the results!  The idea occurred to me that there were actually some meaningful questions in those searches. Yes, it was like one of those Windows 7 epiphanies.  I’m going to start a new weekly (cross your fingers) series based on those search terms. New posts will come every Friday and will incorporate some facet of the search terms that brought people to this blog.  The post size will vary based on the question and the technical depth it may require.  Hopefully, this can prove to be valuable. I’ll also add a pitch here for Alfresco Professional Services — bread on the table, mouths to feed and all — we offer the in-depth knowledge you need to help with your Alfresco Enterprise Subscription. So without further ado….I give you: Search’d

Today’s search terms came in as “alfresco start up disable share studio”.  For most Java Developers who have worked with Java application servers this will be pretty simple — you may even want to find something else to do now.  For those new to Java or not even in the market to learn, it will not be.  But the answer is pretty simple, but first some background…..

Java based applications can be packaged into several different formats. Typically you will find JARs and WARs.  In some circumstances they are packaged in other formats as well…RARs and EARs being the most common.  We at Alfresco like to use AMPs (which may or may not actually include Java code) to manager extensions to Alfresco.  These archives are zip files with specific file layouts. Go ahead, take a copy of the alfresco.war file and open it with a archive manager that understands zip.  It won’t hurt.

Now, if you downloaded Alfresco as one of the full blown installers or as a bundle with tomcat, go ahead and run the installer or unpackaged the archive.  Once that is done navigate down into the tomcat/webapps directory.  In there you will see a few directories and a couple of files with the extension .war (alfresco.war, share.war, mobile.war (Community only), studio.war (Community only)).  To disable any of these simply remove the war file or rename it by anding a new/additional extension.  I like to add the extension .off, which lets me clearly see that this part of Alfresco is — off.  Removing the war files also may seem a bit brute force and unless you are really desperate for the space it won’t hurt anything to leave them.  This is my preferred approach.  If you ever want to add that part of Alfresco back, just remove the extension and restart Alfresco.

If you have already started Alfresco you will see directories that match the name of the war file.  You will also need to remove those directories. (Don’t forget to restart Alfresco after removing those directories).

It’s as simple as that.

Q:  Is there ever a case where I would disable  the Alfresco application? Yes.  If you are using Alfresco Share, you can add a bit of scale to your deployment by having Share installed on another server.  It does require some additional configuration, but we won’t cover that today.

Q: I just removed _____.war!  Or I just removed the ______ directory! How do I get it back? For basic out of the box installs, stop Alfresco and simply grab the war file from the Alfresco Community Page or Alfresco Network (versions must match) and copy it back into the webapps directory.  If you have added extensions via AMPs to your war files, reapply the amps to the new war file. Restart Alfresco.  For the case where you deleted an expanded directory, with the war files in place, restart Alfresco and they will be deployed again.

Written by jared

February 26th, 2010 at 9:28 am

Posted in Alfresco,Search'd

Tagged with ,

Alfresco PDF Toolkit

with 8 comments

A few weeks ago I made my first release of what I am calling the Alfresco PDF Toolkit on Google Code.

Alfresco PDF Toolkit, or as I originally named it, pdf-extension, has been around for a while.  It was originally hosted on my SVN server and in then in my Alfresco SVN Repo.  It was developed as a one-off side project at the request of an early Alfresco customer.  The code has been sitting around with very few updates since that time.  An occasional rebuild to make sure it would work with a new release of Alfresco but that was it. (I think it was original built for 2.1 or 2.2)

Why the update? Focused attention.  It is a way to get my hands dirty and adding some new features that have been rattling around in my head for a while now.

What can it do? This initial release has three functions:

  • Split PDF — This option allows you to split a PDF every specified number of pages
  • Split at Page — This option allows you to split a PDF at a specified page
  • Merge PDF — This options allows you to append a PDF to another PDF

All three options generate new PDFs leaving the originals untouched.

Where can I find these actions? The actions are available in the Run Action Wizard.  They use the same names as above.

What does the future hold? The current list of enhancements is as follows (bot not necessarily in this order):

  • PDF Watermarks / Rubber stamping
  • Digital Signatures
  • TIFF to PDF transformation
  • Extract Pages
  • Delete Pages
  • Transform to PDF/A

Can I help? Yes!  I’m more than happy to accept code contributions or add features the list of enhancements.  And of course, merit can make you a contributor.

Where can I find the code or the AMP again? In the Google Code Project: Alfresco PDF Toolkit

Note: This is NOT an officially supported Alfresco Project.

Written by jared

February 25th, 2010 at 8:39 pm

Posted in Alfresco

Tagged with , ,

Alfresco WCM Forms: Unique Identifier

with 2 comments

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

UUID in Web Form

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

Written by jared

February 24th, 2010 at 2:44 pm

Posted in Alfresco

Tagged with , , ,