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
This commit is contained in:
Matt Ward
2012-04-20 14:33:19 +00:00
parent 617e6486cc
commit 004b2c5e60
12 changed files with 854 additions and 358 deletions

View File

@@ -33,7 +33,7 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
* @author Ivan Rybnikov * @author Ivan Rybnikov
* *
*/ */
public final class LockInfoImpl implements Serializable, LockInfo public class LockInfoImpl implements Serializable, LockInfo
{ {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@@ -262,7 +262,7 @@ public final class LockInfoImpl implements Serializable, LockInfo
{ {
return false; return false;
} }
Date now = new Date(); Date now = dateNow();
return now.after(expires); return now.after(expires);
} }
@@ -321,6 +321,25 @@ public final class LockInfoImpl implements Serializable, LockInfo
return expires; 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. * 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 * Sets the expiry date/time to lockTimeout seconds into the future. Provide
* a lockTimeout of WebDAV.TIMEOUT_INFINITY for never expires. * a lockTimeout of WebDAV.TIMEOUT_INFINITY for never expires.
* *
* @param lockTimeout * @param lockTimeoutSecs
*/ */
@Override @Override
public void setTimeoutSeconds(int lockTimeout) public void setTimeoutSeconds(int lockTimeoutSecs)
{ {
if (lockTimeout == WebDAV.TIMEOUT_INFINITY) if (lockTimeoutSecs == WebDAV.TIMEOUT_INFINITY)
{ {
setExpires(null); setExpires(null);
} }
else else
{ {
int timeoutMillis = (lockTimeout * 60 * 1000); int timeoutMillis = (lockTimeoutSecs * 1000);
Date now = new Date(); Date now = dateNow();
Date nextExpiry = new Date(now.getTime() + timeoutMillis); Date nextExpiry = new Date(now.getTime() + timeoutMillis);
setExpires(nextExpiry); 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();
}
} }

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}
}

View File

