First WebDav PROPFIND implementation server-side
authorMathieu Baudier <mbaudier@argeo.org>
Thu, 15 Sep 2022 11:02:07 +0000 (13:02 +0200)
committerMathieu Baudier <mbaudier@argeo.org>
Thu, 15 Sep 2022 11:02:07 +0000 (13:02 +0200)
29 files changed:
org.argeo.cms/src/org/argeo/cms/acr/CmsContentSession.java
org.argeo.cms/src/org/argeo/cms/acr/CmsContentTypes.java
org.argeo.cms/src/org/argeo/cms/acr/dav/DavContent.java
org.argeo.cms/src/org/argeo/cms/acr/dav/DavContentProvider.java
org.argeo.cms/src/org/argeo/cms/acr/fs/FsContent.java
org.argeo.cms/src/org/argeo/cms/acr/xml/DomContent.java
org.argeo.cms/src/org/argeo/cms/auth/RemoteAuthUtils.java
org.argeo.cms/src/org/argeo/cms/dav/DavClient.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/dav/DavDepth.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/dav/DavHeader.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/dav/DavMethod.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/dav/DavPropfind.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/dav/DavResponse.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/dav/DavXmlElement.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/dav/MultiStatusReader.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/dav/MultiStatusWriter.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsAcrHttpHandler.java
org.argeo.util/src/org/argeo/util/dav/DavClient.java [deleted file]
org.argeo.util/src/org/argeo/util/dav/DavHeader.java [deleted file]
org.argeo.util/src/org/argeo/util/dav/DavMethod.java [deleted file]
org.argeo.util/src/org/argeo/util/dav/DavResponse.java [deleted file]
org.argeo.util/src/org/argeo/util/dav/DavServerHandler.java [deleted file]
org.argeo.util/src/org/argeo/util/dav/DavXmlElement.java [deleted file]
org.argeo.util/src/org/argeo/util/dav/MultiStatusReader.java [deleted file]
org.argeo.util/src/org/argeo/util/http/HttpResponseStatus.java
org.argeo.util/src/org/argeo/util/internal/DisplayQName.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/naming/LdapAttrs.java
org.argeo.util/src/org/argeo/util/naming/LdapObjs.java
org.argeo.util/src/org/argeo/util/naming/QNamed.java [deleted file]

index 757dad143a7ec3fe59a39d26b7e83b5b9076bf09..ba7dfa32d59f7244bef7e417f39c66105c216545 100644 (file)
@@ -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);
index c4ab0b68582b02226b4fe0134b841ec516644a42..fff40c1bb117008fc83551d588b9a9009b0f78e1 100644 (file)
@@ -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"),
index 0003e533433e3ccbdd903ab0804c2f2ad0292e0b..11c3db0060a730c12f22bbd79043be9805b39772 100644 (file)
@@ -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;
index 4f7699b8d935e0f6b60df9b3062d95df1b94fa9e..0622e8852eac9f88246efbf9af7c5f04a9711b1e 100644 (file)
@@ -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;
index b8f98d2c84996e7fa4c6b1ad39e465037298b076..f0c7338579c5f67f319c5122c06a2cebc072948e 100644 (file)
@@ -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);
                                }
