Alfresco – Implementing delete behavior

Alfresco behaviors mechanism allow us to inject custom logic in repository nodes lifecycle. As you know, a detailed tutorial on this is available from Jeff Potts.

However, we need to know some extra information in order to develop a succesful behavior. In this article, we are going to extend node delete policy by deleting all existing copies of a deleted node.

Basic implementation

Following existing instructions, we can write a simple implementation.


public class SimpleBasicDeleteBehaviour implements NodeServicePolicies.BeforeDeleteNodePolicy {

    private PolicyComponent policyComponent;
    private NodeService nodeService;

    public void init() {
        policyComponent.bindClassBehaviour(
            NodeServicePolicies.BeforeDeleteNodePolicy.QNAME, 
                ContentModel.TYPE_CONTENT, 
        new JavaBehaviour(this, "beforeDeleteNode", Behaviour.NotificationFrequency.FIRST_EVENT));
    }

    @Override
    public void beforeDeleteNode(NodeRef nodeRef) {
        // Find copied nodes and delete them
        for (AssociationRef copyAssoc : nodeService.getSourceAssocs(nodeRef,ContentModel.ASSOC_ORIGINAL)) {
            nodeService.deleteNode(copyAssoc.getSourceRef());
        }
    }
}

By using this code, every node with copies can’t be deleted. And no log trace will be produced. Let me show you why.

Extra information

  • Any Alfresco transaction involving a node deletion, will not allow to perform any updating or deleting operation on custom policies
  • Our custom policy will be executed inside the same deletion transaction from the original node and this transaction is managed by thread

Transactional implementation

We can write a new code avoiding to use the same transaction and the same thread from original deletion operation.

