소스 검색

first experiments with jackrabbit-filesystem-adapter

Sebastian Stenzel 9 년 전
부모
커밋
e67c8f2816

+ 78 - 0
main/jackrabbit-filesystem-adapter/pom.xml

@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Copyright (c) 2015 Sebastian Stenzel
+  This file is licensed under the terms of the MIT license.
+  See the LICENSE.txt file for more info.
+  
+  Contributors:
+      Sebastian Stenzel - initial API and implementation
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>org.cryptomator</groupId>
+		<artifactId>main</artifactId>
+		<version>0.11.0-SNAPSHOT</version>
+	</parent>
+	<artifactId>jackrabbit-filesystem-adapter</artifactId>
+	<name>Jackrabbit filesystem-WebDAV-adapter</name>
+	<description>WebDAV servlet based on Apache Jackrabbit serving files from a filesystem</description>
+
+	<properties>
+		<jackrabbit.version>2.11.0</jackrabbit.version>
+	</properties>
+
+	<dependencies>
+		<!-- Filesystem -->
+		<dependency>
+			<groupId>org.cryptomator</groupId>
+			<artifactId>filesystem-api</artifactId>
+		</dependency>
+
+		<!-- Jackrabbit -->
+		<dependency>
+			<groupId>org.apache.jackrabbit</groupId>
+			<artifactId>jackrabbit-webdav</artifactId>
+			<version>${jackrabbit.version}</version>
+		</dependency>
+		
+		<!-- Commons -->
+		<dependency>
+			<groupId>org.apache.commons</groupId>
+			<artifactId>commons-lang3</artifactId>
+		</dependency>
+		
+		<!-- Test -->
+		<dependency>
+			<groupId>org.eclipse.jetty</groupId>
+			<artifactId>jetty-server</artifactId>
+			<version>9.3.3.v20150827</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.eclipse.jetty</groupId>
+			<artifactId>jetty-webapp</artifactId>
+			<version>9.3.3.v20150827</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>commons-httpclient</groupId>
+			<artifactId>commons-httpclient</artifactId>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.cryptomator</groupId>
+			<artifactId>filesystem-inmemory</artifactId>
+			<scope>test</scope>
+		</dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.jacoco</groupId>
+				<artifactId>jacoco-maven-plugin</artifactId>
+			</plugin>
+		</plugins>
+	</build>
+</project>

+ 98 - 0
main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbit/DavFile.java