index 6608e749fce2aac3690f85481ffe84c9745908fa..514d0bd36db23b505577db2bb4b2c13f62a75611 100644 (file)
@@ -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;
index af274d316802603717ee94df432f2fff1181795b..e79032c4c38f0603d1d71d323b038254f2fba104 100644 (file)
@@ -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.cms/src/org/argeo/cms/dav/DavClient.java b/org.argeo.cms/src/org/argeo/cms/dav/DavClient.java
new file mode 100644 (file)
index 0000000..5788a32
--- /dev/null
@@ -0,0 +1,160 @@
+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;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandler;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.util.Iterator;
+
+import javax.xml.namespace.QName;
+
+import org.argeo.util.http.HttpResponseStatus;
+
+public class DavClient {
+
+       private HttpClient httpClient;
+
+       public 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();
+       }
+
+       public void setProperty(String url, QName key, String value) {
+               try {
+                       String body = """
+                                       <?xml version="1.0" encoding="utf-8" ?>
+                                       <D:propertyupdate xmlns:D="DAV:"
+                                       """ //
+                                       + "xmlns:" + key.getPrefix() + "=\"" + key.getNamespaceURI() + "\">" + //
+                                       """
+                                                               <D:set>
+                                                                       <D:prop>
+                                                       """ //
+                                       + "<" + key.getPrefix() + ":" + key.getLocalPart() + ">" + value + "</" + key.getPrefix() + ":"
+                                       + key.getLocalPart() + ">" + //
+                                       """
+                                                                       </D:prop>
+                                                               </D:set>
+                                                       </D:propertyupdate>
+                                                       """;
+                       System.out.println(body);
+                       HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)) //
+                                       .header("Depth", "1") //
+                                       .method(DavMethod.PROPPATCH.name(), BodyPublishers.ofString(body)) //
+                                       .build();
+                       BodyHandler<String> bodyHandler = BodyHandlers.ofString();
+                       HttpResponse<String> response = httpClient.send(request, bodyHandler);
+                       System.out.println(response.body());
+               } catch (IOException | InterruptedException e) {
+                       // TODO Auto-generated catch block
+                       e.printStackTrace();
+               }
+       }
+
+       public Iterator<DavResponse> listChildren(URI uri) {
+               try {
+                       String body = """
+                                       <?xml version="1.0" encoding="utf-8" ?>
+                                       <D:propfind xmlns:D="DAV:">
+                                         <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)) //
+                                       .build();
+
+                       HttpResponse<String> responseStr = httpClient.send(request, BodyHandlers.ofString());
+                       System.out.println(responseStr.body());
+
+                       HttpResponse<InputStream> response = httpClient.send(request, BodyHandlers.ofInputStream());
+                       MultiStatusReader msReader = new MultiStatusReader(response.body(), uri.getPath());
+                       return msReader;
+               } catch (IOException | InterruptedException e) {
+                       throw new IllegalStateException("Cannot list children of " + uri, e);
+               }
+
+       }
+
+       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()) //
+                                       .build();
+                       BodyHandler<String> bodyHandler = BodyHandlers.ofString();
+                       HttpResponse<String> response = httpClient.send(request, bodyHandler);
+                       System.out.println(response.body());
+                       int responseStatusCode = response.statusCode();
+                       if (responseStatusCode == HttpResponseStatus.NOT_FOUND.getCode())
+                               return false;
+                       if (responseStatusCode >= 200 && responseStatusCode < 300)
+                               return true;
+                       throw new IllegalStateException(
+                                       "Cannot check whether " + uri + " exists: Unknown response status code " + responseStatusCode);
+               } catch (IOException | InterruptedException e) {
+                       throw new IllegalStateException("Cannot check whether " + uri + " exists", e);
+               }
+
+       }
+
+       public DavResponse get(URI uri) {
+               try {
+                       String body = """
+                                       <?xml version="1.0" encoding="utf-8" ?>
+                                       <D:propfind xmlns:D="DAV:">
+                                         <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)) //
+                                       .build();
+
+//                     HttpResponse<String> responseStr = httpClient.send(request, BodyHandlers.ofString());
+//                     System.out.println(responseStr.body());
+
+                       HttpResponse<InputStream> response = httpClient.send(request, BodyHandlers.ofInputStream());
+                       MultiStatusReader msReader = new MultiStatusReader(response.body());
+                       if (!msReader.hasNext())
+                               throw new IllegalArgumentException(uri + " does not exist");
+                       return msReader.next();
+               } catch (IOException | InterruptedException e) {
+                       throw new IllegalStateException("Cannot list children of " + uri, e);
+               }
+
+       }
+
+       public static void main(String[] args) {
+               DavClient davClient = new DavClient();
+//             Iterator<DavResponse> responses = davClient
+//                             .listChildren(URI.create("http://localhost/unstable/a2/org.argeo.tp.sdk/"));
+               Iterator<DavResponse> responses = davClient
+                               .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)" : ""));
+                       System.out.println("  " + response.getPropertyNames());
+
+               }
+//             davClient.setProperty("http://localhost/unstable/a2/org.argeo.tp.sdk/org.opentest4j.1.2.jar",
+//                             CrName.uuid.qName(), UUID.randomUUID().toString());
+
+       }
+
+}
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 (file)
index 0000000..3d235fd
--- /dev/null
@@ -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.cms/src/org/argeo/cms/dav/DavHeader.java b/org.argeo.cms/src/org/argeo/cms/dav/DavHeader.java
new file mode 100644 (file)
index 0000000..014b133
--- /dev/null
@@ -0,0 +1,23 @@
+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/DavMethod.java b/org.argeo.cms/src/org/argeo/cms/dav/DavMethod.java
new file mode 100644 (file)
index 0000000..67421a3
--- /dev/null
@@ -0,0 +1,10 @@
+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/DavPropfind.java b/org.argeo.cms/src/org/argeo/cms/dav/DavPropfind.java
new file mode 100644 (file)
index 0000000..8160544
--- /dev/null
@@ -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<QName> 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<QName> 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.cms/src/org/argeo/cms/dav/DavResponse.java b/org.argeo.cms/src/org/argeo/cms/dav/DavResponse.java
new file mode 100644 (file)
index 0000000..6d45246
--- /dev/null
@@ -0,0 +1,49 @@
+package org.argeo.cms.dav;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.xml.namespace.QName;
+
+public class DavResponse {
+       final static String MOD_DAV_NAMESPACE = "http://apache.org/dav/props/";
+
+       private String href;
+       private boolean collection;
+       private Set<QName> propertyNames = new HashSet<>();
+       private Map<QName, String> properties = new HashMap<>();
+       private List<QName> resourceTypes = new ArrayList<>();
+
+       public Map<QName, String> getProperties() {
+               return properties;
+       }
+
+       public void setHref(String href) {
+               this.href = href;
+       }
+
+       public String getHref() {
+               return href;
+       }
+
+       public boolean isCollection() {
+               return collection;
+       }
+
+       public void setCollection(boolean collection) {
+               this.collection = collection;
+       }
+
+       public List<QName> getResourceTypes() {
+               return resourceTypes;
+       }
+
+       public Set<QName> getPropertyNames() {
+               return propertyNames;
+       }
+
+}
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 (file)
index 0000000..a3929a0
--- /dev/null
@@ -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.cms/src/org/argeo/cms/dav/MultiStatusReader.java b/org.argeo.cms/src/org/argeo/cms/dav/MultiStatusReader.java
new file mode 100644 (file)
index 0000000..4224e48
--- /dev/null
@@ -0,0 +1,197 @@
+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;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.xml.namespace.QName;
+import javax.xml.stream.FactoryConfigurationError;
+import javax.xml.stream.XMLInputFactory;
+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<DavResponse> {
+       private CompletableFuture<Boolean> empty = new CompletableFuture<Boolean>();
+       private AtomicBoolean processed = new AtomicBoolean(false);
+
+       private BlockingQueue<DavResponse> queue = new ArrayBlockingQueue<>(64);
+
+       private final String ignoredHref;
+
+       public MultiStatusReader(InputStream in) {
+               this(in, null);
+       }
+
+       /** Typically ignoring self */
+       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, StandardCharsets.UTF_8.name());
+
+                       DavResponse currentResponse = null;
+                       boolean collectiongProperties = false;
+
+                       final QName COLLECTION = DavXmlElement.collection.qName(); // optimisation
+                       elements: 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 response:
+                                                       currentResponse = new DavResponse();
+                                                       break;
+                                               case href:
+                                                       assert currentResponse != null;
+                                                       while (reader.hasNext() && !reader.hasText())
+                                                               reader.next();
+                                                       String href = reader.getText();
+                                                       currentResponse.setHref(href);
+                                                       break;
+//                                             case collection:
+//                                                     currentResponse.setCollection(true);
+//                                                     break;
+                                               case prop:
+                                                       collectiongProperties = true;
+                                                       break;
+                                               case resourcetype:
+                                                       while (reader.hasNext()) {
+                                                               int event = reader.nextTag();
+                                                               QName resourceType = reader.getName();
+                                                               if (event == XMLStreamConstants.END_ELEMENT && name.equals(resourceType))
+                                                                       break;
+                                                               assert currentResponse != null;
+                                                               if (event == XMLStreamConstants.START_ELEMENT) {
+                                                                       if (COLLECTION.equals(resourceType))
+                                                                               currentResponse.setCollection(true);
+                                                                       else
+                                                                               currentResponse.getResourceTypes().add(resourceType);
+                                                               }
+                                                       }
+                                                       break;
+                                               default:
+                                                       // ignore
+                                               }
+                                       } else {
+                                               if (collectiongProperties) {
+                                                       String value = null;
+                                                       // TODO deal with complex properties
+                                                       readProperty: while (reader.hasNext()) {
+                                                               reader.next();
+                                                               if (reader.getEventType() == XMLStreamConstants.END_ELEMENT)
+                                                                       break readProperty;
+                                                               if (reader.getEventType() == XMLStreamConstants.CHARACTERS)
+                                                                       value = reader.getText();
+                                                       }
+
+                                                       if (name.getNamespaceURI().equals(DavResponse.MOD_DAV_NAMESPACE))
+                                                               continue elements; // skip mod_dav properties
+
+                                                       assert currentResponse != null;
+                                                       currentResponse.getPropertyNames().add(name);
+                                                       if (value != null)
+                                                               currentResponse.getProperties().put(name, value);
+
+                                               }
+                                       }
+                               } else if (reader.isEndElement()) {
+                                       QName name = reader.getName();
+//                                     System.out.println(name);
+                                       DavXmlElement davXmlElement = DavXmlElement.toEnum(name);
+                                       if (davXmlElement != null)
+                                               switch (davXmlElement) {
+                                               case response:
+                                                       assert currentResponse != null;
+                                                       if (ignoredHref == null || !ignoredHref.equals(currentResponse.getHref())) {
+                                                               if (!empty.isDone())
+                                                                       empty.complete(false);
+                                                               publish(currentResponse);
+                                                       }
+                                               case prop:
+                                                       collectiongProperties = false;
+                                                       break;
+                                               default:
+                                                       // ignore
+                                               }
+                               }
+                       }
+
+                       if (!empty.isDone())
+                               empty.complete(true);
+               } catch (FactoryConfigurationError | XMLStreamException e) {
+                       empty.completeExceptionally(e);
+                       throw new IllegalStateException("Cannot process DAV response", e);
+               } finally {
+                       processed();
+               }
+       }
+
+       protected synchronized void publish(DavResponse response) {
+               try {
+                       queue.put(response);
+               } catch (InterruptedException e) {
+                       throw new IllegalStateException("Cannot put response " + response, e);
+               } finally {
+                       notifyAll();
+               }
+       }
+
+       protected synchronized void processed() {
+               processed.set(true);
+               notifyAll();
+       }
+
+       @Override
+       public synchronized boolean hasNext() {
+               try {
+                       if (empty.get())
+                               return false;
+                       while (!processed.get() && queue.isEmpty()) {
+                               wait();
+                       }
+                       if (!queue.isEmpty())
+                               return true;
+                       if (processed.get())
+                               return false;
+                       throw new IllegalStateException("Cannot determine hasNext");
+               } catch (InterruptedException | ExecutionException e) {
+                       throw new IllegalStateException("Cannot determine hasNext", e);
+               } finally {
+                       // notifyAll();
+               }
+       }
+
+       @Override
+       public synchronized DavResponse next() {
+               try {
+                       if (!hasNext())
+                               throw new IllegalStateException("No fursther items are available");
+
+                       DavResponse response = queue.take();
+                       return response;
+               } catch (InterruptedException e) {
+                       throw new IllegalStateException("Cannot get next", e);
+               } finally {
+                       // 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 (file)
index 0000000..45fcea0
--- /dev/null
@@ -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<DavResponse> {
+       private BlockingQueue<DavResponse> 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<Void> 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<String> 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();
+               }
+       }
+
+}
index 7799a8c4eea29f9f1547721ecbd1c7d431cd847c..92da1b0c298ed3524087cf308ff0703ddc3af173 100644 (file)
@@ -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<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());
+                       }
+               }
+       }
+
+       protected void publishDavResponses(Content content, DavPropfind davPropfind, Consumer<DavResponse> consumer) {
+               publishDavResponse(content, davPropfind, consumer, 0);
+       }
+
+       protected void publishDavResponse(Content content, DavPropfind davPropfind, Consumer<DavResponse> 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<QName, Object> 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<Long> 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/DavClient.java b/org.argeo.util/src/org/argeo/util/dav/DavClient.java
deleted file mode 100644 (file)
index f8a8fa1..0000000
+++ /dev/null
@@ -1,146 +0,0 @@
-package org.argeo.util.dav;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URI;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpRequest.BodyPublishers;
-import java.net.http.HttpResponse;
-import java.net.http.HttpResponse.BodyHandler;
-import java.net.http.HttpResponse.BodyHandlers;
-import java.util.Iterator;
-
-import javax.xml.namespace.QName;
-
-public class DavClient {
-
-       private HttpClient httpClient;
-
-       public DavClient() {
-               httpClient = HttpClient.newBuilder() //
-//                             .sslContext(insecureContext()) //
-                               .version(HttpClient.Version.HTTP_1_1) //
-                               .build();
-       }
-
-       public void setProperty(String url, QName key, String value) {
-               try {
-                       String body = """
-                                       <?xml version="1.0" encoding="utf-8" ?>
-                                       <D:propertyupdate xmlns:D="DAV:"
-                                       """ //
-                                       + "xmlns:" + key.getPrefix() + "=\"" + key.getNamespaceURI() + "\">" + //
-                                       """
-                                                               <D:set>
-                                                                       <D:prop>
-                                                       """ //
-                                       + "<" + key.getPrefix() + ":" + key.getLocalPart() + ">" + value + "</" + key.getPrefix() + ":"
-                                       + key.getLocalPart() + ">" + //
-                                       """
-                                                                       </D:prop>
-                                                               </D:set>
-                                                       </D:propertyupdate>
-                                                       """;
-                       System.out.println(body);
-                       HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)) //
-                                       .header("Depth", "1") //
-                                       .method(DavMethod.PROPPATCH.name(), BodyPublishers.ofString(body)) //
-                                       .build();
-                       BodyHandler<String> bodyHandler = BodyHandlers.ofString();
-                       HttpResponse<String> response = httpClient.send(request, bodyHandler);
-                       System.out.println(response.body());
-               } catch (IOException | InterruptedException e) {
-                       // TODO Auto-generated catch block
-                       e.printStackTrace();
-               }
-       }
-
-       public Iterator<DavResponse> listChildren(URI uri) {
-               try {
-                       String body = """
-                                       <?xml version="1.0" encoding="utf-8" ?>
-                                       <D:propfind xmlns:D="DAV:">
-                                         <D:propname/>
-                                       </D:propfind>""";
-                       HttpRequest request = HttpRequest.newBuilder().uri(uri) //
-                                       .header(DavHeader.DEPTH.name(), "1") //
-                                       .method(DavMethod.PROPFIND.name(), BodyPublishers.ofString(body)) //
-                                       .build();
-
-//                     HttpResponse<String> responseStr = httpClient.send(request, BodyHandlers.ofString());
-//                     System.out.println(responseStr.body());
-
-                       HttpResponse<InputStream> response = httpClient.send(request, BodyHandlers.ofInputStream());
-                       MultiStatusReader msReader = new MultiStatusReader(response.body(), uri.getPath());
-                       return msReader;
-               } catch (IOException | InterruptedException e) {
-                       throw new IllegalStateException("Cannot list children of " + uri, e);
-               }
-
-       }
-
-       public boolean exists(URI uri) {
-               try {
-                       HttpRequest request = HttpRequest.newBuilder().uri(uri) //
-                                       .header(DavHeader.DEPTH.name(), "0") //
-                                       .method(DavMethod.HEAD.name(), BodyPublishers.noBody()) //
-                                       .build();
-                       BodyHandler<String> bodyHandler = BodyHandlers.ofString();
-                       HttpResponse<String> response = httpClient.send(request, bodyHandler);
-                       System.out.println(response.body());
-                       int responseStatusCode = response.statusCode();
-                       if (responseStatusCode == 404)
-                               return false;
-                       if (responseStatusCode >= 200 && responseStatusCode < 300)
-                               return true;
-                       throw new IllegalStateException(
-                                       "Cannot check whether " + uri + " exists: Unknown response status code " + responseStatusCode);
-               } catch (IOException | InterruptedException e) {
-                       throw new IllegalStateException("Cannot check whether " + uri + " exists", e);
-               }
-
-       }
-
-       public DavResponse get(URI uri) {
-               try {
-                       String body = """
-                                       <?xml version="1.0" encoding="utf-8" ?>
-                                       <D:propfind xmlns:D="DAV:">
-                                         <D:allprop/>
-                                       </D:propfind>""";
-                       HttpRequest request = HttpRequest.newBuilder().uri(uri) //
-                                       .header(DavHeader.DEPTH.name(), "0") //
-                                       .method(DavMethod.PROPFIND.name(), BodyPublishers.ofString(body)) //
-                                       .build();
-
-//                     HttpResponse<String> responseStr = httpClient.send(request, BodyHandlers.ofString());
-//                     System.out.println(responseStr.body());
-
-                       HttpResponse<InputStream> response = httpClient.send(request, BodyHandlers.ofInputStream());
-                       MultiStatusReader msReader = new MultiStatusReader(response.body());
-                       if (!msReader.hasNext())
-                               throw new IllegalArgumentException(uri + " does not exist");
-                       return msReader.next();
-               } catch (IOException | InterruptedException e) {
-                       throw new IllegalStateException("Cannot list children of " + uri, e);
-               }
-
-       }
-
-       public static void main(String[] args) {
-               DavClient davClient = new DavClient();
-               Iterator<DavResponse> responses = davClient
-                               .listChildren(URI.create("http://localhost/unstable/a2/org.argeo.tp.sdk/"));
-               while (responses.hasNext()) {
-                       DavResponse response = responses.next();
-                       System.out.println(response.getHref() + (response.isCollection() ? " (collection)" : ""));
-                       System.out.println("  " + response.getPropertyNames());
-
-               }
-//             davClient.setProperty("http://localhost/unstable/a2/org.argeo.tp.sdk/org.opentest4j.1.2.jar",
-//                             CrName.uuid.qName(), UUID.randomUUID().toString());
-
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/util/dav/DavHeader.java b/org.argeo.util/src/org/argeo/util/dav/DavHeader.java
deleted file mode 100644 (file)
index a1b034b..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-package org.argeo.util.dav;
-
-/** Standard HTTP headers. */
-public enum DavHeader {
-       DEPTH("Depth"), //
-       ;
-
-       private final String name;
-
-       private DavHeader(String headerName) {
-               this.name = headerName;
-       }
-
-       public String getName() {
-               return name;
-       }
-
-       @Override
-       public String toString() {
-               return getName();
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/util/dav/DavMethod.java b/org.argeo.util/src/org/argeo/util/dav/DavMethod.java
deleted file mode 100644 (file)
index 1472c9b..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-package org.argeo.util.dav;
-
-public enum DavMethod {
-       // Generic HTTP
-       HEAD, //
-       // WebDav specific
-       PROPFIND, //
-       PROPPATCH, //
-       ;
-}
diff --git a/org.argeo.util/src/org/argeo/util/dav/DavResponse.java b/org.argeo.util/src/org/argeo/util/dav/DavResponse.java
deleted file mode 100644 (file)
index 424a0e8..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-package org.argeo.util.dav;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import javax.xml.namespace.QName;
-
-public class DavResponse {
-       final static String MOD_DAV_NAMESPACE = "http://apache.org/dav/props/";
-
-       private String href;
-       private boolean collection;
-       private Set<QName> propertyNames = new HashSet<>();
-       private Map<QName, String> properties = new HashMap<>();
-       private List<QName> resourceTypes = new ArrayList<>();
-
-       public Map<QName, String> getProperties() {
-               return properties;
-       }
-
-       void setHref(String href) {
-               this.href = href;
-       }
-
-       public String getHref() {
-               return href;
-       }
-
-       public boolean isCollection() {
-               return collection;
-       }
-
-       void setCollection(boolean collection) {
-               this.collection = collection;
-       }
-
-       public List<QName> getResourceTypes() {
-               return resourceTypes;
-       }
-
-       public Set<QName> getPropertyNames() {
-               return propertyNames;
-       }
-
-}
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 (file)
index 29d689d..0000000
+++ /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 (file)
index c054252..0000000
+++ /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/dav/MultiStatusReader.java b/org.argeo.util/src/org/argeo/util/dav/MultiStatusReader.java
deleted file mode 100644 (file)
index cc79215..0000000
+++ /dev/null
@@ -1,192 +0,0 @@
-package org.argeo.util.dav;
-
-import java.io.InputStream;
-import java.util.Iterator;
-import java.util.concurrent.ArrayBlockingQueue;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ForkJoinPool;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import javax.xml.namespace.QName;
-import javax.xml.stream.FactoryConfigurationError;
-import javax.xml.stream.XMLInputFactory;
-import javax.xml.stream.XMLStreamConstants;
-import javax.xml.stream.XMLStreamException;
-import javax.xml.stream.XMLStreamReader;
-
-class MultiStatusReader implements Iterator<DavResponse> {
-       private CompletableFuture<Boolean> empty = new CompletableFuture<Boolean>();
-       private AtomicBoolean processed = new AtomicBoolean(false);
-
-       private BlockingQueue<DavResponse> queue = new ArrayBlockingQueue<>(64);
-
-       private final String ignoredHref;
-
-       public MultiStatusReader(InputStream in) {
-               this(in, null);
-       }
-
-       /** Typically ignoring self */
-       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);
-
-                       DavResponse currentResponse = null;
-                       boolean collectiongProperties = false;
-
-                       final QName COLLECTION = DavXmlElement.collection.qName(); // optimisation
-                       elements: 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 response:
-                                                       currentResponse = new DavResponse();
-                                                       break;
-                                               case href:
-                                                       assert currentResponse != null;
-                                                       while (reader.hasNext() && !reader.hasText())
-                                                               reader.next();
-                                                       String href = reader.getText();
-                                                       currentResponse.setHref(href);
-                                                       break;
-//                                             case collection:
-//                                                     currentResponse.setCollection(true);
-//                                                     break;
-                                               case prop:
-                                                       collectiongProperties = true;
-                                                       break;
-                                               case resourcetype:
-                                                       while (reader.hasNext()) {
-                                                               int event = reader.nextTag();
-                                                               QName resourceType = reader.getName();
-                                                               if (event == XMLStreamConstants.END_ELEMENT && name.equals(resourceType))
-                                                                       break;
-                                                               assert currentResponse != null;
-                                                               if (event == XMLStreamConstants.START_ELEMENT) {
-                                                                       if (COLLECTION.equals(resourceType))
-                                                                               currentResponse.setCollection(true);
-                                                                       else
-                                                                               currentResponse.getResourceTypes().add(resourceType);
-                                                               }
-                                                       }
-                                                       break;
-                                               default:
-                                                       // ignore
-                                               }
-                                       } else {
-                                               if (collectiongProperties) {
-                                                       String value = null;
-                                                       // TODO deal with complex properties
-                                                       readProperty: while (reader.hasNext()) {
-                                                               reader.next();
-                                                               if (reader.getEventType() == XMLStreamConstants.END_ELEMENT)
-                                                                       break readProperty;
-                                                               if (reader.getEventType() == XMLStreamConstants.CHARACTERS)
-                                                                       value = reader.getText();
-                                                       }
-
-                                                       if (name.getNamespaceURI().equals(DavResponse.MOD_DAV_NAMESPACE))
-                                                               continue elements; // skip mod_dav properties
-
-                                                       assert currentResponse != null;
-                                                       currentResponse.getPropertyNames().add(name);
-                                                       if (value != null)
-                                                               currentResponse.getProperties().put(name, value);
-
-                                               }
-                                       }
-                               } else if (reader.isEndElement()) {
-                                       QName name = reader.getName();
-//                                     System.out.println(name);
-                                       DavXmlElement davXmlElement = DavXmlElement.toEnum(name);
-                                       if (davXmlElement != null)
-                                               switch (davXmlElement) {
-                                               case response:
-                                                       assert currentResponse != null;
-                                                       if (ignoredHref == null || !ignoredHref.equals(currentResponse.getHref())) {
-                                                               if (!empty.isDone())
-                                                                       empty.complete(false);
-                                                               publish(currentResponse);
-                                                       }
-                                               case prop:
-                                                       collectiongProperties = false;
-                                                       break;
-                                               default:
-                                                       // ignore
-                                               }
-                               }
-                       }
-
-                       if (!empty.isDone())
-                               empty.complete(true);
-               } catch (FactoryConfigurationError | XMLStreamException e) {
-                       throw new IllegalStateException("Cannot process DAV response", e);
-               } finally {
-                       processed();
-               }
-       }
-
-       protected synchronized void publish(DavResponse response) {
-               try {
-                       queue.put(response);
-               } catch (InterruptedException e) {
-                       throw new IllegalStateException("Cannot put response " + response, e);
-               } finally {
-                       notifyAll();
-               }
-       }
-
-       protected synchronized void processed() {
-               processed.set(true);
-               notifyAll();
-       }
-
-       @Override
-       public synchronized boolean hasNext() {
-               try {
-                       if (empty.get())
-                               return false;
-                       while (!processed.get() && queue.isEmpty()) {
-                               wait();
-                       }
-                       if (!queue.isEmpty())
-                               return true;
-                       if (processed.get())
-                               return false;
-                       throw new IllegalStateException("Cannot determine hasNext");
-               } catch (InterruptedException | ExecutionException e) {
-                       throw new IllegalStateException("Cannot determine hasNext", e);
-               } finally {
-                       notifyAll();
-               }
-       }
-
-       @Override
-       public synchronized DavResponse next() {
-               try {
-                       if (!hasNext())
-                               throw new IllegalStateException("No fursther items are available");
-
-                       DavResponse response = queue.take();
-                       return response;
-               } catch (InterruptedException e) {
-                       throw new IllegalStateException("Cannot get next", e);
-               } finally {
-                       notifyAll();
-               }
-       }
-
-}
index db7fbe30bbc1df47192f7e7c6daebc0748dbae8d..f0108626e5854ca661e32da3061cd971b7917549 100644 (file)
@@ -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/internal/DisplayQName.java b/org.argeo.util/src/org/argeo/util/internal/DisplayQName.java
new file mode 100644 (file)
index 0000000..6cc39dc
--- /dev/null
@@ -0,0 +1,23 @@
+package org.argeo.util.internal;
+
+import javax.xml.namespace.QName;
+
+public class DisplayQName extends QName {
+               private static final long serialVersionUID = 2376484886212253123L;
+
+               public DisplayQName(String namespaceURI, String localPart, String prefix) {
+                       super(namespaceURI, localPart, prefix);
+               }
+
+               public DisplayQName(String localPart) {
+                       super(localPart);
+               }
+
+               @Override
+               public String toString() {
+                       String prefix = getPrefix();
+                       assert prefix != null;
+                       return "".equals(prefix) ? getLocalPart() : prefix + ":" + getLocalPart();
+               }
+
+       }
\ No newline at end of file
index 5253579e428313db8b1883c681e800fab6d5aec2..2d35e6d94fdc7b3d6f9e6d3164971e00df7ccd35 100644 (file)
@@ -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:<br>
  * - <a href= "https://www.ldap.com/ldap-oid-reference">Standard LDAP</a><br>
