From 03952f85518960e7efec3ea3e9f1b9b485fd1bf6 Mon Sep 17 00:00:00 2001 From: Alan Davis Date: Thu, 18 Sep 2014 17:16:45 +0000 Subject: [PATCH] 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 --- .../repo/domain/node/AbstractNodeDAOImpl.java | 11 +- .../alfresco/repo/domain/node/NodeDAO.java | 13 + .../repo/node/db/DbNodeServiceImpl.java | 104 ++++- .../org/alfresco/Repository01TestSuite.java | 1 + .../db/DbNodeServiceImplPropagationTest.java | 358 ++++++++++++++++++ 5 files changed, 471 insertions(+), 16 deletions(-) create mode 100644 source/test-java/org/alfresco/repo/node/db/DbNodeServiceImplPropagationTest.java 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; + } + +}