mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-10-08 14:51:49 +00:00
Merged HEAD-BUG-FIX (5.0/Cloud) to HEAD (5.0/Cloud)
83892: Merged FEATURE2 to HEAD-BUG-FIX (5.0) 82450, 82478, 83318, 83442 : ACE-898 : Share uses "ModifiedBy" which is not always correct for folders - Propagate cm:modifier and cm:modified. Feature related test git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@84595 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
@@ -2454,6 +2454,11 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
|
|||||||
@Override
|
@Override
|
||||||
public boolean setModifiedDate(Long nodeId, Date modifiedDate)
|
public boolean setModifiedDate(Long nodeId, Date modifiedDate)
|
||||||
{
|
{
|
||||||
|
return setModifiedProperties(nodeId, modifiedDate, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean setModifiedProperties(Long nodeId, Date modifiedDate, String modifiedBy) {
|
||||||
// Do nothing if the node is not cm:auditable
|
// Do nothing if the node is not cm:auditable
|
||||||
if (!hasNodeAspect(nodeId, ContentModel.ASPECT_AUDITABLE))
|
if (!hasNodeAspect(nodeId, ContentModel.ASPECT_AUDITABLE))
|
||||||
{
|
{
|
||||||
@@ -2469,13 +2474,17 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
|
|||||||
{
|
{
|
||||||
// The properties should be present
|
// The properties should be present
|
||||||
auditableProps = new AuditablePropertiesEntity();
|
auditableProps = new AuditablePropertiesEntity();
|
||||||
auditableProps.setAuditValues(null, modifiedDate, true, 1000L);
|
auditableProps.setAuditValues(modifiedBy, modifiedDate, true, 1000L);
|
||||||
dateChanged = true;
|
dateChanged = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
auditableProps = new AuditablePropertiesEntity(auditableProps);
|
auditableProps = new AuditablePropertiesEntity(auditableProps);
|
||||||
dateChanged = auditableProps.setAuditModified(modifiedDate, 1000L);
|
dateChanged = auditableProps.setAuditModified(modifiedDate, 1000L);
|
||||||
|
if (dateChanged)
|
||||||
|
{
|
||||||
|
auditableProps.setAuditModifier(modifiedBy);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (dateChanged)
|
if (dateChanged)
|
||||||
{
|
{
|
||||||
|
@@ -354,9 +354,22 @@ public interface NodeDAO extends NodeBulkLoader
|
|||||||
* @param nodeId the node to change
|
* @param nodeId the node to change
|
||||||
* @param modifiedDate the date to set for <b>cm:modified</b>
|
* @param modifiedDate the date to set for <b>cm:modified</b>
|
||||||
* @return Returns <tt>true</tt> if the <b>cm:modified</b> property was actually set
|
* @return Returns <tt>true</tt> if the <b>cm:modified</b> property was actually set
|
||||||
|
* @deprecated Use {@link #setModifiedProperties(Long, Date, String)} to also change the <b>cm:modifier</b> property
|
||||||
*/
|
*/
|
||||||
public boolean setModifiedDate(Long nodeId, Date date);
|
public boolean setModifiedDate(Long nodeId, Date date);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pull the <b>cm:modified</b> up to the current time without changing any other
|
||||||
|
* <b>cm:auditable</b> properties. The change may be done in the current transaction
|
||||||
|
* or in a later transaction.
|
||||||
|
*
|
||||||
|
* @param nodeId the node to change
|
||||||
|
* @param modifiedDate the date to set for <b>cm:modified</b>
|
||||||
|
* @param modifiedBy the name to set for <b>cm:modifier</b>
|
||||||
|
* @return Returns <tt>true</tt> if the <b>cm:modified</b> and <b>cm:modifier</b> properties were actually set
|
||||||
|
*/
|
||||||
|
public boolean setModifiedProperties(Long nodeId, Date modifiedDate, String modifiedBy);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Aspects
|
* Aspects
|
||||||
*/
|
*/
|
||||||
|
@@ -2913,7 +2913,8 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Propagate, if necessary, a <b>cm:modified</b> timestamp change to the parent of the
|
* Propagate, if necessary, a <b>cm:modified</b> timestamp change to the parent of the
|
||||||
* given association. The parent node has to be <b>cm:auditable</b> and the association
|
* given association, along with the <b>cm:modifier</b> of who changed it.
|
||||||
|
* The parent node has to be <b>cm:auditable</b> and the association
|
||||||
* has to be marked for propagation as well.
|
* has to be marked for propagation as well.
|
||||||
*
|
*
|
||||||
* @param assocRef the association to propagate along
|
* @param assocRef the association to propagate along
|
||||||
@@ -2928,44 +2929,79 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl
|
|||||||
AssociationDefinition assocDef = dictionaryService.getAssociation(assocRef.getTypeQName());
|
AssociationDefinition assocDef = dictionaryService.getAssociation(assocRef.getTypeQName());
|
||||||
if (assocDef == null || !assocDef.isChild())
|
if (assocDef == null || !assocDef.isChild())
|
||||||
{
|
{
|
||||||
|
if (logger.isDebugEnabled())
|
||||||
|
{
|
||||||
|
logger.debug("Not propagating cm:auditable for unknown association type " + assocRef.getTypeQName());
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ChildAssociationDefinition childAssocDef = (ChildAssociationDefinition) assocDef;
|
ChildAssociationDefinition childAssocDef = (ChildAssociationDefinition) assocDef;
|
||||||
if (!childAssocDef.getPropagateTimestamps())
|
if (!childAssocDef.getPropagateTimestamps())
|
||||||
{
|
{
|
||||||
|
if (logger.isDebugEnabled())
|
||||||
|
{
|
||||||
|
logger.debug("Not propagating cm:auditable for association type " + childAssocDef.getName());
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The dictionary says propagate. Now get the parent node and prompt the touch.
|
// The dictionary says propagate. Now get the parent node and prompt the touch.
|
||||||
NodeRef parentNodeRef = assocRef.getParentRef();
|
NodeRef parentNodeRef = assocRef.getParentRef();
|
||||||
|
|
||||||
// Do not propagate if the cm:auditable behaviour is off
|
// Do not propagate if the cm:auditable behaviour is off
|
||||||
if (!policyBehaviourFilter.isEnabled(parentNodeRef, ContentModel.ASPECT_AUDITABLE))
|
if (!policyBehaviourFilter.isEnabled(parentNodeRef, ContentModel.ASPECT_AUDITABLE))
|
||||||
{
|
{
|
||||||
|
if (logger.isDebugEnabled())
|
||||||
|
{
|
||||||
|
logger.debug("Not propagating cm:auditable for non-auditable parent on " + assocRef);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Pair<Long, NodeRef> parentNodePair = getNodePairNotNull(parentNodeRef);
|
Pair<Long, NodeRef> parentNodePair = getNodePairNotNull(parentNodeRef);
|
||||||
Long parentNodeId = parentNodePair.getFirst();
|
Long parentNodeId = parentNodePair.getFirst();
|
||||||
|
|
||||||
|
// Get the ID of the child that triggered this update
|
||||||
|
NodeRef childNodeRef = assocRef.getChildRef();
|
||||||
|
Pair<Long, NodeRef> childNodePair = getNodePairNotNull(childNodeRef);
|
||||||
|
Long childNodeId = childNodePair.getFirst();
|
||||||
|
|
||||||
// If we have already modified a particular parent node in the current txn,
|
// If we have already modified a particular parent node in the current txn,
|
||||||
// it is not necessary to start a new transaction to tweak the cm:modified date.
|
// it is not necessary to start a new transaction to tweak the cm:modified date.
|
||||||
// But if the parent node was NOT touched, then doing so in this transaction would
|
// But if the parent node was NOT touched, then doing so in this transaction would
|
||||||
// create excessive concurrency and retries; in latter case we defer to a small,
|
// create excessive concurrency and retries; in latter case we defer to a small,
|
||||||
// post-commit isolated transaction.
|
// post-commit isolated transaction.
|
||||||
if (TransactionalResourceHelper.getSet(KEY_AUDITABLE_PROPAGATION_PRE).contains(parentNodeId))
|
if (TransactionalResourceHelper.getMap(KEY_AUDITABLE_PROPAGATION_PRE).containsKey(parentNodeId))
|
||||||
{
|
{
|
||||||
// It is already registered in the current transaction.
|
// It is already registered in the current transaction.
|
||||||
|
// Modified By will be taken from the previous node to touch it
|
||||||
|
if (logger.isDebugEnabled())
|
||||||
|
{
|
||||||
|
logger.debug("Update of cm:auditable already requested for " + parentNodePair);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nodeDAO.isInCurrentTxn(parentNodeId))
|
if (nodeDAO.isInCurrentTxn(parentNodeId))
|
||||||
{
|
{
|
||||||
// The parent and child are in the same transaction
|
// The parent and child are in the same transaction
|
||||||
TransactionalResourceHelper.getSet(KEY_AUDITABLE_PROPAGATION_PRE).add(parentNodeId);
|
TransactionalResourceHelper.getMap(KEY_AUDITABLE_PROPAGATION_PRE).put(parentNodeId, childNodeId);
|
||||||
// Make sure that it is not processed after the transaction
|
// Make sure that it is not processed after the transaction
|
||||||
TransactionalResourceHelper.getSet(KEY_AUDITABLE_PROPAGATION_POST).remove(parentNodeId);
|
TransactionalResourceHelper.getMap(KEY_AUDITABLE_PROPAGATION_POST).remove(parentNodeId);
|
||||||
|
|
||||||
|
if (logger.isDebugEnabled())
|
||||||
|
{
|
||||||
|
logger.debug("Performing in-transaction cm:auditable update for " + parentNodePair + " from " + childNodePair);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
TransactionalResourceHelper.getSet(KEY_AUDITABLE_PROPAGATION_POST).add(parentNodeId);
|
TransactionalResourceHelper.getMap(KEY_AUDITABLE_PROPAGATION_POST).put(parentNodeId, childNodeId);
|
||||||
|
|
||||||
|
if (logger.isDebugEnabled())
|
||||||
|
{
|
||||||
|
logger.debug("Requesting later cm:auditable update for " + parentNodePair + " from " + childNodePair);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bind a listener for post-transaction manipulation
|
// Bind a listener for post-transaction manipulation
|
||||||
@@ -2976,7 +3012,8 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl
|
|||||||
private static final String KEY_AUDITABLE_PROPAGATION_POST = "node.auditable.propagation.post";
|
private static final String KEY_AUDITABLE_PROPAGATION_POST = "node.auditable.propagation.post";
|
||||||
private AuditableTransactionListener auditableTransactionListener = new AuditableTransactionListener();
|
private AuditableTransactionListener auditableTransactionListener = new AuditableTransactionListener();
|
||||||
/**
|
/**
|
||||||
* Wrapper to set the <b>cm:modified</b> time on individual nodes.
|
* Wrapper to set the <b>cm:modified</b> time and <b>cm:modifier</b> on
|
||||||
|
* individual nodes.
|
||||||
*
|
*
|
||||||
* @author Derek Hulley
|
* @author Derek Hulley
|
||||||
* @since 3.4.6
|
* @since 3.4.6
|
||||||
@@ -2992,7 +3029,7 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl
|
|||||||
throw new IllegalStateException("Attempting to modify parent cm:modified in read-only txn.");
|
throw new IllegalStateException("Attempting to modify parent cm:modified in read-only txn.");
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<Long> parentNodeIds = TransactionalResourceHelper.getSet(KEY_AUDITABLE_PROPAGATION_PRE);
|
Map<Long,Long> parentNodeIds = TransactionalResourceHelper.getMap(KEY_AUDITABLE_PROPAGATION_PRE);
|
||||||
if (parentNodeIds.size() == 0)
|
if (parentNodeIds.size() == 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -3005,7 +3042,7 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl
|
|||||||
@Override
|
@Override
|
||||||
public void afterCommit()
|
public void afterCommit()
|
||||||
{
|
{
|
||||||
Set<Long> parentNodeIds = TransactionalResourceHelper.getSet(KEY_AUDITABLE_PROPAGATION_POST);
|
Map<Long,Long> parentNodeIds = TransactionalResourceHelper.getMap(KEY_AUDITABLE_PROPAGATION_POST);
|
||||||
if (parentNodeIds.size() == 0)
|
if (parentNodeIds.size() == 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -3015,16 +3052,16 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param parentNodeIds the parent node IDs that need to be touched for <b>cm:modified</b>
|
* @param parentNodeIds the parent node IDs that need to be touched for <b>cm:modified</b>, and the updating child node from which to get the <b>cm:modifier</b> from
|
||||||
* @param modifiedDate the date to set
|
* @param modifiedDate the date to set
|
||||||
* @param useCurrentTxn <tt>true</tt> to use the current transaction
|
* @param useCurrentTxn <tt>true</tt> to use the current transaction
|
||||||
*/
|
*/
|
||||||
private void process(final Set<Long> parentNodeIds, Date modifiedDate, boolean useCurrentTxn)
|
private void process(final Map<Long,Long> parentNodeIds, Date modifiedDate, boolean useCurrentTxn)
|
||||||
{
|
{
|
||||||
// Walk through the IDs
|
// Walk through the IDs
|
||||||
for (Long parentNodeId: parentNodeIds)
|
for (Long parentNodeId: parentNodeIds.keySet())
|
||||||
{
|
{
|
||||||
processSingle(parentNodeId, modifiedDate, useCurrentTxn);
|
processSingle(parentNodeId, parentNodeIds.get(parentNodeId), modifiedDate, useCurrentTxn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3032,10 +3069,11 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl
|
|||||||
* Touch a single node in a new, writable txn
|
* Touch a single node in a new, writable txn
|
||||||
*
|
*
|
||||||
* @param parentNodeId the parent node to touch
|
* @param parentNodeId the parent node to touch
|
||||||
|
* @param childNodeId the child node from which to get the <b>cm:modifier</b> from
|
||||||
* @param modifiedDate the date to set
|
* @param modifiedDate the date to set
|
||||||
* @param useCurrentTxn <tt>true</tt> to use the current transaction
|
* @param useCurrentTxn <tt>true</tt> to use the current transaction
|
||||||
*/
|
*/
|
||||||
private void processSingle(final Long parentNodeId, final Date modifiedDate, boolean useCurrentTxn)
|
private void processSingle(final Long parentNodeId, final Long childNodeId, final Date modifiedDate, boolean useCurrentTxn)
|
||||||
{
|
{
|
||||||
RetryingTransactionHelper txnHelper = transactionService.getRetryingTransactionHelper();
|
RetryingTransactionHelper txnHelper = transactionService.getRetryingTransactionHelper();
|
||||||
txnHelper.setMaxRetries(1);
|
txnHelper.setMaxRetries(1);
|
||||||
@@ -3044,6 +3082,7 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl
|
|||||||
@Override
|
@Override
|
||||||
public Void execute() throws Throwable
|
public Void execute() throws Throwable
|
||||||
{
|
{
|
||||||
|
// Get the details of the parent, and check it's valid to update
|
||||||
Pair<Long, NodeRef> parentNodePair = nodeDAO.getNodePair(parentNodeId);
|
Pair<Long, NodeRef> parentNodePair = nodeDAO.getNodePair(parentNodeId);
|
||||||
if (parentNodePair == null)
|
if (parentNodePair == null)
|
||||||
{
|
{
|
||||||
@@ -3055,11 +3094,46 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl
|
|||||||
}
|
}
|
||||||
NodeRef parentNodeRef = parentNodePair.getSecond();
|
NodeRef parentNodeRef = parentNodePair.getSecond();
|
||||||
|
|
||||||
|
// Fetch the modification details from the child, as best we can
|
||||||
|
Pair<Long, NodeRef> childNodePair = nodeDAO.getNodePair(childNodeId);
|
||||||
|
String modifiedByToPropagate = null;
|
||||||
|
Date modifiedDateToPropagate = modifiedDate;
|
||||||
|
if (childNodePair == null)
|
||||||
|
{
|
||||||
|
// Child has gone away, can't fetch details from children's properties
|
||||||
|
modifiedByToPropagate = AuthenticationUtil.getFullyAuthenticatedUser();
|
||||||
|
}
|
||||||
|
else if (!nodeDAO.hasNodeAspect(childNodeId, ContentModel.ASPECT_AUDITABLE))
|
||||||
|
{
|
||||||
|
// Child isn't auditable, can't fetch details
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Get the child's modification details
|
||||||
|
modifiedByToPropagate = (String)nodeDAO.getNodeProperty(childNodeId, ContentModel.PROP_MODIFIER);
|
||||||
|
modifiedDateToPropagate = (Date)nodeDAO.getNodeProperty(childNodeId, ContentModel.PROP_MODIFIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Did another child get there first?
|
||||||
|
Date parentModifiedAt = (Date)nodeDAO.getNodeProperty(parentNodeId, ContentModel.PROP_MODIFIED);
|
||||||
|
if (parentModifiedAt != null && modifiedDateToPropagate != null
|
||||||
|
&& parentModifiedAt.getTime() > modifiedDateToPropagate.getTime())
|
||||||
|
{
|
||||||
|
// Parent was modified more recently, don't update
|
||||||
|
if(logger.isDebugEnabled())
|
||||||
|
{
|
||||||
|
logger.debug("Parent " + parentNodeRef + " was modified more recently than child " +
|
||||||
|
childNodePair + " so not propogating auditable details");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Invoke policy behaviour
|
// Invoke policy behaviour
|
||||||
invokeBeforeUpdateNode(parentNodeRef);
|
invokeBeforeUpdateNode(parentNodeRef);
|
||||||
|
|
||||||
// Touch the node; it is cm:auditable
|
// Touch the node; it is cm:auditable
|
||||||
boolean changed = nodeDAO.setModifiedDate(parentNodeId, modifiedDate);
|
boolean changed = nodeDAO.setModifiedProperties(parentNodeId, modifiedDate, modifiedByToPropagate);
|
||||||
|
|
||||||
if (changed)
|
if (changed)
|
||||||
{
|
{
|
||||||
|
@@ -225,6 +225,7 @@ public class Repository01TestSuite extends TestSuite
|
|||||||
suite.addTestSuite(org.alfresco.repo.node.archive.LargeArchiveAndRestoreTest.class);
|
suite.addTestSuite(org.alfresco.repo.node.archive.LargeArchiveAndRestoreTest.class);
|
||||||
suite.addTest(new JUnit4TestAdapter(org.alfresco.repo.node.cleanup.TransactionCleanupTest.class));
|
suite.addTest(new JUnit4TestAdapter(org.alfresco.repo.node.cleanup.TransactionCleanupTest.class));
|
||||||
suite.addTestSuite(org.alfresco.repo.node.db.DbNodeServiceImplTest.class);
|
suite.addTestSuite(org.alfresco.repo.node.db.DbNodeServiceImplTest.class);
|
||||||
|
suite.addTestSuite(org.alfresco.repo.node.db.DbNodeServiceImplPropagationTest.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void tests36(TestSuite suite) // Fails with previous tests
|
static void tests36(TestSuite suite) // Fails with previous tests
|
||||||
|
@@ -0,0 +1,358 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2005-2014 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.db;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import javax.transaction.UserTransaction;
|
||||||
|
|
||||||
|
import org.alfresco.model.ContentModel;
|
||||||
|
import org.alfresco.repo.dictionary.DictionaryComponent;
|
||||||
|
import org.alfresco.repo.dictionary.DictionaryDAO;
|
||||||
|
import org.alfresco.repo.dictionary.M2Model;
|
||||||
|
import org.alfresco.repo.domain.node.NodeDAO;
|
||||||
|
import org.alfresco.repo.node.BaseNodeServiceTest;
|
||||||
|
import org.alfresco.repo.security.authentication.AuthenticationComponent;
|
||||||
|
import org.alfresco.repo.security.authentication.AuthenticationUtil;
|
||||||
|
import org.alfresco.service.cmr.dictionary.DictionaryService;
|
||||||
|
import org.alfresco.service.cmr.repository.ChildAssociationRef;
|
||||||
|
import org.alfresco.service.cmr.repository.NodeRef;
|
||||||
|
import org.alfresco.service.cmr.repository.NodeService;
|
||||||
|
import org.alfresco.service.cmr.repository.StoreRef;
|
||||||
|
import org.alfresco.service.namespace.QName;
|
||||||
|
import org.alfresco.service.transaction.TransactionService;
|
||||||
|
import org.alfresco.util.BaseSpringTest;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see org.alfresco.repo.node.db.DbNodeServiceImpl#propagateTimeStamp
|
||||||
|
*
|
||||||
|
* @author sergey.shcherbovich
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class DbNodeServiceImplPropagationTest extends BaseSpringTest
|
||||||
|
{
|
||||||
|
private TransactionService txnService;
|
||||||
|
private NodeDAO nodeDAO;
|
||||||
|
private NodeService nodeService;
|
||||||
|
private AuthenticationComponent authenticationComponent;
|
||||||
|
protected DictionaryService dictionaryService;
|
||||||
|
|
||||||
|
private UserTransaction txn = null;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSetUpInTransaction() throws Exception
|
||||||
|
{
|
||||||
|
super.onSetUpInTransaction();
|
||||||
|
txnService = (TransactionService) applicationContext.getBean("transactionComponent");
|
||||||
|
nodeDAO = (NodeDAO) applicationContext.getBean("nodeDAO");
|
||||||
|
nodeService = (NodeService) applicationContext.getBean("dbNodeService");
|
||||||
|
|
||||||
|
authenticationComponent = (AuthenticationComponent) applicationContext.getBean("authenticationComponent");
|
||||||
|
|
||||||
|
authenticationComponent.setSystemUserAsCurrentUser();
|
||||||
|
|
||||||
|
DictionaryDAO dictionaryDao = (DictionaryDAO) applicationContext.getBean("dictionaryDAO");
|
||||||
|
// load the system model
|
||||||
|
ClassLoader cl = BaseNodeServiceTest.class.getClassLoader();
|
||||||
|
InputStream modelStream = cl.getResourceAsStream("alfresco/model/contentModel.xml");
|
||||||
|
assertNotNull(modelStream);
|
||||||
|
M2Model model = M2Model.createModel(modelStream);
|
||||||
|
dictionaryDao.putModel(model);
|
||||||
|
// load the test model
|
||||||
|
modelStream = cl.getResourceAsStream("org/alfresco/repo/node/BaseNodeServiceTest_model.xml");
|
||||||
|
assertNotNull(modelStream);
|
||||||
|
model = M2Model.createModel(modelStream);
|
||||||
|
dictionaryDao.putModel(model);
|
||||||
|
|
||||||
|
DictionaryComponent dictionary = new DictionaryComponent();
|
||||||
|
dictionary.setDictionaryDAO(dictionaryDao);
|
||||||
|
dictionaryService = loadModel(applicationContext);
|
||||||
|
|
||||||
|
txn = restartAuditableTxn(txn);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onTearDownInTransaction() throws Exception
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
authenticationComponent.clearCurrentSecurityContext();
|
||||||
|
}
|
||||||
|
catch (Throwable e)
|
||||||
|
{
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
super.onTearDownInTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the test model required for building the node graphs
|
||||||
|
*/
|
||||||
|
public static DictionaryService loadModel(ApplicationContext applicationContext)
|
||||||
|
{
|
||||||
|
DictionaryDAO dictionaryDao = (DictionaryDAO) applicationContext.getBean("dictionaryDAO");
|
||||||
|
// load the system model
|
||||||
|
ClassLoader cl = BaseNodeServiceTest.class.getClassLoader();
|
||||||
|
InputStream modelStream = cl.getResourceAsStream("alfresco/model/contentModel.xml");
|
||||||
|
assertNotNull(modelStream);
|
||||||
|
M2Model model = M2Model.createModel(modelStream);
|
||||||
|
dictionaryDao.putModel(model);
|
||||||
|
// load the test model
|
||||||
|
modelStream = cl.getResourceAsStream("org/alfresco/repo/node/BaseNodeServiceTest_model.xml");
|
||||||
|
assertNotNull(modelStream);
|
||||||
|
model = M2Model.createModel(modelStream);
|
||||||
|
dictionaryDao.putModel(model);
|
||||||
|
|
||||||
|
DictionaryComponent dictionary = new DictionaryComponent();
|
||||||
|
dictionary.setDictionaryDAO(dictionaryDao);
|
||||||
|
// done
|
||||||
|
return dictionary;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Tests that the auditable modification details (modified by, modified at)
|
||||||
|
* get correctly propagated to the parent, where appropriate, when children
|
||||||
|
* are added or removed.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
public void testAuditablePropagation() throws Exception
|
||||||
|
{
|
||||||
|
String fullyAuthenticatedUser = AuthenticationUtil.getFullyAuthenticatedUser();
|
||||||
|
|
||||||
|
final QName TYPE_NOT_AUDITABLE = ContentModel.TYPE_CONTAINER;
|
||||||
|
final QName TYPE_AUDITABLE = ContentModel.TYPE_CONTENT;
|
||||||
|
final QName ASSOC_NOT_AUDITABLE = ContentModel.ASSOC_CHILDREN;
|
||||||
|
final QName ASSOC_AUDITABLE = ContentModel.ASSOC_CONTAINS;
|
||||||
|
|
||||||
|
// create a first store directly
|
||||||
|
StoreRef storeRef = nodeService.createStore(
|
||||||
|
StoreRef.PROTOCOL_WORKSPACE,
|
||||||
|
"Test_" + System.currentTimeMillis());
|
||||||
|
NodeRef rootNodeRef = nodeService.getRootNode(storeRef);
|
||||||
|
|
||||||
|
Map<QName, ChildAssociationRef> assocRefs = BaseNodeServiceTest.buildNodeGraph(nodeService, rootNodeRef);
|
||||||
|
// UserTransaction txn = null;
|
||||||
|
Date modifiedAt = null;
|
||||||
|
|
||||||
|
// Get the root node to test against
|
||||||
|
ChildAssociationRef n2pn4Ref = assocRefs.get(QName.createQName(BaseNodeServiceTest.NAMESPACE, "n2_p_n4"));
|
||||||
|
final NodeRef n2Ref = n2pn4Ref.getParentRef();
|
||||||
|
final long n2Id = nodeDAO.getNodePair(n2Ref).getFirst();
|
||||||
|
|
||||||
|
// Doesn't start out auditable
|
||||||
|
assertFalse("Shouldn't be auditable in " + nodeService.getAspects(n2Ref),
|
||||||
|
nodeService.getAspects(n2Ref).contains(ContentModel.ASPECT_AUDITABLE));
|
||||||
|
|
||||||
|
QName typeBefore = nodeService.getType(n2Ref);
|
||||||
|
|
||||||
|
// Get onto our own transactions
|
||||||
|
setComplete();
|
||||||
|
endTransaction();
|
||||||
|
txn = restartAuditableTxn(txn);
|
||||||
|
|
||||||
|
// Create a non-auditable child, parent won't update
|
||||||
|
NodeRef naC = nodeService.createNode(n2Ref, ASSOC_NOT_AUDITABLE,
|
||||||
|
QName.createQName("not-auditable"), TYPE_NOT_AUDITABLE).getChildRef();
|
||||||
|
logger.debug("Created non-auditable child " + naC);
|
||||||
|
|
||||||
|
txn = restartAuditableTxn(txn);
|
||||||
|
|
||||||
|
// Parent hasn't been updated
|
||||||
|
assertNull(nodeService.getProperty(n2Ref, ContentModel.PROP_MODIFIED));
|
||||||
|
assertNull(nodeService.getProperty(n2Ref, ContentModel.PROP_MODIFIER));
|
||||||
|
|
||||||
|
// Create an auditable child, parent won't update either as still not auditable
|
||||||
|
NodeRef adC = nodeService.createNode(n2Ref, ASSOC_NOT_AUDITABLE,
|
||||||
|
QName.createQName("is-auditable"), TYPE_AUDITABLE).getChildRef();
|
||||||
|
nodeService.addAspect(adC, ContentModel.ASPECT_AUDITABLE, null);
|
||||||
|
logger.debug("Created auditable child " + naC + " of non-auditable parent " + n2Ref);
|
||||||
|
txn = restartAuditableTxn(txn);
|
||||||
|
|
||||||
|
// Parent hasn't been updated, but auditable child has
|
||||||
|
assertNull(nodeService.getProperty(n2Ref, ContentModel.PROP_MODIFIED));
|
||||||
|
assertNull(nodeService.getProperty(n2Ref, ContentModel.PROP_MODIFIER));
|
||||||
|
assertNotNull(nodeService.getProperty(adC, ContentModel.PROP_MODIFIED));
|
||||||
|
assertNotNull(nodeService.getProperty(adC, ContentModel.PROP_MODIFIER));
|
||||||
|
|
||||||
|
// Make the parent auditable, and give it a special modified by
|
||||||
|
nodeService.addAspect(n2Ref, ContentModel.ASPECT_AUDITABLE, null);
|
||||||
|
nodeService.setType(n2Ref, ContentModel.TYPE_FOLDER);
|
||||||
|
txn = restartAuditableTxn(txn);
|
||||||
|
|
||||||
|
Date modified = new Date();
|
||||||
|
nodeDAO.setModifiedProperties(n2Id, modified, "TestModifier");
|
||||||
|
txn = restartAuditableTxn(txn);
|
||||||
|
assertEquals(modified.getTime(), ((Date)nodeDAO.getNodeProperty(n2Id, ContentModel.PROP_MODIFIED)).getTime());
|
||||||
|
assertEquals("TestModifier", nodeDAO.getNodeProperty(n2Id, ContentModel.PROP_MODIFIER));
|
||||||
|
|
||||||
|
// Delete the non-auditable child
|
||||||
|
// No change to the parent as non-auditable child
|
||||||
|
logger.debug("Deleting non-auditable child " + naC + " of auditable parent " + n2Ref);
|
||||||
|
nodeService.addAspect(naC, ContentModel.ASPECT_TEMPORARY, null);
|
||||||
|
nodeService.deleteNode(naC);
|
||||||
|
txn = restartAuditableTxn(txn);
|
||||||
|
|
||||||
|
assertEquals(modified.getTime(), ((Date)nodeDAO.getNodeProperty(n2Id, ContentModel.PROP_MODIFIED)).getTime());
|
||||||
|
assertEquals("TestModifier", nodeDAO.getNodeProperty(n2Id, ContentModel.PROP_MODIFIER));
|
||||||
|
|
||||||
|
// Add an auditable child, parent will be updated
|
||||||
|
adC = nodeService.createNode(n2Ref, ASSOC_AUDITABLE,
|
||||||
|
QName.createQName("is-auditable"), TYPE_AUDITABLE).getChildRef();
|
||||||
|
final long adCId = nodeDAO.getNodePair(adC).getFirst();
|
||||||
|
|
||||||
|
txn = restartAuditableTxn(txn);
|
||||||
|
modifiedAt = (Date)nodeService.getProperty(n2Ref, ContentModel.PROP_MODIFIED);
|
||||||
|
assertNotNull(modifiedAt);
|
||||||
|
assertEquals((double)new Date().getTime(), (double)modifiedAt.getTime(), 10000d);
|
||||||
|
assertEquals(fullyAuthenticatedUser, nodeService.getProperty(n2Ref, ContentModel.PROP_MODIFIER));
|
||||||
|
|
||||||
|
// Set well-known modified details on both nodes
|
||||||
|
nodeDAO.setModifiedProperties(n2Id, new Date(Integer.MIN_VALUE), "TestModifierPrnt");
|
||||||
|
nodeDAO.setModifiedProperties(adCId, new Date(Integer.MIN_VALUE), "TestModifierChld");
|
||||||
|
txn = restartAuditableTxn(txn);
|
||||||
|
|
||||||
|
// Now delete the auditable child
|
||||||
|
// The parent's modified date will change, but not the modified by, as the child
|
||||||
|
// has been deleted so the child's modified-by can't be read
|
||||||
|
logger.debug("Deleting auditable child " + adC + " of auditable parent " + n2Ref);
|
||||||
|
nodeService.deleteNode(adC);
|
||||||
|
txn = restartAuditableTxn(txn);
|
||||||
|
|
||||||
|
// Parent's date was updated, but not the modifier, since child was deleted
|
||||||
|
// which means the child's modifier wasn't available to read
|
||||||
|
modifiedAt = (Date)nodeService.getProperty(n2Ref, ContentModel.PROP_MODIFIED);
|
||||||
|
assertNotNull(modifiedAt);
|
||||||
|
assertEquals((double)new Date().getTime(), (double)modifiedAt.getTime(), 10000d);
|
||||||
|
assertEquals(fullyAuthenticatedUser, nodeService.getProperty(n2Ref, ContentModel.PROP_MODIFIER));
|
||||||
|
|
||||||
|
// Set well-known modified detail on our parent again
|
||||||
|
modified = new Date();
|
||||||
|
nodeDAO.setModifiedProperties(n2Id, modified, "ModOn2");
|
||||||
|
txn = restartAuditableTxn(txn);
|
||||||
|
assertEquals("ModOn2", nodeService.getProperty(n2Ref, ContentModel.PROP_MODIFIER));
|
||||||
|
|
||||||
|
// Add two auditable children, both with special modifiers
|
||||||
|
// Only the first child's update in a transaction will be used
|
||||||
|
NodeRef ac1 = nodeService.createNode(n2Ref, ASSOC_AUDITABLE,
|
||||||
|
QName.createQName("is-auditable-1"), TYPE_AUDITABLE).getChildRef();
|
||||||
|
NodeRef ac2 = nodeService.createNode(n2Ref, ASSOC_AUDITABLE,
|
||||||
|
QName.createQName("is-auditable-2"), TYPE_AUDITABLE).getChildRef();
|
||||||
|
final long ac1Id = nodeDAO.getNodePair(ac1).getFirst();
|
||||||
|
final long ac2Id = nodeDAO.getNodePair(ac2).getFirst();
|
||||||
|
|
||||||
|
// Manually set different modifiers on the children, so that
|
||||||
|
// we can test to see if they propagate properly
|
||||||
|
nodeDAO.setModifiedProperties(ac1Id, new Date(), "ModAC1");
|
||||||
|
nodeDAO.setModifiedProperties(ac2Id, new Date(), "ModAC2");
|
||||||
|
// Ensure the parent is "old", so that the propagation can take place
|
||||||
|
nodeDAO.setModifiedProperties(n2Id, new Date(Integer.MIN_VALUE), "ModOn2");
|
||||||
|
|
||||||
|
txn = restartAuditableTxn(txn);
|
||||||
|
|
||||||
|
// Check that only the first reached the parent
|
||||||
|
assertNotNull(nodeService.getProperty(n2Ref, ContentModel.PROP_MODIFIED));
|
||||||
|
assertNotNull(nodeService.getProperty(ac1, ContentModel.PROP_MODIFIED));
|
||||||
|
assertNotNull(nodeService.getProperty(ac2, ContentModel.PROP_MODIFIED));
|
||||||
|
|
||||||
|
assertEquals(fullyAuthenticatedUser, nodeService.getProperty(n2Ref, ContentModel.PROP_MODIFIER));
|
||||||
|
assertEquals(fullyAuthenticatedUser, nodeService.getProperty(ac1, ContentModel.PROP_MODIFIER));
|
||||||
|
assertEquals(fullyAuthenticatedUser, nodeService.getProperty(ac2, ContentModel.PROP_MODIFIER));
|
||||||
|
|
||||||
|
// Updates won't apply if the parent is newer than the child
|
||||||
|
Date now = new Date();
|
||||||
|
long futureShift = 4000l;
|
||||||
|
Date future = new Date(now.getTime()+futureShift);
|
||||||
|
nodeDAO.setModifiedProperties(n2Id, future, "TestModifierPrnt");
|
||||||
|
|
||||||
|
NodeRef ac3 = nodeService.createNode(n2Ref, ASSOC_AUDITABLE,
|
||||||
|
QName.createQName("is-auditable-3"), TYPE_AUDITABLE).getChildRef();
|
||||||
|
|
||||||
|
txn = restartAuditableTxn(txn);
|
||||||
|
|
||||||
|
assertEquals("TestModifierPrnt", nodeService.getProperty(n2Ref, ContentModel.PROP_MODIFIER));
|
||||||
|
assertEquals(fullyAuthenticatedUser, nodeService.getProperty(ac3, ContentModel.PROP_MODIFIER));
|
||||||
|
|
||||||
|
modifiedAt = (Date)nodeService.getProperty(n2Ref, ContentModel.PROP_MODIFIED);
|
||||||
|
assertEquals((double)future.getTime(), (double)modifiedAt.getTime(), 1000d);
|
||||||
|
modifiedAt = (Date)nodeService.getProperty(ac3, ContentModel.PROP_MODIFIED);
|
||||||
|
assertEquals((double)now.getTime(), (double)modifiedAt.getTime(), 1000d);
|
||||||
|
|
||||||
|
// Parent-Child association needs to be a suitable kind to trigger
|
||||||
|
nodeService.setType(n2Ref, typeBefore);
|
||||||
|
|
||||||
|
txn = restartAuditableTxn(txn);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Thread.sleep(futureShift);
|
||||||
|
}
|
||||||
|
catch(InterruptedException e)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
modified = new Date();
|
||||||
|
nodeDAO.setModifiedProperties(n2Id, modified, "TestModifier");
|
||||||
|
|
||||||
|
txn = restartAuditableTxn(txn);
|
||||||
|
|
||||||
|
assertEquals("TestModifier", nodeService.getProperty(n2Ref, ContentModel.PROP_MODIFIER));
|
||||||
|
|
||||||
|
NodeRef ac4 = nodeService.createNode(n2Ref, ASSOC_NOT_AUDITABLE,
|
||||||
|
QName.createQName("is-auditable-4"), TYPE_AUDITABLE).getChildRef();
|
||||||
|
|
||||||
|
txn = restartAuditableTxn(txn);
|
||||||
|
|
||||||
|
assertEquals("TestModifier", nodeService.getProperty(n2Ref, ContentModel.PROP_MODIFIER));
|
||||||
|
assertEquals(fullyAuthenticatedUser, nodeService.getProperty(ac4, ContentModel.PROP_MODIFIER));
|
||||||
|
|
||||||
|
modifiedAt = (Date)nodeService.getProperty(n2Ref, ContentModel.PROP_MODIFIED);
|
||||||
|
assertEquals(modified.getTime(), modifiedAt.getTime());
|
||||||
|
modifiedAt = (Date)nodeService.getProperty(ac4, ContentModel.PROP_MODIFIED);
|
||||||
|
assertEquals((double)new Date().getTime(), (double)modifiedAt.getTime(), 3000d);
|
||||||
|
|
||||||
|
setComplete();
|
||||||
|
endTransaction();
|
||||||
|
|
||||||
|
startNewTransaction();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserTransaction restartAuditableTxn(UserTransaction txn) throws Exception
|
||||||
|
{
|
||||||
|
if (txn != null)
|
||||||
|
txn.commit();
|
||||||
|
txn = txnService.getUserTransaction();
|
||||||
|
txn.begin();
|
||||||
|
|
||||||
|
// Wait long enough that AuditablePropertiesEntity.setAuditModified
|
||||||
|
// will recognize subsequent changes as needing new audit entries
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Thread.sleep(1250L);
|
||||||
|
}
|
||||||
|
catch(InterruptedException e)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
return txn;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Reference in New Issue
Block a user