Commit Diff


commit - 4dc61f92ad9f3fa09122a917118cb69c45840845
commit + c69f66ef9a0647aa8c88102bf4d4ff9eaa4c2d65
blob - 7028744f430a3e18a81939ea3f859c9b5e991eb1
blob + b50dc5fadc645fab21c1f92e533ec272bcb8d346
--- NOTICE
+++ NOTICE
@@ -4,7 +4,7 @@
 =========================================================================
 
 Moxo S3 DAV Proxy Server
-Copyright 2007 Matthias L. Jugel.
+Copyright 2007, 2009 Matthias L. Jugel.
 
 This product includes software developed at
 
blob - 7cdf942f8bdb14058f151c877fbf6464d0769b03
blob + cc9de27a8bca90a735b26b36d7a4451b76a6ce78
--- README
+++ README
@@ -1,5 +1,5 @@
 Moxo S3 DAV Proxy Server
-Copyright 2007 Matthias L. Jugel. See LICENSE for details.
+Copyright 2007, 2009 Matthias L. Jugel. See LICENSE for details.
 http://thinkberg.com/
 
 This is a first go on two issues:
@@ -37,6 +37,7 @@ TODO:
 - Create an executable JAR with all required libraries. The Main is already prepared to do
   that but I have not yet fully understood how to get maven to package the jars right next
   to the compiled classes.
+- implement a good way to handle ETags for webdav (using vfs)
 - WebDAV property handling
 - S3 ACL support
 - separated jar packages for the vfs backend and the dav frontend
blob - 1582d815464b5a6997af06d36a8f835f6544238a
blob + 7ab330241c049a148fc7a2505e2460890c3acc73
--- pom.xml
+++ pom.xml
@@ -58,16 +58,66 @@
         </dependency>
     </dependencies>
     <build>
+        <resources>
+            <resource>
+                <directory>src/main/resources</directory>
+            </resource>
+        </resources>
         <plugins>
             <plugin>
                 <artifactId>maven-compiler-plugin</artifactId>
                 <configuration>
+                    <verbose>true</verbose>
+                    <fork>true</fork>
                     <source>1.5</source>
                     <target>1.5</target>
                 </configuration>
             </plugin>
