diff --git a/config/alfresco/ibatis/org.hibernate.dialect.Dialect/node-common-SqlMap.xml b/config/alfresco/ibatis/org.hibernate.dialect.Dialect/node-common-SqlMap.xml index b9c0dfdc56..8010c9b931 100644 --- a/config/alfresco/ibatis/org.hibernate.dialect.Dialect/node-common-SqlMap.xml +++ b/config/alfresco/ibatis/org.hibernate.dialect.Dialect/node-common-SqlMap.xml @@ -423,13 +423,12 @@ id = #{id} - + update alf_child_assoc set child_node_name_crc = #{childNodeNameCrc}, child_node_name = #{childNodeName} where - child_node_id = #{childNode.id} and - child_node_name_crc > 0 + id = #{id} diff --git a/config/alfresco/messages/lock-service.properties b/config/alfresco/messages/lock-service.properties index 7e4eb9707e..3c51dd2c52 100644 --- a/config/alfresco/messages/lock-service.properties +++ b/config/alfresco/messages/lock-service.properties @@ -3,4 +3,5 @@ lock_service.insufficent_privileges=You have insufficient privileges to release the lock on the node (id: {0}). The node is locked by another user. lock_service.node_locked=The node (id: {0}) could not be locked since it is already locked by another user. lock_service.no_op=Cannot perform operation since the node (id:{0}) is locked. -lock_service.no_op2=Cannot perform operation {0} since the node (id:{1}) is locked. \ No newline at end of file +lock_service.no_op2=Cannot perform operation {0} since the node (id:{1}) is locked. +lock_service.unlock_checkedout=The node (id: {0}) could not be unlocked since it is checked out. \ No newline at end of file diff --git a/config/alfresco/subsystems/fileServers/default/network-protocol-context.xml b/config/alfresco/subsystems/fileServers/default/network-protocol-context.xml index 968bc9ea7b..369516a8d5 100644 --- a/config/alfresco/subsystems/fileServers/default/network-protocol-context.xml +++ b/config/alfresco/subsystems/fileServers/default/network-protocol-context.xml @@ -224,9 +224,9 @@ true - + - ^Word Work File L_.*\.tmp? + ^.*\.tmp 20000 MEDIUM false diff --git a/source/java/org/alfresco/filesys/repo/ContentDiskDriverTest.java b/source/java/org/alfresco/filesys/repo/ContentDiskDriverTest.java index 53ab4ca952..debb2d4874 100644 --- a/source/java/org/alfresco/filesys/repo/ContentDiskDriverTest.java +++ b/source/java/org/alfresco/filesys/repo/ContentDiskDriverTest.java @@ -24,6 +24,7 @@ import java.io.InputStream; import java.io.Serializable; import java.net.InetAddress; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -101,6 +102,7 @@ import org.alfresco.util.FileFilterMode.Client; import org.alfresco.util.Pair; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.hibernate.type.VersionType; import org.springframework.context.ApplicationContext; import org.springframework.core.io.ClassPathResource; @@ -3332,7 +3334,9 @@ public class ContentDiskDriverTest extends TestCase testContext.testCreatedDate = nodeService.getProperty(testContext.testNodeRef, ContentModel.PROP_CREATED); - nodeService.addAspect(testContext.testNodeRef, ContentModel.ASPECT_VERSIONABLE, null); + nodeService.addAspect(testContext.testNodeRef, ContentModel.ASPECT_VERSIONABLE, Collections + . singletonMap(ContentModel.PROP_VERSION_TYPE, + org.alfresco.service.cmr.version.VersionType.MINOR)); return null; } @@ -6405,6 +6409,210 @@ public class ContentDiskDriverTest extends TestCase logger.debug("end testGedit"); } // testGedit + + + /** + * Windows7 Explorer update + * 0) Existing file mark.jpg + * a) Create new file (~ark.tmp) + * b) Existing file rename out of the way. (mark.jpg~RF5bb356.TMP) + * c) New file rename into place. (~ark.tmp - mark.jpg) + * d) Old file opened attributes only + * e) set delete on close + * f) close + */ + public void testWindows7Explorer() throws Exception + { + logger.debug("testWindows7Explorer"); + + final String FILE_NAME = "mark.jpg"; + final String FILE_OLD_TEMP = "mark.jpg~RF5bb356.TMP"; + final String FILE_NEW_TEMP = "~ark.tmp"; + + class TestContext + { + NodeRef testNodeRef; + NetworkFile firstFileHandle; + NetworkFile secondFileHandle; + }; + + final TestContext testContext = new TestContext(); + + final String TEST_DIR = TEST_ROOT_DOS_PATH + "\\testWindows7Explorer"; + + ServerConfiguration scfg = new ServerConfiguration("testServer"); + TestServer testServer = new TestServer("testServer", scfg); + final SrvSession testSession = new TestSrvSession(666, testServer, "test", "remoteName"); + DiskSharedDevice share = getDiskSharedDevice(); + final TreeConnection testConnection = testServer.getTreeConnection(share); + final RetryingTransactionHelper tran = transactionService.getRetryingTransactionHelper(); + + /** + * Clean up just in case garbage is left from a previous run + */ + RetryingTransactionCallback deleteGarbageFileCB = new RetryingTransactionCallback() { + + @Override + public Void execute() throws Throwable + { + driver.deleteFile(testSession, testConnection, TEST_DIR + "\\" + FILE_NAME); + return null; + } + }; + + try + { + tran.doInTransaction(deleteGarbageFileCB); + } + catch (Exception e) + { + // expect to go here + } + + logger.debug("0) create new file"); + RetryingTransactionCallback createFileCB = new RetryingTransactionCallback() { + + @Override + public Void execute() throws Throwable + { + /** + * Create the test directory we are going to use + */ + FileOpenParams createRootDirParams = new FileOpenParams(TEST_ROOT_DOS_PATH, 0, AccessMode.ReadWrite, FileAttribute.NTNormal, 0); + FileOpenParams createDirParams = new FileOpenParams(TEST_DIR, 0, AccessMode.ReadWrite, FileAttribute.NTNormal, 0); + driver.createDirectory(testSession, testConnection, createRootDirParams); + driver.createDirectory(testSession, testConnection, createDirParams); + + /** + * Create the file we are going to test + */ + FileOpenParams createFileParams = new FileOpenParams(TEST_DIR + "\\" + FILE_NAME, 0, AccessMode.ReadWrite, FileAttribute.NTNormal, 0); + testContext.firstFileHandle = driver.createFile(testSession, testConnection, createFileParams); + assertNotNull(testContext.firstFileHandle); + + ClassPathResource fileResource = new ClassPathResource("filesys/ContentDiskDriverTestMark.jpg"); + assertNotNull("unable to find test resource filesys/ContentDiskDriverTestMark.jpg", fileResource); + writeResourceToNetworkFile(fileResource, testContext.firstFileHandle); + driver.closeFile(testSession, testConnection, testContext.firstFileHandle); + NodeRef file1NodeRef = getNodeForPath(testConnection, TEST_DIR + "\\" + FILE_NAME); + testContext.testNodeRef = file1NodeRef; + nodeService.addAspect(file1NodeRef, ContentModel.ASPECT_VERSIONABLE, null); + + return null; + } + }; + tran.doInTransaction(createFileCB, false, true); + + /** + * a) Save the new file + */ + logger.debug("a) save new file"); + RetryingTransactionCallback writeFileCB = new RetryingTransactionCallback() { + + @Override + public Void execute() throws Throwable + { + FileOpenParams createFileParams = new FileOpenParams(TEST_DIR + "\\" + FILE_NEW_TEMP, 0, AccessMode.ReadWrite, FileAttribute.NTNormal, 0); + testContext.secondFileHandle = driver.createFile(testSession, testConnection, createFileParams); + + ClassPathResource fileResource = new ClassPathResource("filesys/ContentDiskDriverTestMark2.jpg"); + assertNotNull("unable to find test resource filesys/ContentDiskDriverTestMark2.jpg", fileResource); + writeResourceToNetworkFile(fileResource, testContext.secondFileHandle); + driver.closeFile(testSession, testConnection, testContext.secondFileHandle); + + return null; + } + }; + tran.doInTransaction(writeFileCB, false, true); + + /** + * b) rename the old file + */ + logger.debug("c) rename old file"); + RetryingTransactionCallback renameOldFileCB = new RetryingTransactionCallback() { + + @Override + public Void execute() throws Throwable + { + driver.renameFile(testSession, testConnection, TEST_DIR + "\\" + FILE_NAME, TEST_DIR + "\\" + FILE_OLD_TEMP); + return null; + } + }; + tran.doInTransaction(renameOldFileCB, false, true); + + /** + * c) Move the new file into place, stuff should get shuffled + */ + logger.debug("d) move new file into place"); + RetryingTransactionCallback moveNewFileCB = new RetryingTransactionCallback() { + + @Override + public Void execute() throws Throwable + { + driver.renameFile(testSession, testConnection, TEST_DIR + "\\" + FILE_NEW_TEMP, TEST_DIR + "\\" + FILE_NAME); + return null; + } + }; + + tran.doInTransaction(moveNewFileCB, false, true); + + /** + * d) Delete the old file + */ + logger.debug("d) delete on close the old file"); + RetryingTransactionCallback deleteOldFileCB = new RetryingTransactionCallback() { + + @Override + public Void execute() throws Throwable + { + FileOpenParams openFileParams = new FileOpenParams(TEST_DIR + "\\" + FILE_OLD_TEMP, 0, AccessMode.NTReadAttributesOnly, FileAttribute.NTNormal, 0); + testContext.secondFileHandle = driver.openFile(testSession, testConnection, openFileParams); + assertNotNull(testContext.secondFileHandle); + + FileInfo info = new FileInfo(); + info.setFileInformationFlags(FileInfo.SetDeleteOnClose); + driver.setFileInformation(testSession, testConnection, TEST_DIR + "\\" + FILE_OLD_TEMP, info); + testContext.secondFileHandle.setDeleteOnClose(true); + + driver.closeFile(testSession, testConnection, testContext.secondFileHandle); + return null; + } + }; + + tran.doInTransaction(deleteOldFileCB, false, true); + + logger.debug("e) validate results"); + + /** + * Now validate everything is correct + */ + RetryingTransactionCallback validateCB = new RetryingTransactionCallback() { + + @Override + public Void execute() throws Throwable + { + NodeRef shuffledNodeRef = getNodeForPath(testConnection, TEST_DIR + "\\" + FILE_NAME); + assertTrue("file has lost versionable aspect", nodeService.hasAspect(shuffledNodeRef, ContentModel.ASPECT_VERSIONABLE)); + assertEquals("node ref has changed", shuffledNodeRef, testContext.testNodeRef); + + + Map props = nodeService.getProperties(shuffledNodeRef); + ContentData data = (ContentData)props.get(ContentModel.PROP_CONTENT); + assertNotNull("data is null", data); + assertEquals("size is wrong", 10407, data.getSize()); + assertEquals("mimeType is wrong", "image/jpeg", data.getMimetype()); + + // TODO - test metadata extraction + //assertEquals(false, props.get(QName.createQName(NamespaceService.EXIF_MODEL_1_0_URI, "flash"))); + + + return null; + } + }; + + tran.doInTransaction(validateCB, true, true); + } // Test Word 7 Explorer Update + /** diff --git a/source/java/org/alfresco/repo/coci/CheckOutCheckInServiceImpl.java b/source/java/org/alfresco/repo/coci/CheckOutCheckInServiceImpl.java index d744157507..fd71a2dd87 100644 --- a/source/java/org/alfresco/repo/coci/CheckOutCheckInServiceImpl.java +++ b/source/java/org/alfresco/repo/coci/CheckOutCheckInServiceImpl.java @@ -547,8 +547,11 @@ public class CheckOutCheckInServiceImpl implements CheckOutCheckInService try { - // Release the lock - lockService.unlock(nodeRef); + if (nodeService.hasAspect(nodeRef, ContentModel.ASPECT_LOCKABLE)) + { + // Release the lock on the original node + lockService.unlock(nodeRef, false, true); + } } catch (UnableToReleaseLockException exception) { @@ -688,8 +691,11 @@ public class CheckOutCheckInServiceImpl implements CheckOutCheckInService behaviourFilter.disableBehaviour(workingCopyNodeRef, ContentModel.ASPECT_WORKING_COPY); try { - // Release the lock on the original node - lockService.unlock(nodeRef); + if (nodeService.hasAspect(nodeRef, ContentModel.ASPECT_LOCKABLE)) + { + // Release the lock on the original node + lockService.unlock(nodeRef, false, true); + } nodeService.removeAspect(nodeRef, ContentModel.ASPECT_CHECKED_OUT); // Delete the working copy diff --git a/source/java/org/alfresco/repo/coci/WorkingCopyAspect.java b/source/java/org/alfresco/repo/coci/WorkingCopyAspect.java index 875d36b53b..49b33f0208 100644 --- a/source/java/org/alfresco/repo/coci/WorkingCopyAspect.java +++ b/source/java/org/alfresco/repo/coci/WorkingCopyAspect.java @@ -133,7 +133,7 @@ public class WorkingCopyAspect implements CopyServicePolicies.OnCopyNodePolicy policyBehaviourFilter.disableBehaviour(checkedOutNodeRef, ContentModel.ASPECT_AUDITABLE); try { - lockService.unlock(checkedOutNodeRef); + lockService.unlock(checkedOutNodeRef, false, true); nodeService.removeAspect(checkedOutNodeRef, ContentModel.ASPECT_CHECKED_OUT); } finally diff --git a/source/java/org/alfresco/repo/domain/node/AbstractNodeDAOImpl.java b/source/java/org/alfresco/repo/domain/node/AbstractNodeDAOImpl.java index 2f8577d8cc..7a49784789 100644 --- a/source/java/org/alfresco/repo/domain/node/AbstractNodeDAOImpl.java +++ b/source/java/org/alfresco/repo/domain/node/AbstractNodeDAOImpl.java @@ -1411,21 +1411,24 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO final StoreEntity newParentStore = newParentNode.getStore(); final Node childNode = getNodeNotNull(childNodeId, true); final StoreEntity childStore = childNode.getStore(); - ChildAssocEntity primaryParentAssoc = getPrimaryParentAssocImpl(childNodeId); + final ChildAssocEntity primaryParentAssoc = getPrimaryParentAssocImpl(childNodeId); final Long oldParentAclId; + final Long oldParentNodeId; if (primaryParentAssoc == null) { oldParentAclId = null; + oldParentNodeId = null; } else { if (primaryParentAssoc.getParentNode() == null) { oldParentAclId = null; + oldParentNodeId = null; } else { - Long oldParentNodeId = primaryParentAssoc.getParentNode().getId(); + oldParentNodeId = primaryParentAssoc.getParentNode().getId(); oldParentAclId = getNodeNotNull(oldParentNodeId, true).getAclId(); } } @@ -1491,6 +1494,18 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO assocQName, childNodeNameToUse); controlDAO.releaseSavepoint(savepoint); + // Ensure we invalidate the name cache (the child version key might not have been 'bumped' by the last + // 'touch') + if (updated > 0 && primaryParentAssoc != null) + { + Pair oldTypeQnamePair = qnameDAO.getQName( + primaryParentAssoc.getTypeQNameId()); + if (oldTypeQnamePair != null) + { + childByNameCache.remove(new ChildByNameKey(oldParentNodeId, oldTypeQnamePair.getSecond(), + primaryParentAssoc.getChildNodeName())); + } + } return updated; } catch (Throwable e) @@ -1515,16 +1530,20 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO }; childAssocRetryingHelper.doWithRetry(callback); - // Check for cyclic relationships - // TODO: This adds a lot of overhead when moving hierarchies. - // While getPaths is faster, it would be better to avoid the parentAssocsCache - // completely. - getPaths(newChildNode.getNodePair(), false); -// cycleCheck(newChildNodeId); + // Optimize for rename case + if (!EqualsHelper.nullSafeEquals(newParentNodeId, oldParentNodeId)) + { + // Check for cyclic relationships + // TODO: This adds a lot of overhead when moving hierarchies. + // While getPaths is faster, it would be better to avoid the parentAssocsCache + // completely. + getPaths(newChildNode.getNodePair(), false); +// cycleCheck(newChildNodeId); - // Update ACLs for moved tree - Long newParentAclId = newParentNode.getAclId(); - accessControlListDAO.updateInheritance(newChildNodeId, oldParentAclId, newParentAclId); + // Update ACLs for moved tree + Long newParentAclId = newParentNode.getAclId(); + accessControlListDAO.updateInheritance(newChildNodeId, oldParentAclId, newParentAclId); + } // Done Pair assocPair = getPrimaryParentAssoc(newChildNode.getId()); @@ -3115,12 +3134,37 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO { public Integer execute() throws Throwable { + int total = 0; Savepoint savepoint = controlDAO.createSavepoint("DuplicateChildNodeNameException"); try { - Integer count = updateChildAssocsUniqueName(childNodeId, childName); + for (ChildAssocEntity parentAssoc : getParentAssocsCached(childNodeId).getParentAssocs().values()) + { + // Subtlety: We only update those associations for which name uniqueness checking is enforced. + // Such associations have a positive CRC + if (parentAssoc.getChildNodeNameCrc() <= 0) + { + continue; + } + Pair oldTypeQnamePair = qnameDAO.getQName(parentAssoc.getTypeQNameId()); + // Ensure we invalidate the name cache (the child version key might not be 'bumped' by the next + // 'touch') + if (oldTypeQnamePair != null) + { + childByNameCache.remove(new ChildByNameKey(parentAssoc.getParentNode().getId(), + oldTypeQnamePair.getSecond(), parentAssoc.getChildNodeName())); + } + int count = updateChildAssocUniqueName(parentAssoc.getId(), childName); + if (count <= 0) + { + // Should not be attempting to delete a deleted node + throw new ConcurrencyFailureException("Failed to update an existing parent association " + + parentAssoc.getId()); + } + total += count; + } controlDAO.releaseSavepoint(savepoint); - return count; + return total; } catch (Throwable e) { @@ -4717,7 +4761,7 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO QName assocTypeQName, QName assocQName, int index); - protected abstract int updateChildAssocsUniqueName(Long childNodeId, String name); + protected abstract int updateChildAssocUniqueName(Long assocId, String name); // protected abstract int deleteChildAssocsToAndFrom(Long nodeId); protected abstract ChildAssocEntity selectChildAssoc(Long assocId); protected abstract List selectChildNodeIds( diff --git a/source/java/org/alfresco/repo/domain/node/ibatis/NodeDAOImpl.java b/source/java/org/alfresco/repo/domain/node/ibatis/NodeDAOImpl.java index 4aa47fba10..a57e8792b3 100644 --- a/source/java/org/alfresco/repo/domain/node/ibatis/NodeDAOImpl.java +++ b/source/java/org/alfresco/repo/domain/node/ibatis/NodeDAOImpl.java @@ -117,7 +117,7 @@ public class NodeDAOImpl extends AbstractNodeDAOImpl private static final String INSERT_CHILD_ASSOC = "alfresco.node.insert.insert_ChildAssoc"; private static final String DELETE_CHILD_ASSOCS = "alfresco.node.delete_ChildAssocs"; private static final String UPDATE_CHILD_ASSOCS_INDEX = "alfresco.node.update_ChildAssocsIndex"; - private static final String UPDATE_CHILD_ASSOCS_UNIQUE_NAME = "alfresco.node.update_ChildAssocsUniqueName"; + private static final String UPDATE_CHILD_ASSOC_UNIQUE_NAME = "alfresco.node.update_ChildAssocUniqueName"; private static final String SELECT_CHILD_ASSOC_BY_ID = "alfresco.node.select_ChildAssocById"; private static final String COUNT_CHILD_ASSOC_BY_PARENT_ID = "alfresco.node.count_ChildAssocByParentId"; private static final String SELECT_CHILD_ASSOCS_BY_PROPERTY_VALUE = "alfresco.node.select_ChildAssocsByPropertyValue"; @@ -843,17 +843,13 @@ public class NodeDAOImpl extends AbstractNodeDAOImpl } @Override - protected int updateChildAssocsUniqueName(Long childNodeId, String name) + protected int updateChildAssocUniqueName(Long assocId, String name) { ChildAssocEntity assoc = new ChildAssocEntity(); - // Child - NodeEntity childNode = new NodeEntity(); - childNode.setId(childNodeId); - assoc.setChildNode(childNode); + assoc.setId(assocId); // Name - assoc.setChildNodeNameAll(null, null, name); - - return template.update(UPDATE_CHILD_ASSOCS_UNIQUE_NAME, assoc); + assoc.setChildNodeNameAll(null, null, name); + return template.update(UPDATE_CHILD_ASSOC_UNIQUE_NAME, assoc); } @Override diff --git a/source/java/org/alfresco/repo/lock/LockServiceImpl.java b/source/java/org/alfresco/repo/lock/LockServiceImpl.java index 7ca0561934..ae6fd022e4 100644 --- a/source/java/org/alfresco/repo/lock/LockServiceImpl.java +++ b/source/java/org/alfresco/repo/lock/LockServiceImpl.java @@ -48,6 +48,7 @@ import org.alfresco.service.cmr.lock.LockType; import org.alfresco.service.cmr.lock.NodeLockedException; import org.alfresco.service.cmr.lock.UnableToAquireLockException; import org.alfresco.service.cmr.lock.UnableToReleaseLockException; +import org.alfresco.service.cmr.lock.UnableToReleaseLockException.CAUSE; import org.alfresco.service.cmr.repository.AspectMissingException; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.NodeRef; @@ -297,18 +298,40 @@ public class LockServiceImpl implements LockService, /** * @see org.alfresco.service.cmr.lock.LockService#unlock(NodeRef, String) */ + @Override public void unlock(NodeRef nodeRef) throws UnableToReleaseLockException { + unlock(nodeRef, false, false); + } + + /** + * @see org.alfresco.service.cmr.lock.LockService#unlock(org.alfresco.service.cmr.repository.NodeRef, boolean) + */ + @Override + public void unlock(NodeRef nodeRef, boolean lockChildren) throws UnableToReleaseLockException + { + unlock(nodeRef, lockChildren, false); + } + + /** + * @see org.alfresco.service.cmr.lock.LockService#unlock(NodeRef, String, + * boolean, boolean) + */ + @Override + public void unlock(NodeRef nodeRef, boolean unlockChildren, boolean allowCheckedOut) + throws UnableToReleaseLockException + { + // Unlock the parent nodeRef = tenantService.getName(nodeRef); - if (!nodeService.hasAspect(nodeRef, ContentModel.ASPECT_LOCKABLE)) + + // MNT-231: forbidden to unlock a checked out node + if (!allowCheckedOut && nodeService.hasAspect(nodeRef, ContentModel.ASPECT_CHECKED_OUT)) { - // Nothing to unlock + throw new UnableToReleaseLockException(nodeRef, CAUSE.CHECKED_OUT); } - else + + if (nodeService.hasAspect(nodeRef, ContentModel.ASPECT_LOCKABLE)) { - // Check for lock aspect - checkForLockApsect(nodeRef); - addToIgnoreSet(nodeRef); try { @@ -320,19 +343,8 @@ public class LockServiceImpl implements LockService, removeFromIgnoreSet(nodeRef); } } - } - /** - * @see org.alfresco.service.cmr.lock.LockService#unlock(NodeRef, String, - * boolean) - */ - public void unlock(NodeRef nodeRef, boolean unlockChildren) - throws UnableToReleaseLockException - { - // Unlock the parent - unlock(nodeRef); - - if (unlockChildren == true) + if (unlockChildren) { // Get the children and unlock them Collection childAssocRefs = this.nodeService.getChildAssocs(nodeRef); diff --git a/source/java/org/alfresco/repo/lock/LockServiceImplTest.java b/source/java/org/alfresco/repo/lock/LockServiceImplTest.java index ed61e3d10a..309b88fca6 100644 --- a/source/java/org/alfresco/repo/lock/LockServiceImplTest.java +++ b/source/java/org/alfresco/repo/lock/LockServiceImplTest.java @@ -18,12 +18,15 @@ */ package org.alfresco.repo.lock; +import static org.junit.Assert.assertNotNull; + import java.io.Serializable; import java.util.HashMap; import java.util.List; import org.alfresco.model.ContentModel; import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.service.cmr.coci.CheckOutCheckInService; import org.alfresco.service.cmr.lock.LockService; import org.alfresco.service.cmr.lock.LockStatus; import org.alfresco.service.cmr.lock.LockType; @@ -52,6 +55,7 @@ public class LockServiceImplTest extends BaseSpringTest private NodeService nodeService; private LockService lockService; private MutableAuthenticationService authenticationService; + private CheckOutCheckInService cociService; /** * Data used in tests @@ -60,6 +64,7 @@ public class LockServiceImplTest extends BaseSpringTest private NodeRef childNode1; private NodeRef childNode2; private NodeRef noAspectNode; + private NodeRef checkedOutNode; private static final String GOOD_USER_NAME = "goodUser"; private static final String BAD_USER_NAME = "badUser"; @@ -76,6 +81,7 @@ public class LockServiceImplTest extends BaseSpringTest this.nodeService = (NodeService)applicationContext.getBean("dbNodeService"); this.lockService = (LockService)applicationContext.getBean("lockService"); this.authenticationService = (MutableAuthenticationService)applicationContext.getBean("authenticationService"); + this.cociService = (CheckOutCheckInService) applicationContext.getBean("checkOutCheckInService"); // Set the authentication AuthenticationComponent authComponent = (AuthenticationComponent)this.applicationContext.getBean("authenticationComponent"); @@ -131,6 +137,22 @@ public class LockServiceImplTest extends BaseSpringTest nodeProperties).getChildRef(); assertNotNull(this.noAspectNode); + // Create node with checkedOut + this.checkedOutNode = this.nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{}checkedOutNode"), + ContentModel.TYPE_CONTAINER, + nodeProperties).getChildRef(); + assertNotNull(this.checkedOutNode); + + // Check out test file + NodeRef fileWorkingCopyNodeRef = cociService.checkout(checkedOutNode); + assertNotNull(fileWorkingCopyNodeRef); + assertTrue(nodeService.hasAspect(checkedOutNode, ContentModel.ASPECT_CHECKED_OUT)); + assertTrue(nodeService.hasAspect(checkedOutNode, ContentModel.ASPECT_LOCKABLE)); + + // Create the users TestWithUserUtils.createUser(GOOD_USER_NAME, PWD, rootNodeRef, this.nodeService, this.authenticationService); TestWithUserUtils.createUser(BAD_USER_NAME, PWD, rootNodeRef, this.nodeService, this.authenticationService); @@ -512,4 +534,23 @@ public class LockServiceImplTest extends BaseSpringTest logger.debug("exception while trying to create a child of a read only lock", e); } } + + /** + * Test that it is impossible to unlock a checked out node + */ + public void testUnlockCheckedOut() throws Exception + { + TestWithUserUtils.authenticateUser(GOOD_USER_NAME, PWD, rootNodeRef, this.authenticationService); + + try + { + this.lockService.unlock(checkedOutNode); + fail("could unlock a checked out node"); + } + catch (UnableToReleaseLockException e) + { + logger.debug("exception while trying to unlock a checked out node", e); + } + } + } diff --git a/source/java/org/alfresco/repo/publishing/facebook/FacebookChannelType.java b/source/java/org/alfresco/repo/publishing/facebook/FacebookChannelType.java index 7af978f632..680148890e 100644 --- a/source/java/org/alfresco/repo/publishing/facebook/FacebookChannelType.java +++ b/source/java/org/alfresco/repo/publishing/facebook/FacebookChannelType.java @@ -42,7 +42,7 @@ import org.springframework.social.oauth2.OAuth2Parameters; public class FacebookChannelType extends AbstractChannelType { public final static String ID = "facebook"; - public final static String DEFAULT_REDIRECT_URI = "http://alfresco.com/stand-alone-auth-return.html"; + public final static String DEFAULT_REDIRECT_URI = "http://www.alfresco.com/stand-alone-auth-return.html"; private FacebookPublishingHelper publishingHelper; private String redirectUri = DEFAULT_REDIRECT_URI; diff --git a/source/java/org/alfresco/service/cmr/lock/LockService.java b/source/java/org/alfresco/service/cmr/lock/LockService.java index 9aa7191ec5..3d007adcea 100644 --- a/source/java/org/alfresco/service/cmr/lock/LockService.java +++ b/source/java/org/alfresco/service/cmr/lock/LockService.java @@ -139,7 +139,8 @@ public interface LockService * Removes the lock on a node; if there is no lock then nothing is done. *

