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())