diff --git a/config/alfresco/hibernate-context.xml b/config/alfresco/hibernate-context.xml index fe7867c761..c13bca8fe4 100644 --- a/config/alfresco/hibernate-context.xml +++ b/config/alfresco/hibernate-context.xml @@ -341,7 +341,28 @@ + + + + + + 1 + + + 50 + + + 100 + + + 50 + + + + + ${system.enableTimestampPropagation} + @@ -357,6 +378,9 @@ + + + diff --git a/config/alfresco/model/contentModel.xml b/config/alfresco/model/contentModel.xml index 6cabfc954b..2af9b53be9 100644 --- a/config/alfresco/model/contentModel.xml +++ b/config/alfresco/model/contentModel.xml @@ -63,6 +63,7 @@ true false + true diff --git a/config/alfresco/public-services-security-context.xml b/config/alfresco/public-services-security-context.xml index 5d9c5bab2f..0904040a62 100644 --- a/config/alfresco/public-services-security-context.xml +++ b/config/alfresco/public-services-security-context.xml @@ -412,6 +412,7 @@ org.alfresco.service.cmr.model.FileFolderService.getFileInfo=ACL_NODE.0.sys:base.ReadProperties org.alfresco.service.cmr.model.FileFolderService.getReader=ACL_NODE.0.sys:base.ReadContent org.alfresco.service.cmr.model.FileFolderService.getWriter=ACL_NODE.0.sys:base.WriteContent + org.alfresco.service.cmr.model.FileFolderService.exists=ACL_ALLOW org.alfresco.service.cmr.model.FileFolderService.getType=ACL_ALLOW diff --git a/config/alfresco/repository.properties b/config/alfresco/repository.properties index 792b0eb0c8..7c585c5756 100644 --- a/config/alfresco/repository.properties +++ b/config/alfresco/repository.properties @@ -123,6 +123,12 @@ system.hibernateMaxExecutions=20000 # transaction that triggers the operation. system.cascadeDeleteInTransaction=true +# +# Determine if modification timestamp propagation from child to parent nodes is respected or not. +# Even if 'true', the functionality is only supported for child associations that declare the +# 'propagateTimestamps' element in the dictionary definition. +system.enableTimestampPropagation=false + # #################### # # Lucene configuration # # #################### # diff --git a/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java b/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java index 9ba5ac3b5b..073ff5dfda 100644 --- a/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java +++ b/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java @@ -1732,7 +1732,7 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa // Get the node for the folder NodeRef nodeRef = cifsHelper.getNodeRef(deviceRootNodeRef, dir); - if (nodeService.exists(nodeRef)) + if (fileFolderService.exists(nodeRef)) { // Check if the folder is empty @@ -1740,7 +1740,7 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa // Delete the folder node - nodeService.deleteNode(nodeRef); + fileFolderService.delete(nodeRef); // Remove the file state @@ -1849,13 +1849,13 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa // We don't know how long the network file has had the reference, so check for existence - if (nodeService.exists(nodeRef)) + if (fileFolderService.exists(nodeRef)) { try { // Delete the file - nodeService.deleteNode(nodeRef); + fileFolderService.delete(nodeRef); // Remove the file state @@ -1866,9 +1866,6 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa // sess.endTransaction(); // beginReadTransaction( sess); - - if ( nodeService.exists( nodeRef)) - System.out.println("Node still exists - " + file.getFullName()); } catch (org.alfresco.repo.security.permissions.AccessDeniedException ex) { @@ -1930,9 +1927,9 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa // Get the node NodeRef nodeRef = getNodeForPath(tree, name); - if (nodeService.exists(nodeRef)) + if (fileFolderService.exists(nodeRef)) { - nodeService.deleteNode(nodeRef); + fileFolderService.delete(nodeRef); // Remove the file state @@ -2551,7 +2548,7 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa { // Check that the node exists - if (nodeService.exists(fstate.getNodeRef())) + if (fileFolderService.exists(fstate.getNodeRef())) { // Bump the file states expiry time diff --git a/source/java/org/alfresco/repo/avm/AVMLockingAwareService.java b/source/java/org/alfresco/repo/avm/AVMLockingAwareService.java index f952110a36..cafe79d4ec 100644 --- a/source/java/org/alfresco/repo/avm/AVMLockingAwareService.java +++ b/source/java/org/alfresco/repo/avm/AVMLockingAwareService.java @@ -587,6 +587,15 @@ public class AVMLockingAwareService implements AVMService, ApplicationContextAwa // TODO Does this need a lock? I don't think so, but revisit. fService.link(parentPath, name, toLink); } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.avm.AVMService#updateLink(java.lang.String, java.lang.String, org.alfresco.service.cmr.avm.AVMNodeDescriptor) + */ + public void updateLink(String parentPath, String name, AVMNodeDescriptor toLink) + { + // TODO Does this need a lock? I don't think so, but revisit. + fService.updateLink(parentPath, name, toLink); + } /* (non-Javadoc) * @see org.alfresco.service.cmr.avm.AVMService#lookup(int, java.lang.String) diff --git a/source/java/org/alfresco/repo/avm/AVMRepository.java b/source/java/org/alfresco/repo/avm/AVMRepository.java index 140950b23e..705a048eba 100644 --- a/source/java/org/alfresco/repo/avm/AVMRepository.java +++ b/source/java/org/alfresco/repo/avm/AVMRepository.java @@ -586,7 +586,7 @@ public class AVMRepository // dstNode.setVersionID(dstRepo.getNextVersionID()); dstNode.setAncestor(srcNode); dirNode.putChild(name, dstNode); - dirNode.updateModTime(); + // dirNode.updateModTime(); String beginingPath = AVMNodeConverter.NormalizePath(srcPath); String finalPath = AVMNodeConverter.ExtendAVMPath(dstPath, name); finalPath = AVMNodeConverter.NormalizePath(finalPath); @@ -853,13 +853,13 @@ public class AVMRepository dstNode = new PlainFileNodeImpl((PlainFileNode) srcNode, dstRepo, parentAcl, ACLCopyMode.COPY); } srcDir.removeChild(sPath, srcName); - srcDir.updateModTime(); + // srcDir.updateModTime(); // dstNode.setVersionID(dstRepo.getNextVersionID()); if (child != null) { dstNode.setAncestor(child); } - dstDir.updateModTime(); + //dstDir.updateModTime(); dstDir.putChild(dstName, dstNode); if (child == null) { @@ -2826,6 +2826,36 @@ public class AVMRepository fLookupCount.set(null); } } + + /** + * Update a link, directly. + * + * @param parentPath + * The path to the parent. + * @param name + * The name to give the node. + * @param toLink + * The node to link. + */ + public void updateLink(String parentPath, String name, AVMNodeDescriptor toLink) + { + fLookupCount.set(1); + try + { + String[] pathParts = SplitPath(parentPath); + AVMStore store = getAVMStoreByName(pathParts[0]); + if (store == null) + { + throw new AVMNotFoundException("Store not found."); + } + store.updateLink(pathParts[1], name, toLink); + fLookupCache.onWrite(pathParts[0]); + } + finally + { + fLookupCount.set(null); + } + } /** * This is the danger version of link. It must be called on a copied and unsnapshotted directory. It blithely diff --git a/source/java/org/alfresco/repo/avm/AVMServiceImpl.java b/source/java/org/alfresco/repo/avm/AVMServiceImpl.java index 69f3885863..93d7263461 100644 --- a/source/java/org/alfresco/repo/avm/AVMServiceImpl.java +++ b/source/java/org/alfresco/repo/avm/AVMServiceImpl.java @@ -1371,6 +1371,21 @@ public class AVMServiceImpl implements AVMService } fAVMRepository.link(parentPath, name, toLink); } + + /** + * This replaces a node into a parent directly. + * @param parentPath The path to the parent directory. + * @param name The name to give the node. + * @param toLink A descriptor for the node to insert. + */ + public void updateLink(String parentPath, String name, AVMNodeDescriptor toLink) + { + if (parentPath == null || name == null || toLink == null) + { + throw new AVMBadArgumentException("Illegal Null Argument."); + } + fAVMRepository.updateLink(parentPath, name, toLink); + } /** * Force copy on write of a path. diff --git a/source/java/org/alfresco/repo/avm/AVMStore.java b/source/java/org/alfresco/repo/avm/AVMStore.java index 97c2dda5cf..a8e637c2ed 100644 --- a/source/java/org/alfresco/repo/avm/AVMStore.java +++ b/source/java/org/alfresco/repo/avm/AVMStore.java @@ -469,12 +469,20 @@ public interface AVMStore public DbAccessControlList getACL(int version, String path); /** - * Link a node intro a directory, directly. + * Link a node into a directory, directly. * @param parentPath The path to the directory. * @param name The name to give the node. * @param toLink The node to link. */ public void link(String parentPath, String name, AVMNodeDescriptor toLink); + + /** + * Update a link to a node in a directory, directly. + * @param parentPath The path to the directory. + * @param name The name to give the node. + * @param toLink The node to link. + */ + public void updateLink(String parentPath, String name, AVMNodeDescriptor toLink); /** * Revert a head path to a given version. This works by cloning diff --git a/source/java/org/alfresco/repo/avm/AVMStoreImpl.java b/source/java/org/alfresco/repo/avm/AVMStoreImpl.java index a06db64ddc..2a6e9ed365 100644 --- a/source/java/org/alfresco/repo/avm/AVMStoreImpl.java +++ b/source/java/org/alfresco/repo/avm/AVMStoreImpl.java @@ -1711,7 +1711,7 @@ public class AVMStoreImpl implements AVMStore, Serializable } /** - * Link a node intro a directory, directly. + * Link a node into a directory, directly. * @param parentPath The path to the directory. * @param name The name to give the parent. * @param toLink The node to link. @@ -1730,6 +1730,37 @@ public class AVMStoreImpl implements AVMStore, Serializable } dir.link(lPath, name, toLink); } + + /** + * Update a link to a node in a directory, directly. + * @param parentPath The path to the directory. + * @param name The name to give the parent. + * @param toLink The node to link. + */ + public void updateLink(String parentPath, String name, AVMNodeDescriptor toLink) + { + Lookup lPath = lookupDirectory(-1, parentPath, true); + if (lPath == null) + { + throw new AVMNotFoundException("Path " + parentPath + " not found."); + } + DirectoryNode dir = (DirectoryNode)lPath.getCurrentNode(); + + Lookup cPath = new Lookup(lPath, AVMDAOs.Instance().fAVMNodeDAO, AVMDAOs.Instance().fAVMStoreDAO); + Pair result = dir.lookupChild(cPath, name, true); + if (result == null) + { + throw new AVMNotFoundException("Path " + parentPath + "/" +name + " not found."); + } + AVMNode child = result.getFirst(); + if (!fAVMRepository.can(null, child, PermissionService.WRITE, cPath.getDirectlyContained())) + { + throw new AccessDeniedException("Not allowed to update node: " + parentPath + "/" +name ); + } + + dir.removeChild(lPath, name); + dir.link(lPath, name, toLink); + } /** * Revert a head path to a given version. This works by cloning diff --git a/source/java/org/alfresco/repo/avm/AVMSyncServiceImpl.java b/source/java/org/alfresco/repo/avm/AVMSyncServiceImpl.java index c2b11c5eaa..dcd397e055 100644 --- a/source/java/org/alfresco/repo/avm/AVMSyncServiceImpl.java +++ b/source/java/org/alfresco/repo/avm/AVMSyncServiceImpl.java @@ -558,18 +558,36 @@ public class AVMSyncServiceImpl implements AVMSyncService fAVMService.removeNode(parentPath, name); return; } - mkdirs(parentPath, AVMNodeConverter.SplitBase(toLink.getPath())[0]); - if (removeFirst) - { - fAVMService.removeNode(parentPath, name); - } + if (toLink.isLayeredDirectory() && !toLink.isPrimary()) { + // Combining the remove and add into a single update API causes all sorts of potential security issues + if (removeFirst) + { + fAVMService.removeNode(parentPath, name); + } recursiveCopy(parentPath, name, toLink, excluder); return; } - - fAVMService.link(parentPath, name, toLink); + + if (removeFirst) + { + if (toLink.isDirectory()) + { + // Combining the remove and add into a single update API causes all sorts of potential security issues + fAVMService.removeNode(parentPath, name); + fAVMService.link(parentPath, name, toLink); + } + else + { + // this API only requires write access to the file + fAVMService.updateLink(parentPath, name, toLink); + } + } + else + { + fAVMService.link(parentPath, name, toLink); + } String newPath = AVMNodeConverter.ExtendAVMPath(parentPath, name); diff --git a/source/java/org/alfresco/repo/avm/MultiTAVMService.java b/source/java/org/alfresco/repo/avm/MultiTAVMService.java index 8851f80b6a..c6aa88af7e 100644 --- a/source/java/org/alfresco/repo/avm/MultiTAVMService.java +++ b/source/java/org/alfresco/repo/avm/MultiTAVMService.java @@ -177,6 +177,11 @@ public class MultiTAVMService implements AVMService fService.deleteStoreProperty(getTenantStoreName(storeName), name); } + public void updateLink(String parentPath, String name, AVMNodeDescriptor toLink) + { + fService.updateLink(getTenantPath(parentPath), name, toLink); + } + /* (non-Javadoc) * @see org.alfresco.service.cmr.avm.AVMService#forceCopy(java.lang.String) */ diff --git a/source/java/org/alfresco/repo/domain/hibernate/TransactionImpl.java b/source/java/org/alfresco/repo/domain/hibernate/TransactionImpl.java index a0c83f2d6a..4a1a78d570 100644 --- a/source/java/org/alfresco/repo/domain/hibernate/TransactionImpl.java +++ b/source/java/org/alfresco/repo/domain/hibernate/TransactionImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2007 Alfresco Software Limited. + * Copyright (C) 2005-2009 Alfresco Software Limited. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -29,6 +29,7 @@ import java.util.Date; import org.alfresco.repo.domain.Server; import org.alfresco.repo.domain.Transaction; +import org.alfresco.util.ISO8601DateFormat; /** * Bean containing all the persistence data representing a Transaction. @@ -59,7 +60,7 @@ public class TransactionImpl extends LifecycleAdapter implements Transaction, Se StringBuilder sb = new StringBuilder(50); sb.append("Transaction") .append("[id=").append(id) - .append(", txnTimeMs=").append(commitTimeMs == null ? "---" : new Date(commitTimeMs)) + .append(", txnTimeMs=").append(commitTimeMs == null ? "---" : ISO8601DateFormat.format(new Date(commitTimeMs))) .append(", changeTxnId=").append(changeTxnId) .append("]"); return sb.toString(); diff --git a/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java b/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java index 785ccde60b..332ea30736 100644 --- a/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java +++ b/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java @@ -242,6 +242,10 @@ public class FileFolderServiceImpl implements FileFolderService } } + public boolean exists(NodeRef nodeRef) + { + return nodeService.exists(nodeRef); + } public FileFolderServiceType getType(QName typeQName) { @@ -753,6 +757,12 @@ public class FileFolderServiceImpl implements FileFolderService public void delete(NodeRef nodeRef) { nodeService.deleteNode(nodeRef); + // Done + if (logger.isDebugEnabled()) + { + logger.debug("Deleted: \n" + + " node: " + nodeRef); + } } public FileInfo makeFolders(NodeRef parentNodeRef, List pathElements, QName folderTypeQName) diff --git a/source/java/org/alfresco/repo/node/MLPropertyInterceptor.java b/source/java/org/alfresco/repo/node/MLPropertyInterceptor.java index e86b1efe8b..39d2700080 100644 --- a/source/java/org/alfresco/repo/node/MLPropertyInterceptor.java +++ b/source/java/org/alfresco/repo/node/MLPropertyInterceptor.java @@ -317,14 +317,18 @@ public class MLPropertyInterceptor implements MethodInterceptor */ private NodeRef getPivotNodeRef(NodeRef nodeRef) { - if (!nodeRef.getStoreRef().getProtocol().equals(StoreRef.PROTOCOL_AVM) && nodeService.hasAspect(nodeRef, ContentModel.ASPECT_MULTILINGUAL_EMPTY_TRANSLATION)) - { - return multilingualContentService.getPivotTranslation(nodeRef); - } - else - { - return null; - } + if (nodeRef == null) + { + throw new IllegalArgumentException("NodeRef may not be null for calls to NodeService. Check client code."); + } + if (!nodeRef.getStoreRef().getProtocol().equals(StoreRef.PROTOCOL_AVM) && nodeService.hasAspect(nodeRef, ContentModel.ASPECT_MULTILINGUAL_EMPTY_TRANSLATION)) + { + return multilingualContentService.getPivotTranslation(nodeRef); + } + else + { + return null; + } } /** diff --git a/source/java/org/alfresco/repo/node/db/hibernate/HibernateNodeDaoServiceImpl.java b/source/java/org/alfresco/repo/node/db/hibernate/HibernateNodeDaoServiceImpl.java index 6645160d5b..ce0c82a02a 100644 --- a/source/java/org/alfresco/repo/node/db/hibernate/HibernateNodeDaoServiceImpl.java +++ b/source/java/org/alfresco/repo/node/db/hibernate/HibernateNodeDaoServiceImpl.java @@ -68,6 +68,7 @@ import org.alfresco.repo.domain.hibernate.DirtySessionMethodInterceptor; import org.alfresco.repo.domain.hibernate.NodeAssocImpl; import org.alfresco.repo.domain.hibernate.NodeImpl; import org.alfresco.repo.domain.hibernate.ServerImpl; +import org.alfresco.repo.domain.hibernate.SessionSizeResourceManager; import org.alfresco.repo.domain.hibernate.StoreImpl; import org.alfresco.repo.domain.hibernate.TransactionImpl; import org.alfresco.repo.node.db.NodeDaoService; @@ -78,8 +79,11 @@ import org.alfresco.repo.security.permissions.SimpleAccessControlListProperties; import org.alfresco.repo.security.permissions.impl.AclChange; import org.alfresco.repo.security.permissions.impl.AclDaoComponent; import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.repo.transaction.RetryingTransactionHelper; import org.alfresco.repo.transaction.TransactionAwareSingleton; +import org.alfresco.repo.transaction.TransactionListenerAdapter; import org.alfresco.repo.transaction.TransactionalDao; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.cmr.dictionary.AssociationDefinition; import org.alfresco.service.cmr.dictionary.ChildAssociationDefinition; import org.alfresco.service.cmr.dictionary.DataTypeDefinition; @@ -108,6 +112,7 @@ import org.alfresco.util.Pair; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.hibernate.Criteria; +import org.hibernate.LockMode; import org.hibernate.ObjectNotFoundException; import org.hibernate.Query; import org.hibernate.ScrollMode; @@ -179,6 +184,8 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements private AclDaoComponent aclDaoComponent; private LocaleDAO localeDAO; private DictionaryService dictionaryService; + private boolean enableTimestampPropagation; + private RetryingTransactionHelper auditableTransactionHelper; /** A cache mapping StoreRef and NodeRef instances to the entity IDs (primary key) */ private SimpleCache storeAndNodeIdCache; /** A cache for more performant lookups of the parent associations */ @@ -211,6 +218,7 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements } changeTxnIdSet = new HashSet(0); + enableTimestampPropagation = true; } /** @@ -272,6 +280,24 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements this.dictionaryService = dictionaryService; } + /** + * Enable/disable propagation of timestamps from child to parent nodes.
+ * Note: This only has an effect on child associations that use the propagateTimestamps element. + */ + public void setEnableTimestampPropagation(boolean enableTimestampPropagation) + { + this.enableTimestampPropagation = enableTimestampPropagation; + } + + /** + * Set the component to start new transactions when setting auditable properties (timestamps) + * in the post-transaction phase. + */ + public void setAuditableTransactionHelper(RetryingTransactionHelper auditableTransactionHelper) + { + this.auditableTransactionHelper = auditableTransactionHelper; + } + /** * Ste the transaction-aware cache to store Store and Root Node IDs by Store Reference * @@ -550,6 +576,19 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements return node; } + /** + * Fetch the node. If the ID is invalid, null is returned. + * + * @param nodeId the node's ID + * @return the node + * @throws ObjectNotFoundException if the ID doesn't refer to a node. + */ + private Node getNodeOrNull(Long nodeId) + { + Node node = (Node) getHibernateTemplate().get(NodeImpl.class, nodeId); + return node; + } + /** * Fetch the child assoc. If the ID is invalid, we assume that the state of the current session * is invalid i.e. the data is stale @@ -766,11 +805,11 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements } } - private static final String UNKOWN_USER = "unkown"; + private static final String UNKNOWN_USER = "unknown"; private String getCurrentUser() { String user = AuthenticationUtil.getFullyAuthenticatedUser(); - return (user == null) ? UNKOWN_USER : user; + return (user == null) ? UNKNOWN_USER : user; } private void recordNodeCreate(Node node) @@ -803,6 +842,8 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements } auditableProperties.setAuditValues(currentUser, currentDate, false); } + // Propagate timestamps + propagateTimestamps(node); } private void recordNodeDelete(Node node) @@ -822,6 +863,159 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements auditableProperties.setAuditValues(currentUser, currentDate, false); } } + + /** + * Ensures that timestamps are propagated for the cm:auditable aspect as required + * and defined by the model. + */ + private void propagateTimestamps(Node node) + { + // Shortcut + if (!enableTimestampPropagation) + { + return; + } + Long nodeId = node.getId(); + Collection parentAssocs = getParentAssocsInternal(nodeId); + for (ChildAssoc parentAssoc : parentAssocs) + { + propagateTimestamps(parentAssoc); + } + } + + /** + * Sets the timestamps for nodes set during the transaction. + *

+ * The implementation attempts to propagate the timestamps in the same transaction, but during periods of high + * concurrent modification to children of a particular parent node, the contention-resolution at the database + * can lead to delays in the processes. When this occurs, the process is pushed to after the transaction for an + * arbitrary period of time, after which the server will again attempt to do the work in the transaction. + * + * @author Derek Hulley + */ + private class TimestampPropagator extends TransactionListenerAdapter implements RetryingTransactionCallback + { + private final Set nodeIds; + + private TimestampPropagator() + { + this.nodeIds = new HashSet(23); + } + + public void addNode(Long nodeId) + { + nodeIds.add(nodeId); + } + + @Override + public void afterCommit() + { + if (nodeIds.size() == 0) + { + return; + } + // Execute using the explicit transaction attributes + try + { + auditableTransactionHelper.doInTransaction(this, false, true); + } + catch (Throwable e) + { + logger.info("Failed to update auditable properties for nodes: " + nodeIds); + } + } + + public static final String QUERY_UPDATE_AUDITABLE_MODIFIED = "node.UpdateAuditableModified"; + public Integer execute() throws Throwable + { + long now = System.currentTimeMillis(); + return executeImpl(now, true); + } + + private Integer executeImpl(long now, boolean isPostTransaction) throws Throwable + { + if (logger.isDebugEnabled()) + { + logger.debug("Updating timestamps for nodes: " + nodeIds); + } + Session session = getSession(); + final Date modifiedDate = new Date(now); + final String modifier = getCurrentUser(); + int count = 0; + for (final Long nodeId : nodeIds) + { + Node node = getNodeOrNull(nodeId); + if (node == null) + { + continue; + } + AuditableProperties auditableProperties = node.getAuditableProperties(); + if (auditableProperties == null) + { + // Don't bother setting anything if there are no values + continue; + } + // Only set the value if our modified date is later + Date currentModifiedDate = (Date) auditableProperties.getAuditableProperty(ContentModel.PROP_MODIFIED); + if (currentModifiedDate != null && currentModifiedDate.compareTo(modifiedDate) >= 0) + { + // The value on the node is greater + continue; + } + // Lock it + session.lock(node, LockMode.UPGRADE_NOWAIT); // Might fail immediately, but that is better than waiting + auditableProperties.setAuditValues(modifier, modifiedDate, false); + count++; + if (count % 1000 == 0) + { + DirtySessionMethodInterceptor.flushSession(session); + SessionSizeResourceManager.clear(session); + } + } + return new Integer(count); + } + } + + private static final String RESOURCE_KEY_TIMESTAMP_PROPAGATOR = "hibernate.timestamp.propagator"; + /** + * Ensures that the timestamps are propogated to the parent node of the association, but only + * if the association requires it. + */ + private void propagateTimestamps(ChildAssoc parentAssoc) + { + // Shortcut + if (!enableTimestampPropagation) + { + return; + } + QName assocTypeQName = parentAssoc.getTypeQName(qnameDAO); + AssociationDefinition assocDef = dictionaryService.getAssociation(assocTypeQName); + if (assocDef == null) + { + // Not found, so just ignore + return; + } + else if (!assocDef.isChild()) + { + // Unexpected, but not our immediate concern + return; + } + ChildAssociationDefinition childAssocDef = (ChildAssociationDefinition) assocDef; + // Do we send timestamps up? + if (!childAssocDef.getPropagateTimestamps()) + { + return; + } + // We have to update the parent + TimestampPropagator propagator = + (TimestampPropagator) AlfrescoTransactionSupport.getResource(RESOURCE_KEY_TIMESTAMP_PROPAGATOR); + if (propagator == null) + { + propagator = new TimestampPropagator(); + AlfrescoTransactionSupport.bindListener(propagator); + } + propagator.addNode(parentAssoc.getParent().getId()); + } public Pair newNode(StoreRef storeRef, String uuid, QName nodeTypeQName) throws InvalidTypeException { @@ -1365,6 +1559,10 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements public void deleteNode(Long nodeId) { Node node = getNodeNotNull(nodeId); + + // Propagate timestamps + propagateTimestamps(node); + Set deletedChildAssocIds = new HashSet(10); deleteNodeInternal(node, false, deletedChildAssocIds); @@ -4159,7 +4357,10 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements { if (collapsedValue != null && !(collapsedValue instanceof Collection)) { - collapsedValue = (Serializable) Collections.singletonList(collapsedValue); + // Can't use Collections.singletonList: ETHREEOH-1172 + ArrayList collection = new ArrayList(1); + collection.add(collapsedValue); + collapsedValue = collection; } } // Store the value @@ -4251,7 +4452,10 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements // Make sure that multi-valued properties are returned as a collection if (propertyDef != null && propertyDef.isMultiValued() && result != null && !(result instanceof Collection)) { - result = (Serializable) Collections.singletonList(result); + // Can't use Collections.singletonList: ETHREEOH-1172 + ArrayList collection = new ArrayList(1); + collection.add(result); + result = collection; } // Done return result; diff --git a/source/java/org/alfresco/repo/node/index/IndexTransactionTracker.java b/source/java/org/alfresco/repo/node/index/IndexTransactionTracker.java index 7f3d129b95..863c0f9eb1 100644 --- a/source/java/org/alfresco/repo/node/index/IndexTransactionTracker.java +++ b/source/java/org/alfresco/repo/node/index/IndexTransactionTracker.java @@ -36,6 +36,7 @@ import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.repo.domain.Transaction; import org.alfresco.repo.transaction.RetryingTransactionHelper; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.util.ISO8601DateFormat; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -195,24 +196,12 @@ public class IndexTransactionTracker extends AbstractReindexComponent } }; - public void reindexFromTxn(long txnId) + public void resetFromTxn(long txnId) { - try - { - logger.info("reindexFromTxn: "+txnId); + logger.info("resetFromTxn: "+txnId); - this.fromTxnId = txnId; - this.started = false; - - reindex(); - - this.started = false; - } - finally - { - this.fromTxnId = 0L; - - } + this.fromTxnId = txnId; + this.started = false; // this will cause index tracker to break out (so that it can be re-started) } @Override @@ -225,7 +214,7 @@ public class IndexTransactionTracker extends AbstractReindexComponent RetryingTransactionHelper retryingTransactionHelper = transactionService.getRetryingTransactionHelper(); - if ((!started) || (this.fromTxnId != 0L)) + if (!started) { // Disable in-transaction indexing if (disableInTransactionIndexing && nodeIndexer != null) @@ -240,6 +229,8 @@ public class IndexTransactionTracker extends AbstractReindexComponent if (this.fromTxnId != 0L) { + logger.info("reindexImpl: start fromTxnId: "+fromTxnId+" "+this); + Long fromTxnCommitTime = getTxnCommitTime(this.fromTxnId); if (fromTxnCommitTime == null) @@ -254,12 +245,10 @@ public class IndexTransactionTracker extends AbstractReindexComponent fromTimeInclusive = retryingTransactionHelper.doInTransaction(getStartingCommitTimeWork, true, true); } + fromTxnId = 0L; started = true; - if (logger.isDebugEnabled()) - { - logger.debug("reindexImpl: fromTimeInclusive: "+fromTimeInclusive+" "+this); - } + logger.info("reindexImpl: start fromTimeInclusive: "+ISO8601DateFormat.format(new Date(fromTimeInclusive))+" "+this); } while (true) @@ -274,9 +263,9 @@ public class IndexTransactionTracker extends AbstractReindexComponent // Wait for the asynchronous reindexing to complete waitForAsynchronousReindexing(); - if (logger.isDebugEnabled()) + if (logger.isTraceEnabled()) { - logger.debug("reindexImpl: completed: " + this); + logger.trace("reindexImpl: completed: "+this); } statusMsg = NO_REINDEX; @@ -383,9 +372,9 @@ public class IndexTransactionTracker extends AbstractReindexComponent previousTxnIds.add(txn.getId()); } - if (isShuttingDown()) + if (isShuttingDown() || (! started)) { - // break out if the VM is shutting down + // break out if the VM is shutting down or tracker has been reset (ie. !started) return false; } else @@ -644,8 +633,9 @@ found: } } - if (isShuttingDown()) + if (isShuttingDown() || (! started)) { + // break out if the VM is shutting down or tracker has been reset (ie. !started) break; } // Flush the reindex buffer, if it is full or if we are on the last transaction and there are no more diff --git a/source/java/org/alfresco/service/cmr/avm/AVMService.java b/source/java/org/alfresco/service/cmr/avm/AVMService.java index 4dbc03269c..dcee00138c 100644 --- a/source/java/org/alfresco/service/cmr/avm/AVMService.java +++ b/source/java/org/alfresco/service/cmr/avm/AVMService.java @@ -1172,6 +1172,20 @@ public interface AVMService */ public void link(String parentPath, String name, AVMNodeDescriptor toLink); + /** + * Low-level internal function:   replace a node + * in a parent directly. Caution: this is not something + * one ordinary applications should do, but it is used + * internally by the AVMSyncService.update() method. + * This function may disappear from the public interface. + * + * @param parentPath The path to the parent directory. + * @param name The name to give the node. + * @param toLink A descriptor for the node to insert. + * @throws AVMNotFoundException + */ + public void updateLink(String parentPath, String name, AVMNodeDescriptor toLink); + /** * Low-level internal function:   Force a copy on write * write event on the given node. This function is not usually diff --git a/source/java/org/alfresco/service/cmr/model/FileFolderService.java b/source/java/org/alfresco/service/cmr/model/FileFolderService.java index ce2e575f1d..c7936af746 100644 --- a/source/java/org/alfresco/service/cmr/model/FileFolderService.java +++ b/source/java/org/alfresco/service/cmr/model/FileFolderService.java @@ -228,8 +228,8 @@ public interface FileFolderService /** * Resolve a file or folder name path from a given root node down to the final node. * - * @param rootNodeRef the start of the path given, i.e. the '/' in '/A/B/C' for example - * @param pathElements a list of names in the path + * @param rootNodeRef the start point node - a cm:folder type or subtype, e.g. the Company Home's nodeRef + * @param pathElements a list of names in the path. Do not include the referenced rootNodeRef's path element. * @return Returns the info of the file or folder * @throws FileNotFoundException if no file or folder exists along the path */ @@ -266,6 +266,14 @@ public interface FileFolderService public ContentWriter getWriter(NodeRef nodeRef); + /** + * Check the validity of a node reference + * + * @return returns true if the NodeRef is valid + */ + @Auditable(key = Auditable.Key.ARG_0, parameters = {"nodeRef"}) + public boolean exists(NodeRef nodeRef); + /** * Checks the type for whether it is a recognised file or folder type or is invalid for the FileFolderService. * diff --git a/source/java/org/alfresco/wcm/asset/AssetServiceImplTest.java b/source/java/org/alfresco/wcm/asset/AssetServiceImplTest.java index cd19dbc472..04ae7d1f3b 100644 --- a/source/java/org/alfresco/wcm/asset/AssetServiceImplTest.java +++ b/source/java/org/alfresco/wcm/asset/AssetServiceImplTest.java @@ -34,6 +34,7 @@ import java.util.Map; import org.alfresco.model.ContentModel; import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.permissions.AccessDeniedException; import org.alfresco.service.cmr.avm.AVMNotFoundException; import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentWriter; @@ -58,6 +59,11 @@ public class AssetServiceImplTest extends AbstractWCMServiceImplTest private SandboxService sbService; private AssetService assetService; + // test data + private static final String PREFIX = "created-by-admin-"; + private static final String FILE = "This is file1 - admin"; + + @Override protected void setUp() throws Exception { @@ -377,6 +383,295 @@ public class AssetServiceImplTest extends AbstractWCMServiceImplTest } } + /** + * Test CRUD - create, retrieve (get, list), update and delete for each role + */ + public void testCRUDforRoles() throws IOException, InterruptedException + { + // create web project (also creates staging sandbox and admin's author sandbox) + WebProjectInfo wpInfo = wpService.createWebProject(TEST_WEBPROJ_DNS+"-crudroles", TEST_WEBPROJ_NAME+"-crudroles", TEST_WEBPROJ_TITLE, TEST_WEBPROJ_DESCRIPTION, TEST_WEBPROJ_DEFAULT_WEBAPP, TEST_WEBPROJ_DONT_USE_AS_TEMPLATE, null); + + String wpStoreId = wpInfo.getStoreId(); + String defaultWebApp = wpInfo.getDefaultWebApp(); + + // get admin sandbox + SandboxInfo sbInfo = sbService.getAuthorSandbox(wpStoreId); + String sbStoreId = sbInfo.getSandboxId(); + + // create some existing folders and files + String[] users = new String[]{USER_ONE, USER_TWO, USER_THREE, USER_FOUR}; + for (String user : users) + { + assetService.createFolderWebApp(sbStoreId, defaultWebApp, "/", PREFIX+user); + + // create file (and add content) + ContentWriter writer = assetService.createFileWebApp(sbStoreId, defaultWebApp, "/"+PREFIX+user, "fileA"); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent(FILE); + } + + sbService.submitWebApp(sbStoreId, defaultWebApp, "some existing folders and files", null); + + Thread.sleep(SUBMIT_DELAY); + + runCRUDforRoles(USER_ONE, WCMUtil.ROLE_CONTENT_MANAGER, wpStoreId, defaultWebApp, true, true, true); + + // TODO - pending ETHREEOH-1314 (see below) if updating folder properties + runCRUDforRoles(USER_TWO, WCMUtil.ROLE_CONTENT_PUBLISHER, wpStoreId, defaultWebApp, true, true, false); + runCRUDforRoles(USER_THREE, WCMUtil.ROLE_CONTENT_REVIEWER, wpStoreId, defaultWebApp, false, true, false); + + runCRUDforRoles(USER_FOUR, WCMUtil.ROLE_CONTENT_CONTRIBUTOR, wpStoreId, defaultWebApp, true, false, false); + } + + private void runCRUDforRoles(String user, String role, String wpStoreId, String defaultWebApp, boolean canCreate, boolean canUpdateExisting, boolean canDeleteExisting) throws IOException, InterruptedException + { + // switch to user - content manager + AuthenticationUtil.setFullyAuthenticatedUser(USER_ADMIN); + + // invite web user and auto-create their (author) sandbox + wpService.inviteWebUser(wpStoreId, user, role, true); + + // switch to user + AuthenticationUtil.setFullyAuthenticatedUser(user); + + // get user's author sandbox + SandboxInfo sbInfo = sbService.getAuthorSandbox(wpStoreId); + String sbStoreId = sbInfo.getSandboxId(); + String path = sbInfo.getSandboxRootPath() + "/" + defaultWebApp; // for checks only + + if (canCreate) + { + // create folder + assetService.createFolderWebApp(sbStoreId, defaultWebApp, "/", user); + + // create file (and add content) + final String MYFILE1 = "This is myFile1 - "+user; + ContentWriter writer = assetService.createFileWebApp(sbStoreId, defaultWebApp, "/"+user, "fileA"); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent(MYFILE1); + + // list assets + assertEquals(1, assetService.listAssetsWebApp(sbStoreId, defaultWebApp, "/"+user, false).size()); + + // get assets + AssetInfo myFolder1Asset = assetService.getAssetWebApp(sbStoreId, defaultWebApp, "/"+user); + checkAssetInfo(myFolder1Asset, user, path+"/"+user, user, false, true, false, false, null); + + AssetInfo myFile1Asset = assetService.getAssetWebApp(sbStoreId, defaultWebApp, "/"+user+"/fileA"); + checkAssetInfo(myFile1Asset, "fileA", path+"/"+user+"/fileA", user, true, false, false, true, user); + + // get content + + ContentReader reader = assetService.getContentReader(myFile1Asset); + InputStream in = reader.getContentInputStream(); + byte[] buff = new byte[1024]; + in.read(buff); + in.close(); + assertEquals(MYFILE1, new String(buff, 0, MYFILE1.length())); // assumes 1byte=1char + + // update content + + final String MYFILE1_MODIFIED = "This is myFile1 ... modified"; + writer = assetService.getContentWriter(myFile1Asset); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent(MYFILE1_MODIFIED); + + // get updated content + + reader = assetService.getContentReader(myFile1Asset); + in = reader.getContentInputStream(); + buff = new byte[1024]; + in.read(buff); + in.close(); + assertEquals(MYFILE1_MODIFIED, new String(buff, 0, MYFILE1_MODIFIED.length())); // assumes 1byte=1char + + // update folder properties - eg. title and description + + Map newProps = new HashMap(2); + newProps.put(ContentModel.PROP_TITLE, "folder title"); + newProps.put(ContentModel.PROP_DESCRIPTION, "folder description"); + + assetService.updateAssetProperties(myFolder1Asset, newProps); + Map props = assetService.getAssetProperties(myFolder1Asset); + assertEquals("folder title", props.get(ContentModel.PROP_TITLE)); + assertEquals("folder description", props.get(ContentModel.PROP_DESCRIPTION)); + + // Delete created file and folder + assetService.deleteAsset(myFile1Asset); + assetService.deleteAsset(myFolder1Asset); + } + else + { + try + { + // try to create folder (-ve test) + assetService.createFolderWebApp(sbStoreId, defaultWebApp, "/", user); + fail("User "+user+" with role "+role+" should not be able to create folder"); + } + catch (AccessDeniedException ade) + { + // expected + } + + try + { + // try to create file (-ve test) + assetService.createFileWebApp(sbStoreId, defaultWebApp, "/", "file-"+user); + fail("User "+user+" with role "+role+" should not be able to create file"); + } + catch (AccessDeniedException ade) + { + // expected + } + } + + // list existing assets + assertEquals(1, assetService.listAssetsWebApp(sbStoreId, defaultWebApp, "/"+PREFIX+user, false).size()); + + // get existing assets + AssetInfo existingFolder1Asset = assetService.getAssetWebApp(sbStoreId, defaultWebApp, "/"+PREFIX+user); + checkAssetInfo(existingFolder1Asset, PREFIX+user, path+"/"+PREFIX+user, USER_ADMIN, false, true, false, false, null); + + AssetInfo existingFile1Asset = assetService.getAssetWebApp(sbStoreId, defaultWebApp, "/"+PREFIX+user+"/fileA"); + checkAssetInfo(existingFile1Asset, "fileA", path+"/"+PREFIX+user+"/fileA", USER_ADMIN, true, false, false, false, null); + + // get existing content + + ContentReader reader = assetService.getContentReader(existingFile1Asset); + InputStream in = reader.getContentInputStream(); + byte[] buff = new byte[1024]; + in.read(buff); + in.close(); + assertEquals(FILE, new String(buff, 0, FILE.length())); // assumes 1byte=1char + + if (canUpdateExisting) + { + // update content + + final String MYFILE1_MODIFIED = "This is myFile1 ... modified"; + ContentWriter writer = assetService.getContentWriter(existingFile1Asset); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent(MYFILE1_MODIFIED); + + // get updated content + + reader = assetService.getContentReader(existingFile1Asset); + in = reader.getContentInputStream(); + buff = new byte[1024]; + in.read(buff); + in.close(); + assertEquals(MYFILE1_MODIFIED, new String(buff, 0, MYFILE1_MODIFIED.length())); // assumes 1byte=1char + + // update file properties - eg. title and description + + Map newProps = new HashMap(2); + newProps.put(ContentModel.PROP_TITLE, "file title"); + newProps.put(ContentModel.PROP_DESCRIPTION, "file description"); + + assetService.updateAssetProperties(existingFile1Asset, newProps); + Map props = assetService.getAssetProperties(existingFile1Asset); + assertEquals("file title", props.get(ContentModel.PROP_TITLE)); + assertEquals("file description", props.get(ContentModel.PROP_DESCRIPTION)); + + /* TODO - pending ETHREEOH-1314 - fails for content contributor / content publisher during submit if updating folder properties + + // update folder properties - eg. title and description + + newProps = new HashMap(2); + newProps.put(ContentModel.PROP_TITLE, "folder title"); + newProps.put(ContentModel.PROP_DESCRIPTION, "folder description"); + + assetService.updateAssetProperties(existingFolder1Asset, newProps); + props = assetService.getAssetProperties(existingFolder1Asset); + assertEquals("folder title", props.get(ContentModel.PROP_TITLE)); + assertEquals("folder description", props.get(ContentModel.PROP_DESCRIPTION)); + */ + } + else + { + try + { + // try to update file (-ve test) + assetService.getContentWriter(existingFile1Asset); + fail("User "+user+" with role "+role+" should not be able to update existing file"); + } + catch (AccessDeniedException ade) + { + // expected + } + + try + { + // try to update file properties (-ve test) + Map newProps = new HashMap(2); + newProps.put(ContentModel.PROP_TITLE, "file title"); + newProps.put(ContentModel.PROP_DESCRIPTION, "file description"); + + assetService.updateAssetProperties(existingFile1Asset, newProps); + fail("User "+user+" with role "+role+" should not be able to update existing file properties"); + } + catch (AccessDeniedException ade) + { + // expected + } + + try + { + // try to update folder properties (-ve test) + Map newProps = new HashMap(2); + newProps.put(ContentModel.PROP_TITLE, "folder title"); + newProps.put(ContentModel.PROP_DESCRIPTION, "folder description"); + + assetService.updateAssetProperties(existingFolder1Asset, newProps); + fail("User "+user+" with role "+role+" should not be able to update existing folder properties"); + } + catch (AccessDeniedException ade) + { + // expected + } + } + + if (canDeleteExisting) + { + // Delete existing file and folder + assetService.deleteAsset(existingFile1Asset); + assetService.deleteAsset(existingFolder1Asset); + } + else + { + try + { + // try to delete file (-ve test) + assetService.deleteAsset(existingFile1Asset); + fail("User "+user+" with role "+role+" should not be able to delete existing file"); + } + catch (AVMNotFoundException nfe) + { + // expected + } + + try + { + // try to delete folder (-ve test) + assetService.deleteAsset(existingFolder1Asset); + fail("User "+user+" with role "+role+" should not be able to delete existing folder"); + } + catch (AVMNotFoundException ade) + { + // expected + } + } + + // submit the changes + sbService.submitWebApp(sbStoreId, defaultWebApp, "some updates by "+user, null); + + Thread.sleep(SUBMIT_DELAY); + } + public void testRenameFile() { // create web project (also creates staging sandbox and admin's author sandbox) diff --git a/source/java/org/alfresco/wcm/sandbox/SandboxServiceImplTest.java b/source/java/org/alfresco/wcm/sandbox/SandboxServiceImplTest.java index 4a8c92dce5..8f30874d2d 100644 --- a/source/java/org/alfresco/wcm/sandbox/SandboxServiceImplTest.java +++ b/source/java/org/alfresco/wcm/sandbox/SandboxServiceImplTest.java @@ -1067,10 +1067,7 @@ public class SandboxServiceImplTest extends AbstractWCMServiceImplTest // Invite web users wpService.inviteWebUser(wpStoreId, USER_ONE, WCMUtil.ROLE_CONTENT_CONTRIBUTOR, true); - - // TODO - pending merge of ETWOTWO-1109 fix - //wpService.inviteWebUser(wpStoreId, USER_TWO, WCMUtil.ROLE_CONTENT_PUBLISHER, true); - wpService.inviteWebUser(wpStoreId, USER_TWO, WCMUtil.ROLE_CONTENT_MANAGER, true); + wpService.inviteWebUser(wpStoreId, USER_TWO, WCMUtil.ROLE_CONTENT_PUBLISHER, true); // Switch to USER_ONE AuthenticationUtil.setFullyAuthenticatedUser(USER_ONE);