@@ -0,0 +1,98 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Sebastian Stenzel and others.
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ *
+ * Contributors:
+ *     Sebastian Stenzel - initial API and implementation
+ *******************************************************************************/
+package org.cryptomator.webdav.jackrabbit;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.WritableByteChannel;
+import java.time.Instant;
+
+import org.apache.jackrabbit.webdav.DavException;
+import org.apache.jackrabbit.webdav.DavResource;
+import org.apache.jackrabbit.webdav.DavResourceIterator;
+import org.apache.jackrabbit.webdav.DavResourceLocator;
+import org.apache.jackrabbit.webdav.DavSession;
+import org.apache.jackrabbit.webdav.io.InputContext;
+import org.apache.jackrabbit.webdav.io.OutputContext;
+import org.apache.jackrabbit.webdav.lock.LockManager;
+import org.cryptomator.filesystem.File;
+import org.cryptomator.filesystem.ReadableFile;
+import org.cryptomator.filesystem.WritableFile;
+
+class DavFile extends DavNode<File> {
+
+	public DavFile(FilesystemResourceFactory factory, LockManager lockManager, DavSession session, DavResourceLocator locator, File node) {
+		super(factory, lockManager, session, locator, node);
+	}
+
+	@Override
+	public boolean isCollection() {
+		return false;
+	}
+
+	@Override
+	public void spool(OutputContext outputContext) throws IOException {
+		outputContext.setModificationTime(node.lastModified().toEpochMilli());
+		if (!outputContext.hasStream()) {
+			return;
+		}
+		try (ReadableFile src = node.openReadable(); WritableByteChannel dst = Channels.newChannel(outputContext.getOutputStream())) {
+			// TODO filesize before sending content
+			outputContext.setContentLength(-1l);
+			ByteBuffer buf = ByteBuffer.allocate(1337);
+			do {
+				buf.clear();
+				src.read(buf);
+				buf.flip();
+			} while (dst.write(buf) > 0);
+		}
+	}
+
+	@Override
+	public void addMember(DavResource resource, InputContext inputContext) throws DavException {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public DavResourceIterator getMembers() {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public void removeMember(DavResource member) throws DavException {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public void move(DavResource destination) throws DavException {
+		// TODO Auto-generated method stub
+
+	}
+
+	@Override
+	public void copy(DavResource destination, boolean shallow) throws DavException {
+		// TODO Auto-generated method stub
+
+	}
+
+	@Override
+	protected void setModificationTime(Instant instant) {
+		try (WritableFile writable = node.openWritable()) {
+			writable.setLastModified(instant);
+		}
+	}
+
+	@Override
+	protected void setCreationTime(Instant instant) {
+		// TODO Auto-generated method stub
+
+	}
+
+}

+ 104 - 0
main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbit/DavFolder.java

@@ -0,0 +1,104 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Sebastian Stenzel and others.
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ *
+ * Contributors:
+ *     Sebastian Stenzel - initial API and implementation
+ *******************************************************************************/
+package org.cryptomator.webdav.jackrabbit;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.jackrabbit.webdav.DavException;
+import org.apache.jackrabbit.webdav.DavResource;
+import org.apache.jackrabbit.webdav.DavResourceIterator;
+import org.apache.jackrabbit.webdav.DavResourceIteratorImpl;
+import org.apache.jackrabbit.webdav.DavResourceLocator;
+import org.apache.jackrabbit.webdav.DavSession;
+import org.apache.jackrabbit.webdav.io.InputContext;
+import org.apache.jackrabbit.webdav.io.OutputContext;
+import org.apache.jackrabbit.webdav.lock.LockManager;
+import org.apache.jackrabbit.webdav.property.DavPropertyName;
+import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
+import org.apache.jackrabbit.webdav.property.ResourceType;
+import org.cryptomator.filesystem.File;
+import org.cryptomator.filesystem.Folder;
+
+class DavFolder extends DavNode<Folder> {
+
+	public DavFolder(FilesystemResourceFactory factory, LockManager lockManager, DavSession session, DavResourceLocator locator, Folder folder) {
+		super(factory, lockManager, session, locator, folder);
+		properties.add(new ResourceType(ResourceType.COLLECTION));
+		properties.add(new DefaultDavProperty<Integer>(DavPropertyName.ISCOLLECTION, 1));
+	}
+
+	@Override
+	public boolean isCollection() {
+		return true;
+	}
+
+	@Override
+	public void spool(OutputContext outputContext) throws IOException {
+		// no-op
+	}
+
+	@Override
+	public void addMember(DavResource resource, InputContext inputContext) throws DavException {
+		// TODO Auto-generated method stub
+
+	}
+
+	@Override
+	public DavResourceIterator getMembers() {
+		final Stream<DavFolder> folders = node.folders().map(this::getMemberFolder);
+		final Stream<DavFile> files = node.files().map(this::getMemberFile);
+		return new DavResourceIteratorImpl(Stream.concat(folders, files).collect(Collectors.toList()));
+	}
+
+	private DavFolder getMemberFolder(Folder memberFolder) {
+		final String subFolderResourcePath = locator.getResourcePath() + memberFolder.name() + '/';
+		final DavResourceLocator subFolderLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), subFolderResourcePath);
+		return factory.createFolder(memberFolder, subFolderLocator, session);
+	}
+
+	private DavFile getMemberFile(File memberFile) {
+		final String subFolderResourcePath = locator.getResourcePath() + memberFile.name();
+		final DavResourceLocator subFolderLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), subFolderResourcePath);
+		return factory.createFile(memberFile, subFolderLocator, session);
+	}
+
+	@Override
+	public void removeMember(DavResource member) throws DavException {
+		// TODO Auto-generated method stub
+
+	}
+
+	@Override
+	public void move(DavResource destination) throws DavException {
+		// TODO Auto-generated method stub
+
+	}
+
+	@Override
+	public void copy(DavResource destination, boolean shallow) throws DavException {
+		// TODO Auto-generated method stub
+
+	}
+
+	@Override
+	protected void setModificationTime(Instant instant) {
+		// TODO Auto-generated method stub
+
+	}
+
+	@Override
+	protected void setCreationTime(Instant instant) {
+		// TODO Auto-generated method stub
+
+	}
+
+}

