From 004b2c5e6078b1e12654652c13e81e6212a838ae Mon Sep 17 00:00:00 2001 From: Matt Ward Date: Fri, 20 Apr 2012 14:33:19 +0000 Subject: [PATCH] ALF-13028: Sharepoint broken by changes to WebDAV Integrated fix for ALF-11777 so that locks are not kept for more than 24 hours and 24 hour or infinite locks are dropped on user's session destruction. Extracted interface from WebDAVLockService and moved the implementation to WebDAVLockServiceImpl. Modified WebDAVLockServiceImpl to use the LockStore in-memory locking. WebDAV and SPP use WebDAVLockService instead of directly using LockStore. git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@35486 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- .../alfresco/repo/webdav/LockInfoImpl.java | 62 ++- .../repo/webdav/LockInfoImplTest.java | 98 ++++ .../org/alfresco/repo/webdav/LockMethod.java | 4 +- .../org/alfresco/repo/webdav/PutMethod.java | 2 +- .../alfresco/repo/webdav/UnlockMethod.java | 4 +- .../alfresco/repo/webdav/WebDAVHelper.java | 13 +- .../repo/webdav/WebDAVLockService.java | 338 +----------- .../repo/webdav/WebDAVLockServiceImpl.java | 487 ++++++++++++++++++ .../webdav/WebDAVLockServiceImplTest.java | 184 +++++++ .../alfresco/repo/webdav/WebDAVMethod.java | 14 +- .../alfresco/repo/webdav/WebDAVServlet.java | 4 +- .../repo/webdav/WebDAVSessionListener.java | 2 +- 12 files changed, 854 insertions(+), 358 deletions(-) create mode 100644 source/java/org/alfresco/repo/webdav/LockInfoImplTest.java create mode 100644 source/java/org/alfresco/repo/webdav/WebDAVLockServiceImpl.java create mode 100644 source/java/org/alfresco/repo/webdav/WebDAVLockServiceImplTest.java diff --git a/source/java/org/alfresco/repo/webdav/LockInfoImpl.java b/source/java/org/alfresco/repo/webdav/LockInfoImpl.java index 1db0239b8a..bbc168fa60 100644 --- a/source/java/org/alfresco/repo/webdav/LockInfoImpl.java +++ b/source/java/org/alfresco/repo/webdav/LockInfoImpl.java @@ -33,7 +33,7 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; * @author Ivan Rybnikov * */ -public final class LockInfoImpl implements Serializable, LockInfo +public class LockInfoImpl implements Serializable, LockInfo { private static final long serialVersionUID = 1L; @@ -262,7 +262,7 @@ public final class LockInfoImpl implements Serializable, LockInfo { return false; } - Date now = new Date(); + Date now = dateNow(); return now.after(expires); } @@ -321,6 +321,25 @@ public final class LockInfoImpl implements Serializable, LockInfo return expires; } + /** + * Remaining time before lock expires, in seconds. + */ + @Override + public long getRemainingTimeoutSeconds() + { + Date expires = getExpires(); + if (expires == null) + { + return WebDAV.TIMEOUT_INFINITY; + } + else + { + Date now = dateNow(); + long timeout = ((expires.getTime() - now.getTime()) / 1000); + return timeout; + } + } + /** * Sanity check the state of this LockInfo. */ @@ -336,21 +355,50 @@ public final class LockInfoImpl implements Serializable, LockInfo * Sets the expiry date/time to lockTimeout seconds into the future. Provide * a lockTimeout of WebDAV.TIMEOUT_INFINITY for never expires. * - * @param lockTimeout + * @param lockTimeoutSecs */ @Override - public void setTimeoutSeconds(int lockTimeout) + public void setTimeoutSeconds(int lockTimeoutSecs) { - if (lockTimeout == WebDAV.TIMEOUT_INFINITY) + if (lockTimeoutSecs == WebDAV.TIMEOUT_INFINITY) { setExpires(null); } else { - int timeoutMillis = (lockTimeout * 60 * 1000); - Date now = new Date(); + int timeoutMillis = (lockTimeoutSecs * 1000); + Date now = dateNow(); Date nextExpiry = new Date(now.getTime() + timeoutMillis); setExpires(nextExpiry); } } + + /** + * Sets the expiry date/time to lockTimeout minutes into the future. Provide + * a lockTimeout of WebDAV.TIMEOUT_INFINITY for never expires. + * + * @param lockTimeoutMins + */ + @Override + public void setTimeoutMinutes(int lockTimeoutMins) + { + if (lockTimeoutMins != WebDAV.TIMEOUT_INFINITY) + { + setTimeoutSeconds(lockTimeoutMins * 60); + } + else + { + setTimeoutSeconds(WebDAV.TIMEOUT_INFINITY); + } + } + + /** + * Hook to allow unit testing - gets the current date/time. + * + * @return Date + */ + protected Date dateNow() + { + return new Date(); + } } diff --git a/source/java/org/alfresco/repo/webdav/LockInfoImplTest.java b/source/java/org/alfresco/repo/webdav/LockInfoImplTest.java new file mode 100644 index 0000000000..eae565b9a1 --- /dev/null +++ b/source/java/org/alfresco/repo/webdav/LockInfoImplTest.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2005-2012 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.webdav; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.util.Date; + +import org.junit.Test; + + +public class LockInfoImplTest +{ + @Test + public void canSetTimeoutSeconds() + { + LockInfoImplEx lockInfo = new LockInfoImplEx(); + + // This should add 7 seconds (7000 millis) to the expiry date. + lockInfo.setTimeoutSeconds(7); + + // Check the new date. + assertEquals(86407000, lockInfo.getExpires().getTime()); + } + + @Test + public void canSetTimeoutSecondsToInfinity() + { + LockInfoImplEx lockInfo = new LockInfoImplEx(); + + lockInfo.setTimeoutSeconds(WebDAV.TIMEOUT_INFINITY); + + // Check the new date. + assertNull(lockInfo.getExpires()); + } + + @Test + public void canSetTimeoutMinutes() + { + LockInfoImplEx lockInfo = new LockInfoImplEx(); + + // This should add 5 minutes to the expiry date. + lockInfo.setTimeoutMinutes(5); + + // Check the new date. + assertEquals(86700000, lockInfo.getExpires().getTime()); + } + + @Test + public void canSetTimeoutMinutesToInfinity() + { + LockInfoImplEx lockInfo = new LockInfoImplEx(); + + lockInfo.setTimeoutMinutes(WebDAV.TIMEOUT_INFINITY); + + // Check the new date. + assertNull(lockInfo.getExpires()); + } + + @Test + public void canGetRemainingTimeoutSeconds() + { + LockInfoImplEx lockInfo = new LockInfoImplEx(); + + lockInfo.setTimeoutSeconds(7); + + assertEquals(7, lockInfo.getRemainingTimeoutSeconds()); + } + + public static class LockInfoImplEx extends LockInfoImpl + { + public static final Date DATE_NOW = new Date(86400000); + private static final long serialVersionUID = 1669378516554195322L; + + @Override + protected Date dateNow() + { + return DATE_NOW; + } + } +} diff --git a/source/java/org/alfresco/repo/webdav/LockMethod.java b/source/java/org/alfresco/repo/webdav/LockMethod.java index 6ad362a217..b07c56c162 100644 --- a/source/java/org/alfresco/repo/webdav/LockMethod.java +++ b/source/java/org/alfresco/repo/webdav/LockMethod.java @@ -90,7 +90,7 @@ public class LockMethod extends WebDAVMethod } /** - * Return the lock timeout, in minutes + * Return the lock timeout, in seconds. * * @return int */ @@ -426,7 +426,7 @@ public class LockMethod extends WebDAVMethod // Store the owner of this lock lockInfo.setOwner(userName); // Lock the node - getLockStore().put(lockNode.getNodeRef(), lockInfo); + getDAVLockService().lock(lockNode.getNodeRef(), lockInfo); if (logger.isDebugEnabled()) { diff --git a/source/java/org/alfresco/repo/webdav/PutMethod.java b/source/java/org/alfresco/repo/webdav/PutMethod.java index 17d43a3470..a0d4a9d455 100644 --- a/source/java/org/alfresco/repo/webdav/PutMethod.java +++ b/source/java/org/alfresco/repo/webdav/PutMethod.java @@ -190,7 +190,7 @@ public class PutMethod extends WebDAVMethod implements ActivityPostProducer } String userName = getDAVHelper().getAuthenticationService().getCurrentUserName(); - LockInfo lockInfo = getLockStore().get(contentNodeInfo.getNodeRef()); + LockInfo lockInfo = getDAVLockService().getLockInfo(contentNodeInfo.getNodeRef()); if (lockInfo != null) { diff --git a/source/java/org/alfresco/repo/webdav/UnlockMethod.java b/source/java/org/alfresco/repo/webdav/UnlockMethod.java index 0b2909c282..38d8ae24e3 100644 --- a/source/java/org/alfresco/repo/webdav/UnlockMethod.java +++ b/source/java/org/alfresco/repo/webdav/UnlockMethod.java @@ -130,7 +130,7 @@ public class UnlockMethod extends WebDAVMethod } NodeRef nodeRef = lockNodeInfo.getNodeRef(); - LockInfo lockInfo = getLockStore().get(nodeRef); + LockInfo lockInfo = getDAVLockService().getLockInfo(nodeRef); if (lockInfo == null) { @@ -169,7 +169,7 @@ public class UnlockMethod extends WebDAVMethod String currentUser = getAuthenticationService().getCurrentUserName(); if (currentUser.equals(lockInfo.getOwner())) { - getLockStore().remove(nodeRef); + getDAVLockService().unlock(nodeRef); // Indicate that the unlock was successful m_response.setStatus(HttpServletResponse.SC_NO_CONTENT); diff --git a/source/java/org/alfresco/repo/webdav/WebDAVHelper.java b/source/java/org/alfresco/repo/webdav/WebDAVHelper.java index 5dad8e58a1..796696391e 100644 --- a/source/java/org/alfresco/repo/webdav/WebDAVHelper.java +++ b/source/java/org/alfresco/repo/webdav/WebDAVHelper.java @@ -91,7 +91,6 @@ public class WebDAVHelper private DictionaryService m_dictionaryService; private MimetypeService m_mimetypeService; private WebDAVLockService m_lockService; - private LockStore m_lockStore; private ActionService m_actionService; private AuthenticationService m_authService; private PermissionService m_permissionService; @@ -104,7 +103,7 @@ public class WebDAVHelper /** * Class constructor */ - protected WebDAVHelper(ServiceRegistry serviceRegistry, LockStore lockStore, AuthenticationService authService, TenantService tenantService) + protected WebDAVHelper(ServiceRegistry serviceRegistry, AuthenticationService authService, TenantService tenantService) { m_serviceRegistry = serviceRegistry; @@ -119,8 +118,6 @@ public class WebDAVHelper m_permissionService = m_serviceRegistry.getPermissionService(); m_tenantService = tenantService; m_authService = authService; - - m_lockStore = lockStore; } /** @@ -191,14 +188,6 @@ public class WebDAVHelper { return m_lockService; } - - /** - * @return Return the {@link LockStore lock store}. - */ - public final LockStore getLockStore() - { - return m_lockStore; - } /** * @return Return the action service diff --git a/source/java/org/alfresco/repo/webdav/WebDAVLockService.java b/source/java/org/alfresco/repo/webdav/WebDAVLockService.java index 649c23f428..5c60c53381 100644 --- a/source/java/org/alfresco/repo/webdav/WebDAVLockService.java +++ b/source/java/org/alfresco/repo/webdav/WebDAVLockService.java @@ -19,262 +19,39 @@ package org.alfresco.repo.webdav; -import java.util.ArrayList; -import java.util.List; - import javax.servlet.http.HttpSession; -import org.alfresco.model.ContentModel; -import org.alfresco.repo.security.authentication.AuthenticationUtil; -import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; -import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.Auditable; -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; import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.cmr.repository.NodeService; -import org.alfresco.service.transaction.TransactionService; -import org.alfresco.util.Pair; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; /** - *