* The user must have sufficient permissions to remove the lock (ie: be the - * owner of the lock or have admin rights) otherwise an exception will be raised. + * owner of the lock or have admin rights) and the node must not be checked + * out. Otherwise an exception will be raised. * * @param nodeRef a reference to a node * @throws UnableToReleaseLockException @@ -149,6 +150,29 @@ public interface LockService public void unlock(NodeRef nodeRef) throws UnableToReleaseLockException; + /** + * Removes the lock on a node and optional on its children. + *

+ * The user must have sufficient permissions to remove the lock(s) (ie: be + * the owner of the lock(s) or have admin rights) and the node(s) must not be + * checked out. Otherwise an exception will be raised. + *

+ * If one of the child nodes is not locked then it will be ignored and + * the process continue without error. + *

+ * If the lock on any one of the child nodes cannot be released then an + * exception will be raised. + * + * @param nodeRef a node reference + * @param lockChildren if true then all the children (and grandchildren, etc) + * of the node will also be unlocked, false otherwise + * @throws UnableToReleaseLockException + * thrown if the lock could not be released + */ + @Auditable(parameters = {"nodeRef", "lockChildren"}) + public void unlock(NodeRef nodeRef, boolean lockChildren) + throws UnableToReleaseLockException; + /** * Removes the lock on a node and optional on its children. *

@@ -165,13 +189,13 @@ public interface LockService * @param nodeRef a node reference * @param lockChildren if true then all the children (and grandchildren, etc) * of the node will also be unlocked, false otherwise + * @param allowCheckedOut is it permissable for a node to be a checked out node? * @throws UnableToReleaseLockException * thrown if the lock could not be released */ - @Auditable(parameters = {"nodeRef", "lockChildren"}) - public void unlock(NodeRef nodeRef, boolean lockChildren) + @Auditable(parameters = {"nodeRef", "lockChildren", "allowCheckedOut"}) + public void unlock(NodeRef nodeRef, boolean lockChildren, boolean allowCheckedOut) throws UnableToReleaseLockException; - /** * Removes a lock on the nodes provided. *

diff --git a/source/java/org/alfresco/service/cmr/lock/UnableToReleaseLockException.java b/source/java/org/alfresco/service/cmr/lock/UnableToReleaseLockException.java index b94e95765a..88d7b97af2 100644 --- a/source/java/org/alfresco/service/cmr/lock/UnableToReleaseLockException.java +++ b/source/java/org/alfresco/service/cmr/lock/UnableToReleaseLockException.java @@ -33,18 +33,45 @@ public class UnableToReleaseLockException extends RuntimeException /** * Serial verison UID */ - private static final long serialVersionUID = 3257565088071432243L; + private static final long serialVersionUID = 3257565088071432244L; /** * Error message */ - private static final String ERROR_MESSAGE = I18NUtil.getMessage("lock_service.insufficent_privileges"); + private static final String ERROR_MESSAGE_1 = I18NUtil.getMessage("lock_service.insufficent_privileges"); + private static final String ERROR_MESSAGE_2 = I18NUtil.getMessage("lock_service.unlock_checkedout"); /** * Constructor */ public UnableToReleaseLockException(NodeRef nodeRef) { - super(MessageFormat.format(ERROR_MESSAGE, new Object[]{nodeRef.getId()})); + super(MessageFormat.format(ERROR_MESSAGE_1, new Object[]{nodeRef.getId()})); } + + public enum CAUSE { INSUFFICIENT, CHECKED_OUT }; + + private static String createMessage(NodeRef nodeRef, CAUSE cause) + { + if (cause == null) + { + return MessageFormat.format(ERROR_MESSAGE_1, new Object[] { nodeRef.getId() }); + } + + switch (cause) + { + case INSUFFICIENT: + return MessageFormat.format(ERROR_MESSAGE_1, new Object[] { nodeRef.getId() }); + case CHECKED_OUT: + return MessageFormat.format(ERROR_MESSAGE_2, new Object[] { nodeRef.getId() }); + default: + return MessageFormat.format(ERROR_MESSAGE_1, new Object[] { nodeRef.getId() }); + } + } + + public UnableToReleaseLockException(NodeRef nodeRef, CAUSE cause) + { + super(createMessage(nodeRef, cause)); + } + } diff --git a/source/test-resources/filesys/ContentDiskDriverTestMark.jpg b/source/test-resources/filesys/ContentDiskDriverTestMark.jpg new file mode 100644 index 0000000000..b6b9c209da Binary files /dev/null and b/source/test-resources/filesys/ContentDiskDriverTestMark.jpg differ diff --git a/source/test-resources/filesys/ContentDiskDriverTestMark2.jpg b/source/test-resources/filesys/ContentDiskDriverTestMark2.jpg new file mode 100644 index 0000000000..f926d884d7 Binary files /dev/null and b/source/test-resources/filesys/ContentDiskDriverTestMark2.jpg differ