+ 228 - 0
main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbit/DavNode.java

@@ -0,0 +1,228 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Sebastian Stenzel and others.
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ *
+ * Contributors:
+ *     Sebastian Stenzel - initial API and implementation
+ *******************************************************************************/
+package org.cryptomator.webdav.jackrabbit;
+
+import java.io.UncheckedIOException;
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.jackrabbit.webdav.DavException;
+import org.apache.jackrabbit.webdav.DavResource;
+import org.apache.jackrabbit.webdav.DavResourceLocator;
+import org.apache.jackrabbit.webdav.DavSession;
+import org.apache.jackrabbit.webdav.MultiStatusResponse;
+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.apache.jackrabbit.webdav.property.DavProperty;
+import org.apache.jackrabbit.webdav.property.DavPropertyName;
+import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
+import org.apache.jackrabbit.webdav.property.DavPropertySet;
+import org.apache.jackrabbit.webdav.property.PropEntry;
+import org.cryptomator.filesystem.Node;
+
+abstract class DavNode<T extends Node> implements DavResource {
+
+	private static final String DAV_COMPLIANCE_CLASSES = "1, 2";
+	private static final String[] DAV_CREATIONDATE_PROPNAMES = {DavPropertyName.CREATIONDATE.getName(), "Win32CreationTime"};
+	private static final String[] DAV_MODIFIEDDATE_PROPNAMES = {DavPropertyName.GETLASTMODIFIED.getName(), "Win32LastModifiedTime"};
+
+	protected final FilesystemResourceFactory factory;
+	protected final LockManager lockManager;
+	protected final DavSession session;
+	protected final DavResourceLocator locator;
+	protected final T node;
+	protected final DavPropertySet properties;
+
+	public DavNode(FilesystemResourceFactory factory, LockManager lockManager, DavSession session, DavResourceLocator locator, T node) {
+		this.factory = factory;
+		this.lockManager = lockManager;
+		this.session = session;
+		this.locator = locator;
+		this.node = node;
+		this.properties = new DavPropertySet();
+	}
+
+	@Override
+	public String getComplianceClass() {
+		return DAV_COMPLIANCE_CLASSES;
+	}
+
+	@Override
+	public String getSupportedMethods() {
+		return METHODS;
+	}
+
+	@Override
+	public boolean exists() {
+		return node.exists();
+	}
+
+	@Override
+	public String getDisplayName() {
+		return node.name();
+	}
+
+	@Override
+	public DavResourceLocator getLocator() {
+		return locator;
+	}
+
+	@Override
+	public String getResourcePath() {
+		return locator.getResourcePath();
+	}
+
+	@Override
+	public String getHref() {
+		return locator.getHref(this.isCollection());
+	}
+
+	@Override
+	public long getModificationTime() {
+		try {
+			return node.lastModified().toEpochMilli();
+		} catch (UncheckedIOException e) {
+			return -1l;
+		}
+	}
+
+	protected abstract void setModificationTime(Instant instant);
+
+	protected abstract void setCreationTime(Instant instant);
+
+	@Override
+	public DavPropertyName[] getPropertyNames() {
+		return getProperties().getPropertyNames();
+	}
+
+	@Override
+	public DavProperty<?> getProperty(DavPropertyName name) {
+		return getProperties().get(name);
+	}
+
+	@Override
+	public DavPropertySet getProperties() {
+		return properties;
+	}
+
+	@Override
+	public void setProperty(DavProperty<?> property) throws DavException {
+		getProperties().add(property);
+
+		final String namespacelessPropertyName = property.getName().getName();
+		if (Arrays.asList(DAV_CREATIONDATE_PROPNAMES).contains(namespacelessPropertyName) && property.getValue() instanceof String) {
+			final String createDateStr = (String) property.getValue();
+			final Instant createTime = Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(createDateStr));
+			this.setCreationTime(createTime);
+		} else if (Arrays.asList(DAV_MODIFIEDDATE_PROPNAMES).contains(namespacelessPropertyName) && property.getValue() instanceof String) {
+			final String lastModifiedTimeStr = (String) property.getValue();
+			final Instant createTime = Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(lastModifiedTimeStr));
+			this.setCreationTime(createTime);
+		}
+	}
+
+	@Override
+	public void removeProperty(DavPropertyName propertyName) throws DavException {
+		getProperties().remove(propertyName);
+	}
+
+	@Override
+	public MultiStatusResponse alterProperties(List<? extends PropEntry> changeList) throws DavException {
+		final DavPropertyNameSet names = new DavPropertyNameSet();
+		for (final PropEntry entry : changeList) {
+			if (entry instanceof DavProperty) {
+				final DavProperty<?> prop = (DavProperty<?>) entry;
+				this.setProperty(prop);
+				names.add(prop.getName());
+			} else if (entry instanceof DavPropertyName) {
+				final DavPropertyName name = (DavPropertyName) entry;
+				this.removeProperty(name);
+				names.add(name);
+			}
+		}
+		return new MultiStatusResponse(this, names);
+	}
+
+	@Override
+	public DavResource getCollection() {
+		if (locator.isRootLocation()) {
+			return null;
+		}
+
+		final String parentResource = FilenameUtils.getPathNoEndSeparator(locator.getResourcePath());
+		final DavResourceLocator parentLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), parentResource);
+		try {
+			return factory.createResource(parentLocator, session);
+		} catch (DavException e) {
+			throw new IllegalStateException("Unable to get parent resource with path " + parentLocator.getResourcePath(), e);
+		}
+	}
+
+	@Override
+	public boolean isLockable(Type type, Scope scope) {
+		return true;
+	}
+
+	@Override
+	public boolean hasLock(Type type, Scope scope) {
+		return getLock(type, scope) != null;
+	}
+
+	@Override
+	public ActiveLock getLock(Type type, Scope scope) {
+		return lockManager.getLock(type, scope, this);
+	}
+
+	@Override
+	public ActiveLock[] getLocks() {
+		final ActiveLock exclusiveWriteLock = getLock(Type.WRITE, Scope.EXCLUSIVE);
+		if (exclusiveWriteLock != null) {
+			return new ActiveLock[] {exclusiveWriteLock};
+		} else {
+			return new ActiveLock[0];
+		}
+	}
+
+	@Override
+	public ActiveLock lock(LockInfo reqLockInfo) throws DavException {
+		return lockManager.createLock(reqLockInfo, this);
+	}
+
+	@Override
+	public ActiveLock refreshLock(LockInfo reqLockInfo, String lockToken) throws DavException {
+		return lockManager.refreshLock(reqLockInfo, lockToken, this);
+	}
+
+	@Override
+	public void unlock(String lockToken) throws DavException {
+		lockManager.releaseLock(lockToken, this);
+	}
+
+	@Override
+	public void addLockManager(LockManager lockmgr) {
+		throw new UnsupportedOperationException("Locks are managed");
+	}
+
+	@Override
+	public FilesystemResourceFactory getFactory() {
+		return factory;
+	}
+
+	@Override
+	public DavSession getSession() {
+		return session;
+	}
+
+}