public class TransactionalDeleteBehaviour 
    implements NodeServicePolicies.BeforeDeleteNodePolicy {
    
    // Key to identify resources associated to transaction
    private static final String KEY_RELATED_NODES = 
            TransactionalDeleteBehaviour.class.getName() + ".relatedNodes";
    
    private PolicyComponent policyComponent;
    private NodeService nodeService;
    private TransactionService transactionService;
    private TransactionListener transactionListener;
    private ThreadPoolExecutor threadPoolExecutor;
    
    // Bind behaviour and initialize transaction listener
    public void init() {
        
        policyComponent.bindClassBehaviour(
                NodeServicePolicies.BeforeDeleteNodePolicy.QNAME, 
                ContentModel.TYPE_CONTENT, 
                new JavaBehaviour(this, "beforeDeleteNode", Behaviour.NotificationFrequency.FIRST_EVENT));
        
        this.transactionListener = new RelatedNodesTransactionListener();
        
    }
    

    @Override
    public void beforeDeleteNode(NodeRef nodeRef) {
        
        // Bind listener to current transaction
        AlfrescoTransactionSupport.bindListener(transactionListener);

        // Get some related nodes to work with
        List<NodeRef> relatedNodes = new ArrayList<NodeRef>();
        for (AssociationRef copyAssoc : nodeService.getSourceAssocs(nodeRef, ContentModel.ASSOC_ORIGINAL)) {
            relatedNodes.add(copyAssoc.getSourceRef());
        }

        // Transactions involving several nodes need resource updating
        List<NodeRef> currentRelatedNodes = AlfrescoTransactionSupport.getResource(KEY_RELATED_NODES);
        if (currentRelatedNodes == null) {
            currentRelatedNodes = relatedNodes;
        } else {
            currentRelatedNodes.addAll(relatedNodes);
        }
        
        // Put resources to be used in transaction listener
        AlfrescoTransactionSupport.bindResource(KEY_RELATED_NODES, currentRelatedNodes);

    }
    
    // Listening "afterCommit" transaction event
    private class RelatedNodesTransactionListener 
        extends TransactionListenerAdapter implements TransactionListener {

        @Override
        public void afterCommit() {
            @SuppressWarnings("unchecked")
            List<NodeRef> nodesToBeReviewed = 
                (List<NodeRef>) AlfrescoTransactionSupport.getResource(KEY_RELATED_NODES);
            if (nodesToBeReviewed != null) {
                for (NodeRef nodeToBeReviewed : nodesToBeReviewed) {
                    // Launch every node work in a different thread
                    Runnable runnable = new RelatedNodeDeletion(nodeToBeReviewed);
                    threadPoolExecutor.execute(runnable);
                }
            }
        }
        
        @Override
        public void flush() {
        }
        
    }
    
    // Thread to work with an individual node
    private class RelatedNodeDeletion implements Runnable {
        
        private NodeRef nodeToBeReviewed;
        
        private RelatedNodeDeletion(NodeRef nodeToBeReviewed) {
            this.nodeToBeReviewed = nodeToBeReviewed;
        }

        @Override
        public void run() {
            AuthenticationUtil.runAsSystem(new RunAsWork<Void>() {
                
                public Void doWork() throws Exception {
                    
                    RetryingTransactionCallback<Void> callback = new RetryingTransactionCallback<Void>() {
                        
                        @Override
                        public Void execute() throws Throwable {
                            nodeService.deleteNode(nodeToBeReviewed);
                            return null;
                        }
                    };
                    
                    try {
                        RetryingTransactionHelper txnHelper = 
                            transactionService.getRetryingTransactionHelper();
                        txnHelper.doInTransaction(callback, false, true);
                    } catch (Throwable e) {
                        e.printStackTrace();
                    }
                    
                    return null;
                    
                }
            });
        }
        
    }

So, node deletion will be performed asynchronously once the original deletion operation is finished. And any transaction conflict with Alfresco default behavior will be avoided.

Anuncios

Alfresco – Implementando el behavior de borrado

El mecanismo de behaviors de Alfresco permite inyectar lógica adicional en el ciclo de vida de los nodos en el repositorio de contenido. Como es habitual, disponéis de un detallado tutorial de Jeff Potts con el que podréis experimentar con este mecanismo.

Sin embargo, en algunos casos, es necesario conocer algunos conceptos adicionales para poder desarrollar un behavior exitoso. A continuación vamos a construir una extensión para la política de borrado de nodos de Alfresco mediante un programa que elimina todas las copias existentes de un documento cuando se borra ese mismo documento.

Implementación básica del behavior

Siguiendo los tutoriales existentes en la Red, podríamos realizar una implementación básica.


public class SimpleBasicDeleteBehaviour implements NodeServicePolicies.BeforeDeleteNodePolicy {

    private PolicyComponent policyComponent;
    private NodeService nodeService;

    public void init() {
        policyComponent.bindClassBehaviour(
    	    NodeServicePolicies.BeforeDeleteNodePolicy.QNAME, 
                ContentModel.TYPE_CONTENT, 
		new JavaBehaviour(this, "beforeDeleteNode", Behaviour.NotificationFrequency.FIRST_EVENT));
    }

    @Override
    public void beforeDeleteNode(NodeRef nodeRef) {
        // Find copied nodes and delete it
        for (AssociationRef copyAssoc : nodeService.getSourceAssocs(nodeRef,ContentModel.ASSOC_ORIGINAL)) {
            nodeService.deleteNode(copyAssoc.getSourceRef());
        }
    }
}

En caso de que utilicemos este código en nuestra implantación de Alfresco observaremos que aquellos nodos que tengan copias no van a poder ser eliminados. A pesar de que no aparecerá ningún error en la traza de ejecución.

Conceptos adicionales

  • Cuando una transacción de Alfresco implica la eliminación de un nodo, no se permitirá realizar ninguna operación de modificación o borrado en las políticas
  • El behavior se ejecuta integrado en la misma transacción de borrado del nodo que está asociada al thread de ejecución

Implementación transaccional del behavior

En consecuencia de lo anterior, vamos a modificar nuestro desarrollo para ejecutar de manera desacoplada (tanto a nivel transaccional como a nivel de thread) el borrado de los nodos que son copia del original.

public class TransactionalDeleteBehaviour 
    implements NodeServicePolicies.BeforeDeleteNodePolicy {
    
    // Key to identify resources associated to transaction
    private static final String KEY_RELATED_NODES = 
            TransactionalDeleteBehaviour.class.getName() + ".relatedNodes";
    
    private PolicyComponent policyComponent;
    private NodeService nodeService;
    private TransactionService transactionService;
    private TransactionListener transactionListener;
    private ThreadPoolExecutor threadPoolExecutor;
    
    // Bind behaviour and initialize transaction listener
    public void init() {
        
        policyComponent.bindClassBehaviour(
                NodeServicePolicies.BeforeDeleteNodePolicy.QNAME, 
                ContentModel.TYPE_CONTENT, 
                new JavaBehaviour(this, "beforeDeleteNode", Behaviour.NotificationFrequency.FIRST_EVENT));
        
        this.transactionListener = new RelatedNodesTransactionListener();
        
    }
    

    @Override
    public void beforeDeleteNode(NodeRef nodeRef) {
        
        // Bind listener to current transaction
        AlfrescoTransactionSupport.bindListener(transactionListener);

        // Get some related nodes to work with
        List<NodeRef> relatedNodes = new ArrayList<NodeRef>();
        for (AssociationRef copyAssoc : nodeService.getSourceAssocs(nodeRef, ContentModel.ASSOC_ORIGINAL)) {
            relatedNodes.add(copyAssoc.getSourceRef());
        }

        // Transactions involving several nodes need resource updating
        List<NodeRef> currentRelatedNodes = AlfrescoTransactionSupport.getResource(KEY_RELATED_NODES);
        if (currentRelatedNodes == null) {
            currentRelatedNodes = relatedNodes;
        } else {
            currentRelatedNodes.addAll(relatedNodes);
        }
        
        // Put resources to be used in transaction listener
        AlfrescoTransactionSupport.bindResource(KEY_RELATED_NODES, currentRelatedNodes);

    }
    
    // Listening "afterCommit" transaction event
    private class RelatedNodesTransactionListener 
        extends TransactionListenerAdapter implements TransactionListener {

        @Override
        public void afterCommit() {
            @SuppressWarnings("unchecked")
            List<NodeRef> nodesToBeReviewed = 
                (List<NodeRef>) AlfrescoTransactionSupport.getResource(KEY_RELATED_NODES);
            if (nodesToBeReviewed != null) {
                for (NodeRef nodeToBeReviewed : nodesToBeReviewed) {
                    // Launch every node work in a different thread
                    Runnable runnable = new RelatedNodeDeletion(nodeToBeReviewed);
                    threadPoolExecutor.execute(runnable);
                }
            }
        }
        
        @Override
        public void flush() {
        }
        
    }
    
    // Thread to work with an individual node
    private class RelatedNodeDeletion implements Runnable {
        
        private NodeRef nodeToBeReviewed;
        
        private RelatedNodeDeletion(NodeRef nodeToBeReviewed) {
            this.nodeToBeReviewed = nodeToBeReviewed;
        }

        @Override
        public void run() {
            AuthenticationUtil.runAsSystem(new RunAsWork<Void>() {
                
                public Void doWork() throws Exception {
                    
                    RetryingTransactionCallback<Void> callback = new RetryingTransactionCallback<Void>() {
                        
                        @Override
                        public Void execute() throws Throwable {
                            nodeService.deleteNode(nodeToBeReviewed);
                            return null;
                        }
                    };
                    
                    try {
                        RetryingTransactionHelper txnHelper = 
                            transactionService.getRetryingTransactionHelper();
                        txnHelper.doInTransaction(callback, false, true);
                    } catch (Throwable e) {
                        e.printStackTrace();
                    }
                    
                    return null;
                    
                }
            });
        }
        
    }

De esta manera, la eliminación de nodos copiados se realizará en modo asíncrono una vez que haya terminado la eliminación del nodo original y evitaremos los conflictos con el control transaccional por defecto de Alfresco.