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