@@ -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)</a>
  */
-public enum LdapAttrs implements SpecifiedName, Supplier<String>, QNamed {
+public enum LdapAttrs implements SpecifiedName, Supplier<String> {
        /** */
        uid("0.9.2342.19200300.100.1.1", "RFC 4519"),
        /** */
@@ -293,10 +297,16 @@ public enum LdapAttrs implements SpecifiedName, Supplier<String>, 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<String>, 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
index 6dcb3e9eb4a829d6fcfcae8ebea953153217f8ab..0e05e2e76cb400036f683efa2fecadfd056c04a7 100644 (file)
@@ -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
  * <a href="https://www.ldap.com/ldap-oid-reference">https://www.ldap.com/ldap-
  * oid-reference</a>
  */
-public enum LdapObjs implements SpecifiedName, QNamed {
+public enum LdapObjs implements SpecifiedName, Supplier<String> {
        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();
        }
 
 }
diff --git a/org.argeo.util/src/org/argeo/util/naming/QNamed.java b/org.argeo.util/src/org/argeo/util/naming/QNamed.java
deleted file mode 100644 (file)
index bcbb474..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-package org.argeo.util.naming;
-
-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 {
-               private static final long serialVersionUID = 2376484886212253123L;
-
-               public DisplayQName(String namespaceURI, String localPart, String prefix) {
-                       super(namespaceURI, localPart, prefix);
-               }
-
-               public DisplayQName(String localPart) {
-                       super(localPart);
-               }
-
-               @Override
-               public String toString() {
-                       String prefix = getPrefix();
-                       assert prefix != null;
-                       return "".equals(prefix) ? getLocalPart() : prefix + ":" + getLocalPart();
-               }
-
-       }
-}