Browse Source

now passing 94.6% of litmus lock tests

Sebastian Stenzel 9 years ago
parent
commit
93ef366125

+ 1 - 1
main/frontend-webdav/src/main/java/org/cryptomator/filesystem/jackrabbit/FileLocator.java

@@ -14,7 +14,7 @@ import org.apache.jackrabbit.webdav.DavLocatorFactory;
 import org.cryptomator.filesystem.File;
 import org.cryptomator.filesystem.delegating.DelegatingFile;
 
-public class FileLocator extends DelegatingFile<FolderLocator>implements FileSystemResourceLocator {
+public class FileLocator extends DelegatingFile<FolderLocator>implements InternalFileSystemResourceLocator {
 
 	private final DavLocatorFactory factory;
 	private final String prefix;

+ 0 - 11
main/frontend-webdav/src/main/java/org/cryptomator/filesystem/jackrabbit/FileSystemResourceLocator.java

@@ -9,24 +9,13 @@
 package org.cryptomator.filesystem.jackrabbit;
 
 import java.util.Optional;
-import java.util.concurrent.atomic.AtomicReference;
 
 import org.apache.jackrabbit.webdav.DavResourceLocator;
 import org.apache.jackrabbit.webdav.util.EncodeUtil;
-import org.cryptomator.common.LazyInitializer;
 import org.cryptomator.filesystem.Node;
 
 public interface FileSystemResourceLocator extends DavResourceLocator, Node {
 
-	@Override
-	default String getResourcePath() {
-		return LazyInitializer.initializeLazily(getResourcePathRef(), this::computeResourcePath);
-	}
-
-	AtomicReference<String> getResourcePathRef();
-
-	String computeResourcePath();
-
 	@Override
 	Optional<FolderLocator> parent();
 

+ 1 - 1
main/frontend-webdav/src/main/java/org/cryptomator/filesystem/jackrabbit/FolderLocator.java

@@ -16,7 +16,7 @@ import org.cryptomator.filesystem.File;
 import org.cryptomator.filesystem.Folder;
 import org.cryptomator.filesystem.delegating.DelegatingFolder;
 
-public class FolderLocator extends DelegatingFolder<FolderLocator, FileLocator>implements FileSystemResourceLocator {
+public class FolderLocator extends DelegatingFolder<FolderLocator, FileLocator>implements InternalFileSystemResourceLocator {
 
 	private final DavLocatorFactory factory;
 	private final String prefix;

+ 21 - 0
main/frontend-webdav/src/main/java/org/cryptomator/filesystem/jackrabbit/InternalFileSystemResourceLocator.java

@@ -0,0 +1,21 @@
+package org.cryptomator.filesystem.jackrabbit;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.cryptomator.common.LazyInitializer;
+
+/**
+ * Adds package-private API to {@link FileSystemResourceLocator}.
+ */
+interface InternalFileSystemResourceLocator extends FileSystemResourceLocator {
+
+	@Override
+	default String getResourcePath() {
+		return LazyInitializer.initializeLazily(getResourcePathRef(), this::computeResourcePath);
+	}
+
+	AtomicReference<String> getResourcePathRef();
+
+	String computeResourcePath();
+
+}

+ 6 - 12
main/frontend-webdav/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavNode.java

@@ -16,12 +16,12 @@ import java.time.format.DateTimeFormatter;
 import java.time.temporal.Temporal;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
+import java.util.stream.Stream;
 
 import org.apache.jackrabbit.webdav.DavException;
 import org.apache.jackrabbit.webdav.DavResource;
-import org.apache.jackrabbit.webdav.DavResourceLocator;
-import org.apache.jackrabbit.webdav.DavServletResponse;
 import org.apache.jackrabbit.webdav.DavSession;
 import org.apache.jackrabbit.webdav.MultiStatusResponse;
 import org.apache.jackrabbit.webdav.lock.ActiveLock;
@@ -78,7 +78,7 @@ abstract class DavNode<T extends FileSystemResourceLocator> implements DavResour
 	}
 
 	@Override
-	public DavResourceLocator getLocator() {
+	public FileSystemResourceLocator getLocator() {
 		return node;
 	}
 
@@ -202,7 +202,7 @@ abstract class DavNode<T extends FileSystemResourceLocator> implements DavResour
 
 	@Override
 	public boolean isLockable(Type type, Scope scope) {
-		return true;
+		return Type.WRITE.equals(type) && Scope.EXCLUSIVE.equals(scope) || Scope.SHARED.equals(scope);
 	}
 
 	@Override
@@ -218,18 +218,12 @@ abstract class DavNode<T extends FileSystemResourceLocator> implements DavResour
 	@Override
 	public ActiveLock[] getLocks() {
 		final ActiveLock exclusiveWriteLock = getLock(Type.WRITE, Scope.EXCLUSIVE);
-		if (exclusiveWriteLock != null) {
-			return new ActiveLock[] {exclusiveWriteLock};
-		} else {
-			return new ActiveLock[0];
-		}
+		final ActiveLock sharedWriteLock = getLock(Type.WRITE, Scope.SHARED);
+		return Stream.of(exclusiveWriteLock, sharedWriteLock).filter(Objects::nonNull).toArray(ActiveLock[]::new);
 	}
 
 	@Override
 	public ActiveLock lock(LockInfo reqLockInfo) throws DavException {
-		if (Scope.SHARED.equals(reqLockInfo.getScope())) {
-			throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "Only exclusive write locks supported.");
-		}
 		return lockManager.createLock(reqLockInfo, this);
 	}
 

+ 101 - 0
main/frontend-webdav/src/main/java/org/cryptomator/webdav/jackrabbitservlet/ExclusiveSharedLock.java

@@ -0,0 +1,101 @@
+package org.cryptomator.webdav.jackrabbitservlet;
+
+import org.apache.jackrabbit.webdav.DavConstants;
+import org.apache.jackrabbit.webdav.lock.AbstractActiveLock;
+import org.apache.jackrabbit.webdav.lock.LockInfo;
+import org.apache.jackrabbit.webdav.lock.Scope;
+import org.apache.jackrabbit.webdav.lock.Type;
+
+public class ExclusiveSharedLock extends AbstractActiveLock {
+
+	private final String token;
+	private final Type type;
+	private final Scope scope;
+	private String owner;
+	private boolean isDeep = true; // deep by default
+	private long expirationTime = DavConstants.INFINITE_TIMEOUT; // never expires by default;
+
+	ExclusiveSharedLock(String token, LockInfo lockInfo) {
+		this.token = token;
+		this.type = lockInfo.getType();
+		this.scope = lockInfo.getScope();
+		this.owner = lockInfo.getOwner();
+		this.isDeep = lockInfo.isDeep();
+		setTimeout(lockInfo.getTimeout());
+	}
+
+	@Override
+	public boolean isLockedByToken(String lockToken) {
+		return token.equals(lockToken);
+	}
+
+	@Override
+	public boolean isExpired() {
+		return System.currentTimeMillis() > expirationTime;
+	}
+
+	@Override
+	public String getToken() {
+		return token;
+	}
+
+	@Override
+	public String getOwner() {
+		return owner;
+	}
+
+	@Override
+	public void setOwner(String owner) {
+		this.owner = owner;
+	}
+
+	@Override
+	public long getTimeout() {
+		return expirationTime - System.currentTimeMillis();
+	}
+
+	@Override
+	public void setTimeout(long timeout) {
+		if (timeout > 0) {
+			expirationTime = System.currentTimeMillis() + timeout;
+		}
+	}
+
+	@Override
+	public boolean isDeep() {
+		return isDeep;
+	}
+
+	@Override
+	public void setIsDeep(boolean isDeep) {
+		this.isDeep = isDeep;
+	}
+
+	@Override
+	public Type getType() {
+		return type;
+	}
+
+	@Override
+	public Scope getScope() {
+		return scope;
+	}
+
+	/* HASHCODE / EQUALS */
+
+	@Override
+	public int hashCode() {
+		return getToken().hashCode();
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (obj instanceof ExclusiveSharedLock) {
+			ExclusiveSharedLock other = (ExclusiveSharedLock) obj;
+			return this.getToken().equals(other.getToken());
+		} else {
+			return false;
+		}
+	}
+
+}

+ 183 - 0
main/frontend-webdav/src/main/java/org/cryptomator/webdav/jackrabbitservlet/ExclusiveSharedLockManager.java

@@ -0,0 +1,183 @@
+package org.cryptomator.webdav.jackrabbitservlet;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.apache.jackrabbit.webdav.DavConstants;
+import org.apache.jackrabbit.webdav.DavException;
+import org.apache.jackrabbit.webdav.DavResource;
+import org.apache.jackrabbit.webdav.DavServletResponse;
+import org.apache.jackrabbit.webdav.lock.ActiveLock;
+import org.apache.jackrabbit.webdav.lock.LockInfo;
+import org.apache.jackrabbit.webdav.lock.LockManager;
+import org.apache.jackrabbit.webdav.lock.Scope;
+import org.apache.jackrabbit.webdav.lock.Type;
+import org.cryptomator.filesystem.jackrabbit.FileSystemResourceLocator;
+import org.cryptomator.filesystem.jackrabbit.FolderLocator;
+
+public class ExclusiveSharedLockManager implements LockManager {
+
+	private final ConcurrentMap<FileSystemResourceLocator, Map<String, ActiveLock>> lockedResources = new ConcurrentHashMap<>();
+
+	@Override
+	public ActiveLock createLock(LockInfo lockInfo, DavResource resource) throws DavException {
+		Objects.requireNonNull(lockInfo);
+		Objects.requireNonNull(resource);
+		if (resource instanceof DavNode) {
+			return createLockInternal(lockInfo, (DavNode<?>) resource);
+		} else {
+			throw new IllegalArgumentException("Unsupported resource type " + resource.getClass());
+		}
+	}
+
+	private synchronized ActiveLock createLockInternal(LockInfo lockInfo, DavNode<?> resource) throws DavException {
+		FileSystemResourceLocator locator = resource.getLocator();
+		removedExpiredLocksInLocatorHierarchy(locator);
+
+		ActiveLock existingExclusiveLock = getLock(lockInfo.getType(), Scope.EXCLUSIVE, resource);
+		ActiveLock existingSharedLock = getLock(lockInfo.getType(), Scope.SHARED, resource);
+		boolean hasExclusiveLock = existingExclusiveLock != null;
+		boolean hasSharedLock = existingSharedLock != null;
+		boolean isLocked = hasExclusiveLock || hasSharedLock;
+		if ((Scope.EXCLUSIVE.equals(lockInfo.getScope()) && isLocked) || (Scope.SHARED.equals(lockInfo.getScope()) && hasExclusiveLock)) {
+			throw new DavException(DavServletResponse.SC_LOCKED, "Resource already locked.");
+		}
+
+		for (Entry<FileSystemResourceLocator, Map<String, ActiveLock>> entry : lockedResources.entrySet()) {
+			final FileSystemResourceLocator entryLocator = entry.getKey();
+			final Collection<ActiveLock> entryLocks = entry.getValue().values();
+			if (isAncestor(entryLocator, locator) && isAffectedByParentLocks(lockInfo, locator, entryLocks, entryLocator)) {
+				throw new DavException(DavServletResponse.SC_LOCKED, "Parent resource already locked. " + entryLocator);
+			} else if (isAncestor(locator, entryLocator) && isAffectedByChildLocks(lockInfo, locator, entryLocks, entryLocator)) {
+				throw new DavException(DavServletResponse.SC_CONFLICT, "Subresource already locked. " + entryLocator);
+			}
+		}
+
+		String token = DavConstants.OPAQUE_LOCK_TOKEN_PREFIX + UUID.randomUUID();
+		return lockedResources.computeIfAbsent(locator, loc -> new HashMap<>()).computeIfAbsent(token, t -> new ExclusiveSharedLock(t, lockInfo));
+	}
+
+	private void removedExpiredLocksInLocatorHierarchy(FileSystemResourceLocator locator) {
+		lockedResources.getOrDefault(locator, Collections.emptyMap()).values().removeIf(ActiveLock::isExpired);
+		locator.parent().ifPresent(this::removedExpiredLocksInLocatorHierarchy);
+	}
+
+	private boolean isAncestor(FileSystemResourceLocator parent, FileSystemResourceLocator child) {
+		if (parent instanceof FolderLocator) {
+			FolderLocator folder = (FolderLocator) parent;
+			return folder.isAncestorOf(child);
+		} else {
+			return false;
+		}
+	}
+
+	private boolean isAffectedByParentLocks(LockInfo childLockInfo, FileSystemResourceLocator childLocator, Collection<ActiveLock> parentLocks, FileSystemResourceLocator parentLocator) {
+		assert childLocator.parent().isPresent();
+		for (ActiveLock lock : parentLocks) {
+			if (Scope.SHARED.equals(childLockInfo.getScope()) && Scope.SHARED.equals(lock.getScope())) {
+				continue;
+			} else if (lock.isDeep() || childLocator.parent().get().equals(parentLocator)) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	private boolean isAffectedByChildLocks(LockInfo parentLockInfo, FileSystemResourceLocator parentLocator, Collection<ActiveLock> childLocks, FileSystemResourceLocator childLocator) {
+		assert childLocator.parent().isPresent();
+		for (ActiveLock lock : childLocks) {
+			if (Scope.SHARED.equals(lock.getScope()) && Scope.SHARED.equals(parentLockInfo.getScope())) {
+				continue;
+			} else if (parentLockInfo.isDeep() || childLocator.parent().get().equals(parentLocator)) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	@Override
+	public ActiveLock refreshLock(LockInfo lockInfo, String lockToken, DavResource resource) throws DavException {
+		ActiveLock lock = getLock(lockInfo.getType(), lockInfo.getScope(), resource);
+		if (lock == null) {
+			throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED);
+		} else if (!lock.getToken().equals(lockToken)) {
+			throw new DavException(DavServletResponse.SC_LOCKED);
+		}
+		lock.setTimeout(lockInfo.getTimeout());
+		return lock;
+	}
+
+	@Override
+	public synchronized void releaseLock(String lockToken, DavResource resource) throws DavException {
+		if (resource instanceof DavNode) {
+			try {
+				releaseLockInternal(lockToken, (DavNode<?>) resource);
+			} catch (UncheckedDavException e) {
+				throw e.toDavException();
+			}
+		} else {
+			throw new IllegalArgumentException("Unsupported resource type " + resource.getClass());
+		}
+	}
+
+	private synchronized void releaseLockInternal(String lockToken, DavNode<?> resource) throws UncheckedDavException {
+		lockedResources.compute(resource.getLocator(), (loc, locks) -> {
+			if (locks == null || locks.isEmpty()) {
+				// no lock exists, nothing needs to change.
+				return null;
+			} else if (!locks.containsKey(lockToken)) {
+				throw new UncheckedDavException(DavServletResponse.SC_LOCKED, "Resource locked with different token.");
+			} else {
+				locks.remove(lockToken);
+				return locks.isEmpty() ? null : locks;
+			}
+		});
+	}
+
+	@Override
+	public ActiveLock getLock(Type type, Scope scope, DavResource resource) {
+		if (resource instanceof DavNode) {
+			return getLockInternal(type, scope, ((DavNode<?>) resource).getLocator());
+		} else {
+			throw new IllegalArgumentException("Unsupported resource type " + resource.getClass());
+		}
+	}
+
+	private ActiveLock getLockInternal(Type type, Scope scope, FileSystemResourceLocator locator) {
+		// try to find a lock directly on this resource:
+		if (lockedResources.containsKey(locator)) {
+			for (ActiveLock lock : lockedResources.get(locator).values()) {
+				if (type.equals(lock.getType()) && scope.equals(lock.getScope())) {
+					return lock;
+				}
+			}
+		}
+		// or otherwise look for parent locks:
+		if (locator.parent().isPresent()) {
+			return getLockInternal(type, scope, locator.parent().get());
+		} else {
+			return null;
+		}
+	}
+
+	@Override
+	public boolean hasLock(String lockToken, DavResource resource) {
+		if (resource instanceof DavNode) {
+			return hasLockInternal(lockToken, (DavNode<?>) resource);
+		} else {
+			throw new IllegalArgumentException("Unsupported resource type " + resource.getClass());
+		}
+	}
+
+	private boolean hasLockInternal(String lockToken, DavNode<?> resource) {
+		return lockedResources.getOrDefault(resource.getLocator(), Collections.emptyMap()).containsKey(lockToken);
+	}
+
+}

+ 1 - 2
main/frontend-webdav/src/main/java/org/cryptomator/webdav/jackrabbitservlet/FilesystemResourceFactory.java

@@ -16,7 +16,6 @@ import org.apache.jackrabbit.webdav.DavServletRequest;
 import org.apache.jackrabbit.webdav.DavServletResponse;
 import org.apache.jackrabbit.webdav.DavSession;
 import org.apache.jackrabbit.webdav.lock.LockManager;
-import org.apache.jackrabbit.webdav.lock.SimpleLockManager;
 import org.cryptomator.filesystem.jackrabbit.FileLocator;
 import org.cryptomator.filesystem.jackrabbit.FolderLocator;
 
@@ -25,7 +24,7 @@ class FilesystemResourceFactory implements DavResourceFactory {
 	private final LockManager lockManager;
 
 	public FilesystemResourceFactory() {
-		this.lockManager = new SimpleLockManager();
+		this.lockManager = new ExclusiveSharedLockManager();
 	}
 
 	@Override

+ 22 - 0
main/frontend-webdav/src/main/java/org/cryptomator/webdav/jackrabbitservlet/UncheckedDavException.java

@@ -0,0 +1,22 @@
+package org.cryptomator.webdav.jackrabbitservlet;
+
+import org.apache.jackrabbit.webdav.DavException;
+
+public class UncheckedDavException extends RuntimeException {
+
+	private final int errorCode;
+
+	public UncheckedDavException(int errorCode, String message) {
+		this(errorCode, message, null);
+	}
+
+	public UncheckedDavException(int errorCode, String message, Throwable cause) {
+		super(message, cause);
+		this.errorCode = errorCode;
+	}
+
+	public DavException toDavException() {
+		return new DavException(errorCode, getMessage(), getCause(), null);
+	}
+
+}

+ 14 - 10
main/frontend-webdav/src/main/java/org/cryptomator/webdav/jackrabbitservlet/WebDavServlet.java

@@ -78,42 +78,46 @@ public class WebDavServlet extends AbstractWebdavServlet {
 
 	@Override
 	protected int validateDestination(DavResource destResource, WebdavRequest request, boolean checkHeader) throws DavException {
-		if (destResource.hasLock(Type.WRITE, Scope.EXCLUSIVE) && (request.getHeader(HEADER_IF) == null || !request.matchesIfHeader(destResource))) {
-			throw new DavException(DavServletResponse.SC_LOCKED, "The destination resource was locked");
+		if (isLocked(destResource) && (request.getHeader(HEADER_IF) == null || !request.matchesIfHeader(destResource))) {
+			throw new DavException(DavServletResponse.SC_LOCKED, "The destination resource is locked");
 		}
 		return super.validateDestination(destResource, request, checkHeader);
 	}
 
 	@Override
 	protected void doPut(WebdavRequest request, WebdavResponse response, DavResource resource) throws IOException, DavException {
-		if (resource.hasLock(Type.WRITE, Scope.EXCLUSIVE) && (request.getHeader(HEADER_IF) == null || !request.matchesIfHeader(resource))) {
-			throw new DavException(DavServletResponse.SC_LOCKED, "The resource was locked");
+		if (isLocked(resource) && (request.getHeader(HEADER_IF) == null || !request.matchesIfHeader(resource))) {
+			throw new DavException(DavServletResponse.SC_LOCKED, "The resource is locked");
 		}
 		super.doPut(request, response, resource);
 	}
 
 	@Override
 	protected void doDelete(WebdavRequest request, WebdavResponse response, DavResource resource) throws IOException, DavException {
-		if (resource.hasLock(Type.WRITE, Scope.EXCLUSIVE) && (request.getHeader(HEADER_IF) == null || !request.matchesIfHeader(resource))) {
-			throw new DavException(DavServletResponse.SC_LOCKED, "The resource was locked");
+		if (isLocked(resource) && (request.getHeader(HEADER_IF) == null || !request.matchesIfHeader(resource))) {
+			throw new DavException(DavServletResponse.SC_LOCKED, "The resource is locked");
 		}
 		super.doDelete(request, response, resource);
 	}
 
 	@Override
 	protected void doMove(WebdavRequest request, WebdavResponse response, DavResource resource) throws IOException, DavException {
-		if (resource.hasLock(Type.WRITE, Scope.EXCLUSIVE) && (request.getHeader(HEADER_IF) == null || !request.matchesIfHeader(resource))) {
-			throw new DavException(DavServletResponse.SC_LOCKED, "The source resource was locked");
+		if (isLocked(resource) && (request.getHeader(HEADER_IF) == null || !request.matchesIfHeader(resource))) {
+			throw new DavException(DavServletResponse.SC_LOCKED, "The source resource is locked");
 		}
 		super.doMove(request, response, resource);
 	}
 
 	@Override
 	protected void doPropPatch(WebdavRequest request, WebdavResponse response, DavResource resource) throws IOException, DavException {
-		if (resource.hasLock(Type.WRITE, Scope.EXCLUSIVE) && (request.getHeader(HEADER_IF) == null || !request.matchesIfHeader(resource))) {
-			throw new DavException(DavServletResponse.SC_LOCKED, "The resource was locked");
+		if (isLocked(resource) && (request.getHeader(HEADER_IF) == null || !request.matchesIfHeader(resource))) {
+			throw new DavException(DavServletResponse.SC_LOCKED, "The resource is locked");
 		}
 		super.doPropPatch(request, response, resource);
 	}
 
+	private boolean isLocked(DavResource resource) {
+		return resource.hasLock(Type.WRITE, Scope.EXCLUSIVE) || resource.hasLock(Type.WRITE, Scope.SHARED);
+	}
+
 }