+ 45 - 0
main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbit/DavSessionImpl.java

@@ -0,0 +1,45 @@
+/*******************************************************************************
+ * Copyright (c) 2014, 2015 Sebastian Stenzel
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ * 
+ * Contributors:
+ *     Sebastian Stenzel - initial API and implementation
+ ******************************************************************************/
+package org.cryptomator.webdav.jackrabbit;
+
+import java.util.HashSet;
+
+import org.apache.jackrabbit.webdav.DavSession;
+
+class DavSessionImpl implements DavSession {
+	
+	private final HashSet<String> lockTokens = new HashSet<String>();
+	private final HashSet<Object> references = new HashSet<Object>();
+
+	@Override
+	public void addReference(Object reference) {
+		references.add(reference);
+	}
+
+	@Override
+	public void removeReference(Object reference) {
+		references.remove(reference);
+	}
+
+	@Override
+	public void addLockToken(String token) {
+		lockTokens.add(token);
+	}
+
+	@Override
+	public String[] getLockTokens() {
+		return lockTokens.toArray(new String[lockTokens.size()]);
+	}
+
+	@Override
+	public void removeLockToken(String token) {
+		lockTokens.remove(token);
+	}
+
+}

+ 36 - 0
main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbit/DavSessionProviderImpl.java

