Introduce WebDav support
authorMathieu Baudier <mbaudier@argeo.org>
Fri, 15 Jul 2022 11:35:23 +0000 (13:35 +0200)
committerMathieu Baudier <mbaudier@argeo.org>
Fri, 15 Jul 2022 11:35:23 +0000 (13:35 +0200)
12 files changed:
org.argeo.cms/.classpath
org.argeo.cms/src/org/argeo/cms/acr/CmsContentTypes.java
org.argeo.cms/src/org/argeo/cms/acr/dav/DavContent.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/acr/dav/DavContentProvider.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/acr/schemas/webdav.xsd [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/runtime/DeployedContentRepository.java
org.argeo.util/src/org/argeo/util/dav/DavClient.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/dav/DavHeader.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/dav/DavMethod.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/dav/DavResponse.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/dav/DavXmlElement.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/dav/MultiStatusReader.java [new file with mode: 0644]

index 4a00becd81df57ed094ab0d2958c974510456391..3628e336878e528db30e5202048bfc67be000fbd 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <classpath>
-       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17">
                <attributes>
                        <attribute name="module" value="true"/>
                </attributes>
index 3f8fd9568ed56467d5fd9de6efe713bb7af2e84f..c4ab0b68582b02226b4fe0134b841ec516644a42 100644 (file)
@@ -24,6 +24,8 @@ 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"),
+       //
        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
new file mode 100644 (file)
index 0000000..0003e53
--- /dev/null
@@ -0,0 +1,110 @@
+package org.argeo.cms.acr.dav;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import javax.xml.namespace.QName;
+
+import org.argeo.api.acr.Content;
+import org.argeo.api.acr.ContentName;
+import org.argeo.api.acr.NamespaceUtils;
+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;
+
+public class DavContent extends AbstractContent {
+       private final DavContentProvider provider;
+       private final URI uri;
+
+       private Set<QName> keyNames;
+       private Optional<Map<QName, String>> values;
+
+       public DavContent(ProvidedSession session, DavContentProvider provider, URI uri, Set<QName> keyNames) {
+               this(session, provider, uri, keyNames, Optional.empty());
+       }
+
+       public DavContent(ProvidedSession session, DavContentProvider provider, URI uri, Set<QName> keyNames,
+                       Optional<Map<QName, String>> values) {
+               super(session);
+               this.provider = provider;
+               this.uri = uri;
+               this.keyNames = keyNames;
+               this.values = values;
+       }
+
+       @Override
+       public QName getName() {
+               String fileName = ContentUtils.getParentPath(uri.getPath())[1];
+               ContentName name = NamespaceUtils.parsePrefixedName(provider, fileName);
+               return name;
+       }
+
+       @Override
+       public Content getParent() {
+               try {
+                       String parentPath = ContentUtils.getParentPath(uri.getPath())[0];
+                       URI parentUri = new URI(uri.getScheme(), uri.getHost(), parentPath, null);
+                       return provider.getDavContent(getSession(), parentUri);
+               } catch (URISyntaxException e) {
+                       throw new IllegalStateException("Cannot create parent", e);
+               }
+       }
+
+       @Override
+       public Iterator<Content> iterator() {
+               Iterator<DavResponse> responses = provider.getDavClient().listChildren(uri);
+               return new DavResponseIterator(responses);
+       }
+
+       @Override
+       protected Iterable<QName> keys() {
+               return keyNames;
+       }
+
+       @SuppressWarnings("unchecked")
+       @Override
+       public <A> Optional<A> get(QName key, Class<A> clss) {
+               if (values.isEmpty()) {
+                       DavResponse response = provider.getDavClient().get(uri);
+                       values = Optional.of(response.getProperties());
+               }
+               String valueStr = values.get().get(key);
+               if (valueStr == null)
+                       return Optional.empty();
+               // TODO convert
+               return Optional.of((A) valueStr);
+       }
+
+       @Override
+       public ContentProvider getProvider() {
+               return provider;
+       }
+
+       class DavResponseIterator implements Iterator<Content> {
+               private final Iterator<DavResponse> responses;
+
+               public DavResponseIterator(Iterator<DavResponse> responses) {
+                       this.responses = responses;
+               }
+
+               @Override
+               public boolean hasNext() {
+                       return responses.hasNext();
+               }
+
+               @Override
+               public Content next() {
+                       DavResponse response = responses.next();
+                       String relativePath = response.getHref();
+                       URI contentUri = provider.relativePathToUri(relativePath);
+                       return new DavContent(getSession(), provider, contentUri, response.getPropertyNames());
+               }
+
+       }
+}
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
new file mode 100644 (file)
index 0000000..4f7699b
--- /dev/null
@@ -0,0 +1,76 @@
+package org.argeo.cms.acr.dav;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Iterator;
+
+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;
+
+public class DavContentProvider implements ContentProvider {
+       private String mountPath;
+       private URI baseUri;
+
+       private DavClient davClient;
+
+       public DavContentProvider(String mountPath, URI baseUri) {
+               this.mountPath = mountPath;
+               this.baseUri = baseUri;
+               if (!baseUri.getPath().endsWith("/"))
+                       throw new IllegalArgumentException("Base URI " + baseUri + " path does not end with /");
+               this.davClient = new DavClient();
+       }
+
+       @Override
+       public String getNamespaceURI(String prefix) {
+               // FIXME retrieve mappings from WebDav
+               return RuntimeNamespaceContext.getNamespaceContext().getNamespaceURI(prefix);
+       }
+
+       @Override
+       public Iterator<String> getPrefixes(String namespaceURI) {
+               // FIXME retrieve mappings from WebDav
+               return RuntimeNamespaceContext.getNamespaceContext().getPrefixes(namespaceURI);
+       }
+
+       @Override
+       public ProvidedContent get(ProvidedSession session, String relativePath) {
+               URI contentUri = relativePathToUri(relativePath);
+               return getDavContent(session, contentUri);
+       }
+
+       DavContent getDavContent(ProvidedSession session, URI uri) {
+               DavResponse response = davClient.get(uri);
+               return new DavContent(session, this, uri, response.getPropertyNames());
+       }
+
+       @Override
+       public boolean exists(ProvidedSession session, String relativePath) {
+               URI contentUri = relativePathToUri(relativePath);
+               return davClient.exists(contentUri);
+       }
+
+       @Override
+       public String getMountPath() {
+               return mountPath;
+       }
+
+       DavClient getDavClient() {
+               return davClient;
+       }
+
+       URI relativePathToUri(String relativePath) {
+               try {
+                       // TODO check last slash
+                       String path = relativePath.startsWith("/") ? relativePath : baseUri.getPath() + relativePath;
+                       URI uri = new URI(baseUri.getScheme(), baseUri.getHost(), path, baseUri.getFragment());
+                       return uri;
+               } catch (URISyntaxException e) {
+                       throw new IllegalArgumentException("Cannot build URI for " + relativePath + " relatively to " + baseUri, e);
+               }
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/acr/schemas/webdav.xsd b/org.argeo.cms/src/org/argeo/cms/acr/schemas/webdav.xsd
new file mode 100644 (file)
index 0000000..e7443f7
--- /dev/null
@@ -0,0 +1,449 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+-->
+
+<schema xmlns="http://www.w3.org/2001/XMLSchema"
+        targetNamespace="DAV:"
+        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+        xmlns:dav="DAV:"
+        elementFormDefault="qualified">
+
+    <element name="activelock">
+        <complexType>
+            <sequence>
+                <element ref="dav:lockscope"/>
+                <element ref="dav:locktype"/>
+                <element ref="dav:depth"/>
+                <element ref="dav:owner" minOccurs="0" maxOccurs="1"/>
+                <element ref="dav:timeout" minOccurs="0" maxOccurs="1"/>
+                <element ref="dav:locktoken" minOccurs="0" maxOccurs="1"/>
+            </sequence>
+        </complexType>
+    </element>
+
+    <element name="lockentry">
+        <complexType>
+            <sequence>
+                <element ref="dav:lockscope"/>
+                <element ref="dav:locktype"/>
+            </sequence>
+        </complexType>
+    </element>
+
+    <element name="lockinfo">
+        <complexType>
+            <sequence>
+                <element ref="dav:lockscope"/>
+                <element ref="dav:locktype"/>
+                <element ref="dav:owner" minOccurs="0" maxOccurs="1"/>
+            </sequence>
+        </complexType>
+    </element>
+
+    <element name="locktype">
+        <complexType>
+            <sequence>
+                <element ref="dav:write"/>
+            </sequence>
+        </complexType>
+    </element>
+
+    <element name="write">
+        <complexType/>
+    </element>
+
+    <element name="lockscope">
+        <complexType>
+            <choice>
+                <element ref="dav:exclusive"/>
+                <element ref="dav:shared"/>
+            </choice>
+        </complexType>
+    </element>
+
+    <element name="exclusive">
+        <complexType/>
+    </element>
+
+    <element name="shared">
+        <complexType/>
+    </element>
+
+    <element name="depth" type="xsd:string"/>
+
+    <element name="owner">
+        <complexType mixed="true">
+            <sequence>
+                <any namespace="http://www.w3.org/namespace/"/>
+            </sequence>
+        </complexType>
+    </element>
+
+    <element name="timeout" type="xsd:string"/>
+
+    <element name="locktoken">
+        <complexType>
+            <sequence>
+                <element ref="dav:href" maxOccurs="unbounded"/>
+            </sequence>
+        </complexType>
+    </element>
+
+    <element name="href" type="xsd:string"/>
+
+    <element name="link">
+        <complexType>
+            <sequence>
+                <element ref="dav:src" maxOccurs="unbounded"/>
+                <element ref="dav:dst" maxOccurs="unbounded"/>
+            </sequence>
+        </complexType>
+    </element>
+
+    <element name="dst" type="xsd:string"/>
+
+    <element name="src" type="xsd:string"/>
+
+    <element name="multistatus">
+        <complexType>
+            <sequence>
+                <element ref="dav:response" maxOccurs="unbounded"/>
+                <element ref="dav:responsedescription" minOccurs="0" maxOccurs="1"/>
+                <element ref="dav:sync-token" minOccurs="0" maxOccurs="1"/>
+            </sequence>
+        </complexType>
+    </element>
+
+    <element name="response">
+        <complexType>
+            <sequence>
+                <element ref="dav:href" minOccurs="1" maxOccurs="unbounded"/>
+                <choice>
+                    <sequence>
+                        <element ref="dav:status"/>
+                    </sequence>
+                    <element ref="dav:propstat" maxOccurs="unbounded"/>
+                </choice>
+                <element ref="dav:error" minOccurs="0" maxOccurs="1"/>
+                <element ref="dav:responsedescription" minOccurs="0" maxOccurs="1"/>
+                <element ref="dav:location" minOccurs="0" maxOccurs="1"/>
+            </sequence>
+        </complexType>
+    </element>
+
+    <element name="status" type="xsd:string"/>
+
+    <element name="error">
+        <complexType>
+            <sequence>
+                <any namespace="##any"/>
+            </sequence>
+        </complexType>
+    </element>
+
+    <element name="propstat">
+        <complexType>
+            <sequence>
+                <element ref="dav:prop"/>
+                <element ref="dav:status"/>
+                <element ref="dav:error" minOccurs="0" maxOccurs="1"/>
+                <element ref="dav:responsedescription" minOccurs="0" maxOccurs="1"/>
+            </sequence>
+        </complexType>
+    </element>
+
+    <element name="responsedescription" type="xsd:string"/>
+
+    <element name="location">
+        <complexType>
+            <sequence>
+                <element ref="dav:href" maxOccurs="1"/>
+            </sequence>
+        </complexType>
+    </element>
+
+    <element name="prop">
+        <complexType>
+            <all>
+                <element ref="dav:creationdate" minOccurs="0"/>
+                <element ref="dav:displayname" minOccurs="0"/>
+                <element ref="dav:getcontentlanguage" minOccurs="0"/>
+                <element ref="dav:getcontentlength" minOccurs="0"/>
+                <element ref="dav:getcontenttype" minOccurs="0"/>
+                <element ref="dav:getetag" minOccurs="0"/>
+                <element ref="dav:getlastmodified" minOccurs="0"/>
+                <element ref="dav:lockdiscovery" minOccurs="0"/>
+                <element ref="dav:resourcetype" minOccurs="0"/>
+                <element ref="dav:supportedlock" minOccurs="0"/>
+                <element ref="dav:supported-report-set" minOccurs="0"/>
+                <element ref="dav:quota-available-bytes" minOccurs="0"/>
+                <element ref="dav:quota-used-bytes" minOccurs="0"/>
+                <!-- Microsoft has some own elements in DAV namespace - don't use it for now -->
+                <!--
+                <element ref="dav:contentclass" minOccurs="0"/>
+                <element ref="dav:defaultdocument" minOccurs="0"/>
+                <element ref="dav:href" minOccurs="0"/>
+                <element ref="dav:iscollection" minOccurs="0"/>
+                <element ref="dav:ishidden" minOccurs="0"/>
+                <element ref="dav:isreadonly" minOccurs="0"/>
+                <element ref="dav:isroot" minOccurs="0"/>
+                <element ref="dav:isstructureddocument" minOccurs="0"/>
+                <element ref="dav:lastaccessed" minOccurs="0"/>
+                <element ref="dav:name" minOccurs="0"/>
+                <element ref="dav:parentname" minOccurs="0"/>
+                -->
+                <any processContents="skip" namespace="##other" maxOccurs="unbounded" />
+            </all>
+        </complexType>
+    </element>
+
+    <element name="propertybehavior">
+        <complexType>
+            <choice>
+                <element ref="dav:omit"/>
+                <element ref="dav:keepalive"/>
+            </choice>
+        </complexType>
+    </element>
+
+    <element name="omit">
+        <complexType/>
+    </element>
+
+    <element name="keepalive">
+        <complexType mixed="true">
+            <sequence>
+                <element ref="dav:href" maxOccurs="unbounded"/>
+            </sequence>
+        </complexType>
+    </element>
+
+    <element name="propertyupdate">
+        <complexType>
+            <choice maxOccurs="unbounded">
+                <element ref="dav:remove"/>
+                <element ref="dav:set"/>
+            </choice>
+        </complexType>
+    </element>
+
+    <element name="remove">
+        <complexType>
+            <sequence>
+                <element ref="dav:prop"/>
+            </sequence>
+        </complexType>
+    </element>
+
+    <element name="set">
+        <complexType>
+            <sequence>
+                <element ref="dav:prop"/>
+            </sequence>
+        </complexType>
+    </element>
+
+    <element name="propfind">
+        <complexType>
+            <choice>
+                <element ref="dav:allprop"/>
+                <element ref="dav:propname"/>
+                <element ref="dav:prop"/>
+            </choice>
+        </complexType>
+    </element>
+
+    <element name="allprop">
+        <complexType/>
+    </element>
+
+    <element name="propname">
+        <complexType/>
+    </element>
+
+    <element name="collection">
+        <complexType/>
+    </element>
+
+    <element name="creationdate">
+        <complexType mixed="true">
+            <sequence/>
+        </complexType>
+    </element>
+
+    <element name="displayname">
+        <complexType mixed="true">
+            <sequence/>
+        </complexType>
+    </element>
+
+    <element name="getcontentlanguage">
+        <complexType mixed="true">
+            <sequence/>
+        </complexType>
+    </element>
+
+    <element name="getcontentlength">
+        <complexType mixed="true">
+            <sequence/>
+        </complexType>
+    </element>
+
+    <element name="getcontenttype">
+        <complexType mixed="true">
+            <sequence/>
+        </complexType>
+    </element>
+
+    <element name="getetag">
+        <complexType mixed="true">
+            <sequence/>
+        </complexType>
+    </element>
+
+    <element name="getlastmodified">
+        <complexType mixed="true">
+            <sequence/>
+        </complexType>
+    </element>
+
+    <element name="lockdiscovery">
+        <complexType>
+            <sequence minOccurs="0" maxOccurs="unbounded">
+                <element ref="dav:activelock"/>
+            </sequence>
+        </complexType>
+    </element>
+
+    <element name="resourcetype">
+        <complexType>
+            <sequence>
+                <element ref="dav:collection" minOccurs="0"/>
+                <any processContents="skip" namespace="##other" minOccurs="0" maxOccurs="unbounded" />
+            </sequence>
+        </complexType>
+    </element>
+
+    <element name="supportedlock">
+        <complexType>
+            <sequence minOccurs="0" maxOccurs="unbounded">
+                <element ref="dav:lockentry"/>
+            </sequence>
+        </complexType>
+    </element>
+
+    <element name="source">
+        <complexType>
+            <sequence minOccurs="0" maxOccurs="unbounded">
+                <element ref="dav:link"/>
+            </sequence>
+        </complexType>
+    </element>
+
+    <element name="quota-available-bytes">
+        <complexType mixed="true">
+            <sequence/>
+        </complexType>
+    </element>
+
+    <element name="quota-used-bytes">
+        <complexType mixed="true">
+            <sequence/>
+        </complexType>
+    </element>
+
+    <element name="searchrequest">
+        <complexType>
+            <sequence>
+                <any processContents="skip" namespace="##other" minOccurs="1" maxOccurs="1" />
+            </sequence>
+        </complexType>
+    </element>
+
+    <element name="supported-report-set">
+        <complexType>
+            <sequence>
+                <element maxOccurs="unbounded" ref="dav:supported-report"/>
+            </sequence>
+        </complexType>
+    </element>
+
+    <element name="supported-report">
+        <complexType>
+            <sequence>
+                <element ref="dav:report"/>
+            </sequence>
+        </complexType>
+    </element>
+
+    <element name="report">
+        <complexType>
+            <sequence>
+                <any processContents="skip" namespace="##other" minOccurs="1" maxOccurs="1"/>
+            </sequence>
+        </complexType>
+    </element>
+
+    <element name="sync-collection">
+        <complexType>
+            <sequence>
+                <element ref="dav:sync-token" minOccurs="1" maxOccurs="1"/>
+                <element ref="dav:sync-level" minOccurs="1" maxOccurs="1"/>
+                <element ref="dav:limit" minOccurs="0" maxOccurs="1"/>
+                <element ref="dav:prop" minOccurs="1" maxOccurs="1"/>
+            </sequence>
+        </complexType>
+    </element>
+
+    <element name="sync-token" type="anyURI"/>
+
+    <element name="sync-level" type="string">
+        <simpleType>
+            <restriction base="string">
+                <enumeration value="1"/>
+                <enumeration value="infinite"/>
+            </restriction>
+        </simpleType>
+    </element>
+
+    <element name="limit">
+        <complexType>
+            <sequence>
+                <element ref="dav:nresults" minOccurs="1" maxOccurs="1"/>
+            </sequence>
+        </complexType>
+    </element>
+
+    <element name="nresults" type="integer"/>
+
+    <!-- Microsoft has some own elements in DAV namespace - don't use it for now -->
+    <!--
+    <element name="contentclass" type="xsd:string"/>
+    <element name="defaultdocument" type="xsd:string"/>
+    <element name="iscollection" type="xsd:string"/>
+    <element name="ishidden" type="xsd:string"/>
+    <element name="isreadonly" type="xsd:string"/>
+    <element name="isroot" type="xsd:string"/>
+    <element name="isstructureddocument" type="xsd:string"/>
+    <element name="lastaccessed" type="xsd:string"/>
+    <element name="name" type="xsd:string"/>
+    <element name="parentname" type="xsd:string"/>
+    -->
+</schema>
+
index aa7d8f884dbc0584098bd346319c6ee45f3b5436..72a30fb083b032f0f6f10838bf6fc1523418b253 100644 (file)
@@ -39,11 +39,16 @@ public class DeployedContentRepository extends CmsContentRepository {
                        DirectoryContentProvider directoryContentProvider = new DirectoryContentProvider(
                                        CmsContentRepository.DIRECTORY_BASE, userManager);
                        addProvider(directoryContentProvider);
+
+                       // remote
+//                     DavContentProvider davContentProvider = new DavContentProvider("/srv",
+//                                     URI.create("http://localhost/unstable/a2/"));
+//                     addProvider(davContentProvider);
                } catch (IOException e) {
                        throw new IllegalStateException("Cannot start content repository", e);
                }
-               long duration = System.currentTimeMillis()-begin;
-               log.debug(() -> "CMS content repository available (initialisation took "+duration+" ms)");
+               long duration = System.currentTimeMillis() - begin;
+               log.debug(() -> "CMS content repository available (initialisation took " + duration + " ms)");
        }
 
        @Override
@@ -51,14 +56,6 @@ public class DeployedContentRepository extends CmsContentRepository {
                super.stop();
        }
 
-//     public void addContentProvider(ContentProvider provider, Map<String, Object> properties) {
-////           String base = LangUtils.get(properties, CmsContentRepository.ACR_MOUNT_PATH_PROPERTY);
-//             addProvider(provider);
-//     }
-
-//     public void removeContentProvider(ContentProvider provider, Map<String, Object> properties) {
-//     }
-
        public void setUserManager(CmsUserManager userManager) {
                this.userManager = userManager;
        }
diff --git a/org.argeo.util/src/org/argeo/util/dav/DavClient.java b/org.argeo.util/src/org/argeo/util/dav/DavClient.java
new file mode 100644 (file)
index 0000000..4fa8648
--- /dev/null
@@ -0,0 +1,146 @@
+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
new file mode 100644 (file)
index 0000000..e527ba7
--- /dev/null
@@ -0,0 +1,5 @@
+package org.argeo.util.dav;
+
+public enum DavHeader {
+       Depth;
+}
diff --git a/org.argeo.util/src/org/argeo/util/dav/DavMethod.java b/org.argeo.util/src/org/argeo/util/dav/DavMethod.java
new file mode 100644 (file)
index 0000000..1472c9b
--- /dev/null
@@ -0,0 +1,10 @@
+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
new file mode 100644 (file)
index 0000000..22ffa17
--- /dev/null
@@ -0,0 +1,49 @@
+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 MODE_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/DavXmlElement.java b/org.argeo.util/src/org/argeo/util/dav/DavXmlElement.java
new file mode 100644 (file)
index 0000000..c054252
--- /dev/null
@@ -0,0 +1,45 @@
+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
new file mode 100644 (file)
index 0000000..966100e
--- /dev/null
@@ -0,0 +1,192 @@
+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.MODE_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();
+               }
+       }
+
+}