From e5a22cdc7d0f4918f2740c626e1ab6384bd5ee44 Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Thu, 20 Jan 2022 12:58:20 +0100 Subject: [PATCH] Introduce namespace support --- .../org/argeo/api/gcr/CompositeString.java | 25 ++- .../src/org/argeo/api/gcr/Content.java | 57 +++++-- .../org/argeo/api/gcr/ContentMetadata.java | 15 -- .../src/org/argeo/api/gcr/ContentName.java | 153 +++++++++++++++++- .../argeo/api/gcr/ContentNameSupplier.java | 102 ++++++++++++ .../org/argeo/api/gcr/ContentNamespace.java | 106 ------------ .../api/gcr/ContentNotFoundException.java | 16 ++ .../src/org/argeo/api/gcr/ContentSession.java | 38 ++++- .../src/org/argeo/api/gcr/ContentUtils.java | 4 +- ...ttributeType.java => CrAttributeType.java} | 4 +- .../src/org/argeo/api/gcr/CrName.java | 48 ++++-- .../argeo/api/gcr/spi/AbstractContent.java | 36 +++-- .../argeo/api/gcr/spi/ContentProvider.java | 6 +- .../argeo/api/gcr/spi/ProvidedContent.java | 9 ++ .../argeo/api/gcr/spi/ProvidedRepository.java | 6 + .../argeo/api/gcr/spi/ProvidedSession.java | 68 ++++++++ .../src/org/argeo/cms/jcr/gcr/JcrContent.java | 60 +++---- .../argeo/cms/jcr/gcr/JcrContentProvider.java | 52 +++++- .../argeo/cms/swt/gcr/GcrContentTreeView.java | 14 +- .../argeo/cms/gcr/CmsContentRepository.java | 88 +++++++++- .../src/org/argeo/cms/gcr/fs/FsContent.java | 104 +++++++----- .../argeo/cms/gcr/fs/FsContentProvider.java | 10 +- .../src/org/argeo/cms/gcr/xml/DomContent.java | 131 ++++++++++++--- .../argeo/cms/gcr/xml/DomContentProvider.java | 108 +++++++++---- .../argeo/cms/gcr/xml/ElementIterator.java | 13 +- 25 files changed, 928 insertions(+), 345 deletions(-) delete mode 100644 org.argeo.api/src/org/argeo/api/gcr/ContentMetadata.java create mode 100644 org.argeo.api/src/org/argeo/api/gcr/ContentNameSupplier.java delete mode 100644 org.argeo.api/src/org/argeo/api/gcr/ContentNamespace.java create mode 100644 org.argeo.api/src/org/argeo/api/gcr/ContentNotFoundException.java rename org.argeo.api/src/org/argeo/api/gcr/{StandardAttributeType.java => CrAttributeType.java} (97%) create mode 100644 org.argeo.api/src/org/argeo/api/gcr/spi/ProvidedContent.java create mode 100644 org.argeo.api/src/org/argeo/api/gcr/spi/ProvidedRepository.java create mode 100644 org.argeo.api/src/org/argeo/api/gcr/spi/ProvidedSession.java diff --git a/org.argeo.api/src/org/argeo/api/gcr/CompositeString.java b/org.argeo.api/src/org/argeo/api/gcr/CompositeString.java index 1533c10ce..f979357b1 100644 --- a/org.argeo.api/src/org/argeo/api/gcr/CompositeString.java +++ b/org.argeo.api/src/org/argeo/api/gcr/CompositeString.java @@ -97,7 +97,13 @@ public class CompositeString { while (st.hasMoreTokens()) { res.add(st.nextToken().toLowerCase()); } - } else {// CAML + } else { + // single + String strLowerCase = str.toLowerCase(); + if (str.toUpperCase().equals(str) || strLowerCase.equals(str)) + return new String[] { strLowerCase }; + + // CAML StringBuilder current = null; for (char c : str.toCharArray()) { if (Character.isUpperCase(c)) { @@ -129,6 +135,13 @@ public class CompositeString { } static boolean smokeTests() { + CompositeString plainName = new CompositeString("NAME"); + assert "name".equals(plainName.toString()); + assert "NAME".equals(plainName.toString(UNDERSCORE, true)); + assert "name".equals(plainName.toString(UNDERSCORE, false)); + assert "name".equals(plainName.toStringCaml(false)); + assert "Name".equals(plainName.toStringCaml(true)); + CompositeString camlName = new CompositeString("myComplexName"); assert new CompositeString("my-complex-name").equals(camlName); @@ -138,16 +151,14 @@ public class CompositeString { assert "my-complex-name".equals(camlName.toString()); assert "MY_COMPLEX_NAME".equals(camlName.toString(UNDERSCORE, true)); + assert "my_complex_name".equals(camlName.toString(UNDERSCORE, false)); assert "myComplexName".equals(camlName.toStringCaml(false)); assert "MyComplexName".equals(camlName.toStringCaml(true)); - CompositeString plainName = new CompositeString("name"); - assert "name".equals(plainName.toString()); - return CompositeString.class.desiredAssertionStatus(); } -// public static void main(String[] args) { -// System.out.println(smokeTests()); -// } + public static void main(String[] args) { + System.out.println(smokeTests()); + } } diff --git a/org.argeo.api/src/org/argeo/api/gcr/Content.java b/org.argeo.api/src/org/argeo/api/gcr/Content.java index 9ce6ea4f6..e35f42f27 100644 --- a/org.argeo.api/src/org/argeo/api/gcr/Content.java +++ b/org.argeo.api/src/org/argeo/api/gcr/Content.java @@ -2,12 +2,15 @@ package org.argeo.api.gcr; import java.util.Map; +import javax.xml.XMLConstants; +import javax.xml.namespace.QName; + /** * A semi-structured content, with attributes, within a hierarchical structure. */ -public interface Content extends Iterable, Map { +public interface Content extends Iterable, Map { - String getName(); + QName getName(); String getPath(); @@ -17,12 +20,36 @@ public interface Content extends Iterable, Map { * ATTRIBUTES OPERATIONS */ - A get(String key, Class clss) throws IllegalArgumentException; + A get(QName key, Class clss) throws IllegalArgumentException; + + default Object get(String key) { + if (key.indexOf(':') >= 0) + throw new IllegalArgumentException("Name " + key + " has a prefix"); + return get(new QName(XMLConstants.NULL_NS_URI, key, XMLConstants.DEFAULT_NS_PREFIX)); + } + + default Object put(String key, Object value) { + if (key.indexOf(':') >= 0) + throw new IllegalArgumentException("Name " + key + " has a prefix"); + return put(new QName(XMLConstants.NULL_NS_URI, key, XMLConstants.DEFAULT_NS_PREFIX), value); + } + + default Object remove(String key) { + if (key.indexOf(':') >= 0) + throw new IllegalArgumentException("Name " + key + " has a prefix"); + return remove(new QName(XMLConstants.NULL_NS_URI, key, XMLConstants.DEFAULT_NS_PREFIX)); + } /* * CONTENT OPERATIONS */ - Content add(String name, ContentName... classes); + Content add(QName name, QName... classes); + + default Content add(String name, QName... classes) { + if (name.indexOf(':') >= 0) + throw new IllegalArgumentException("Name " + name + " has a prefix"); + return add(new QName(XMLConstants.NULL_NS_URI, name, XMLConstants.DEFAULT_NS_PREFIX), classes); + } void remove(); @@ -40,17 +67,17 @@ public interface Content extends Iterable, Map { /* * CONVENIENCE METHODS */ - default String attr(String key) { - return get(key, String.class); - } - - default String attr(Object key) { - return key != null ? attr(key.toString()) : attr(null); - } - - default A get(Object key, Class clss) { - return key != null ? get(key.toString(), clss) : get(null, clss); - } +// default String attr(String key) { +// return get(key, String.class); +// } +// +// default String attr(Object key) { +// return key != null ? attr(key.toString()) : attr(null); +// } +// +// default A get(Object key, Class clss) { +// return key != null ? get(key.toString(), clss) : get(null, clss); +// } /* * EXPERIMENTAL UNSUPPORTED diff --git a/org.argeo.api/src/org/argeo/api/gcr/ContentMetadata.java b/org.argeo.api/src/org/argeo/api/gcr/ContentMetadata.java deleted file mode 100644 index 841887f87..000000000 --- a/org.argeo.api/src/org/argeo/api/gcr/ContentMetadata.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.argeo.api.gcr; - -import java.io.Serializable; -import java.util.Set; - -public interface ContentMetadata extends Serializable { - Set getKnownKeys(); - - Set getTypes(); - - /** Whether that content can have unknown keys. */ - default boolean isStructured() { - return false; - } -} diff --git a/org.argeo.api/src/org/argeo/api/gcr/ContentName.java b/org.argeo.api/src/org/argeo/api/gcr/ContentName.java index ddc80c5b6..5acd53a00 100644 --- a/org.argeo.api/src/org/argeo/api/gcr/ContentName.java +++ b/org.argeo.api/src/org/argeo/api/gcr/ContentName.java @@ -1,17 +1,156 @@ package org.argeo.api.gcr; +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; import java.util.UUID; -public interface ContentName { - UUID getUuid(); +import javax.xml.XMLConstants; +import javax.xml.namespace.NamespaceContext; +import javax.xml.namespace.QName; + +/** + * A {@link QName} which MUST have prefix and whose {@link #toString()} method + * returns the prefixed form (prefix:localPart). + */ +public class ContentName extends QName { + private static final long serialVersionUID = 5722920985400306100L; + public final static UUID NIL_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000"); + /** + * The UUID v3 of http://www.w3.org/2000/xmlns/ within the standard DNS + * namespace, to be used as a base for the namespaces. + * + * @see https://www.w3.org/TR/xml-names/#ns-decl + */ + // uuidgen --md5 --namespace @dns --name http://www.w3.org/2000/xmlns/ + // NOTE : must be declared before default namespaces + public final static UUID XMLNS_UUID = UUID.fromString("4b352aad-ba1c-3139-b9d3-41e5816f6088"); + // uuidgen --md5 --namespace 4b352aad-ba1c-3139-b9d3-41e5816f6088 --name "" + public final static UUID NULL_NS_UUID = UUID.fromString("f07726e3-99c8-3178-b758-a86ed41f300d"); + + private final static Map namespaceUuids = Collections.synchronizedMap(new TreeMap<>()); + private final static Map nameUuids = Collections.synchronizedMap(new TreeMap<>()); + + static { + assert NULL_NS_UUID.equals(nameUUIDv3(XMLNS_UUID, XMLConstants.NULL_NS_URI.getBytes(UTF_8))); + } + +// private final UUID uuid; + + public ContentName(String namespaceURI, String localPart, NamespaceContext nsContext) { + this(namespaceURI, localPart, nsContext.getPrefix(namespaceURI)); + } + + protected ContentName(String namespaceURI, String localPart, String prefix) { + super(namespaceURI, localPart, prefix); + if (prefix == null) + throw new IllegalArgumentException("Prefix annot be null"); + } + + public ContentName(String localPart) { + this(XMLConstants.NULL_NS_URI, localPart, XMLConstants.DEFAULT_NS_PREFIX); + } + + public ContentName(QName qName, NamespaceContext nsContext) { + this(qName.getNamespaceURI(), qName.getLocalPart(), nsContext); + } + + public String toQNameString() { + return super.toString(); + } + + public String toPrefixedString() { + return toPrefixedString(this); + } + + /* + * OBJECT METHOS + */ + + @Override + public String toString() { + return toPrefixedString(); + } + + @Override + protected Object clone() throws CloneNotSupportedException { + return new ContentName(getNamespaceURI(), getLocalPart(), getPrefix()); + } + + public static String toPrefixedString(QName name) { + String prefix = name.getPrefix(); + assert prefix != null; + return "".equals(prefix) ? name.getLocalPart() : prefix + ":" + name.getLocalPart(); + } +// ContentNamespace getNamespace(); +// +// String getName(); + + public static UUID namespaceUuid(String namespaceURI) { + if (XMLConstants.NULL_NS_URI.equals(namespaceURI)) + return NULL_NS_UUID; + Objects.requireNonNull(namespaceURI, "Namespace URI cannot be null"); + synchronized (namespaceUuids) { + UUID namespaceUuid = namespaceUuids.get(namespaceURI); + if (namespaceUuid == null) { + namespaceUuid = nameUUIDv3(ContentName.XMLNS_UUID, + namespaceURI.toString().getBytes(StandardCharsets.UTF_8)); + namespaceUuids.put(namespaceURI, namespaceUuid); + } + return namespaceUuid; + } + } + + public static UUID nameUuid(String namespaceURI, QName name) { + return nameUuid(name.getNamespaceURI(), name.getLocalPart()); + } - ContentNamespace getNamespace(); + public static UUID nameUuid(String namespaceURI, String name) { + Objects.requireNonNull(namespaceURI, "Namespace cannot be null"); + Objects.requireNonNull(name, "Name cannot be null"); + synchronized (nameUuids) { + String key = XMLConstants.NULL_NS_URI.equals(namespaceURI) ? name : "{" + namespaceURI + "}" + name; + UUID nameUuid = nameUuids.get(key); + if (nameUuid == null) { + UUID namespaceUuid = namespaceUuid(namespaceURI); + nameUuid = nameUUIDv3(namespaceUuid, name.getBytes(StandardCharsets.UTF_8)); + namespaceUuids.put(key, nameUuid); + } + return nameUuid; + } + } + + /* + * CANONICAL IMPLEMENTATION based on java.util.UUID.nameUUIDFromBytes(byte[]) + */ + static UUID nameUUIDv3(UUID namespace, byte[] name) { + byte[] arr = new byte[name.length + 16]; + ContentName.copyUuidBytes(namespace, arr, 0); + System.arraycopy(name, 0, arr, 16, name.length); + return UUID.nameUUIDFromBytes(arr); + } + + static void copyUuidBytes(UUID uuid, byte[] arr, int offset) { + long msb = uuid.getMostSignificantBits(); + long lsb = uuid.getLeastSignificantBits(); + assert arr.length >= 16 + offset; + for (int i = offset; i < 8 + offset; i++) + arr[i] = (byte) ((msb >> ((7 - i) * 8)) & 0xff); + for (int i = 8 + offset; i < 16 + offset; i++) + arr[i] = (byte) ((lsb >> ((15 - i) * 8)) & 0xff); + } - String getName(); + /* + * UTILITIES + */ - static boolean contains(ContentName[] classes, ContentName name) { - for (ContentName clss : classes) { - if (clss.getUuid().equals(name.getUuid())) + public static boolean contains(QName[] classes, QName name) { + for (QName clss : classes) { + if (clss.equals(name)) return true; } return false; diff --git a/org.argeo.api/src/org/argeo/api/gcr/ContentNameSupplier.java b/org.argeo.api/src/org/argeo/api/gcr/ContentNameSupplier.java new file mode 100644 index 000000000..b6d802049 --- /dev/null +++ b/org.argeo.api/src/org/argeo/api/gcr/ContentNameSupplier.java @@ -0,0 +1,102 @@ +package org.argeo.api.gcr; + +import java.util.Collections; +import java.util.Iterator; +import java.util.function.Supplier; + +import javax.xml.XMLConstants; +import javax.xml.namespace.NamespaceContext; + +public interface ContentNameSupplier extends Supplier, NamespaceContext { + String name(); + + @Override + default ContentName get() { + return toContentName(); + } + + default ContentName toContentName() { + CompositeString cs = new CompositeString(name()); + String camlName = cs.toStringCaml(false); + return new ContentName(getNamespaceURI(), camlName, this); + } + + default String getNamespaceURI() { + return XMLConstants.NULL_NS_URI; + } + + default String getDefaultPrefix() { + return XMLConstants.DEFAULT_NS_PREFIX; + } + +// static ContentName toContentName(String namespaceURI, String localName, String prefix) { +// CompositeString cs = new CompositeString(localName); +// String camlName = cs.toStringCaml(false); +// return new ContentName(namespaceURI, camlName, this); +// } + + /* + * NAMESPACE CONTEXT + */ + + @Override + default String getNamespaceURI(String prefix) { + String namespaceURI = getStandardNamespaceURI(prefix); + if (namespaceURI != null) + return namespaceURI; + if (prefix.equals(getDefaultPrefix())) + return getNamespaceURI(); + return XMLConstants.NULL_NS_URI; + } + + @Override + default String getPrefix(String namespaceURI) { + String prefix = getStandardPrefix(namespaceURI); + if (prefix != null) + return prefix; + if (namespaceURI.equals(getNamespaceURI())) + return getDefaultPrefix(); + return null; + } + + @Override + default Iterator getPrefixes(String namespaceURI) { + Iterator it = getStandardPrefixes(namespaceURI); + if (it != null) + return it; + if (namespaceURI.equals(getNamespaceURI())) + return Collections.singleton(getDefaultPrefix()).iterator(); + return Collections.emptyIterator(); + } + + /* + * DEFAULT NAMESPACE CONTEXT OPERATIONS as specified in NamespaceContext + */ + static String getStandardPrefix(String namespaceURI) { + if (namespaceURI == null) + throw new IllegalArgumentException("Namespace URI cannot be null"); + if (XMLConstants.XML_NS_URI.equals(namespaceURI)) + return XMLConstants.XML_NS_PREFIX; + else if (XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(namespaceURI)) + return XMLConstants.XMLNS_ATTRIBUTE; + return null; + } + + static Iterator getStandardPrefixes(String namespaceURI) { + String prefix = ContentNameSupplier.getStandardPrefix(namespaceURI); + if (prefix == null) + return null; + return Collections.singleton(prefix).iterator(); + } + + static String getStandardNamespaceURI(String prefix) { + if (prefix == null) + throw new IllegalArgumentException("Prefix cannot be null"); + if (XMLConstants.XML_NS_PREFIX.equals(prefix)) + return XMLConstants.XML_NS_URI; + else if (XMLConstants.XMLNS_ATTRIBUTE.equals(prefix)) + return XMLConstants.XMLNS_ATTRIBUTE_NS_URI; + return null; + } + +} diff --git a/org.argeo.api/src/org/argeo/api/gcr/ContentNamespace.java b/org.argeo.api/src/org/argeo/api/gcr/ContentNamespace.java deleted file mode 100644 index a59557180..000000000 --- a/org.argeo.api/src/org/argeo/api/gcr/ContentNamespace.java +++ /dev/null @@ -1,106 +0,0 @@ -package org.argeo.api.gcr; - -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.util.Objects; -import java.util.UUID; - -/** - * An XML-style namespace with a related UUID v3. - * - * @see https://www.w3.org/TR/xml-names/ - */ -public class ContentNamespace { - private final UUID uuid; - private final URI uri; - - public ContentNamespace(String uri) { - try { - this.uri = new URI(uri); - } catch (URISyntaxException e) { - throw new IllegalArgumentException("Cannot interpret " + uri + " as URI", e); - } - this.uuid = namespaceUuid(this.uri); - } - - public ContentNamespace(URI uri) { - this.uri = uri; - this.uuid = namespaceUuid(uri); - } - - ContentNamespace(URI uri, UUID uuid) { - this.uri = uri; - assert uuid.equals(namespaceUuid(uri)); - this.uuid = uuid; - } - - /** Empty namespace */ - private ContentNamespace() { - try { - this.uri = new URI(""); - } catch (URISyntaxException e) { - throw new IllegalStateException("Cannot create empty URI"); - } - this.uuid = NIL_UUID; - } - - public UUID getUuid() { - return uuid; - } - - public URI getUri() { - return uri; - } - - public UUID nameUuid(String name) { - return nameUuid(getUuid(), name); - } - - - public final static UUID NIL_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000"); - public final static ContentNamespace EMPTY_NS = new ContentNamespace(); - - /** - * The UUID v3 of http://www.w3.org/2000/xmlns/ within the standard DNS - * namespace, to be used as a base for the namespaces. - * - * @see https://www.w3.org/TR/xml-names/#ns-decl - */ - // uuidgen --md5 --namespace @dns --name http://www.w3.org/2000/xmlns/ - // NOTE : must be declared before default namespaces - public final static UUID XMLNS_UUID = UUID.fromString("4b352aad-ba1c-3139-b9d3-41e5816f6088"); - public final static ContentNamespace CR_NS = new ContentNamespace("http://argeo.org/ns/cr"); - - - public static UUID namespaceUuid(URI namespaceUri) { - Objects.requireNonNull(namespaceUri, "Namespace URI cannot be null"); - return nameUUIDv3(XMLNS_UUID, namespaceUri.toString().getBytes(StandardCharsets.UTF_8)); - } - - public static UUID nameUuid(UUID namespace, String name) { - Objects.requireNonNull(namespace, "Namespace cannot be null"); - Objects.requireNonNull(namespace, "Name cannot be null"); - return nameUUIDv3(namespace, name.getBytes(StandardCharsets.UTF_8)); - } - - /* - * CANONICAL IMPLEMENTATION based on java.util.UUID.nameUUIDFromBytes(byte[]) - */ - private static UUID nameUUIDv3(UUID namespace, byte[] name) { - byte[] arr = new byte[name.length + 16]; - copyUuidBytes(namespace, arr, 0); - System.arraycopy(name, 0, arr, 16, name.length); - return UUID.nameUUIDFromBytes(arr); - } - - private static void copyUuidBytes(UUID uuid, byte[] arr, int offset) { - long msb = uuid.getMostSignificantBits(); - long lsb = uuid.getLeastSignificantBits(); - assert arr.length >= 16 + offset; - for (int i = offset; i < 8 + offset; i++) - arr[i] = (byte) ((msb >> ((7 - i) * 8)) & 0xff); - for (int i = 8 + offset; i < 16 + offset; i++) - arr[i] = (byte) ((lsb >> ((15 - i) * 8)) & 0xff); - } -} diff --git a/org.argeo.api/src/org/argeo/api/gcr/ContentNotFoundException.java b/org.argeo.api/src/org/argeo/api/gcr/ContentNotFoundException.java new file mode 100644 index 000000000..8a0909342 --- /dev/null +++ b/org.argeo.api/src/org/argeo/api/gcr/ContentNotFoundException.java @@ -0,0 +1,16 @@ +package org.argeo.api.gcr; + +/** When a countent was requested which does not exists, equivalent to HTTP code 404.*/ +public class ContentNotFoundException extends RuntimeException { + private static final long serialVersionUID = -8629074900713760886L; + + public ContentNotFoundException(String message, Throwable cause) { + super(message, cause); + } + + public ContentNotFoundException(String message) { + super(message); + } + + +} diff --git a/org.argeo.api/src/org/argeo/api/gcr/ContentSession.java b/org.argeo.api/src/org/argeo/api/gcr/ContentSession.java index d779093be..6c80189b2 100644 --- a/org.argeo.api/src/org/argeo/api/gcr/ContentSession.java +++ b/org.argeo.api/src/org/argeo/api/gcr/ContentSession.java @@ -1,13 +1,49 @@ package org.argeo.api.gcr; import java.util.Locale; +import java.util.Objects; import javax.security.auth.Subject; +import javax.xml.XMLConstants; +import javax.xml.namespace.NamespaceContext; +import javax.xml.namespace.QName; -public interface ContentSession { +public interface ContentSession extends NamespaceContext { Subject getSubject(); Locale getLocale(); Content get(String path); + + /* + * NAMESPACE CONTEXT + */ + + default ContentName parsePrefixedName(String nameWithPrefix) { + Objects.requireNonNull(nameWithPrefix, "Name cannot be null"); + if (nameWithPrefix.charAt(0) == '{') { + return new ContentName(QName.valueOf(nameWithPrefix), this); + } + int index = nameWithPrefix.indexOf(':'); + if (index < 0) { + return new ContentName(nameWithPrefix); + } + String prefix = nameWithPrefix.substring(0, index); + // TODO deal with empty name? + String localName = nameWithPrefix.substring(index + 1); + String namespaceURI = getNamespaceURI(prefix); + if (XMLConstants.NULL_NS_URI.equals(namespaceURI)) + throw new IllegalStateException("Prefix " + prefix + " is unbound."); + return new ContentName(namespaceURI, localName, prefix); + } + + default String toPrefixedName(QName name) { + if (XMLConstants.NULL_NS_URI.equals(name.getNamespaceURI())) + return name.getLocalPart(); + String prefix = getPrefix(name.getNamespaceURI()); + if (prefix == null) + throw new IllegalStateException("Namespace " + name.getNamespaceURI() + " is unbound."); + return prefix + ":" + name.getLocalPart(); + } + } diff --git a/org.argeo.api/src/org/argeo/api/gcr/ContentUtils.java b/org.argeo.api/src/org/argeo/api/gcr/ContentUtils.java index 01949347f..cf5115590 100644 --- a/org.argeo.api/src/org/argeo/api/gcr/ContentUtils.java +++ b/org.argeo.api/src/org/argeo/api/gcr/ContentUtils.java @@ -8,6 +8,8 @@ import java.util.Base64; import java.util.List; import java.util.function.BiConsumer; +import javax.xml.namespace.QName; + public class ContentUtils { public static void traverse(Content content, BiConsumer doIt) { traverse(content, doIt, 0); @@ -28,7 +30,7 @@ public class ContentUtils { } String prefix = sb.toString(); out.println(prefix + content.getName()); - for (String key : content.keySet()) { + for (QName key : content.keySet()) { out.println(prefix + " " + key + "=" + content.get(key)); } if (printText) { diff --git a/org.argeo.api/src/org/argeo/api/gcr/StandardAttributeType.java b/org.argeo.api/src/org/argeo/api/gcr/CrAttributeType.java similarity index 97% rename from org.argeo.api/src/org/argeo/api/gcr/StandardAttributeType.java rename to org.argeo.api/src/org/argeo/api/gcr/CrAttributeType.java index ea70880b5..28f1bc0f6 100644 --- a/org.argeo.api/src/org/argeo/api/gcr/StandardAttributeType.java +++ b/org.argeo.api/src/org/argeo/api/gcr/CrAttributeType.java @@ -11,7 +11,7 @@ import java.util.UUID; * belong to java.base and can be implicitly derived form a given * String. */ -public enum StandardAttributeType { +public enum CrAttributeType { BOOLEAN(Boolean.class, new BooleanFormatter()), // INTEGER(Integer.class, new IntegerFormatter()), // LONG(Long.class, new LongFormatter()), // @@ -24,7 +24,7 @@ public enum StandardAttributeType { STRING(String.class, new StringFormatter()), // ; - private StandardAttributeType(Class clss, AttributeFormatter formatter) { + private CrAttributeType(Class clss, AttributeFormatter formatter) { this.clss = clss; this.formatter = formatter; } diff --git a/org.argeo.api/src/org/argeo/api/gcr/CrName.java b/org.argeo.api/src/org/argeo/api/gcr/CrName.java index 3fc148185..283e5cc71 100644 --- a/org.argeo.api/src/org/argeo/api/gcr/CrName.java +++ b/org.argeo.api/src/org/argeo/api/gcr/CrName.java @@ -1,42 +1,58 @@ package org.argeo.api.gcr; -import java.util.UUID; - /** Standard names. */ -public enum CrName implements ContentName { +public enum CrName implements ContentNameSupplier { + /* * TYPES */ - COLLECTION("collection"), // a collection type + COLLECTION, // a collection type /* * ATTRIBUTES */ - UUID("uuid"), // the UUID of a content + UUID, // the UUID of a content + + /* + * ATTRIBUTES FROM FILE SEMANTICS + */ + CREATION_TIME, // + LAST_MODIFIED_TIME, // + SIZE, // + FILE_KEY, // + OWNER, // + GROUP, // + PERMISSIONS, // + + /* + * CONTENT NAMES + */ + ROOT, + // ; - private String name; - private UUID uuid; + public final static String CR_NAMESPACE_URI = "http://argeo.org/ns/cr"; + public final static String CR_DEFAULT_PREFIX = "cr"; + private final ContentName value; - CrName(String name) { - this.name = name; - this.uuid = ContentNamespace.CR_NS.nameUuid(name); + CrName() { + value = toContentName(); } @Override - public UUID getUuid() { - return uuid; + public ContentName get() { + return value; } @Override - public ContentNamespace getNamespace() { - return ContentNamespace.CR_NS; + public String getNamespaceURI() { + return CR_NAMESPACE_URI; } @Override - public String getName() { - return name; + public String getDefaultPrefix() { + return CR_DEFAULT_PREFIX; } } diff --git a/org.argeo.api/src/org/argeo/api/gcr/spi/AbstractContent.java b/org.argeo.api/src/org/argeo/api/gcr/spi/AbstractContent.java index 79c59f2f6..10f36acdf 100644 --- a/org.argeo.api/src/org/argeo/api/gcr/spi/AbstractContent.java +++ b/org.argeo.api/src/org/argeo/api/gcr/spi/AbstractContent.java @@ -8,12 +8,15 @@ import java.util.List; import java.util.Map; import java.util.Set; +import javax.xml.namespace.QName; + import org.argeo.api.gcr.Content; +import org.argeo.api.gcr.CrName; -public abstract class AbstractContent extends AbstractMap implements Content { +public abstract class AbstractContent extends AbstractMap implements Content { @Override - public Set> entrySet() { + public Set> entrySet() { // Set> result = new HashSet<>(); // for (String key : keys()) { // Entry entry = new Entry() { @@ -36,13 +39,13 @@ public abstract class AbstractContent extends AbstractMap implem // }; // result.add(entry); // } - Set> result = new AttrSet(); + Set> result = new AttrSet(); return result; } - protected abstract Iterable keys(); + protected abstract Iterable keys(); - protected abstract void removeAttr(String key); + protected abstract void removeAttr(QName key); @Override public String getPath() { @@ -50,8 +53,9 @@ public abstract class AbstractContent extends AbstractMap implem collectAncestors(ancestors, this); StringBuilder path = new StringBuilder(); for (Content c : ancestors) { - String name = c.getName(); - if (!"".equals(name)) + QName name = c.getName(); + // FIXME + if (!CrName.ROOT.get().equals(name)) path.append('/').append(name); } return path.toString(); @@ -74,21 +78,21 @@ public abstract class AbstractContent extends AbstractMap implem @Override public String toString() { - return "content "+getPath(); + return "content " + getPath(); } /* * SUB CLASSES */ - class AttrSet extends AbstractSet> { + class AttrSet extends AbstractSet> { @Override - public Iterator> iterator() { - final Iterator keys = keys().iterator(); - Iterator> it = new Iterator>() { + public Iterator> iterator() { + final Iterator keys = keys().iterator(); + Iterator> it = new Iterator>() { - String key = null; + QName key = null; @Override public boolean hasNext() { @@ -96,11 +100,11 @@ public abstract class AbstractContent extends AbstractMap implem } @Override - public Entry next() { + public Entry next() { key = keys.next(); // TODO check type Object value = get(key, Object.class); - AbstractMap.SimpleEntry entry = new SimpleEntry<>(key, value); + AbstractMap.SimpleEntry entry = new SimpleEntry<>(key, value); return entry; } @@ -120,7 +124,7 @@ public abstract class AbstractContent extends AbstractMap implem @Override public int size() { int count = 0; - for (String key : keys()) { + for (QName key : keys()) { count++; } return count; diff --git a/org.argeo.api/src/org/argeo/api/gcr/spi/ContentProvider.java b/org.argeo.api/src/org/argeo/api/gcr/spi/ContentProvider.java index 973131948..3a24ca24b 100644 --- a/org.argeo.api/src/org/argeo/api/gcr/spi/ContentProvider.java +++ b/org.argeo.api/src/org/argeo/api/gcr/spi/ContentProvider.java @@ -1,11 +1,9 @@ package org.argeo.api.gcr.spi; -import java.util.function.Supplier; - import org.argeo.api.gcr.Content; -public interface ContentProvider extends Supplier { +public interface ContentProvider { - Content get(String relativePath); + Content get(ProvidedSession session, String mountPath, String relativePath); } diff --git a/org.argeo.api/src/org/argeo/api/gcr/spi/ProvidedContent.java b/org.argeo.api/src/org/argeo/api/gcr/spi/ProvidedContent.java new file mode 100644 index 000000000..e75f62d45 --- /dev/null +++ b/org.argeo.api/src/org/argeo/api/gcr/spi/ProvidedContent.java @@ -0,0 +1,9 @@ +package org.argeo.api.gcr.spi; + +import org.argeo.api.gcr.Content; + +public interface ProvidedContent extends Content { + ProvidedSession getSession(); + + ContentProvider getProvider(); +} diff --git a/org.argeo.api/src/org/argeo/api/gcr/spi/ProvidedRepository.java b/org.argeo.api/src/org/argeo/api/gcr/spi/ProvidedRepository.java new file mode 100644 index 000000000..2db13f9a1 --- /dev/null +++ b/org.argeo.api/src/org/argeo/api/gcr/spi/ProvidedRepository.java @@ -0,0 +1,6 @@ +package org.argeo.api.gcr.spi; + +import org.argeo.api.gcr.ContentRepository; + +public interface ProvidedRepository extends ContentRepository { +} diff --git a/org.argeo.api/src/org/argeo/api/gcr/spi/ProvidedSession.java b/org.argeo.api/src/org/argeo/api/gcr/spi/ProvidedSession.java new file mode 100644 index 000000000..4e93684e5 --- /dev/null +++ b/org.argeo.api/src/org/argeo/api/gcr/spi/ProvidedSession.java @@ -0,0 +1,68 @@ +package org.argeo.api.gcr.spi; + +import java.util.Collections; +import java.util.Iterator; +import java.util.Set; + +import javax.xml.XMLConstants; +import javax.xml.namespace.NamespaceContext; + +import org.argeo.api.gcr.ContentNameSupplier; +import org.argeo.api.gcr.ContentSession; + +public interface ProvidedSession extends ContentSession, NamespaceContext { + ProvidedRepository getRepository(); + + /* + * NAMESPACE CONTEXT + */ + /** @return the bound namespace or null if not found */ + String findNamespace(String prefix); + + // TODO find the default prefix? + Set findPrefixes(String namespaceURI); + + /** To be overridden for optimisation, as it will be called a lot */ + default String findPrefix(String namespaceURI) { + Set prefixes = findPrefixes(namespaceURI); + if (prefixes.isEmpty()) + return null; + return prefixes.iterator().next(); + } + + @Override + default String getNamespaceURI(String prefix) { + String namespaceURI = ContentNameSupplier.getStandardNamespaceURI(prefix); + if (namespaceURI != null) + return namespaceURI; + if (XMLConstants.DEFAULT_NS_PREFIX.equals(prefix)) + return XMLConstants.NULL_NS_URI; + namespaceURI = findNamespace(prefix); + if (namespaceURI != null) + return namespaceURI; + return XMLConstants.NULL_NS_URI; + } + + @Override + default String getPrefix(String namespaceURI) { + String prefix = ContentNameSupplier.getStandardPrefix(namespaceURI); + if (prefix != null) + return prefix; + if (XMLConstants.NULL_NS_URI.equals(namespaceURI)) + return XMLConstants.DEFAULT_NS_PREFIX; + return findPrefix(namespaceURI); + } + + @Override + default Iterator getPrefixes(String namespaceURI) { + Iterator standard = ContentNameSupplier.getStandardPrefixes(namespaceURI); + if (standard != null) + return standard; + if (XMLConstants.NULL_NS_URI.equals(namespaceURI)) + return Collections.singleton(XMLConstants.DEFAULT_NS_PREFIX).iterator(); + Set prefixes = findPrefixes(namespaceURI); + assert prefixes != null; + return prefixes.iterator(); + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/gcr/JcrContent.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/gcr/JcrContent.java index d319c5504..ddef0078e 100644 --- a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/gcr/JcrContent.java +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/gcr/JcrContent.java @@ -11,53 +11,57 @@ import javax.jcr.PropertyType; import javax.jcr.RepositoryException; import javax.jcr.Value; import javax.jcr.nodetype.NodeType; +import javax.xml.namespace.QName; import org.argeo.api.gcr.Content; -import org.argeo.api.gcr.ContentName; import org.argeo.api.gcr.spi.AbstractContent; +import org.argeo.api.gcr.spi.ProvidedSession; import org.argeo.jcr.Jcr; import org.argeo.jcr.JcrException; public class JcrContent extends AbstractContent { - private JcrContentProvider contentProvider; private Node jcrNode; - protected JcrContent(JcrContentProvider contentSession, Node node) { - this.contentProvider = contentSession; + private JcrContentProvider provider; + private ProvidedSession session; + + protected JcrContent(ProvidedSession session, JcrContentProvider provider, Node node) { + this.session = session; + this.provider = provider; this.jcrNode = node; } @Override - public String getName() { - return Jcr.getName(jcrNode); + public QName getName() { + return session.parsePrefixedName(Jcr.getName(jcrNode)); } @Override - public A get(String key, Class clss) { + public A get(QName key, Class clss) { if (isDefaultAttrTypeRequested(clss)) { - return (A) get(jcrNode, key); + return (A) get(jcrNode, key.toString()); } - return (A) Jcr.get(jcrNode, key); + return (A) Jcr.get(jcrNode, key.toString()); } @Override public Iterator iterator() { try { - return new JcrContentIterator(contentProvider, jcrNode.getNodes()); + return new JcrContentIterator(jcrNode.getNodes()); } catch (RepositoryException e) { throw new JcrException("Cannot list children of " + jcrNode, e); } } @Override - protected Iterable keys() { - return new Iterable() { + protected Iterable keys() { + return new Iterable() { @Override - public Iterator iterator() { + public Iterator iterator() { try { PropertyIterator propertyIterator = jcrNode.getProperties(); - return new JcrKeyIterator(contentProvider, propertyIterator); + return new JcrKeyIterator(provider, propertyIterator); } catch (RepositoryException e) { throw new JcrException("Cannot retrive properties from " + jcrNode, e); } @@ -95,14 +99,12 @@ public class JcrContent extends AbstractContent { } } - static class JcrContentIterator implements Iterator { - private final JcrContentProvider contentSession; + class JcrContentIterator implements Iterator { private final NodeIterator nodeIterator; // we keep track in order to be able to delete it private JcrContent current = null; - protected JcrContentIterator(JcrContentProvider contentSession, NodeIterator nodeIterator) { - this.contentSession = contentSession; + protected JcrContentIterator(NodeIterator nodeIterator) { this.nodeIterator = nodeIterator; } @@ -113,7 +115,7 @@ public class JcrContent extends AbstractContent { @Override public Content next() { - current = new JcrContent(contentSession, nodeIterator.nextNode()); + current = new JcrContent(session, provider, nodeIterator.nextNode()); return current; } @@ -128,14 +130,14 @@ public class JcrContent extends AbstractContent { @Override public Content getParent() { - return new JcrContent(contentProvider, Jcr.getParent(getJcrNode())); + return new JcrContent(session, provider, Jcr.getParent(getJcrNode())); } @Override - public Content add(String name, ContentName... classes) { + public Content add(QName name, QName... classes) { if (classes.length > 0) { - ContentName primaryType = classes[0]; - Node child = Jcr.addNode(getJcrNode(), name, primaryType.toString()); + QName primaryType = classes[0]; + Node child = Jcr.addNode(getJcrNode(), name.toString(), primaryType.toString()); for (int i = 1; i < classes.length; i++) { try { child.addMixin(classes[i].toString()); @@ -145,7 +147,7 @@ public class JcrContent extends AbstractContent { } } else { - Jcr.addNode(getJcrNode(), name, NodeType.NT_UNSTRUCTURED); + Jcr.addNode(getJcrNode(), name.toString(), NodeType.NT_UNSTRUCTURED); } return null; } @@ -156,8 +158,8 @@ public class JcrContent extends AbstractContent { } @Override - protected void removeAttr(String key) { - Property property = Jcr.getProperty(getJcrNode(), key); + protected void removeAttr(QName key) { + Property property = Jcr.getProperty(getJcrNode(), key.toString()); if (property != null) { try { property.remove(); @@ -168,7 +170,7 @@ public class JcrContent extends AbstractContent { } - static class JcrKeyIterator implements Iterator { + class JcrKeyIterator implements Iterator { private final JcrContentProvider contentSession; private final PropertyIterator propertyIterator; @@ -183,12 +185,12 @@ public class JcrContent extends AbstractContent { } @Override - public String next() { + public QName next() { Property property = null; try { property = propertyIterator.nextProperty(); // TODO map standard property names - return property.getName(); + return session.parsePrefixedName(property.getName()); } catch (RepositoryException e) { throw new JcrException("Cannot retrieve property " + property, null); } diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/gcr/JcrContentProvider.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/gcr/JcrContentProvider.java index 5e4a1b337..761c087f0 100644 --- a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/gcr/JcrContentProvider.java +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/gcr/JcrContentProvider.java @@ -1,18 +1,30 @@ package org.argeo.cms.jcr.gcr; +import java.util.Arrays; +import java.util.Iterator; + import javax.jcr.Repository; +import javax.jcr.RepositoryException; import javax.jcr.Session; +import javax.xml.namespace.NamespaceContext; import org.argeo.api.gcr.Content; import org.argeo.api.gcr.spi.ContentProvider; +import org.argeo.api.gcr.spi.ProvidedSession; import org.argeo.cms.jcr.CmsJcrUtils; +import org.argeo.jcr.JcrException; +import org.argeo.jcr.JcrUtils; -public class JcrContentProvider implements ContentProvider { +public class JcrContentProvider implements ContentProvider, NamespaceContext { private Repository jcrRepository; - private Session jcrSession; + private Session adminSession; public void init() { - jcrSession = CmsJcrUtils.openDataAdminSession(jcrRepository, null); + adminSession = CmsJcrUtils.openDataAdminSession(jcrRepository, null); + } + + public void destroy() { + JcrUtils.logoutQuietly(adminSession); } public void setJcrRepository(Repository jcrRepository) { @@ -20,15 +32,39 @@ public class JcrContentProvider implements ContentProvider { } @Override - public Content get() { + public Content get(ProvidedSession session, String mountPath, String relativePath) { + // TODO Auto-generated method stub return null; } + /* + * NAMESPACE CONTEXT + */ @Override - public Content get(String relativePath) { - // TODO Auto-generated method stub - return null; + public String getNamespaceURI(String prefix) { + try { + return adminSession.getNamespaceURI(prefix); + } catch (RepositoryException e) { + throw new JcrException(e); + } + } + + @Override + public String getPrefix(String namespaceURI) { + try { + return adminSession.getNamespacePrefix(namespaceURI); + } catch (RepositoryException e) { + throw new JcrException(e); + } + } + + @Override + public Iterator getPrefixes(String namespaceURI) { + try { + return Arrays.asList(adminSession.getNamespacePrefix(namespaceURI)).iterator(); + } catch (RepositoryException e) { + throw new JcrException(e); + } } - } diff --git a/org.argeo.cms.swt/src/org/argeo/cms/swt/gcr/GcrContentTreeView.java b/org.argeo.cms.swt/src/org/argeo/cms/swt/gcr/GcrContentTreeView.java index 092a35739..ba6d7b477 100644 --- a/org.argeo.cms.swt/src/org/argeo/cms/swt/gcr/GcrContentTreeView.java +++ b/org.argeo.cms.swt/src/org/argeo/cms/swt/gcr/GcrContentTreeView.java @@ -3,6 +3,8 @@ package org.argeo.cms.swt.gcr; import java.nio.file.Path; import java.nio.file.Paths; +import javax.xml.namespace.QName; + import org.argeo.api.gcr.Content; import org.argeo.cms.gcr.fs.FsContentProvider; import org.argeo.cms.swt.CmsSwtUtils; @@ -36,7 +38,7 @@ public class GcrContentTreeView extends Composite { treeGd.widthHint = 300; tree.setLayoutData(treeGd); initTable(); - + table.setLayoutData(CmsSwtUtils.fillAll()); } @@ -44,7 +46,7 @@ public class GcrContentTreeView extends Composite { tree = new Tree(this, 0); for (Content c : rootContent) { TreeItem root = new TreeItem(tree, 0); - root.setText(c.getName()); + root.setText(c.getName().toString()); root.setData(c); new TreeItem(root, 0); } @@ -59,7 +61,7 @@ public class GcrContentTreeView extends Composite { Content content = (Content) root.getData(); for (Content c : content) { TreeItem item = new TreeItem(root, 0); - item.setText(c.getName()); + item.setText(c.getName().toString()); item.setData(c); boolean hasChildren = true; if (hasChildren) { @@ -91,9 +93,9 @@ public class GcrContentTreeView extends Composite { for (TableItem item : table.getItems()) { item.dispose(); } - for (String key : selected.keySet()) { + for (QName key : selected.keySet()) { TableItem item = new TableItem(table, 0); - item.setText(0, key); + item.setText(0, key.toString()); Object value = selected.get(key); item.setText(1, value.toString()); } @@ -115,7 +117,7 @@ public class GcrContentTreeView extends Composite { shell.setLayout(new FillLayout()); FsContentProvider contentSession = new FsContentProvider(basePath); - GcrContentTreeView treeView = new GcrContentTreeView(shell, 0, contentSession.get()); +// GcrContentTreeView treeView = new GcrContentTreeView(shell, 0, contentSession.get("/")); shell.setSize(shell.computeSize(800, 600)); shell.open(); diff --git a/org.argeo.cms/src/org/argeo/cms/gcr/CmsContentRepository.java b/org.argeo.cms/src/org/argeo/cms/gcr/CmsContentRepository.java index 0baae356c..69f2e94d4 100644 --- a/org.argeo.cms/src/org/argeo/cms/gcr/CmsContentRepository.java +++ b/org.argeo.cms/src/org/argeo/cms/gcr/CmsContentRepository.java @@ -4,19 +4,45 @@ import java.security.AccessController; import java.util.Locale; import java.util.Map; import java.util.NavigableMap; +import java.util.Set; import java.util.TreeMap; +import java.util.stream.Collectors; import javax.security.auth.Subject; import org.argeo.api.gcr.Content; -import org.argeo.api.gcr.ContentRepository; import org.argeo.api.gcr.ContentSession; +import org.argeo.api.gcr.CrName; import org.argeo.api.gcr.spi.ContentProvider; +import org.argeo.api.gcr.spi.ProvidedRepository; +import org.argeo.api.gcr.spi.ProvidedSession; import org.argeo.cms.internal.runtime.CmsContextImpl; -public class CmsContentRepository implements ContentRepository { +public class CmsContentRepository implements ProvidedRepository { private NavigableMap partitions = new TreeMap<>(); + // TODO synchronize ? + private NavigableMap prefixes = new TreeMap<>(); + + public CmsContentRepository() { + prefixes.put(CrName.CR_DEFAULT_PREFIX, CrName.CR_NAMESPACE_URI); + prefixes.put("basic", CrName.CR_NAMESPACE_URI); + prefixes.put("owner", CrName.CR_NAMESPACE_URI); + prefixes.put("posix", CrName.CR_NAMESPACE_URI); + } + + public void start() { + + } + + public void stop() { + + } + + /* + * REPOSITORY + */ + @Override public ContentSession get() { return get(CmsContextImpl.getCmsContext().getDefaultLocale()); @@ -32,7 +58,26 @@ public class CmsContentRepository implements ContentRepository { partitions.put(base, provider); } - class CmsContentSession implements ContentSession { + public void registerPrefix(String prefix, String namespaceURI) { + String registeredUri = prefixes.get(prefix); + if (registeredUri == null) { + prefixes.put(prefix, namespaceURI); + return; + } + if (!registeredUri.equals(namespaceURI)) + throw new IllegalStateException("Prefix " + prefix + " is already registred for " + registeredUri); + // do nothing if same namespace is already registered + } + + /* + * NAMESPACE CONTEXT + */ + + /* + * SESSION + */ + + class CmsContentSession implements ProvidedSession { private Subject subject; private Locale locale; @@ -43,9 +88,11 @@ public class CmsContentRepository implements ContentRepository { @Override public Content get(String path) { - Map.Entry provider = partitions.floorEntry(path); - String relativePath = path.substring(provider.getKey().length()); - return provider.getValue().get(relativePath); + Map.Entry entry = partitions.floorEntry(path); + String mountPath = entry.getKey(); + ContentProvider provider = entry.getValue(); + String relativePath = path.substring(mountPath.length()); + return provider.get(CmsContentSession.this, mountPath, relativePath); } @Override @@ -58,6 +105,35 @@ public class CmsContentRepository implements ContentRepository { return locale; } + @Override + public ProvidedRepository getRepository() { + return CmsContentRepository.this; + } + + /* + * NAMESPACE CONTEXT + */ + + @Override + public String findNamespace(String prefix) { + return prefixes.get(prefix); + } + + @Override + public Set findPrefixes(String namespaceURI) { + Set res = prefixes.entrySet().stream().filter(e -> e.getValue().equals(namespaceURI)) + .map(Map.Entry::getKey).collect(Collectors.toUnmodifiableSet()); + + return res; + } + + @Override + public String findPrefix(String namespaceURI) { + if (CrName.CR_NAMESPACE_URI.equals(namespaceURI) && prefixes.containsKey(CrName.CR_DEFAULT_PREFIX)) + return CrName.CR_DEFAULT_PREFIX; + return ProvidedSession.super.findPrefix(namespaceURI); + } + } } diff --git a/org.argeo.cms/src/org/argeo/cms/gcr/fs/FsContent.java b/org.argeo.cms/src/org/argeo/cms/gcr/fs/FsContent.java index 62ae613f0..5efa65959 100644 --- a/org.argeo.cms/src/org/argeo/cms/gcr/fs/FsContent.java +++ b/org.argeo.cms/src/org/argeo/cms/gcr/fs/FsContent.java @@ -8,41 +8,62 @@ import java.nio.file.Path; import java.nio.file.attribute.FileTime; import java.nio.file.attribute.UserDefinedFileAttributeView; import java.time.Instant; -import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.Map; import java.util.Set; +import javax.xml.namespace.QName; + import org.argeo.api.gcr.Content; +import org.argeo.api.gcr.ContentName; import org.argeo.api.gcr.ContentResourceException; import org.argeo.api.gcr.CrName; -import org.argeo.api.gcr.ContentName; import org.argeo.api.gcr.spi.AbstractContent; +import org.argeo.api.gcr.spi.ProvidedContent; +import org.argeo.api.gcr.spi.ProvidedSession; import org.argeo.util.FsUtils; -public class FsContent extends AbstractContent implements Content { +public class FsContent extends AbstractContent implements ProvidedContent { private final static String USER_ = "user:"; - private static final Set BASIC_KEYS = new HashSet<>( - Arrays.asList("basic:creationTime", "basic:lastModifiedTime", "basic:size", "basic:fileKey")); - private static final Set POSIX_KEYS; + private static final Map BASIC_KEYS; + private static final Map POSIX_KEYS; static { - POSIX_KEYS = new HashSet<>(BASIC_KEYS); - POSIX_KEYS.add("owner:owner"); - POSIX_KEYS.add("posix:group"); - POSIX_KEYS.add("posix:permissions"); + BASIC_KEYS = new HashMap<>(); + BASIC_KEYS.put(CrName.CREATION_TIME.get(), "basic:creationTime"); + BASIC_KEYS.put(CrName.LAST_MODIFIED_TIME.get(), "basic:lastModifiedTime"); + BASIC_KEYS.put(CrName.SIZE.get(), "basic:size"); + BASIC_KEYS.put(CrName.FILE_KEY.get(), "basic:fileKey"); + + POSIX_KEYS = new HashMap<>(BASIC_KEYS); + POSIX_KEYS.put(CrName.OWNER.get(), "owner:owner"); + POSIX_KEYS.put(CrName.GROUP.get(), "posix:group"); + POSIX_KEYS.put(CrName.PERMISSIONS.get(), "posix:permissions"); } - private final FsContentProvider contentProvider; + private final ProvidedSession session; + private final FsContentProvider provider; private final Path path; private final boolean isRoot; + private final QName name; - public FsContent(FsContentProvider contentProvider, Path path) { - super(); - this.contentProvider = contentProvider; + protected FsContent(ProvidedSession session, FsContentProvider contentProvider, Path path) { + this.session = session; + this.provider = contentProvider; this.path = path; this.isRoot = contentProvider.isRoot(path); + // TODO check file names with ':' ? + if (isRoot) + this.name = CrName.ROOT.get(); + else + this.name = session.parsePrefixedName(path.getFileName().toString()); + } + + protected FsContent(FsContent context, Path path) { + this(context.getSession(), context.getProvider(), path); } private boolean isPosix() { @@ -50,10 +71,8 @@ public class FsContent extends AbstractContent implements Content { } @Override - public String getName() { - if (isRoot) - return ""; - return path.getFileName().toString(); + public QName getName() { + return name; } /* @@ -61,7 +80,7 @@ public class FsContent extends AbstractContent implements Content { */ @Override - public A get(String key, Class clss) { + public A get(QName key, Class clss) { Object value; try { // We need to add user: when accessing via Files#getAttribute @@ -85,13 +104,13 @@ public class FsContent extends AbstractContent implements Content { } @Override - protected Iterable keys() { - Set result = new HashSet<>(isPosix() ? POSIX_KEYS : BASIC_KEYS); + protected Iterable keys() { + Set result = new HashSet<>(isPosix() ? POSIX_KEYS.keySet() : BASIC_KEYS.keySet()); UserDefinedFileAttributeView udfav = Files.getFileAttributeView(path, UserDefinedFileAttributeView.class); if (udfav != null) { try { for (String name : udfav.list()) { - result.add(name); + result.add(session.parsePrefixedName(name)); } } catch (IOException e) { throw new ContentResourceException("Cannot list attributes for " + path, e); @@ -101,33 +120,33 @@ public class FsContent extends AbstractContent implements Content { } @Override - protected void removeAttr(String key) { + protected void removeAttr(QName key) { UserDefinedFileAttributeView udfav = Files.getFileAttributeView(path, UserDefinedFileAttributeView.class); try { - udfav.delete(key); + udfav.delete(session.toPrefixedName(key)); } catch (IOException e) { throw new ContentResourceException("Cannot delete attribute " + key + " for " + path, e); } } @Override - public Object put(String key, Object value) { + public Object put(QName key, Object value) { Object previous = get(key); UserDefinedFileAttributeView udfav = Files.getFileAttributeView(path, UserDefinedFileAttributeView.class); ByteBuffer bb = ByteBuffer.wrap(value.toString().getBytes(StandardCharsets.UTF_8)); try { - int size = udfav.write(key, bb); + int size = udfav.write(session.toPrefixedName(key), bb); } catch (IOException e) { throw new ContentResourceException("Cannot delete attribute " + key + " for " + path, e); } return previous; } - protected String toFsAttributeKey(String key) { - if (POSIX_KEYS.contains(key)) - return key; + protected String toFsAttributeKey(QName key) { + if (POSIX_KEYS.containsKey(key)) + return POSIX_KEYS.get(key); else - return USER_ + key; + return USER_ + session.toPrefixedName(key); } /* @@ -137,7 +156,7 @@ public class FsContent extends AbstractContent implements Content { public Iterator iterator() { if (Files.isDirectory(path)) { try { - return Files.list(path).map((p) -> (Content) new FsContent(contentProvider, p)).iterator(); + return Files.list(path).map((p) -> (Content) new FsContent(this, p)).iterator(); } catch (IOException e) { throw new ContentResourceException("Cannot list " + path, e); } @@ -147,10 +166,10 @@ public class FsContent extends AbstractContent implements Content { } @Override - public Content add(String name, ContentName... classes) { + public Content add(QName name, QName... classes) { try { - Path newPath = path.resolve(name); - if (ContentName.contains(classes, CrName.COLLECTION)) + Path newPath = path.resolve(session.toPrefixedName(name)); + if (ContentName.contains(classes, CrName.COLLECTION.get())) Files.createDirectory(newPath); else Files.createFile(newPath); @@ -158,7 +177,7 @@ public class FsContent extends AbstractContent implements Content { // for(ContentClass clss:classes) { // Files.setAttribute(newPath, name, newPath, null) // } - return new FsContent(contentProvider, newPath); + return new FsContent(this, newPath); } catch (IOException e) { throw new ContentResourceException("Cannot create new content", e); } @@ -173,7 +192,20 @@ public class FsContent extends AbstractContent implements Content { public Content getParent() { if (isRoot) return null;// TODO deal with mounts - return new FsContent(contentProvider, path.getParent()); + return new FsContent(this, path.getParent()); + } + + /* + * ACCESSORS + */ + @Override + public ProvidedSession getSession() { + return session; + } + + @Override + public FsContentProvider getProvider() { + return provider; } } diff --git a/org.argeo.cms/src/org/argeo/cms/gcr/fs/FsContentProvider.java b/org.argeo.cms/src/org/argeo/cms/gcr/fs/FsContentProvider.java index d15f9c32b..e5732da1e 100644 --- a/org.argeo.cms/src/org/argeo/cms/gcr/fs/FsContentProvider.java +++ b/org.argeo.cms/src/org/argeo/cms/gcr/fs/FsContentProvider.java @@ -7,6 +7,7 @@ import java.nio.file.Path; import org.argeo.api.gcr.Content; import org.argeo.api.gcr.ContentResourceException; import org.argeo.api.gcr.spi.ContentProvider; +import org.argeo.api.gcr.spi.ProvidedSession; public class FsContentProvider implements ContentProvider { private final Path rootPath; @@ -25,12 +26,7 @@ public class FsContentProvider implements ContentProvider { } @Override - public Content get() { - return new FsContent(this, rootPath); - } - - @Override - public Content get(String relativePath) { - return new FsContent(this, rootPath.resolve(relativePath)); + public Content get(ProvidedSession session, String mountPath, String relativePath) { + return new FsContent(session, this, rootPath.resolve(relativePath)); } } diff --git a/org.argeo.cms/src/org/argeo/cms/gcr/xml/DomContent.java b/org.argeo.cms/src/org/argeo/cms/gcr/xml/DomContent.java index 30d590df4..57e6ba7ce 100644 --- a/org.argeo.cms/src/org/argeo/cms/gcr/xml/DomContent.java +++ b/org.argeo.cms/src/org/argeo/cms/gcr/xml/DomContent.java @@ -4,57 +4,105 @@ import java.util.HashSet; import java.util.Iterator; import java.util.Set; +import javax.xml.XMLConstants; +import javax.xml.namespace.NamespaceContext; +import javax.xml.namespace.QName; + import org.argeo.api.gcr.Content; import org.argeo.api.gcr.ContentName; import org.argeo.api.gcr.spi.AbstractContent; +import org.argeo.api.gcr.spi.ProvidedContent; +import org.argeo.api.gcr.spi.ProvidedSession; import org.w3c.dom.Attr; +import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.Text; -public class DomContent extends AbstractContent implements Content { +public class DomContent extends AbstractContent implements ProvidedContent { - private final DomContentProvider contentProvider; + private final ProvidedSession session; + private final DomContentProvider provider; private final Element element; // private String text = null; private Boolean hasText = null; - public DomContent(DomContentProvider contentProvider, Element element) { - this.contentProvider = contentProvider; + public DomContent(ProvidedSession session, DomContentProvider contentProvider, Element element) { + this.session = session; + this.provider = contentProvider; this.element = element; } - @Override - public Iterator iterator() { - NodeList nodeList = element.getChildNodes(); - return new ElementIterator(contentProvider, nodeList); + public DomContent(DomContent context, Element element) { + this(context.getSession(), context.getProvider(), element); } @Override - public String getName() { - return element.getNodeName(); + public QName getName() { + return toQName(this.element); + } + + protected QName toQName(Node node) { + String prefix = node.getPrefix(); + if (prefix == null) { + String namespaceURI = node.getNamespaceURI(); + if (namespaceURI == null) + namespaceURI = node.getOwnerDocument().lookupNamespaceURI(null); + if (namespaceURI == null) { + return toQName(node, node.getLocalName()); + } else { + String contextPrefix = session.getPrefix(namespaceURI); + if (contextPrefix == null) + throw new IllegalStateException("Namespace " + namespaceURI + " is unbound"); + return toQName(node, namespaceURI, node.getLocalName(), session); + } + } else { + String namespaceURI = node.getNamespaceURI(); + if (namespaceURI == null) + namespaceURI = node.getOwnerDocument().lookupNamespaceURI(prefix); + if (namespaceURI == null) { + namespaceURI = session.getNamespaceURI(prefix); + if (XMLConstants.NULL_NS_URI.equals(namespaceURI)) + throw new IllegalStateException("Prefix " + prefix + " is unbound"); + // TODO bind the prefix in the document? + } + return toQName(node, namespaceURI, node.getLocalName(), session); + } + } + + protected QName toQName(Node source, String namespaceURI, String localName, NamespaceContext namespaceContext) { + return new ContentName(namespaceURI, localName, session); + } + + protected QName toQName(Node source, String localName) { + return new ContentName(localName); } + /* + * ATTRIBUTES OPERATIONS + */ @Override - public Iterable keys() { + public Iterable keys() { // TODO implement an iterator? - Set result = new HashSet<>(); + Set result = new HashSet<>(); NamedNodeMap attributes = element.getAttributes(); for (int i = 0; i < attributes.getLength(); i++) { Attr attr = (Attr) attributes.item(i); - String attrName = attr.getNodeName(); - result.add(attrName); + QName key = toQName(attr); + result.add(key); } return result; } @Override - public A get(String key, Class clss) { - if (element.hasAttribute(key)) { - String value = element.getAttribute(key); + public A get(QName key, Class clss) { + String namespaceUriOrNull = XMLConstants.NULL_NS_URI.equals(key.getNamespaceURI()) ? null + : key.getNamespaceURI(); + if (element.hasAttributeNS(namespaceUriOrNull, key.getLocalPart())) { + String value = element.getAttributeNS(namespaceUriOrNull, key.getLocalPart()); if (clss.isAssignableFrom(String.class)) return (A) value; else @@ -63,6 +111,17 @@ public class DomContent extends AbstractContent implements Content { return null; } + @Override + public Object put(QName key, Object value) { + Object previous = get(key); + String namespaceUriOrNull = XMLConstants.NULL_NS_URI.equals(key.getNamespaceURI()) ? null + : key.getNamespaceURI(); + element.setAttributeNS(namespaceUriOrNull, + namespaceUriOrNull == null ? key.getLocalPart() : key.getPrefix() + ":" + key.getLocalPart(), + value.toString()); + return previous; + } + @Override public boolean hasText() { // return element instanceof Text; @@ -100,6 +159,16 @@ public class DomContent extends AbstractContent implements Content { return null; } + /* + * CONTENT OPERATIONS + */ + + @Override + public Iterator iterator() { + NodeList nodeList = element.getChildNodes(); + return new ElementIterator(session, provider, nodeList); + } + @Override public Content getParent() { Node parent = element.getParentNode(); @@ -107,14 +176,19 @@ public class DomContent extends AbstractContent implements Content { return null; if (!(parent instanceof Element)) throw new IllegalStateException("Parent is not an element"); - return new DomContent(contentProvider, (Element) parent); + return new DomContent(this, (Element) parent); } @Override - public Content add(String name, ContentName... classes) { + public Content add(QName name, QName... classes) { // TODO consider classes - Element child = contentProvider.createElement(name); - return new DomContent(contentProvider, child); + Document document = this.element.getOwnerDocument(); + String namespaceUriOrNull = XMLConstants.NULL_NS_URI.equals(name.getNamespaceURI()) ? null + : name.getNamespaceURI(); + Element child = document.createElementNS(namespaceUriOrNull, + namespaceUriOrNull == null ? name.getLocalPart() : name.getPrefix() + ":" + name.getLocalPart()); + element.appendChild(child); + return new DomContent(this, child); } @Override @@ -125,9 +199,20 @@ public class DomContent extends AbstractContent implements Content { } @Override - protected void removeAttr(String key) { - element.removeAttribute(key); + protected void removeAttr(QName key) { + String namespaceUriOrNull = XMLConstants.NULL_NS_URI.equals(key.getNamespaceURI()) ? null + : key.getNamespaceURI(); + element.removeAttributeNS(namespaceUriOrNull, + namespaceUriOrNull == null ? key.getLocalPart() : key.getPrefix() + ":" + key.getLocalPart()); + + } + + public ProvidedSession getSession() { + return session; + } + public DomContentProvider getProvider() { + return provider; } } diff --git a/org.argeo.cms/src/org/argeo/cms/gcr/xml/DomContentProvider.java b/org.argeo.cms/src/org/argeo/cms/gcr/xml/DomContentProvider.java index d1a625e32..dc200bf3d 100644 --- a/org.argeo.cms/src/org/argeo/cms/gcr/xml/DomContentProvider.java +++ b/org.argeo.cms/src/org/argeo/cms/gcr/xml/DomContentProvider.java @@ -1,61 +1,99 @@ package org.argeo.cms.gcr.xml; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Set; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.namespace.NamespaceContext; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; import org.argeo.api.gcr.Content; -import org.argeo.api.gcr.ContentUtils; +import org.argeo.api.gcr.ContentNotFoundException; import org.argeo.api.gcr.spi.ContentProvider; +import org.argeo.api.gcr.spi.ProvidedSession; import org.w3c.dom.Document; import org.w3c.dom.Element; +import org.w3c.dom.NodeList; -public class DomContentProvider implements ContentProvider { +public class DomContentProvider implements ContentProvider, NamespaceContext { private Document document; + // XPath + // TODO centralise in some executor? + private final ThreadLocal xPath; + public DomContentProvider(Document document) { this.document = document; this.document.normalizeDocument(); + XPathFactory xPathFactory = XPathFactory.newInstance(); + xPath = new ThreadLocal<>() { + + @Override + protected XPath initialValue() { + // TODO set the document as namespace context? + XPath res= xPathFactory.newXPath(); + res.setNamespaceContext(DomContentProvider.this); + return res; + } + }; } +// @Override +// public Content get() { +// return new DomContent(this, document.getDocumentElement()); +// } + +// public Element createElement(String name) { +// return document.createElementNS(null, name); +// +// } + @Override - public Content get() { - return new DomContent(this, document.getDocumentElement()); - } + public Content get(ProvidedSession session, String mountPath, String relativePath) { + if ("".equals(relativePath)) + return new DomContent(session, this, document.getDocumentElement()); + if (relativePath.startsWith("/")) + throw new IllegalArgumentException("Relative path cannot start with /"); - public Element createElement(String name) { - return document.createElement(name); + String xPathExpression = '/' + relativePath; + if ("/".equals(mountPath)) + xPathExpression = "/cr:root" + xPathExpression; + try { + NodeList nodes = (NodeList) xPath.get().evaluate(xPathExpression, document, XPathConstants.NODESET); + if (nodes.getLength() > 1) + throw new IllegalArgumentException( + "Multiple content found for " + relativePath + " under " + mountPath); + if (nodes.getLength() == 0) + throw new ContentNotFoundException("Path " + relativePath + " under " + mountPath + " was not found"); + Element element = (Element) nodes.item(0); + return new DomContent(session, this, element); + } catch (XPathExpressionException e) { + throw new IllegalArgumentException("XPath expression " + xPathExpression + " cannot be evaluated", e); + } } + /* + * NAMESPACE CONTEXT + */ @Override - public Content get(String relativePath) { - // TODO Auto-generated method stub - return null; + public String getNamespaceURI(String prefix) { + return document.lookupNamespaceURI(prefix); } - public static void main(String args[]) throws Exception { - HashMap map = new HashMap<>(); - map.put(null, "test"); - System.out.println(map.get(null)); - - Set set = new HashSet<>(); - set.add(null); - - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - DocumentBuilder dBuilder = factory.newDocumentBuilder(); - Path testFile; - testFile = Paths.get(System.getProperty("user.home") + "/dev/git/unstable/argeo-commons/pom.xml"); - testFile = Paths.get(System.getProperty("user.home") + "/tmp/test.xml"); - Document doc = dBuilder.parse(Files.newInputStream(testFile)); - - DomContentProvider contentSession = new DomContentProvider(doc); - ContentUtils.traverse(contentSession.get(), (c, d) -> ContentUtils.print(c, System.out, d, true)); + @Override + public String getPrefix(String namespaceURI) { + return document.lookupPrefix(namespaceURI); + } + @Override + public Iterator getPrefixes(String namespaceURI) { + List res = new ArrayList<>(); + res.add(getPrefix(namespaceURI)); + return Collections.unmodifiableList(res).iterator(); } + } diff --git a/org.argeo.cms/src/org/argeo/cms/gcr/xml/ElementIterator.java b/org.argeo.cms/src/org/argeo/cms/gcr/xml/ElementIterator.java index e3efcb4b3..db25b653d 100644 --- a/org.argeo.cms/src/org/argeo/cms/gcr/xml/ElementIterator.java +++ b/org.argeo.cms/src/org/argeo/cms/gcr/xml/ElementIterator.java @@ -4,20 +4,23 @@ import java.util.Iterator; import java.util.NoSuchElementException; import org.argeo.api.gcr.Content; +import org.argeo.api.gcr.spi.ProvidedSession; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; -public class ElementIterator implements Iterator { - private final DomContentProvider contentSession; +class ElementIterator implements Iterator { + private final ProvidedSession session; + private final DomContentProvider provider; private final NodeList nodeList; private int currentIndex; private final int length; private Element nextElement = null; - public ElementIterator(DomContentProvider contentSession, NodeList nodeList) { - this.contentSession = contentSession; + public ElementIterator(ProvidedSession session, DomContentProvider provider, NodeList nodeList) { + this.session = session; + this.provider = provider; this.nodeList = nodeList; this.length = nodeList.getLength(); @@ -45,7 +48,7 @@ public class ElementIterator implements Iterator { public Content next() { if (nextElement == null) throw new NoSuchElementException(); - DomContent result = new DomContent(contentSession, nextElement); + DomContent result = new DomContent(session, provider, nextElement); currentIndex++; nextElement = findNext(); return result; -- 2.30.2