@@ -0,0 +1,36 @@
+/*******************************************************************************
+ * Copyright (c) 2014, 2015 Sebastian Stenzel
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ * 
+ * Contributors:
+ *     Sebastian Stenzel - initial API and implementation
+ ******************************************************************************/
+package org.cryptomator.webdav.jackrabbit;
+
+import org.apache.jackrabbit.webdav.DavException;
+import org.apache.jackrabbit.webdav.DavSession;
+import org.apache.jackrabbit.webdav.DavSessionProvider;
+import org.apache.jackrabbit.webdav.WebdavRequest;
+
+class DavSessionProviderImpl implements DavSessionProvider {
+
+	@Override
+	public boolean attachSession(WebdavRequest request) throws DavException {
+		// every request gets a new session
+		final DavSession session = new DavSessionImpl();
+		session.addReference(request);
+		request.setDavSession(session);
+		return true;
+	}
+
+	@Override
+	public void releaseSession(WebdavRequest request) {
+		final DavSession session = request.getDavSession();
+		if (session != null) {
+			session.removeReference(request);
+			request.setDavSession(null);
+		}
+	}
+
+}

+ 92 - 0
main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbit/FilesystemResourceFactory.java

@@ -0,0 +1,92 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Sebastian Stenzel and others.
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ *
+ * Contributors:
+ *     Sebastian Stenzel - initial API and implementation
+ *******************************************************************************/
+package org.cryptomator.webdav.jackrabbit;
+
+import java.util.Arrays;
+import java.util.Iterator;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.jackrabbit.webdav.DavException;
+import org.apache.jackrabbit.webdav.DavResource;
+import org.apache.jackrabbit.webdav.DavResourceFactory;
+import org.apache.jackrabbit.webdav.DavResourceLocator;
+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.File;
+import org.cryptomator.filesystem.FileSystem;
+import org.cryptomator.filesystem.Folder;
+import org.cryptomator.filesystem.Node;
+
+class FilesystemResourceFactory implements DavResourceFactory {
+
+	private static final Class<Folder> FOLDER = Folder.class;
+	private static final Class<File> FILE = File.class;
+
+	private final FileSystem filesystem;
+	private final LockManager lockManager;
+
+	public FilesystemResourceFactory(FileSystem filesystem) {
+		this.filesystem = filesystem;
+		this.lockManager = new SimpleLockManager();
+	}
+
+	@Override
+	public DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException {
+		return createResource(locator, request.getDavSession());
+	}
+
+	@Override
+	public DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException {
+		final String path = locator.getResourcePath();
+		if (path.endsWith("/")) {
+			Folder folder = this.resolve(path, FOLDER);
+			return createFolder(folder, locator, session);
+		} else {
+			File file = this.resolve(path, FILE);
+			return createFile(file, locator, session);
+		}
+	}
+
+	DavFolder createFolder(Folder folder, DavResourceLocator locator, DavSession session) {
+		return new DavFolder(this, lockManager, session, locator, folder);
+	}
+
+	public DavFile createFile(File file, DavResourceLocator locator, DavSession session) {
+		return new DavFile(this, lockManager, session, locator, file);
+	}
+
+	private <T extends Node> T resolve(String path, Class<T> expectedNodeType) {
+		final String[] pathFragments = StringUtils.split(path, '/');
+		if (ArrayUtils.isEmpty(pathFragments)) {
+			assert expectedNodeType.isAssignableFrom(Folder.class);
+			return expectedNodeType.cast(filesystem);
+		} else {
+			return resolve(filesystem, Arrays.stream(pathFragments).iterator(), expectedNodeType);
+		}
+	}
+
+	private <T extends Node> T resolve(Folder parent, Iterator<String> pathIterator, Class<T> expectedNodeType) {
+		assert pathIterator.hasNext();
+		final String childName = pathIterator.next();
+		if (pathIterator.hasNext()) {
+			return resolve(parent.folder(childName), pathIterator, expectedNodeType);
+		} else if (expectedNodeType.isAssignableFrom(Folder.class)) {
+			return expectedNodeType.cast(parent.folder(childName));
+		} else if (expectedNodeType.isAssignableFrom(File.class)) {
+			return expectedNodeType.cast(parent.file(childName));
+		} else {
+			throw new IllegalArgumentException("Supported expectedNodeTypes are File or Folder.");
+		}
+	}
+
+}

