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:
Alan Davis
2014-09-18 17:16:45 +00:00
parent d9363413c3
commit 03952f8551
5 changed files with 471 additions and 16 deletions

View File

@@ -2454,6 +2454,11 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
@Override
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
if (!hasNodeAspect(nodeId, ContentModel.ASPECT_AUDITABLE))
{
@@ -2469,13 +2474,17 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
{
// The properties should be present
auditableProps = new AuditablePropertiesEntity();
auditableProps.setAuditValues(null, modifiedDate, true, 1000L);
auditableProps.setAuditValues(modifiedBy, modifiedDate, true, 1000L);
dateChanged = true;
}
else
{
auditableProps = new AuditablePropertiesEntity(auditableProps);
dateChanged = auditableProps.setAuditModified(modifiedDate, 1000L);
if (dateChanged)
{
auditableProps.setAuditModifier(modifiedBy);
}
}
if (dateChanged)
{

View File

@@ -354,9 +354,22 @@ public interface NodeDAO extends NodeBulkLoader
* @param nodeId the node to change
* @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
* @deprecated Use {@link #setModifiedProperties(Long, Date, String)} to also change the <b>cm:modifier</b> property
*/
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
*/

View File

@@ -2913,7 +2913,8 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl
/**
* 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.
*
* @param assocRef the association to propagate along
@@ -2928,44 +2929,79 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl
AssociationDefinition assocDef = dictionaryService.getAssociation(assocRef.getTypeQName());
if (assocDef == null || !assocDef.isChild())
{
if (logger.isDebugEnabled())
{
logger.debug("Not propagating cm:auditable for unknown association type " + assocRef.getTypeQName());
}
return;
}
ChildAssociationDefinition childAssocDef = (ChildAssociationDefinition) assocDef;
if (!childAssocDef.getPropagateTimestamps())
{
if (logger.isDebugEnabled())
{
logger.debug("Not propagating cm:auditable for association type " + childAssocDef.getName());
}
return;
}
// The dictionary says propagate. Now get the parent node and prompt the touch.
NodeRef parentNodeRef = assocRef.getParentRef();
// Do not propagate if the cm:auditable behaviour is off
if (!policyBehaviourFilter.isEnabled(parentNodeRef, ContentModel.ASPECT_AUDITABLE))
{
if (logger.isDebugEnabled())
{
logger.debug("Not propagating cm:auditable for non-auditable parent on " + assocRef);
}
return;
}
Pair<Long, NodeRef> parentNodePair = getNodePairNotNull(parentNodeRef);
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,
// 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
// create excessive concurrency and retries; in latter case we defer to a small,
// 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.
// 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;
}
if (nodeDAO.isInCurrentTxn(parentNodeId))
{
// 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
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
{
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
@@ -2976,7 +3012,8 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl
private static final String KEY_AUDITABLE_PROPAGATION_POST = "node.auditable.propagation.post";
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
* @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.");
}
Set<Long> parentNodeIds = TransactionalResourceHelper.getSet(KEY_AUDITABLE_PROPAGATION_PRE);
Map<Long,Long> parentNodeIds = TransactionalResourceHelper.getMap(KEY_AUDITABLE_PROPAGATION_PRE);
if (parentNodeIds.size() == 0)
{
return;
@@ -3005,7 +3042,7 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl
@Override
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)
{
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 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
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
*
* @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 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();
txnHelper.setMaxRetries(1);
@@ -3044,6 +3082,7 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl
@Override
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);
if (parentNodePair == null)
{
@@ -3055,12 +3094,47 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl
}
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
invokeBeforeUpdateNode(parentNodeRef);
// Touch the node; it is cm:auditable
boolean changed = nodeDAO.setModifiedDate(parentNodeId, modifiedDate);
boolean changed = nodeDAO.setModifiedProperties(parentNodeId, modifiedDate, modifiedByToPropagate);
if (changed)
{
// Invoke policy behaviour

View File

@@ -225,6 +225,7 @@ public class Repository01TestSuite extends TestSuite
suite.addTestSuite(org.alfresco.repo.node.archive.LargeArchiveAndRestoreTest.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.DbNodeServiceImplPropagationTest.class);
}
static void tests36(TestSuite suite) // Fails with previous tests

View File

@@ -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;
}
}