@@ -90,7 +90,7 @@ public class LockMethod extends WebDAVMethod
} }
/** /**
* Return the lock timeout, in minutes * Return the lock timeout, in seconds.
* *
* @return int * @return int
*/ */
@@ -426,7 +426,7 @@ public class LockMethod extends WebDAVMethod
// Store the owner of this lock // Store the owner of this lock
lockInfo.setOwner(userName); lockInfo.setOwner(userName);
// Lock the node // Lock the node
getLockStore().put(lockNode.getNodeRef(), lockInfo); getDAVLockService().lock(lockNode.getNodeRef(), lockInfo);
if (logger.isDebugEnabled()) if (logger.isDebugEnabled())
{ {

View File

@@ -190,7 +190,7 @@ public class PutMethod extends WebDAVMethod implements ActivityPostProducer
} }
String userName = getDAVHelper().getAuthenticationService().getCurrentUserName(); String userName = getDAVHelper().getAuthenticationService().getCurrentUserName();
LockInfo lockInfo = getLockStore().get(contentNodeInfo.getNodeRef()); LockInfo lockInfo = getDAVLockService().getLockInfo(contentNodeInfo.getNodeRef());
if (lockInfo != null) if (lockInfo != null)
{ {

View File

@@ -130,7 +130,7 @@ public class UnlockMethod extends WebDAVMethod
} }
NodeRef nodeRef = lockNodeInfo.getNodeRef(); NodeRef nodeRef = lockNodeInfo.getNodeRef();
LockInfo lockInfo = getLockStore().get(nodeRef); LockInfo lockInfo = getDAVLockService().getLockInfo(nodeRef);
if (lockInfo == null) if (lockInfo == null)
{ {
@@ -169,7 +169,7 @@ public class UnlockMethod extends WebDAVMethod
String currentUser = getAuthenticationService().getCurrentUserName(); String currentUser = getAuthenticationService().getCurrentUserName();
if (currentUser.equals(lockInfo.getOwner())) if (currentUser.equals(lockInfo.getOwner()))
{ {
getLockStore().remove(nodeRef); getDAVLockService().unlock(nodeRef);
// Indicate that the unlock was successful // Indicate that the unlock was successful
m_response.setStatus(HttpServletResponse.SC_NO_CONTENT); m_response.setStatus(HttpServletResponse.SC_NO_CONTENT);

View File

@@ -91,7 +91,6 @@ public class WebDAVHelper
private DictionaryService m_dictionaryService; private DictionaryService m_dictionaryService;
private MimetypeService m_mimetypeService; private MimetypeService m_mimetypeService;
private WebDAVLockService m_lockService; private WebDAVLockService m_lockService;
private LockStore m_lockStore;
private ActionService m_actionService; private ActionService m_actionService;
private AuthenticationService m_authService; private AuthenticationService m_authService;
private PermissionService m_permissionService; private PermissionService m_permissionService;
@@ -104,7 +103,7 @@ public class WebDAVHelper
/** /**
* Class constructor * Class constructor
*/ */
protected WebDAVHelper(ServiceRegistry serviceRegistry, LockStore lockStore, AuthenticationService authService, TenantService tenantService) protected WebDAVHelper(ServiceRegistry serviceRegistry, AuthenticationService authService, TenantService tenantService)
{ {
m_serviceRegistry = serviceRegistry; m_serviceRegistry = serviceRegistry;
@@ -119,8 +118,6 @@ public class WebDAVHelper
m_permissionService = m_serviceRegistry.getPermissionService(); m_permissionService = m_serviceRegistry.getPermissionService();
m_tenantService = tenantService; m_tenantService = tenantService;
m_authService = authService; m_authService = authService;
m_lockStore = lockStore;
} }
/** /**
@@ -192,14 +189,6 @@ public class WebDAVHelper
return m_lockService; return m_lockService;
} }
/**
* @return Return the {@link LockStore lock store}.
*/
public final LockStore getLockStore()
{
return m_lockStore;
}
/** /**
* @return Return the action service * @return Return the action service
*/ */

View File

@@ -19,262 +19,39 @@
package org.alfresco.repo.webdav; package org.alfresco.repo.webdav;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpSession; 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.Auditable;
import org.alfresco.service.cmr.coci.CheckOutCheckInService;
import org.alfresco.service.cmr.lock.LockService; import org.alfresco.service.cmr.lock.LockService;
import org.alfresco.service.cmr.lock.LockStatus; import org.alfresco.service.cmr.lock.LockStatus;
import org.alfresco.service.cmr.lock.LockType; import org.alfresco.service.cmr.lock.LockType;
import org.alfresco.service.cmr.repository.NodeRef; 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;
/** /**
* <p>
* WebDAVLockService is used to manage file locks for WebDAV and Sharepoint protocol. It ensures a lock never persists * 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. * for more than 24 hours, and also ensures locks are timed out on session timeout.
* *
* @author Pavel.Yurkevich * @author Pavel.Yurkevich
* @author Matt Ward
*/ */
public class WebDAVLockService public interface WebDAVLockService
{ {
public static final String BEAN_NAME = "webDAVLockService"; 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<HttpSession> currentSession = new ThreadLocal<HttpSession>();
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);
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public void sessionDestroyed() 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<Pair<String, NodeRef>> lockedResources = (List<Pair<String, NodeRef>>) 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<String, NodeRef> lockedResource : lockedResources)
{
String runAsUser = lockedResource.getFirst();
final NodeRef nodeRef = lockedResource.getSecond();
// there are some document that should be forcibly unlocked
AuthenticationUtil.runAs(new RunAsWork<Void>()
{
@Override
public Void doWork() throws Exception
{
return transactionService.getRetryingTransactionHelper().doInTransaction(
new RetryingTransactionCallback<Void>()
{
@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());
}
}
}
/** /**
* Shared method for webdav/vti protocols to lock node. If node is locked for more than 24 hours it is automatically added * 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. * to the current session locked resources list.
* *
* @param nodeRef the node to lock * @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 * @param timeout the number of seconds before the locks expires
*/ */
public void lock(NodeRef nodeRef, LockType lockType, int timeout) void lock(NodeRef nodeRef, String userName, int timeout);
{
boolean performSessionBehavior = false;
// ALF-11777 fix, do not lock node for more than 24 hours (webdav and vti) void lock(NodeRef nodeRef, LockInfo lockInfo);
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<String, NodeRef>(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 to unlock node. Unlocked node is automatically removed from * 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 * @param nodeRef the node to lock
*/ */
public void unlock(NodeRef nodeRef) void unlock(NodeRef nodeRef);
{
this.lockService.unlock(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<String, NodeRef>(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) * @see LockService#getLockStatus(NodeRef, NodeRef)
* *
* @param nodeRef the node reference * @param nodeRef the node reference
* @return the lock status * @return the lock status
*/ */
@Auditable(parameters = {"nodeRef"}) @Auditable(parameters = { "nodeRef" })
public LockStatus getLockStatus(NodeRef nodeRef) LockInfo getLockInfo(NodeRef nodeRef);
{
return this.lockService.getLockStatus(nodeRef);
}
/** /**
* Add the given <code>object</code> to the session list that is stored in session under <code>listName</code> attribute * Caches current session in a thread local variable.
* *
* @param session the session * @param session
* @param listName the list name (session attribute name)
* @param object the object to store in session list
*/ */
@SuppressWarnings("unchecked") void setCurrentSession(HttpSession session);
private static final void storeObjectInSessionList(HttpSession session, String listName, Object object)
{
List<Object> list = null;
synchronized (session)
{
list = (List<Object>) session.getAttribute(listName);
if (list == null)
{
list = new ArrayList<Object>();
session.setAttribute(listName, list);
}
}
synchronized (list)
{
if (!list.contains(object))
{
list.add(object);
}
}
}
/**
* Removes the given <code>object</code> from the session list that is stored in session under <code>listName</code> attribute
*
* @param session the session
* @param listName the list name (session attribute name)
* @param object the object to store in session list
*
* @return <tt>true</tt> if session list contained the specified element, otherwise <tt>false</tt>
*/
@SuppressWarnings("unchecked")
private static final boolean removeObjectFromSessionList(HttpSession session, String listName, Object object)
{
List<Object> list = null;
synchronized (session)
{
list = (List<Object>) session.getAttribute(listName);
}
if (list == null)
{
return false;
}
synchronized (list)
{
return list.remove(object);
}
}
} }

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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;
/**
* <p>
* 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<HttpSession> currentSession = new ThreadLocal<HttpSession>();
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<Pair<String, NodeRef>> lockedResources = (List<Pair<String, NodeRef>>) 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<String, NodeRef> lockedResource : lockedResources)
{
String runAsUser = lockedResource.getFirst();
final NodeRef nodeRef = lockedResource.getSecond();
// there are some document that should be forcibly unlocked
AuthenticationUtil.runAs(new RunAsWork<Void>()
{
@Override
public Void doWork() throws Exception
{
return transactionService.getRetryingTransactionHelper().doInTransaction(
new RetryingTransactionCallback<Void>()
{
@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<String, NodeRef>(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<String, NodeRef>(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 <code>object</code> to the session list that is stored in session under <code>listName</code> 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<Object> list = null;
synchronized (session)
{
list = (List<Object>) session.getAttribute(listName);
if (list == null)
{
list = new ArrayList<Object>();
session.setAttribute(listName, list);
}
}
synchronized (list)
{
if (!list.contains(object))
{
list.add(object);
}
}
}
/**
* Removes the given <code>object</code> from the session list that is stored in session under <code>listName</code> attribute
*
* @param session the session
* @param listName the list name (session attribute name)
* @param object the object to store in session list
*
* @return <tt>true</tt> if session list contained the specified element, otherwise <tt>false</tt>
*/
@SuppressWarnings("unchecked")
private static final boolean removeObjectFromSessionList(HttpSession session, String listName, Object object)
{
List<Object> list = null;
synchronized (session)
{
list = (List<Object>) 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;
}
}

View File

@@ -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<Pair<String, NodeRef>> 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<Void> callback = (RetryingTransactionCallback<Void>) args[0];
callback.execute();
return null;
}
});
Mockito.when(transactionService.getRetryingTransactionHelper()).thenReturn(txHelper);
davLockService.setTransactionService(transactionService);
}
@Test
public void testSessionDestroyed()
{
List<Pair<String, NodeRef>> lockedNodes = new ArrayList<Pair<String, NodeRef>>(2);
lockedNodes.add(new Pair<String, NodeRef>("some_user_name", nodeRef1));
lockedNodes.add(new Pair<String, NodeRef>("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<String, NodeRef>("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<String, NodeRef>("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<String, NodeRef>("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));
}
}

View File

@@ -334,7 +334,7 @@ public abstract class WebDAVMethod
WebDAVMethod.this.m_reader = null; WebDAVMethod.this.m_reader = null;
// cache current session // cache current session
WebDAVLockService.setCurrentSession(m_request.getSession()); getDAVHelper().getLockService().setCurrentSession(m_request.getSession());
executeImpl(); executeImpl();
return null; 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) private LockInfo getNodeLockInfoDirect(FileInfo nodeInfo)
{ {
LockInfo lock = getLockStore().get(nodeInfo.getNodeRef()); LockInfo lock = getDAVLockService().getLockInfo(nodeInfo.getNodeRef());
if (lock == null) if (lock == null)
{ {
@@ -1283,7 +1283,7 @@ public abstract class WebDAVMethod
*/ */
private LockInfo getNodeLockInfoIndirect(NodeRef parent) private LockInfo getNodeLockInfoIndirect(NodeRef parent)
{ {
LockInfo parentLock = getLockStore().get(parent); LockInfo parentLock = getDAVLockService().getLockInfo(parent);
if (parentLock == null) if (parentLock == null)
{ {

View File

@@ -290,8 +290,6 @@ public class WebDAVServlet extends HttpServlet
NodeService nodeService = (NodeService) context.getBean("NodeService"); NodeService nodeService = (NodeService) context.getBean("NodeService");
SearchService searchService = (SearchService) context.getBean("SearchService"); SearchService searchService = (SearchService) context.getBean("SearchService");
NamespaceService namespaceService = (NamespaceService) context.getBean("NamespaceService"); NamespaceService namespaceService = (NamespaceService) context.getBean("NamespaceService");
LockStoreFactory lockStoreFactory = (LockStoreFactory) context.getBean("webdavLockStoreFactory");
LockStore lockStore = lockStoreFactory.getLockStore();
ActivityService activityService = (ActivityService) context.getBean("activityService"); ActivityService activityService = (ActivityService) context.getBean("activityService");
PersonService personService = m_serviceRegistry.getPersonService(); PersonService personService = m_serviceRegistry.getPersonService();
@@ -299,7 +297,7 @@ public class WebDAVServlet extends HttpServlet
activityPoster = new ActivityPosterImpl(activityService, nodeService, personService); activityPoster = new ActivityPosterImpl(activityService, nodeService, personService);
// Create the WebDAV helper // 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 // Initialize the root node

View File

@@ -65,7 +65,7 @@ public class WebDAVSessionListener implements HttpSessionListener, ServletContex
@Override @Override
public void sessionDestroyed(HttpSessionEvent hse) public void sessionDestroyed(HttpSessionEvent hse)
{ {
WebDAVLockService.setCurrentSession(hse.getSession()); webDAVLockService.setCurrentSession(hse.getSession());
webDAVLockService.sessionDestroyed(); webDAVLockService.sessionDestroyed();
if (logger.isDebugEnabled()) if (logger.isDebugEnabled())