commit c69f66ef9a0647aa8c88102bf4d4ff9eaa4c2d65 from: leo date: Sun Jan 25 14:22:18 2009 UTC implementes condition syntax parsing lock handling now implemented (ETag unpredictable) 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 @@ + + + src/main/resources + + maven-compiler-plugin + true + true 1.5 1.5 + + org.apache.maven.plugins + maven-surefire-plugin + + + **/MoxoTest.java + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.maven.plugins maven-jar-plugin 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 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 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 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. + *

+ * Evaluate example 2:
+ * + * URI(/resource1) { ( + * is-locked-with(urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2) + * AND matches-etag(W/"A weak ETag") ) + * OR ( matches-etag("strong ETag") ) } + * + *

+ * Examples: + *

    + *
  1. <http://cid:8080/litmus/unmapped_url> (<opaquelocktoken:cd6798>)
  2. + *
  3. </resource1> (<urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2> [W/"A weak ETag"]) (["strong ETag"])
  4. + *
  5. (<urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2>) (Not <DAV:no-lock>)
  6. + *
  7. (Not <urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2> <urn:uuid:58f202ac-22cf-11d1-b12d-002035b29092>)
  8. + *
  9. </specs/rfc2518.doc> (["4217"])
  10. + *
  11. </specs/rfc2518.doc> (Not ["4217"])
  12. + *
* - * @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 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 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 locks = new ArrayList(); + 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 = " ()"; + 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 )"; + assertTrue("using (Not ) 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 [" + eTag + "x])"; + LockManager.getInstance().acquireLock(aLock); + assertFalse("complex condition with bogus eTag should fail", + LockManager.getInstance().evaluateCondition(aFile, condition).result); + } + + +// assertFalse(lockManager.evaluateCondition(aFile, " ( [W/\"A weak ETag\"]) ([\"strong ETag\"])"); +// lockManager.evaluateCondition(aFile, "() (Not )"); +// lockManager.evaluateCondition(aFile, "(Not )"); +// lockManager.evaluateCondition(aFile, " ([\"4217\"])"); +// lockManager.evaluateCondition(aFile, " (Not [\"4217\"])"); +// lockManager.evaluateCondition(aFile, "() (Not ) (Not [\"4217\"])"); +// lockManager.evaluateCondition(aFile, "( [10a198]) (Not [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); } }