Refactor WebDav implementation
authorMathieu Baudier <mbaudier@argeo.org>
Fri, 16 Sep 2022 08:01:03 +0000 (10:01 +0200)
committerMathieu Baudier <mbaudier@argeo.org>
Fri, 16 Sep 2022 08:01:03 +0000 (10:01 +0200)
17 files changed:
org.argeo.api.acr/src/org/argeo/api/acr/ContentNotFoundException.java
org.argeo.cms.ee/src/org/argeo/cms/integration/CmsExceptionsChain.java
org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContentProvider.java
org.argeo.cms/src/org/argeo/cms/acr/xml/DomContentProvider.java
org.argeo.cms/src/org/argeo/cms/dav/DavClient.java
org.argeo.cms/src/org/argeo/cms/dav/DavDepth.java
org.argeo.cms/src/org/argeo/cms/dav/DavHeader.java [deleted file]
org.argeo.cms/src/org/argeo/cms/dav/DavHttpHandler.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/dav/DavMethod.java [deleted file]
org.argeo.cms/src/org/argeo/cms/dav/DavXmlElement.java
org.argeo.cms/src/org/argeo/cms/dav/MultiStatusWriter.java
org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsAcrHttpHandler.java
org.argeo.util/src/org/argeo/util/ExceptionsChain.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/http/HttpHeader.java
org.argeo.util/src/org/argeo/util/http/HttpMethod.java
org.argeo.util/src/org/argeo/util/http/HttpResponseStatus.java
org.argeo.util/src/org/argeo/util/http/HttpServerUtils.java

index b86c92c1ed300b1b80ae4217003400c2a7a886de..51efb3860a49d914b22e090be7750a10804ee91c 100644 (file)
@@ -1,16 +1,35 @@
 package org.argeo.api.acr;
 
