From b8f50d6d8e7b9c9215d156ba33f9dedfcee913a7 Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Thu, 15 Sep 2022 13:02:07 +0200 Subject: [PATCH] First WebDav PROPFIND implementation server-side --- .../org/argeo/cms/acr/CmsContentSession.java | 2 + .../org/argeo/cms/acr/CmsContentTypes.java | 2 +- .../src/org/argeo/cms/acr/dav/DavContent.java | 2 +- .../argeo/cms/acr/dav/DavContentProvider.java | 4 +- .../src/org/argeo/cms/acr/fs/FsContent.java | 3 + .../src/org/argeo/cms/acr/xml/DomContent.java | 2 + .../org/argeo/cms/auth/RemoteAuthUtils.java | 4 +- .../src/org/argeo/cms}/dav/DavClient.java | 30 +++- .../src/org/argeo/cms/dav/DavDepth.java | 35 +++++ .../src/org/argeo/cms}/dav/DavHeader.java | 6 +- .../src/org/argeo/cms}/dav/DavMethod.java | 2 +- .../src/org/argeo/cms/dav/DavPropfind.java | 92 +++++++++++ .../src/org/argeo/cms}/dav/DavResponse.java | 6 +- .../src/org/argeo/cms/dav/DavXmlElement.java | 84 ++++++++++ .../org/argeo/cms}/dav/MultiStatusReader.java | 15 +- .../org/argeo/cms/dav/MultiStatusWriter.java | 143 ++++++++++++++++++ .../internal/runtime/CmsAcrHttpHandler.java | 138 ++++++++++++++++- .../org/argeo/util/dav/DavServerHandler.java | 33 ---- .../src/org/argeo/util/dav/DavXmlElement.java | 45 ------ .../argeo/util/http/HttpResponseStatus.java | 9 +- .../DisplayQName.java} | 20 +-- .../src/org/argeo/util/naming/LdapAttrs.java | 26 ++-- .../src/org/argeo/util/naming/LdapObjs.java | 27 ++-- 23 files changed, 578 insertions(+), 152 deletions(-) rename {org.argeo.util/src/org/argeo/util => org.argeo.cms/src/org/argeo/cms}/dav/DavClient.java (81%) create mode 100644 org.argeo.cms/src/org/argeo/cms/dav/DavDepth.java rename {org.argeo.util/src/org/argeo/util => org.argeo.cms/src/org/argeo/cms}/dav/DavHeader.java (73%) rename {org.argeo.util/src/org/argeo/util => org.argeo.cms/src/org/argeo/cms}/dav/DavMethod.java (79%) create mode 100644 org.argeo.cms/src/org/argeo/cms/dav/DavPropfind.java rename {org.argeo.util/src/org/argeo/util => org.argeo.cms/src/org/argeo/cms}/dav/DavResponse.java (88%) create mode 100644 org.argeo.cms/src/org/argeo/cms/dav/DavXmlElement.java rename {org.argeo.util/src/org/argeo/util => org.argeo.cms/src/org/argeo/cms}/dav/MultiStatusReader.java (95%) create mode 100644 org.argeo.cms/src/org/argeo/cms/dav/MultiStatusWriter.java delete mode 100644 org.argeo.util/src/org/argeo/util/dav/DavServerHandler.java delete mode 100644 org.argeo.util/src/org/argeo/util/dav/DavXmlElement.java rename org.argeo.util/src/org/argeo/util/{naming/QNamed.java => internal/DisplayQName.java} (58%) diff --git a/org.argeo.cms/src/org/argeo/cms/acr/CmsContentSession.java b/org.argeo.cms/src/org/argeo/cms/acr/CmsContentSession.java index 757dad143..ba7dfa32d 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/CmsContentSession.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/CmsContentSession.java @@ -72,6 +72,8 @@ class CmsContentSession implements ProvidedSession { @Override public boolean exists(String path) { + if (!path.startsWith(ContentUtils.ROOT_SLASH)) + throw new IllegalArgumentException(path + " is not an absolute path"); ContentProvider contentProvider = contentRepository.getMountManager().findContentProvider(path); String mountPath = contentProvider.getMountPath(); String relativePath = extractRelativePath(mountPath, path); diff --git a/org.argeo.cms/src/org/argeo/cms/acr/CmsContentTypes.java b/org.argeo.cms/src/org/argeo/cms/acr/CmsContentTypes.java index c4ab0b685..fff40c1bb 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/CmsContentTypes.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/CmsContentTypes.java @@ -24,7 +24,7 @@ public enum CmsContentTypes { // XLINK_1999("xlink", "http://www.w3.org/1999/xlink", "xlink.xsd", "http://www.w3.org/XML/2008/06/xlink.xsd"), // -// WEBDAV("dav", "DAV:", "webdav.xsd", "https://raw.githubusercontent.com/lookfirst/sardine/master/webdav.xsd"), + WEBDAV("D", "DAV:", null, "https://raw.githubusercontent.com/lookfirst/sardine/master/webdav.xsd"), // XSLT_2_0("xsl", "http://www.w3.org/1999/XSL/Transform", "schema-for-xslt20.xsd", "https://www.w3.org/2007/schema-for-xslt20.xsd"), diff --git a/org.argeo.cms/src/org/argeo/cms/acr/dav/DavContent.java b/org.argeo.cms/src/org/argeo/cms/acr/dav/DavContent.java index 0003e5334..11c3db006 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/dav/DavContent.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/dav/DavContent.java @@ -16,7 +16,7 @@ import org.argeo.api.acr.spi.ContentProvider; import org.argeo.api.acr.spi.ProvidedSession; import org.argeo.cms.acr.AbstractContent; import org.argeo.cms.acr.ContentUtils; -import org.argeo.util.dav.DavResponse; +import org.argeo.cms.dav.DavResponse; public class DavContent extends AbstractContent { private final DavContentProvider provider; diff --git a/org.argeo.cms/src/org/argeo/cms/acr/dav/DavContentProvider.java b/org.argeo.cms/src/org/argeo/cms/acr/dav/DavContentProvider.java index 4f7699b8d..0622e8852 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/dav/DavContentProvider.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/dav/DavContentProvider.java @@ -8,8 +8,8 @@ import org.argeo.api.acr.RuntimeNamespaceContext; import org.argeo.api.acr.spi.ContentProvider; import org.argeo.api.acr.spi.ProvidedContent; import org.argeo.api.acr.spi.ProvidedSession; -import org.argeo.util.dav.DavClient; -import org.argeo.util.dav.DavResponse; +import org.argeo.cms.dav.DavClient; +import org.argeo.cms.dav.DavResponse; public class DavContentProvider implements ContentProvider { private String mountPath; diff --git a/org.argeo.cms/src/org/argeo/cms/acr/fs/FsContent.java b/org.argeo.cms/src/org/argeo/cms/acr/fs/FsContent.java index b8f98d2c8..f0c733857 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/fs/FsContent.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/fs/FsContent.java @@ -24,6 +24,7 @@ import java.util.Set; import java.util.StringJoiner; import java.util.concurrent.CompletableFuture; +import javax.xml.XMLConstants; import javax.xml.namespace.QName; import javax.xml.transform.Source; import javax.xml.transform.TransformerException; @@ -196,6 +197,8 @@ public class FsContent extends AbstractContent implements ProvidedContent { try { for (String name : udfav.list()) { QName providerName = NamespaceUtils.parsePrefixedName(provider, name); + if (providerName.getNamespaceURI().equals(XMLConstants.XMLNS_ATTRIBUTE_NS_URI)) + continue; // skip prefix mapping QName sessionName = new ContentName(providerName, getSession()); result.add(sessionName); } diff --git a/org.argeo.cms/src/org/argeo/cms/acr/xml/DomContent.java b/org.argeo.cms/src/org/argeo/cms/acr/xml/DomContent.java index 6608e749f..514d0bd36 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/xml/DomContent.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/xml/DomContent.java @@ -122,6 +122,8 @@ public class DomContent extends AbstractContent implements ProvidedContent { for (int i = 0; i < attributes.getLength(); i++) { Attr attr = (Attr) attributes.item(i); QName key = toQName(attr); + if (key.getNamespaceURI().equals(XMLConstants.XMLNS_ATTRIBUTE_NS_URI)) + continue;// skip prefix mapping result.add(key); } return result; diff --git a/org.argeo.cms/src/org/argeo/cms/auth/RemoteAuthUtils.java b/org.argeo.cms/src/org/argeo/cms/auth/RemoteAuthUtils.java index af274d316..e79032c4c 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/RemoteAuthUtils.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/RemoteAuthUtils.java @@ -155,7 +155,7 @@ public class RemoteAuthUtils { .startsWith(HttpHeader.NEGOTIATE)) { negotiateFailed = true; } else { - return HttpResponseStatus.FORBIDDEN.getStatusCode(); + return HttpResponseStatus.FORBIDDEN.getCode(); } } @@ -175,7 +175,7 @@ public class RemoteAuthUtils { // response.setHeader("Keep-Alive", "timeout=5, max=97"); // response.setContentType("text/html; charset=UTF-8"); - return HttpResponseStatus.UNAUTHORIZED.getStatusCode(); + return HttpResponseStatus.UNAUTHORIZED.getCode(); } private static boolean hasAcceptorCredentials() { diff --git a/org.argeo.util/src/org/argeo/util/dav/DavClient.java b/org.argeo.cms/src/org/argeo/cms/dav/DavClient.java similarity index 81% rename from org.argeo.util/src/org/argeo/util/dav/DavClient.java rename to org.argeo.cms/src/org/argeo/cms/dav/DavClient.java index f8a8fa1f0..5788a32b4 100644 --- a/org.argeo.util/src/org/argeo/util/dav/DavClient.java +++ b/org.argeo.cms/src/org/argeo/cms/dav/DavClient.java @@ -1,7 +1,9 @@ -package org.argeo.util.dav; +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; @@ -13,6 +15,8 @@ import java.util.Iterator; import javax.xml.namespace.QName; +import org.argeo.util.http.HttpResponseStatus; + public class DavClient { private HttpClient httpClient; @@ -21,6 +25,14 @@ public class DavClient { httpClient = HttpClient.newBuilder() // // .sslContext(insecureContext()) // .version(HttpClient.Version.HTTP_1_1) // +// .authenticator(new Authenticator() { +// +// @Override +// protected PasswordAuthentication getPasswordAuthentication() { +// return new PasswordAuthentication("root", "demo".toCharArray()); +// } +// +// }) // .build(); } @@ -64,12 +76,12 @@ public class DavClient { """; HttpRequest request = HttpRequest.newBuilder().uri(uri) // - .header(DavHeader.DEPTH.name(), "1") // + .header(DavHeader.DEPTH.getHeaderName(), DavDepth.DEPTH_1.getValue()) // .method(DavMethod.PROPFIND.name(), BodyPublishers.ofString(body)) // .build(); -// HttpResponse responseStr = httpClient.send(request, BodyHandlers.ofString()); -// System.out.println(responseStr.body()); + HttpResponse responseStr = httpClient.send(request, BodyHandlers.ofString()); + System.out.println(responseStr.body()); HttpResponse response = httpClient.send(request, BodyHandlers.ofInputStream()); MultiStatusReader msReader = new MultiStatusReader(response.body(), uri.getPath()); @@ -83,14 +95,14 @@ public class DavClient { public boolean exists(URI uri) { try { HttpRequest request = HttpRequest.newBuilder().uri(uri) // - .header(DavHeader.DEPTH.name(), "0") // + .header(DavHeader.DEPTH.getHeaderName(), DavDepth.DEPTH_0.getValue()) // .method(DavMethod.HEAD.name(), BodyPublishers.noBody()) // .build(); BodyHandler bodyHandler = BodyHandlers.ofString(); HttpResponse response = httpClient.send(request, bodyHandler); System.out.println(response.body()); int responseStatusCode = response.statusCode(); - if (responseStatusCode == 404) + if (responseStatusCode == HttpResponseStatus.NOT_FOUND.getCode()) return false; if (responseStatusCode >= 200 && responseStatusCode < 300) return true; @@ -110,7 +122,7 @@ public class DavClient { """; HttpRequest request = HttpRequest.newBuilder().uri(uri) // - .header(DavHeader.DEPTH.name(), "0") // + .header(DavHeader.DEPTH.getHeaderName(), DavDepth.DEPTH_0.getValue()) // .method(DavMethod.PROPFIND.name(), BodyPublishers.ofString(body)) // .build(); @@ -130,8 +142,10 @@ public class DavClient { public static void main(String[] args) { DavClient davClient = new DavClient(); +// Iterator responses = davClient +// .listChildren(URI.create("http://localhost/unstable/a2/org.argeo.tp.sdk/")); Iterator responses = davClient - .listChildren(URI.create("http://localhost/unstable/a2/org.argeo.tp.sdk/")); + .listChildren(URI.create("http://root:demo@localhost:7070/api/acr/srv/example")); while (responses.hasNext()) { DavResponse response = responses.next(); System.out.println(response.getHref() + (response.isCollection() ? " (collection)" : "")); diff --git a/org.argeo.cms/src/org/argeo/cms/dav/DavDepth.java b/org.argeo.cms/src/org/argeo/cms/dav/DavDepth.java new file mode 100644 index 000000000..3d235fdb4 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/dav/DavDepth.java @@ -0,0 +1,35 @@ +package org.argeo.cms.dav; + +import com.sun.net.httpserver.HttpExchange; + +public enum DavDepth { + DEPTH_0("0"), DEPTH_1("1"), DEPTH_INFINITY("infinity"); + + private final String value; + + private DavDepth(String value) { + this.value = value; + } + + @Override + public String toString() { + return getValue(); + } + + public String getValue() { + return value; + } + + public static DavDepth fromHttpExchange(HttpExchange httpExchange) { + String value = httpExchange.getRequestHeaders().getFirst(DavHeader.DEPTH.getHeaderName()); + if (value == null) + return null; + DavDepth depth = switch (value) { + case "0" -> DEPTH_0; + case "1" -> DEPTH_1; + case "infinity" -> DEPTH_INFINITY; + default -> throw new IllegalArgumentException("Unexpected value: " + value); + }; + return depth; + } +} diff --git a/org.argeo.util/src/org/argeo/util/dav/DavHeader.java b/org.argeo.cms/src/org/argeo/cms/dav/DavHeader.java similarity index 73% rename from org.argeo.util/src/org/argeo/util/dav/DavHeader.java rename to org.argeo.cms/src/org/argeo/cms/dav/DavHeader.java index a1b034bf3..014b13351 100644 --- a/org.argeo.util/src/org/argeo/util/dav/DavHeader.java +++ b/org.argeo.cms/src/org/argeo/cms/dav/DavHeader.java @@ -1,4 +1,4 @@ -package org.argeo.util.dav; +package org.argeo.cms.dav; /** Standard HTTP headers. */ public enum DavHeader { @@ -11,13 +11,13 @@ public enum DavHeader { this.name = headerName; } - public String getName() { + public String getHeaderName() { return name; } @Override public String toString() { - return getName(); + return getHeaderName(); } } diff --git a/org.argeo.util/src/org/argeo/util/dav/DavMethod.java b/org.argeo.cms/src/org/argeo/cms/dav/DavMethod.java similarity index 79% rename from org.argeo.util/src/org/argeo/util/dav/DavMethod.java rename to org.argeo.cms/src/org/argeo/cms/dav/DavMethod.java index 1472c9b30..67421a34b 100644 --- a/org.argeo.util/src/org/argeo/util/dav/DavMethod.java +++ b/org.argeo.cms/src/org/argeo/cms/dav/DavMethod.java @@ -1,4 +1,4 @@ -package org.argeo.util.dav; +package org.argeo.cms.dav; public enum DavMethod { // Generic HTTP diff --git a/org.argeo.cms/src/org/argeo/cms/dav/DavPropfind.java b/org.argeo.cms/src/org/argeo/cms/dav/DavPropfind.java new file mode 100644 index 000000000..8160544e9 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/dav/DavPropfind.java @@ -0,0 +1,92 @@ +package org.argeo.cms.dav; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.namespace.QName; +import javax.xml.stream.FactoryConfigurationError; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +public class DavPropfind { + private DavDepth depth; + private boolean propname = false; + private boolean allprop = false; + private List props = new ArrayList<>(); + + public DavPropfind(DavDepth depth) { + this.depth = depth; + } + + public boolean isPropname() { + return propname; + } + + public void setPropname(boolean propname) { + this.propname = propname; + } + + public boolean isAllprop() { + return allprop; + } + + public void setAllprop(boolean allprop) { + this.allprop = allprop; + } + + public List getProps() { + return props; + } + + public DavDepth getDepth() { + return depth; + } + + public static DavPropfind load(DavDepth depth, InputStream in) throws IOException { + try { + DavPropfind res = null; + XMLInputFactory inputFactory = XMLInputFactory.newFactory(); + XMLStreamReader reader = inputFactory.createXMLStreamReader(in); + while (reader.hasNext()) { + reader.next(); + if (reader.isStartElement()) { + QName name = reader.getName(); +// System.out.println(name); + DavXmlElement davXmlElement = DavXmlElement.toEnum(name); + if (davXmlElement != null) { + switch (davXmlElement) { + case propfind: + res = new DavPropfind(depth); + break; + case allprop: + res.setAllprop(true); + break; + case propname: + res.setPropname(true); + case prop: + // ignore + case include: + // ignore + break; + default: + // TODO check that the format is really respected + res.getProps().add(reader.getName()); + } + } + } + } + + // checks + if (res.isPropname()) { + if (!res.getProps().isEmpty() || res.isAllprop()) + throw new IllegalArgumentException("Cannot set other values if propname is set"); + } + return res; + } catch (FactoryConfigurationError | XMLStreamException e) { + throw new RuntimeException("Cannot load propfind", e); + } + } +} diff --git a/org.argeo.util/src/org/argeo/util/dav/DavResponse.java b/org.argeo.cms/src/org/argeo/cms/dav/DavResponse.java similarity index 88% rename from org.argeo.util/src/org/argeo/util/dav/DavResponse.java rename to org.argeo.cms/src/org/argeo/cms/dav/DavResponse.java index 424a0e8b7..6d45246db 100644 --- a/org.argeo.util/src/org/argeo/util/dav/DavResponse.java +++ b/org.argeo.cms/src/org/argeo/cms/dav/DavResponse.java @@ -1,4 +1,4 @@ -package org.argeo.util.dav; +package org.argeo.cms.dav; import java.util.ArrayList; import java.util.HashMap; @@ -22,7 +22,7 @@ public class DavResponse { return properties; } - void setHref(String href) { + public void setHref(String href) { this.href = href; } @@ -34,7 +34,7 @@ public class DavResponse { return collection; } - void setCollection(boolean collection) { + public void setCollection(boolean collection) { this.collection = collection; } diff --git a/org.argeo.cms/src/org/argeo/cms/dav/DavXmlElement.java b/org.argeo.cms/src/org/argeo/cms/dav/DavXmlElement.java new file mode 100644 index 000000000..a3929a02b --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/dav/DavXmlElement.java @@ -0,0 +1,84 @@ +package org.argeo.cms.dav; + +import javax.xml.namespace.QName; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +import org.argeo.api.acr.QNamed; + +public enum DavXmlElement implements QNamed { + response, // + multistatus, // + href, // + collection, // + prop, // + resourcetype, // + + // propfind + propfind, // + allprop, // + propname, // + include, // + propstat, // + + // locking + lockscope, // + locktype, // + supportedlock, // + lockentry, // + lockdiscovery, // + write, // + shared, // + exclusive, // + ; + + final static String WEBDAV_NAMESPACE_URI = "DAV:"; + final static String WEBDAV_DEFAULT_PREFIX = "D"; + +// private final QName value; +// +// private DavXmlElement() { +// this.value = new ContentName(getNamespace(), localName(), RuntimeNamespaceContext.getNamespaceContext()); +// } +// +// @Override +// public QName qName() { +// return value; +// } + + @Override + public String getNamespace() { + return WEBDAV_NAMESPACE_URI; + } + + @Override + public String getDefaultPrefix() { + return WEBDAV_DEFAULT_PREFIX; + } + + public static DavXmlElement toEnum(QName name) { + for (DavXmlElement e : values()) { + if (e.qName().equals(name)) + return e; + } + return null; + } + + public void setSimpleValue(XMLStreamWriter xsWriter, String value) throws XMLStreamException { + if (value == null) { + emptyElement(xsWriter); + return; + } + startElement(xsWriter); + xsWriter.writeCData(value); + xsWriter.writeEndElement(); + } + + public void emptyElement(XMLStreamWriter xsWriter) throws XMLStreamException { + xsWriter.writeEmptyElement(WEBDAV_NAMESPACE_URI, name()); + } + + public void startElement(XMLStreamWriter xsWriter) throws XMLStreamException { + xsWriter.writeStartElement(WEBDAV_NAMESPACE_URI, name()); + } +} diff --git a/org.argeo.util/src/org/argeo/util/dav/MultiStatusReader.java b/org.argeo.cms/src/org/argeo/cms/dav/MultiStatusReader.java similarity index 95% rename from org.argeo.util/src/org/argeo/util/dav/MultiStatusReader.java rename to org.argeo.cms/src/org/argeo/cms/dav/MultiStatusReader.java index cc7921523..4224e488c 100644 --- a/org.argeo.util/src/org/argeo/util/dav/MultiStatusReader.java +++ b/org.argeo.cms/src/org/argeo/cms/dav/MultiStatusReader.java @@ -1,6 +1,7 @@ -package org.argeo.util.dav; +package org.argeo.cms.dav; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.Iterator; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; @@ -16,6 +17,10 @@ import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; +/** + * Asynchronously iterate over the response statuses of the response to a + * PROPFIND request. + */ class MultiStatusReader implements Iterator { private CompletableFuture empty = new CompletableFuture(); private AtomicBoolean processed = new AtomicBoolean(false); @@ -32,13 +37,12 @@ class MultiStatusReader implements Iterator { public MultiStatusReader(InputStream in, String ignoredHref) { this.ignoredHref = ignoredHref; ForkJoinPool.commonPool().execute(() -> process(in)); - } protected void process(InputStream in) { try { XMLInputFactory inputFactory = XMLInputFactory.newFactory(); - XMLStreamReader reader = inputFactory.createXMLStreamReader(in); + XMLStreamReader reader = inputFactory.createXMLStreamReader(in, StandardCharsets.UTF_8.name()); DavResponse currentResponse = null; boolean collectiongProperties = false; @@ -133,6 +137,7 @@ class MultiStatusReader implements Iterator { if (!empty.isDone()) empty.complete(true); } catch (FactoryConfigurationError | XMLStreamException e) { + empty.completeExceptionally(e); throw new IllegalStateException("Cannot process DAV response", e); } finally { processed(); @@ -170,7 +175,7 @@ class MultiStatusReader implements Iterator { } catch (InterruptedException | ExecutionException e) { throw new IllegalStateException("Cannot determine hasNext", e); } finally { - notifyAll(); + // notifyAll(); } } @@ -185,7 +190,7 @@ class MultiStatusReader implements Iterator { } catch (InterruptedException e) { throw new IllegalStateException("Cannot get next", e); } finally { - notifyAll(); + // notifyAll(); } } diff --git a/org.argeo.cms/src/org/argeo/cms/dav/MultiStatusWriter.java b/org.argeo.cms/src/org/argeo/cms/dav/MultiStatusWriter.java new file mode 100644 index 000000000..45fcea022 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/dav/MultiStatusWriter.java @@ -0,0 +1,143 @@ +package org.argeo.cms.dav; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +import javax.xml.XMLConstants; +import javax.xml.namespace.NamespaceContext; +import javax.xml.namespace.QName; +import javax.xml.stream.FactoryConfigurationError; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +public class MultiStatusWriter implements Consumer { + private BlockingQueue queue = new ArrayBlockingQueue<>(64); + +// private OutputStream out; + + private Thread processingThread; + + private AtomicBoolean done = new AtomicBoolean(false); + + private AtomicBoolean polling = new AtomicBoolean(); + + public void process(NamespaceContext namespaceContext, OutputStream out, CompletionStage published, + boolean propname) throws IOException { + published.thenRun(() -> allPublished()); + processingThread = Thread.currentThread(); +// this.out = out; + + try { + XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newFactory(); + XMLStreamWriter xsWriter = xmlOutputFactory.createXMLStreamWriter(out, StandardCharsets.UTF_8.name()); + xsWriter.setNamespaceContext(namespaceContext); + xsWriter.setDefaultNamespace(DavXmlElement.WEBDAV_NAMESPACE_URI); + + xsWriter.writeStartDocument(); + DavXmlElement.multistatus.startElement(xsWriter); + xsWriter.writeDefaultNamespace(DavXmlElement.WEBDAV_NAMESPACE_URI); + + poll: while (!(done.get() && queue.isEmpty())) { + DavResponse davResponse; + try { + polling.set(true); + davResponse = queue.poll(10, TimeUnit.MILLISECONDS); + if (davResponse == null) + continue poll; + System.err.println(davResponse.getHref()); + } catch (InterruptedException e) { + System.err.println(e); + continue poll; + } finally { + polling.set(false); + } + + writeDavResponse(xsWriter, davResponse, propname); + } + + xsWriter.writeEndElement();// multistatus + xsWriter.writeEndDocument(); + xsWriter.close(); + out.close(); + } catch (FactoryConfigurationError | XMLStreamException e) { + synchronized (this) { + processingThread = null; + } + } + } + + protected void writeDavResponse(XMLStreamWriter xsWriter, DavResponse davResponse, boolean propname) + throws XMLStreamException { + Set namespaces = new HashSet<>(); + for (QName key : davResponse.getPropertyNames()) { + if (key.getNamespaceURI().equals(DavXmlElement.WEBDAV_NAMESPACE_URI)) + continue; // skip + if (key.getNamespaceURI().equals(XMLConstants.W3C_XML_SCHEMA_NS_URI)) + continue; // skip + namespaces.add(key.getNamespaceURI()); + } + DavXmlElement.response.startElement(xsWriter); + // namespaces + for (String ns : namespaces) + xsWriter.writeNamespace(xsWriter.getNamespaceContext().getPrefix(ns), ns); + + DavXmlElement.href.setSimpleValue(xsWriter, davResponse.getHref()); + + { + DavXmlElement.propstat.startElement(xsWriter); + { + DavXmlElement.prop.startElement(xsWriter); + if (!davResponse.getResourceTypes().isEmpty() || davResponse.isCollection()) { + DavXmlElement.resourcetype.startElement(xsWriter); + if (davResponse.isCollection()) + DavXmlElement.collection.emptyElement(xsWriter); + for (QName resourceType : davResponse.getResourceTypes()) { + xsWriter.writeEmptyElement(resourceType.getNamespaceURI(), resourceType.getLocalPart()); + } + xsWriter.writeEndElement();// resource type + } + for (QName key : davResponse.getPropertyNames()) { + if (propname) { + xsWriter.writeEmptyElement(key.getNamespaceURI(), key.getLocalPart()); + } else { + xsWriter.writeStartElement(key.getNamespaceURI(), key.getLocalPart()); + xsWriter.writeCData(davResponse.getProperties().get(key)); + xsWriter.writeEndElement(); + } + } + xsWriter.writeEndElement();// prop + } + xsWriter.writeEndElement();// propstat + } + xsWriter.writeEndElement();// response + } + + @Override + public void accept(DavResponse davResponse) { + try { + queue.put(davResponse); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + protected synchronized void allPublished() { + done.set(true); + if (processingThread != null && queue.isEmpty() && polling.get()) { + // we only interrupt if the queue is already processed + // so as not to interrupt I/O + processingThread.interrupt(); + } + } + +} 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 7799a8c4e..92da1b0c2 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,34 +2,164 @@ 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; +import java.util.StringJoiner; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ForkJoinPool; +import java.util.function.Consumer; + +import javax.xml.namespace.QName; import org.argeo.api.acr.Content; import org.argeo.api.acr.ContentSession; import org.argeo.api.acr.CrName; 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.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.dav.DavServerHandler; +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 extends DavServerHandler { +public class CmsAcrHttpHandler implements HttpHandler { private ProvidedRepository contentRepository; @Override - protected void handleGET(HttpExchange exchange) { + 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 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; + } + 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; + } + 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()); + } + } + } + + protected void publishDavResponses(Content content, DavPropfind davPropfind, Consumer consumer) { + publishDavResponse(content, davPropfind, consumer, 0); + } + + protected void publishDavResponse(Content content, DavPropfind davPropfind, Consumer consumer, + int currentDepth) { + DavResponse davResponse = new DavResponse(); + String href = CmsConstants.PATH_API_ACR + content.getPath(); + davResponse.setHref(href); + if (content.hasContentClass(CrName.collection)) + davResponse.setCollection(true); + if (davPropfind.isAllprop()) { + for (Map.Entry entry : content.entrySet()) { + davResponse.getPropertyNames().add(entry.getKey()); + processMapEntry(davResponse, entry.getKey(), entry.getValue()); + } + davResponse.getResourceTypes().addAll(content.getContentClasses()); + } else if (davPropfind.isPropname()) { + for (QName key : content.keySet()) { + davResponse.getPropertyNames().add(key); + } + } else { + for (QName key : davPropfind.getProps()) { + if (content.containsKey(key)) { + davResponse.getPropertyNames().add(key); + Object value = content.get(key); + processMapEntry(davResponse, key, value); + } + if (DavXmlElement.resourcetype.qName().equals(key)) { + davResponse.getResourceTypes().addAll(content.getContentClasses()); + } + } + + } + + consumer.accept(davResponse); + + // recurse only on collections + if (content.hasContentClass(CrName.collection)) { + if (davPropfind.getDepth() == DavDepth.DEPTH_INFINITY + || (davPropfind.getDepth() == DavDepth.DEPTH_1 && currentDepth == 0)) { + for (Content child : content) { + publishDavResponse(child, davPropfind, consumer, currentDepth + 1); + } + } + } + } + + protected void processMapEntry(DavResponse davResponse, QName key, Object value) { + // ignore content classes + if (CrName.cc.qName().equals(key)) + return; + String str; + if (value instanceof Collection) { + StringJoiner sj = new StringJoiner("\n"); + for (Object v : (Collection) value) { + sj.add(v.toString()); + } + str = sj.toString(); + } else { + str = value.toString(); + } + davResponse.getProperties().put(key, str); + + } + + 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(CrName.size, Long.class); try (InputStream in = content.open(InputStream.class)) { - exchange.sendResponseHeaders(HttpResponseStatus.OK.getStatusCode(), size.orElse(0l)); + exchange.sendResponseHeaders(HttpResponseStatus.OK.getCode(), size.orElse(0l)); StreamUtils.copy(in, exchange.getResponseBody()); } catch (IOException e) { throw new RuntimeException("Cannot process " + relativePath, e); diff --git a/org.argeo.util/src/org/argeo/util/dav/DavServerHandler.java b/org.argeo.util/src/org/argeo/util/dav/DavServerHandler.java deleted file mode 100644 index 29d689dbb..000000000 --- a/org.argeo.util/src/org/argeo/util/dav/DavServerHandler.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.argeo.util.dav; - -import java.io.IOException; - -import org.argeo.util.http.HttpMethod; - -import com.sun.net.httpserver.HttpExchange; -import com.sun.net.httpserver.HttpHandler; - -public class DavServerHandler implements HttpHandler { - - @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) { - throw new UnsupportedOperationException(); - } - - protected DavResponse handlePROPFIND(HttpExchange exchange) { - throw new UnsupportedOperationException(); - } - -} diff --git a/org.argeo.util/src/org/argeo/util/dav/DavXmlElement.java b/org.argeo.util/src/org/argeo/util/dav/DavXmlElement.java deleted file mode 100644 index c05425298..000000000 --- a/org.argeo.util/src/org/argeo/util/dav/DavXmlElement.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.argeo.util.dav; - -import javax.xml.namespace.QName; - -import org.argeo.util.naming.QNamed; - -public enum DavXmlElement implements QNamed { - response, // - href, // - collection, // - prop, // - resourcetype, // - - // locking - lockscope, // - locktype, // - supportedlock, // - lockentry, // - lockdiscovery, // - write, // - shared, // - exclusive, // - ; - - final static String WEBDAV_NAMESPACE_URI = "DAV:"; - final static String WEBDAV_DEFAULT_PREFIX = "D"; - - @Override - public String getNamespace() { - return WEBDAV_NAMESPACE_URI; - } - - @Override - public String getDefaultPrefix() { - return WEBDAV_DEFAULT_PREFIX; - } - - public static DavXmlElement toEnum(QName name) { - for (DavXmlElement e : values()) { - if (e.qName().equals(name)) - return e; - } - return null; - } -} 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 db7fbe30b..f0108626e 100644 --- a/org.argeo.util/src/org/argeo/util/http/HttpResponseStatus.java +++ b/org.argeo.util/src/org/argeo/util/http/HttpResponseStatus.java @@ -8,20 +8,21 @@ package org.argeo.util.http; public enum HttpResponseStatus { // Successful responses (200–299) OK(200), // + MULTI_STATUS(207), // WebDav // Client error responses (400–499) UNAUTHORIZED(401), // FORBIDDEN(403), // NOT_FOUND(404), // ; - private final int statusCode; + private final int code; HttpResponseStatus(int statusCode) { - this.statusCode = statusCode; + this.code = statusCode; } - public int getStatusCode() { - return statusCode; + public int getCode() { + return code; } } diff --git a/org.argeo.util/src/org/argeo/util/naming/QNamed.java b/org.argeo.util/src/org/argeo/util/internal/DisplayQName.java similarity index 58% rename from org.argeo.util/src/org/argeo/util/naming/QNamed.java rename to org.argeo.util/src/org/argeo/util/internal/DisplayQName.java index bcbb4742a..6cc39dc6a 100644 --- a/org.argeo.util/src/org/argeo/util/naming/QNamed.java +++ b/org.argeo.util/src/org/argeo/util/internal/DisplayQName.java @@ -1,21 +1,8 @@ -package org.argeo.util.naming; +package org.argeo.util.internal; import javax.xml.namespace.QName; -/** A (possibly) qualified name. To be used in enums. */ -@Deprecated -public interface QNamed { - String name(); - - default QName qName() { - return new DisplayQName(getNamespace(), name(), getDefaultPrefix()); - } - - String getNamespace(); - - String getDefaultPrefix(); - - static class DisplayQName extends QName { +public class DisplayQName extends QName { private static final long serialVersionUID = 2376484886212253123L; public DisplayQName(String namespaceURI, String localPart, String prefix) { @@ -33,5 +20,4 @@ public interface QNamed { return "".equals(prefix) ? getLocalPart() : prefix + ":" + getLocalPart(); } - } -} + } \ No newline at end of file diff --git a/org.argeo.util/src/org/argeo/util/naming/LdapAttrs.java b/org.argeo.util/src/org/argeo/util/naming/LdapAttrs.java index 5253579e4..2d35e6d94 100644 --- a/org.argeo.util/src/org/argeo/util/naming/LdapAttrs.java +++ b/org.argeo.util/src/org/argeo/util/naming/LdapAttrs.java @@ -2,6 +2,10 @@ package org.argeo.util.naming; import java.util.function.Supplier; +import javax.xml.namespace.QName; + +import org.argeo.util.internal.DisplayQName; + /** * Standard LDAP attributes as per:
* - Standard LDAP
@@ -9,7 +13,7 @@ import java.util.function.Supplier; * "https://github.com/krb5/krb5/blob/master/src/plugins/kdb/ldap/libkdb_ldap/kerberos.schema">Kerberos * LDAP (partial) */ -public enum LdapAttrs implements SpecifiedName, Supplier, QNamed { +public enum LdapAttrs implements SpecifiedName, Supplier { /** */ uid("0.9.2342.19200300.100.1.1", "RFC 4519"), /** */ @@ -293,10 +297,16 @@ public enum LdapAttrs implements SpecifiedName, Supplier, QNamed { // private final static String LDAP_ = "ldap:"; private final String oid, spec; + private final QName value; LdapAttrs(String oid, String spec) { this.oid = oid; this.spec = spec; + this.value = new DisplayQName(LdapObjs.LDAP_NAMESPACE_URI, name(), LdapObjs.LDAP_DEFAULT_PREFIX); + } + + public QName qName() { + return value; } @Override @@ -319,19 +329,9 @@ public enum LdapAttrs implements SpecifiedName, Supplier, QNamed { return get(); } - public String get() { - String prefix = getDefaultPrefix(); - return prefix != null ? prefix + ":" + name() : name(); - } - @Override - public String getDefaultPrefix() { - return LdapObjs.LDAP_DEFAULT_PREFIX; - } - - @Override - public String getNamespace() { - return LdapObjs.LDAP_NAMESPACE_URI; + public String get() { + return LdapObjs.LDAP_DEFAULT_PREFIX + ":" + name(); } @Override diff --git a/org.argeo.util/src/org/argeo/util/naming/LdapObjs.java b/org.argeo.util/src/org/argeo/util/naming/LdapObjs.java index 6dcb3e9eb..0e05e2e76 100644 --- a/org.argeo.util/src/org/argeo/util/naming/LdapObjs.java +++ b/org.argeo.util/src/org/argeo/util/naming/LdapObjs.java @@ -1,11 +1,17 @@ package org.argeo.util.naming; +import java.util.function.Supplier; + +import javax.xml.namespace.QName; + +import org.argeo.util.internal.DisplayQName; + /** * Standard LDAP object classes as per * https://www.ldap.com/ldap- * oid-reference */ -public enum LdapObjs implements SpecifiedName, QNamed { +public enum LdapObjs implements SpecifiedName, Supplier { account("0.9.2342.19200300.100.4.5", "RFC 4524"), /** */ document("0.9.2342.19200300.100.4.6", "RFC 4524"), @@ -96,12 +102,17 @@ public enum LdapObjs implements SpecifiedName, QNamed { /** MUST be equal to ContentRepository LDAP prefix. */ final static String LDAP_DEFAULT_PREFIX = "ldap"; - private final static String LDAP_ = LDAP_DEFAULT_PREFIX + ":"; private final String oid, spec; + private final QName value; private LdapObjs(String oid, String spec) { this.oid = oid; this.spec = spec; + this.value = new DisplayQName(LDAP_NAMESPACE_URI, name(), LDAP_DEFAULT_PREFIX); + } + + public QName qName() { + return value; } public String getOid() { @@ -112,18 +123,14 @@ public enum LdapObjs implements SpecifiedName, QNamed { return spec; } + @Deprecated public String property() { - return new StringBuilder(LDAP_).append(name()).toString(); - } - - @Override - public String getDefaultPrefix() { - return LdapObjs.LDAP_DEFAULT_PREFIX; + return get(); } @Override - public String getNamespace() { - return LdapObjs.LDAP_NAMESPACE_URI; + public String get() { + return LdapObjs.LDAP_DEFAULT_PREFIX + ":" + name(); } } -- 2.30.2