+ 140 - 0
main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbit/IdentityLocatorFactory.java

@@ -0,0 +1,140 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Sebastian Stenzel and others.
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ *
+ * Contributors:
+ *     Sebastian Stenzel - initial API and implementation
+ *******************************************************************************/
+package org.cryptomator.webdav.jackrabbit;
+
+import java.net.URI;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.jackrabbit.webdav.DavLocatorFactory;
+import org.apache.jackrabbit.webdav.DavResourceLocator;
+import org.apache.jackrabbit.webdav.util.EncodeUtil;
+
+/**
+ * A LocatorFactory constructing Locators, whose {@link DavResourceLocator#getResourcePath() resourcePath} and {@link DavResourceLocator#getRepositoryPath() repositoryPath} are equal.
+ * These paths will be plain, case-sensitive, absolute, unencoded Strings with Unix-style path separators.
+ */
+class IdentityLocatorFactory implements DavLocatorFactory {
+
+	private final String pathPrefix;
+
+	public IdentityLocatorFactory(URI contextRootUri) {
+		this.pathPrefix = StringUtils.removeEnd(contextRootUri.toString(), "/");
+	}
+
+	@Override
+	public DavResourceLocator createResourceLocator(String prefix, String href) {
+		final String fullPrefix = StringUtils.removeEnd(prefix, "/");
+		final String remainingHref = StringUtils.removeStart(href, fullPrefix);
+		final String unencodedRemaingingHref = EncodeUtil.unescape(remainingHref);
+		assert unencodedRemaingingHref.startsWith("/");
+		return new IdentityLocator(unencodedRemaingingHref);
+	}
+
+	@Override
+	public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String resourcePath) {
+		assert resourcePath.startsWith("/");
+		return new IdentityLocator(resourcePath);
+	}
+
+	@Override
+	public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String path, boolean isResourcePath) {
+		assert path.startsWith("/");
+		return new IdentityLocator(path);
+	}
+
+	private class IdentityLocator implements DavResourceLocator {
+
+		private final String absPath;
+
+		private IdentityLocator(String absPath) {
+			assert absPath.startsWith("/");
+			this.absPath = FilenameUtils.normalize(absPath, true);
+		}
+
+		@Override
+		public String getPrefix() {
+			return pathPrefix;
+		}
+
+		@Override
+		public String getResourcePath() {
+			return absPath;
+		}
+
+		@Override
+		public String getWorkspacePath() {
+			return null;
+		}
+
+		@Override
+		public String getWorkspaceName() {
+			return null;
+		}
+
+		@Override
+		public boolean isSameWorkspace(DavResourceLocator locator) {
+			return false;
+		}
+
+		@Override
+		public boolean isSameWorkspace(String workspaceName) {
+			return false;
+		}
+
+		@Override
+		public String getHref(boolean isCollection) {
+			final String encodedResourcePath = EncodeUtil.escapePath(absPath);
+			if (isRootLocation()) {
+				return pathPrefix + "/";
+			} else {
+				assert isCollection ? encodedResourcePath.endsWith("/") : true;
+				return pathPrefix + encodedResourcePath;
+			}
+		}
+
+		@Override
+		public boolean isRootLocation() {
+			return "/".equals(absPath);
+		}
+
+		@Override
+		public DavLocatorFactory getFactory() {
+			return IdentityLocatorFactory.this;
+		}
+
+		@Override
+		public String getRepositoryPath() {
+			return absPath;
+		}
+
+		@Override
+		public String toString() {
+			return "Locator: " + absPath + " (Prefix: " + pathPrefix + ")";
+		}
+
+		@Override
+		public int hashCode() {
+			return absPath.hashCode();
+		}
+
+		@Override
+		public boolean equals(Object obj) {
+			if (obj instanceof IdentityLocator) {
+				final IdentityLocator other = (IdentityLocator) obj;
+				final boolean samePrefix = this.getPrefix() == null && other.getPrefix() == null || this.getPrefix().equals(other.getPrefix());
+				final boolean sameRelativeCleartextPath = this.absPath == null && other.absPath == null || this.absPath.equals(other.absPath);
+				return samePrefix && sameRelativeCleartextPath;
+			} else {
+				return false;
+			}
+		}
+	}
+
+}

