mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-10-15 15:02:20 +00:00
Fix for ACE-4420 SOLR 4 - sharded - rename and move operations will not update descendants and add incorrect nodes to the shard
git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@114654 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
@@ -373,6 +373,25 @@
|
||||
</property>
|
||||
</properties>
|
||||
</aspect>
|
||||
|
||||
<!-- Aspect to record the last transaction that requires a cascade update to index children (like move, rename, link and unlink) -->
|
||||
<aspect name="sys:cascadeUpdate">
|
||||
<title>Cascade update</title>
|
||||
<properties>
|
||||
<property name="sys:cascadeCRC">
|
||||
<title>Cascade CRC</title>
|
||||
<type>d:long</type>
|
||||
<mandatory>false</mandatory>
|
||||
<multiple>false</multiple>
|
||||
</property>
|
||||
<property name="sys:cascadeTx">
|
||||
<title>Cascade Tx</title>
|
||||
<type>d:long</type>
|
||||
<mandatory>false</mandatory>
|
||||
<multiple>false</multiple>
|
||||
</property>
|
||||
</properties>
|
||||
</aspect>
|
||||
</aspects>
|
||||
|
||||
</model>
|
@@ -305,4 +305,12 @@
|
||||
<property name="jobLockService" ref="jobLockService"/>
|
||||
</bean>
|
||||
|
||||
<!-- The cascade update aspect -->
|
||||
<bean id="cascadeUpdateAspect" class="org.alfresco.repo.node.CascadeUpdateAspect" init-method="init">
|
||||
<property name="nodeService" ref="nodeService"/>
|
||||
<property name="policyComponent" ref="policyComponent"/>
|
||||
<property name="dictionaryService" ref="dictionaryService" />
|
||||
<property name="solrTrackingComponent" ref="solrTrackingComponent" />
|
||||
</bean>
|
||||
|
||||
</beans>
|
||||
|
162
source/java/org/alfresco/repo/node/CascadeUpdateAspect.java
Normal file
162
source/java/org/alfresco/repo/node/CascadeUpdateAspect.java
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<QName, Serializable> before, Map<QName, Serializable> after)
|
||||
// {
|
||||
// HashSet<QName> combinedPropertyNames = new HashSet<QName>(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);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// }
|
||||
//
|
||||
// }
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
@@ -650,6 +650,66 @@ public class SOLRTrackingComponentImpl implements SOLRTrackingComponent
|
||||
return props;
|
||||
}
|
||||
|
||||
public long getCRC(Long nodeId)
|
||||
{
|
||||
Status status = nodeDAO.getNodeIdStatus(nodeId);
|
||||
Set<QName> aspects = getNodeAspects(nodeId);
|
||||
Map<QName, Serializable> 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<Pair<Path, QName>>(), new ArrayList<ChildAssociationRef>());
|
||||
////categoryPaths = getCategoryPaths(status.getNodeRef(), aspects, props);
|
||||
|
||||
final List<ChildAssociationRef> parentAssocs = new ArrayList<ChildAssociationRef>(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<Long, ChildAssociationRef> childAssocPair,
|
||||
Pair<Long, NodeRef> parentNodePair, Pair<Long, NodeRef> 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}
|
||||
*/
|
||||
|
@@ -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<QName, Serializable> props = new HashMap<QName, Serializable>();
|
||||
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<QName, Serializable> aspectProps = new HashMap<QName, Serializable>();
|
||||
ArrayList<NodeRef> cats = new ArrayList<NodeRef>();
|
||||
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));
|
||||
|
||||
; }
|
||||
}
|
||||
|
Reference in New Issue
Block a user