Alfresco Version Pruning Behaviour....Mmmm Prunes

This is the first post of a multi-part series for an umbrella project I might be calling "alfresco-content-control." Its a working title. In this particular post we'll demonstrate how you can limit your version history and prune the unneeded versions. I racked my brain trying to think of a cooler theme for this post, but best I can come up with is....the elderly and their love for prunes. Mainly for the digestive benefits...and possibly for the taste. Neither of which are related to enterprise content management...but we'll just go with it.

I should say that this problem has already been tackled before by one of my fellow Alfresco colleagues Jared Ottley and he also blogged about it in his Max Version Policy post. His project on GitHub was an amazing foundation and actually provided a bulk of the logic needed for a prospective customer of mine. I just needed to turn the hearing aid up to 11 to cover some of the following requirements that were missing....

  • Expose the max version property in Alfresco Share through content model properties.
  • Provide the ability to dynamically apply the version pruning behavior to individual pieces of content.
  • Provide the ability keep the root version if needed. This however can be a slippery slope since you may want to mark specific versions as ones to keep permanently. We're not going to tackle that. At least not for now.

So with our list of enhanced requirements, I had enough information to take the project to the next level. Boom! Crack open a can of PBR because we're halfway there!

The first thing we had to do was to create a super simple content model with an aspect or property group called "Version Prunable." Essentially our goal was to move the properties that were initially stored in a file on the classpath and elevate them into content model. That included the existing property for maximum version count and an additional boolean property to provide the flexibility to keep the root version or not. By moving these into a content model, we can easily expose the properties and their associated values in Share.

version-pruning-model.xml:

<?xml version="1.0" encoding="UTF-8"?>
<model name="prune:versionPruningModel" xmlns="http://www.alfresco.org/model/dictionary/1.0">
    <description>Version Pruning Content Model</description>
    <author>Kyle Adams</author>
    <version>1.0</version>

    <imports>
        <import uri="http://www.alfresco.org/model/dictionary/1.0" prefix="d" />
        <import uri="http://www.alfresco.org/model/content/1.0" prefix="cm" />
    </imports>

    <namespaces>
        <namespace uri="http://www.alfresco.org/model/extension/version-pruning/1.0" prefix="prune" />
    </namespaces>

    <aspects>
        <!-- Version Prunable Aspect -->
        <aspect name="prune:versionPrunable">
            <title>Version Prunable</title>
            <properties>
                <property name="prune:maxVersionCount">
                    <title>Max Version Count</title>
                    <description>Max Version Count</description>
                    <type>d:int</type>
                    <default>-1</default>
                </property>
                <property name="prune:keepRootVersion">
                    <title>Keep Root Version</title>
                    <description>Keep Root Version</description>
                    <type>d:boolean</type>
                </property>
            </properties>
        </aspect>
    </aspects>
</model>

Then we had to make some small modifications to the behavior (or behaviour if you think thats more proper). I feel like I should be drinking my PBR with my pinky turned up when saying behaviouuuuur.

Anyways, this included adding logic to pull in property values from a given content node and an if statement to evaluate if the Version Prunable aspect has been applied to the node. Another minor additional was the if/then/else block to evaluate whether we need to delete the root version or the successor of the root version within the VersionHistory

Here's the VersionPruningBehaviour.java implementation (BTW Squarespace has horrible syntax highlighting support for Java code blocks...sorry?)

package org.alfresco.extension.version.pruning.behaviour;

import org.alfresco.extension.version.pruning.model.VersionPruningContentModel;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.policy.Behaviour;
import org.alfresco.repo.policy.JavaBehaviour;
import org.alfresco.repo.policy.PolicyComponent;
import org.alfresco.repo.version.VersionServicePolicies;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.version.Version;
import org.alfresco.service.cmr.version.VersionHistory;
import org.alfresco.service.cmr.version.VersionService;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.util.Collection;

/**
 * Created by kadams on 7/14/15.
 */
public class VersionPruningBehaviour implements VersionServicePolicies.AfterCreateVersionPolicy {
    private static final Log logger = LogFactory.getLog(VersionPruningBehaviour.class);

    private ServiceRegistry serviceRegistry;
    private PolicyComponent policyComponent;
    private NodeService nodeService;
    private VersionService versionService;

    private int maxVersionCount;
    private boolean keepRootVersion;

    public void init(){
        this.policyComponent.bindClassBehaviour(
                VersionServicePolicies.AfterCreateVersionPolicy.QNAME,
                ContentModel.TYPE_CONTENT,
                new JavaBehaviour(this, "afterCreateVersion", Behaviour.NotificationFrequency.TRANSACTION_COMMIT));

        this.nodeService = this.serviceRegistry.getNodeService();
        this.versionService = this.serviceRegistry.getVersionService();

    }

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

        try {
            if(this.nodeService.hasAspect(versionedNodeRef, VersionPruningContentModel.ASPECT_VERSION_PRUNABLE)) {
                VersionHistory versionHistory = this.versionService.getVersionHistory(versionedNodeRef);
                if(versionHistory != null){
                    this.keepRootVersion = (boolean) this.nodeService.getProperty(versionedNodeRef, VersionPruningContentModel.PROP_KEEP_ROOT_VERSION);
                    this.maxVersionCount = (int) this.nodeService.getProperty(versionedNodeRef, VersionPruningContentModel.PROP_MAX_VERSION_COUNT);

                    if(maxVersionCount > 0){
                        while(versionHistory.getAllVersions().size() > maxVersionCount){
                            Version versionToBeDeleted = null;
                            if(keepRootVersion) {
                                 versionToBeDeleted = versionHistory.getSuccessors(versionHistory.getRootVersion()).iterator().next();
                            }
                            else{
                                versionToBeDeleted = versionHistory.getRootVersion();
                            }

                            if(logger.isDebugEnabled()){
                                logger.debug("Max Version Count: " + maxVersionCount);
                                logger.debug("Keep Root Version? " + keepRootVersion);
                                logger.debug("Current version history collection size: " + versionHistory.getAllVersions().size());
                                logger.debug("Preparing to remove version: " + versionToBeDeleted.getVersionLabel() + " type: " + versionToBeDeleted.getVersionType());
                            }
                            this.versionService.deleteVersion(versionedNodeRef, versionToBeDeleted);
                            versionHistory = this.versionService.getVersionHistory(versionedNodeRef);
                        }
                    }
                }
                else{
                    if(logger.isDebugEnabled()){
                        logger.debug("No version history found!");
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

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

ROCK! Now we have everything in place to keep our VersionHistory from getting out of hand. I should however provide an alternative. The latest version of Alfresco's DoD 5015.2-certified Records Management module supports the ability to retain and subsequently destroy individual versions of a work-in-process document. This module is a far better approach for content that is regulatory in nature. Now if you're not managing regulatory content in Alfresco, the RM module might possibly be more horse power than you need. So let's take a quick tour of the version pruning behaviour functionality. Demo........ENGAGE!

So after leveraging a good base from Jared Ottley's Max Version Policy project, we were able to expose version pruning behaviour configuration pretty easily in Share. You can access the source for the alfresco-content-control umbrella project here on GitHub.

- Keep Calm and Grandpa On