+ 70 - 0
main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavServlet.java

@@ -0,0 +1,70 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Sebastian Stenzel and others.
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ *
+ * Contributors:
+ *     Sebastian Stenzel - initial API and implementation
+ *******************************************************************************/
+package org.cryptomator.webdav.jackrabbit;
+
+import java.net.URI;
+
+import org.apache.jackrabbit.webdav.DavLocatorFactory;
+import org.apache.jackrabbit.webdav.DavResource;
+import org.apache.jackrabbit.webdav.DavResourceFactory;
+import org.apache.jackrabbit.webdav.DavSessionProvider;
+import org.apache.jackrabbit.webdav.WebdavRequest;
+import org.apache.jackrabbit.webdav.server.AbstractWebdavServlet;
+import org.cryptomator.filesystem.FileSystem;
+
+public class WebDavServlet extends AbstractWebdavServlet {
+
+	private static final long serialVersionUID = -6632687979352625020L;
+
+	private final DavSessionProvider davSessionProvider;
+	private final DavLocatorFactory davLocatorFactory;
+	private final DavResourceFactory davResourceFactory;
+
+	public WebDavServlet(URI contextRootUri, FileSystem filesystem) {
+		davSessionProvider = new DavSessionProviderImpl();
+		davLocatorFactory = new IdentityLocatorFactory(contextRootUri);
+		davResourceFactory = new FilesystemResourceFactory(filesystem);
+	}
+
+	@Override
+	protected boolean isPreconditionValid(WebdavRequest request, DavResource resource) {
+		return !resource.exists() || request.matchesIfHeader(resource);
+	}
+
+	@Override
+	public DavSessionProvider getDavSessionProvider() {
+		return davSessionProvider;
+	}
+
+	@Override
+	public void setDavSessionProvider(DavSessionProvider davSessionProvider) {
+		throw new UnsupportedOperationException("Setting davSessionProvider not supported.");
+	}
+
+	@Override
+	public DavLocatorFactory getLocatorFactory() {
+		return davLocatorFactory;
+	}
+
+	@Override
+	public void setLocatorFactory(DavLocatorFactory locatorFactory) {
+		throw new UnsupportedOperationException("Setting locatorFactory not supported.");
+	}
+
+	@Override
+	public DavResourceFactory getResourceFactory() {
+		return davResourceFactory;
+	}
+
+	@Override
+	public void setResourceFactory(DavResourceFactory resourceFactory) {
+		throw new UnsupportedOperationException("Setting resourceFactory not supported.");
+	}
+
+}