+
+            <!--
+              mvn test
+              Documentation: http://maven.apache.org/plugins/maven-surefire-plugin/
+            -->
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <configuration>
+                    <includes>
+                        <include>**/MoxoTest.java</include>
+                    </includes>
+                </configuration>
+            </plugin>
+
+            <!--
+               jar
+               mvn package
+               Documentation: http://maven.apache.org/plugins/maven-jar-plugin/
+             -->
+            <!--<plugin>-->
+            <!--<groupId>org.apache.maven.plugins</groupId>-->
+            <!--<artifactId>maven-jar-plugin</artifactId>-->
+            <!--<executions>-->
+            <!--<execution>-->
+            <!--<phase>package</phase>-->
+            <!--<goals>-->
+            <!--<goal>jar</goal>-->
+            <!--</goals>-->
+            <!--<configuration>-->
+            <!--<classifier>client</classifier>-->
+            <!--<includes>-->
+            <!--<include>**/client/*</include>-->
+            <!--<include>**/servlet/*</include>-->
+            <!--<include>barcode-client.properties</include>-->
+            <!--<include>log4j.properties</include>-->
+            <!--<include>images/*</include>-->
+            <!--<include>templates/*</include>-->
+            <!--</includes>-->
+            <!--</configuration>-->
+            <!--</execution>-->
+            <!--</executions>-->
+            <!--</plugin>-->
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-jar-plugin</artifactId>
                 <configuration>
                     <archive>
blob - 12c455d7b8e0da5274b892269a2919198c3cace9
blob + 3e03b367c7a948895653aafa48ac0d07ee118c5f
--- src/main/java/com/thinkberg/moxo/dav/CopyMoveBase.java
+++ src/main/java/com/thinkberg/moxo/dav/CopyMoveBase.java
@@ -25,6 +25,7 @@ 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
@@ -37,20 +38,27 @@ public abstract class CopyMoveBase extends WebdavHandl
     FileObject object = getVFSObject(request.getPathInfo());
     FileObject targetObject = getDestination(request);
 
+
     try {
-      // check that we can write the target
-      LockManager.getInstance().checkCondition(targetObject, getIf(request));
-      // if we move, check that we can actually write on the source
+      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())) {
-        LockManager.getInstance().checkCondition(object, getIf(request));
+        evaluation = lockManager.evaluateCondition(object, getIf(request));
+        if (!evaluation.result) {
+          response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+          return;
+        }
       }
     } catch (LockException e) {
-      if (e.getLocks() != null) {
-        response.sendError(SC_LOCKED);
-      } else {
-        response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
-      }
+      response.sendError(SC_LOCKED);
       return;
+    } catch (ParseException e) {
+      response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+      return;
     }
 
 
blob - bd7f9830c75e3fb90449db9a7f2685aeeff2e237
blob + 1cd9fda42df8a35bd14e6be5cd93938a0344a9ce
--- src/main/java/com/thinkberg/moxo/dav/DeleteHandler.java
+++ src/main/java/com/thinkberg/moxo/dav/DeleteHandler.java
@@ -28,6 +28,7 @@ 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
@@ -57,14 +58,16 @@ public class DeleteHandler extends WebdavHandler {
     }
 
     try {
-      LockManager.getInstance().checkCondition(object, getIf(request));
-    } catch (LockException e) {
-      if (e.getLocks() != null) {
-        response.sendError(SC_LOCKED);
-      } else {
+      if (!LockManager.getInstance().evaluateCondition(object, getIf(request)).result) {
         response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+        return;
       }
+    } catch (LockException e) {
+      response.sendError(WebdavHandler.SC_LOCKED);
       return;
+    } catch (ParseException e) {
+      response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+      return;
     }
 
     if (object.exists()) {
blob - 2d54a47a9b4dc425e1300cefea568d7718c7f90f
blob + 2a0d7c02a9d33230c3492d40a0a444e5c8d0efb1
--- src/main/java/com/thinkberg/moxo/dav/GetHandler.java
+++ src/main/java/com/thinkberg/moxo/dav/GetHandler.java
@@ -56,6 +56,7 @@ public class GetHandler extends WebdavHandler {
   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 - d4223a80e73f36560895ea20948921ce11bb8739
blob + 2cf6621edb3bdded57a86109a03ee75a17d97cfc
--- src/main/java/com/thinkberg/moxo/dav/LockHandler.java
+++ src/main/java/com/thinkberg/moxo/dav/LockHandler.java
@@ -18,7 +18,6 @@ package com.thinkberg.moxo.dav;
 
 import com.thinkberg.moxo.dav.lock.Lock;
 import com.thinkberg.moxo.dav.lock.LockConflictException;
-import com.thinkberg.moxo.dav.lock.LockException;
 import com.thinkberg.moxo.dav.lock.LockManager;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
@@ -33,7 +32,9 @@ 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.
@@ -57,13 +58,29 @@ public class LockHandler extends WebdavHandler {
     FileObject object = getVFSObject(request.getPathInfo());
 
     try {
-      Lock lock = LockManager.getInstance().checkCondition(object, getIf(request));
-      if (lock != null) {
-        sendLockAcquiredResponse(response, lock);
+      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 (LockException e) {
-      // handle locks below
+    } catch (LockConflictException e) {
+      List<Lock> locks = e.getLocks();
+      for (Lock lock : locks) {
+        if (Lock.EXCLUSIVE.equals(lock.getType())) {
+          response.sendError(WebdavHandler.SC_LOCKED);
+          return;
+        }
+      }
+    } catch (ParseException e) {
+      response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+      return;
     }
 
     try {
blob - 26c2f2d817fea565d73ce19efee0bf802f26c328
blob + 3edc720043d1911ec3a98cd4b63e24554884e6a9
--- src/main/java/com/thinkberg/moxo/dav/MkColHandler.java
+++ src/main/java/com/thinkberg/moxo/dav/MkColHandler.java
@@ -26,6 +26,7 @@ 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
@@ -44,14 +45,16 @@ public class MkColHandler extends WebdavHandler {
     FileObject object = getVFSObject(request.getPathInfo());
 
     try {
-      LockManager.getInstance().checkCondition(object, getIf(request));
-    } catch (LockException e) {
-      if (e.getLocks() != null) {
-        response.sendError(SC_LOCKED);
-      } else {
+      if (!LockManager.getInstance().evaluateCondition(object, getIf(request)).result) {
         response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+        return;
       }
+    } catch (LockException e) {
+      response.sendError(WebdavHandler.SC_LOCKED);
       return;
+    } catch (ParseException e) {
+      response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+      return;
     }
 
     if (object.exists()) {
blob - 47200c23bde07c62df6a59cebb06f1d458fe4661
blob + 029e0da28dec7284d703d42725af8549d2f49ae5
--- src/main/java/com/thinkberg/moxo/dav/PropPatchHandler.java
+++ src/main/java/com/thinkberg/moxo/dav/PropPatchHandler.java
@@ -34,6 +34,7 @@ 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;
 
 /**
@@ -49,14 +50,16 @@ public class PropPatchHandler extends WebdavHandler {
     FileObject object = getVFSObject(request.getPathInfo());
 
     try {
-      LockManager.getInstance().checkCondition(object, getIf(request));
-    } catch (LockException e) {
-      if (e.getLocks() != null) {
-        response.sendError(SC_LOCKED);
-      } else {
+      if (!LockManager.getInstance().evaluateCondition(object, getIf(request)).result) {
         response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+        return;
       }
+    } catch (LockException e) {
+      response.sendError(WebdavHandler.SC_LOCKED);
       return;
+    } catch (ParseException e) {
+      response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+      return;
     }
 
     SAXReader saxReader = new SAXReader();
blob - fc92a3d6e348a399694d0d684681d25c85f99d27
blob + ed879c915a369d9649cbba45551e983960a733ad
--- src/main/java/com/thinkberg/moxo/dav/PutHandler.java
+++ src/main/java/com/thinkberg/moxo/dav/PutHandler.java
@@ -28,6 +28,7 @@ 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
@@ -40,16 +41,17 @@ public class PutHandler extends WebdavHandler {
     FileObject object = getVFSObject(request.getPathInfo());
 
     try {
-      LockManager.getInstance().checkCondition(object, getIf(request));
-    } catch (LockException e) {
-      if (e.getLocks() != null) {
-        response.sendError(SC_LOCKED);
-      } else {
+      if (!LockManager.getInstance().evaluateCondition(object, getIf(request)).result) {
         response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+        return;
       }
+    } catch (LockException e) {
+      response.sendError(WebdavHandler.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);
@@ -69,8 +71,9 @@ public class PutHandler extends WebdavHandler {
 
     InputStream is = request.getInputStream();
     OutputStream os = object.getContent().getOutputStream();
-    int bytesCopied = Util.copyStream(is, os);
-    LOG.debug("sending " + bytesCopied + "/" + request.getHeader("Content-length") + " bytes");
+    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();
 
blob - 6cc9911ce6b996e0c0610c4987c090772f356817
blob + 9b6e71d63c5a52df24c66a3d45ff9da5b93a6115
--- src/main/java/com/thinkberg/moxo/dav/Util.java
+++ src/main/java/com/thinkberg/moxo/dav/Util.java
@@ -19,6 +19,10 @@ package com.thinkberg.moxo.dav;
 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;
@@ -40,15 +44,25 @@ public class Util {
 //  }
 
 
-  public static int copyStream(InputStream is, OutputStream os) throws IOException {
-    byte[] buffer = new byte[8192];
-    int bytesRead, bytesCount = 0;
-    while ((bytesRead = is.read(buffer)) != -1) {
-      os.write(buffer, 0, bytesRead);
-      bytesCount += bytesRead;
+  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();
     }
-    os.flush();
+    buffer.flip();
+    while (buffer.hasRemaining()) {
+      bytesWritten += wbc.write(buffer);
+    }
 
-    return bytesCount;
+    rbc.close();
+    wbc.close();
+
+    return bytesWritten;
   }
 }
blob - bb559777fd0928f6c9ee3f7bb12b897d76ffc0d7
blob + b52d655c1ac100625581cb02aba7e7f923b502b6
--- src/main/java/com/thinkberg/moxo/dav/WebdavHandler.java
+++ src/main/java/com/thinkberg/moxo/dav/WebdavHandler.java
@@ -51,7 +51,7 @@ public abstract class WebdavHandler {
       FileSystemManager fsm = VFS.getManager();
 
       // create a virtual filesystemusing the url provided or fall back to RAM
-      fileSystemRoot = fsm.resolveFile(properties.getStringProperty("vfs.url", "ram:/"));
+      fileSystemRoot = fsm.resolveFile(properties.getStringProperty("vfs.uri", "ram:/"));
 
       LOG.info("created virtual file system: " + fileSystemRoot);
     } catch (FileSystemException e) {
@@ -97,7 +97,7 @@ public abstract class WebdavHandler {
       depthValue = Integer.parseInt(depth);
     }
 
-    LOG.debug("request header: Depth: " + (depthValue == Integer.MAX_VALUE ? "infinity" : depthValue));
+    LOG.debug(String.format("request header: Depth: %s", (depthValue == Integer.MAX_VALUE ? "infinity" : depthValue)));
     return depthValue;
   }
 
@@ -112,7 +112,7 @@ public abstract class WebdavHandler {
     String overwrite = request.getHeader("Overwrite");
     boolean overwriteValue = overwrite == null || "T".equals(overwrite);
 
-    LOG.debug("request header: Overwrite: " + overwriteValue);
+    LOG.debug(String.format("request header: Overwrite: %s", overwriteValue));
     return overwriteValue;
   }
 
@@ -131,7 +131,7 @@ public abstract class WebdavHandler {
     if (null != targetUrlStr) {
       URL target = new URL(targetUrlStr);
       targetObject = getVFSObject(target.getPath());
-      LOG.debug("request header: Destination: " + targetObject.getName().getPath());
+      LOG.debug(String.format("request header: Destination: %s", targetObject.getName().getPath()));
     }
 
     return targetObject;
@@ -147,7 +147,7 @@ public abstract class WebdavHandler {
     String getIfHeader = request.getHeader("If");
 
     if (null != getIfHeader) {
-      LOG.debug("request header: If: " + getIfHeader);
+      LOG.debug(String.format("request header: If: '%s'", getIfHeader));
     }
     return getIfHeader;
   }
@@ -162,7 +162,7 @@ public abstract class WebdavHandler {
     String timeout = request.getHeader("Timeout");
     if (null != timeout) {
       String[] timeoutValues = timeout.split(",[ ]*");
-      LOG.debug("request header: Timeout: " + Arrays.asList(timeoutValues).toString());
+      LOG.debug(String.format("request header: Timeout: %s", Arrays.asList(timeoutValues).toString()));
       if ("infinity".equalsIgnoreCase(timeoutValues[0])) {
         return -1;
       } else {
blob - 60ddbc7d9240cdb82e2d97eba5c93761a077ca9d
blob + 7b8460688401f8da61a40a80db7fe85b44f0b4c3
--- src/main/java/com/thinkberg/moxo/dav/data/DavResource.java
+++ src/main/java/com/thinkberg/moxo/dav/data/DavResource.java
@@ -63,8 +63,7 @@ public class DavResource extends AbstractDavResource {
           PROP_LOCK_DISCOVERY,
           PROP_RESOURCETYPE,
           PROP_SOURCE,
-          PROP_SUPPORTED_LOCK,
-          PROP_QUOTA_AVAILABLE_BYTES
+          PROP_SUPPORTED_LOCK
   );
 
   protected final FileObject object;
blob - 28d71a7844d3ecca4d5bc448fb9e7c6e47913291
blob + 1e07eee9313edf1cf95329fbec218d5878378116
--- src/main/java/com/thinkberg/moxo/dav/lock/Lock.java
+++ src/main/java/com/thinkberg/moxo/dav/lock/Lock.java
@@ -132,10 +132,7 @@ public class Lock {
 
 
   public String toString() {
-    return new StringBuffer().append("Lock[")
-            .append(object).append(",")
-            .append(type).append(",")
-            .append(scope).append("]").toString();
+    return String.format("Lock[%s,%s,%s,%s]", object, type, scope, token);
   }
 }
 
blob - /dev/null
blob + 217437da42098d75357b98160f381a27ba1e4c95 (mode 644)
--- /dev/null
+++ src/main/java/com/thinkberg/moxo/dav/lock/LockConditionRequiredException.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2009 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.moxo.dav.lock;
+
+import java.util.List;
+
+public class LockConditionRequiredException extends LockException {
+  public LockConditionRequiredException(List<Lock> locks) {
+    super(locks);
+  }
+}
blob - 937cae1dfcf4c4ed1d768feb4e82ebf20cff4a46
blob + 1ca6c2f973c038a647cd769743a77a78708243bc
--- src/main/java/com/thinkberg/moxo/dav/lock/LockException.java
+++ src/main/java/com/thinkberg/moxo/dav/lock/LockException.java
@@ -33,4 +33,8 @@ public class LockException extends Exception {
   public List<Lock> getLocks() {
     return locks;
   }
+
+  public String toString() {
+    return String.format("[%s: %s]", this.getClass(), locks);
+  }
 }
blob - 3a78f6d504fbd03a7ef5615a5b68b6a02b64931e
blob + ffac2fafb4c81fc700c4686ab276f1625eebe709
--- src/main/java/com/thinkberg/moxo/dav/lock/LockManager.java
+++ src/main/java/com/thinkberg/moxo/dav/lock/LockManager.java
@@ -17,14 +17,21 @@
 package com.thinkberg.moxo.dav.lock;
 
 import com.thinkberg.moxo.vfs.DepthFileSelector;
+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.FileSystemException;
 
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
  * The lock manager is responsible for exclusive and shared write locks on the
@@ -36,7 +43,15 @@ import java.util.Map;
  */
 public class LockManager {
   private static LockManager instance = null;
+  private static final Log LOG = LogFactory.getLog(LockManager.class);
 
+  // condition parser patterns and tokens
+  private static final Pattern IF_PATTERN = Pattern.compile("(<[^>]+>)|(\\([^)]+\\))");
+  private static final Pattern CONDITION_PATTERN = Pattern.compile("([Nn][Oo][Tt])|(<[^>]+>)|(\\[[^]]+\\])");
+  private static final char TOKEN_LOWER_THAN = '<';
+  private static final char TOKEN_LEFT_BRACE = '(';
+  private static final char TOKEN_LEFT_BRACKET = '[';
+
   /**
    * Get an instance of the lock manager.
    *
@@ -117,44 +132,137 @@ public class LockManager {
   }
 
   /**
-   * Check a condition for a file object. The condition check looks for locks on the
-   * given file object and will throw exceptions if the condition does not meet the
-   * lock requirements (i.e. lock token in the condition differes from the token in
-   * the discovered lock) or no condition exists if a lock was discovered. If no lock
-   * was discovered but a condition exists a lock condition exception will be thrown.
+   * Evaluate an 'If:' header condition.
+   * The condition may be a tagged list or an untagged list. Tagged lists define the resource, the condition
+   * applies to in front of the condition (ex. 1, 2, 5, 6). Conditions may be inverted by using 'Not' at the
+   * beginning of the condition (ex. 3, 4, 6). The list constitutes an OR expression while the list of
+   * conditions within braces () constitutes an AND expression.
+   * <p/>
+   * Evaluate example 2:<br/>
+   * <code>
+   * URI(/resource1) { (
+   * is-locked-with(urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2)
+   * AND matches-etag(W/"A weak ETag") )
+   * OR ( matches-etag("strong ETag") ) }
+   * </code>
+   * <p/>
+   * Examples:
+   * <ol>
+   * <li> &lt;http://cid:8080/litmus/unmapped_url&gt; (&lt;opaquelocktoken:cd6798&gt;)</li>
+   * <li> &lt;/resource1&gt; (&lt;urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2&gt; [W/"A weak ETag"]) (["strong ETag"])</li>
+   * <li> (&lt;urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2&gt;) (Not &lt;DAV:no-lock&gt;)</li>
+   * <li> (Not &lt;urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2&gt; &lt;urn:uuid:58f202ac-22cf-11d1-b12d-002035b29092&gt;)</li>
+   * <li> &lt;/specs/rfc2518.doc&gt; (["4217"])</li>
+   * <li> &lt;/specs/rfc2518.doc&gt; (Not ["4217"])</li>
+   * </ol>
    *
-   * @param object the file object in question
-   * @param ifCond the condition to check
-   * @return the lock found for the given if condition and the object
-   * @throws FileSystemException          if the object or path cannot be accessed
-   * @throws LockConflictException        if there is a a lock but no condition
-   * @throws LockConditionFailedException if the condition and lock does not match
+   * @param contextObject the contextual resource (needed when the If: condition is not tagged)
+   * @param ifCondition   the string of the condition as sent by the If: header
+   * @return evaluation of the condition expression
+   * @throws ParseException        if the condition does not meet the syntax requirements
+   * @throws LockConflictException
+   * @throws FileSystemException
    */
-  public Lock checkCondition(FileObject object, String ifCond)
-          throws FileSystemException, LockConflictException, LockConditionFailedException {
-    List<Lock> locks = discoverLock(object);
-    if (null != locks && !locks.isEmpty()) {
-      // if there is no condition but a lock, this must fail
-      if (null == ifCond) {
+  public EvaluationResult evaluateCondition(FileObject contextObject, String ifCondition)
+          throws FileSystemException, LockConflictException, ParseException {
+    List<Lock> locks = discoverLock(contextObject);
+    EvaluationResult evaluation = new EvaluationResult();
+
+    if (ifCondition == null || "".equals(ifCondition)) {
+      if (locks != null) {
         throw new LockConflictException(locks);
       }
+      evaluation.result = true;
+      return evaluation;
+    }
 
-      // simple check whether the token is in the condition (TODO: check for NOT)
-      for (Lock lock : locks) {
-        if (ifCond.indexOf("<" + lock.getToken() + ">") != -1) {
-          return lock;
-        }
+    Matcher matcher = IF_PATTERN.matcher(ifCondition);
+    FileObject resource = contextObject;
+    while (matcher.find()) {
+      String token = matcher.group();
+      switch (token.charAt(0)) {
+        case TOKEN_LOWER_THAN:
+          String resourceUri = token.substring(1, token.length() - 1);
+          try {
+            resource = contextObject.getFileSystem().resolveFile(new URI(resourceUri).getPath());
+            locks = discoverLock(resource);
+          } catch (URISyntaxException e) {
+            throw new ParseException(ifCondition, matcher.start());
+          }
+          break;
+        case TOKEN_LEFT_BRACE:
+          LOG.debug(String.format("URI(%s) {", resource));
+          Matcher condMatcher = CONDITION_PATTERN.matcher(token.substring(1, token.length() - 1));
+          boolean expressionResult = true;
+          while (condMatcher.find()) {
+            String condToken = condMatcher.group();
+            boolean negate = false;
+            if (condToken.matches("[Nn][Oo][Tt]")) {
+              negate = true;
+              condMatcher.find();
+              condToken = condMatcher.group();
+            }
+            switch (condToken.charAt(0)) {
+              case TOKEN_LOWER_THAN:
+                String lockToken = condToken.substring(1, condToken.length() - 1);
+
+                boolean foundLock = false;
+                if (locks != null) {
+                  for (Lock lock : locks) {
+                    if (lockToken.equals(lock.getToken())) {
+                      evaluation.locks.add(lock);
+                      foundLock = true;
+                      break;
+                    }
+                  }
+                }
+                final boolean foundLockResult = negate ? !foundLock : foundLock;
+                LOG.debug(String.format("  %sis-locked-with(%s) = %b",
+                                        negate ? "NOT " : "", lockToken, foundLockResult));
+                expressionResult = expressionResult && foundLockResult;
+                break;
+              case TOKEN_LEFT_BRACKET:
+                String eTag = condToken.substring(1, condToken.length() - 1);
+                String resourceETag = String.format("%x", resource.hashCode());
+                boolean resourceTagMatches = resourceETag.equals(eTag);
+                final boolean matchesEtagResult = negate ? !resourceTagMatches : resourceTagMatches;
+                LOG.debug(String.format("  %smatches-etag(%s) = %b",
+                                        negate ? "NOT " : "", eTag, matchesEtagResult));
+                expressionResult = expressionResult && matchesEtagResult;
+                break;
+              default:
+                throw new ParseException(String.format("syntax error in condition '%s' at %d",
+                                                       ifCondition, matcher.start() + condMatcher.start()),
+                                         matcher.start() + condMatcher.start());
+            }
+          }
+
+          evaluation.result = evaluation.result || expressionResult;
+          LOG.debug("} => " + evaluation.result);
+          break;
+        default:
+          throw new ParseException(String.format("syntax error in condition '%s' at %d", ifCondition, matcher.start()),
+                                   matcher.start());
       }
-      throw new LockConditionFailedException(locks);
-    } else if (null != ifCond) {
-      // no lock but a condition must fail too
-      throw new LockConditionFailedException(null);
     }
 
-    return null;
+    // regardless of the evaluation, if the object is locked but there is no valed lock token in the
+    // conditions we must fail with a lock conflict too
+    if (evaluation.result && (locks != null && !locks.isEmpty()) && evaluation.locks.isEmpty()) {
+      throw new LockConflictException(locks);
+    }
+    return evaluation;
   }
 
+  public class EvaluationResult {
+    public List<Lock> locks = new ArrayList<Lock>();
+    public boolean result = false;
 
+    public String toString() {
+      return String.format("EvaluationResult[%b,%s]", result, locks);
+    }
+  }
+
   /**
    * Add a lock to the list of shared locks of a given object.
    *
@@ -210,4 +318,5 @@ public class LockManager {
       }, false, new ArrayList());
     }
   }
+
 }
blob - c8a306a053d53a8a29bdcfae8a10e3c852f7f3b0
blob + 094aa76addd911ee350612557694b1c5fecd0879
--- src/main/java/com/thinkberg/moxo/servlet/MoxoWebDAVServlet.java
+++ src/main/java/com/thinkberg/moxo/servlet/MoxoWebDAVServlet.java
@@ -78,10 +78,16 @@ public class MoxoWebDAVServlet extends HttpServlet {
 //    }
 
 
+    // show we are doing the litmus test
+    String litmusTest = request.getHeader("X-Litmus");
+    if (null == litmusTest) {
+      litmusTest = request.getHeader("X-Litmus-Second");
+    }
+    if (litmusTest != null) {
+      LOG.info(String.format("WebDAV Litmus Test: %s", litmusTest));
+    }
+
     String method = request.getMethod();
-    if (request.getHeader("X-Litmus") != null) {
-      LOG.info(String.format("WebDAV Litmus Test: %s", request.getHeader("X-Litmus")));
-    }
     LOG.debug(String.format(">> %s %s", request.getMethod(), request.getPathInfo()));
     if (handlers.containsKey(method)) {
       handlers.get(method).service(request, response);
@@ -90,6 +96,6 @@ public class MoxoWebDAVServlet extends HttpServlet {
     }
     Response jettyResponse = ((Response) response);
     String reason = jettyResponse.getReason();
-    LOG.debug(String.format("<< %s (%b%s)", request.getMethod(), jettyResponse.getStatus(), reason != null ? ": " + reason : ""));
+    LOG.debug(String.format("<< %s (%d%s)", request.getMethod(), jettyResponse.getStatus(), reason != null ? ": " + reason : ""));
   }
 }
blob - 5c2181514077735de9eaee6f6f59f9db52ebc405
blob + 1c80a615bbd89066c8f4fccfe435b9b490d829b9
--- src/main/java/com/thinkberg/moxo/vfs/jets3t/Jets3tFileSystem.java
+++ src/main/java/com/thinkberg/moxo/vfs/jets3t/Jets3tFileSystem.java
@@ -48,10 +48,10 @@ public class Jets3tFileSystem extends AbstractFileSyst
     try {
       service = Jets3tConnector.getInstance().getService();
       if (!service.isBucketAccessible(bucketId)) {
-        LOG.info("creating new S3 bucket (" + bucketId + ") for file system");
+        LOG.info(String.format("creating new S3 bucket '%s' for file system root", bucketId));
         bucket = service.createBucket(bucketId);
       } else {
-        LOG.info("using existing S3 bucket: " + bucketId);
+        LOG.info(String.format("using existing S3 bucket '%s' for file system root", bucketId));
         bucket = new S3Bucket(bucketId);
       }
     } catch (S3ServiceException e) {
@@ -59,6 +59,16 @@ public class Jets3tFileSystem extends AbstractFileSyst
     }
   }
 
+  public void destroyFileSystem() throws FileSystemException {
+    try {
+      service.deleteBucket(bucket);
+    } catch (S3ServiceException e) {
+      throw new FileSystemException("can't delete file system root", e);
+    }
+
+
+  }
+
   @SuppressWarnings({"unchecked"})
   protected void addCapabilities(Collection caps) {
     caps.addAll(S3FileProvider.capabilities);
@@ -67,4 +77,6 @@ public class Jets3tFileSystem extends AbstractFileSyst
   protected FileObject createFile(FileName fileName) throws Exception {
     return new Jets3tFileObject(fileName, this, service, bucket);
   }
+
+
 }
blob - 9ac36182f7b6d695e49901c76968ce5efc0a39bf
blob + 674ed457589717c031e07eeae6ec32eea0b1deb7
--- src/test/java/com/thinkberg/moxo/MoxoTest.java
+++ src/test/java/com/thinkberg/moxo/MoxoTest.java
@@ -38,7 +38,7 @@ public class MoxoTest extends TestCase {
     String propertiesFileName = System.getProperty("moxo.properties", "moxo.properties");
     Jets3tProperties properties = Jets3tProperties.getInstance(propertiesFileName);
 
-    String vfsUrl = properties.getStringProperty("vfs.url", null);
+    String vfsUrl = properties.getStringProperty("vfs.uri", null);
     if (null != vfsUrl && vfsUrl.startsWith("s3:")) {
       s.addTestSuite(S3FileNameTest.class);
       s.addTestSuite(S3FileProviderTest.class);
blob - 2fa2574ff79738bae97230feeef528a7ff9781d9
blob + df58a524ec201431dcad1cd542b86f58aed5384d
--- src/test/java/com/thinkberg/moxo/dav/DavLockManagerTest.java
+++ src/test/java/com/thinkberg/moxo/dav/DavLockManagerTest.java
@@ -14,7 +14,7 @@ public class DavLockManagerTest extends DavTestCase {
     super();
   }
 
-  public void testAcquireSharedFileLock() {
+  public void testAcquireSingleSharedFileLock() {
     Lock sharedLock = new Lock(aFile, Lock.WRITE, Lock.SHARED, OWNER_STR, 0, 3600);
     try {
       LockManager.getInstance().acquireLock(sharedLock);
@@ -34,7 +34,7 @@ public class DavLockManagerTest extends DavTestCase {
     }
   }
 
-  public void testAcquireExclusiveLock() {
+  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 {
@@ -45,4 +45,124 @@ public class DavLockManagerTest extends DavTestCase {
       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 - 38485aeefa013dff633c31b5e00104bd1efcdd46
blob + 5b2ab9887bbf9efd98ff97084bc3b832cb1ed6ef
--- src/test/java/com/thinkberg/moxo/vfs/S3FileProviderTest.java
+++ src/test/java/com/thinkberg/moxo/vfs/S3FileProviderTest.java
@@ -16,6 +16,7 @@
 
 package com.thinkberg.moxo.vfs;
 
+import com.thinkberg.moxo.vfs.jets3t.Jets3tFileSystem;
 import org.apache.commons.vfs.*;
 
 import java.io.IOException;
@@ -168,13 +169,18 @@ public class S3FileProviderTest extends S3TestCase {
     assertFalse(destFolder.exists());
   }
 
-  public void testMoveFolder() throws FileSystemException {
+//  public void testMoveFolder() throws FileSystemException {
+//
+//  }
 
-  }
-
   public void testCloseFileSystem() throws FileSystemException {
     FileSystem fs = VFS.getManager().resolveFile(ROOT).getFileSystem();
     VFS.getManager().closeFileSystem(fs);
   }
 
+  public void testDestroyFileSystem() throws FileSystemException {
+    FileSystem fs = VFS.getManager().resolveFile(ROOT).getFileSystem();
+    assertTrue(fs instanceof Jets3tFileSystem);
+    ((Jets3tFileSystem) fs).destroyFileSystem();
+  }
 }
blob - 790d3007daff25069746237120ced9b14239a05a
blob + 76282b1a08134826075cca2b0d02c7972257af5c
--- src/test/java/com/thinkberg/moxo/vfs/S3TestCase.java
+++ src/test/java/com/thinkberg/moxo/vfs/S3TestCase.java
@@ -19,6 +19,8 @@ package com.thinkberg.moxo.vfs;
 import junit.framework.TestCase;
 import org.jets3t.service.Jets3tProperties;
 
+import java.util.Random;
+
 /**
  * @author Matthias L. Jugel
  */
@@ -28,6 +30,8 @@ public class S3TestCase extends TestCase {
   static {
     String propertiesFileName = System.getProperty("moxo.properties", "moxo.properties");
     Jets3tProperties properties = Jets3tProperties.getInstance(propertiesFileName);
-    ROOT = properties.getStringProperty("vfs.url", "ram:/");
+    System.out.println("ignoring original vfs.url settings for test: " + properties.getStringProperty("vfs.uri", "ram:/"));
+    ROOT = "s3://MOXOTEST" + String.format("%X", new Random(System.currentTimeMillis()).nextLong()) + "/";
+    System.out.println("using " + ROOT);
   }
 }