* WebDAVLockService is used to manage file locks for WebDAV and Sharepoint protocol. It ensures a lock never persists * for more than 24 hours, and also ensures locks are timed out on session timeout. * * @author Pavel.Yurkevich + * @author Matt Ward */ -public class WebDAVLockService +public interface WebDAVLockService { - public static final String BEAN_NAME = "webDAVLockService"; - - /** The session attribute under which webdav/vti stores its locked documents. */ - private static final String LOCKED_RESOURCES = "_webdavLockedResources"; - - private static Log logger = LogFactory.getLog(WebDAVLockService.class); - - private static ThreadLocal currentSession = new ThreadLocal(); - - private LockService lockService; - private NodeService nodeService; - private TransactionService transactionService; - private CheckOutCheckInService checkOutCheckInService; - - /** - * Set the LockService - * - * @param lockService - */ - public void setLockService(LockService lockService) - { - this.lockService = lockService; - } - - /** - * Set the NodeService - * - * @param nodeService - */ - public void setNodeService(NodeService nodeService) - { - this.nodeService = nodeService; - } - - /** - * Set the TransactionService - * - * @param transactionService - */ - public void setTransactionService(TransactionService transactionService) - { - this.transactionService = transactionService; - } - - /** - * Set the CheckOutCheckInService - * - * @param checkOutCheckInService - */ - public void setCheckOutCheckInService(CheckOutCheckInService checkOutCheckInService) - { - this.checkOutCheckInService = checkOutCheckInService; - } - - /** - * Caches current session to the thread local variable - * - * @param currentSession - */ - public static void setCurrentSession(HttpSession session) - { - currentSession.set(session); - } + static final String BEAN_NAME = "webDAVLockService"; @SuppressWarnings("unchecked") - public void sessionDestroyed() - { - HttpSession session = currentSession.get(); - - if (session == null) - { - if (logger.isDebugEnabled()) - { - logger.debug("Couldn't find current session."); - } - return; - } - - // look for locked documents list in http session - final List> lockedResources = (List>) session.getAttribute(LOCKED_RESOURCES); - - if (lockedResources != null && lockedResources.size() > 0) - { - if (logger.isDebugEnabled()) - { - logger.debug("Found " + lockedResources.size() + " locked resources for session: " + session.getId()); - } - - for (Pair lockedResource : lockedResources) - { - String runAsUser = lockedResource.getFirst(); - final NodeRef nodeRef = lockedResource.getSecond(); - - // there are some document that should be forcibly unlocked - AuthenticationUtil.runAs(new RunAsWork() - { - @Override - public Void doWork() throws Exception - { - return transactionService.getRetryingTransactionHelper().doInTransaction( - new RetryingTransactionCallback() - { - @Override - public Void execute() throws Throwable - { - // check whether this document still exists in repo - if (nodeService.exists(nodeRef)) - { - if (logger.isDebugEnabled()) - { - logger.debug("Trying to release lock for: " + nodeRef); - } - - // check the lock status of document - LockStatus lockStatus = lockService.getLockStatus(nodeRef); - - // check if document was checked out - boolean hasWorkingCopy = checkOutCheckInService.getWorkingCopy(nodeRef) != null; - boolean isWorkingCopy = nodeService.hasAspect(nodeRef, ContentModel.ASPECT_WORKING_COPY); - - // forcibly unlock document if it is still locked and not checked out - if ((lockStatus.equals(LockStatus.LOCKED) || - lockStatus.equals(LockStatus.LOCK_OWNER)) && !hasWorkingCopy && !isWorkingCopy) - { - try - { - // try to unlock it - lockService.unlock(nodeRef); - - if (logger.isDebugEnabled()) - { - logger.debug("Lock was successfully released for: " - + nodeRef); - } - } - catch (Exception e) - { - if (logger.isDebugEnabled()) - { - logger.debug("Unable to unlock " + nodeRef - + " cause: " + e.getMessage()); - } - } - } - else - { - // document is not locked or is checked out - if (logger.isDebugEnabled()) - { - logger.debug("Skip lock releasing for: " + nodeRef - + " as it is not locked or is checked out"); - } - } - } - else - { - // document no longer exists in repo - if (logger.isDebugEnabled()) - { - logger.debug("Skip lock releasing for an unexisting node: " + nodeRef); - } - } - return null; - } - }, transactionService.isReadOnly()); - } - }, runAsUser == null ? AuthenticationUtil.getSystemUserName() : runAsUser); - } - } - else - { - // there are no documents with unexpected lock left on it - if (logger.isDebugEnabled()) - { - logger.debug("No locked resources were found for session: " + session.getId()); - } - } - } + void sessionDestroyed(); /** * Shared method for webdav/vti protocols to lock node. If node is locked for more than 24 hours it is automatically added * to the current session locked resources list. * * @param nodeRef the node to lock - * @param lockType the lock type + * @param userName the current user's user name * @param timeout the number of seconds before the locks expires */ - public void lock(NodeRef nodeRef, LockType lockType, int timeout) - { - boolean performSessionBehavior = false; + void lock(NodeRef nodeRef, String userName, int timeout); - // ALF-11777 fix, do not lock node for more than 24 hours (webdav and vti) - if (timeout >= WebDAV.TIMEOUT_24_HOURS || timeout == WebDAV.TIMEOUT_INFINITY) - { - timeout = WebDAV.TIMEOUT_24_HOURS; - performSessionBehavior = true; - } - - this.lockService.lock(nodeRef, lockType, timeout); - - if (logger.isDebugEnabled()) - { - logger.debug(nodeRef + " was locked for " + timeout + " seconds."); - } - - if (performSessionBehavior) - { - HttpSession session = currentSession.get(); - - if (session == null) - { - if (logger.isDebugEnabled()) - { - logger.debug("Couldn't find current session."); - } - return; - } - - storeObjectInSessionList(session, LOCKED_RESOURCES, new Pair(AuthenticationUtil.getRunAsUser(), nodeRef)); - - if (logger.isDebugEnabled()) - { - logger.debug(nodeRef + " was added to the session " + session.getId() + " for post expiration processing."); - } - } - } + void lock(NodeRef nodeRef, LockInfo lockInfo); /** * Shared method for webdav/vti to unlock node. Unlocked node is automatically removed from @@ -282,108 +59,23 @@ public class WebDAVLockService * * @param nodeRef the node to lock */ - public void unlock(NodeRef nodeRef) - { - this.lockService.unlock(nodeRef); + void unlock(NodeRef nodeRef); - if (logger.isDebugEnabled()) - { - logger.debug(nodeRef + " was unlocked."); - } - - HttpSession session = currentSession.get(); - - if (session == null) - { - if (logger.isDebugEnabled()) - { - logger.debug("Couldn't find current session."); - } - return; - } - - boolean removed = removeObjectFromSessionList(session, LOCKED_RESOURCES, new Pair(AuthenticationUtil.getRunAsUser(), nodeRef)); - - if (removed && logger.isDebugEnabled()) - { - logger.debug(nodeRef + " was removed from the session " + session.getId()); - } - } - /** - * Gets the lock status for the node reference relative to the current user. + * Gets the lock info for the node reference relative to the current user. * * @see LockService#getLockStatus(NodeRef, NodeRef) * * @param nodeRef the node reference * @return the lock status */ - @Auditable(parameters = {"nodeRef"}) - public LockStatus getLockStatus(NodeRef nodeRef) - { - return this.lockService.getLockStatus(nodeRef); - } + @Auditable(parameters = { "nodeRef" }) + LockInfo getLockInfo(NodeRef nodeRef); /** - * Add the given object to the session list that is stored in session under listName attribute + * Caches current session in a thread local variable. * - * @param session the session - * @param listName the list name (session attribute name) - * @param object the object to store in session list + * @param session */ - @SuppressWarnings("unchecked") - private static final void storeObjectInSessionList(HttpSession session, String listName, Object object) - { - List list = null; - - synchronized (session) - { - list = (List) session.getAttribute(listName); - - if (list == null) - { - list = new ArrayList(); - session.setAttribute(listName, list); - } - } - - synchronized (list) - { - if (!list.contains(object)) - { - list.add(object); - } - } - } - - /** - * Removes the given object from the session list that is stored in session under listName attribute - * - * @param session the session - * @param listName the list name (session attribute name) - * @param object the object to store in session list - * - * @return true if session list contained the specified element, otherwise false - */ - @SuppressWarnings("unchecked") - private static final boolean removeObjectFromSessionList(HttpSession session, String listName, Object object) - { - List list = null; - - synchronized (session) - { - list = (List) session.getAttribute(listName); - } - - if (list == null) - { - return false; - } - - synchronized (list) - { - return list.remove(object); - } - } - -} + void setCurrentSession(HttpSession session); +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/webdav/WebDAVLockServiceImpl.java b/source/java/org/alfresco/repo/webdav/WebDAVLockServiceImpl.java new file mode 100644 index 0000000000..e39e947a0d --- /dev/null +++ b/source/java/org/alfresco/repo/webdav/WebDAVLockServiceImpl.java @@ -0,0 +1,487 @@ +/* + * Copyright (C) 2005-2012 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.webdav; + +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.http.HttpSession; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.Auditable; +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; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.Pair; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + *

+ * WebDAVLockService is used to manage file locks for WebDAV and Sharepoint protocol. It ensures a lock never persists + * for more than 24 hours, and also ensures locks are timed out on session timeout. + * + * @author Pavel.Yurkevich + */ +public class WebDAVLockServiceImpl implements WebDAVLockService +{ + /** The session attribute under which webdav/vti stores its locked documents. */ + private static final String LOCKED_RESOURCES = "_webdavLockedResources"; + + private static Log logger = LogFactory.getLog(WebDAVLockServiceImpl.class); + + private static ThreadLocal currentSession = new ThreadLocal(); + + private LockService lockService; + private NodeService nodeService; + private LockStore lockStore; + private TransactionService transactionService; + private CheckOutCheckInService checkOutCheckInService; + + /** + * Set the LockService + * + * @param lockService + */ + public void setLockService(LockService lockService) + { + this.lockService = lockService; + } + + /** + * Set the NodeService + * + * @param nodeService + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Set the LockStore that will be used to keep hold of relavent LockInfo objects. + * + * @param lockStore + */ + public void setLockStoreFactory(LockStoreFactory lockStoreFactory) + { + LockStore lockStore = lockStoreFactory.getLockStore(); + this.lockStore = lockStore; + } + + /** + * Set the TransactionService + * + * @param transactionService + */ + public void setTransactionService(TransactionService transactionService) + { + this.transactionService = transactionService; + } + + /** + * Set the CheckOutCheckInService + * + * @param checkOutCheckInService + */ + public void setCheckOutCheckInService(CheckOutCheckInService checkOutCheckInService) + { + this.checkOutCheckInService = checkOutCheckInService; + } + + /** + * Caches current session to the thread local variable + * + * @param currentSession + */ + @Override + public void setCurrentSession(HttpSession session) + { + currentSession.set(session); + } + + @Override + @SuppressWarnings("unchecked") + public void sessionDestroyed() + { + HttpSession session = currentSession.get(); + + if (session == null) + { + if (logger.isDebugEnabled()) + { + logger.debug("Couldn't find current session."); + } + return; + } + + // look for locked documents list in http session + final List> lockedResources = (List>) session.getAttribute(LOCKED_RESOURCES); + + if (lockedResources != null && lockedResources.size() > 0) + { + if (logger.isDebugEnabled()) + { + logger.debug("Found " + lockedResources.size() + " locked resources for session: " + session.getId()); + } + + for (Pair lockedResource : lockedResources) + { + String runAsUser = lockedResource.getFirst(); + final NodeRef nodeRef = lockedResource.getSecond(); + + // there are some document that should be forcibly unlocked + AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public Void doWork() throws Exception + { + return transactionService.getRetryingTransactionHelper().doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + // check whether this document still exists in repo + if (nodeService.exists(nodeRef)) + { + if (logger.isDebugEnabled()) + { + logger.debug("Trying to release lock for: " + nodeRef); + } + + // check the lock status of document + LockStatus lockStatus = lockService.getLockStatus(nodeRef); + + // check if document was checked out + boolean hasWorkingCopy = checkOutCheckInService.getWorkingCopy(nodeRef) != null; + boolean isWorkingCopy = nodeService.hasAspect(nodeRef, ContentModel.ASPECT_WORKING_COPY); + + // forcibly unlock document if it is still locked and not checked out + if ((lockStatus.equals(LockStatus.LOCKED) || + lockStatus.equals(LockStatus.LOCK_OWNER)) && !hasWorkingCopy && !isWorkingCopy) + { + try + { + // try to unlock it + lockService.unlock(nodeRef); + lockStore.remove(nodeRef); + + if (logger.isDebugEnabled()) + { + logger.debug("Lock was successfully released for: " + + nodeRef); + } + } + catch (Exception e) + { + if (logger.isDebugEnabled()) + { + logger.debug("Unable to unlock " + nodeRef + + " cause: " + e.getMessage()); + } + } + } + else + { + // document is not locked or is checked out + if (logger.isDebugEnabled()) + { + logger.debug("Skip lock releasing for: " + nodeRef + + " as it is not locked or is checked out"); + } + } + } + else + { + // document no longer exists in repo + if (logger.isDebugEnabled()) + { + logger.debug("Skip lock releasing for an unexisting node: " + nodeRef); + } + } + return null; + } + }, transactionService.isReadOnly()); + } + }, runAsUser == null ? AuthenticationUtil.getSystemUserName() : runAsUser); + } + } + else + { + // there are no documents with unexpected lock left on it + if (logger.isDebugEnabled()) + { + logger.debug("No locked resources were found for session: " + session.getId()); + } + } + } + + public void lock(NodeRef nodeRef, LockInfo lockInfo) + { + boolean performSessionBehavior = false; + long timeout; + lockInfo.getRWLock().readLock().lock(); + try + { + timeout = lockInfo.getRemainingTimeoutSeconds(); + } + finally + { + lockInfo.getRWLock().readLock().unlock(); + } + + // ALF-11777 fix, do not lock node for more than 24 hours (webdav and vti) + if (timeout >= WebDAV.TIMEOUT_24_HOURS || timeout == WebDAV.TIMEOUT_INFINITY) + { + lockInfo.getRWLock().writeLock().lock(); + try + { + // Repeat the precondition check + if (timeout >= WebDAV.TIMEOUT_24_HOURS || timeout == WebDAV.TIMEOUT_INFINITY) + { + timeout = WebDAV.TIMEOUT_24_HOURS; + lockInfo.setTimeoutSeconds((int) timeout); + performSessionBehavior = true; + } + } + finally + { + lockInfo.getRWLock().writeLock().unlock(); + } + } + + lockStore.put(nodeRef, lockInfo); + + + if (logger.isDebugEnabled()) + { + logger.debug(nodeRef + " was locked for " + timeout + " seconds."); + } + + if (performSessionBehavior) + { + HttpSession session = currentSession.get(); + + if (session == null) + { + if (logger.isDebugEnabled()) + { + logger.debug("Couldn't find current session."); + } + return; + } + + storeObjectInSessionList(session, LOCKED_RESOURCES, new Pair(AuthenticationUtil.getRunAsUser(), nodeRef)); + + if (logger.isDebugEnabled()) + { + logger.debug(nodeRef + " was added to the session " + session.getId() + " for post expiration processing."); + } + } + } + + /** + * Shared method for webdav/vti protocols to lock node. If node is locked for more than 24 hours it is automatically added + * to the current session locked resources list. + * + * @param nodeRef the node to lock + * @param lockType the lock type + * @param timeout the number of seconds before the locks expires + */ + @Override + public void lock(NodeRef nodeRef, String userName, int timeout) + { + LockInfo lockInfo = createLock(nodeRef, userName, true, timeout); + lock(nodeRef, lockInfo); + } + + /** + * Shared method for webdav/vti to unlock node. Unlocked node is automatically removed from + * current sessions's locked resources list. + * + * @param nodeRef the node to lock + */ + @Override + public void unlock(NodeRef nodeRef) + { + lockStore.remove(nodeRef); + + if (logger.isDebugEnabled()) + { + logger.debug(nodeRef + " was unlocked."); + } + + HttpSession session = currentSession.get(); + + if (session == null) + { + if (logger.isDebugEnabled()) + { + logger.debug("Couldn't find current session."); + } + return; + } + + boolean removed = removeObjectFromSessionList(session, LOCKED_RESOURCES, new Pair(AuthenticationUtil.getRunAsUser(), nodeRef)); + + if (removed && logger.isDebugEnabled()) + { + logger.debug(nodeRef + " was removed from the session " + session.getId()); + } + } + + /** + * Gets the lock status for the node reference relative to the current user. + * + * @see LockService#getLockStatus(NodeRef, NodeRef) + * + * @param nodeRef the node reference + * @return the lock status + */ + @Override + @Auditable(parameters = {"nodeRef"}) + public LockInfo getLockInfo(NodeRef nodeRef) + { + return lockStore.get(nodeRef); + } + + /** + * Add the given object to the session list that is stored in session under listName attribute + * + * @param session the session + * @param listName the list name (session attribute name) + * @param object the object to store in session list + */ + @SuppressWarnings("unchecked") + private static final void storeObjectInSessionList(HttpSession session, String listName, Object object) + { + List list = null; + + synchronized (session) + { + list = (List) session.getAttribute(listName); + + if (list == null) + { + list = new ArrayList(); + session.setAttribute(listName, list); + } + } + + synchronized (list) + { + if (!list.contains(object)) + { + list.add(object); + } + } + } + + /** + * Removes the given object from the session list that is stored in session under listName attribute + * + * @param session the session + * @param listName the list name (session attribute name) + * @param object the object to store in session list + * + * @return true if session list contained the specified element, otherwise false + */ + @SuppressWarnings("unchecked") + private static final boolean removeObjectFromSessionList(HttpSession session, String listName, Object object) + { + List list = null; + + synchronized (session) + { + list = (List) session.getAttribute(listName); + } + + if (list == null) + { + return false; + } + + synchronized (list) + { + return list.remove(object); + } + } + + /** + * Create a new lock + * + * @param lockNode NodeRef + * @param userName String + * @exception WebDAVServerException + */ + protected LockInfo createLock(NodeRef nodeRef, String userName, boolean createExclusive, int timeoutSecs) + { + // Create Lock token + String lockToken = WebDAV.makeLockToken(nodeRef, userName); + + LockInfo lockInfo = new LockInfoImpl(); + + lockInfo.getRWLock().writeLock().lock(); + try + { + if (createExclusive) + { + // Lock the node + lockInfo.setTimeoutSeconds(timeoutSecs); + lockInfo.setExclusiveLockToken(lockToken); + } + else + { + lockInfo.addSharedLockToken(lockToken); + } + + // Store lock depth + lockInfo.setDepth(WebDAV.getDepthName(WebDAV.DEPTH_INFINITY)); + // Store lock scope (shared/exclusive) + String scope = createExclusive ? WebDAV.XML_EXCLUSIVE : WebDAV.XML_SHARED; + lockInfo.setScope(scope); + // Store the owner of this lock + lockInfo.setOwner(userName); + // Lock the node + lockStore.put(nodeRef, lockInfo); + + if (logger.isDebugEnabled()) + { + logger.debug("Locked node " + nodeRef + ": " + lockInfo); + } + } + finally + { + lockInfo.getRWLock().writeLock().unlock(); + } + + return lockInfo; + } + +} diff --git a/source/java/org/alfresco/repo/webdav/WebDAVLockServiceImplTest.java b/source/java/org/alfresco/repo/webdav/WebDAVLockServiceImplTest.java new file mode 100644 index 0000000000..a8e8d6c324 --- /dev/null +++ b/source/java/org/alfresco/repo/webdav/WebDAVLockServiceImplTest.java @@ -0,0 +1,184 @@ +package org.alfresco.repo.webdav; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; + +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.http.HttpSession; + +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +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.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.Pair; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +@RunWith(MockitoJUnitRunner.class) +public class WebDAVLockServiceImplTest +{ + private WebDAVLockServiceImpl davLockService; + private @Mock LockStore lockStore; + private @Mock HttpSession session; + private @Mock List> sessionList; + private @Mock AuthenticationUtil authenticationUtil; + private @Mock TransactionService transactionService; + private @Mock RetryingTransactionHelper txHelper; + private @Mock NodeService nodeService; + private @Mock LockService lockService; + private @Mock CheckOutCheckInService cociService; + private NodeRef nodeRef1; + private NodeRef nodeRef2; + private LockInfoImpl lockInfo1; + private LockInfoImpl lockInfo2; + + @SuppressWarnings("unchecked") + @Before + public void setUp() throws Exception + { + davLockService = new WebDAVLockServiceImpl(); + LockStoreFactory lockStoreFactory = Mockito.mock(LockStoreFactory.class); + Mockito.when(lockStoreFactory.getLockStore()).thenReturn(lockStore); + davLockService.setLockStoreFactory(lockStoreFactory); + davLockService.setNodeService(nodeService); + davLockService.setCheckOutCheckInService(cociService); + davLockService.setCurrentSession(session); + davLockService.setLockService(lockService); + + // Train the mock LockStore to respond to get() requests for certain noderefs. + nodeRef1 = new NodeRef("workspace://SpacesStore/f6e3f82a-cfef-445b-9fca-7986a14181cc"); + lockInfo1 = new LockInfoImplTest.LockInfoImplEx(); + Mockito.when(lockStore.get(nodeRef1)).thenReturn(lockInfo1); + nodeRef2 = new NodeRef("workspace://SpacesStore/a6a4371c-99b9-4618-8cd2-e71d7d96aa87"); + lockInfo2 = new LockInfoImplTest.LockInfoImplEx(); + Mockito.when(lockStore.get(nodeRef2)).thenReturn(lockInfo2); + + // The mock HttpSession should return the mock session list. + Mockito.when(session.getAttribute("_webdavLockedResources")).thenReturn(sessionList); + + // Provide a user name for our fictional user. + authenticationUtil = new AuthenticationUtil(); + authenticationUtil.afterPropertiesSet(); + AuthenticationUtil.setFullyAuthenticatedUser("some_user_name"); + + Mockito.when(txHelper.doInTransaction(any(RetryingTransactionCallback.class), anyBoolean())).thenAnswer(new Answer() + { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable + { + Object[] args = invocation.getArguments(); + RetryingTransactionCallback callback = (RetryingTransactionCallback) args[0]; + callback.execute(); + return null; + } + + }); + Mockito.when(transactionService.getRetryingTransactionHelper()).thenReturn(txHelper); + davLockService.setTransactionService(transactionService); + } + + + @Test + public void testSessionDestroyed() + { + List> lockedNodes = new ArrayList>(2); + lockedNodes.add(new Pair("some_user_name", nodeRef1)); + lockedNodes.add(new Pair("another_user_name", nodeRef2)); + Mockito.when(sessionList.size()).thenReturn(2); + Mockito.when(sessionList.iterator()).thenReturn(lockedNodes.iterator()); + + Mockito.when(nodeService.exists(nodeRef1)).thenReturn(true); + Mockito.when(nodeService.exists(nodeRef2)).thenReturn(true); + + Mockito.when(lockService.getLockStatus(nodeRef1)).thenReturn(LockStatus.LOCKED); + Mockito.when(lockService.getLockStatus(nodeRef2)).thenReturn(LockStatus.LOCKED); + + // We're not going to do anything with nodeRef2 + NodeRef wcNodeRef2 = new NodeRef("workspace://SpacesStore/a6e3f82a-cfef-363d-9fca-3986a14180a0"); + Mockito.when(cociService.getWorkingCopy(nodeRef2)).thenReturn(wcNodeRef2); + + davLockService.sessionDestroyed(); + + // nodeRef1 is unlocked + Mockito.verify(lockService).unlock(nodeRef1); + Mockito.verify(lockStore).remove(nodeRef1); + + // nodeRef2 is not unlocked + Mockito.verify(lockService, Mockito.never()).unlock(nodeRef2); + Mockito.verify(lockStore, Mockito.never()).remove(nodeRef2); + } + + @Test + public void lockLessThan24Hours() + { + lockInfo1.setTimeoutSeconds(100); + + davLockService.lock(nodeRef1, lockInfo1); + + Mockito.verify(lockStore).put(nodeRef1, lockInfo1); + // 100 seconds (in millis) should have been added to the date/time stamp. + assertEquals(86500000, lockInfo1.getExpires().getTime()); + } + + @Test + public void lockGreaterThan24Hours() + { + int timeout25hours = WebDAV.TIMEOUT_24_HOURS + 3600; + lockInfo1.setTimeoutSeconds(timeout25hours); + + davLockService.lock(nodeRef1, lockInfo1); + + Mockito.verify(lockStore).put(nodeRef1, lockInfo1); + Mockito.verify(sessionList).add(new Pair("some_user_name", nodeRef1)); + // Timeout should be capped at 24 hours. + assertEquals(WebDAV.TIMEOUT_24_HOURS, lockInfo1.getRemainingTimeoutSeconds()); + } + + @Test + public void lockForInfinityTime() + { + lockInfo1.setTimeoutSeconds(WebDAV.TIMEOUT_INFINITY); + + davLockService.lock(nodeRef1, lockInfo1); + + Mockito.verify(lockStore).put(nodeRef1, lockInfo1); + Mockito.verify(sessionList).add(new Pair("some_user_name", nodeRef1)); + // Timeout should be capped at 24 hours. + assertEquals(WebDAV.TIMEOUT_24_HOURS, lockInfo1.getRemainingTimeoutSeconds()); + } + + @Test + public void canUnlock() + { + davLockService.unlock(nodeRef1); + + // NodeRef should have been removed from the LockStore + Mockito.verify(lockStore).remove(nodeRef1); + // Node should have been removed from the list in the user's session. + Mockito.verify(sessionList).remove(new Pair("some_user_name", nodeRef1)); + } + + @Test + public void canGetLockInfo() + { + // Sanity check that what we're putting in, is what we're getting out. + assertNull("LockInfo should be null", davLockService.getLockInfo(null)); + assertEquals(lockInfo1, davLockService.getLockInfo(nodeRef1)); + assertEquals(lockInfo2, davLockService.getLockInfo(nodeRef2)); + } +} diff --git a/source/java/org/alfresco/repo/webdav/WebDAVMethod.java b/source/java/org/alfresco/repo/webdav/WebDAVMethod.java index 38a0e62e04..996192d2cd 100644 --- a/source/java/org/alfresco/repo/webdav/WebDAVMethod.java +++ b/source/java/org/alfresco/repo/webdav/WebDAVMethod.java @@ -334,7 +334,7 @@ public abstract class WebDAVMethod WebDAVMethod.this.m_reader = null; // cache current session - WebDAVLockService.setCurrentSession(m_request.getSession()); + getDAVHelper().getLockService().setCurrentSession(m_request.getSession()); executeImpl(); return null; @@ -674,13 +674,13 @@ public abstract class WebDAVMethod } /** - * Retrieve the (WebDAV protocol-level) {@link LockStore lock store}. + * Retrieve the (WebDAV protocol-level) locking service. * - * @return LockStore + * @return WebDAVLockService */ - protected final LockStore getLockStore() + protected final WebDAVLockService getDAVLockService() { - return m_davHelper.getLockStore(); + return m_davHelper.getLockService(); } /** @@ -1253,7 +1253,7 @@ public abstract class WebDAVMethod */ private LockInfo getNodeLockInfoDirect(FileInfo nodeInfo) { - LockInfo lock = getLockStore().get(nodeInfo.getNodeRef()); + LockInfo lock = getDAVLockService().getLockInfo(nodeInfo.getNodeRef()); if (lock == null) { @@ -1283,7 +1283,7 @@ public abstract class WebDAVMethod */ private LockInfo getNodeLockInfoIndirect(NodeRef parent) { - LockInfo parentLock = getLockStore().get(parent); + LockInfo parentLock = getDAVLockService().getLockInfo(parent); if (parentLock == null) { diff --git a/source/java/org/alfresco/repo/webdav/WebDAVServlet.java b/source/java/org/alfresco/repo/webdav/WebDAVServlet.java index e880955a4e..ae6a89011a 100644 --- a/source/java/org/alfresco/repo/webdav/WebDAVServlet.java +++ b/source/java/org/alfresco/repo/webdav/WebDAVServlet.java @@ -290,8 +290,6 @@ public class WebDAVServlet extends HttpServlet NodeService nodeService = (NodeService) context.getBean("NodeService"); SearchService searchService = (SearchService) context.getBean("SearchService"); NamespaceService namespaceService = (NamespaceService) context.getBean("NamespaceService"); - LockStoreFactory lockStoreFactory = (LockStoreFactory) context.getBean("webdavLockStoreFactory"); - LockStore lockStore = lockStoreFactory.getLockStore(); ActivityService activityService = (ActivityService) context.getBean("activityService"); PersonService personService = m_serviceRegistry.getPersonService(); @@ -299,7 +297,7 @@ public class WebDAVServlet extends HttpServlet activityPoster = new ActivityPosterImpl(activityService, nodeService, personService); // Create the WebDAV helper - m_davHelper = new WebDAVHelper(m_serviceRegistry, lockStore, authService, tenantService); + m_davHelper = new WebDAVHelper(m_serviceRegistry, authService, tenantService); // Initialize the root node diff --git a/source/java/org/alfresco/repo/webdav/WebDAVSessionListener.java b/source/java/org/alfresco/repo/webdav/WebDAVSessionListener.java index b1d6c57c18..669c4e3493 100644 --- a/source/java/org/alfresco/repo/webdav/WebDAVSessionListener.java +++ b/source/java/org/alfresco/repo/webdav/WebDAVSessionListener.java @@ -65,7 +65,7 @@ public class WebDAVSessionListener implements HttpSessionListener, ServletContex @Override public void sessionDestroyed(HttpSessionEvent hse) { - WebDAVLockService.setCurrentSession(hse.getSession()); + webDAVLockService.setCurrentSession(hse.getSession()); webDAVLockService.sessionDestroyed(); if (logger.isDebugEnabled())