Merge tag 'v2.3.18' into testing
authorMathieu Baudier <mbaudier@argeo.org>
Thu, 29 Jun 2023 03:35:07 +0000 (05:35 +0200)
committerMathieu Baudier <mbaudier@argeo.org>
Thu, 29 Jun 2023 03:35:07 +0000 (05:35 +0200)
63 files changed:
Makefile
org.argeo.api.acr/src/org/argeo/api/acr/AttributeFormatter.java
org.argeo.api.acr/src/org/argeo/api/acr/Content.java
org.argeo.api.acr/src/org/argeo/api/acr/ContentName.java
org.argeo.api.acr/src/org/argeo/api/acr/ContentSession.java
org.argeo.api.acr/src/org/argeo/api/acr/CrAttributeType.java
org.argeo.api.acr/src/org/argeo/api/acr/NamespaceUtils.java
org.argeo.api.acr/src/org/argeo/api/acr/QNamed.java
org.argeo.api.acr/src/org/argeo/api/acr/RuntimeNamespaceContext.java
org.argeo.api.acr/src/org/argeo/api/acr/search/AndFilter.java
org.argeo.api.acr/src/org/argeo/api/acr/search/BasicSearch.java
org.argeo.api.acr/src/org/argeo/api/acr/search/Composition.java
org.argeo.api.acr/src/org/argeo/api/acr/search/Constraint.java
org.argeo.api.acr/src/org/argeo/api/acr/search/ContentFilter.java
org.argeo.api.acr/src/org/argeo/api/acr/search/Intersection.java
org.argeo.api.acr/src/org/argeo/api/acr/search/OrFilter.java
org.argeo.api.acr/src/org/argeo/api/acr/search/Union.java
org.argeo.api.acr/src/org/argeo/api/acr/spi/ContentProvider.java
org.argeo.api.acr/src/org/argeo/api/acr/spi/ProvidedContent.java
org.argeo.api.acr/src/org/argeo/api/acr/spi/ProvidedSession.java
org.argeo.api.cms/src/org/argeo/api/cms/CmsSession.java
org.argeo.api.cms/src/org/argeo/api/cms/ux/Cms2DSize.java
org.argeo.api.cms/src/org/argeo/api/cms/ux/CmsImageManager.java
org.argeo.api.cms/src/org/argeo/api/cms/ux/CmsView.java
org.argeo.api.uuid/src/org/argeo/api/uuid/UuidIdentified.java [new file with mode: 0644]
org.argeo.cms.ux/src/org/argeo/cms/ux/AbstractCmsEditable.java
org.argeo.cms.ux/src/org/argeo/cms/ux/AbstractImageManager.java
org.argeo.cms.ux/src/org/argeo/cms/ux/CmsUxUtils.java
org.argeo.cms.ux/src/org/argeo/cms/ux/acr/ContentCmsEditable.java [new file with mode: 0644]
org.argeo.cms.ux/src/org/argeo/cms/ux/acr/ContentHierarchicalPart.java
org.argeo.cms.ux/src/org/argeo/cms/ux/acr/ContentPart.java
org.argeo.cms/OSGI-INF/cmsAcrHttpHandler.xml
org.argeo.cms/src/org/argeo/cms/acr/AbstractContent.java
org.argeo.cms/src/org/argeo/cms/acr/AbstractSimpleContentProvider.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/acr/CmsContentRepository.java
org.argeo.cms/src/org/argeo/cms/acr/CmsContentSession.java
org.argeo.cms/src/org/argeo/cms/acr/ContentUtils.java
org.argeo.cms/src/org/argeo/cms/acr/SvgAttrs.java
org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContentProvider.java
org.argeo.cms/src/org/argeo/cms/acr/fs/FsContent.java
org.argeo.cms/src/org/argeo/cms/acr/xml/DomContent.java
org.argeo.cms/src/org/argeo/cms/acr/xml/DomContentProvider.java
org.argeo.cms/src/org/argeo/cms/auth/RemoteAuthUtils.java
org.argeo.cms/src/org/argeo/cms/dav/DavHttpHandler.java
org.argeo.cms/src/org/argeo/cms/internal/auth/CmsSessionImpl.java
org.argeo.cms/src/org/argeo/cms/internal/runtime/DeployedContentRepository.java
sdk/branches/testing.bnd
sdk/branches/unstable.bnd
swt/org.argeo.cms.swt/src/org/argeo/cms/swt/AbstractSwtImageManager.java
swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/AbstractPageViewer.java
swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/AcrSwtImageManager.java
swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/ContentComposite.java
swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/ContentStyledControl.java [new file with mode: 0644]
swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/Img.java
swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/LinkedControl.java [new file with mode: 0644]
swt/org.argeo.cms.swt/src/org/argeo/cms/swt/osgi/BundleSvgTheme.java
swt/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/CmsLink.java [new file with mode: 0644]
swt/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/EditableImage.java
swt/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/EditableText.java
swt/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/StyledControl.java
swt/rap/org.argeo.cms.swt.rap/src/org/argeo/cms/web/CmsWebEntryPoint.java
swt/rap/org.argeo.swt.specific.rap/src/org/argeo/eclipse/ui/specific/CmsFileDialog.java [deleted file]
swt/rcp/org.argeo.cms.swt.rcp/bnd.bnd

