From: Mathieu Baudier Date: Thu, 29 Jun 2023 03:35:07 +0000 (+0200) Subject: Merge tag 'v2.3.18' into testing X-Git-Tag: v2.1.113~1 X-Git-Url: http://git.argeo.org/?a=commitdiff_plain;h=5724ab347ddfba8f2b21cdcc2fa0b8e1e2b4e527;hp=c4e86a450338692b80e99e1fb271e948eaed6b82;p=lgpl%2Fargeo-commons.git Merge tag 'v2.3.18' into testing --- diff --git a/Makefile b/Makefile index 8d5395724..ff2c9ccad 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ DEP_CATEGORIES = \ crypto/fips/org.argeo.tp.crypto \ org.argeo.tp \ org.argeo.tp.httpd \ -osgi/api/org.argeo.tp.osgi \ +osgi/equinox/org.argeo.tp.osgi \ osgi/equinox/org.argeo.tp.eclipse \ swt/rap/org.argeo.tp.swt \ $(A2_CATEGORY) \ diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/AttributeFormatter.java b/org.argeo.api.acr/src/org/argeo/api/acr/AttributeFormatter.java index c7023f1a3..9f338965f 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/AttributeFormatter.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/AttributeFormatter.java @@ -1,5 +1,7 @@ package org.argeo.api.acr; +import javax.xml.namespace.NamespaceContext; + /** * An attribute type MUST consistently parse a string to an object so that * parse(obj.toString()).equals(obj) is verified. @@ -9,7 +11,15 @@ package org.argeo.api.acr; */ public interface AttributeFormatter { /** Parses a String to a Java object. */ - T parse(String str) throws IllegalArgumentException; + default T parse(String str) throws IllegalArgumentException { + return parse(RuntimeNamespaceContext.getNamespaceContext(), str); + } + + /** + * Parses a String to a Java object, possibly using the namespace context to + * resolve QName or CURIE. + */ + T parse(NamespaceContext namespaceContext, String str) throws IllegalArgumentException; /** Default implementation returns {@link Object#toString()} on the argument. */ default String format(T obj) { diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/Content.java b/org.argeo.api.acr/src/org/argeo/api/acr/Content.java index f52ab31b8..df5c149e6 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/Content.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/Content.java @@ -16,6 +16,8 @@ import javax.xml.namespace.QName; * A semi-structured content, with attributes, within a hierarchical structure. */ public interface Content extends Iterable, Map { + /** The base of a repository path. */ + String ROOT_PATH = "/"; QName getName(); @@ -82,25 +84,11 @@ public interface Content extends Iterable, Map { } List res = getMultiple(key, type); return res; -// if (res == null) -// return null; -// else { -// if (res.isEmpty()) -// throw new IllegalStateException("Metadata " + key + " is not availabel as list of type " + type); -// return res.get(); -// } } /* * CONTENT OPERATIONS */ -// default CompletionStage edit(Consumer work) { -// return CompletableFuture.supplyAsync(() -> { -// work.accept(this); -// return this; -// }).minimalCompletionStage(); -// } - Content add(QName name, QName... classes); default Content add(String name, QName... classes) { @@ -222,6 +210,14 @@ public interface Content extends Iterable, Map { return res; } + default List children(QNamed name) { + return children(name.qName()); + } + + default Optional soleChild(QNamed name) { + return soleChild(name.qName()); + } + default Optional soleChild(QName name) { List res = children(name); if (res.isEmpty()) @@ -242,29 +238,51 @@ public interface Content extends Iterable, Map { /* * ATTR AS STRING */ + /** + * Convenience method returning an attribute as a {@link String}. + * + * @param key the attribute name + * @return the attribute value as a {@link String} or null. + * + * @see Object#toString() + */ default String attr(QName key) { - // TODO check String type? - Object obj = get(key); - if (obj == null) - return null; - return obj.toString(); + return get(key, String.class).orElse(null); } + /** + * Convenience method returning an attribute as a {@link String}. + * + * @param key the attribute name + * @return the attribute value as a {@link String} or null. + * + * @see Object#toString() + */ default String attr(QNamed key) { return attr(key.qName()); } + /** + * Convenience method returning an attribute as a {@link String}. + * + * @param key the attribute name + * @return the attribute value as a {@link String} or null. + * + * @see Object#toString() + */ default String attr(String key) { return attr(unqualified(key)); } -// -// 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); -// } + + /* + * CONTEXT + */ + /** + * A content within this repository + * + * @param path either an absolute path or a path relative to this content + */ + Optional getContent(String path); /* * EXPERIMENTAL UNSUPPORTED diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/ContentName.java b/org.argeo.api.acr/src/org/argeo/api/acr/ContentName.java index 113f98da0..92cd795ce 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/ContentName.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/ContentName.java @@ -83,7 +83,7 @@ public class ContentName extends QName { */ @Override - public String toString() { + public final String toString() { return toQNameString(); } diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/ContentSession.java b/org.argeo.api.acr/src/org/argeo/api/acr/ContentSession.java index b8ecd98da..7a6e67981 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/ContentSession.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/ContentSession.java @@ -3,7 +3,6 @@ package org.argeo.api.acr; import java.util.Locale; import java.util.concurrent.CompletionStage; import java.util.function.Consumer; -import java.util.function.Supplier; import java.util.stream.Stream; import javax.security.auth.Subject; diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/CrAttributeType.java b/org.argeo.api.acr/src/org/argeo/api/acr/CrAttributeType.java index 3e12fb1c8..3e0dddee4 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/CrAttributeType.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/CrAttributeType.java @@ -9,10 +9,10 @@ import java.time.format.DateTimeParseException; import java.util.Arrays; import java.util.Base64; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.UUID; +import javax.xml.namespace.NamespaceContext; import javax.xml.namespace.QName; /** @@ -29,6 +29,7 @@ public enum CrAttributeType { // (e.g. optional primitives) DATE_TIME(Instant.class, W3C_XML_SCHEMA_NS_URI, "dateTime", new InstantFormatter()), // UUID(UUID.class, ArgeoNamespace.CR_NAMESPACE_URI, "uuid", new UuidFormatter()), // + QNAME(QName.class, W3C_XML_SCHEMA_NS_URI, "QName", new QNameFormatter()), // ANY_URI(URI.class, W3C_XML_SCHEMA_NS_URI, "anyUri", new UriFormatter()), // STRING(String.class, W3C_XML_SCHEMA_NS_URI, "string", new StringFormatter()), // ; @@ -45,7 +46,7 @@ public enum CrAttributeType { qName = new ContentName(namespaceUri, localName, RuntimeNamespaceContext.getNamespaceContext()); } - public QName getqName() { + public QName getQName() { return qName; } @@ -75,43 +76,71 @@ public enum CrAttributeType { /** Default parsing procedure from a String to an object. */ public static Object parse(String str) { + return parse(RuntimeNamespaceContext.getNamespaceContext(), str); + } + + /** Default parsing procedure from a String to an object. */ + public static Object parse(NamespaceContext namespaceContext, String str) { if (str == null) throw new IllegalArgumentException("String cannot be null"); // order IS important try { if (str.length() == 4 || str.length() == 5) - return BOOLEAN.getFormatter().parse(str); + return BOOLEAN.getFormatter().parse(namespaceContext, str); } catch (IllegalArgumentException e) { // silent } try { - return INTEGER.getFormatter().parse(str); + return INTEGER.getFormatter().parse(namespaceContext, str); } catch (IllegalArgumentException e) { // silent } try { - return LONG.getFormatter().parse(str); + return LONG.getFormatter().parse(namespaceContext, str); } catch (IllegalArgumentException e) { // silent } try { - return DOUBLE.getFormatter().parse(str); + return DOUBLE.getFormatter().parse(namespaceContext, str); } catch (IllegalArgumentException e) { // silent } try { - return DATE_TIME.getFormatter().parse(str); + return DATE_TIME.getFormatter().parse(namespaceContext, str); } catch (IllegalArgumentException e) { // silent } try { if (str.length() == 36) - return UUID.getFormatter().parse(str); + return UUID.getFormatter().parse(namespaceContext, str); + } catch (IllegalArgumentException e) { + // silent + } + + // CURIE + if (str.startsWith("[") && str.endsWith("]")) { + try { + if (str.indexOf(":") >= 0) { + QName qName = (QName) QNAME.getFormatter().parse(namespaceContext, str); + return (java.net.URI) ANY_URI.getFormatter().parse(qName.getNamespaceURI() + qName.getLocalPart()); + } + } catch (IllegalArgumentException e) { + // silent + } + } + + try { + if (str.indexOf(":") >= 0) { + QName qName = (QName) QNAME.getFormatter().parse(namespaceContext, str); + // note: this QName may not be valid + // note: CURIE should be explicitly defined with surrounding brackets + return qName; + } } catch (IllegalArgumentException e) { // silent } try { - java.net.URI uri = (java.net.URI) ANY_URI.getFormatter().parse(str); + java.net.URI uri = (java.net.URI) ANY_URI.getFormatter().parse(namespaceContext, str); if (uri.getScheme() != null) return uri; String path = uri.getPath(); @@ -129,7 +158,7 @@ public enum CrAttributeType { // see https://www.oreilly.com/library/view/xml-schema/0596002521/re91.html // default - return STRING.getFormatter().parse(str); + return STRING.getFormatter().parse(namespaceContext, str); } /** @@ -137,34 +166,72 @@ public enum CrAttributeType { * object. * */ - @SuppressWarnings("unchecked") public static Optional cast(Class clss, Object value) { - // TODO Or should we? - Objects.requireNonNull(value, "Cannot cast a null value"); + return cast(RuntimeNamespaceContext.getNamespaceContext(), clss, value); + } + + /** + * Cast well know java types based on {@link Object#toString()} of the provided + * object. + * + */ + @SuppressWarnings("unchecked") + public static Optional cast(NamespaceContext namespaceContext, Class clss, Object value) { + // if value is null, optional is empty + if (value == null) + return Optional.empty(); + + // if a default has been explicitly requested by passing Object.class + // we parse the related String + if (clss.isAssignableFrom(Object.class)) { + return Optional.of((T) parse(value.toString())); + } + + // if value can be cast directly, let's do it + if (value.getClass().isAssignableFrom(clss)) { + return Optional.of(((T) value)); + } + + // let's cast between various numbers (possibly losing precision) + if (value instanceof Number number) { + if (Long.class.isAssignableFrom(clss)) + return Optional.of((T) (Long) number.longValue()); + else if (Integer.class.isAssignableFrom(clss)) + return Optional.of((T) (Integer) number.intValue()); + else if (Double.class.isAssignableFrom(clss)) + return Optional.of((T) (Double) number.doubleValue()); + } + + // let's now try with the string representation + String strValue = value instanceof String ? (String) value : value.toString(); + if (String.class.isAssignableFrom(clss)) { - return Optional.of((T) value.toString()); + return Optional.of((T) strValue); + } + if (QName.class.isAssignableFrom(clss)) { + return Optional.of((T) NamespaceUtils.parsePrefixedName(namespaceContext, strValue)); } // Numbers else if (Long.class.isAssignableFrom(clss)) { if (value instanceof Long) return Optional.of((T) value); - return Optional.of((T) Long.valueOf(value.toString())); + return Optional.of((T) Long.valueOf(strValue)); } else if (Integer.class.isAssignableFrom(clss)) { if (value instanceof Integer) return Optional.of((T) value); - return Optional.of((T) Integer.valueOf(value.toString())); + return Optional.of((T) Integer.valueOf(strValue)); } else if (Double.class.isAssignableFrom(clss)) { if (value instanceof Double) return Optional.of((T) value); - return Optional.of((T) Double.valueOf(value.toString())); + return Optional.of((T) Double.valueOf(strValue)); } - // Numbers -// else if (Number.class.isAssignableFrom(clss)) { -// if (value instanceof Number) -// return Optional.of((T) value); -// return Optional.of((T) Number.valueOf(value.toString())); -// } - return Optional.empty(); + + // let's now try to parse the string representation to a well-known type + Object parsedValue = parse(strValue); + if (parsedValue.getClass().isAssignableFrom(clss)) { + return Optional.of(((T) value)); + } + throw new ClassCastException("Cannot convert " + value.getClass() + " to " + clss); } /** Utility to convert a data: URI to bytes. */ @@ -202,7 +269,7 @@ public enum CrAttributeType { * contract than {@link Boolean#parseBoolean(String)}. */ @Override - public Boolean parse(String str) throws IllegalArgumentException { + public Boolean parse(NamespaceContext namespaceContext, String str) throws IllegalArgumentException { if ("true".equals(str)) return Boolean.TRUE; if ("false".equals(str)) @@ -213,14 +280,14 @@ public enum CrAttributeType { static class IntegerFormatter implements AttributeFormatter { @Override - public Integer parse(String str) throws NumberFormatException { + public Integer parse(NamespaceContext namespaceContext, String str) throws NumberFormatException { return Integer.parseInt(str); } } static class LongFormatter implements AttributeFormatter { @Override - public Long parse(String str) throws NumberFormatException { + public Long parse(NamespaceContext namespaceContext, String str) throws NumberFormatException { return Long.parseLong(str); } } @@ -228,7 +295,7 @@ public enum CrAttributeType { static class DoubleFormatter implements AttributeFormatter { @Override - public Double parse(String str) throws NumberFormatException { + public Double parse(NamespaceContext namespaceContext, String str) throws NumberFormatException { return Double.parseDouble(str); } } @@ -236,7 +303,7 @@ public enum CrAttributeType { static class InstantFormatter implements AttributeFormatter { @Override - public Instant parse(String str) throws IllegalArgumentException { + public Instant parse(NamespaceContext namespaceContext, String str) throws IllegalArgumentException { try { return Instant.parse(str); } catch (DateTimeParseException e) { @@ -248,7 +315,7 @@ public enum CrAttributeType { static class UuidFormatter implements AttributeFormatter { @Override - public UUID parse(String str) throws IllegalArgumentException { + public UUID parse(NamespaceContext namespaceContext, String str) throws IllegalArgumentException { return java.util.UUID.fromString(str); } } @@ -256,7 +323,7 @@ public enum CrAttributeType { static class UriFormatter implements AttributeFormatter { @Override - public URI parse(String str) throws IllegalArgumentException { + public URI parse(NamespaceContext namespaceContext, String str) throws IllegalArgumentException { try { return new URI(str); } catch (URISyntaxException e) { @@ -266,10 +333,19 @@ public enum CrAttributeType { } + static class QNameFormatter implements AttributeFormatter { + + @Override + public QName parse(NamespaceContext namespaceContext, String str) throws IllegalArgumentException { + return NamespaceUtils.parsePrefixedName(namespaceContext, str); + } + + } + static class StringFormatter implements AttributeFormatter { @Override - public String parse(String str) { + public String parse(NamespaceContext namespaceContext, String str) { return str; } diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/NamespaceUtils.java b/org.argeo.api.acr/src/org/argeo/api/acr/NamespaceUtils.java index df582868b..e845560b0 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/NamespaceUtils.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/NamespaceUtils.java @@ -11,12 +11,41 @@ import javax.xml.XMLConstants; import javax.xml.namespace.NamespaceContext; import javax.xml.namespace.QName; +/** Static utilities around namespaces and prefixes. */ public class NamespaceUtils { + /** + * A {@link Comparator} ordering by namespace (full URI) and then local part. + */ + public final static Comparator QNAME_COMPARATOR = new Comparator() { + + @Override + public int compare(QName qn1, QName qn2) { + if (Objects.equals(qn1.getNamespaceURI(), qn2.getNamespaceURI())) {// same namespace + return qn1.getLocalPart().compareTo(qn2.getLocalPart()); + } else { + return qn1.getNamespaceURI().compareTo(qn2.getNamespaceURI()); + } + } + }; + + /** + * Return a {@link ContentName} from a prefixed name, using the default runtime + * {@link NamespaceContext}. + * + * @see RuntimeNamespaceContext#getNamespaceContext() + */ public static ContentName parsePrefixedName(String nameWithPrefix) { return parsePrefixedName(RuntimeNamespaceContext.getNamespaceContext(), nameWithPrefix); } + /** + * Return a {@link ContentName} from a prefixed name, using the provided + * {@link NamespaceContext}. Since {@link QName#QName(String, String)} does not + * validate, it can conceptually parse a CURIE. + * + * @see https://en.wikipedia.org/wiki/CURIE + */ public static ContentName parsePrefixedName(NamespaceContext nameSpaceContext, String nameWithPrefix) { Objects.requireNonNull(nameWithPrefix, "Name cannot be null"); if (nameWithPrefix.charAt(0) == '{') { @@ -31,14 +60,24 @@ public class NamespaceUtils { String localName = nameWithPrefix.substring(index + 1); String namespaceURI = nameSpaceContext.getNamespaceURI(prefix); if (XMLConstants.NULL_NS_URI.equals(namespaceURI)) - throw new IllegalStateException("Prefix " + prefix + " is unbound."); + throw new IllegalArgumentException("Prefix " + prefix + " is unbound."); return new ContentName(namespaceURI, localName, prefix); } + /** + * The prefixed name of this {@link QName}, using the default runtime + * {@link NamespaceContext}. + * + * @see RuntimeNamespaceContext#getNamespaceContext() + */ public static String toPrefixedName(QName name) { return toPrefixedName(RuntimeNamespaceContext.getNamespaceContext(), name); } + /** + * The prefixed name of this {@link QName}, using the provided + * {@link NamespaceContext}. + */ public static String toPrefixedName(NamespaceContext nameSpaceContext, QName name) { if (XMLConstants.NULL_NS_URI.equals(name.getNamespaceURI())) return name.getLocalPart(); @@ -48,36 +87,56 @@ public class NamespaceUtils { return prefix + ":" + name.getLocalPart(); } - public final static Comparator QNAME_COMPARATOR = new Comparator() { - - @Override - public int compare(QName qn1, QName qn2) { - if (Objects.equals(qn1.getNamespaceURI(), qn2.getNamespaceURI())) {// same namespace - return qn1.getLocalPart().compareTo(qn2.getLocalPart()); - } else { - return qn1.getNamespaceURI().compareTo(qn2.getNamespaceURI()); - } - } - - }; - + /** + * Whether thei {@link QName} has a namespace, that is its namespace is not + * {@link XMLConstants#NULL_NS_URI}. + */ public static boolean hasNamespace(QName qName) { return !qName.getNamespaceURI().equals(XMLConstants.NULL_NS_URI); } - public static void checkNoPrefix(String unqualified) { + /** Throws an exception if the provided string has a prefix. */ + public static void checkNoPrefix(String unqualified) throws IllegalArgumentException { if (unqualified.indexOf(':') >= 0) throw new IllegalArgumentException("Name " + unqualified + " has a prefix"); } + /** + * Create an unqualified {@link QName}, checking that the argument does not + * contain a prefix. + */ public static QName unqualified(String name) { checkNoPrefix(name); return new ContentName(XMLConstants.NULL_NS_URI, name, XMLConstants.DEFAULT_NS_PREFIX); } + /** + * The common (fully qualified) representation of this name, as defined in + * {@link QName#toString()}. This should be used when a fully qualified + * representation is required, as subclasses of {@link QName} may override the + * {@link QName#toString()} method. + * + * @see ContentName#toString() + */ + public static String toFullyQualified(QName name) { + if (name.getNamespaceURI().equals(XMLConstants.NULL_NS_URI)) { + return name.getLocalPart(); + } else { + return "{" + name.getNamespaceURI() + "}" + name.getLocalPart(); + } + + } + /* - * DEFAULT NAMESPACE CONTEXT OPERATIONS as specified in NamespaceContext + * STANDARD NAMESPACE CONTEXT OPERATIONS as specified in NamespaceContext + */ + /** + * The standard prefix for well known namespaces as defined in + * {@link NamespaceContext}, or null if the namespace is not well-known. Helper + * method simplifying the implementation of a {@link NamespaceContext}. + * + * @see NamespaceContext#getPrefix(String) */ public static String getStandardPrefix(String namespaceURI) { if (namespaceURI == null) @@ -89,6 +148,13 @@ public class NamespaceUtils { return null; } + /** + * The standard prefixes for well known namespaces as defined in + * {@link NamespaceContext}, or null if the namespace is not well-known. Helper + * method simplifying the implementation of a {@link NamespaceContext}. + * + * @see NamespaceContext#getPrefixes(String) + */ public static Iterator getStandardPrefixes(String namespaceURI) { String prefix = getStandardPrefix(namespaceURI); if (prefix == null) @@ -96,6 +162,13 @@ public class NamespaceUtils { return Collections.singleton(prefix).iterator(); } + /** + * The standard URI for well known prefixes as defined in + * {@link NamespaceContext}, or null if the prefix is not well-known. Helper + * method simplifying the implementation of a {@link NamespaceContext}. + * + * @see NamespaceContext#getNamespaceURI(String) + */ public static String getStandardNamespaceURI(String prefix) { if (prefix == null) throw new IllegalArgumentException("Prefix cannot be null"); @@ -106,6 +179,13 @@ public class NamespaceUtils { return null; } + /** + * The namespace URI for a given prefix, based on the provided mapping and the + * standard prefixes/URIs. + * + * @return the namespace URI for this prefix, or + * {@link XMLConstants#NULL_NS_URI} if unknown. + */ public static String getNamespaceURI(Function mapping, String prefix) { String namespaceURI = NamespaceUtils.getStandardNamespaceURI(prefix); if (namespaceURI != null) @@ -118,6 +198,13 @@ public class NamespaceUtils { return XMLConstants.NULL_NS_URI; } + /** + * The prefix for a given namespace URI, based on the provided mapping and the + * standard prefixes/URIs. + * + * @return the prefix for this namespace URI. What is returned or throws as + * exception if the prefix is not found depdnds on the mapping function. + */ public static String getPrefix(Function mapping, String namespaceURI) { String prefix = NamespaceUtils.getStandardPrefix(namespaceURI); if (prefix != null) @@ -127,6 +214,13 @@ public class NamespaceUtils { return mapping.apply(namespaceURI); } + /** + * The prefixes for a given namespace URI, based on the provided mapping and the + * standard prefixes/URIs. + * + * @return the prefixes for this namespace URI. What is returned or throws as + * exception if the prefix is not found depdnds on the mapping function. + */ public static Iterator getPrefixes(Function> mapping, String namespaceURI) { Iterator standard = NamespaceUtils.getStandardPrefixes(namespaceURI); if (standard != null) diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/QNamed.java b/org.argeo.api.acr/src/org/argeo/api/acr/QNamed.java index 063a7d321..73ae4f02e 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/QNamed.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/QNamed.java @@ -23,6 +23,7 @@ public interface QNamed extends Supplier { return namespaceContext.getPrefix(getNamespace()) + ":" + localName(); } + /** This qualified named with its default prefix. If it is unqualified this method should be overridden, or QNamed.Unqualified be used. */ default String get() { return getDefaultPrefix() + ":" + localName(); } @@ -43,5 +44,10 @@ public interface QNamed extends Supplier { return XMLConstants.DEFAULT_NS_PREFIX; } + @Override + default String get() { + return localName(); + } + } } diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/RuntimeNamespaceContext.java b/org.argeo.api.acr/src/org/argeo/api/acr/RuntimeNamespaceContext.java index 1c55156ee..fe50fd0c3 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/RuntimeNamespaceContext.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/RuntimeNamespaceContext.java @@ -10,8 +10,10 @@ import javax.xml.XMLConstants; import javax.xml.namespace.NamespaceContext; /** - * Programmatically defined {@link NamespaceContext}, code contributing - * namespaces MUST register here with a single default prefix. + * Programmatically defined {@link NamespaceContext}, which is valid at runtime + * (when the software is running). Code contributing namespaces MUST register + * here with a single default prefix, nad MUST make sure that stored data + * contains the fully qualified namespace URI. */ public class RuntimeNamespaceContext implements NamespaceContext { public final static String XSD_DEFAULT_PREFIX = "xs"; @@ -20,35 +22,38 @@ public class RuntimeNamespaceContext implements NamespaceContext { private NavigableMap prefixes = new TreeMap<>(); private NavigableMap namespaces = new TreeMap<>(); + /* + * NAMESPACE CONTEXT IMPLEMENTATION + */ + @Override - public String getPrefix(String namespaceURI) { + public String getPrefix(String namespaceURI) throws IllegalArgumentException { return NamespaceUtils.getPrefix((ns) -> { String prefix = namespaces.get(ns); if (prefix == null) - throw new IllegalStateException("Namespace " + ns + " is not registered."); + throw new IllegalArgumentException("Namespace " + ns + " is not registered."); return prefix; }, namespaceURI); } @Override - public String getNamespaceURI(String prefix) { + public String getNamespaceURI(String prefix) throws IllegalArgumentException { return NamespaceUtils.getNamespaceURI((p) -> { String ns = prefixes.get(p); if (ns == null) - throw new IllegalStateException("Prefix " + p + " is not registered."); + throw new IllegalArgumentException("Prefix " + p + " is not registered."); return ns; }, prefix); } @Override - public Iterator getPrefixes(String namespaceURI) { + public Iterator getPrefixes(String namespaceURI) throws IllegalArgumentException { return Collections.singleton(getPrefix(namespaceURI)).iterator(); } /* * STATIC */ - private final static RuntimeNamespaceContext INSTANCE = new RuntimeNamespaceContext(); static { @@ -66,30 +71,34 @@ public class RuntimeNamespaceContext implements NamespaceContext { register(ArgeoNamespace.ROLE_NAMESPACE_URI, ArgeoNamespace.ROLE_DEFAULT_PREFIX); } + /** The runtime namespace context instance. */ public static NamespaceContext getNamespaceContext() { return INSTANCE; } + /** The registered prefixes. */ public static Map getPrefixes() { return Collections.unmodifiableNavigableMap(INSTANCE.prefixes); } - public synchronized static void register(String namespaceURI, String prefix) { + /** Registers a namespace URI / default prefix mapping. */ + public synchronized static void register(String namespaceURI, String defaultPrefix) { NavigableMap prefixes = INSTANCE.prefixes; NavigableMap namespaces = INSTANCE.namespaces; - if (prefixes.containsKey(prefix)) { - String ns = prefixes.get(prefix); + if (prefixes.containsKey(defaultPrefix)) { + String ns = prefixes.get(defaultPrefix); if (ns.equals(namespaceURI)) return; // ignore silently - throw new IllegalStateException("Prefix " + prefix + " is already registered with namespace URI " + ns); + throw new IllegalStateException( + "Prefix " + defaultPrefix + " is already registered with namespace URI " + ns); } if (namespaces.containsKey(namespaceURI)) { String p = namespaces.get(namespaceURI); - if (p.equals(prefix)) + if (p.equals(defaultPrefix)) return; // ignore silently throw new IllegalStateException("Namespace " + namespaceURI + " is already registered with prefix " + p); } - prefixes.put(prefix, namespaceURI); - namespaces.put(namespaceURI, prefix); + prefixes.put(defaultPrefix, namespaceURI); + namespaces.put(namespaceURI, defaultPrefix); } } diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/search/AndFilter.java b/org.argeo.api.acr/src/org/argeo/api/acr/search/AndFilter.java index e58b21236..d3880d899 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/search/AndFilter.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/search/AndFilter.java @@ -1,5 +1,6 @@ package org.argeo.api.acr.search; +/** AND filter based on the intersection composition. */ public class AndFilter extends ContentFilter { public AndFilter() { diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/search/BasicSearch.java b/org.argeo.api.acr/src/org/argeo/api/acr/search/BasicSearch.java index 8028f5d20..8cbdebf7e 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/search/BasicSearch.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/search/BasicSearch.java @@ -9,9 +9,13 @@ import java.util.function.Consumer; import javax.xml.namespace.QName; -import org.argeo.api.acr.DName; import org.argeo.api.acr.QNamed; +/** + * A basic search mechanism modelled on WebDav basicsearch. + * + * @see http://www.webdav.org/specs/rfc5323.html + */ public class BasicSearch { private List select = new ArrayList<>(); @@ -30,10 +34,20 @@ public class BasicSearch { return this; } + /** + * Convenience method, to search below this absolute path, with depth + * {@link Depth#INFINITTY}. + */ + public BasicSearch from(String path) { + return from(URI.create(path), Depth.INFINITTY); + } + + /** Search below this URI, with depth {@link Depth#INFINITTY}. */ public BasicSearch from(URI uri) { return from(uri, Depth.INFINITTY); } + /** Search below this URI, with this {@link Depth}. */ public BasicSearch from(URI uri, Depth depth) { Objects.requireNonNull(uri); Objects.requireNonNull(depth); @@ -87,10 +101,10 @@ public class BasicSearch { } - static void main(String[] args) { - BasicSearch search = new BasicSearch(); - search.select(DName.creationdate.qName()) // - .from(URI.create("/test")) // - .where((f) -> f.eq(DName.creationdate.qName(), "")); - } +// static void main(String[] args) { +// BasicSearch search = new BasicSearch(); +// search.select(DName.creationdate.qName()) // +// .from(URI.create("/test")) // +// .where((f) -> f.eq(DName.creationdate.qName(), "")); +// } } diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/search/Composition.java b/org.argeo.api.acr/src/org/argeo/api/acr/search/Composition.java index 80786f462..622d8c268 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/search/Composition.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/search/Composition.java @@ -1,4 +1,5 @@ package org.argeo.api.acr.search; + +/** Marker interface for a composition of multiple constraints. */ interface Composition { } - diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/search/Constraint.java b/org.argeo.api.acr/src/org/argeo/api/acr/search/Constraint.java index fc4313d7a..36c98f7a5 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/search/Constraint.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/search/Constraint.java @@ -1,4 +1,5 @@ package org.argeo.api.acr.search; +/** Marker interface for a constraint. */ public interface Constraint { } diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/search/ContentFilter.java b/org.argeo.api.acr/src/org/argeo/api/acr/search/ContentFilter.java index c5f5fc607..45f2d848c 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/search/ContentFilter.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/search/ContentFilter.java @@ -6,9 +6,9 @@ import java.util.function.Consumer; import javax.xml.namespace.QName; -import org.argeo.api.acr.DName; import org.argeo.api.acr.QNamed; +/** A constraint filtering based ona given composition (and/or). */ public abstract class ContentFilter implements Constraint { private Set constraintss = new HashSet<>(); @@ -167,17 +167,17 @@ public abstract class ContentFilter implements } - public static void main(String[] args) { - AndFilter filter = new AndFilter(); - filter.eq(new QName("test"), "test").and().not().eq(new QName("type"), "integer"); - - OrFilter unionFilter = new OrFilter(); - unionFilter.all((f) -> { - f.eq(DName.displayname, "").and().eq(DName.creationdate, ""); - }).or().not().any((f) -> { - f.eq(DName.creationdate, "").or().eq(DName.displayname, ""); - }); - - } +// public static void main(String[] args) { +// AndFilter filter = new AndFilter(); +// filter.eq(new QName("test"), "test").and().not().eq(new QName("type"), "integer"); +// +// OrFilter unionFilter = new OrFilter(); +// unionFilter.all((f) -> { +// f.eq(DName.displayname, "").and().eq(DName.creationdate, ""); +// }).or().not().any((f) -> { +// f.eq(DName.creationdate, "").or().eq(DName.displayname, ""); +// }); +// +// } } diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/search/Intersection.java b/org.argeo.api.acr/src/org/argeo/api/acr/search/Intersection.java index 5fff2ae88..44ca90605 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/search/Intersection.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/search/Intersection.java @@ -1,5 +1,7 @@ package org.argeo.api.acr.search; -class Intersection implements Composition { + +/** A composition which is the intersection of sets (AND). */ +public class Intersection implements Composition { ContentFilter filter; @SuppressWarnings("unchecked") @@ -12,4 +14,3 @@ class Intersection implements Composition { } } - diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/search/OrFilter.java b/org.argeo.api.acr/src/org/argeo/api/acr/search/OrFilter.java index 40460d499..80a1568be 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/search/OrFilter.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/search/OrFilter.java @@ -1,5 +1,6 @@ package org.argeo.api.acr.search; +/** OR filter based on the union composition. */ public class OrFilter extends ContentFilter { public OrFilter() { diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/search/Union.java b/org.argeo.api.acr/src/org/argeo/api/acr/search/Union.java index d4b342b5f..d33b13da2 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/search/Union.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/search/Union.java @@ -1,6 +1,7 @@ package org.argeo.api.acr.search; -class Union implements Composition { +/** A composition which is the union of sets (OR). */ +public class Union implements Composition { ContentFilter filter; @SuppressWarnings("unchecked") diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/spi/ContentProvider.java b/org.argeo.api.acr/src/org/argeo/api/acr/spi/ContentProvider.java index 25b9be5c2..e354672a7 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/spi/ContentProvider.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/spi/ContentProvider.java @@ -6,16 +6,77 @@ import java.util.Spliterator; import javax.xml.namespace.NamespaceContext; import org.argeo.api.acr.Content; +import org.argeo.api.acr.ContentNotFoundException; +import org.argeo.api.acr.ContentSession; import org.argeo.api.acr.search.BasicSearch; +/** + * A prover of {@link Content}, which can be mounted in a + * {@link ProvidedRepository}. + */ public interface ContentProvider extends NamespaceContext { - ProvidedContent get(ProvidedSession session, String relativePath); + /** + * Return the content at this path, relative to the mount path. + * + * @return the content at this relative path, never null + * @throws ContentNotFoundException if there is no content at this relative path + */ + ProvidedContent get(ProvidedSession session, String relativePath) throws ContentNotFoundException; - boolean exists(ProvidedSession session, String relativePath); + /** + * Whether a content exist at his relative path. The default implementation call + * {@link #get(ProvidedSession, String)} and check whether a + * {@link ContentNotFoundException} is thrown or not. It should be overridden as + * soon as there is a mechanism to check existence before actually getting the + * content. + */ + default boolean exists(ProvidedSession session, String relativePath) { + try { + get(session, relativePath); + return true; + } catch (ContentNotFoundException e) { + return false; + } + } + /** The absolute path where this provider is mounted. */ String getMountPath(); + /** + * Search content within this provider. The default implementation throws an + * {@link UnsupportedOperationException}. + */ + default Spliterator search(ProvidedSession session, BasicSearch search, String relPath) { + throw new UnsupportedOperationException(); + } + + /* + * EDITION + */ + /** Switch this content (and its subtree) to editing mode. */ + default void openForEdit(ProvidedSession session, String relativePath) { + throw new UnsupportedOperationException(); + } + + /** Switch this content (and its subtree) to frozen mode. */ + default void freeze(ProvidedSession session, String relativePath) { + throw new UnsupportedOperationException(); + } + + /** Whether this content (and its subtree) are in editing mode. */ + default boolean isOpenForEdit(ProvidedSession session, String relativePath) { + throw new UnsupportedOperationException(); + } + + /** + * Called when an edition cycle is completed. Does nothing by default. + * + * @see ContentSession#edit(java.util.function.Consumer) + */ + default void persist(ProvidedSession session) { + } + /* * NAMESPACE CONTEXT */ @@ -25,16 +86,4 @@ public interface ContentProvider extends NamespaceContext { return prefixes.hasNext() ? prefixes.next() : null; } - default Spliterator search(ProvidedSession session, BasicSearch search, String relPath) { - throw new UnsupportedOperationException(); - } - -// default ContentName parsePrefixedName(String nameWithPrefix) { -// return NamespaceUtils.parsePrefixedName(this, nameWithPrefix); -// } -// -// default String toPrefixedName(QName name) { -// return NamespaceUtils.toPrefixedName(this, name); -// } - } diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/spi/ProvidedContent.java b/org.argeo.api.acr/src/org/argeo/api/acr/spi/ProvidedContent.java index e2807c0ef..f1e5aaaa8 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/spi/ProvidedContent.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/spi/ProvidedContent.java @@ -1,35 +1,64 @@ package org.argeo.api.acr.spi; +import java.util.Optional; + import org.argeo.api.acr.Content; /** A {@link Content} implementation. */ public interface ProvidedContent extends Content { - final static String ROOT_PATH = "/"; - + /** The related {@link ProvidedSession}. */ ProvidedSession getSession(); + /** The {@link ContentProvider} this {@link Content} belongs to. */ ContentProvider getProvider(); + /** Depth relative to the root of the repository. */ int getDepth(); + /** + * Whether this is the root node of the related repository. Default checks + * whether {@link #getDepth()} == 0, but it can be optimised by + * implementations. + */ + default boolean isRoot() { + return getDepth() == 0; + } + /** * An opaque ID which is guaranteed to uniquely identify this content within the * session return by {@link #getSession()}. Typically used for UI. */ String getSessionLocalId(); + /** + * The {@link Content} within the same {@link ContentProvider} which can be used + * to mount another {@link ContentProvider}. + */ default ProvidedContent getMountPoint(String relativePath) { throw new UnsupportedOperationException("This content doe not support mount"); } - default ProvidedContent getContent(String path) { - Content fileNode; - if (path.startsWith(ROOT_PATH)) {// absolute - fileNode = getSession().get(path); + @Override + default Optional getContent(String path) { + String absolutePath; + if (path.startsWith(Content.ROOT_PATH)) {// absolute + absolutePath = path; } else {// relative - String absolutePath = getPath() + '/' + path; - fileNode = getSession().get(absolutePath); + absolutePath = getPath() + '/' + path; } - return (ProvidedContent) fileNode; + return getSession().exists(absolutePath) ? Optional.of(getSession().get(absolutePath)) : Optional.empty(); + } + + /* + * ACCESS + */ + /** Whether the session has the right to access the parent. */ + default boolean isParentAccessible() { + return true; + } + + /** Whether the related session can open this content for edit. */ + default boolean canEdit() { + return false; } } diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/spi/ProvidedSession.java b/org.argeo.api.acr/src/org/argeo/api/acr/spi/ProvidedSession.java index 5a538b57a..6d0dfbb24 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/spi/ProvidedSession.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/spi/ProvidedSession.java @@ -20,7 +20,7 @@ public interface ProvidedSession extends ContentSession { void notifyModification(ProvidedContent content); - UUID getUuid(); + UUID uuid(); // Content getSessionRunDir(); diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/CmsSession.java b/org.argeo.api.cms/src/org/argeo/api/cms/CmsSession.java index dda1dac1f..b69e54f98 100644 --- a/org.argeo.api.cms/src/org/argeo/api/cms/CmsSession.java +++ b/org.argeo.api.cms/src/org/argeo/api/cms/CmsSession.java @@ -13,7 +13,7 @@ public interface CmsSession { final static String SESSION_UUID = "entryUUID"; final static String SESSION_LOCAL_ID = "uniqueIdentifier"; - UUID getUuid(); + UUID uuid(); String getUserRole(); diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/ux/Cms2DSize.java b/org.argeo.api.cms/src/org/argeo/api/cms/ux/Cms2DSize.java index 1ec753a18..f35e98c50 100644 --- a/org.argeo.api.cms/src/org/argeo/api/cms/ux/Cms2DSize.java +++ b/org.argeo.api.cms/src/org/argeo/api/cms/ux/Cms2DSize.java @@ -1,38 +1,9 @@ package org.argeo.api.cms.ux; /** A 2D size. */ -public class Cms2DSize { - private Integer width; - private Integer height; - - public Cms2DSize() { - } - - public Cms2DSize(Integer width, Integer height) { - super(); - this.width = width; - this.height = height; - } - - public Integer getWidth() { - return width; - } - - public void setWidth(Integer width) { - this.width = width; - } - - public Integer getHeight() { - return height; - } - - public void setHeight(Integer height) { - this.height = height; - } - +public record Cms2DSize(int width, int height) { @Override public String toString() { return Cms2DSize.class.getSimpleName() + "[" + width + "," + height + "]"; } - } diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/ux/CmsImageManager.java b/org.argeo.api.cms/src/org/argeo/api/cms/ux/CmsImageManager.java index 1ec54a9d9..4fb393a56 100644 --- a/org.argeo.api.cms/src/org/argeo/api/cms/ux/CmsImageManager.java +++ b/org.argeo.api.cms/src/org/argeo/api/cms/ux/CmsImageManager.java @@ -1,11 +1,12 @@ package org.argeo.api.cms.ux; import java.io.InputStream; +import java.net.URI; /** Read and write access to images. */ public interface CmsImageManager { /** Load image in control */ - public Boolean load(M node, V control, Cms2DSize size); + public Boolean load(M node, V control, Cms2DSize size, URI link); /** @return (0,0) if not available */ public Cms2DSize getImageSize(M node); diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/ux/CmsView.java b/org.argeo.api.cms/src/org/argeo/api/cms/ux/CmsView.java index 15b6a5dc7..a36baf8e0 100644 --- a/org.argeo.api.cms/src/org/argeo/api/cms/ux/CmsView.java +++ b/org.argeo.api.cms/src/org/argeo/api/cms/ux/CmsView.java @@ -31,7 +31,7 @@ public interface CmsView { // SERVICES void exception(Throwable e); - CmsImageManager getImageManager(); + CmsImageManager getImageManager(); boolean isAnonymous(); diff --git a/org.argeo.api.uuid/src/org/argeo/api/uuid/UuidIdentified.java b/org.argeo.api.uuid/src/org/argeo/api/uuid/UuidIdentified.java new file mode 100644 index 000000000..77cab371e --- /dev/null +++ b/org.argeo.api.uuid/src/org/argeo/api/uuid/UuidIdentified.java @@ -0,0 +1,57 @@ +package org.argeo.api.uuid; + +import java.util.UUID; + +/** + * An object identified by a {@link UUID}. Typically used to fasten indexing and + * comparisons of objects or records. THe method to implement is {@link #uuid()} + * so that any record with an uuid field can easily be enriched + * with this interface. + */ +public interface UuidIdentified { + /** The UUID identifier. */ + UUID uuid(); + + /** The UUID identifier, for compatibility with beans accessors. */ + default UUID getUuid() { + return uuid(); + } + + /** + * Helper to implement the equals method of an {@link UuidIdentified}.
+ * + *
+	 * @Override
+	 * public boolean equals(Object o) {
+	 * 	return UuidIdentified.equals(this, o);
+	 * }
+	 * 
+ */ + static boolean equals(UuidIdentified uuidIdentified, Object o) { + assert uuidIdentified != null; + if (o == null) + return false; + if (uuidIdentified == o) + return true; + if (o instanceof UuidIdentified u) + return uuidIdentified.uuid().equals(u.uuid()); + else + return false; + } + + /** + * Helper to implement the hash code method of an {@link UuidIdentified}.
+ * + *
+	 * @Override
+	 * public int hashCode() {
+	 * 	return UuidIdentified.hashCode(this);
+	 * }
+	 * 
+ */ + static int hashCode(UuidIdentified uuidIdentified) { + assert uuidIdentified != null; + return uuidIdentified.getUuid().hashCode(); + } + +} diff --git a/org.argeo.cms.ux/src/org/argeo/cms/ux/AbstractCmsEditable.java b/org.argeo.cms.ux/src/org/argeo/cms/ux/AbstractCmsEditable.java index a1cd3d90e..96c15bbca 100644 --- a/org.argeo.cms.ux/src/org/argeo/cms/ux/AbstractCmsEditable.java +++ b/org.argeo.cms.ux/src/org/argeo/cms/ux/AbstractCmsEditable.java @@ -6,9 +6,14 @@ import org.argeo.api.cms.ux.CmsEditable; import org.argeo.api.cms.ux.CmsEditionEvent; import org.argeo.api.cms.ux.CmsEditionListener; +/** + * Base class for implementing {@link CmsEditable}, mostly managing + * {@link CmsEditionListener}s. + */ public abstract class AbstractCmsEditable implements CmsEditable { private IdentityHashMap listeners = new IdentityHashMap<>(); + /** Notifies listeners of a {@link CmsEditionEvent}. */ protected void notifyListeners(CmsEditionEvent e) { if (CmsEditionEvent.START_EDITING == e.getType()) { for (CmsEditionListener listener : listeners.keySet()) diff --git a/org.argeo.cms.ux/src/org/argeo/cms/ux/AbstractImageManager.java b/org.argeo.cms.ux/src/org/argeo/cms/ux/AbstractImageManager.java index 41c905ef6..31a0d6bed 100644 --- a/org.argeo.cms.ux/src/org/argeo/cms/ux/AbstractImageManager.java +++ b/org.argeo.cms.ux/src/org/argeo/cms/ux/AbstractImageManager.java @@ -10,16 +10,16 @@ public abstract class AbstractImageManager implements CmsImageManager implements CmsImageManager implements HierarchicalPart { @Override - public List getChildren(Content content) { + public List getChildren(Content parent) { List res = new ArrayList<>(); - if (isLeaf(content)) + if (isLeaf(parent)) return res; - if (content == null) + if (parent == null) return res; - for (Iterator it = content.iterator(); it.hasNext();) { + for (Iterator it = parent.iterator(); it.hasNext();) { res.add(it.next()); } diff --git a/org.argeo.cms.ux/src/org/argeo/cms/ux/acr/ContentPart.java b/org.argeo.cms.ux/src/org/argeo/cms/ux/acr/ContentPart.java index 0a0ad0ea5..db8a746fc 100644 --- a/org.argeo.cms.ux/src/org/argeo/cms/ux/acr/ContentPart.java +++ b/org.argeo.cms.ux/src/org/argeo/cms/ux/acr/ContentPart.java @@ -6,9 +6,9 @@ import org.argeo.api.acr.Content; public interface ContentPart { Content getContent(); - @Deprecated - default Content getNode() { - return getContent(); - } +// @Deprecated +// default Content getNode() { +// return getContent(); +// } } diff --git a/org.argeo.cms/OSGI-INF/cmsAcrHttpHandler.xml b/org.argeo.cms/OSGI-INF/cmsAcrHttpHandler.xml index afd4e0aaf..39400d99a 100644 --- a/org.argeo.cms/OSGI-INF/cmsAcrHttpHandler.xml +++ b/org.argeo.cms/OSGI-INF/cmsAcrHttpHandler.xml @@ -5,5 +5,6 @@ + diff --git a/org.argeo.cms/src/org/argeo/cms/acr/AbstractContent.java b/org.argeo.cms/src/org/argeo/cms/acr/AbstractContent.java index 16f39609e..1acdcc380 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/AbstractContent.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/AbstractContent.java @@ -13,6 +13,7 @@ import java.util.Set; import javax.xml.namespace.QName; import org.argeo.api.acr.Content; +import org.argeo.api.acr.CrAttributeType; import org.argeo.api.acr.CrName; import org.argeo.api.acr.NamespaceUtils; import org.argeo.api.acr.spi.ProvidedContent; @@ -31,18 +32,22 @@ public abstract class AbstractContent extends AbstractMap impleme } /* - * ATTRIBUTES OPERATIONS + * ATTRIBUTES MAP IMPLEMENTATION */ -// protected abstract Iterable keys(); -// -// protected abstract void removeAttr(QName key); - @Override public Set> entrySet() { Set> result = new AttrSet(); return result; } + @Override + public Object get(Object key) { + return get((QName) key, Object.class).orElse(null); + } + + /* + * ATTRIBUTES OPERATIONS + */ @Override public Class getType(QName key) { return String.class; @@ -60,22 +65,18 @@ public abstract class AbstractContent extends AbstractMap impleme if (value == null) return new ArrayList<>(); if (value instanceof List) { - if (isDefaultAttrTypeRequested(clss)) + if (clss.isAssignableFrom(Object.class)) return (List
) value; List res = new ArrayList<>(); List lst = (List) value; for (Object o : lst) { - A item = clss.isAssignableFrom(String.class) ? (A) o.toString() : (A) o; + A item = CrAttributeType.cast(clss, o).get(); res.add(item); } return res; } else {// singleton -// try { - A res = (A) value; + A res = CrAttributeType.cast(clss, value).get(); return Collections.singletonList(res); -// } catch (ClassCastException e) { -// return Optional.empty(); -// } } } @@ -120,6 +121,11 @@ public abstract class AbstractContent extends AbstractMap impleme return ancestors.size(); } + @Override + public boolean isRoot() { + return CrName.root.qName().equals(getName()); + } + @Override public String getSessionLocalId() { return getPath(); @@ -146,10 +152,10 @@ public abstract class AbstractContent extends AbstractMap impleme /* * UTILITIES */ - protected boolean isDefaultAttrTypeRequested(Class clss) { - // check whether clss is Object.class - return clss.isAssignableFrom(Object.class); - } +// protected boolean isDefaultAttrTypeRequested(Class clss) { +// // check whether clss is Object.class +// return clss.isAssignableFrom(Object.class); +// } // @Override // public String toString() { @@ -183,7 +189,7 @@ public abstract class AbstractContent extends AbstractMap impleme @Override public Optional get(QName key, Class clss) { - return null; + return Optional.empty(); } protected void removeAttr(QName key) { diff --git a/org.argeo.cms/src/org/argeo/cms/acr/AbstractSimpleContentProvider.java b/org.argeo.cms/src/org/argeo/cms/acr/AbstractSimpleContentProvider.java new file mode 100644 index 000000000..f07a08d10 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/acr/AbstractSimpleContentProvider.java @@ -0,0 +1,133 @@ +package org.argeo.cms.acr; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.xml.namespace.QName; + +import org.argeo.api.acr.Content; +import org.argeo.api.acr.ContentName; +import org.argeo.api.acr.spi.ContentProvider; +import org.argeo.api.acr.spi.ProvidedContent; +import org.argeo.api.acr.spi.ProvidedSession; +import org.argeo.api.cms.CmsConstants; + +/** + * Base for simple content providers based on a service supporting only one + * namespace. Typically used in higher level applications for domain-specific + * modelling. + */ +public abstract class AbstractSimpleContentProvider implements ContentProvider { + private final String namespaceUri; + private final String defaultPrefix; + private SERVICE service; + private String mountPath; + private String mountName; + + protected AbstractSimpleContentProvider(String namespaceUri, String defaultPrefix) { + this(namespaceUri, defaultPrefix, null, null); + } + + protected AbstractSimpleContentProvider(String namespaceUri, String defaultPrefix, SERVICE service, + String mountPath) { + this.namespaceUri = namespaceUri; + this.defaultPrefix = defaultPrefix; + this.service = service; + setMountPath(mountPath); + } + + /** The first level of content provided by the service. */ + protected abstract Iterator firstLevel(ProvidedSession session); + + /** + * Retrieve the content at this relative path. Root content is already dealt + * with. + */ + protected abstract ProvidedContent get(ProvidedSession session, List segments); + + @Override + public final ProvidedContent get(ProvidedSession session, String relativePath) { + List segments = ContentUtils.toPathSegments(relativePath); + if (segments.size() == 0) + return new ServiceContent(session); + return get(session, segments); + } + + public void start(Map properties) { + mountPath = properties.get(CmsConstants.ACR_MOUNT_PATH); + if (mountPath == null) + throw new IllegalStateException(CmsConstants.ACR_MOUNT_PATH + " must be specified."); + setMountPath(mountPath); + } + + private void setMountPath(String mountPath) { + if (mountPath == null) + return; + this.mountPath = mountPath; + List mountSegments = ContentUtils.toPathSegments(mountPath); + this.mountName = mountSegments.get(mountSegments.size() - 1); + + } + + @Override + public String getNamespaceURI(String prefix) { + if (defaultPrefix.equals(prefix)) + return namespaceUri; + throw new IllegalArgumentException("Only prefix " + defaultPrefix + " is supported"); + } + + @Override + public Iterator getPrefixes(String namespaceURI) { + if (namespaceUri.equals(namespaceURI)) + return Collections.singletonList(defaultPrefix).iterator(); + throw new IllegalArgumentException("Only namespace URI " + namespaceUri + " is supported"); + } + + @Override + public String getMountPath() { + return mountPath; + } + + protected String getMountName() { + return mountName; + } + + protected SERVICE getService() { + return service; + } + + public void setService(SERVICE service) { + this.service = service; + } + + protected class ServiceContent extends AbstractContent { + + public ServiceContent(ProvidedSession session) { + super(session); + } + + @Override + public ContentProvider getProvider() { + return AbstractSimpleContentProvider.this; + } + + @Override + public QName getName() { + return new ContentName(getMountName()); + } + + @Override + public Content getParent() { + return null; + } + + @Override + public Iterator iterator() { + return firstLevel(getSession()); + } + + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/acr/CmsContentRepository.java b/org.argeo.cms/src/org/argeo/cms/acr/CmsContentRepository.java index 89e725043..15b893bb3 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/CmsContentRepository.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/CmsContentRepository.java @@ -57,7 +57,7 @@ public class CmsContentRepository extends AbstractContentRepository { CmsSession cmsSession = CurrentUser.getCmsSession(); CmsContentSession contentSession = userSessions.get(cmsSession); if (contentSession == null) { - final CmsContentSession newContentSession = new CmsContentSession(this, cmsSession.getUuid(), + final CmsContentSession newContentSession = new CmsContentSession(this, cmsSession.uuid(), cmsSession.getSubject(), locale, uuidFactory); cmsSession.addOnCloseCallback((c) -> { newContentSession.close(); 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 af7dca046..daefe9835 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/CmsContentSession.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/CmsContentSession.java @@ -26,10 +26,11 @@ import org.argeo.api.acr.spi.ProvidedContent; import org.argeo.api.acr.spi.ProvidedRepository; import org.argeo.api.acr.spi.ProvidedSession; import org.argeo.api.uuid.UuidFactory; -import org.argeo.cms.acr.xml.DomContentProvider; +import org.argeo.api.uuid.UuidIdentified; +import org.argeo.cms.CurrentUser; /** Implements {@link ProvidedSession}. */ -class CmsContentSession implements ProvidedSession { +class CmsContentSession implements ProvidedSession, UuidIdentified { final private AbstractContentRepository contentRepository; private final UUID uuid; @@ -69,32 +70,25 @@ class CmsContentSession implements ProvidedSession { @Override public Content get(String path) { - if (!path.startsWith(ContentUtils.ROOT_SLASH)) + if (!path.startsWith(Content.ROOT_PATH)) throw new IllegalArgumentException(path + " is not an absolute path"); ContentProvider contentProvider = contentRepository.getMountManager().findContentProvider(path); String mountPath = contentProvider.getMountPath(); - String relativePath = extractRelativePath(mountPath, path); + String relativePath = ContentUtils.relativize(mountPath, path); ProvidedContent content = contentProvider.get(CmsContentSession.this, relativePath); return content; } @Override public boolean exists(String path) { - if (!path.startsWith(ContentUtils.ROOT_SLASH)) + if (!path.startsWith(Content.ROOT_PATH)) throw new IllegalArgumentException(path + " is not an absolute path"); ContentProvider contentProvider = contentRepository.getMountManager().findContentProvider(path); String mountPath = contentProvider.getMountPath(); - String relativePath = extractRelativePath(mountPath, path); + String relativePath = ContentUtils.relativize(mountPath, path); return contentProvider.exists(this, relativePath); } - private String extractRelativePath(String mountPath, String path) { - String relativePath = path.substring(mountPath.length()); - if (relativePath.length() > 0 && relativePath.charAt(0) == '/') - relativePath = relativePath.substring(1); - return relativePath; - } - @Override public Subject getSubject() { return subject; @@ -137,9 +131,10 @@ class CmsContentSession implements ProvidedSession { synchronized (CmsContentSession.this) { // TODO optimise for (ContentProvider provider : modifiedProviders) { - if (provider instanceof DomContentProvider) { - ((DomContentProvider) provider).persist(s); - } + provider.persist(s); +// if (provider instanceof DomContentProvider) { +// ((DomContentProvider) provider).persist(s); +// } } modifiedProviders.clear(); return s; @@ -160,7 +155,7 @@ class CmsContentSession implements ProvidedSession { } @Override - public UUID getUuid() { + public UUID uuid() { return uuid; } @@ -179,6 +174,25 @@ class CmsContentSession implements ProvidedSession { return sessionRunDir; } + /* + * OBJECT METHODS + */ + + @Override + public boolean equals(Object o) { + return UuidIdentified.equals(this, o); + } + + @Override + public int hashCode() { + return UuidIdentified.hashCode(this); + } + + @Override + public String toString() { + return "Content Session " + uuid + " (" + CurrentUser.getUsername(subject) + ")"; + } + /* * SEARCH */ diff --git a/org.argeo.cms/src/org/argeo/cms/acr/ContentUtils.java b/org.argeo.cms/src/org/argeo/cms/acr/ContentUtils.java index d324ac475..bf5954411 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/ContentUtils.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/ContentUtils.java @@ -3,6 +3,7 @@ package org.argeo.cms.acr; import java.io.PrintStream; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.StringJoiner; import java.util.function.BiConsumer; @@ -15,6 +16,7 @@ import org.argeo.api.acr.ContentRepository; import org.argeo.api.acr.ContentSession; import org.argeo.api.acr.DName; import org.argeo.api.cms.CmsAuth; +import org.argeo.api.cms.CmsSession; import org.argeo.api.cms.directory.CmsDirectory; import org.argeo.api.cms.directory.CmsUserManager; import org.argeo.api.cms.directory.HierarchyUnit; @@ -66,7 +68,6 @@ public class ContentUtils { public static final char SLASH = '/'; public static final String SLASH_STRING = Character.toString(SLASH); - public static final String ROOT_SLASH = "" + SLASH; public static final String EMPTY = ""; /** @@ -102,7 +103,7 @@ public class ContentUtils { public static List toPathSegments(String path) { List res = new ArrayList<>(); - if (EMPTY.equals(path) || ROOT_SLASH.equals(path)) + if (EMPTY.equals(path) || Content.ROOT_PATH.equals(path)) return res; collectPathSegments(path, res); return res; @@ -176,7 +177,7 @@ public class ContentUtils { } } - public static ContentSession openDataAdminSession(ContentRepository repository) { + public static ContentSession openDataAdminSession(ContentRepository contentRepository) { LoginContext loginContext; try { loginContext = CmsAuth.DATA_ADMIN.newLoginContext(); @@ -189,12 +190,33 @@ public class ContentUtils { ClassLoader currentCl = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(ContentUtils.class.getClassLoader()); - return CurrentSubject.callAs(loginContext.getSubject(), () -> repository.get()); + return CurrentSubject.callAs(loginContext.getSubject(), () -> contentRepository.get()); } finally { Thread.currentThread().setContextClassLoader(currentCl); } } + public static ContentSession openSession(ContentRepository contentRepository, CmsSession cmsSession) { + return CurrentSubject.callAs(cmsSession.getSubject(), () -> contentRepository.get()); + } + + /** + * Constructs a relative path between a base path and a given path. + * + * @throws IllegalArgumentException if the base path is not an ancestor of the + * path + */ + public static String relativize(String basePath, String path) throws IllegalArgumentException { + Objects.requireNonNull(basePath); + Objects.requireNonNull(path); + if (!path.startsWith(basePath)) + throw new IllegalArgumentException(basePath + " is not an ancestor of " + path); + String relativePath = path.substring(basePath.length()); + if (relativePath.length() > 0 && relativePath.charAt(0) == '/') + relativePath = relativePath.substring(1); + return relativePath; + } + /** Singleton. */ private ContentUtils() { diff --git a/org.argeo.cms/src/org/argeo/cms/acr/SvgAttrs.java b/org.argeo.cms/src/org/argeo/cms/acr/SvgAttrs.java index 61d8e0429..18527f590 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/SvgAttrs.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/SvgAttrs.java @@ -2,6 +2,10 @@ package org.argeo.cms.acr; import org.argeo.api.acr.QNamed; +/** + * Some core SVG attributes, which are used to standardise generic concepts such + * as width, height, etc. + */ public enum SvgAttrs implements QNamed { /** */ width, diff --git a/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContentProvider.java b/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContentProvider.java index 8b6eb6bbd..ab84aef37 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContentProvider.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContentProvider.java @@ -1,15 +1,11 @@ package org.argeo.cms.acr.directory; import java.util.ArrayList; -import java.util.Collections; import java.util.Iterator; import java.util.List; -import javax.xml.namespace.QName; - import org.argeo.api.acr.ArgeoNamespace; import org.argeo.api.acr.Content; -import org.argeo.api.acr.ContentName; import org.argeo.api.acr.ContentNotFoundException; import org.argeo.api.acr.spi.ContentProvider; import org.argeo.api.acr.spi.ProvidedContent; @@ -17,38 +13,39 @@ import org.argeo.api.acr.spi.ProvidedSession; import org.argeo.api.cms.directory.CmsUserManager; import org.argeo.api.cms.directory.HierarchyUnit; import org.argeo.api.cms.directory.UserDirectory; -import org.argeo.cms.acr.AbstractContent; +import org.argeo.cms.acr.AbstractSimpleContentProvider; import org.argeo.cms.acr.ContentUtils; import org.osgi.service.useradmin.User; -public class DirectoryContentProvider implements ContentProvider { - private String mountPath; - private String mountName; +/** A {@link ContentProvider} based on a {@link CmsUserManager} service. */ +public class DirectoryContentProvider extends AbstractSimpleContentProvider { - private CmsUserManager userManager; + public DirectoryContentProvider(CmsUserManager service, String mountPath) { + super(ArgeoNamespace.LDAP_NAMESPACE_URI, ArgeoNamespace.LDAP_DEFAULT_PREFIX, service, mountPath); + } - public DirectoryContentProvider(String mountPath, CmsUserManager userManager) { - this.mountPath = mountPath; - List mountSegments = ContentUtils.toPathSegments(mountPath); - this.mountName = mountSegments.get(mountSegments.size() - 1); - this.userManager = userManager; + @Override + protected Iterator firstLevel(ProvidedSession session) { + List res = new ArrayList<>(); + for (UserDirectory userDirectory : getService().getUserDirectories()) { + DirectoryContent content = new DirectoryContent(session, DirectoryContentProvider.this, userDirectory); + res.add(content); + } + return res.iterator(); } @Override - public ProvidedContent get(ProvidedSession session, String relativePath) { - List segments = ContentUtils.toPathSegments(relativePath); - if (segments.size() == 0) - return new UserManagerContent(session); + public ProvidedContent get(ProvidedSession session, List segments) { String userDirectoryName = segments.get(0); UserDirectory userDirectory = null; - userDirectories: for (UserDirectory ud : userManager.getUserDirectories()) { + userDirectories: for (UserDirectory ud : getService().getUserDirectories()) { if (userDirectoryName.equals(ud.getName())) { userDirectory = ud; break userDirectories; } } if (userDirectory == null) - throw new ContentNotFoundException(session, mountPath + "/" + relativePath, + throw new ContentNotFoundException(session, getMountPath() + "/" + ContentUtils.toPath(segments), "Cannot find user directory " + userDirectoryName); if (segments.size() == 1) { return new DirectoryContent(session, this, userDirectory); @@ -73,7 +70,7 @@ public class DirectoryContentProvider implements ContentProvider { HierarchyUnit hierarchyUnit = userDirectory.getHierarchyUnit(pathWithinUserDirectory); if (hierarchyUnit == null) throw new ContentNotFoundException(session, - mountPath + "/" + relativePath + "/" + pathWithinUserDirectory, + getMountPath() + "/" + ContentUtils.toPath(segments) + "/" + pathWithinUserDirectory, "Cannot find " + pathWithinUserDirectory + " within " + userDirectoryName); return new HierarchyUnitContent(session, this, hierarchyUnit); } @@ -81,75 +78,19 @@ public class DirectoryContentProvider implements ContentProvider { @Override public boolean exists(ProvidedSession session, String relativePath) { - // TODO Auto-generated method stub - return false; - } - - @Override - public String getMountPath() { - return mountPath; - } - - @Override - public String getNamespaceURI(String prefix) { - if (ArgeoNamespace.LDAP_DEFAULT_PREFIX.equals(prefix)) - return ArgeoNamespace.LDAP_NAMESPACE_URI; - throw new IllegalArgumentException("Only prefix " + ArgeoNamespace.LDAP_DEFAULT_PREFIX + " is supported"); - } - - @Override - public Iterator getPrefixes(String namespaceURI) { - if (ArgeoNamespace.LDAP_NAMESPACE_URI.equals(namespaceURI)) - return Collections.singletonList(ArgeoNamespace.LDAP_DEFAULT_PREFIX).iterator(); - throw new IllegalArgumentException("Only namespace URI " + ArgeoNamespace.LDAP_NAMESPACE_URI + " is supported"); - } - - public void setUserManager(CmsUserManager userManager) { - this.userManager = userManager; + // TODO optimise? + return exists(session, relativePath); } - public CmsUserManager getUserManager() { - return userManager; - } +// public void setUserManager(CmsUserManager userManager) { +// this.userManager = userManager; +// } - UserManagerContent getRootContent(ProvidedSession session) { - return new UserManagerContent(session); + CmsUserManager getUserManager() { + return getService(); } - /* - * COMMON UTILITIES - */ - class UserManagerContent extends AbstractContent { - - public UserManagerContent(ProvidedSession session) { - super(session); - } - - @Override - public ContentProvider getProvider() { - return DirectoryContentProvider.this; - } - - @Override - public QName getName() { - return new ContentName(mountName); - } - - @Override - public Content getParent() { - return null; - } - - @Override - public Iterator iterator() { - List res = new ArrayList<>(); - for (UserDirectory userDirectory : userManager.getUserDirectories()) { - DirectoryContent content = new DirectoryContent(getSession(), DirectoryContentProvider.this, - userDirectory); - res.add(content); - } - return res.iterator(); - } - + ServiceContent getRootContent(ProvidedSession session) { + return new ServiceContent(session); } } 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 8fca831b0..13b19aabb 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 @@ -41,14 +41,13 @@ import org.argeo.api.acr.NamespaceUtils; import org.argeo.api.acr.spi.ContentProvider; import org.argeo.api.acr.spi.ProvidedContent; import org.argeo.api.acr.spi.ProvidedSession; -import org.argeo.api.cms.CmsLog; import org.argeo.cms.acr.AbstractContent; import org.argeo.cms.acr.ContentUtils; import org.argeo.cms.util.FsUtils; /** Content persisted as a filesystem {@link Path}. */ public class FsContent extends AbstractContent implements ProvidedContent { - private CmsLog log = CmsLog.getLog(FsContent.class); +// private CmsLog log = CmsLog.getLog(FsContent.class); final static String USER_ = "user:"; @@ -81,7 +80,7 @@ public class FsContent extends AbstractContent implements ProvidedContent { // TODO check file names with ':' ? if (isMountBase) { String mountPath = provider.getMountPath(); - if (mountPath != null && !mountPath.equals(ContentUtils.ROOT_SLASH)) { + if (mountPath != null && !mountPath.equals(Content.ROOT_PATH)) { Content mountPoint = session.getMountPoint(mountPath); this.name = mountPoint.getName(); } else { @@ -160,11 +159,15 @@ public class FsContent extends AbstractContent implements ProvidedContent { String[] arr = str.split("\n"); if (arr.length == 1) { - if (clss.isAssignableFrom(String.class)) { - res = (A) arr[0]; - } else { - res = (A) CrAttributeType.parse(arr[0]); - } +// if (clss.isAssignableFrom(String.class)) { +// res = (A) arr[0]; +// } else { +// res = (A) CrAttributeType.parse(arr[0]); +// } +// if (isDefaultAttrTypeRequested(clss)) +// return Optional.of((A) CrAttributeType.parse(str)); + return CrAttributeType.cast(clss, str); + } else { List lst = new ArrayList<>(); for (String s : arr) { @@ -174,14 +177,15 @@ public class FsContent extends AbstractContent implements ProvidedContent { } } if (res == null) { - if (isDefaultAttrTypeRequested(clss)) - return Optional.of((A) CrAttributeType.parse(value.toString())); - if (clss.isAssignableFrom(value.getClass())) - return Optional.of((A) value); - if (clss.isAssignableFrom(String.class)) - return Optional.of((A) value.toString()); - log.warn("Cannot interpret " + key + " in " + this); - return Optional.empty(); +// if (isDefaultAttrTypeRequested(clss)) +// return Optional.of((A) CrAttributeType.parse(value.toString())); + return CrAttributeType.cast(clss, value); +// if (clss.isAssignableFrom(value.getClass())) +// return Optional.of((A) value); +// if (clss.isAssignableFrom(String.class)) +// return Optional.of((A) value.toString()); +// log.warn("Cannot interpret " + key + " in " + this); +// return Optional.empty(); // try { // res = (A) value; // } catch (ClassCastException e) { @@ -365,12 +369,13 @@ public class FsContent extends AbstractContent implements ProvidedContent { @Override public List getContentClasses() { - List res = new ArrayList<>(); - List value = getMultiple(DName.resourcetype.qName(), String.class); - for (String s : value) { - QName name = NamespaceUtils.parsePrefixedName(provider, s); - res.add(name); - } +// List res = new ArrayList<>(); +// List value = getMultiple(DName.resourcetype.qName(), String.class); +// for (String s : value) { +// QName name = NamespaceUtils.parsePrefixedName(provider, s); +// res.add(name); +// } + List res = getMultiple(DName.resourcetype.qName(), QName.class); if (Files.isDirectory(path)) res.add(DName.collection.qName()); return res; 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 a4c14186a..0686be7cb 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 @@ -1,7 +1,5 @@ package org.argeo.cms.acr.xml; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; @@ -25,13 +23,13 @@ import javax.xml.transform.Result; import javax.xml.transform.Source; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; -import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMResult; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.argeo.api.acr.Content; import org.argeo.api.acr.ContentName; +import org.argeo.api.acr.CrAttributeType; import org.argeo.api.acr.CrName; import org.argeo.api.acr.spi.ProvidedContent; import org.argeo.api.acr.spi.ProvidedSession; @@ -71,7 +69,7 @@ public class DomContent extends AbstractContent implements ProvidedContent { if (isLocalRoot()) {// root String mountPath = provider.getMountPath(); if (mountPath != null) { - if (ContentUtils.ROOT_SLASH.equals(mountPath)) { + if (Content.ROOT_PATH.equals(mountPath)) { return CrName.root.qName(); } Content mountPoint = getSession().getMountPoint(mountPath); @@ -140,17 +138,16 @@ public class DomContent extends AbstractContent implements ProvidedContent { return result; } - @SuppressWarnings("unchecked") +// @SuppressWarnings("unchecked") @Override public Optional 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 Optional.of((A) value); - else - return Optional.empty(); +// if (isDefaultAttrTypeRequested(clss)) +// return Optional.of((A) CrAttributeType.parse(value)); + return CrAttributeType.cast(clss, value); } else return Optional.empty(); } @@ -239,7 +236,7 @@ public class DomContent extends AbstractContent implements ProvidedContent { String mountPath = provider.getMountPath(); if (mountPath == null) return null; - if (ContentUtils.ROOT_SLASH.equals(mountPath)) { + if (Content.ROOT_PATH.equals(mountPath)) { return null; } String[] parent = ContentUtils.getParentPath(mountPath); @@ -389,7 +386,9 @@ public class DomContent extends AbstractContent implements ProvidedContent { List res = new ArrayList<>(); if (isLocalRoot()) { String mountPath = provider.getMountPath(); - if (mountPath != null) { + if (Content.ROOT_PATH.equals(mountPath)) {// repository root + res.add(CrName.root.qName()); + } else { Content mountPoint = getSession().getMountPoint(mountPath); res.addAll(mountPoint.getContentClasses()); } @@ -403,7 +402,9 @@ public class DomContent extends AbstractContent implements ProvidedContent { public void addContentClasses(QName... contentClass) { if (isLocalRoot()) { String mountPath = provider.getMountPath(); - if (mountPath != null) { + if (Content.ROOT_PATH.equals(mountPath)) {// repository root + throw new IllegalArgumentException("Cannot add content classes to repository root"); + } else { Content mountPoint = getSession().getMountPoint(mountPath); mountPoint.addContentClasses(contentClass); } diff --git a/org.argeo.cms/src/org/argeo/cms/acr/xml/DomContentProvider.java b/org.argeo.cms/src/org/argeo/cms/acr/xml/DomContentProvider.java index 66ff878d5..a5abe8dd4 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/xml/DomContentProvider.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/xml/DomContentProvider.java @@ -57,16 +57,6 @@ public class DomContentProvider implements ContentProvider, NamespaceContext { }; } -// @Override -// public Content get() { -// return new DomContent(this, document.getDocumentElement()); -// } - -// public Element createElement(String name) { -// return document.createElementNS(null, name); -// -// } - @Override public ProvidedContent get(ProvidedSession session, String relativePath) { if ("".equals(relativePath)) @@ -86,7 +76,7 @@ public class DomContentProvider implements ContentProvider, NamespaceContext { if (relativePath.startsWith("/")) throw new IllegalArgumentException("Relative path cannot start with /"); String xPathExpression = '/' + relativePath; - if ("/".equals(mountPath)) + if (Content.ROOT_PATH.equals(mountPath)) // repository root xPathExpression = "/" + CrName.root.qName() + xPathExpression; try { NodeList nodes = (NodeList) xPath.get().evaluate(xPathExpression, document, XPathConstants.NODESET); @@ -105,6 +95,7 @@ public class DomContentProvider implements ContentProvider, NamespaceContext { return nodes.getLength() != 0; } + @Override public void persist(ProvidedSession session) { if (mountPath != null) { Content mountPoint = session.getMountPoint(mountPath); 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 3c436ba1f..af4b5379c 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/RemoteAuthUtils.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/RemoteAuthUtils.java @@ -46,39 +46,8 @@ public class RemoteAuthUtils { public final static T doAs(Supplier supplier, RemoteAuthRequest req) { CmsSession cmsSession = getCmsSession(req); return CurrentSubject.callAs(cmsSession.getSubject(), () -> supplier.get()); -// ClassLoader currentContextCl = Thread.currentThread().getContextClassLoader(); -// Thread.currentThread().setContextClassLoader(RemoteAuthUtils.class.getClassLoader()); -// try { -// return Subject.doAs( -// Subject.getSubject((AccessControlContext) req.getAttribute(AccessControlContext.class.getName())), -// new PrivilegedAction() { -// -// @Override -// public T run() { -// return supplier.get(); -// } -// -// }); -// } finally { -// Thread.currentThread().setContextClassLoader(currentContextCl); -// } } -// public final static void configureRequestSecurity(RemoteAuthRequest req) { -// if (req.getAttribute(AccessControlContext.class.getName()) != null) -// throw new IllegalStateException("Request already authenticated."); -// AccessControlContext acc = AccessController.getContext(); -// req.setAttribute(REMOTE_USER, CurrentUser.getUsername()); -// req.setAttribute(AccessControlContext.class.getName(), acc); -// } -// -// public final static void clearRequestSecurity(RemoteAuthRequest req) { -// if (req.getAttribute(AccessControlContext.class.getName()) == null) -// throw new IllegalStateException("Cannot clear non-authenticated request."); -// req.setAttribute(REMOTE_USER, null); -// req.setAttribute(AccessControlContext.class.getName(), null); -// } - public static CmsSession getCmsSession(RemoteAuthRequest req) { CmsSession cmsSession = (CmsSession) req.getAttribute(CmsSession.class.getName()); if (cmsSession == null) diff --git a/org.argeo.cms/src/org/argeo/cms/dav/DavHttpHandler.java b/org.argeo.cms/src/org/argeo/cms/dav/DavHttpHandler.java index 63f4f82c7..b5cd289a1 100644 --- a/org.argeo.cms/src/org/argeo/cms/dav/DavHttpHandler.java +++ b/org.argeo.cms/src/org/argeo/cms/dav/DavHttpHandler.java @@ -10,6 +10,7 @@ import java.util.function.Consumer; import javax.xml.namespace.NamespaceContext; import org.argeo.api.acr.ContentNotFoundException; +import org.argeo.api.cms.CmsLog; import org.argeo.cms.http.HttpHeader; import org.argeo.cms.http.HttpMethod; import org.argeo.cms.http.HttpStatus; @@ -24,6 +25,7 @@ import com.sun.net.httpserver.HttpHandler; * ACR-specific code more readable and maintainable. */ public abstract class DavHttpHandler implements HttpHandler { + private final static CmsLog log = CmsLog.getLog(DavHttpHandler.class); @Override public void handle(HttpExchange exchange) throws IOException { @@ -60,10 +62,12 @@ public abstract class DavHttpHandler implements HttpHandler { exchange.sendResponseHeaders(HttpStatus.NOT_FOUND.getCode(), -1); } // TODO return a structured error message + // TODO better filter application errors and failed login etc. catch (UnsupportedOperationException e) { e.printStackTrace(); exchange.sendResponseHeaders(HttpStatus.NOT_IMPLEMENTED.getCode(), -1); } catch (Exception e) { + log.error("Failed HTTP exchange " + exchange.getRequestURI(), e); exchange.sendResponseHeaders(HttpStatus.INTERNAL_SERVER_ERROR.getCode(), -1); } diff --git a/org.argeo.cms/src/org/argeo/cms/internal/auth/CmsSessionImpl.java b/org.argeo.cms/src/org/argeo/cms/internal/auth/CmsSessionImpl.java index 4f5a85ddf..3a23870bd 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/auth/CmsSessionImpl.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/auth/CmsSessionImpl.java @@ -21,11 +21,12 @@ import org.argeo.api.cms.CmsAuth; import org.argeo.api.cms.CmsConstants; import org.argeo.api.cms.CmsLog; import org.argeo.api.cms.CmsSession; +import org.argeo.api.uuid.UuidIdentified; import org.argeo.cms.internal.runtime.CmsContextImpl; import org.osgi.service.useradmin.Authorization; /** Default CMS session implementation. */ -public class CmsSessionImpl implements CmsSession, Serializable { +public class CmsSessionImpl implements CmsSession, Serializable, UuidIdentified { private static final long serialVersionUID = 1867719354246307225L; private final static CmsLog log = CmsLog.getLog(CmsSessionImpl.class); @@ -128,7 +129,7 @@ public class CmsSessionImpl implements CmsSession, Serializable { } @Override - public UUID getUuid() { + public UUID uuid() { return uuid; } @@ -175,6 +176,21 @@ public class CmsSessionImpl implements CmsSession, Serializable { views.put(uid, view); } + /* + * OBJECT METHODS + */ + + @Override + public boolean equals(Object o) { + return UuidIdentified.equals(this, o); + } + + @Override + public int hashCode() { + return UuidIdentified.hashCode(this); + } + + @Override public String toString() { return "CMS Session " + userDn + " localId=" + localSessionId + ", uuid=" + uuid; } diff --git a/org.argeo.cms/src/org/argeo/cms/internal/runtime/DeployedContentRepository.java b/org.argeo.cms/src/org/argeo/cms/internal/runtime/DeployedContentRepository.java index bb1f6112a..c467bcef4 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/runtime/DeployedContentRepository.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/runtime/DeployedContentRepository.java @@ -24,7 +24,7 @@ public class DeployedContentRepository extends CmsContentRepository { try { super.start(); // FIXME does not work on Windows - //Path rootXml = KernelUtils.getOsgiInstancePath(ROOT_XML); + // Path rootXml = KernelUtils.getOsgiInstancePath(ROOT_XML); initRootContentProvider(null); // Path srvPath = KernelUtils.getOsgiInstancePath(CmsConstants.SRV_WORKSPACE); @@ -40,8 +40,8 @@ public class DeployedContentRepository extends CmsContentRepository { } // users - DirectoryContentProvider directoryContentProvider = new DirectoryContentProvider( - CmsContentRepository.DIRECTORY_BASE, userManager); + DirectoryContentProvider directoryContentProvider = new DirectoryContentProvider(userManager, + CmsContentRepository.DIRECTORY_BASE); addProvider(directoryContentProvider); // remote diff --git a/sdk/branches/testing.bnd b/sdk/branches/testing.bnd index 7dba24cb4..c4fe67f0d 100644 --- a/sdk/branches/testing.bnd +++ b/sdk/branches/testing.bnd @@ -2,3 +2,12 @@ major=2 minor=1 micro=112 qualifier=.next + +Bundle-Copyright= \ +Copyright 2007-2023 Mathieu Baudier, \ +Copyright 2012-2023 Argeo GmbH + +SPDX-License-Identifier= \ +LGPL-2.1-or-later \ +OR EPL-2.0 \ +OR LicenseRef-argeo2-GPL-2.0-or-later-with-EPL-and-JCR-permissions diff --git a/sdk/branches/unstable.bnd b/sdk/branches/unstable.bnd index 1d8495879..1203fef8d 100644 --- a/sdk/branches/unstable.bnd +++ b/sdk/branches/unstable.bnd @@ -1,6 +1,6 @@ major=2 minor=3 -micro=17 +micro=18 qualifier= Bundle-Copyright= \ diff --git a/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/AbstractSwtImageManager.java b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/AbstractSwtImageManager.java index 00a51ef92..30d6bc907 100644 --- a/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/AbstractSwtImageManager.java +++ b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/AbstractSwtImageManager.java @@ -1,24 +1,29 @@ package org.argeo.cms.swt; +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + import org.argeo.api.cms.ux.Cms2DSize; import org.argeo.cms.ux.AbstractImageManager; -import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Label; /** Manages only public images so far. */ public abstract class AbstractSwtImageManager extends AbstractImageManager { - protected abstract Image getSwtImage(M node); + protected abstract ImageData getSwtImageData(M node); protected abstract String noImg(Cms2DSize size); - public Boolean load(M node, Control control, Cms2DSize preferredSize) { + @Override + public Boolean load(M node, Control control, Cms2DSize preferredSize, URI link) { Cms2DSize imageSize = getImageSize(node); Cms2DSize size; String imgTag = null; - if (preferredSize == null || imageSize.getWidth() == 0 || imageSize.getHeight() == 0 - || (preferredSize.getWidth() == 0 && preferredSize.getHeight() == 0)) { - if (imageSize.getWidth() != 0 && imageSize.getHeight() != 0) { + if (preferredSize == null || imageSize.width() == 0 || imageSize.height() == 0 + || (preferredSize.width() == 0 && preferredSize.height() == 0)) { + if (imageSize.width() != 0 && imageSize.height() != 0) { // actual image size if completely known size = imageSize; } else { @@ -27,15 +32,15 @@ public abstract class AbstractSwtImageManager extends AbstractImageManager extends AbstractImageManager"); + sb.append(imgTag); + if (link != null) + sb.append(""); + lbl.setText(imgTag); // lbl.setSize(size); // } else if (control instanceof FileUpload) { @@ -70,8 +82,8 @@ public abstract class AbstractSwtImageManager extends AbstractImageManager { @Override @@ -24,8 +28,13 @@ public class AcrSwtImageManager extends AbstractSwtImageManager { } @Override - protected Image getSwtImage(Content node) { - throw new UnsupportedOperationException(); + protected ImageData getSwtImageData(Content node) { + try (InputStream in = node.open(InputStream.class)) { + ImageData imageData = new ImageData(in); + return imageData; + } catch (IOException e) { + throw new RuntimeException(e); + } } @Override @@ -45,4 +54,15 @@ public class AcrSwtImageManager extends AbstractSwtImageManager { buf.append(node.getPath()); return buf.toString(); } + + @Override + public Cms2DSize getImageSize(Content node) { + // TODO cache it? + Optional width = node.get(SvgAttrs.width, Integer.class); + Optional height = node.get(SvgAttrs.height, Integer.class); + if (!width.isEmpty() && !height.isEmpty()) + return new Cms2DSize(width.get(), height.get()); + return super.getImageSize(node); + } + } diff --git a/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/ContentComposite.java b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/ContentComposite.java index 4a35a3bdd..4cab6d008 100644 --- a/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/ContentComposite.java +++ b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/ContentComposite.java @@ -2,10 +2,11 @@ package org.argeo.cms.swt.acr; import org.argeo.api.acr.Content; import org.argeo.api.acr.spi.ProvidedContent; +import org.argeo.cms.ux.acr.ContentPart; import org.eclipse.swt.widgets.Composite; /** A composite which can (optionally) manage a content. */ -public class ContentComposite extends Composite { +public class ContentComposite extends Composite implements ContentPart { private static final long serialVersionUID = -1447009015451153367L; public ContentComposite(Composite parent, int style, Content item) { @@ -20,6 +21,7 @@ public class ContentComposite extends Composite { return getData() instanceof Content; } + @Override public Content getContent() { return (Content) getData(); } diff --git a/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/ContentStyledControl.java b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/ContentStyledControl.java new file mode 100644 index 000000000..78ec8002b --- /dev/null +++ b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/ContentStyledControl.java @@ -0,0 +1,22 @@ +package org.argeo.cms.swt.acr; + +import org.argeo.api.acr.Content; +import org.argeo.cms.swt.widgets.StyledControl; +import org.argeo.cms.ux.acr.ContentPart; +import org.eclipse.swt.widgets.Composite; + +public abstract class ContentStyledControl extends StyledControl implements ContentPart { + + private static final long serialVersionUID = -5714246408818696583L; + + public ContentStyledControl(Composite parent, int swtStyle, Content content) { + super(parent, swtStyle); + setData(content); + } + + @Override + public Content getContent() { + return (Content) getData(); + } + +} diff --git a/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/Img.java b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/Img.java index eb52fc6a3..578dac251 100644 --- a/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/Img.java +++ b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/Img.java @@ -6,7 +6,6 @@ import org.argeo.api.acr.spi.ProvidedContent; import org.argeo.api.cms.ux.Cms2DSize; import org.argeo.api.cms.ux.CmsImageManager; import org.argeo.cms.swt.CmsSwtUtils; -import org.argeo.cms.swt.widgets.EditableImage; import org.argeo.cms.ux.acr.ContentPart; import org.argeo.eclipse.ui.specific.CmsFileUpload; import org.eclipse.swt.SWT; @@ -14,39 +13,36 @@ import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; /** An image within the Argeo Text framework */ -public class Img extends EditableImage implements SwtSectionPart, ContentPart { +public class Img extends LinkedControl implements SwtSectionPart, ContentPart { private static final long serialVersionUID = 6233572783968188476L; private final SwtSection section; private final CmsImageManager imageManager; -// private FileUploadHandler currentUploadHandler = null; -// private FileUploadListener fileUploadListener; + + private Cms2DSize preferredImageSize; public Img(Composite parent, int swtStyle, Content imgNode, Cms2DSize preferredImageSize) { this(SwtSection.findSection(parent), parent, swtStyle, imgNode, preferredImageSize, null); -// setStyle(TextStyles.TEXT_IMAGE); } public Img(Composite parent, int swtStyle, Content imgNode) { this(SwtSection.findSection(parent), parent, swtStyle, imgNode, null, null); -// setStyle(TextStyles.TEXT_IMAGE); } public Img(Composite parent, int swtStyle, Content imgNode, CmsImageManager imageManager) { this(SwtSection.findSection(parent), parent, swtStyle, imgNode, null, imageManager); -// setStyle(TextStyles.TEXT_IMAGE); } Img(SwtSection section, Composite parent, int swtStyle, Content imgNode, Cms2DSize preferredImageSize, CmsImageManager imageManager) { - super(parent, swtStyle, preferredImageSize); + super(parent, swtStyle); + this.preferredImageSize = preferredImageSize; this.section = section; - this.imageManager = imageManager != null ? imageManager - : (CmsImageManager) CmsSwtUtils.getCmsView(section).getImageManager(); -// CmsSwtUtils.style(this, TextStyles.TEXT_IMG); + this.imageManager = imageManager != null ? imageManager : CmsSwtUtils.getCmsView(section).getImageManager(); setData(imgNode); } @@ -59,20 +55,23 @@ public class Img extends EditableImage implements SwtSectionPart, ContentPart { } } - @Override - public synchronized void stopEditing() { - super.stopEditing(); -// fileUploadListener = null; - } - - @Override protected synchronized Boolean load(Control lbl) { Content imgNode = getContent(); - boolean loaded = imageManager.load(imgNode, lbl, getPreferredImageSize()); - // getParent().layout(); + boolean loaded = imageManager.load(imgNode, lbl, preferredImageSize, toUri()); return loaded; } + protected Label createLabel(Composite box, String style) { + Label lbl = new Label(box, getStyle()); + // lbl.setLayoutData(CmsUiUtils.fillWidth()); + CmsSwtUtils.markup(lbl); + CmsSwtUtils.style(lbl, style); + if (mouseListener != null) + lbl.addMouseListener(mouseListener); + load(lbl); + return lbl; + } + protected Content getUploadFolder() { return getContent().getParent(); } @@ -141,4 +140,15 @@ public class Img extends EditableImage implements SwtSectionPart, ContentPart { return "Img #" + getPartId(); } + public void setPreferredSize(Cms2DSize size) { + this.preferredImageSize = size; + } + + public Cms2DSize getPreferredImageSize() { + return preferredImageSize; + } + + public void setPreferredImageSize(Cms2DSize preferredImageSize) { + this.preferredImageSize = preferredImageSize; + } } diff --git a/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/LinkedControl.java b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/LinkedControl.java new file mode 100644 index 000000000..6a75dfb2c --- /dev/null +++ b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/LinkedControl.java @@ -0,0 +1,64 @@ +package org.argeo.cms.swt.acr; + +import java.net.URI; + +import org.argeo.api.acr.Content; +import org.argeo.cms.swt.CmsSwtUtils; +import org.argeo.cms.swt.widgets.StyledControl; +import org.eclipse.swt.widgets.Composite; + +/** + * A {@link StyledControl} which can link either to an internal {@link Content} + * or an external URI. + */ +public abstract class LinkedControl extends StyledControl { + + private static final long serialVersionUID = -7603153425459801216L; + + private Content linkedContent; + private URI plainUri; + + public LinkedControl(Composite parent, int swtStyle) { + super(parent, swtStyle); + } + + public void setLink(Content linkedContent) { + if (plainUri != null) + throw new IllegalStateException("An URI is already set"); + this.linkedContent = linkedContent; + } + + public void setLink(URI uri) { + if (linkedContent != null) + throw new IllegalStateException("A linked content is already set"); + this.plainUri = uri; + } + + public boolean isInternalLink() { + if (!hasLink()) + throw new IllegalStateException("No link has been set"); + return linkedContent != null; + } + + public boolean hasLink() { + return plainUri != null || linkedContent != null; + } + + public Content getLinkedContent() { + return linkedContent; + } + + public URI getPlainUri() { + return plainUri; + } + + public URI toUri() { + if (plainUri != null) + return plainUri; + if (linkedContent != null) + return URI.create("#" + CmsSwtUtils.cleanPathForUrl(linkedContent.getPath())); + return null; + + } + +} diff --git a/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/osgi/BundleSvgTheme.java b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/osgi/BundleSvgTheme.java index 1e52001cb..74de83e09 100644 --- a/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/osgi/BundleSvgTheme.java +++ b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/osgi/BundleSvgTheme.java @@ -10,6 +10,7 @@ import java.lang.System.Logger.Level; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import org.apache.batik.transcoder.TranscoderException; import org.apache.batik.transcoder.TranscoderInput; @@ -25,17 +26,29 @@ import org.osgi.framework.BundleContext; public class BundleSvgTheme extends BundleCmsSwtTheme { private final static Logger logger = System.getLogger(BundleSvgTheme.class.getName()); - private Map> imageCache = Collections.synchronizedMap(new HashMap<>()); + private Map> imageDataCache = Collections.synchronizedMap(new HashMap<>()); private Map transcoders = Collections.synchronizedMap(new HashMap<>()); + private final static String IMAGE_CACHE_KEY = BundleSvgTheme.class.getName() + ".imageCache"; + @Override public Image getIcon(String name, Integer preferredSize) { String path = "icons/types/svg/" + name + ".svg"; return createImageFromSvg(path, preferredSize); } + @SuppressWarnings("unchecked") protected Image createImageFromSvg(String path, Integer preferredSize) { + Display display = Display.getCurrent(); + Objects.requireNonNull(display, "Not a user interface thread"); + + Map> imageCache = (Map>) display + .getData(IMAGE_CACHE_KEY); + if (imageCache == null) + display.setData(IMAGE_CACHE_KEY, new HashMap>()); + imageCache = (Map>) display.getData(IMAGE_CACHE_KEY); + Image image = null; if (imageCache.containsKey(path)) { image = imageCache.get(path).get(preferredSize); @@ -43,7 +56,7 @@ public class BundleSvgTheme extends BundleCmsSwtTheme { if (image != null) return image; ImageData imageData = loadFromSvg(path, preferredSize); - image = new Image(Display.getDefault(), imageData); + image = new Image(display, imageData); if (!imageCache.containsKey(path)) imageCache.put(path, Collections.synchronizedMap(new HashMap<>())); imageCache.get(path).put(preferredSize, image); @@ -51,6 +64,12 @@ public class BundleSvgTheme extends BundleCmsSwtTheme { } protected ImageData loadFromSvg(String path, int size) { + ImageData imageData = null; + if (imageDataCache.containsKey(path)) + imageData = imageDataCache.get(path).get(size); + if (imageData != null) + return imageData; + ImageTranscoder transcoder = null; synchronized (this) { transcoder = transcoders.get(size); @@ -62,7 +81,6 @@ public class BundleSvgTheme extends BundleCmsSwtTheme { transcoders.put(size, transcoder); } } - ImageData imageData; try (InputStream in = getResourceAsStream(path); ByteArrayOutputStream out = new ByteArrayOutputStream();) { if (in == null) throw new IllegalArgumentException(path + " not found"); @@ -77,6 +95,11 @@ public class BundleSvgTheme extends BundleCmsSwtTheme { throw new RuntimeException("Cannot transcode SVG " + path, e); } + // cache it + if (!imageDataCache.containsKey(path)) + imageDataCache.put(path, Collections.synchronizedMap(new HashMap<>())); + imageDataCache.get(path).put(size, imageData); + return imageData; } @@ -92,16 +115,16 @@ public class BundleSvgTheme extends BundleCmsSwtTheme { // } } - @Override - public void destroy(BundleContext bundleContext, Map properties) { - Display display = Display.getDefault(); - if (display != null) - for (String path : imageCache.keySet()) { - for (Image image : imageCache.get(path).values()) { - display.syncExec(() -> image.dispose()); - } - } - super.destroy(bundleContext, properties); - } +// @Override +// public void destroy(BundleContext bundleContext, Map properties) { +// Display display = Display.getDefault(); +// if (display != null) +// for (String path : imageCache.keySet()) { +// for (Image image : imageCache.get(path).values()) { +// display.syncExec(() -> image.dispose()); +// } +// } +// super.destroy(bundleContext, properties); +// } } diff --git a/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/CmsLink.java b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/CmsLink.java new file mode 100644 index 000000000..a3bfa8eed --- /dev/null +++ b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/CmsLink.java @@ -0,0 +1,238 @@ +package org.argeo.cms.swt.widgets; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; + +import org.argeo.api.cms.CmsLog; +import org.argeo.api.cms.ux.CmsStyle; +import org.argeo.cms.swt.CmsSwtUtils; +import org.eclipse.rap.rwt.RWT; +import org.eclipse.rap.rwt.service.ResourceManager; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.MouseListener; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.osgi.framework.BundleContext; + +/** A link to an internal or external location. */ +public class CmsLink { + private final static CmsLog log = CmsLog.getLog(CmsLink.class); + private BundleContext bundleContext; + + private String label; + private String style; + private String target; + private String image; + private boolean openNew = false; + private MouseListener mouseListener; + + private int horizontalAlignment = SWT.CENTER; + private int verticalAlignment = SWT.CENTER; + + // internal + // private Boolean isUrl = false; + private Integer imageWidth, imageHeight; + + public CmsLink() { + super(); + } + + public CmsLink(String label, String target) { + this(label, target, (String) null); + } + + public CmsLink(String label, String target, CmsStyle style) { + this(label, target, style != null ? style.style() : null); + } + + public CmsLink(String label, String target, String style) { + super(); + this.label = label; + this.target = target; + this.style = style; + init(); + } + + public void init() { + if (image != null) { + ImageData image = loadImage(); + if (imageHeight == null && imageWidth == null) { + imageWidth = image.width; + imageHeight = image.height; + } else if (imageHeight == null) { + imageHeight = (imageWidth * image.height) / image.width; + } else if (imageWidth == null) { + imageWidth = (imageHeight * image.width) / image.height; + } + } + } + + /** @return {@link Composite} with a single {@link Label} child. */ + public Control createUi(Composite parent) { +// if (image != null && (imageWidth == null || imageHeight == null)) { +// throw new CmsException("Image is not properly configured." +// + " Make sure bundleContext property is set and init() method has been called."); +// } + + Composite comp = new Composite(parent, SWT.NONE); + comp.setLayout(CmsSwtUtils.noSpaceGridLayout()); + + Label link = new Label(comp, SWT.NONE); + CmsSwtUtils.markup(link); + GridData layoutData = new GridData(horizontalAlignment, verticalAlignment, false, false); + if (image != null) { + if (imageHeight != null) + layoutData.heightHint = imageHeight; + if (label == null) + if (imageWidth != null) + layoutData.widthHint = imageWidth; + } + + link.setLayoutData(layoutData); + CmsSwtUtils.style(comp, style != null ? style : getDefaultStyle()); + CmsSwtUtils.style(link, style != null ? style : getDefaultStyle()); + + // label + StringBuilder labelText = new StringBuilder(); + if (target != null) { + labelText.append(""); + } + if (image != null) { + registerImageIfNeeded(); + String imageLocation = RWT.getResourceManager().getLocation(image); + labelText.append(""); + + } + + if (label != null) { + labelText.append(' ').append(label); + } + + if (target != null) + labelText.append(""); + + link.setText(labelText.toString()); + + if (mouseListener != null) + link.addMouseListener(mouseListener); + + return comp; + } + + private void registerImageIfNeeded() { + ResourceManager resourceManager = RWT.getResourceManager(); + if (!resourceManager.isRegistered(image)) { + URL res = getImageUrl(); + try (InputStream inputStream = res.openStream()) { + resourceManager.register(image, inputStream); + if (log.isTraceEnabled()) + log.trace("Registered image " + image); + } catch (IOException e) { + throw new RuntimeException("Cannot load image " + image, e); + } + } + } + + private ImageData loadImage() { + URL url = getImageUrl(); + ImageData result = null; + try (InputStream inputStream = url.openStream()) { + result = new ImageData(inputStream); + if (log.isTraceEnabled()) + log.trace("Loaded image " + image); + } catch (IOException e) { + throw new RuntimeException("Cannot load image " + image, e); + } + return result; + } + + private URL getImageUrl() { + URL url; + try { + // pure URL + url = new URL(image); + } catch (MalformedURLException e1) { + url = bundleContext.getBundle().getResource(image); + } + + if (url == null) + throw new IllegalStateException("No image " + image + " available."); + + return url; + } + + public void setBundleContext(BundleContext bundleContext) { + this.bundleContext = bundleContext; + } + + public void setLabel(String label) { + this.label = label; + } + + public void setStyle(String style) { + this.style = style; + } + + /** @deprecated Use {@link #setStyle(String)} instead. */ + @Deprecated + public void setCustom(String custom) { + this.style = custom; + } + + public void setTarget(String target) { + this.target = target; + } + + public void setImage(String image) { + this.image = image; + } + + public void setMouseListener(MouseListener mouseListener) { + this.mouseListener = mouseListener; + } + + public void setvAlign(String vAlign) { + if ("bottom".equals(vAlign)) { + verticalAlignment = SWT.BOTTOM; + } else if ("top".equals(vAlign)) { + verticalAlignment = SWT.TOP; + } else if ("center".equals(vAlign)) { + verticalAlignment = SWT.CENTER; + } else { + throw new IllegalArgumentException( + "Unsupported vertical alignment " + vAlign + " (must be: top, bottom or center)"); + } + } + + public void setImageWidth(Integer imageWidth) { + this.imageWidth = imageWidth; + } + + public void setImageHeight(Integer imageHeight) { + this.imageHeight = imageHeight; + } + + public void setOpenNew(boolean openNew) { + this.openNew = openNew; + } + + protected String getDefaultStyle() { + return "link"; +// return SimpleStyle.link.name(); + } +} diff --git a/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/EditableImage.java b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/EditableImage.java index e712e2fe6..1800a712a 100644 --- a/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/EditableImage.java +++ b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/EditableImage.java @@ -12,6 +12,7 @@ import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Text; /** A stylable and editable image. */ +@Deprecated public abstract class EditableImage extends StyledControl { private static final long serialVersionUID = -5689145523114022890L; private final static CmsLog log = CmsLog.getLog(EditableImage.class); @@ -73,9 +74,9 @@ public abstract class EditableImage extends StyledControl { loaded = true; if (control != null) { ((Label) control).setText(imgTag); - control.setSize(preferredImageSize != null - ? new Point(preferredImageSize.getWidth(), preferredImageSize.getHeight()) - : getSize()); + control.setSize( + preferredImageSize != null ? new Point(preferredImageSize.width(), preferredImageSize.height()) + : getSize()); } else { loaded = false; } diff --git a/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/EditableText.java b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/EditableText.java index 0612e8f9b..6ba03e86e 100644 --- a/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/EditableText.java +++ b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/EditableText.java @@ -21,6 +21,14 @@ public class EditableText extends StyledControl { private boolean useTextAsLabel = false; + /** + * Message to display if there is not value. Only used with SWT.FLAT (label + * displayed with a {@link Text}) + * + * @see Text#setMessage(String) + */ + private String message; + public EditableText(Composite parent, int style) { super(parent, style); editable = !(SWT.READ_ONLY == (style & SWT.READ_ONLY)); @@ -54,9 +62,11 @@ public class EditableText extends StyledControl { } protected Text createTextLabel(Composite box, String style) { - Text lbl = new Text(box, getStyle() | (multiLine ? SWT.MULTI : SWT.SINGLE)); + Text lbl = new Text(box, getStyle() | (multiLine ? SWT.MULTI | SWT.WRAP : SWT.SINGLE)); lbl.setEditable(false); - lbl.setLayoutData(CmsSwtUtils.fillWidth()); + if (message != null) + lbl.setMessage(message); + lbl.setLayoutData(multiLine ? CmsSwtUtils.fillAll() : CmsSwtUtils.fillWidth()); if (style != null) CmsSwtUtils.style(lbl, style); CmsSwtUtils.markup(lbl); @@ -68,14 +78,15 @@ public class EditableText extends StyledControl { protected Text createText(Composite box, String style, boolean editable) { highlight = new Composite(box, SWT.NONE); highlight.setBackground(highlightColor); - GridData highlightGd = new GridData(SWT.FILL, SWT.FILL, false, false); + GridData highlightGd = new GridData(SWT.FILL, SWT.FILL, false, multiLine); highlightGd.widthHint = 5; - highlightGd.heightHint = 3; + if (!multiLine) + highlightGd.heightHint = 3; highlight.setLayoutData(highlightGd); final Text text = new Text(box, getStyle() | (multiLine ? SWT.MULTI : SWT.SINGLE) | SWT.WRAP); text.setEditable(editable); - GridData textLayoutData = CmsSwtUtils.fillWidth(); + GridData textLayoutData = multiLine ? CmsSwtUtils.fillAll() : CmsSwtUtils.fillWidth(); // textLayoutData.heightHint = preferredHeight; text.setLayoutData(textLayoutData); if (style != null) @@ -132,4 +143,31 @@ public class EditableText extends StyledControl { this.useTextAsLabel = useTextAsLabel; } + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + Control control = getControl(); + if (control != null && control instanceof Text txt) + txt.setMessage(this.message); + } + + @Override + protected void setContainerLayoutData(Composite composite) { + if (multiLine) + composite.setLayoutData(CmsSwtUtils.fillAll()); + else + super.setContainerLayoutData(composite); + } + + @Override + protected void setControlLayoutData(Control control) { +// if (multiLine) +// control.setLayoutData(CmsSwtUtils.fillAll()); +// else + super.setControlLayoutData(control); + } + } diff --git a/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/StyledControl.java b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/StyledControl.java index 82c04a26c..9370b52c5 100644 --- a/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/StyledControl.java +++ b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/StyledControl.java @@ -91,6 +91,10 @@ public abstract class StyledControl extends Composite implements SwtEditablePart setStyle(style.style()); } + /** + * Set the style, creating all related controls and composites. It should be + * called after all properties have been set. + */ public void setStyle(String style) { Object currentStyle = null; if (control != null) @@ -107,6 +111,18 @@ public abstract class StyledControl extends Composite implements SwtEditablePart } } + /** + * Convenience method when no style is explicitly set, so that the control can + * effectively be created. Does nothing if a control already exists, otherwise + * it is equivalent to {@link #setStyle(String)} with a null + * argument. + */ + public void initControl() { + if (control != null) + return; + setStyle((String) null); + } + /** To be overridden */ protected void setControlLayoutData(Control control) { control.setLayoutData(CmsSwtUtils.fillWidth()); diff --git a/swt/rap/org.argeo.cms.swt.rap/src/org/argeo/cms/web/CmsWebEntryPoint.java b/swt/rap/org.argeo.cms.swt.rap/src/org/argeo/cms/web/CmsWebEntryPoint.java index 4d91cf8e2..216dc3654 100644 --- a/swt/rap/org.argeo.cms.swt.rap/src/org/argeo/cms/web/CmsWebEntryPoint.java +++ b/swt/rap/org.argeo.cms.swt.rap/src/org/argeo/cms/web/CmsWebEntryPoint.java @@ -271,25 +271,24 @@ public class CmsWebEntryPoint extends AbstractSwtCmsView implements EntryPoint, return null; } }); - } catch (Throwable e) { - if (e instanceof SWTError) { - SWTError swtError = (SWTError) e; - if (swtError.code == SWT.ERROR_FUNCTION_DISPOSED) { - log.error("Unexpected SWT error in event loop, ignoring it. " + e.getMessage()); - continue eventLoop; - } else { - log.error("Unexpected SWT error in event loop, shutting down...", e); - break eventLoop; - } - } else if (e instanceof ThreadDeath) { - throw (ThreadDeath) e; - } else if (e instanceof Error) { - log.error("Unexpected error in event loop, shutting down...", e); - break eventLoop; - } else { - log.error("Unexpected exception in event loop, ignoring it. " + e.getMessage()); + } catch (SWTError e) { + SWTError swtError = (SWTError) e; + if (swtError.code == SWT.ERROR_FUNCTION_DISPOSED) { + log.error("Unexpected SWT error in event loop, ignoring it. " + e.getMessage()); continue eventLoop; + } else { + log.error("Unexpected SWT error in event loop, shutting down...", e); + break eventLoop; } + } catch (ThreadDeath e) { + // ThreadDeath is expected when the UI thread terminates + throw (ThreadDeath) e; + } catch (Error e) { + log.error("Unexpected error in event loop, shutting down...", e); + break eventLoop; + } catch (Throwable e) { + log.error("Unexpected exception in event loop, ignoring it. " + e.getMessage()); + continue eventLoop; } } if (!display.isDisposed()) diff --git a/swt/rap/org.argeo.swt.specific.rap/src/org/argeo/eclipse/ui/specific/CmsFileDialog.java b/swt/rap/org.argeo.swt.specific.rap/src/org/argeo/eclipse/ui/specific/CmsFileDialog.java deleted file mode 100644 index 6100c1a83..000000000 --- a/swt/rap/org.argeo.swt.specific.rap/src/org/argeo/eclipse/ui/specific/CmsFileDialog.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.argeo.eclipse.ui.specific; - -import org.eclipse.swt.widgets.FileDialog; -import org.eclipse.swt.widgets.Shell; - -public class CmsFileDialog extends FileDialog { - private static final long serialVersionUID = -7540791204102318801L; - - public CmsFileDialog(Shell parent, int style) { - super(parent, style); - } - - public CmsFileDialog(Shell parent) { - super(parent); - } - -} diff --git a/swt/rcp/org.argeo.cms.swt.rcp/bnd.bnd b/swt/rcp/org.argeo.cms.swt.rcp/bnd.bnd index e1735f03c..6f3758250 100644 --- a/swt/rcp/org.argeo.cms.swt.rcp/bnd.bnd +++ b/swt/rcp/org.argeo.cms.swt.rcp/bnd.bnd @@ -3,6 +3,7 @@ Bundle-SymbolicName: org.argeo.cms.swt.rcp;singleton=true Import-Package:\ org.argeo.cms.auth,\ org.eclipse.swt,\ +org.eclipse.swt.widgets,\ org.eclipse.swt.graphics,\ org.w3c.css.sac,\ org.freedesktop.dbus.connections,\