-/** When a countent was requested which does not exists, equivalent to HTTP code 404.*/
+/**
+ * When a content was requested which does not exists, equivalent to HTTP code
+ * 404.
+ */
 public class ContentNotFoundException extends RuntimeException {
        private static final long serialVersionUID = -8629074900713760886L;
 
-       public ContentNotFoundException(String message, Throwable cause) {
-               super(message, cause);
+       private final String path;
+
+       public ContentNotFoundException(ContentSession session, String path, Throwable cause) {
+               super(message(session, path), cause);
+               this.path = path;
+               // we don't keep reference to the session for security reasons
+       }
+
+       public ContentNotFoundException(ContentSession session, String path) {
+               this(session, path, (String) null);
        }
 
-       public ContentNotFoundException(String message) {
-               super(message);
+       public ContentNotFoundException(ContentSession session, String path, String message) {
+               super(message != null ? message : message(session, path));
+               this.path = path;
+               // we don't keep reference to the session for security reasons
        }
 
-       
+       private static String message(ContentSession session, String path) {
+               return "Content " + path + "cannot be found.";
+       }
+
+       public String getPath() {
+               return path;
+       }
 }
index fb289c18e946de59dbe2c85a4d25bc76a81687bb..205699464f43aab954127acb2cb82faa211f676d 100644 (file)
@@ -2,29 +2,26 @@ package org.argeo.cms.integration;
 
 import java.io.IOException;
 import java.io.Writer;
-import java.util.ArrayList;
-import java.util.List;
 
 import javax.servlet.http.HttpServletResponse;
 
 import org.argeo.api.cms.CmsLog;
+import org.argeo.util.ExceptionsChain;
 
 import com.fasterxml.jackson.core.JsonGenerator;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
 /** Serialisable wrapper of a {@link Throwable}. */
-public class CmsExceptionsChain {
+public class CmsExceptionsChain extends ExceptionsChain {
        public final static CmsLog log = CmsLog.getLog(CmsExceptionsChain.class);
 
-       private List<SystemException> exceptions = new ArrayList<>();
-
        public CmsExceptionsChain() {
                super();
        }
 
        public CmsExceptionsChain(Throwable exception) {
-               writeException(exception);
+               super(exception);
                if (log.isDebugEnabled())
                        log.error("Exception chain", exception);
        }
@@ -56,77 +53,6 @@ public class CmsExceptionsChain {
                }
        }
 
-       /** recursive */
-       protected void writeException(Throwable exception) {
-               SystemException systemException = new SystemException(exception);
-               exceptions.add(systemException);
-               Throwable cause = exception.getCause();
-               if (cause != null)
-                       writeException(cause);
-       }
-
-       public List<SystemException> getExceptions() {
-               return exceptions;
-       }
-
-       public void setExceptions(List<SystemException> exceptions) {
-               this.exceptions = exceptions;
-       }
-
-       /** An exception in the chain. */
-       public static class SystemException {
-               private String type;
-               private String message;
-               private List<String> stackTrace;
-
-               public SystemException() {
-               }
-
-               public SystemException(Throwable exception) {
-                       this.type = exception.getClass().getName();
-                       this.message = exception.getMessage();
-                       this.stackTrace = new ArrayList<>();
-                       StackTraceElement[] elems = exception.getStackTrace();
-                       for (int i = 0; i < elems.length; i++)
-                               stackTrace.add("at " + elems[i].toString());
-               }
-
-               public String getType() {
-                       return type;
-               }
-
-               public void setType(String type) {
-                       this.type = type;
-               }
-
-               public String getMessage() {
-                       return message;
-               }
-
-               public void setMessage(String message) {
-                       this.message = message;
-               }
-
-               public List<String> getStackTrace() {
-                       return stackTrace;
-               }
-
-               public void setStackTrace(List<String> stackTrace) {
-                       this.stackTrace = stackTrace;
-               }
-
-               @Override
-               public String toString() {
-                       return "System exception: " + type + ", " + message + ", " + stackTrace;
-               }
-
-       }
-
-       @Override
-       public String toString() {
-               return exceptions.toString();
-       }
-
 //     public static void main(String[] args) throws Exception {
 //             try {
 //                     try {
@@ -139,9 +65,9 @@ public class CmsExceptionsChain {
 //                             throw new RuntimeException("Top exception", e);
 //                     }
 //             } catch (Exception e) {
-//                     CmsExceptionsChain vjeSystemErrors = new CmsExceptionsChain(e);
+//                     CmsExceptionsChain systemErrors = new CmsExceptionsChain(e);
 //                     ObjectMapper objectMapper = new ObjectMapper();
-//                     System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(vjeSystemErrors));
+//                     System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(systemErrors));
 //                     System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(e));
 //                     e.printStackTrace();
 //             }
index f4a416aa7bdfefcf38faf4126f249c4bca3638c8..aab5d6dc06a56ab69cdd533a800450c38f01c8a9 100644 (file)
@@ -48,7 +48,8 @@ public class DirectoryContentProvider implements ContentProvider {
                        }
                }
                if (userDirectory == null)
-                       throw new ContentNotFoundException("Cannot find user directory " + userDirectoryName);
+                       throw new ContentNotFoundException(session, mountPath + "/" + relativePath,
+                                       "Cannot find user directory " + userDirectoryName);
                if (segments.size() == 1) {
                        return new DirectoryContent(session, this, userDirectory);
                } else {
@@ -71,7 +72,8 @@ public class DirectoryContentProvider implements ContentProvider {
                        }
                        HierarchyUnit hierarchyUnit = userDirectory.getHierarchyUnit(pathWithinUserDirectory);
                        if (hierarchyUnit == null)
-                               throw new ContentNotFoundException(
+                               throw new ContentNotFoundException(session,
+                                               mountPath + "/" + relativePath + "/" + pathWithinUserDirectory,
                                                "Cannot find " + pathWithinUserDirectory + " within " + userDirectoryName);
                        return new HierarchyUnitContent(session, this, hierarchyUnit);
                }
index f76d38ce07a338401452834a3541400892c5cccf..66ff878d5c6a9a644445a97bafa64869e19e2cfd 100644 (file)
@@ -76,7 +76,8 @@ public class DomContentProvider implements ContentProvider, NamespaceContext {
                if (nodes.getLength() > 1)
                        throw new IllegalArgumentException("Multiple content found for " + relativePath + " under " + mountPath);
                if (nodes.getLength() == 0)
-                       throw new ContentNotFoundException("Path " + relativePath + " under " + mountPath + " was not found");
+                       throw new ContentNotFoundException(session, mountPath + "/" + relativePath,
+                                       "Path " + relativePath + " under " + mountPath + " was not found");
                Element element = (Element) nodes.item(0);
                return new DomContent(session, this, element);
        }
index 5788a32b462e78dcbe6e25e6a53a392efc78c803..e1e5f749948d6c6cdc5a368b12e555428f9b2b02 100644 (file)
@@ -2,8 +2,6 @@ package org.argeo.cms.dav;
 
 import java.io.IOException;
 import java.io.InputStream;
-import java.net.Authenticator;
-import java.net.PasswordAuthentication;
 import java.net.URI;
 import java.net.http.HttpClient;
 import java.net.http.HttpRequest;
@@ -15,6 +13,8 @@ import java.util.Iterator;
 
 import javax.xml.namespace.QName;
 
+import org.argeo.util.http.HttpHeader;
+import org.argeo.util.http.HttpMethod;
 import org.argeo.util.http.HttpResponseStatus;
 
 public class DavClient {
@@ -56,8 +56,8 @@ public class DavClient {
                                                        """;
                        System.out.println(body);
                        HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)) //
-                                       .header("Depth", "1") //
-                                       .method(DavMethod.PROPPATCH.name(), BodyPublishers.ofString(body)) //
+                                       .header(HttpHeader.DEPTH.getHeaderName(), DavDepth.DEPTH_1.getValue()) //
+                                       .method(HttpMethod.PROPPATCH.name(), BodyPublishers.ofString(body)) //
                                        .build();
                        BodyHandler<String> bodyHandler = BodyHandlers.ofString();
                        HttpResponse<String> response = httpClient.send(request, bodyHandler);
@@ -76,8 +76,8 @@ public class DavClient {
                                          <D:propname/>
                                        </D:propfind>""";
                        HttpRequest request = HttpRequest.newBuilder().uri(uri) //
-                                       .header(DavHeader.DEPTH.getHeaderName(), DavDepth.DEPTH_1.getValue()) //
-                                       .method(DavMethod.PROPFIND.name(), BodyPublishers.ofString(body)) //
+                                       .header(HttpHeader.DEPTH.getHeaderName(), DavDepth.DEPTH_1.getValue()) //
+                                       .method(HttpMethod.PROPFIND.name(), BodyPublishers.ofString(body)) //
                                        .build();
 
                        HttpResponse<String> responseStr = httpClient.send(request, BodyHandlers.ofString());
@@ -95,8 +95,8 @@ public class DavClient {
        public boolean exists(URI uri) {
                try {
                        HttpRequest request = HttpRequest.newBuilder().uri(uri) //
-                                       .header(DavHeader.DEPTH.getHeaderName(), DavDepth.DEPTH_0.getValue()) //
-                                       .method(DavMethod.HEAD.name(), BodyPublishers.noBody()) //
+                                       .header(HttpHeader.DEPTH.getHeaderName(), DavDepth.DEPTH_0.getValue()) //
+                                       .method(HttpMethod.HEAD.name(), BodyPublishers.noBody()) //
                                        .build();
                        BodyHandler<String> bodyHandler = BodyHandlers.ofString();
                        HttpResponse<String> response = httpClient.send(request, bodyHandler);
@@ -122,8 +122,8 @@ public class DavClient {
                                          <D:allprop/>
                                        </D:propfind>""";
                        HttpRequest request = HttpRequest.newBuilder().uri(uri) //
-                                       .header(DavHeader.DEPTH.getHeaderName(), DavDepth.DEPTH_0.getValue()) //
-                                       .method(DavMethod.PROPFIND.name(), BodyPublishers.ofString(body)) //
+                                       .header(HttpHeader.DEPTH.getHeaderName(), DavDepth.DEPTH_0.getValue()) //
+                                       .method(HttpMethod.PROPFIND.name(), BodyPublishers.ofString(body)) //
                                        .build();
 
 //                     HttpResponse<String> responseStr = httpClient.send(request, BodyHandlers.ofString());
index 3d235fdb4b8e91ae2b47ad8cf84e77fafc973f36..24695e7b1fc52c737503bc134898a910dfe18b53 100644 (file)
@@ -1,5 +1,7 @@
 package org.argeo.cms.dav;
 
+import org.argeo.util.http.HttpHeader;
+
 import com.sun.net.httpserver.HttpExchange;
 
 public enum DavDepth {
@@ -21,7 +23,7 @@ public enum DavDepth {
        }
 
        public static DavDepth fromHttpExchange(HttpExchange httpExchange) {
-               String value = httpExchange.getRequestHeaders().getFirst(DavHeader.DEPTH.getHeaderName());
+               String value = httpExchange.getRequestHeaders().getFirst(HttpHeader.DEPTH.getHeaderName());
                if (value == null)
                        return null;
                DavDepth depth = switch (value) {
diff --git a/org.argeo.cms/src/org/argeo/cms/dav/DavHeader.java b/org.argeo.cms/src/org/argeo/cms/dav/DavHeader.java
deleted file mode 100644 (file)
index 014b133..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-package org.argeo.cms.dav;
-
-/** Standard HTTP headers. */
-public enum DavHeader {
-       DEPTH("Depth"), //
-       ;
-
-       private final String name;
-
-       private DavHeader(String headerName) {
-               this.name = headerName;
-       }
-
-       public String getHeaderName() {
-               return name;
-       }
-
-       @Override
-       public String toString() {
-               return getHeaderName();
-       }
-
-}
diff --git a/org.argeo.cms/src/org/argeo/cms/dav/DavHttpHandler.java b/org.argeo.cms/src/org/argeo/cms/dav/DavHttpHandler.java
new file mode 100644 (file)
index 0000000..0bd3b39
--- /dev/null
@@ -0,0 +1,99 @@
+package org.argeo.cms.dav;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.StringJoiner;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+
+import javax.xml.namespace.NamespaceContext;
+
+import org.argeo.api.acr.ContentNotFoundException;
+import org.argeo.util.http.HttpHeader;
+import org.argeo.util.http.HttpMethod;
+import org.argeo.util.http.HttpResponseStatus;
+import org.argeo.util.http.HttpServerUtils;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+
+/**
+ * Centralise patterns which are not ACR specific. Not really meant as a
+ * framework for building WebDav servers, but rather to make uppe-level of
+ * ACR-specific code more readable and maintainable.
+ */
+public abstract class DavHttpHandler implements HttpHandler {
+       private NamespaceContext namespaceContext;
+
+       @Override
+       public void handle(HttpExchange exchange) throws IOException {
+               String subPath = HttpServerUtils.subPath(exchange);
+               String method = exchange.getRequestMethod();
+               try {
+                       if (HttpMethod.GET.name().equals(method)) {
+                               handleGET(exchange, subPath);
+                       } else if (HttpMethod.OPTIONS.name().equals(method)) {
+                               handleOPTIONS(exchange, subPath);
+                               exchange.sendResponseHeaders(HttpResponseStatus.NO_CONTENT.getCode(), -1);
+                       } else if (HttpMethod.PROPFIND.name().equals(method)) {
+                               DavDepth depth = DavDepth.fromHttpExchange(exchange);
+                               if (depth == null) {
+                                       // default, as per http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND
+                                       depth = DavDepth.DEPTH_INFINITY;
+                               }
+                               DavPropfind davPropfind;
+                               try (InputStream in = exchange.getRequestBody()) {
+                                       davPropfind = DavPropfind.load(depth, in);
+                               }
+                               MultiStatusWriter multiStatusWriter = new MultiStatusWriter();
+                               CompletableFuture<Void> published = handlePROPFIND(exchange, subPath, davPropfind, multiStatusWriter);
+                               exchange.sendResponseHeaders(HttpResponseStatus.MULTI_STATUS.getCode(), 0l);
+                               try (OutputStream out = exchange.getResponseBody()) {
+                                       multiStatusWriter.process(namespaceContext, out, published.minimalCompletionStage(),
+                                                       davPropfind.isPropname());
+                               }
+                       } else {
+                               throw new IllegalArgumentException("Unsupported method " + method);
+                       }
+               } catch (ContentNotFoundException e) {
+                       exchange.sendResponseHeaders(HttpResponseStatus.NOT_FOUND.getCode(), -1);
+               }
+               // TODO return a structured error message
+               catch (UnsupportedOperationException e) {
+                       exchange.sendResponseHeaders(HttpResponseStatus.NOT_IMPLEMENTED.getCode(), -1);
+               } catch (Exception e) {
+                       exchange.sendResponseHeaders(HttpResponseStatus.INTERNAL_SERVER_ERROR.getCode(), -1);
+               }
+
+       }
+
+       protected abstract CompletableFuture<Void> handlePROPFIND(HttpExchange exchange, String path,
+                       DavPropfind davPropfind, Consumer<DavResponse> consumer) throws IOException;
+
+       protected abstract void handleGET(HttpExchange exchange, String path) throws IOException;
+
+       protected void handleOPTIONS(HttpExchange exchange, String path) throws IOException {
+               exchange.getResponseHeaders().set(HttpHeader.DAV.getHeaderName(), "1, 3");
+               StringJoiner methods = new StringJoiner(",");
+               methods.add(HttpMethod.OPTIONS.name());
+               methods.add(HttpMethod.HEAD.name());
+               methods.add(HttpMethod.GET.name());
+               methods.add(HttpMethod.POST.name());
+               methods.add(HttpMethod.PUT.name());
+               methods.add(HttpMethod.PROPFIND.name());
+               // TODO :
+               methods.add(HttpMethod.PROPPATCH.name());
+               methods.add(HttpMethod.MKCOL.name());
+               methods.add(HttpMethod.DELETE.name());
+               methods.add(HttpMethod.MOVE.name());
+               methods.add(HttpMethod.COPY.name());
+
+               exchange.getResponseHeaders().add(HttpHeader.ALLOW.getHeaderName(), methods.toString());
+       }
+
+       public void setNamespaceContext(NamespaceContext namespaceContext) {
+               this.namespaceContext = namespaceContext;
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/dav/DavMethod.java b/org.argeo.cms/src/org/argeo/cms/dav/DavMethod.java
deleted file mode 100644 (file)
index 67421a3..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-package org.argeo.cms.dav;
-
-public enum DavMethod {
-       // Generic HTTP
-       HEAD, //
-       // WebDav specific
-       PROPFIND, //
-       PROPPATCH, //
-       ;
-}
index 452a24f446201daf9fa8b4ca55fbee7f1470990a..b980683999a2b15b38478f3a853037ee393902e7 100644 (file)
@@ -6,7 +6,7 @@ import javax.xml.stream.XMLStreamWriter;
 
 import org.argeo.api.acr.QNamed;
 
-public enum DavXmlElement implements QNamed {
+enum DavXmlElement implements QNamed {
        response, //
        multistatus, //
        href, //
index 45fcea0221a72ffe7faff2de2095258fce002e52..4c06b032f092d04623ba61dff316c549224a0f5f 100644 (file)
@@ -20,7 +20,7 @@ import javax.xml.stream.XMLOutputFactory;
 import javax.xml.stream.XMLStreamException;
 import javax.xml.stream.XMLStreamWriter;
 
-public class MultiStatusWriter implements Consumer<DavResponse> {
+class MultiStatusWriter implements Consumer<DavResponse> {
        private BlockingQueue<DavResponse> queue = new ArrayBlockingQueue<>(64);
 
 //     private OutputStream out;
index 11fc1a3667140ea5b444947cd83dab98112f1642..5a42a3e383ec2d9435af424f7276f394e3f18623 100644 (file)
@@ -2,7 +2,6 @@ package org.argeo.cms.internal.runtime;
 
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.OutputStream;
 import java.util.Collection;
 import java.util.Map;
 import java.util.Optional;
@@ -14,76 +13,57 @@ import java.util.function.Consumer;
 import javax.xml.namespace.QName;
 
 import org.argeo.api.acr.Content;
+import org.argeo.api.acr.ContentNotFoundException;
 import org.argeo.api.acr.ContentSession;
 import org.argeo.api.acr.DName;
 import org.argeo.api.acr.spi.ProvidedRepository;
 import org.argeo.api.cms.CmsConstants;
-import org.argeo.cms.acr.ContentUtils;
 import org.argeo.cms.auth.RemoteAuthUtils;
 import org.argeo.cms.dav.DavDepth;
-import org.argeo.cms.dav.DavMethod;
+import org.argeo.cms.dav.DavHttpHandler;
 import org.argeo.cms.dav.DavPropfind;
 import org.argeo.cms.dav.DavResponse;
-import org.argeo.cms.dav.DavXmlElement;
-import org.argeo.cms.dav.MultiStatusWriter;
 import org.argeo.cms.internal.http.RemoteAuthHttpExchange;
 import org.argeo.util.StreamUtils;
-import org.argeo.util.http.HttpMethod;
 import org.argeo.util.http.HttpResponseStatus;
-import org.argeo.util.http.HttpServerUtils;
 
 import com.sun.net.httpserver.HttpExchange;
-import com.sun.net.httpserver.HttpHandler;
 
-public class CmsAcrHttpHandler implements HttpHandler {
+/** A partial WebDav implementation based on ACR. */
+public class CmsAcrHttpHandler extends DavHttpHandler {
        private ProvidedRepository contentRepository;
 
        @Override
-       public void handle(HttpExchange exchange) throws IOException {
-               String method = exchange.getRequestMethod();
-               if (DavMethod.PROPFIND.name().equals(method)) {
-                       handlePROPFIND(exchange);
-               } else if (HttpMethod.GET.name().equals(method)) {
-                       handleGET(exchange);
-               } else {
-                       throw new IllegalArgumentException("Unsupported method " + method);
+       protected void handleGET(HttpExchange exchange, String path) throws IOException {
+               ContentSession session = RemoteAuthUtils.doAs(() -> contentRepository.get(),
+                               new RemoteAuthHttpExchange(exchange));
+               if (!session.exists(path)) // not found
+                       throw new ContentNotFoundException(session, path);
+               Content content = session.get(path);
+               Optional<Long> size = content.get(DName.getcontentlength, Long.class);
+               try (InputStream in = content.open(InputStream.class)) {
+                       exchange.sendResponseHeaders(HttpResponseStatus.OK.getCode(), size.orElse(0l));
+                       StreamUtils.copy(in, exchange.getResponseBody());
+               } catch (IOException e) {
+                       throw new RuntimeException("Cannot process " + path, e);
                }
-
        }
 
-       protected void handlePROPFIND(HttpExchange exchange) throws IOException {
-               String relativePath = HttpServerUtils.relativize(exchange);
-
-               DavDepth depth = DavDepth.fromHttpExchange(exchange);
-               if (depth == null) {
-                       // default, as per http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND
-                       depth = DavDepth.DEPTH_INFINITY;
-               }
-
+       @Override
+       protected CompletableFuture<Void> handlePROPFIND(HttpExchange exchange, String path, DavPropfind davPropfind,
+                       Consumer<DavResponse> consumer) throws IOException {
                ContentSession session = RemoteAuthUtils.doAs(() -> contentRepository.get(),
                                new RemoteAuthHttpExchange(exchange));
-
-               String path = ContentUtils.ROOT_SLASH + relativePath;
-               if (!session.exists(path)) {// not found
-                       exchange.sendResponseHeaders(HttpResponseStatus.NOT_FOUND.getCode(), -1);
-                       return;
-               }
+               if (!session.exists(path)) // not found
+                       throw new ContentNotFoundException(session, path);
                Content content = session.get(path);
 
                CompletableFuture<Void> published = new CompletableFuture<Void>();
-
-               try (InputStream in = exchange.getRequestBody()) {
-                       DavPropfind davPropfind = DavPropfind.load(depth, in);
-                       MultiStatusWriter msWriter = new MultiStatusWriter();
-                       ForkJoinPool.commonPool().execute(() -> {
-                               publishDavResponses(content, davPropfind, msWriter);
-                               published.complete(null);
-                       });
-                       exchange.sendResponseHeaders(HttpResponseStatus.MULTI_STATUS.getCode(), 0l);
-                       try (OutputStream out = exchange.getResponseBody()) {
-                               msWriter.process(session, out, published.minimalCompletionStage(), davPropfind.isPropname());
-                       }
-               }
+               ForkJoinPool.commonPool().execute(() -> {
+                       publishDavResponses(content, davPropfind, consumer);
+                       published.complete(null);
+               });
+               return published;
        }
 
        protected void publishDavResponses(Content content, DavPropfind davPropfind, Consumer<DavResponse> consumer) {
@@ -114,7 +94,7 @@ public class CmsAcrHttpHandler implements HttpHandler {
                                        Object value = content.get(key);
                                        processMapEntry(davResponse, key, value);
                                }
-                               if (DavXmlElement.resourcetype.qName().equals(key)) {
+                               if (DName.resourcetype.qName().equals(key)) {
                                        davResponse.getResourceTypes().addAll(content.getContentClasses());
                                }
                        }
@@ -152,20 +132,6 @@ public class CmsAcrHttpHandler implements HttpHandler {
 
        }
 
-       protected void handleGET(HttpExchange exchange) {
-               String relativePath = HttpServerUtils.relativize(exchange);
-               ContentSession session = RemoteAuthUtils.doAs(() -> contentRepository.get(),
-                               new RemoteAuthHttpExchange(exchange));
-               Content content = session.get(ContentUtils.ROOT_SLASH + relativePath);
-               Optional<Long> size = content.get(DName.getcontentlength, Long.class);
-               try (InputStream in = content.open(InputStream.class)) {
-                       exchange.sendResponseHeaders(HttpResponseStatus.OK.getCode(), size.orElse(0l));
-                       StreamUtils.copy(in, exchange.getResponseBody());
-               } catch (IOException e) {
-                       throw new RuntimeException("Cannot process " + relativePath, e);
-               }
-       }
-
        public void setContentRepository(ProvidedRepository contentRepository) {
                this.contentRepository = contentRepository;
        }
diff --git a/org.argeo.util/src/org/argeo/util/ExceptionsChain.java b/org.argeo.util/src/org/argeo/util/ExceptionsChain.java
new file mode 100644 (file)
index 0000000..9f82421
--- /dev/null
@@ -0,0 +1,90 @@
+package org.argeo.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Serialisable wrapper of a {@link Throwable}. typically to be written as XML
+ * or JSON in a server error response.
+ */
+public class ExceptionsChain {
+       private List<SystemException> exceptions = new ArrayList<>();
+
+       public ExceptionsChain() {
+       }
+
+       public ExceptionsChain(Throwable exception) {
+               writeException(exception);
+       }
+
+       /** recursive */
+       protected void writeException(Throwable exception) {
+               SystemException systemException = new SystemException(exception);
+               exceptions.add(systemException);
+               Throwable cause = exception.getCause();
+               if (cause != null)
+                       writeException(cause);
+       }
+
+       public List<SystemException> getExceptions() {
+               return exceptions;
+       }
+
+       public void setExceptions(List<SystemException> exceptions) {
+               this.exceptions = exceptions;
+       }
+
+       /** An exception in the chain. */
+       public static class SystemException {
+               private String type;
+               private String message;
+               private List<String> stackTrace;
+
+               public SystemException() {
+               }
+
+               public SystemException(Throwable exception) {
+                       this.type = exception.getClass().getName();
+                       this.message = exception.getMessage();
+                       this.stackTrace = new ArrayList<>();
+                       StackTraceElement[] elems = exception.getStackTrace();
+                       for (int i = 0; i < elems.length; i++)
+                               stackTrace.add("at " + elems[i].toString());
+               }
+
+               public String getType() {
+                       return type;
+               }
+
+               public void setType(String type) {
+                       this.type = type;
+               }
+
+               public String getMessage() {
+                       return message;
+               }
+
+               public void setMessage(String message) {
+                       this.message = message;
+               }
+
+               public List<String> getStackTrace() {
+                       return stackTrace;
+               }
+
+               public void setStackTrace(List<String> stackTrace) {
+                       this.stackTrace = stackTrace;
+               }
+
+               @Override
+               public String toString() {
+                       return "System exception: " + type + ", " + message + ", " + stackTrace;
+               }
+
+       }
+
+       @Override
+       public String toString() {
+               return exceptions.toString();
+       }
+}
index 2fb8f302c68fb2deb15567bfd3742e96746404b5..74cf94c03101d209bb06b89c1dfdb82cec158451 100644 (file)
@@ -1,9 +1,14 @@
 package org.argeo.util.http;
 
-/** HTTP headers which are specific to WebDAV. */
+/** Standard HTTP headers (including WebDav). */
 public enum HttpHeader {
        AUTHORIZATION("Authorization"), //
        WWW_AUTHENTICATE("WWW-Authenticate"), //
+       ALLOW("Allow"), //
+
+       // WebDav
+       DAV("DAV"), //
+       DEPTH("Depth"), //
        ;
 
        public final static String BASIC = "Basic";
index 2116057a4dfe7233c335c48ad4d540484c3df158..27b4d8f1990d68ef210d4a687529b9df49d060a4 100644 (file)
@@ -1,6 +1,19 @@
 package org.argeo.util.http;
 
+/** Generic HTTP methods. */
 public enum HttpMethod {
-       GET,//
+       OPTIONS, //
+       HEAD, //
+       GET, //
+       POST, //
+       PUT, //
+       DELETE, //
+
+       // WebDav
+       PROPFIND, //
+       PROPPATCH, //
+       MKCOL, //
+       MOVE, //
+       COPY, //
        ;
 }
index f0108626e5854ca661e32da3061cd971b7917549..c813a1f6b14ae183d86d50a29c2a0bf0a531b010 100644 (file)
@@ -1,18 +1,22 @@
 package org.argeo.util.http;
 
 /**
- * Standard HTTP response status codes.
+ * Standard HTTP response status codes (including WebDav ones).
  * 
  * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
  */
 public enum HttpResponseStatus {
        // Successful responses (200–299)
        OK(200), //
+       NO_CONTENT(204), //
        MULTI_STATUS(207), // WebDav
        // Client error responses (400–499)
        UNAUTHORIZED(401), //
        FORBIDDEN(403), //
        NOT_FOUND(404), //
+       // Server error responses (500-599)
+       INTERNAL_SERVER_ERROR(500), //
+       NOT_IMPLEMENTED(501), //
        ;
 
        private final int code;
index 6c6e88414546823827615eda550d6255ab19ed6f..9127d2c216dcd074f14721b4ab3d34f5660fc791 100644 (file)
@@ -7,22 +7,35 @@ import com.sun.net.httpserver.HttpContext;
 import com.sun.net.httpserver.HttpExchange;
 
 public class HttpServerUtils {
+       private final static String SLASH = "/";
 
-       public static String relativize(HttpContext httpContext, String path) {
-               Objects.requireNonNull(path);
-               if (!path.startsWith(httpContext.getPath()))
-                       throw new IllegalArgumentException(path + " does not belong to context" + httpContext.getPath());
-               String relativePath = path.substring(httpContext.getPath().length());
+       private static String extractPathWithingContext(HttpContext httpContext, String fullPath, boolean startWithSlash) {
+               Objects.requireNonNull(fullPath);
+               String contextPath = httpContext.getPath();
+               if (!fullPath.startsWith(contextPath))
+                       throw new IllegalArgumentException(fullPath + " does not belong to context" + contextPath);
+               String path = fullPath.substring(contextPath.length());
                // TODO optimise?
-               if (relativePath.startsWith("/"))
-                       relativePath = relativePath.substring(1);
-               return relativePath;
+               if (!startWithSlash && path.startsWith(SLASH)) {
+                       path = path.substring(1);
+               } else if (startWithSlash && !path.startsWith(SLASH)) {
+                       path = SLASH + path;
+               }
+               return path;
        }
 
+       /** Path within the context, NOT starting with a slash. */
        public static String relativize(HttpExchange exchange) {
                URI uri = exchange.getRequestURI();
                HttpContext httpContext = exchange.getHttpContext();
-               return relativize(httpContext, uri.getPath());
+               return extractPathWithingContext(httpContext, uri.getPath(), false);
+       }
+
+       /** Path within the context, starting with a slash. */
+       public static String subPath(HttpExchange exchange) {
+               URI uri = exchange.getRequestURI();
+               HttpContext httpContext = exchange.getHttpContext();
+               return extractPathWithingContext(httpContext, uri.getPath(), true);
        }
 
        /** singleton */