+ 34 - 0
main/jackrabbit-filesystem-adapter/src/main/resources/log4j2.xml

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+  Copyright (c) 2014 Markus Kreusch
+  This file is licensed under the terms of the MIT license.
+  See the LICENSE.txt file for more info.
+  
+  Contributors:
+      Sebastian Stenzel - log4j config for WebDAV unit tests
+-->
+<Configuration status="WARN">
+
+	<Appenders>
+		<Console name="Console" target="SYSTEM_OUT">
+			<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
+			<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="ACCEPT" />
+		</Console>
+		<Console name="StdErr" target="SYSTEM_ERR">
+			<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
+			<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
+		</Console>
+	</Appenders>
+
+	<Loggers>
+		<!-- show our own debug messages: -->
+		<Logger name="org.cryptomator" level="DEBUG" />
+		<Logger name="org.eclipse.jetty.server.Server" level="DEBUG" />
+		<!-- mute dependencies: -->
+		<Root level="INFO">
+			<AppenderRef ref="Console" />
+			<AppenderRef ref="StdErr" />
+		</Root>
+	</Loggers>
+
+</Configuration>

+ 84 - 0
main/jackrabbit-filesystem-adapter/src/test/java/org/cryptomator/webdav/jackrabbit/InMemoryWebDavServer.java

@@ -0,0 +1,84 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Sebastian Stenzel and others.
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ *
+ * Contributors:
+ *     Sebastian Stenzel - initial API and implementation
+ *******************************************************************************/
+package org.cryptomator.webdav.jackrabbit;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import org.cryptomator.filesystem.FileSystem;
+import org.cryptomator.filesystem.FolderCreateMode;
+import org.cryptomator.filesystem.WritableFile;
+import org.cryptomator.filesystem.inmem.InMemoryFileSystem;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.handler.ContextHandlerCollection;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.eclipse.jetty.util.thread.ThreadPool;
+
+public class InMemoryWebDavServer {
+
+	private final Server server;
+	private final ServerConnector localConnector;
+	private final ContextHandlerCollection servletCollection;
+	private final FileSystem inMemoryFileSystem = new InMemoryFileSystem();
+
+	private InMemoryWebDavServer() {
+		final BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(10);
+		final ThreadPool tp = new QueuedThreadPool(4, 1, 1, queue);
+		server = new Server(tp);
+		localConnector = new ServerConnector(server);
+		localConnector.setHost("localhost");
+		localConnector.setPort(8080);
+		servletCollection = new ContextHandlerCollection();
+
+		URI servletContextRootUri;
+		try {
+			servletContextRootUri = new URI("http", null, "localhost", 8080, "/", null, null);
+		} catch (URISyntaxException e) {
+			throw new IllegalStateException(e);
+		}
+		final ServletContextHandler servletContext = new ServletContextHandler(servletCollection, "/", ServletContextHandler.SESSIONS);
+		final ServletHolder servletHolder = new ServletHolder("InMemory-WebDAV-Servlet", new WebDavServlet(servletContextRootUri, inMemoryFileSystem));
+		servletContext.addServlet(servletHolder, "/*");
+		servletCollection.mapContexts();
+
+		server.setConnectors(new Connector[] {localConnector});
+		server.setHandler(servletCollection);
+	}
+
+	private void start() throws Exception {
+		server.start();
+	}
+
+	private void stop() throws Exception {
+		server.stop();
+	}
+
+	public static void main(String[] args) throws Exception {
+		final InMemoryWebDavServer server = new InMemoryWebDavServer();
+
+		server.inMemoryFileSystem.folder("mamals").folder("cats").create(FolderCreateMode.INCLUDING_PARENTS);
+		server.inMemoryFileSystem.folder("mamals").folder("dogs").create(FolderCreateMode.INCLUDING_PARENTS);
+		try (WritableFile writable = server.inMemoryFileSystem.folder("mamals").folder("cats").file("Garfield.txt").openWritable()) {
+			writable.write(ByteBuffer.wrap("meow".getBytes()));
+		}
+
+		server.start();
+		System.out.println("Server started. Press any key to stop it...");
+		System.in.read();
+		server.stop();
+	}
+
+}

+ 1 - 0
main/pom.xml

@@ -222,6 +222,7 @@
 		<module>crypto-aes</module>
 		<module>core</module>
 		<module>ui</module>
+		<module>jackrabbit-filesystem-adapter</module>
 	</modules>
 
 	<profiles>