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)); + +; } }