<?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>
//
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"),
//
--- /dev/null
+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());
+ }
+
+ }
+}
--- /dev/null
+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);
+ }
+ }
+}
--- /dev/null
+<?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>
+
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
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;
}
--- /dev/null
+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());
+
+ }
+
+}
--- /dev/null
+package org.argeo.util.dav;
+
+public enum DavHeader {
+ Depth;
+}
--- /dev/null
+package org.argeo.util.dav;
+
+public enum DavMethod {
+ // Generic HTTP
+ HEAD, //
+ // WebDav specific
+ PROPFIND, //
+ PROPPATCH, //
+ ;
+}
--- /dev/null
+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;
+ }
+
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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();
+ }
+ }
+
+}