index 8d53957244ebbfde19c06ac59ef24b137cdc80b9..ff2c9ccad68e4d1555b9140ec9fce5c65c75c83d 100644 (file)
--- 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) \
index c7023f1a37a4a58b2582fc62e4f9f7d509fb7336..9f338965f829d2781649cb7fe192f9e2afa11a28 100644 (file)
@@ -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
  * <code>parse(obj.toString()).equals(obj)</code> is verified.
@@ -9,7 +11,15 @@ package org.argeo.api.acr;
  */
 public interface AttributeFormatter<T> {
        /** 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) {
index f52ab31b8f25223e109b4781707502f924cd5ebd..df5c149e6fc1fc696187d197766ad5f9cf0c2c52 100644 (file)
@@ -16,6 +16,8 @@ import javax.xml.namespace.QName;
  * A semi-structured content, with attributes, within a hierarchical structure.
  */
 public interface Content extends Iterable<Content>, Map<QName, Object> {
+       /** The base of a repository path. */
+       String ROOT_PATH = "/";
 
        QName getName();
 
@@ -82,25 +84,11 @@ public interface Content extends Iterable<Content>, Map<QName, Object> {
                }
                List<A> 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<Content> edit(Consumer<Content> 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<Content>, Map<QName, Object> {
                return res;
        }
 
+       default List<Content> children(QNamed name) {
+               return children(name.qName());
+       }
+
+       default Optional<Content> soleChild(QNamed name) {
+               return soleChild(name.qName());
+       }
+
        default Optional<Content> soleChild(QName name) {
                List<Content> res = children(name);
                if (res.isEmpty())
@@ -242,29 +238,51 @@ public interface Content extends Iterable<Content>, Map<QName, Object> {
        /*
         * 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 <code>null</code>.
+        * 
+        * @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 <code>null</code>.
+        * 
+        * @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 <code>null</code>.
+        * 
+        * @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> A get(Object key, Class<A> 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<Content> getContent(String path);
 
        /*
         * EXPERIMENTAL UNSUPPORTED
index 113f98da0d6350d07279d261e2a99b443b25541a..92cd795ce6191f869c7437c142bc2a7c307ddd17 100644 (file)
@@ -83,7 +83,7 @@ public class ContentName extends QName {
         */
 
        @Override
-       public String toString() {
+       public final String toString() {
                return toQNameString();
        }
 
index b8ecd98da97ae29b548fffef0cb44f01ad668d32..7a6e67981d71899643bd5ad53bc115b530877a1f 100644 (file)
@@ -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;
index 3e12fb1c87067fd188bfa34cac4c44794c9ec5d2..3e0dddee4c55ed6cf6f6f81e836d8941dca3fd52 100644 (file)
@@ -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 <T> Optional<T> cast(Class<T> 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 <T> Optional<T> cast(NamespaceContext namespaceContext, Class<T> 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<Integer> {
                @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<Long> {
                @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<Double> {
 
                @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<Instant> {
 
                @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<UUID> {
 
                @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<URI> {
 
                @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<QName> {
+
+               @Override
+               public QName parse(NamespaceContext namespaceContext, String str) throws IllegalArgumentException {
+                       return NamespaceUtils.parsePrefixedName(namespaceContext, str);
+               }
+
+       }
+
        static class StringFormatter implements AttributeFormatter<String> {
 
                @Override
-               public String parse(String str) {
+               public String parse(NamespaceContext namespaceContext, String str) {
                        return str;
                }
 
index df582868b349798da88d124264d5f846c432feec..e845560b032cc261fb4485e0fdc19b32d3a8456d 100644 (file)
@@ -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> QNAME_COMPARATOR = new Comparator<QName>() {
+
+               @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> QNAME_COMPARATOR = new Comparator<QName>() {
-
-               @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<String> 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<String, String> 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<String, String> 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<String> getPrefixes(Function<String, Set<String>> mapping, String namespaceURI) {
                Iterator<String> standard = NamespaceUtils.getStandardPrefixes(namespaceURI);
                if (standard != null)
index 063a7d32175f1d0e0a949964720c35a239464f68..73ae4f02ead43b8c69038712e6e3a17477090ffe 100644 (file)
@@ -23,6 +23,7 @@ public interface QNamed extends Supplier<String> {
                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<String> {
                        return XMLConstants.DEFAULT_NS_PREFIX;
                }
 
+               @Override
+               default String get() {
+                       return localName();
+               }
+
        }
 }
index 1c55156ee352b389c387c56f2ea30103dae2371c..fe50fd0c3b58058c55a16b0c3feef24486f64510 100644 (file)
@@ -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<String, String> prefixes = new TreeMap<>();
        private NavigableMap<String, String> 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<String> getPrefixes(String namespaceURI) {
+       public Iterator<String> 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<String, String> 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<String, String> prefixes = INSTANCE.prefixes;
                NavigableMap<String, String> 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);
        }
 }
index e58b212367edb68ba2156b3a51a8253ed05f1945..d3880d899e7346cbddceea1fd662e7d528202c22 100644 (file)
@@ -1,5 +1,6 @@
 package org.argeo.api.acr.search;
 
+/** AND filter based on the intersection composition. */
 public class AndFilter extends ContentFilter<Intersection> {
 
        public AndFilter() {
index 8028f5d2033e8d5138bf23aaca90a029b2e2f4ce..8cbdebf7e5d5c8e6614dea460a739e59328158d1 100644 (file)
@@ -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<QName> 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(), ""));
+//     }
 }
index 80786f462f403a86597706a9728e741ed434a7dc..622d8c268e5b65d56bf34b08d70d6ac694b5bd07 100644 (file)
@@ -1,4 +1,5 @@
 package org.argeo.api.acr.search;
+
+/** Marker interface for a composition of multiple constraints. */
 interface Composition {
 }
-
index fc4313d7a0b69648e42525902a96dca34514d532..36c98f7a5f9a6b511d2bb095dc010adb092eda7c 100644 (file)
@@ -1,4 +1,5 @@
 package org.argeo.api.acr.search;
 
+/** Marker interface for a constraint. */
 public interface Constraint {
 }
index c5f5fc607b634a9b10447da8303aede30f3f3f60..45f2d848c4183754c3935e73bdc77d0c1f5d4590 100644 (file)
@@ -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<COMPOSITION extends Composition> implements Constraint {
        private Set<Constraint> constraintss = new HashSet<>();
 
@@ -167,17 +167,17 @@ public abstract class ContentFilter<COMPOSITION extends Composition> 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, "");
+//             });
+//
+//     }
 
 }
index 5fff2ae8868867f4a82fa95055bfe3363298b22b..44ca906056e7df060c15c71008614cda315008f7 100644 (file)
@@ -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<Intersection> filter;
 
        @SuppressWarnings("unchecked")
@@ -12,4 +14,3 @@ class Intersection implements Composition {
        }
 
 }
-
index 40460d499fd255032a047270a68ce52fee47578e..80a1568be934dc02dfb9b3d48ed3bf3ef25047cc 100644 (file)
@@ -1,5 +1,6 @@
 package org.argeo.api.acr.search;
 
+/** OR filter based on the union composition. */
 public class OrFilter extends ContentFilter<Union> {
 
        public OrFilter() {
index d4b342b5fcff09cab39fc577fd1fb1cef31e1c3e..d33b13da2fd7d6050d27b7993abf551a2f44604f 100644 (file)
@@ -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<Union> filter;
 
        @SuppressWarnings("unchecked")
index 25b9be5c2081b924b7f08a0d4f0ce72b8a5e1559..e354672a781c77adc1a09a009f42a3f2264c71b1 100644 (file)
@@ -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 <code>null</code>
+        * @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<Content> 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<Content> 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);
-//     }
-
 }
index e2807c0efd19e746c7a9ab41a2971e36c1fe55e9..f1e5aaaa80f24af4dce505a935b342645d9910c8 100644 (file)
@@ -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 <code>{@link #getDepth()} == 0</code>, 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<Content> 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;
        }
 }
index 5a538b57a45ec4593aca8a9267e67570393169b2..6d0dfbb245b323db00063472aa8a8c161cd790d9 100644 (file)
@@ -20,7 +20,7 @@ public interface ProvidedSession extends ContentSession {
 
        void notifyModification(ProvidedContent content);
 
-       UUID getUuid();
+       UUID uuid();
 
 //     Content getSessionRunDir();
 
index dda1dac1f7fa118ae4247b93e21bf7963c11e689..b69e54f98a5cd7f7ea59064ba2cb9d0edfeb4d38 100644 (file)
@@ -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();
 
index 1ec753a187770d4d6f67c02476754dadc6a05a9e..f35e98c5022bb730b343d4e4a1b003c5c0aef0ac 100644 (file)
@@ -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 + "]";
        }
-
 }
index 1ec54a9d9666534d571100bff10ecc166ec2b544..4fb393a569cc74aa6bd78ce8fda5041cc45b002e 100644 (file)
@@ -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<V, M> {
        /** 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);
index 15b6a5dc7aac338a7180f748fa404c31f4f28bb0..a36baf8e08b642cd2c7b625162494ae9710b9d14 100644 (file)
@@ -31,7 +31,7 @@ public interface CmsView {
        // SERVICES
        void exception(Throwable e);
 
-       CmsImageManager<?, ?> getImageManager();
+       <V,M> CmsImageManager<V, M> 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 (file)
index 0000000..77cab37
--- /dev/null
@@ -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 <code>uuid</code> 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}.<br/>
+        * 
+        * <pre>
+        * &#64;Override
+        * public boolean equals(Object o) {
+        *      return UuidIdentified.equals(this, o);
+        * }
+        * </pre>
+        */
+       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}.<br/>
+        * 
+        * <pre>
+        * &#64;Override
+        * public int hashCode() {
+        *      return UuidIdentified.hashCode(this);
+        * }
+        * </pre>
+        */
+       static int hashCode(UuidIdentified uuidIdentified) {
+               assert uuidIdentified != null;
+               return uuidIdentified.getUuid().hashCode();
+       }
+
+}
index a1cd3d90e072e99b2209c98b8959c26141c8ed24..96c15bbca95660cbf7ec2b156d2ad0ab721b1278 100644 (file)
@@ -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<CmsEditionListener, Object> 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())
index 41c905ef6c56b18199b697ffc89e6525b1b6e225..31a0d6bed044288176d03166f6cb1a1fe36606e6 100644 (file)
@@ -10,16 +10,16 @@ public abstract class AbstractImageManager<V, M> implements CmsImageManager<V, M
        public final static Float NO_IMAGE_RATIO = 1f;
 
        protected Cms2DSize resizeTo(Cms2DSize orig, Cms2DSize constraints) {
-               if (constraints.getWidth() != 0 && constraints.getHeight() != 0) {
+               if (constraints.width() != 0 && constraints.height() != 0) {
                        return constraints;
-               } else if (constraints.getWidth() == 0 && constraints.getHeight() == 0) {
+               } else if (constraints.width() == 0 && constraints.height() == 0) {
                        return orig;
-               } else if (constraints.getHeight() == 0) {// force width
-                       return new Cms2DSize(constraints.getWidth(),
-                                       scale(orig.getHeight(), orig.getWidth(), constraints.getWidth()));
-               } else if (constraints.getWidth() == 0) {// force height
-                       return new Cms2DSize(scale(orig.getWidth(), orig.getHeight(), constraints.getHeight()),
-                                       constraints.getHeight());
+               } else if (constraints.height() == 0) {// force width
+                       return new Cms2DSize(constraints.width(),
+                                       scale(orig.height(), orig.width(), constraints.width()));
+               } else if (constraints.width() == 0) {// force height
+                       return new Cms2DSize(scale(orig.width(), orig.height(), constraints.height()),
+                                       constraints.height());
                }
                throw new IllegalArgumentException("Cannot resize " + orig + " to " + constraints);
        }
@@ -48,7 +48,7 @@ public abstract class AbstractImageManager<V, M> implements CmsImageManager<V, M
        /** @return null if not available */
        @Override
        public StringBuilder getImageTagBuilder(M node, Cms2DSize size) {
-               return getImageTagBuilder(node, Integer.toString(size.getWidth()), Integer.toString(size.getHeight()));
+               return getImageTagBuilder(node, Integer.toString(size.width()), Integer.toString(size.height()));
        }
 
        /** @return null if not available */
index 087b4ff7c725116eb8fe4f8be1b5536d9bb23681..84e471aafbe2103403a24ed7535c941469f0ff36 100644 (file)
@@ -31,6 +31,6 @@ public class CmsUxUtils {
        }
 
        public static String img(String src, Cms2DSize size) {
-               return img(src, Integer.toString(size.getWidth()), Integer.toString(size.getHeight()));
+               return img(src, Integer.toString(size.width()), Integer.toString(size.height()));
        }
 }
diff --git a/org.argeo.cms.ux/src/org/argeo/cms/ux/acr/ContentCmsEditable.java b/org.argeo.cms.ux/src/org/argeo/cms/ux/acr/ContentCmsEditable.java
new file mode 100644 (file)
index 0000000..7278c28
--- /dev/null
@@ -0,0 +1,48 @@
+package org.argeo.cms.ux.acr;
+
+import org.argeo.api.acr.Content;
+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.ux.CmsEditable;
+import org.argeo.cms.acr.ContentUtils;
+import org.argeo.cms.ux.AbstractCmsEditable;
+
+/** {@link CmsEditable} semantics for a {@link Content}. */
+public class ContentCmsEditable extends AbstractCmsEditable {
+
+       private final boolean canEdit;
+       /** The path of this content, relative to its content provider. */
+       private final String relativePath;
+       private final ProvidedSession session;
+       private final ContentProvider provider;
+
+       public ContentCmsEditable(Content content) {
+               ProvidedContent providedContent = (ProvidedContent) content;
+               canEdit = providedContent.canEdit();
+               session = providedContent.getSession();
+               provider = providedContent.getProvider();
+               relativePath = ContentUtils.relativize(provider.getMountPath(), content.getPath());
+       }
+
+       @Override
+       public Boolean canEdit() {
+               return canEdit;
+       }
+
+       @Override
+       public Boolean isEditing() {
+               return provider.isOpenForEdit(session, relativePath);
+       }
+
+       @Override
+       public void startEditing() {
+               provider.openForEdit(session, relativePath);
+       }
+
+       @Override
+       public void stopEditing() {
+               provider.freeze(session, relativePath);
+       }
+
+}
index baaa25238ebb0f39a33871cbb67ab99270ddb8ea..bb9c7422d3189cb973665bf1fad9d26fa546d1ea 100644 (file)
@@ -8,15 +8,16 @@ import org.argeo.api.acr.Content;
 import org.argeo.cms.ux.widgets.AbstractHierarchicalPart;
 import org.argeo.cms.ux.widgets.HierarchicalPart;
 
+/** A {@link HierarchicalPart} based on {@link Content}. */
 public class ContentHierarchicalPart extends AbstractHierarchicalPart<Content> implements HierarchicalPart<Content> {
        @Override
-       public List<Content> getChildren(Content content) {
+       public List<Content> getChildren(Content parent) {
                List<Content> res = new ArrayList<>();
-               if (isLeaf(content))
+               if (isLeaf(parent))
                        return res;
-               if (content == null)
+               if (parent == null)
                        return res;
-               for (Iterator<Content> it = content.iterator(); it.hasNext();) {
+               for (Iterator<Content> it = parent.iterator(); it.hasNext();) {
                        res.add(it.next());
                }
 
index 0a0ad0ea5e6eb2c81a96417bf92ed05f360e056a..db8a746fcf5d4be79b614c7c7f2a29b66525e2a3 100644 (file)
@@ -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();
+//     }
 
 }
index afd4e0aaf06f6f559301c717226df04ca78cf6e6..39400d99ae4286a3ce5f83dfadbefaf51be3f0c7 100644 (file)
@@ -5,5 +5,6 @@
       <provide interface="com.sun.net.httpserver.HttpHandler"/>
    </service>
    <property name="context.path" type="String" value="/api/acr/" />
+   <property name="context.public" type="String" value="true" />
    <reference bind="setContentRepository" cardinality="1..1" interface="org.argeo.api.acr.spi.ProvidedRepository" name="ProvidedRepository" policy="static"/>
 </scr:component>
index 16f39609e8aafb1c9185dec0a274ef74431cdedf..1acdcc3809ba3914a4c256fb814baed41ce3d880 100644 (file)
@@ -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<QName, Object> impleme
        }
 
        /*
-        * ATTRIBUTES OPERATIONS
+        * ATTRIBUTES MAP IMPLEMENTATION
         */
-//     protected abstract Iterable<QName> keys();
-//
-//     protected abstract void removeAttr(QName key);
-
        @Override
        public Set<Entry<QName, Object>> entrySet() {
                Set<Entry<QName, Object>> 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<QName, Object> impleme
                if (value == null)
                        return new ArrayList<>();
                if (value instanceof List) {
-                       if (isDefaultAttrTypeRequested(clss))
+                       if (clss.isAssignableFrom(Object.class))
                                return (List<A>) value;
                        List<A> 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<QName, Object> 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<QName, Object> 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<QName, Object> impleme
 
        @Override
        public <A> Optional<A> get(QName key, Class<A> 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 (file)
index 0000000..f07a08d
--- /dev/null
@@ -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<SERVICE> 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<Content> firstLevel(ProvidedSession session);
+
+       /**
+        * Retrieve the content at this relative path. Root content is already dealt
+        * with.
+        */
+       protected abstract ProvidedContent get(ProvidedSession session, List<String> segments);
+
+       @Override
+       public final ProvidedContent get(ProvidedSession session, String relativePath) {
+               List<String> segments = ContentUtils.toPathSegments(relativePath);
+               if (segments.size() == 0)
+                       return new ServiceContent(session);
+               return get(session, segments);
+       }
+
+       public void start(Map<String, String> 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<String> 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<String> 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<Content> iterator() {
+                       return firstLevel(getSession());
+               }
+
+       }
+
+}
index 89e725043923694498846d06b0307fd9d454fa50..15b893bb3d3ade0aab47e084fd0ace6316511910 100644 (file)
@@ -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();
index af7dca046ab636b014c7c601f0908ff3060661a6..daefe9835e371c4210bf6487ce108ac04700e05d 100644 (file)
@@ -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
         */
index d324ac475283530ae658498f0bce39f305897225..bf5954411bc157a32003bf17d3e4363e8ee9d5bc 100644 (file)
@@ -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<String> toPathSegments(String path) {
                List<String> 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() {
 
index 61d8e04298971802987b1a1a1f99f0e50ba57d82..18527f5909759d3960fc454dab839c028f3071f3 100644 (file)
@@ -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,
index 8b6eb6bbd4b2d8e73413dfa5aaae82383f44a472..ab84aef37131b4a780fb92644f9ed4a96754161f 100644 (file)
@@ -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<CmsUserManager> {
 
-       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<String> mountSegments = ContentUtils.toPathSegments(mountPath);
-               this.mountName = mountSegments.get(mountSegments.size() - 1);
-               this.userManager = userManager;
+       @Override
+       protected Iterator<Content> firstLevel(ProvidedSession session) {
+               List<Content> 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<String> segments = ContentUtils.toPathSegments(relativePath);
-               if (segments.size() == 0)
-                       return new UserManagerContent(session);
+       public ProvidedContent get(ProvidedSession session, List<String> 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<String> 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<Content> iterator() {
-                       List<Content> 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);
        }
 }
index 8fca831b024293084a07ded2ff93d9ad8a9085e4..13b19aabb901a468b4c20ef25df9e3bcf82e3500 100644 (file)
@@ -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<Object> 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<QName> getContentClasses() {
-               List<QName> res = new ArrayList<>();
-               List<String> value = getMultiple(DName.resourcetype.qName(), String.class);
-               for (String s : value) {
-                       QName name = NamespaceUtils.parsePrefixedName(provider, s);
-                       res.add(name);
-               }
+//             List<QName> res = new ArrayList<>();
+//             List<String> value = getMultiple(DName.resourcetype.qName(), String.class);
+//             for (String s : value) {
+//                     QName name = NamespaceUtils.parsePrefixedName(provider, s);
+//                     res.add(name);
+//             }
+               List<QName> res = getMultiple(DName.resourcetype.qName(), QName.class);
                if (Files.isDirectory(path))
                        res.add(DName.collection.qName());
                return res;
index a4c14186ac17d299b0673e1b67ee0857db166c13..0686be7cb59b2cf63d034fc8331a1d1884e39f08 100644 (file)
@@ -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 <A> Optional<A> get(QName key, Class<A> 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<QName> 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);
                        }
index 66ff878d5c6a9a644445a97bafa64869e19e2cfd..a5abe8dd47f165f170d98154ee559d80987bec97 100644 (file)
@@ -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);
index 3c436ba1fc40edd772e161d65ea1c70bc5f39cea..af4b5379c8383712c1e8870b13fc8e2e4c897874 100644 (file)
@@ -46,39 +46,8 @@ public class RemoteAuthUtils {
        public final static <T> T doAs(Supplier<T> 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<T>() {
-//
-//                                             @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)
index 63f4f82c7574b5d936fdeb1336bfd7296f758cbf..b5cd289a111f8f3b3907fc60208ad64c9d2f4778 100644 (file)
@@ -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);
                }
 
index 4f5a85ddfc998a0bbd24b33d4b4fc567bf21e565..3a23870bd0703a37608311c694d70a80ee82f672 100644 (file)
@@ -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;
        }
index bb1f6112a927616caec811bb98d61321fc7319ea..c467bcef41d21d64d55220910bb95adaa4f0b5ab 100644 (file)
@@ -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
index 7dba24cb4b50172d8a0e3d4105dd993da652e542..c4fe67f0dfac66e1553aabf836a4917a1458b947 100644 (file)
@@ -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
index 1d84958795c50221e0f968e2f46e127fde45e9f1..1203fef8dc90116b9622583f6eb9f09638697b80 100644 (file)
@@ -1,6 +1,6 @@
 major=2
 minor=3
-micro=17
+micro=18
 qualifier=
 
 Bundle-Copyright= \
index 00a51ef921f9cc62a27e6bd6609b78b5f549b414..30d6bc907b54229a2d00a4263878c13e3ccbaf22 100644 (file)
@@ -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<M> extends AbstractImageManager<Control, M> {
-       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<M> extends AbstractImageManager<Co
                                imgTag = noImg(size);
                        }
 
-               } else if (preferredSize.getWidth() != 0 && preferredSize.getHeight() != 0) {
+               } else if (preferredSize.width() != 0 && preferredSize.height() != 0) {
                        // given size if completely provided
                        size = preferredSize;
                } else {
                        // at this stage :
                        // image is completely known
-                       assert imageSize.getWidth() != 0 && imageSize.getHeight() != 0;
+                       assert imageSize.width() != 0 && imageSize.height() != 0;
                        // one and only one of the dimension as been specified
-                       assert preferredSize.getWidth() == 0 || preferredSize.getHeight() == 0;
+                       assert preferredSize.width() == 0 || preferredSize.height() == 0;
                        size = resizeTo(imageSize, preferredSize);
                }
 
@@ -55,6 +60,13 @@ public abstract class AbstractSwtImageManager<M> extends AbstractImageManager<Co
                        }
 
                        Label lbl = (Label) control;
+                       StringBuilder sb = new StringBuilder();
+                       if (link != null)
+                               sb.append("<a href='").append(URLEncoder.encode(link.toString(), StandardCharsets.UTF_8)).append("'>");
+                       sb.append(imgTag);
+                       if (link != null)
+                               sb.append("</a>");
+
                        lbl.setText(imgTag);
                        // lbl.setSize(size);
 //             } else if (control instanceof FileUpload) {
@@ -70,8 +82,8 @@ public abstract class AbstractSwtImageManager<M> extends AbstractImageManager<Co
 
        public Cms2DSize getImageSize(M node) {
                // TODO optimise
-               Image image = getSwtImage(node);
-               return new Cms2DSize(image.getBounds().width, image.getBounds().height);
+               ImageData imageData = getSwtImageData(node);
+               return new Cms2DSize(imageData.width, imageData.height);
        }
 
 }
index cf05f6f6455d939f7dd4618ad62e3ec9de78677c..fab29fa3b1625c7e782ab7d3aaf8aa80df866e79 100644 (file)
@@ -4,9 +4,6 @@ import java.security.PrivilegedAction;
 
 import javax.security.auth.Subject;
 
-import org.argeo.api.acr.Content;
-import org.argeo.api.acr.ContentSession;
-import org.argeo.api.acr.spi.ProvidedContent;
 import org.argeo.api.cms.CmsLog;
 import org.argeo.api.cms.ux.CmsEditable;
 import org.argeo.cms.CurrentUser;
@@ -274,17 +271,17 @@ public abstract class AbstractPageViewer {
                        throw new IllegalStateException("Edited should not be null or disposed at this stage");
        }
 
-       /** Persist all changes. */
-       protected void persistChanges(ContentSession session) {
-//             session.save();
-//             session.refresh(false);
-               // TODO notify that changes have been persisted
-       }
-
-       /** Convenience method using a Node in order to save the underlying session. */
-       protected void persistChanges(Content anyNode) {
-               persistChanges(((ProvidedContent) anyNode).getSession());
-       }
+//     /** Persist all changes. */
+//     protected void persistChanges(ContentSession session) {
+////           session.save();
+////           session.refresh(false);
+//             // TODO notify that changes have been persisted
+//     }
+//
+//     /** Convenience method using a Node in order to save the underlying session. */
+//     protected void persistChanges(Content anyNode) {
+//             persistChanges(((ProvidedContent) anyNode).getSession());
+//     }
 
        /** Notify edition exception */
        protected void notifyEditionException(Throwable e) {
index 78a9930051657ed4669f4caf196113f6c2c44ee7..0984f57fb2786a5366f833ac9f551c3ba9e31348 100644 (file)
@@ -1,15 +1,19 @@
 package org.argeo.cms.swt.acr;
 
+import java.io.IOException;
 import java.io.InputStream;
+import java.util.Optional;
 
 import org.argeo.api.acr.Content;
 import org.argeo.api.cms.CmsConstants;
 import org.argeo.api.cms.ux.Cms2DSize;
+import org.argeo.cms.acr.SvgAttrs;
 import org.argeo.cms.swt.AbstractSwtImageManager;
 import org.argeo.cms.swt.CmsSwtUtils;
 import org.argeo.cms.ux.CmsUxUtils;
-import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
 
+/** Implementation of {@link AbstractSwtImageManager} based on ACR. */
 public class AcrSwtImageManager extends AbstractSwtImageManager<Content> {
 
        @Override
@@ -24,8 +28,13 @@ public class AcrSwtImageManager extends AbstractSwtImageManager<Content> {
        }
 
        @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<Content> {
                buf.append(node.getPath());
                return buf.toString();
        }
+
+       @Override
+       public Cms2DSize getImageSize(Content node) {
+               // TODO cache it?
+               Optional<Integer> width = node.get(SvgAttrs.width, Integer.class);
+               Optional<Integer> height = node.get(SvgAttrs.height, Integer.class);
+               if (!width.isEmpty() && !height.isEmpty())
+                       return new Cms2DSize(width.get(), height.get());
+               return super.getImageSize(node);
+       }
+
 }
index 4a35a3bdd2b697e519662858f258989b7b752787..4cab6d0083d40a08ad31e315031a0a530712d518 100644 (file)
@@ -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 (file)
index 0000000..78ec800
--- /dev/null
@@ -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();
+       }
+
+}
index eb52fc6a37904ad00ed20e193d241559da714edd..578dac25142d919c8dccfbcecbe1329b014dc509 100644 (file)
@@ -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<Control, Content> 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<Control, Content> 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<Control, Content> imageManager) {
-               super(parent, swtStyle, preferredImageSize);
+               super(parent, swtStyle);
+               this.preferredImageSize = preferredImageSize;
                this.section = section;
-               this.imageManager = imageManager != null ? imageManager
-                               : (CmsImageManager<Control, Content>) 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 (file)
index 0000000..6a75dfb
--- /dev/null
@@ -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;
+
+       }
+
+}
index 1e52001cbc429999a0fbc62a5a3e1aa7c3ed215d..74de83e09ce895ce442e214ef64e5bb835163f9f 100644 (file)
@@ -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<String, Map<Integer, Image>> imageCache = Collections.synchronizedMap(new HashMap<>());
+       private Map<String, Map<Integer, ImageData>> imageDataCache = Collections.synchronizedMap(new HashMap<>());
 
        private Map<Integer, ImageTranscoder> 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<String, Map<Integer, Image>> imageCache = (Map<String, Map<Integer, Image>>) display
+                               .getData(IMAGE_CACHE_KEY);
+               if (imageCache == null)
+                       display.setData(IMAGE_CACHE_KEY, new HashMap<String, Map<Integer, Image>>());
+               imageCache = (Map<String, Map<Integer, Image>>) 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<String, String> 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<String, String> 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 (file)
index 0000000..a3bfa8e
--- /dev/null
@@ -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("<a style='color:inherit;text-decoration:inherit;' href='");
+                       labelText.append(target).append("'");
+                       if (openNew) {
+                               labelText.append(" target='_blank'");
+                       }
+                       labelText.append(">");
+               }
+               if (image != null) {
+                       registerImageIfNeeded();
+                       String imageLocation = RWT.getResourceManager().getLocation(image);
+                       labelText.append("<img");
+                       if (imageWidth != null)
+                               labelText.append(" width='").append(imageWidth).append('\'');
+                       if (imageHeight != null)
+                               labelText.append(" height='").append(imageHeight).append('\'');
+                       labelText.append(" src=\"").append(imageLocation).append("\"/>");
+
+               }
+
+               if (label != null) {
+                       labelText.append(' ').append(label);
+               }
+
+               if (target != null)
+                       labelText.append("</a>");
+
+               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();
+       }
+}
index e712e2fe6dc2eeba001ccb11e8faa73bb01f0f7d..1800a712af86e8adb7e1104e12ea33e4ccfe7163 100644 (file)
@@ -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;
                }
index 0612e8f9bcbe6a8c4db91931f6d60bd6093c3103..6ba03e86e92a425c578357e9433640cc4c2f4bc2 100644 (file)
@@ -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);
+       }
+
 }
index 82c04a26cde06121c7f7756c075cf3d3c7ee4dfd..9370b52c58fe859b8b719baceb3de925d5b7ec0d 100644 (file)
@@ -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 <b>after</b> 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 <code>null<code>
+        * argument.
+        */
+       public void initControl() {
+               if (control != null)
+                       return;
+               setStyle((String) null);
+       }
+
        /** To be overridden */
        protected void setControlLayoutData(Control control) {
                control.setLayoutData(CmsSwtUtils.fillWidth());
index 4d91cf8e20e8a163740f58e4deeecc39e50abfa1..216dc3654100b45f82ac2b5a07d33b558209916d 100644 (file)
@@ -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 (file)
index 6100c1a..0000000
+++ /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);
-       }
-
-}
index e1735f03cd5e850df581fa66fbcdb345d858c205..6f37582501b419080fd7a893f8e3c5bd9025fb6b 100644 (file)
@@ -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,\