From 1d6840195189cbdbf632ca2800b6179d3b6349df Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Fri, 16 Sep 2022 10:01:03 +0200 Subject: [PATCH] Refactor WebDav implementation --- .../api/acr/ContentNotFoundException.java | 31 ++++-- .../cms/integration/CmsExceptionsChain.java | 84 +--------------- .../directory/DirectoryContentProvider.java | 6 +- .../argeo/cms/acr/xml/DomContentProvider.java | 3 +- .../src/org/argeo/cms/dav/DavClient.java | 20 ++-- .../src/org/argeo/cms/dav/DavDepth.java | 4 +- .../src/org/argeo/cms/dav/DavHeader.java | 23 ----- .../src/org/argeo/cms/dav/DavHttpHandler.java | 99 +++++++++++++++++++ .../src/org/argeo/cms/dav/DavMethod.java | 10 -- .../src/org/argeo/cms/dav/DavXmlElement.java | 2 +- .../org/argeo/cms/dav/MultiStatusWriter.java | 2 +- .../internal/runtime/CmsAcrHttpHandler.java | 88 +++++------------ .../src/org/argeo/util/ExceptionsChain.java | 90 +++++++++++++++++ .../src/org/argeo/util/http/HttpHeader.java | 7 +- .../src/org/argeo/util/http/HttpMethod.java | 15 ++- .../argeo/util/http/HttpResponseStatus.java | 6 +- .../org/argeo/util/http/HttpServerUtils.java | 31 ++++-- 17 files changed, 314 insertions(+), 207 deletions(-) delete mode 100644 org.argeo.cms/src/org/argeo/cms/dav/DavHeader.java create mode 100644 org.argeo.cms/src/org/argeo/cms/dav/DavHttpHandler.java delete mode 100644 org.argeo.cms/src/org/argeo/cms/dav/DavMethod.java create mode 100644 org.argeo.util/src/org/argeo/util/ExceptionsChain.java diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/ContentNotFoundException.java b/org.argeo.api.acr/src/org/argeo/api/acr/ContentNotFoundException.java index b86c92c1e..51efb3860 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/ContentNotFoundException.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/ContentNotFoundException.java @@ -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; + } } diff --git a/org.argeo.cms.ee/src/org/argeo/cms/integration/CmsExceptionsChain.java b/org.argeo.cms.ee/src/org/argeo/cms/integration/CmsExceptionsChain.java index fb289c18e..205699464 100644 --- a/org.argeo.cms.ee/src/org/argeo/cms/integration/CmsExceptionsChain.java +++ b/org.argeo.cms.ee/src/org/argeo/cms/integration/CmsExceptionsChain.java @@ -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 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 getExceptions() { - return exceptions; - } - - public void setExceptions(List exceptions) { - this.exceptions = exceptions; - } - - /** An exception in the chain. */ - public static class SystemException { - private String type; - private String message; - private List 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 getStackTrace() { - return stackTrace; - } - - public void setStackTrace(List 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(); // } diff --git a/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContentProvider.java b/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContentProvider.java index f4a416aa7..aab5d6dc0 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContentProvider.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContentProvider.java @@ -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); } diff --git a/org.argeo.cms/src/org/argeo/cms/acr/xml/DomContentProvider.java b/org.argeo.cms/src/org/argeo/cms/acr/xml/DomContentProvider.java index f76d38ce0..66ff878d5 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/xml/DomContentProvider.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/xml/DomContentProvider.java @@ -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); } diff --git a/org.argeo.cms/src/org/argeo/cms/dav/DavClient.java b/org.argeo.cms/src/org/argeo/cms/dav/DavClient.java index 5788a32b4..e1e5f7499 100644 --- a/org.argeo.cms/src/org/argeo/cms/dav/DavClient.java +++ b/org.argeo.cms/src/org/argeo/cms/dav/DavClient.java @@ -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 bodyHandler = BodyHandlers.ofString(); HttpResponse response = httpClient.send(request, bodyHandler); @@ -76,8 +76,8 @@ public class DavClient { """; 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 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 bodyHandler = BodyHandlers.ofString(); HttpResponse response = httpClient.send(request, bodyHandler); @@ -122,8 +122,8 @@ public class DavClient { """; 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 responseStr = httpClient.send(request, BodyHandlers.ofString()); diff --git a/org.argeo.cms/src/org/argeo/cms/dav/DavDepth.java b/org.argeo.cms/src/org/argeo/cms/dav/DavDepth.java index 3d235fdb4..24695e7b1 100644 --- a/org.argeo.cms/src/org/argeo/cms/dav/DavDepth.java +++ b/org.argeo.cms/src/org/argeo/cms/dav/DavDepth.java @@ -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 index 014b13351..000000000 --- a/org.argeo.cms/src/org/argeo/cms/dav/DavHeader.java +++ /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 index 000000000..0bd3b398e --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/dav/DavHttpHandler.java @@ -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 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 handlePROPFIND(HttpExchange exchange, String path, + DavPropfind davPropfind, Consumer 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 index 67421a34b..000000000 --- a/org.argeo.cms/src/org/argeo/cms/dav/DavMethod.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.argeo.cms.dav; - -public enum DavMethod { - // Generic HTTP - HEAD, // - // WebDav specific - PROPFIND, // - PROPPATCH, // - ; -} diff --git a/org.argeo.cms/src/org/argeo/cms/dav/DavXmlElement.java b/org.argeo.cms/src/org/argeo/cms/dav/DavXmlElement.java index 452a24f44..b98068399 100644 --- a/org.argeo.cms/src/org/argeo/cms/dav/DavXmlElement.java +++ b/org.argeo.cms/src/org/argeo/cms/dav/DavXmlElement.java @@ -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, // diff --git a/org.argeo.cms/src/org/argeo/cms/dav/MultiStatusWriter.java b/org.argeo.cms/src/org/argeo/cms/dav/MultiStatusWriter.java index 45fcea022..4c06b032f 100644 --- a/org.argeo.cms/src/org/argeo/cms/dav/MultiStatusWriter.java +++ b/org.argeo.cms/src/org/argeo/cms/dav/MultiStatusWriter.java @@ -20,7 +20,7 @@ import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; -public class MultiStatusWriter implements Consumer { +class MultiStatusWriter implements Consumer { private BlockingQueue queue = new ArrayBlockingQueue<>(64); // private OutputStream out; diff --git a/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsAcrHttpHandler.java b/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsAcrHttpHandler.java index 11fc1a366..5a42a3e38 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsAcrHttpHandler.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsAcrHttpHandler.java @@ -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 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 handlePROPFIND(HttpExchange exchange, String path, DavPropfind davPropfind, + Consumer 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 published = new CompletableFuture(); - - 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 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 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 index 000000000..9f824213d --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/ExceptionsChain.java @@ -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 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 getExceptions() { + return exceptions; + } + + public void setExceptions(List exceptions) { + this.exceptions = exceptions; + } + + /** An exception in the chain. */ + public static class SystemException { + private String type; + private String message; + private List 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 getStackTrace() { + return stackTrace; + } + + public void setStackTrace(List stackTrace) { + this.stackTrace = stackTrace; + } + + @Override + public String toString() { + return "System exception: " + type + ", " + message + ", " + stackTrace; + } + + } + + @Override + public String toString() { + return exceptions.toString(); + } +} diff --git a/org.argeo.util/src/org/argeo/util/http/HttpHeader.java b/org.argeo.util/src/org/argeo/util/http/HttpHeader.java index 2fb8f302c..74cf94c03 100644 --- a/org.argeo.util/src/org/argeo/util/http/HttpHeader.java +++ b/org.argeo.util/src/org/argeo/util/http/HttpHeader.java @@ -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"; diff --git a/org.argeo.util/src/org/argeo/util/http/HttpMethod.java b/org.argeo.util/src/org/argeo/util/http/HttpMethod.java index 2116057a4..27b4d8f19 100644 --- a/org.argeo.util/src/org/argeo/util/http/HttpMethod.java +++ b/org.argeo.util/src/org/argeo/util/http/HttpMethod.java @@ -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, // ; } diff --git a/org.argeo.util/src/org/argeo/util/http/HttpResponseStatus.java b/org.argeo.util/src/org/argeo/util/http/HttpResponseStatus.java index f0108626e..c813a1f6b 100644 --- a/org.argeo.util/src/org/argeo/util/http/HttpResponseStatus.java +++ b/org.argeo.util/src/org/argeo/util/http/HttpResponseStatus.java @@ -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; diff --git a/org.argeo.util/src/org/argeo/util/http/HttpServerUtils.java b/org.argeo.util/src/org/argeo/util/http/HttpServerUtils.java index 6c6e88414..9127d2c21 100644 --- a/org.argeo.util/src/org/argeo/util/http/HttpServerUtils.java +++ b/org.argeo.util/src/org/argeo/util/http/HttpServerUtils.java @@ -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 */ -- 2.30.2