diff --git a/source/java/org/alfresco/repo/domain/node/AbstractNodeDAOImpl.java b/source/java/org/alfresco/repo/domain/node/AbstractNodeDAOImpl.java
index ffe3585a50..26c64630cf 100644
--- a/source/java/org/alfresco/repo/domain/node/AbstractNodeDAOImpl.java
+++ b/source/java/org/alfresco/repo/domain/node/AbstractNodeDAOImpl.java
@@ -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)
{
diff --git a/source/java/org/alfresco/repo/domain/node/NodeDAO.java b/source/java/org/alfresco/repo/domain/node/NodeDAO.java
index 4dfe46dc3f..d4e8d393ef 100644
--- a/source/java/org/alfresco/repo/domain/node/NodeDAO.java
+++ b/source/java/org/alfresco/repo/domain/node/NodeDAO.java
@@ -354,9 +354,22 @@ public interface NodeDAO extends NodeBulkLoader
* @param nodeId the node to change
* @param modifiedDate the date to set for cm:modified
* @return Returns true if the cm:modified property was actually set
+ * @deprecated Use {@link #setModifiedProperties(Long, Date, String)} to also change the cm:modifier property
*/
public boolean setModifiedDate(Long nodeId, Date date);
+ /**
+ * Pull the cm:modified up to the current time without changing any other
+ * cm:auditable 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 cm:modified
+ * @param modifiedBy the name to set for cm:modifier
+ * @return Returns true if the cm:modified and cm:modifier properties were actually set
+ */
+ public boolean setModifiedProperties(Long nodeId, Date modifiedDate, String modifiedBy);
+
/*
* Aspects
*/
diff --git a/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java b/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java
index 2524a4e43c..fbd53be94a 100644
--- a/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java
+++ b/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java
@@ -2913,7 +2913,8 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl
/**
* Propagate, if necessary, a cm:modified timestamp change to the parent of the
- * given association. The parent node has to be cm:auditable and the association
+ * given association, along with the cm:modifier of who changed it.
+ * The parent node has to be cm:auditable 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 parentNodePair = getNodePairNotNull(parentNodeRef);
Long parentNodeId = parentNodePair.getFirst();
+
+ // Get the ID of the child that triggered this update
+ NodeRef childNodeRef = assocRef.getChildRef();
+ Pair 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 cm:modified time on individual nodes.
+ * Wrapper to set the cm:modified time and cm:modifier 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 parentNodeIds = TransactionalResourceHelper.getSet(KEY_AUDITABLE_PROPAGATION_PRE);
+ Map 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 parentNodeIds = TransactionalResourceHelper.getSet(KEY_AUDITABLE_PROPAGATION_POST);
+ Map 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 cm:modified
+ * @param parentNodeIds the parent node IDs that need to be touched for cm:modified, and the updating child node from which to get the cm:modifier from
* @param modifiedDate the date to set
* @param useCurrentTxn true to use the current transaction
*/
- private void process(final Set parentNodeIds, Date modifiedDate, boolean useCurrentTxn)
+ private void process(final Map 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 cm:modifier from
* @param modifiedDate the date to set
* @param useCurrentTxn true 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 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 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
diff --git a/source/test-java/org/alfresco/Repository01TestSuite.java b/source/test-java/org/alfresco/Repository01TestSuite.java
index 3c25017872..e590c3b988 100644
--- a/source/test-java/org/alfresco/Repository01TestSuite.java
+++ b/source/test-java/org/alfresco/Repository01TestSuite.java
@@ -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
diff --git a/source/test-java/org/alfresco/repo/node/db/DbNodeServiceImplPropagationTest.java b/source/test-java/org/alfresco/repo/node/db/DbNodeServiceImplPropagationTest.java
new file mode 100644
index 0000000000..99f1174bc8
--- /dev/null
+++ b/source/test-java/org/alfresco/repo/node/db/DbNodeServiceImplPropagationTest.java
@@ -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 .
+ */
+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 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;
+ }
+
+}