diff --git a/config/alfresco/model/systemModel.xml b/config/alfresco/model/systemModel.xml
index 2bceb3bbd1..0877ec34ae 100644
--- a/config/alfresco/model/systemModel.xml
+++ b/config/alfresco/model/systemModel.xml
@@ -373,6 +373,25 @@
+
+
+
+ Cascade update
+
+
+ Cascade CRC
+ d:long
+ false
+ false
+
+
+ Cascade Tx
+ d:long
+ false
+ false
+
+
+
\ No newline at end of file
diff --git a/config/alfresco/node-services-context.xml b/config/alfresco/node-services-context.xml
index a979d6a5b4..85a9b423d7 100644
--- a/config/alfresco/node-services-context.xml
+++ b/config/alfresco/node-services-context.xml
@@ -305,4 +305,12 @@
+
+
+
+
+
+
+
+
diff --git a/source/java/org/alfresco/repo/node/CascadeUpdateAspect.java b/source/java/org/alfresco/repo/node/CascadeUpdateAspect.java
new file mode 100644
index 0000000000..846f5ae668
--- /dev/null
+++ b/source/java/org/alfresco/repo/node/CascadeUpdateAspect.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2005-2015 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.repo.node;
+
+import java.io.Serializable;
+import java.util.HashSet;
+import java.util.Map;
+
+import org.alfresco.model.ContentModel;
+import org.alfresco.repo.node.NodeServicePolicies.OnCreateChildAssociationPolicy;
+import org.alfresco.repo.node.NodeServicePolicies.OnDeleteChildAssociationPolicy;
+import org.alfresco.repo.node.NodeServicePolicies.OnMoveNodePolicy;
+import org.alfresco.repo.node.NodeServicePolicies.OnUpdatePropertiesPolicy;
+import org.alfresco.repo.policy.Behaviour;
+import org.alfresco.repo.policy.JavaBehaviour;
+import org.alfresco.repo.policy.PolicyComponent;
+import org.alfresco.repo.solr.SOLRTrackingComponent;
+import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
+import org.alfresco.service.cmr.dictionary.DictionaryService;
+import org.alfresco.service.cmr.dictionary.PropertyDefinition;
+import org.alfresco.service.cmr.repository.ChildAssociationRef;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.repository.NodeRef.Status;
+import org.alfresco.service.cmr.repository.NodeService;
+import org.alfresco.service.namespace.QName;
+import org.alfresco.util.EqualsHelper;
+
+/**
+ * @author Andy
+ *
+ */
+public class CascadeUpdateAspect implements OnCreateChildAssociationPolicy, OnDeleteChildAssociationPolicy, OnMoveNodePolicy
+{
+ private PolicyComponent policyComponent;
+ private NodeService nodeService;
+ private SOLRTrackingComponent solrTrackingComponent;
+ private DictionaryService dictionaryService;
+
+
+ public void setPolicyComponent(PolicyComponent policyComponent)
+ {
+ this.policyComponent = policyComponent;
+ }
+
+ public void setNodeService(NodeService nodeService)
+ {
+ this.nodeService = nodeService;
+ }
+
+ public void setSolrTrackingComponent(SOLRTrackingComponent solrTrackingComponent)
+ {
+ this.solrTrackingComponent = solrTrackingComponent;
+ }
+
+ public void setDictionaryService(DictionaryService dictionaryService)
+ {
+ this.dictionaryService = dictionaryService;
+ }
+
+ /**
+ * Initialise method
+ */
+ public void init()
+ {
+ // need to listen to:
+ // invokeOnCreateChildAssociation(newParentAssocRef, false);
+ // invokeOnDeleteChildAssociation(oldParentAssocRef);
+ // invokeOnMoveNode(oldParentAssocRef, newParentAssocRef);
+ // categories affect paths via membership (not paths beneath nodes that are categories)
+ // - only changing category structure requires a cascade not changing a node's on a categories
+
+ this.policyComponent.bindAssociationBehaviour(OnCreateChildAssociationPolicy.QNAME,
+ ContentModel.TYPE_BASE,
+ new JavaBehaviour(this, "onCreateChildAssociation", Behaviour.NotificationFrequency.EVERY_EVENT));
+ this.policyComponent.bindAssociationBehaviour(OnDeleteChildAssociationPolicy.QNAME,
+ ContentModel.TYPE_BASE,
+ new JavaBehaviour(this, "onDeleteChildAssociation", Behaviour.NotificationFrequency.EVERY_EVENT));
+ this.policyComponent.bindClassBehaviour(OnMoveNodePolicy.QNAME,
+ ContentModel.TYPE_BASE,
+ new JavaBehaviour(this, "onMoveNode", Behaviour.NotificationFrequency.EVERY_EVENT));
+ }
+
+ /* (non-Javadoc)
+ * @see org.alfresco.repo.node.NodeServicePolicies.OnMoveNodePolicy#onMoveNode(org.alfresco.service.cmr.repository.ChildAssociationRef, org.alfresco.service.cmr.repository.ChildAssociationRef)
+ */
+ @Override
+ public void onMoveNode(ChildAssociationRef oldChildAssocRef, ChildAssociationRef newChildAssocRef)
+ {
+ markCascadeUpdate(oldChildAssocRef.getChildRef());
+ markCascadeUpdate(newChildAssocRef.getChildRef());
+ }
+
+ /* (non-Javadoc)
+ * @see org.alfresco.repo.node.NodeServicePolicies.OnDeleteChildAssociationPolicy#onDeleteChildAssociation(org.alfresco.service.cmr.repository.ChildAssociationRef)
+ */
+ @Override
+ public void onDeleteChildAssociation(ChildAssociationRef childAssocRef)
+ {
+ markCascadeUpdate(childAssocRef.getChildRef());
+ }
+
+ /* (non-Javadoc)
+ * @see org.alfresco.repo.node.NodeServicePolicies.OnCreateChildAssociationPolicy#onCreateChildAssociation(org.alfresco.service.cmr.repository.ChildAssociationRef, boolean)
+ */
+ @Override
+ public void onCreateChildAssociation(ChildAssociationRef childAssocRef, boolean isNewNode)
+ {
+ if(!isNewNode)
+ {
+ markCascadeUpdate(childAssocRef.getChildRef());
+ }
+ }
+
+
+ private void markCascadeUpdate(NodeRef nodeRef)
+ {
+ Status status = nodeService.getNodeStatus(nodeRef);
+ nodeService.setProperty(status.getNodeRef(), ContentModel.PROP_CASCADE_CRC, solrTrackingComponent.getCRC(status.getDbId()));
+ nodeService.setProperty(status.getNodeRef(), ContentModel.PROP_CASCADE_TX, status.getDbTxnId());
+ }
+// /* (non-Javadoc)
+// * @see org.alfresco.repo.node.NodeServicePolicies.OnUpdatePropertiesPolicy#onUpdateProperties(org.alfresco.service.cmr.repository.NodeRef, java.util.Map, java.util.Map)
+// */
+// @Override
+// public void onUpdateProperties(NodeRef nodeRef, Map before, Map after)
+// {
+// HashSet combinedPropertyNames = new HashSet(before.size() + 10);
+// combinedPropertyNames.addAll(before.keySet());
+// combinedPropertyNames.addAll(after.keySet());
+// for(QName propertyQName : combinedPropertyNames)
+// {
+// PropertyDefinition propDef = dictionaryService.getProperty(propertyQName);
+// if((propDef != null) && (propDef.getDataType().getName().equals(DataTypeDefinition.CATEGORY)))
+// {
+// Serializable beforeValue = before.get(propDef.getName());
+// Serializable afterValue = after.get(propDef.getName());
+// if(false == EqualsHelper.nullSafeEquals(beforeValue, afterValue))
+// {
+// markCascadeUpdate(nodeRef);
+// }
+// }
+//
+// }
+//
+// }
+}
diff --git a/source/java/org/alfresco/repo/solr/SOLRTrackingComponent.java b/source/java/org/alfresco/repo/solr/SOLRTrackingComponent.java
index 8376457e70..a8e9826b9a 100644
--- a/source/java/org/alfresco/repo/solr/SOLRTrackingComponent.java
+++ b/source/java/org/alfresco/repo/solr/SOLRTrackingComponent.java
@@ -181,4 +181,12 @@ public interface SOLRTrackingComponent
* This is an optional feature.
*/
public ShardRegistry getShardRegistry();
+
+ /**
+ * Compute the CRC for the parent associations to this node that can cause its PATH to change
+ * - primary & secondary associations and virtual associations from categories.
+ * @param nodeId
+ * @return
+ */
+ public long getCRC(Long nodeId);
}
diff --git a/source/java/org/alfresco/repo/solr/SOLRTrackingComponentImpl.java b/source/java/org/alfresco/repo/solr/SOLRTrackingComponentImpl.java
index 6e2280ed95..7217a06b61 100644
--- a/source/java/org/alfresco/repo/solr/SOLRTrackingComponentImpl.java
+++ b/source/java/org/alfresco/repo/solr/SOLRTrackingComponentImpl.java
@@ -650,6 +650,66 @@ public class SOLRTrackingComponentImpl implements SOLRTrackingComponent
return props;
}
+ public long getCRC(Long nodeId)
+ {
+ Status status = nodeDAO.getNodeIdStatus(nodeId);
+ Set aspects = getNodeAspects(nodeId);
+ Map props = getProperties(nodeId);
+
+ //Category membership does not cascade to children - only the node needs reindexing, not its children
+ //This was producing cascade updates that were not required
+ ////CategoryPaths categoryPaths = new CategoryPaths(new ArrayList>(), new ArrayList());
+ ////categoryPaths = getCategoryPaths(status.getNodeRef(), aspects, props);
+
+ final List parentAssocs = new ArrayList(100);
+ nodeDAO.getParentAssocs(nodeId, null, null, null, new ChildAssocRefQueryCallback()
+ {
+ @Override
+ public boolean preLoadNodes()
+ {
+ return false;
+ }
+
+ @Override
+ public boolean orderResults()
+ {
+ return false;
+ }
+
+ @Override
+ public boolean handle(Pair childAssocPair,
+ Pair parentNodePair, Pair childNodePair)
+ {
+ parentAssocs.add(tenantService.getBaseName(childAssocPair.getSecond(), true));
+ return true;
+ }
+
+ @Override
+ public void done()
+ {
+ }
+ });
+// for(ChildAssociationRef ref : categoryPaths.getCategoryParents())
+// {
+// parentAssocs.add(tenantService.getBaseName(ref, true));
+// }
+
+ CRC32 crc = new CRC32();
+ for(ChildAssociationRef car : parentAssocs)
+ {
+ try
+ {
+ crc.update(car.toString().getBytes("UTF-8"));
+ }
+ catch (UnsupportedEncodingException e)
+ {
+ throw new RuntimeException("UTF-8 encoding is not supported");
+ }
+ }
+ return crc.getValue();
+
+ }
+
/**
* {@inheritDoc}
*/
diff --git a/source/test-java/org/alfresco/repo/node/NodeServiceTest.java b/source/test-java/org/alfresco/repo/node/NodeServiceTest.java
index 0a91746a3c..500f372c2c 100644
--- a/source/test-java/org/alfresco/repo/node/NodeServiceTest.java
+++ b/source/test-java/org/alfresco/repo/node/NodeServiceTest.java
@@ -980,11 +980,12 @@ public class NodeServiceTest
props.put(ContentModel.PROP_TITLE, "some title");
nodeService.addAspect(nodeRef, ContentModel.ASPECT_TITLED, props);
nodeService.setProperty(nodeRef, ContentModel.PROP_DESCRIPTION, "Some description");
- nodeService.addChild(
- Collections.singletonList(workspaceRootNodeRef),
- nodeRef,
- ContentModel.ASSOC_CHILDREN,
- QName.createQName(TEST_PREFIX, "secondary"));
+ // Adding a child node now triggers behaviour to update a CRC property
+// nodeService.addChild(
+// Collections.singletonList(workspaceRootNodeRef),
+// nodeRef,
+// ContentModel.ASSOC_CHILDREN,
+// QName.createQName(TEST_PREFIX, "secondary"));
return null;
}
};
@@ -1781,4 +1782,118 @@ public class NodeServiceTest
assertEquals(nodes[1], nodesParentFirst.get(0).sourceAssocs.get(0).getSecond().getSourceRef());
assertEquals(0, nodesParentFirst.get(1).sourceAssocs.size());
}
+
+ @Test public void testCascadeUpdate()
+ {
+ Map props = new HashMap();
+ NodeRef nodeRef1 = nodeService.createNode(
+ rootNodeRef,
+ ContentModel.ASSOC_CHILDREN,
+ QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, this.getClass().getName()),
+ ContentModel.TYPE_CONTAINER).getChildRef();
+
+ assertFalse(nodeService.getAspects(nodeRef1).contains(ContentModel.ASPECT_CASCADE_UPDATE));
+
+
+
+ Map aspectProps = new HashMap();
+ ArrayList cats = new ArrayList();
+ cats.add(nodeRef1);
+ aspectProps.put(ContentModel.PROP_CATEGORIES, cats);
+ nodeService.addAspect(nodeRef1, ContentModel.ASPECT_GEN_CLASSIFIABLE, aspectProps);
+ assertTrue(nodeService.getAspects(nodeRef1).contains(ContentModel.ASPECT_GEN_CLASSIFIABLE));
+ assertFalse(nodeService.getAspects(nodeRef1).contains(ContentModel.ASPECT_CASCADE_UPDATE));
+
+
+ NodeRef nodeRef2 = nodeService.createNode(
+ rootNodeRef,
+ ContentModel.ASSOC_CHILDREN,
+ QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, this.getClass().getName()),
+ ContentModel.TYPE_CONTAINER).getChildRef();
+
+ NodeRef nodeRef3 = nodeService.createNode(
+ rootNodeRef,
+ ContentModel.ASSOC_CHILDREN,
+ QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, this.getClass().getName()),
+ ContentModel.TYPE_CONTAINER).getChildRef();
+
+ NodeRef nodeRef4 = nodeService.createNode(
+ nodeRef2,
+ ContentModel.ASSOC_CHILDREN,
+ QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, this.getClass().getName()),
+ ContentModel.TYPE_CONTAINER).getChildRef();
+
+ assertFalse(nodeService.getAspects(nodeRef2).contains(ContentModel.ASPECT_CASCADE_UPDATE));
+ assertFalse(nodeService.getAspects(nodeRef3).contains(ContentModel.ASPECT_CASCADE_UPDATE));
+ assertFalse(nodeService.getAspects(nodeRef4).contains(ContentModel.ASPECT_CASCADE_UPDATE));
+
+ nodeService.moveNode(nodeRef4, nodeRef3, ContentModel.ASSOC_CHILDREN, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, this.getClass().getName()));
+
+
+ assertFalse(nodeService.getAspects(nodeRef2).contains(ContentModel.ASPECT_CASCADE_UPDATE));
+ assertFalse(nodeService.getAspects(nodeRef3).contains(ContentModel.ASPECT_CASCADE_UPDATE));
+ assertTrue(nodeService.getAspects(nodeRef4).contains(ContentModel.ASPECT_CASCADE_UPDATE));
+ Status status = nodeService.getNodeStatus(nodeRef4);
+ Long lastCascadeTx = (Long)nodeService.getProperty(nodeRef4, ContentModel.PROP_CASCADE_TX);
+ assertTrue(status.getDbTxnId().equals(lastCascadeTx));
+ assertTrue(nodeService.getProperty(nodeRef4, ContentModel.PROP_CASCADE_CRC) != null);
+ Long crcIn3 = (Long)nodeService.getProperty(nodeRef4, ContentModel.PROP_CASCADE_CRC);
+
+ nodeService.moveNode(nodeRef4, nodeRef2, ContentModel.ASSOC_CHILDREN, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, this.getClass().getName()));
+ Long crcIn2 = (Long)nodeService.getProperty(nodeRef4, ContentModel.PROP_CASCADE_CRC);
+
+ assertFalse(crcIn2.equals(crcIn3));
+
+
+ NodeRef nodeRef5 = nodeService.createNode(
+ rootNodeRef,
+ ContentModel.ASSOC_CHILDREN,
+ QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "5"),
+ ContentModel.TYPE_CONTAINER).getChildRef();
+
+ NodeRef nodeRef6 = nodeService.createNode(
+ rootNodeRef,
+ ContentModel.ASSOC_CHILDREN,
+ QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "6"),
+ ContentModel.TYPE_CONTAINER).getChildRef();
+
+ NodeRef nodeRef7 = nodeService.createNode(
+ nodeRef5,
+ ContentModel.ASSOC_CHILDREN,
+ QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "7"),
+ ContentModel.TYPE_CONTAINER).getChildRef();
+
+ NodeRef nodeRef8 = nodeService.createNode(
+ nodeRef5,
+ ContentModel.ASSOC_CHILDREN,
+ QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "8"),
+ ContentModel.TYPE_CONTAINER).getChildRef();
+
+
+ assertFalse(nodeService.getAspects(nodeRef5).contains(ContentModel.ASPECT_CASCADE_UPDATE));
+ assertFalse(nodeService.getAspects(nodeRef6).contains(ContentModel.ASPECT_CASCADE_UPDATE));
+ assertFalse(nodeService.getAspects(nodeRef7).contains(ContentModel.ASPECT_CASCADE_UPDATE));
+ assertFalse(nodeService.getAspects(nodeRef8).contains(ContentModel.ASPECT_CASCADE_UPDATE));
+
+ nodeService.addChild(nodeRef6, nodeRef7, ContentModel.ASSOC_CHILDREN, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, this.getClass().getName()));
+ assertFalse(nodeService.getAspects(nodeRef5).contains(ContentModel.ASPECT_CASCADE_UPDATE));
+ assertFalse(nodeService.getAspects(nodeRef6).contains(ContentModel.ASPECT_CASCADE_UPDATE));
+ assertTrue(nodeService.getAspects(nodeRef7).contains(ContentModel.ASPECT_CASCADE_UPDATE));
+ assertFalse(nodeService.getAspects(nodeRef8).contains(ContentModel.ASPECT_CASCADE_UPDATE));
+
+ Long doubleLinkCRC = (Long)nodeService.getProperty(nodeRef7, ContentModel.PROP_CASCADE_CRC);
+
+ nodeService.removeChild(nodeRef6, nodeRef7);
+ Long singleLinkCRC = (Long)nodeService.getProperty(nodeRef7, ContentModel.PROP_CASCADE_CRC);
+ assertFalse(doubleLinkCRC.equals(singleLinkCRC));
+
+ nodeService.addChild(nodeRef6, nodeRef7, ContentModel.ASSOC_CHILDREN, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, this.getClass().getName()));
+ Long doubleLinkCRC2 = (Long)nodeService.getProperty(nodeRef7, ContentModel.PROP_CASCADE_CRC);
+ assertTrue(doubleLinkCRC.equals(doubleLinkCRC2));
+
+ nodeService.removeChild(nodeRef6, nodeRef7);
+ Long singleLinkCRC2 = (Long)nodeService.getProperty(nodeRef7, ContentModel.PROP_CASCADE_CRC);
+ assertTrue(singleLinkCRC2.equals(singleLinkCRC));
+
+; }
}