Commit Diff


commit - c4610f866124bd70c2c5eee691a35ecd94ab2487
commit + 09048cb93e22e7d59bcf356673986574a7979a30
blob - /dev/null
blob + 6ce51b79ebc9ab0be5de9ae2c702d8799a4835f6 (mode 644)
--- /dev/null
+++ modules/webdav/src/main/java/com/thinkberg/webdav/CopyHandler.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2007 Matthias L. Jugel.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.thinkberg.webdav;
+
+import org.apache.commons.vfs.FileObject;
+import org.apache.commons.vfs.FileSelectInfo;
+import org.apache.commons.vfs.FileSelector;
+import org.apache.commons.vfs.FileSystemException;
+
+/**
+ * @author Matthias L. Jugel
+ * @version $Id$
+ */
+public class CopyHandler extends CopyMoveBase {
+  protected void copyOrMove(FileObject object, FileObject target, final int depth) throws FileSystemException {
+    target.copyFrom(object, new FileSelector() {
+      public boolean includeFile(FileSelectInfo fileSelectInfo) throws Exception {
+        return fileSelectInfo.getDepth() <= depth;
+      }
+
+      public boolean traverseDescendents(FileSelectInfo fileSelectInfo) throws Exception {
+        return fileSelectInfo.getDepth() < depth;
+      }
+    });
+  }
+}
blob - /dev/null
blob + 673a2a9aef5ecaff2c19f0317fa21de8a46d2320 (mode 644)
--- /dev/null
+++ modules/webdav/src/main/java/com/thinkberg/webdav/CopyMoveBase.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2007 Matthias L. Jugel.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.thinkberg.webdav;
+
+import com.thinkberg.webdav.lock.LockException;
+import com.thinkberg.webdav.lock.LockManager;
+import com.thinkberg.webdav.vfs.VFSBackend;
+import org.apache.commons.vfs.FileObject;
+import org.apache.commons.vfs.FileSystemException;
+import org.apache.commons.vfs.FileType;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.text.ParseException;
+
+/**
+ * @author Matthias L. Jugel
+ * @version $Id$
+ */
+public abstract class CopyMoveBase extends WebdavHandler {
+
+  public void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
+    boolean overwrite = getOverwrite(request);
+    FileObject object = VFSBackend.resolveFile(request.getPathInfo());
+    FileObject targetObject = getDestination(request);
+
+
+    try {
+      final LockManager lockManager = LockManager.getInstance();
+      LockManager.EvaluationResult evaluation = lockManager.evaluateCondition(targetObject, getIf(request));
+      if (!evaluation.result) {
+        response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+        return;
+      }
+      if ("MOVE".equals(request.getMethod())) {
+        evaluation = lockManager.evaluateCondition(object, getIf(request));
+        if (!evaluation.result) {
+          response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+          return;
+        }
+      }
+    } catch (LockException e) {
+      response.sendError(SC_LOCKED);
+      return;
+    } catch (ParseException e) {
+      response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+      return;
+    }
+
+
+    if (null == targetObject) {
+      response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+      return;
+    }
+
+    if (object.equals(targetObject)) {
+      response.sendError(HttpServletResponse.SC_FORBIDDEN);
+      return;
+    }
+
+    if (targetObject.exists()) {
+      if (!overwrite) {
+        response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+        return;
+      }
+      response.setStatus(HttpServletResponse.SC_NO_CONTENT);
+    } else {
+      FileObject targetParent = targetObject.getParent();
+      if (!targetParent.exists() ||
+              !FileType.FOLDER.equals(targetParent.getType())) {
+        response.sendError(HttpServletResponse.SC_CONFLICT);
+      }
+      response.setStatus(HttpServletResponse.SC_CREATED);
+    }
+
+    copyOrMove(object, targetObject, getDepth(request));
+  }
+
+  protected abstract void copyOrMove(FileObject object, FileObject target, int depth) throws FileSystemException;
+
+}
blob - /dev/null
blob + 09b60a7b33e721815fda022a1e7167429e306ca8 (mode 644)
--- /dev/null
+++ modules/webdav/src/main/java/com/thinkberg/webdav/DeleteHandler.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2007 Matthias L. Jugel.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.thinkberg.webdav;
+
+import com.thinkberg.webdav.lock.LockException;
+import com.thinkberg.webdav.lock.LockManager;
+import com.thinkberg.webdav.vfs.VFSBackend;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.commons.vfs.FileObject;
+import org.apache.commons.vfs.FileSelectInfo;
+import org.apache.commons.vfs.FileSelector;
+import org.mortbay.jetty.Request;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.text.ParseException;
+
+/**
+ * @author Matthias L. Jugel
+ * @version $Id$
+ */
+public class DeleteHandler extends WebdavHandler {
+  private static final Log LOG = LogFactory.getLog(DeleteHandler.class);
+
+  private final static FileSelector ALL_FILES_SELECTOR = new FileSelector() {
+    public boolean includeFile(FileSelectInfo fileSelectInfo) throws Exception {
+      return true;
+    }
+
+    public boolean traverseDescendents(FileSelectInfo fileSelectInfo) throws Exception {
+      return true;
+    }
+  };
+
+  public void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
+    FileObject object = VFSBackend.resolveFile(request.getPathInfo());
+    if (request instanceof Request) {
+      String fragment = ((Request) request).getUri().getFragment();
+      if (fragment != null) {
+        response.sendError(HttpServletResponse.SC_FORBIDDEN);
+        return;
+      }
+    }
+
+    try {
+      if (!LockManager.getInstance().evaluateCondition(object, getIf(request)).result) {
+        response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+        return;
+      }
+    } catch (LockException e) {
+      response.sendError(SC_LOCKED);
+      return;
+    } catch (ParseException e) {
+      response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+      return;
+    }
+
+    if (object.exists()) {
+      int deletedObjects = object.delete(ALL_FILES_SELECTOR);
+      LOG.debug("deleted " + deletedObjects + " objects");
+      if (deletedObjects > 0) {
+        response.setStatus(HttpServletResponse.SC_OK);
+      } else {
+        response.sendError(HttpServletResponse.SC_FORBIDDEN);
+      }
+    } else {
+      response.sendError(HttpServletResponse.SC_NOT_FOUND);
+    }
+  }
+}
blob - /dev/null
blob + da2dfd1e8b0b8b132cea073670f143a461a16b91 (mode 644)
--- /dev/null
+++ modules/webdav/src/main/java/com/thinkberg/webdav/vfs/DepthFileSelector.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2007 Matthias L. Jugel.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.thinkberg.webdav.vfs;
+
+import org.apache.commons.vfs.FileSelectInfo;
+import org.apache.commons.vfs.FileSelector;
+
+/**
+ * A file selector that operates depth of the directory structure and will
+ * select all files up to and including the depth given in the constructor.
+ *
+ * @author Matthias L. Jugel
+ * @version $Id$
+ */
+public class DepthFileSelector implements FileSelector {
+  private final int maxDepth;
+  private final int minDepth;
+
+  /**
+   * Create a file selector that will select ALL files.
+   */
+  public DepthFileSelector() {
+    this(0, Integer.MAX_VALUE);
+  }
+
+  /**
+   * Create a file selector that will select all files up to and including
+   * the directory depth.
+   *
+   * @param depth the maximum depth
+   */
+  public DepthFileSelector(int depth) {
+    this(0, depth);
+  }
+
+  public DepthFileSelector(int min, int max) {
+    minDepth = min;
+    maxDepth = max;
+  }
+
+  public boolean includeFile(FileSelectInfo fileSelectInfo) throws Exception {
+    int depth = fileSelectInfo.getDepth();
+    return depth >= minDepth && depth <= maxDepth;
+  }
+
+  public boolean traverseDescendents(FileSelectInfo fileSelectInfo) throws Exception {
+    return fileSelectInfo.getDepth() < maxDepth;
+  }
+}
blob - /dev/null
blob + ba1bb8e542feecaca819e506aaa13f7b8be94cfa (mode 644)
--- /dev/null
+++ modules/webdav/src/main/java/com/thinkberg/webdav/GetHandler.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2007 Matthias L. Jugel.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.thinkberg.webdav;
+
+import com.thinkberg.webdav.vfs.VFSBackend;
+import org.apache.commons.vfs.FileContent;
+import org.apache.commons.vfs.FileObject;
+import org.apache.commons.vfs.FileSystemException;
+import org.apache.commons.vfs.FileType;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * @author Matthias L. Jugel
+ * @version $Id$
+ */
+public class GetHandler extends WebdavHandler {
+
+  public void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
+    FileObject object = VFSBackend.resolveFile(request.getPathInfo());
+
+    if (object.exists()) {
+      if (FileType.FOLDER.equals(object.getType())) {
+        response.sendError(HttpServletResponse.SC_FORBIDDEN);
+        return;
+      }
+
+      setHeader(response, object.getContent());
+
+      InputStream is = object.getContent().getInputStream();
+      OutputStream os = response.getOutputStream();
+      Util.copyStream(is, os);
+      is.close();
+    } else {
+      response.sendError(HttpServletResponse.SC_NOT_FOUND);
+    }
+  }
+
+  void setHeader(HttpServletResponse response, FileContent content) throws FileSystemException {
+    response.setHeader("Last-Modified", Util.getDateString(content.getLastModifiedTime()));
+    response.setHeader("Content-Type", content.getContentInfo().getContentType());
+    response.setHeader("ETag", String.format("%x", content.getFile().hashCode()));
+  }
+
+
+}
blob - /dev/null
blob + 96f6528d7d218c2dc9f6d4e8843711e802848a02 (mode 644)
--- /dev/null
+++ modules/webdav/src/main/java/com/thinkberg/webdav/HeadHandler.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2007 Matthias L. Jugel.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.thinkberg.webdav;
+
+import com.thinkberg.webdav.vfs.VFSBackend;
+import org.apache.commons.vfs.FileObject;
+import org.apache.commons.vfs.FileType;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * @author Matthias L. Jugel
+ * @version $Id$
+ */
+public class HeadHandler extends GetHandler {
+
+  public void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
+    FileObject object = VFSBackend.resolveFile(request.getPathInfo());
+
+    if (object.exists()) {
+      if (FileType.FOLDER.equals(object.getType())) {
+        response.sendError(HttpServletResponse.SC_FORBIDDEN);
+      } else {
+        setHeader(response, object.getContent());
+      }
+    } else {
+      response.sendError(HttpServletResponse.SC_NOT_FOUND);
+    }
+  }
+}
blob - /dev/null
blob + 204d06d06be8367f5bb86da172606b16b3c525ab (mode 644)
--- /dev/null
+++ modules/webdav/src/main/java/com/thinkberg/webdav/LockHandler.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2007 Matthias L. Jugel.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.thinkberg.webdav;
+
+import com.thinkberg.webdav.lock.Lock;
+import com.thinkberg.webdav.lock.LockConflictException;
+import com.thinkberg.webdav.lock.LockManager;
+import com.thinkberg.webdav.vfs.VFSBackend;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.commons.vfs.FileObject;
+import org.dom4j.*;
+import org.dom4j.io.OutputFormat;
+import org.dom4j.io.SAXReader;
+import org.dom4j.io.XMLWriter;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.URL;
+import java.text.ParseException;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Handle WebDAV LOCK requests.
+ *
+ * @author Matthias L. Jugel
+ * @version $Id$
+ */
+public class LockHandler extends WebdavHandler {
+  private static final Log LOG = LogFactory.getLog(LockHandler.class);
+
+  private static final String TAG_LOCKSCOPE = "lockscope";
+  private static final String TAG_LOCKTYPE = "locktype";
+  private static final String TAG_OWNER = "owner";
+  private static final String TAG_HREF = "href";
+  private static final String TAG_PROP = "prop";
+  private static final String TAG_LOCKDISCOVERY = "lockdiscovery";
+
+  private static final String HEADER_LOCK_TOKEN = "Lock-Token";
+
+  public void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
+    FileObject object = VFSBackend.resolveFile(request.getPathInfo());
+
+    try {
+      final LockManager manager = LockManager.getInstance();
+      final LockManager.EvaluationResult evaluation = manager.evaluateCondition(object, getIf(request));
+      if (!evaluation.result) {
+        response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+        return;
+      } else {
+        if (!evaluation.locks.isEmpty()) {
+          LOG.debug(String.format("discovered locks: %s", evaluation.locks));
+          sendLockAcquiredResponse(response, evaluation.locks.get(0));
+          return;
+        }
+      }
+    } catch (LockConflictException e) {
+      List<Lock> locks = e.getLocks();
+      for (Lock lock : locks) {
+        if (Lock.EXCLUSIVE.equals(lock.getType())) {
+          response.sendError(SC_LOCKED);
+          return;
+        }
+      }
+    } catch (ParseException e) {
+      response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+      return;
+    }
+
+    try {
+      SAXReader saxReader = new SAXReader();
+      Document lockInfo = saxReader.read(request.getInputStream());
+      //log(lockInfo);
+
+      Element rootEl = lockInfo.getRootElement();
+      String lockScope = null, lockType = null;
+      Object owner = null;
+      Iterator elIt = rootEl.elementIterator();
+      while (elIt.hasNext()) {
+        Element el = (Element) elIt.next();
+        if (TAG_LOCKSCOPE.equals(el.getName())) {
+          lockScope = el.selectSingleNode("*").getName();
+        } else if (TAG_LOCKTYPE.equals(el.getName())) {
+          lockType = el.selectSingleNode("*").getName();
+        } else if (TAG_OWNER.equals(el.getName())) {
+          // TODO correctly handle owner
+          Node subEl = el.selectSingleNode("*");
+          if (subEl != null && TAG_HREF.equals(subEl.getName())) {
+            owner = new URL(el.selectSingleNode("*").getText());
+          } else {
+            owner = el.getText();
+          }
+        }
+      }
+
+      LOG.debug("LOCK(" + lockType + ", " + lockScope + ", " + owner + ")");
+
+      Lock requestedLock = new Lock(object, lockType, lockScope, owner, getDepth(request), getTimeout(request));
+      try {
+        LockManager.getInstance().acquireLock(requestedLock);
+        sendLockAcquiredResponse(response, requestedLock);
+      } catch (LockConflictException e) {
+        response.sendError(SC_LOCKED);
+      } catch (IllegalArgumentException e) {
+        response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+      }
+    } catch (DocumentException e) {
+      e.printStackTrace();
+      response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+    }
+  }
+
+  private void sendLockAcquiredResponse(HttpServletResponse response, Lock lock) throws IOException {
+    if (!lock.getObject().exists()) {
+      response.setStatus(SC_CREATED);
+    }
+    response.setContentType("text/xml");
+    response.setCharacterEncoding("UTF-8");
+    response.setHeader(HEADER_LOCK_TOKEN, "<" + lock.getToken() + ">");
+
+    Document propDoc = DocumentHelper.createDocument();
+    Element propEl = propDoc.addElement(TAG_PROP, "DAV:");
+    Element lockdiscoveryEl = propEl.addElement(TAG_LOCKDISCOVERY);
+
+    lock.serializeToXml(lockdiscoveryEl);
+
+    XMLWriter xmlWriter = new XMLWriter(response.getWriter());
+    xmlWriter.write(propDoc);
+
+    logXml(propDoc);
+  }
+
+  private void logXml(Node element) {
+    ByteArrayOutputStream bos = new ByteArrayOutputStream();
+    try {
+      XMLWriter xmlWriter = new XMLWriter(bos, OutputFormat.createPrettyPrint());
+      xmlWriter.write(element);
+      LOG.debug(bos.toString());
+    } catch (IOException e) {
+      LOG.debug("ERROR writing XML log: " + e.getMessage());
+    }
+  }
+}
blob - /dev/null
blob + f6b909f7909935b5eeb8a150694a36d899dd37a1 (mode 644)
--- /dev/null
+++ modules/webdav/src/main/java/com/thinkberg/webdav/MkColHandler.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2007 Matthias L. Jugel.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.thinkberg.webdav;
+
+import com.thinkberg.webdav.lock.LockException;
+import com.thinkberg.webdav.lock.LockManager;
+import com.thinkberg.webdav.vfs.VFSBackend;
+import org.apache.commons.vfs.FileObject;
+import org.apache.commons.vfs.FileSystemException;
+import org.apache.commons.vfs.FileType;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.text.ParseException;
+
+/**
+ * @author Matthias L. Jugel
+ * @version $Id$
+ */
+public class MkColHandler extends WebdavHandler {
+
+  public void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
+    BufferedReader bufferedReader = request.getReader();
+    String line = bufferedReader.readLine();
+    if (line != null) {
+      response.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE);
+      return;
+    }
+
+    FileObject object = VFSBackend.resolveFile(request.getPathInfo());
+
+    try {
+      if (!LockManager.getInstance().evaluateCondition(object, getIf(request)).result) {
+        response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+        return;
+      }
+    } catch (LockException e) {
+      response.sendError(SC_LOCKED);
+      return;
+    } catch (ParseException e) {
+      response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+      return;
+    }
+
+    if (object.exists()) {
+      response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
+      return;
+    }
+
+    if (!object.getParent().exists() || !FileType.FOLDER.equals(object.getParent().getType())) {
+      response.sendError(HttpServletResponse.SC_CONFLICT);
+      return;
+    }
+
+    try {
+      object.createFolder();
+      response.setStatus(HttpServletResponse.SC_CREATED);
+    } catch (FileSystemException e) {
+      response.sendError(HttpServletResponse.SC_FORBIDDEN);
+    }
+  }
+}
blob - /dev/null
blob + 21fb7f4fd07568da5160aadb3563b7b18d3ed5f1 (mode 644)
--- /dev/null
+++ modules/webdav/src/main/java/com/thinkberg/webdav/MoveHandler.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2007 Matthias L. Jugel.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.thinkberg.webdav;
+
+import com.thinkberg.webdav.vfs.DepthFileSelector;
+import org.apache.commons.vfs.FileObject;
+import org.apache.commons.vfs.FileSystemException;
+
+/**
+ * @author Matthias L. Jugel
+ * @version $Id$
+ */
+public class MoveHandler extends CopyMoveBase {
+  protected void copyOrMove(FileObject object, FileObject target, int depth) throws FileSystemException {
+    target.copyFrom(object, new DepthFileSelector(depth));
+    object.delete(new DepthFileSelector());
+  }
+}
blob - /dev/null
blob + 6058cbe74bff642e068a09137748ef65593fb12d (mode 644)
--- /dev/null
+++ modules/webdav/src/main/java/com/thinkberg/webdav/OptionsHandler.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2007 Matthias L. Jugel.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.thinkberg.webdav;
+
+import com.thinkberg.webdav.vfs.VFSBackend;
+import org.apache.commons.vfs.FileObject;
+import org.apache.commons.vfs.FileType;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * @author Matthias L. Jugel
+ * @version $Id$
+ */
+public class OptionsHandler extends WebdavHandler {
+
+  public void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
+    response.setHeader("DAV", "1, 2");
+
+    String path = request.getPathInfo();
+    StringBuffer options = new StringBuffer();
+    FileObject object = VFSBackend.resolveFile(path);
+    if (object.exists()) {
+      options.append("OPTIONS, GET, HEAD, POST, DELETE, TRACE, COPY, MOVE, LOCK, UNLOCK, PROPFIND");
+      if (FileType.FOLDER.equals(object.getType())) {
+        options.append(", PUT");
+      }
+    } else {
+      options.append("OPTIONS, MKCOL, PUT, LOCK");
+    }
+    response.setHeader("Allow", options.toString());
+
+    // see: http://www-128.ibm.com/developerworks/rational/library/2089.html
+    response.setHeader("MS-Author-Via", "DAV");
+  }
+}
blob - /dev/null
blob + a85eb352d826917304160ce46e2bde264bed1e40 (mode 644)
--- /dev/null
+++ modules/webdav/src/main/java/com/thinkberg/webdav/PostHandler.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2007 Matthias L. Jugel.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.thinkberg.webdav;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * @author Matthias L. Jugel
+ * @version $Id$
+ */
+public class PostHandler extends WebdavHandler {
+
+  public void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
+    response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+  }
+}
blob - /dev/null
blob + 00a7383b8d339f22c7242ad22205a99bfaec87cf (mode 644)
--- /dev/null
+++ modules/webdav/src/main/java/com/thinkberg/webdav/PropFindHandler.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2007 Matthias L. Jugel.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.thinkberg.webdav;
+
+import com.thinkberg.webdav.data.DavResource;
+import com.thinkberg.webdav.data.DavResourceFactory;
+import com.thinkberg.webdav.vfs.DepthFileSelector;
+import com.thinkberg.webdav.vfs.VFSBackend;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.commons.vfs.FileObject;
+import org.apache.commons.vfs.FileSystemException;
+import org.dom4j.*;
+import org.dom4j.io.OutputFormat;
+import org.dom4j.io.SAXReader;
+import org.dom4j.io.XMLWriter;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Matthias L. Jugel
+ * @version $Id$
+ */
+public class PropFindHandler extends WebdavHandler {
+  private static final String TAG_PROP = "prop";
+  private static final String TAG_ALLPROP = "allprop";
+  private static final String TAG_PROPNAMES = "propnames";
+  private static final String TAG_MULTISTATUS = "multistatus";
+  private static final String TAG_HREF = "href";
+  private static final String TAG_RESPONSE = "response";
+  private static final Log LOG = LogFactory.getLog(PropFindHandler.class);
+
+  void logXml(Node element) {
+    ByteArrayOutputStream bos = new ByteArrayOutputStream();
+    try {
+      XMLWriter xmlWriter = new XMLWriter(bos, OutputFormat.createPrettyPrint());
+      xmlWriter.write(element);
+      LOG.debug(bos.toString());
+    } catch (IOException e) {
+      LOG.error(e.getMessage());
+    }
+  }
+
+
+  public void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
+    SAXReader saxReader = new SAXReader();
+    try {
+      Document propDoc = saxReader.read(request.getInputStream());
+      logXml(propDoc);
+
+      Element propFindEl = propDoc.getRootElement();
+      Element propEl = (Element) propFindEl.elementIterator().next();
+      String propElName = propEl.getName();
+
+      List<String> requestedProperties = new ArrayList<String>();
+      boolean ignoreValues = false;
+      if (TAG_PROP.equals(propElName)) {
+        for (Object id : propEl.elements()) {
+          requestedProperties.add(((Element) id).getName());
+        }
+      } else if (TAG_ALLPROP.equals(propElName)) {
+        requestedProperties = DavResource.ALL_PROPERTIES;
+      } else if (TAG_PROPNAMES.equals(propElName)) {
+        requestedProperties = DavResource.ALL_PROPERTIES;
+        ignoreValues = true;
+      }
+
+      FileObject object = VFSBackend.resolveFile(request.getPathInfo());
+      if (object.exists()) {
+        // respond as XML encoded multi status
+        response.setContentType("text/xml");
+        response.setCharacterEncoding("UTF-8");
+        response.setStatus(SC_MULTI_STATUS);
+
+        Document multiStatusResponse =
+                getMultiStatusRespons(object,
+                                      requestedProperties,
+                                      getBaseUrl(request),
+                                      getDepth(request),
+                                      ignoreValues);
+        logXml(multiStatusResponse);
+
+        // write the actual response
+        XMLWriter writer = new XMLWriter(response.getWriter(), OutputFormat.createCompactFormat());
+        writer.write(multiStatusResponse);
+        writer.flush();
+        writer.close();
+
+      } else {
+        LOG.error(object.getName().getPath() + " NOT FOUND");
+        response.sendError(HttpServletResponse.SC_NOT_FOUND);
+      }
+    } catch (DocumentException e) {
+      LOG.error("invalid request: " + e.getMessage());
+      response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+    }
+  }
+
+  @SuppressWarnings({"ConstantConditions"})
+  private Document getMultiStatusRespons(FileObject object,
+                                         List<String> requestedProperties,
+                                         URL baseUrl,
+                                         int depth,
+                                         boolean ignoreValues) throws FileSystemException {
+    Document propDoc = DocumentHelper.createDocument();
+    propDoc.setXMLEncoding("UTF-8");
+
+    Element multiStatus = propDoc.addElement(TAG_MULTISTATUS, "DAV:");
+    FileObject[] children = object.findFiles(new DepthFileSelector(depth));
+    for (FileObject child : children) {
+      Element responseEl = multiStatus.addElement(TAG_RESPONSE);
+      try {
+        URL url = new URL(baseUrl, URLEncoder.encode(child.getName().getPath(), "UTF-8"));
+        LOG.debug(url);
+        responseEl.addElement(TAG_HREF).addText(url.toExternalForm());
+      } catch (Exception e) {
+        e.printStackTrace();
+      }
+      DavResource resource = DavResourceFactory.getInstance().getDavResource(child);
+      resource.setIgnoreValues(ignoreValues);
+      resource.serializeToXml(responseEl, requestedProperties);
+    }
+    return propDoc;
+  }
+}
blob - /dev/null
blob + 07ffae65a4113a2f5526d060bb9abd511fc50a5a (mode 644)
--- /dev/null
+++ modules/webdav/src/main/java/com/thinkberg/webdav/PropPatchHandler.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2007 Matthias L. Jugel.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.thinkberg.webdav;
+
+import com.thinkberg.webdav.data.DavResource;
+import com.thinkberg.webdav.lock.LockException;
+import com.thinkberg.webdav.lock.LockManager;
+import com.thinkberg.webdav.vfs.VFSBackend;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.commons.vfs.FileObject;
+import org.dom4j.Document;
+import org.dom4j.DocumentException;
+import org.dom4j.DocumentHelper;
+import org.dom4j.Element;
+import org.dom4j.io.OutputFormat;
+import org.dom4j.io.SAXReader;
+import org.dom4j.io.XMLWriter;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URL;
+import java.text.ParseException;
+import java.util.List;
+
+/**
+ * Handle PROPPATCH requests. This currently a dummy only and will return a
+ * forbidden status for any attempt to modify or remove a property.
+ *
+ * @author Matthias L. Jugel
+ */
+public class PropPatchHandler extends WebdavHandler {
+  private static final Log LOG = LogFactory.getLog(PropPatchHandler.class);
+
+  public void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
+    FileObject object = VFSBackend.resolveFile(request.getPathInfo());
+
+    try {
+      if (!LockManager.getInstance().evaluateCondition(object, getIf(request)).result) {
+        response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+        return;
+      }
+    } catch (LockException e) {
+      response.sendError(SC_LOCKED);
+      return;
+    } catch (ParseException e) {
+      response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+      return;
+    }
+
+    SAXReader saxReader = new SAXReader();
+    try {
+      Document propDoc = saxReader.read(request.getInputStream());
+//      log(propDoc);
+
+      response.setContentType("text/xml");
+      response.setCharacterEncoding("UTF-8");
+      response.setStatus(SC_MULTI_STATUS);
+
+      if (object.exists()) {
+        Document resultDoc = DocumentHelper.createDocument();
+        Element multiStatusResponse = resultDoc.addElement("multistatus", "DAV:");
+        Element responseEl = multiStatusResponse.addElement("response");
+        try {
+          URL url = new URL(getBaseUrl(request), URLEncoder.encode(object.getName().getPath(), "UTF-8"));
+          LOG.debug(url);
+          responseEl.addElement("href").addText(url.toExternalForm());
+        } catch (Exception e) {
+          e.printStackTrace();
+        }
+
+        Element propstatEl = responseEl.addElement("propstat");
+        Element propEl = propstatEl.addElement("prop");
+
+        Element propertyUpdateEl = propDoc.getRootElement();
+        for (Object elObject : propertyUpdateEl.elements()) {
+          Element el = (Element) elObject;
+          if ("set".equals(el.getName())) {
+            for (Object propObject : el.elements()) {
+              setProperty(propEl, object, (Element) propObject);
+            }
+          } else if ("remove".equals(el.getName())) {
+            for (Object propObject : el.elements()) {
+              removeProperty(propEl, object, (Element) propObject);
+            }
+          }
+        }
+        propstatEl.addElement("status").addText(DavResource.STATUS_403);
+
+//        log(resultDoc);
+
+        // write the actual response
+        XMLWriter writer = new XMLWriter(response.getWriter(), OutputFormat.createCompactFormat());
+        writer.write(resultDoc);
+        writer.flush();
+        writer.close();
+      } else {
+        LOG.error(object.getName().getPath() + " NOT FOUND");
+        response.sendError(HttpServletResponse.SC_NOT_FOUND);
+      }
+    } catch (DocumentException e) {
+      LOG.error("invalid request: " + e.getMessage());
+      response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+    }
+  }
+
+  private void setProperty(Element root, FileObject object, Element el) {
+    List propList = el.elements();
+    for (Object propElObject : propList) {
+      Element propEl = (Element) propElObject;
+      for (int i = 0; i < propEl.nodeCount(); i++) {
+        propEl.node(i).detach();
+      }
+      root.add(propEl.detach());
+    }
+  }
+
+  private void removeProperty(Element root, FileObject object, Element el) {
+    setProperty(root, object, el);
+  }
+
+
+}
blob - /dev/null
blob + 3fef13476f5e0d253c3c0fe446adb91682414f74 (mode 644)
--- /dev/null
+++ modules/webdav/src/main/java/com/thinkberg/webdav/PutHandler.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2007 Matthias L. Jugel.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.thinkberg.webdav;
+
+import com.thinkberg.webdav.lock.LockException;
+import com.thinkberg.webdav.lock.LockManager;
+import com.thinkberg.webdav.vfs.VFSBackend;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.commons.vfs.FileObject;
+import org.apache.commons.vfs.FileType;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.text.ParseException;
+
+/**
+ * @author Matthias L. Jugel
+ * @version $Id$
+ */
+public class PutHandler extends WebdavHandler {
+  private static final Log LOG = LogFactory.getLog(PutHandler.class);
+
+  public void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
+    FileObject object = VFSBackend.resolveFile(request.getPathInfo());
+
+    try {
+      if (!LockManager.getInstance().evaluateCondition(object, getIf(request)).result) {
+        response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+        return;
+      }
+    } catch (LockException e) {
+      response.sendError(SC_LOCKED);
+      return;
+    } catch (ParseException e) {
+      response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+      return;
+    }
+    // it is forbidden to write data on a folder
+    if (object.exists() && FileType.FOLDER.equals(object.getType())) {
+      response.sendError(HttpServletResponse.SC_FORBIDDEN);
+      return;
+    }
+
+    FileObject parent = object.getParent();
+    if (!parent.exists()) {
+      response.sendError(HttpServletResponse.SC_FORBIDDEN);
+      return;
+    }
+
+    if (!FileType.FOLDER.equals(parent.getType())) {
+      response.sendError(HttpServletResponse.SC_CONFLICT);
+      return;
+    }
+
+    InputStream is = request.getInputStream();
+    OutputStream os = object.getContent().getOutputStream();
+    long bytesCopied = Util.copyStream(is, os);
+    String contentLengthHeader = request.getHeader("Content-length");
+    LOG.debug(String.format("sent %d/%s bytes", bytesCopied, contentLengthHeader == null ? "unknown" : contentLengthHeader));
+    os.flush();
+    object.close();
+
+    response.setStatus(HttpServletResponse.SC_CREATED);
+  }
+}
blob - /dev/null
blob + 52760b5e28fb05e62c113b44b1bb3564f2192ff0 (mode 644)
--- /dev/null
+++ modules/webdav/src/main/java/com/thinkberg/webdav/URLEncoder.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2007 Matthias L. Jugel.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.thinkberg.webdav;
+
+import java.io.UnsupportedEncodingException;
+import java.util.BitSet;
+
+/**
+ * Encode a URL but leave some special characters in plain text.
+ *
+ * @author Matthias L. Jugel
+ */
+class URLEncoder {
+
+  private static final BitSet keepPlain;
+
+  static {
+    keepPlain = new BitSet(256);
+    int i;
+    for (i = 'a'; i <= 'z'; i++) {
+      keepPlain.set(i);
+    }
+    for (i = 'A'; i <= 'Z'; i++) {
+      keepPlain.set(i);
+    }
+    for (i = '0'; i <= '9'; i++) {
+      keepPlain.set(i);
+    }
+    keepPlain.set('+');
+    keepPlain.set('-');
+    keepPlain.set('_');
+    keepPlain.set('.');
+    keepPlain.set('*');
+    keepPlain.set('/');
+    keepPlain.set(':');
+  }
+
+
+  public static String encode(String s, String enc) throws UnsupportedEncodingException {
+    byte[] buf = s.getBytes(enc);
+    StringBuffer result = new StringBuffer();
+    for (byte aBuf : buf) {
+      int c = (int) aBuf;
+      if (keepPlain.get(c & 0xFF)) {
+        result.append((char) c);
+      } else {
+        result.append('%').append(Integer.toHexString(c & 0xFF).toUpperCase());
+      }
+    }
+    return result.toString();
+  }
+}
blob - /dev/null
blob + 176c5c3b89711ed18bc14acb2012a7c370162406 (mode 644)
--- /dev/null
+++ modules/webdav/src/main/java/com/thinkberg/webdav/UnlockHandler.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2007 Matthias L. Jugel.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.thinkberg.webdav;
+
+import com.thinkberg.webdav.lock.LockManager;
+import com.thinkberg.webdav.vfs.VFSBackend;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.commons.vfs.FileObject;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * @author Matthias L. Jugel
+ * @version $Id$
+ */
+public class UnlockHandler extends WebdavHandler {
+  private static final Log LOG = LogFactory.getLog(UnlockHandler.class);
+
+  public void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
+    FileObject object = VFSBackend.resolveFile(request.getPathInfo());
+    String lockTokenHeader = request.getHeader("Lock-Token");
+    String lockToken = lockTokenHeader.substring(1, lockTokenHeader.length() - 1);
+    LOG.debug("UNLOCK(" + lockToken + ")");
+
+    if (LockManager.getInstance().releaseLock(object, lockToken)) {
+      response.setStatus(HttpServletResponse.SC_NO_CONTENT);
+    } else {
+      response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+    }
+  }
+}
blob - /dev/null
blob + 3a04b32724c8f5ab5247f0d1cae38cba747cea8a (mode 644)
--- /dev/null
+++ modules/webdav/src/main/java/com/thinkberg/webdav/Util.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2007 Matthias L. Jugel.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.thinkberg.webdav;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.WritableByteChannel;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * @author Matthias L. Jugel
+ * @version $Id$
+ */
+public class Util {
+
+  private static final SimpleDateFormat httpDateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
+
+  public static String getDateString(long time) {
+    return httpDateFormat.format(new Date(time));
+  }
+
+//  public static String getISODateString(long time) {
+//    return "";    4
+//  }
+
+
+  public static long copyStream(final InputStream is, final OutputStream os) throws IOException {
+    ReadableByteChannel rbc = Channels.newChannel(is);
+    WritableByteChannel wbc = Channels.newChannel(os);
+
+    int bytesWritten = 0;
+    final ByteBuffer buffer = ByteBuffer.allocateDirect(16 * 1024);
+    while (rbc.read(buffer) != -1) {
+      buffer.flip();
+      bytesWritten += wbc.write(buffer);
+      buffer.compact();
+    }
+    buffer.flip();
+    while (buffer.hasRemaining()) {
+      bytesWritten += wbc.write(buffer);
+    }
+
+    rbc.close();
+    wbc.close();
+
+    return bytesWritten;
+  }
+}
blob - /dev/null
blob + bca6db1498389193b487c1a5c50753dda989f4c5 (mode 644)
--- /dev/null
+++ modules/webdav/src/main/java/com/thinkberg/webdav/WebdavHandler.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2007 Matthias L. Jugel.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.thinkberg.webdav;
+
+import com.thinkberg.webdav.vfs.VFSBackend;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.commons.vfs.FileObject;
+import org.apache.commons.vfs.FileSystemException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Arrays;
+
+/**
+ * @author Matthias L. Jugel
+ * @version $Id$
+ */
+public abstract class WebdavHandler {
+  private static final Log LOG = LogFactory.getLog(WebdavHandler.class);
+
+  static final int SC_CREATED = 201;
+  static final int SC_LOCKED = 423;
+  static final int SC_MULTI_STATUS = 207;
+
+  protected static URL getBaseUrl(HttpServletRequest request) {
+    try {
+      String requestUrl = request.getRequestURL().toString();
+      String requestUri = request.getRequestURI();
+      String requestUrlBase = requestUrl.substring(0, requestUrl.length() - requestUri.length());
+      return new URL(requestUrlBase);
+    } catch (MalformedURLException e) {
+      // ignore ...
+    }
+    return null;
+  }
+
+  public abstract void service(HttpServletRequest request, HttpServletResponse response) throws IOException;
+
+  /**
+   * Get the depth header value. This value defines how operations
+   * like propfind, move, copy etc. handle collections. A depth value
+   * of 0 will only return the current collection, 1 will return
+   * children too and infinity will recursively operate.
+   *
+   * @param request the servlet request
+   * @return the depth value as 0, 1 or Integer.MAX_VALUE;
+   */
+  int getDepth(HttpServletRequest request) {
+    String depth = request.getHeader("Depth");
+    int depthValue;
+
+    if (null == depth || "infinity".equalsIgnoreCase(depth)) {
+      depthValue = Integer.MAX_VALUE;
+    } else {
+      depthValue = Integer.parseInt(depth);
+    }
+
+    LOG.debug(String.format("request header: Depth: %s", (depthValue == Integer.MAX_VALUE ? "infinity" : depthValue)));
+    return depthValue;
+  }
+
+  /**
+   * Get the overwrite header value, whether to overwrite destination
+   * objects or collections or not.
+   *
+   * @param request the servlet request
+   * @return true or false
+   */
+  boolean getOverwrite(HttpServletRequest request) {
+    String overwrite = request.getHeader("Overwrite");
+    boolean overwriteValue = overwrite == null || "T".equals(overwrite);
+
+    LOG.debug(String.format("request header: Overwrite: %s", overwriteValue));
+    return overwriteValue;
+  }
+
+  /**
+   * Get the destination object or collection. The destination header contains
+   * a URL to the destination which is returned as a file object.
+   *
+   * @param request the servlet request
+   * @return the file object of the destination
+   * @throws FileSystemException   if the file system cannot create a file object
+   * @throws MalformedURLException if the url is misformatted
+   */
+  FileObject getDestination(HttpServletRequest request) throws FileSystemException, MalformedURLException {
+    String targetUrlStr = request.getHeader("Destination");
+    FileObject targetObject = null;
+    if (null != targetUrlStr) {
+      URL target = new URL(targetUrlStr);
+      targetObject = VFSBackend.resolveFile(target.getPath());
+      LOG.debug(String.format("request header: Destination: %s", targetObject.getName().getPath()));
+    }
+
+    return targetObject;
+  }
+
+  /**
+   * Get the if header.
+   *
+   * @param request the request
+   * @return the value if the If: header.
+   */
+  String getIf(HttpServletRequest request) {
+    String getIfHeader = request.getHeader("If");
+
+    if (null != getIfHeader) {
+      LOG.debug(String.format("request header: If: '%s'", getIfHeader));
+    }
+    return getIfHeader;
+  }
+
+  /**
+   * Get and parse the timeout header value.
+   *
+   * @param request the request
+   * @return the timeout
+   */
+  long getTimeout(HttpServletRequest request) {
+    String timeout = request.getHeader("Timeout");
+    if (null != timeout) {
+      String[] timeoutValues = timeout.split(",[ ]*");
+      LOG.debug(String.format("request header: Timeout: %s", Arrays.asList(timeoutValues).toString()));
+      if ("infinity".equalsIgnoreCase(timeoutValues[0])) {
+        return -1;
+      } else {
+        return Integer.parseInt(timeoutValues[0].replaceAll("Second-", ""));
+      }
+    }
+    return -1;
+  }
+}
blob - /dev/null
blob + 9483a4df30a0be93e787b2e9400ce60e5aead2f0 (mode 755)
--- /dev/null
+++ modules/webdav/src/test/java/com/thinkberg/webdav/tests/DavLockManagerTest.java
@@ -0,0 +1,169 @@
+package com.thinkberg.webdav.tests;
+
+import com.thinkberg.webdav.DavTestCase;
+import com.thinkberg.webdav.lock.Lock;
+import com.thinkberg.webdav.lock.LockConflictException;
+import com.thinkberg.webdav.lock.LockManager;
+
+/**
+ * @author Matthias L. Jugel
+ */
+public class DavLockManagerTest extends DavTestCase {
+  private final String OWNER_STR = "testowner";
+
+  public DavLockManagerTest() {
+    super();
+  }
+
+  public void testAcquireSingleSharedFileLock() {
+    Lock sharedLock = new Lock(aFile, Lock.WRITE, Lock.SHARED, OWNER_STR, 0, 3600);
+    try {
+      LockManager.getInstance().acquireLock(sharedLock);
+    } catch (Exception e) {
+      assertNull(e.getMessage(), e);
+    }
+  }
+
+  public void testAcquireDoubleSharedFileLock() {
+    Lock sharedLock1 = new Lock(aFile, Lock.WRITE, Lock.SHARED, OWNER_STR, 0, 3600);
+    Lock sharedLock2 = new Lock(aFile, Lock.WRITE, Lock.SHARED, OWNER_STR + "1", 0, 3600);
+    try {
+      LockManager.getInstance().acquireLock(sharedLock1);
+      LockManager.getInstance().acquireLock(sharedLock2);
+    } catch (Exception e) {
+      assertNull(e.getMessage(), e);
+    }
+  }
+
+  public void testFailToAcquireExclusiveLockOverSharedLock() {
+    Lock sharedLock = new Lock(aFile, Lock.WRITE, Lock.SHARED, OWNER_STR, 0, 3600);
+    Lock exclusiveLock = new Lock(aFile, Lock.WRITE, Lock.EXCLUSIVE, OWNER_STR, 0, 3600);
+    try {
+      LockManager.getInstance().acquireLock(sharedLock);
+      LockManager.getInstance().acquireLock(exclusiveLock);
+      assertTrue("acquireLock() should fail", false);
+    } catch (Exception e) {
+      assertEquals(LockConflictException.class, e.getClass());
+    }
+  }
+
+  public void testConditionUnmappedFails() throws Exception {
+    final String condition = "<http://cid:8080/litmus/unmapped_url> (<opaquelocktoken:cd6798>)";
+    assertFalse("condition for unmapped resource must fail",
+                LockManager.getInstance().evaluateCondition(aFile, condition).result);
+  }
+
+  public void testConditionSimpleLockToken() throws Exception {
+    Lock aLock = new Lock(aFile, Lock.WRITE, Lock.SHARED, OWNER_STR, 0, 3600);
+    final String condition = "(<" + aLock.getToken() + ">)";
+    LockManager.getInstance().acquireLock(aLock);
+    assertTrue("condition with existing lock token should not fail",
+               LockManager.getInstance().evaluateCondition(aFile, condition).result);
+  }
+
+  public void testConditionSimpleLockLokenWrong() throws Exception {
+    Lock aLock = new Lock(aFile, Lock.WRITE, Lock.SHARED, OWNER_STR, 0, 3600);
+    final String condition = "(<" + aLock.getToken() + "x>)";
+    LockManager.getInstance().acquireLock(aLock);
+    try {
+      LockManager.getInstance().evaluateCondition(aFile, condition);
+    } catch (LockConflictException e) {
+      assertFalse("condition with wrong lock token must fail on locked resource", e.getLocks().isEmpty());
+    }
+  }
+
+  public void testConditionSimpleLockTokenAndETag() throws Exception {
+    Lock aLock = new Lock(aFile, Lock.WRITE, Lock.SHARED, OWNER_STR, 0, 3600);
+    final String condition = "(<" + aLock.getToken() + "> [" + Integer.toHexString(aFile.hashCode()) + "])";
+    LockManager.getInstance().acquireLock(aLock);
+    assertTrue("condition with existing lock token and correct ETag should not fail",
+               LockManager.getInstance().evaluateCondition(aFile, condition).result);
+  }
+
+  public void testConditionSimpleLockTokenWrongAndETag() throws Exception {
+    Lock aLock = new Lock(aFile, Lock.WRITE, Lock.SHARED, OWNER_STR, 0, 3600);
+    final String condition = "(<" + aLock.getToken() + "x> [" + Integer.toHexString(aFile.hashCode()) + "])";
+    LockManager.getInstance().acquireLock(aLock);
+    try {
+      LockManager.getInstance().evaluateCondition(aFile, condition);
+    } catch (LockConflictException e) {
+      assertFalse("condition with non-existing lock token and correct ETag should fail",
+                  e.getLocks().isEmpty());
+    }
+  }
+
+  public void testConditionSimpleLockTokenAndETagWrong() throws Exception {
+    Lock aLock = new Lock(aFile, Lock.WRITE, Lock.SHARED, OWNER_STR, 0, 3600);
+    final String condition = "(<" + aLock.getToken() + "> [" + Integer.toHexString(aFile.hashCode()) + "x])";
+    LockManager.getInstance().acquireLock(aLock);
+    assertFalse("condition with existing lock token and incorrect ETag should fail",
+                LockManager.getInstance().evaluateCondition(aFile, condition).result);
+  }
+
+  public void testConditionSimpleLockTokenWrongAndETagWrong() throws Exception {
+    Lock aLock = new Lock(aFile, Lock.WRITE, Lock.SHARED, OWNER_STR, 0, 3600);
+    final String condition = "(<" + aLock.getToken() + "x> [" + Integer.toHexString(aFile.hashCode()) + "x])";
+    LockManager.getInstance().acquireLock(aLock);
+    assertFalse("condition with non-existing lock token and incorrect ETag should fail",
+                LockManager.getInstance().evaluateCondition(aFile, condition).result);
+  }
+
+  public void testConditionSimpleLockTokenWrongAndETagOrSimpleETag() throws Exception {
+    Lock aLock = new Lock(aFile, Lock.WRITE, Lock.SHARED, OWNER_STR, 0, 3600);
+    final String eTag = Integer.toHexString(aFile.hashCode());
+    final String condition = "(<" + aLock.getToken() + "x> [" + eTag + "]) ([" + eTag + "])";
+    LockManager.getInstance().acquireLock(aLock);
+    try {
+      LockManager.getInstance().evaluateCondition(aFile, condition);
+    } catch (LockConflictException e) {
+      assertFalse("condition with one correct ETag in list should not fail on locked resource",
+                  e.getLocks().isEmpty());
+    }
+  }
+
+  public void testConditionSimpleNegatedLockTokenWrongAndETag() throws Exception {
+    Lock aLock = new Lock(aFile, Lock.WRITE, Lock.SHARED, OWNER_STR, 0, 3600);
+    final String eTag = Integer.toHexString(aFile.hashCode());
+    final String condition = "(Not <" + aLock.getToken() + "x> [" + eTag + "])";
+    assertTrue("condition with negated wrong lock token and correct ETag should not fail on unlocked resource",
+               LockManager.getInstance().evaluateCondition(aFile, condition).result);
+  }
+
+
+  public void testConditionMustNotFail() throws Exception {
+    Lock aLock = new Lock(aFile, Lock.WRITE, Lock.SHARED, OWNER_STR, 0, 3600);
+    final String condition = "(<" + aLock.getToken() + "x>) (Not <DAV:no-lock>)";
+    assertTrue("using (Not <DAV:no-lock>) in condition list must not fail on unlocked resource",
+               LockManager.getInstance().evaluateCondition(aFile, condition).result);
+  }
+
+
+  public void testComplexConditionWithBogusLockToken() throws Exception {
+    Lock aLock = new Lock(aFile, Lock.WRITE, Lock.SHARED, OWNER_STR, 0, 3600);
+    final String eTag = Integer.toHexString(aFile.hashCode());
+    final String condition = "(<" + aLock.getToken() + "> [" + eTag + "x]) (Not <DAV:no-lock> [" + eTag + "x])";
+    LockManager.getInstance().acquireLock(aLock);
+    assertFalse("complex condition with bogus eTag should fail",
+                LockManager.getInstance().evaluateCondition(aFile, condition).result);
+  }
+
+
+//  assertFalse(lockManager.evaluateCondition(aFile, "</resource1> (<urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2> [W/\"A weak ETag\"]) ([\"strong ETag\"])");
+//  lockManager.evaluateCondition(aFile, "(<urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2>) (Not <DAV:no-lock>)");
+//  lockManager.evaluateCondition(aFile, "(Not <urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2> <urn:uuid:58f202ac-22cf-11d1-b12d-002035b29092>)");
+//  lockManager.evaluateCondition(aFile, "</specs/rfc2518.doc> ([\"4217\"])");
+//  lockManager.evaluateCondition(aFile, "</specs/rfc2518.doc> (Not [\"4217\"])");
+//  lockManager.evaluateCondition(aFile, "(<urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2>) (Not <DAV:no-lock>) </specs/rfc2518.doc> (Not [\"4217\"])");
+//  lockManager.evaluateCondition(aFile, "(<opaquelocktoken:10a098> [10a198]) (Not <DAV:no-lock> [10a198])");
+
+//  public void testLockConditionRequiredException() {
+//    Lock sharedLock = new Lock(aFile, Lock.WRITE, Lock.SHARED, OWNER_STR, 0, 3600);
+//    try {
+//      LockManager.getInstance().acquireLock(sharedLock);
+//      LockManager.getInstance().checkCondition(aFile, null);
+//      assertTrue("checkCondition() should fail", false);
+//    } catch (Exception e) {
+//      assertEquals(LockConditionRequiredException.class, e.getClass());
+//    }
+//  }
+}
blob - /dev/null
blob + 2389b60f777cec922576a63c3d4ad792dd4073da (mode 644)
--- /dev/null
+++ modules/webdav/src/test/java/com/thinkberg/webdav/DavTestCase.java
@@ -0,0 +1,81 @@
+package com.thinkberg.webdav;
+
+import com.thinkberg.webdav.data.DavResource;
+import com.thinkberg.webdav.data.DavResourceFactory;
+import junit.framework.TestCase;
+import org.apache.commons.vfs.FileObject;
+import org.apache.commons.vfs.FileSystemException;
+import org.apache.commons.vfs.FileSystemManager;
+import org.apache.commons.vfs.VFS;
+import org.dom4j.DocumentHelper;
+import org.dom4j.Element;
+import org.dom4j.Node;
+
+import java.util.Arrays;
+
+/**
+ * Helper class for DAV tests.
+ *
+ * @author Matthias L. Jugel
+ */
+public class DavTestCase extends TestCase {
+  private static final String PROP_EXISTS = "propstat[status='HTTP/1.1 200 OK']/prop/";
+  private static final String PROP_MISSING = "propstat[status='HTTP/1.1 404 Not Found']/prop/";
+  private static final String EMPTY = "";
+
+  protected FileObject aFile;
+  protected FileObject aDirectory;
+
+
+  protected void setUp() throws Exception {
+    super.setUp();
+    FileSystemManager fsm = VFS.getManager();
+    FileObject fsRoot = fsm.createVirtualFileSystem(fsm.resolveFile("ram:/"));
+    aFile = fsRoot.resolveFile("/file.txt");
+    aFile.delete();
+    aFile.createFile();
+    aDirectory = fsRoot.resolveFile("/folder");
+    aDirectory.delete();
+    aDirectory.createFolder();
+  }
+
+  protected void testPropertyValue(FileObject object, String propertyName, String propertyValue) throws FileSystemException {
+    Element root = serializeDavResource(object, propertyName);
+    assertEquals(propertyValue, selectExistingPropertyValue(root, propertyName));
+  }
+
+  protected void testPropertyNoValue(FileObject object, String propertyName) throws FileSystemException {
+    Element root = serializeDavResource(object, propertyName, true);
+    assertEquals(EMPTY, selectExistingPropertyValue(root, propertyName));
+  }
+
+  protected Node selectExistingProperty(Element root, String propertyName) {
+    return root.selectSingleNode(PROP_EXISTS + propertyName);
+  }
+
+  protected Node selectMissingProperty(Element root, String propertyName) {
+    return root.selectSingleNode(PROP_MISSING + propertyName);
+  }
+
+  protected String selectMissingPropertyName(Element root, String propertyName) {
+    return selectMissingProperty(root, propertyName).getName();
+  }
+
+  protected String selectExistingPropertyValue(Element root, String propertyName) {
+    return selectExistingProperty(root, propertyName).getText();
+  }
+
+  protected Element serializeDavResource(FileObject object, String propertyName) throws FileSystemException {
+    return serializeDavResource(object, propertyName, false);
+  }
+
+  protected Element serializeDavResource(FileObject object, String propertyName, boolean ignoreValues) throws FileSystemException {
+    Element root = DocumentHelper.createElement("root");
+    DavResourceFactory factory = DavResourceFactory.getInstance();
+    DavResource davResource = factory.getDavResource(object);
+    davResource.setIgnoreValues(ignoreValues);
+    davResource.serializeToXml(root, Arrays.asList(propertyName));
+    return root;
+  }
+
+}