Merge tag 'v2.3.23' into testing
authorMathieu Baudier <mbaudier@argeo.org>
Thu, 9 Nov 2023 10:34:18 +0000 (11:34 +0100)
committerMathieu Baudier <mbaudier@argeo.org>
Thu, 9 Nov 2023 10:34:18 +0000 (11:34 +0100)
99 files changed:
Makefile
NOTICE
org.argeo.api.acr/src/org/argeo/api/acr/Content.java
org.argeo.api.acr/src/org/argeo/api/acr/CrAttributeType.java
org.argeo.api.acr/src/org/argeo/api/acr/CrName.java
org.argeo.api.acr/src/org/argeo/api/acr/DName.java
org.argeo.api.acr/src/org/argeo/api/acr/QNamed.java
org.argeo.api.acr/src/org/argeo/api/acr/search/BasicSearch.java
org.argeo.api.acr/src/org/argeo/api/acr/search/ContentFilter.java
org.argeo.api.acr/src/org/argeo/api/acr/search/Eq.java [new file with mode: 0644]
org.argeo.api.acr/src/org/argeo/api/acr/search/Gt.java [new file with mode: 0644]
org.argeo.api.acr/src/org/argeo/api/acr/search/Gte.java [new file with mode: 0644]
org.argeo.api.acr/src/org/argeo/api/acr/search/IsContentClass.java [new file with mode: 0644]
org.argeo.api.acr/src/org/argeo/api/acr/search/IsDefined.java [new file with mode: 0644]
org.argeo.api.acr/src/org/argeo/api/acr/search/Like.java [new file with mode: 0644]
org.argeo.api.acr/src/org/argeo/api/acr/search/Lt.java [new file with mode: 0644]
org.argeo.api.acr/src/org/argeo/api/acr/search/Lte.java [new file with mode: 0644]
org.argeo.api.acr/src/org/argeo/api/acr/search/Not.java [new file with mode: 0644]
org.argeo.api.acr/src/org/argeo/api/acr/search/PropertyValueContraint.java [new file with mode: 0644]
org.argeo.api.cms/src/org/argeo/api/cms/CmsDeployment.java
org.argeo.api.cms/src/org/argeo/api/cms/CmsSshd.java [new file with mode: 0644]
org.argeo.api.cms/src/org/argeo/api/cms/ux/CmsView.java
org.argeo.cms.cli/bnd.bnd
org.argeo.cms.ee/src/org/argeo/cms/servlet/httpserver/ServletHttpExchange.java
org.argeo.cms.ee/src/org/argeo/cms/servlet/internal/PkgServlet.java
org.argeo.cms.jshell/src/org/argeo/cms/jshell/CmsJShell.java
org.argeo.cms.jshell/src/org/argeo/cms/jshell/JShellClient.java
org.argeo.cms.jshell/src/org/argeo/internal/cms/jshell/osgi/OsgiExecutionControlProvider.java
org.argeo.cms.lib.jetty/src/org/argeo/cms/jetty/JettyHttpServer.java
org.argeo.cms.lib.jetty/src/org/argeo/cms/jetty/ServletHttpContext.java
org.argeo.cms.lib.json/.classpath [new file with mode: 0644]
org.argeo.cms.lib.json/.project [new file with mode: 0644]
org.argeo.cms.lib.json/.settings/org.eclipse.core.resources.prefs [new file with mode: 0644]
org.argeo.cms.lib.json/.settings/org.eclipse.jdt.core.prefs [new file with mode: 0644]
org.argeo.cms.lib.json/.settings/org.eclipse.pde.core.prefs [new file with mode: 0644]
org.argeo.cms.lib.json/bnd.bnd [new file with mode: 0644]
org.argeo.cms.lib.json/build.properties [new file with mode: 0644]
org.argeo.cms.lib.json/src/org/argeo/cms/acr/json/AcrJsonUtils.java [new file with mode: 0644]
org.argeo.cms.lib.sshd/OSGI-INF/cmsSshServer.xml
org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/CmsSshServer.java
org.argeo.cms/OSGI-INF/cmsDeployment.xml
org.argeo.cms/OSGI-INF/cmsFileSystemProvider.xml [new file with mode: 0644]
org.argeo.cms/bnd.bnd
org.argeo.cms/build.properties
org.argeo.cms/src/org/argeo/cms/CmsSshd.java [deleted file]
org.argeo.cms/src/org/argeo/cms/LocaleUtils.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/MountManager.java
org.argeo.cms/src/org/argeo/cms/acr/xml/DomContentProvider.java
org.argeo.cms/src/org/argeo/cms/auth/SingleUserLoginModule.java
org.argeo.cms/src/org/argeo/cms/file/provider/CmsFileStore.java
org.argeo.cms/src/org/argeo/cms/file/provider/CmsFileSystem.java
org.argeo.cms/src/org/argeo/cms/file/provider/CmsFileSystemProvider.java
org.argeo.cms/src/org/argeo/cms/file/provider/CmsPath.java
org.argeo.cms/src/org/argeo/cms/file/provider/ContentAttributes.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/file/provider/ContentDirectoryStream.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/file/provider/ContentFileAttributeView.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/http/HttpHeader.java
org.argeo.cms/src/org/argeo/cms/http/RemoteAuthHttpExchange.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/http/server/HttpServerUtils.java
org.argeo.cms/src/org/argeo/cms/http/server/StaticHttpHandler.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/http/CmsAuthenticator.java
org.argeo.cms/src/org/argeo/cms/internal/http/RemoteAuthHttpExchange.java [deleted file]
org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsAcrHttpHandler.java
org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsDeploymentImpl.java
org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsStateImpl.java
org.argeo.cms/src/org/argeo/cms/util/AsyncPipedOutputStream.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/util/LangUtils.java
org.argeo.cms/src/org/argeo/cms/util/OS.java
org.argeo.cms/src/org/argeo/cms/util/StreamUtils.java
org.argeo.init/src/org/argeo/init/a2/AbstractProvisioningSource.java
org.argeo.init/src/org/argeo/init/logging/ThinLogging.java
osgi/equinox/org.argeo.cms.lib.equinox/OSGI-INF/equinoxJettyServer.xml [new file with mode: 0644]
osgi/equinox/org.argeo.cms.lib.equinox/OSGI-INF/jettyServiceFactory.xml [deleted file]
osgi/equinox/org.argeo.cms.lib.equinox/bnd.bnd
sdk/argeo-build
sdk/branches/unstable.bnd
sdk/cms-rcp.properties
sdk/deploy/argeo-cms/usr/bin/jshc [new file with mode: 0755]
swt/org.argeo.cms.swt/OSGI-INF/cmsUserApp.xml
swt/org.argeo.cms.swt/src/org/argeo/cms/swt/CmsSwtUtils.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/LinkedControl.java
swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/SwtUiProvider.java
swt/org.argeo.cms.swt/src/org/argeo/cms/swt/app/CmsUserApp.java
swt/org.argeo.cms.swt/src/org/argeo/cms/swt/app/SimpleSwtApp.java [new file with mode: 0644]
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/EclipseUiSpecificUtils.java
swt/rcp/org.argeo.cms.swt.rcp/OSGI-INF/cmsRcpDBusLauncher.xml
swt/rcp/org.argeo.cms.swt.rcp/OSGI-INF/cmsRcpDisplayFactory.xml
swt/rcp/org.argeo.cms.swt.rcp/bnd.bnd
swt/rcp/org.argeo.cms.swt.rcp/src/org/argeo/cms/ui/rcp/CmsRcpApp.java
swt/rcp/org.argeo.cms.swt.rcp/src/org/argeo/cms/ui/rcp/CmsRcpDisplayFactory.java
swt/rcp/org.argeo.cms.swt.rcp/src/org/argeo/cms/ui/rcp/CmsRcpHttpLauncher.java
swt/rcp/org.argeo.cms.swt.rcp/src/org/argeo/cms/ui/rcp/dbus/CmsRcpDBusLauncher.java
swt/rcp/org.argeo.cms.swt.rcp/src/org/argeo/cms/ui/rcp/dbus/CmsRcpFreeDesktopApplication.java
swt/rcp/org.argeo.swt.specific.rcp/src/org/eclipse/rap/fileupload/DiskFileUploadReceiver.java [new file with mode: 0644]
swt/rcp/org.argeo.swt.specific.rcp/src/org/eclipse/rap/rwt/widgets/FileUpload.java

index ff2c9ccad68e4d1555b9140ec9fce5c65c75c83d..ec06bd782e55106e6a994968ff0ff7e43d321d4a 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -19,11 +19,12 @@ org.argeo.api.cli \
 org.argeo.api.cms \
 org.argeo.cms \
 org.argeo.cms.ux \
-org.argeo.cms.ee \
+org.argeo.cms.jshell \
+org.argeo.cms.lib.json \
 org.argeo.cms.lib.jetty \
 org.argeo.cms.lib.dbus \
 org.argeo.cms.lib.sshd \
-org.argeo.cms.jshell \
+org.argeo.cms.ee \
 org.argeo.cms.cli \
 osgi/equinox/org.argeo.cms.lib.equinox \
 swt/org.argeo.swt.minidesktop \
diff --git a/NOTICE b/NOTICE
index 0db946a93003fbd5468fac95e28367432971d008..afa7583abe6fe8089b224fe1781f4ab49d576a89 100644 (file)
--- a/NOTICE
+++ b/NOTICE
@@ -58,6 +58,27 @@ whether to do so. The GNU General Public License gives permission to release a
 modified version without this exception; this exception also makes it possible 
 to release a modified version which carries forward this exception.
 
+# Apache License Permission
+
+Linking Argeo Commons statically or dynamically with other modules is making a 
+combined work based on Argeo Commons. Thus, the terms and conditions of the GNU 
+General Public License cover the whole combination when this license becomes 
+applicable.
+
+In addition, as a special exception, the copyright holders of Argeo Commons give 
+you permission to combine Argeo Commons with any program released under the 
+terms and conditions of the Apache License v2.0 or any later version of this 
+license. You may copy and distribute such a system following the terms of 
+the GNU GPL for Argeo Commons and the licenses of the other code concerned, 
+provided that you include the source code of that other code when and as 
+the GNU GPL requires distribution of source code.
+
+Note that people who make modified versions of Argeo Commons are not obligated 
+to grant this special exception for their modified versions; it is their choice 
+whether to do so. The GNU General Public License gives permission to release a 
+modified version without this exception; this exception also makes it possible 
+to release a modified version which carries forward this exception.
+
 # Java Content Repository API version 2.0 Permission
 
 Linking Argeo Commons statically or dynamically with other modules is making a 
index df5c149e6fc1fc696187d197766ad5f9cf0c2c52..7ec29594713e885d8f39a2efa00c0a24b186f7f3 100644 (file)
@@ -89,12 +89,33 @@ public interface Content extends Iterable<Content>, Map<QName, Object> {
        /*
         * CONTENT OPERATIONS
         */
-       Content add(QName name, QName... classes);
+       /** Adds a new empty {@link Content} to this {@link Content}. */
+       Content add(QName name, QName... contentClass);
+
+       default Content add(QName name, QNamed... contentClass) {
+               return add(name, toQNames(contentClass));
+       }
+
+       /**
+        * Adds a new {@link Content} to this {@link Content}, setting the provided
+        * attributes. The provided attributes can be used as hints by the
+        * implementation. In particular, setting {@link DName#getcontenttype} will
+        * imply that this content has a file semantic.
+        */
+       default Content add(QName name, Map<QName, Object> attrs, QName... classes) {
+               Content child = add(name, classes);
+               putAll(attrs);
+               return child;
+       }
 
        default Content add(String name, QName... classes) {
                return add(unqualified(name), classes);
        }
 
+       default Content add(String name, Map<QName, Object> attrs, QName... classes) {
+               return add(unqualified(name), attrs, classes);
+       }
+
        void remove();
 
        /*
@@ -118,10 +139,7 @@ public interface Content extends Iterable<Content>, Map<QName, Object> {
 
        /** AND */
        default boolean isContentClass(QNamed... contentClass) {
-               List<QName> lst = new ArrayList<>();
-               for (QNamed qNamed : contentClass)
-                       lst.add(qNamed.qName());
-               return isContentClass(lst.toArray(new QName[lst.size()]));
+               return isContentClass(toQNames(contentClass));
        }
 
        /** OR */
@@ -136,10 +154,14 @@ public interface Content extends Iterable<Content>, Map<QName, Object> {
 
        /** OR */
        default boolean hasContentClass(QNamed... contentClass) {
-               List<QName> lst = new ArrayList<>();
-               for (QNamed qNamed : contentClass)
-                       lst.add(qNamed.qName());
-               return hasContentClass(lst.toArray(new QName[lst.size()]));
+               return hasContentClass(toQNames(contentClass));
+       }
+
+       static QName[] toQNames(QNamed... names) {
+               QName[] res = new QName[names.length];
+               for (int i = 0; i < names.length; i++)
+                       res[i] = names[i].qName();
+               return res;
        }
 
        /*
@@ -227,6 +249,10 @@ public interface Content extends Iterable<Content>, Map<QName, Object> {
                return Optional.of(res.get(0));
        }
 
+       default Content soleOrAddChild(QName name, QName... classes) {
+               return soleChild(name).orElseGet(() -> this.add(name, classes));
+       }
+
        default Content child(QName name) {
                return soleChild(name).orElseThrow();
        }
index 3e0dddee4c55ed6cf6f6f81e836d8941dca3fd52..888d376c44bb7346c19367c6d12ddeeaac0f9538 100644 (file)
@@ -208,6 +208,9 @@ public enum CrAttributeType {
                if (String.class.isAssignableFrom(clss)) {
                        return Optional.of((T) strValue);
                }
+               if (java.util.UUID.class.isAssignableFrom(clss)) {
+                       return Optional.of((T) java.util.UUID.fromString(strValue));
+               }
                if (QName.class.isAssignableFrom(clss)) {
                        return Optional.of((T) NamespaceUtils.parsePrefixedName(namespaceContext, strValue));
                }
index ead47377bddff3eb5cd26d179636735d3f578bdc..62a21fbd3e3db6a00a4303147d38ca365aab14ed 100644 (file)
@@ -12,6 +12,7 @@ public enum CrName implements QNamed {
         * ATTRIBUTES
         */
        uuid, // the UUID of a content
+       path, // the path to a content
        mount, // a mount point
 //     cc, // content class
 
index be065a8d96750ef8d0e2399be80bf3b0753e49a6..d39f35d86ff96104d8693477e8f77cb70034ac1d 100644 (file)
@@ -23,15 +23,38 @@ public enum DName implements QNamed
        // RFC4918 (WebDav) value used as CR class
        collection, //
 
-       // RFC3744 (ACL) properties uase as CR attr
+       // RFC3744 (ACL) properties used as CR attr
        owner, //
        group, //
+
+       // RFC3253 (versioning) properties used as CR attr
+       checkedOut("checked-out"), //
+       checkedIn("checked-in"), //
        //
        ;
 
        public final static String WEBDAV_NAMESPACE_URI = "DAV:";
        public final static String WEBDAV_DEFAULT_PREFIX = "D";
 
+       private final String localName;
+
+       private DName(String localName) {
+               assert localName != null;
+               this.localName = localName;
+       }
+
+       private DName() {
+               this.localName = null;
+       }
+
+       @Override
+       public String localName() {
+               if (localName != null)
+                       return localName;
+               else
+                       return name();
+       }
+
        @Override
        public String getNamespace() {
                return WEBDAV_NAMESPACE_URI;
index 73ae4f02ead43b8c69038712e6e3a17477090ffe..9852a602a339e3b8f796a2c85186933e53f4dd69 100644 (file)
@@ -15,23 +15,52 @@ public interface QNamed extends Supplier<String> {
                return name();
        }
 
+       /**
+        * A {@link QName} corresponding to this definition. Calls
+        * {@link #createQName()} by default, but it could return a cached value.
+        */
        default QName qName() {
-               return new ContentName(getNamespace(), localName(), getDefaultPrefix());
+               return createQName();
        }
 
+       /**
+        * A prefixed representation of this qualified name within the provided
+        * {@link NamespaceContext}.
+        */
        default String get(NamespaceContext namespaceContext) {
                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. */
+       /**
+        * Create a {@link QName} corresponding on this definition. Can typically be
+        * used to cache the {@link QName} in enums.
+        */
+       default QName createQName() {
+               return new ContentName(getNamespace(), localName(), getDefaultPrefix());
+       }
+
+       /**
+        * 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();
        }
 
+       /** The namespace URI of this qualified name. */
        String getNamespace();
 
+       /**
+        * The default prefix of this qualified name, as expected to be found in
+        * {@link RuntimeNamespaceContext}.
+        */
        String getDefaultPrefix();
 
+       /** Compares to a plain {@link QName}. */
+       default boolean equals(QName qName) {
+               return qName().equals(qName);
+       }
+
        /** To be used by enums without namespace (typically XML attributes). */
        static interface Unqualified extends QNamed {
                @Override
index 8cbdebf7e5d5c8e6614dea460a739e59328158d1..7aae8cca07721df4cc8a40b819e75eab2d743995 100644 (file)
@@ -57,11 +57,11 @@ public class BasicSearch {
        }
 
        public BasicSearch where(Consumer<AndFilter> and) {
-               if (where != null)
-                       throw new IllegalStateException("A where clause is already set");
-               AndFilter subFilter = new AndFilter();
-               and.accept(subFilter);
-               where = subFilter;
+//             if (where != null)
+//                     throw new IllegalStateException("A where clause is already set");
+//             AndFilter subFilter = new AndFilter();
+               and.accept((AndFilter) getWhere());
+//             where = subFilter;
                return this;
        }
 
@@ -74,6 +74,8 @@ public class BasicSearch {
        }
 
        public ContentFilter<? extends Composition> getWhere() {
+               if (where == null)
+                       where = new AndFilter();
                return where;
        }
 
index 45f2d848c4183754c3935e73bdc77d0c1f5d4590..66c7559e968c92d2703b9463a1379ab2c2901ac7 100644 (file)
@@ -1,7 +1,8 @@
 package org.argeo.api.acr.search;
 
-import java.util.HashSet;
-import java.util.Set;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
 import java.util.function.Consumer;
 
 import javax.xml.namespace.QName;
@@ -10,7 +11,8 @@ 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<>();
+       // even though not necessary, we use a list in order to have a predictable order
+       private List<Constraint> constraints = new ArrayList<>();
 
        private COMPOSITION composition;
 
@@ -74,10 +76,67 @@ public abstract class ContentFilter<COMPOSITION extends Composition> implements
        }
 
        public COMPOSITION eq(QNamed attr, Object value) {
-               addConstraint(new Eq(attr.qName(), value));
+               return eq(attr.qName(), value);
+       }
+
+       public COMPOSITION lt(QName attr, Object value) {
+               addConstraint(new Lt(attr, value));
+               return composition;
+       }
+
+       public COMPOSITION lt(QNamed attr, Object value) {
+               return lt(attr.qName(), value);
+       }
+
+       public COMPOSITION lte(QName attr, Object value) {
+               addConstraint(new Lte(attr, value));
                return composition;
        }
 
+       public COMPOSITION lte(QNamed attr, Object value) {
+               return lte(attr.qName(), value);
+       }
+
+       public COMPOSITION gt(QName attr, Object value) {
+               addConstraint(new Gt(attr, value));
+               return composition;
+       }
+
+       public COMPOSITION gt(QNamed attr, Object value) {
+               return gt(attr.qName(), value);
+       }
+
+       public COMPOSITION gte(QName attr, Object value) {
+               addConstraint(new Gte(attr, value));
+               return composition;
+       }
+
+       public COMPOSITION gte(QNamed attr, Object value) {
+               return gte(attr.qName(), value);
+       }
+
+       public COMPOSITION like(QName attr, String pattern) {
+               addConstraint(new Like(attr, pattern));
+               return composition;
+       }
+
+       public COMPOSITION like(QNamed attr, String pattern) {
+               return like(attr.qName(), pattern);
+       }
+
+       /*
+        * PROPERTIES CONSTRAINTS
+        */
+
+       public COMPOSITION isDefined(QName attr) {
+               addConstraint(new IsDefined(attr));
+               return composition;
+       }
+
+       public COMPOSITION isDefined(QNamed attr) {
+               return isDefined(attr.qName());
+       }
+
        /*
         * UTILITIES
         */
@@ -90,21 +149,21 @@ public abstract class ContentFilter<COMPOSITION extends Composition> implements
                } else {
                        operatorToAdd = operator;
                }
-               constraintss.add(operatorToAdd);
+               constraints.add(operatorToAdd);
        }
 
        /** Checks that the root operator is not set. */
        private void checkAddConstraint() {
-               if (composition == null && !constraintss.isEmpty())
-                       throw new IllegalStateException("An operator is already registered (" + constraintss.iterator().next()
+               if (composition == null && !constraints.isEmpty())
+                       throw new IllegalStateException("An operator is already registered (" + constraints.iterator().next()
                                        + ") and no composition is defined");
        }
 
        /*
         * ACCESSORs
         */
-       public Set<Constraint> getConstraints() {
-               return constraintss;
+       public Collection<Constraint> getConstraints() {
+               return constraints;
        }
 
        public boolean isUnion() {
@@ -115,58 +174,6 @@ public abstract class ContentFilter<COMPOSITION extends Composition> implements
         * CLASSES
         */
 
-       public static class Not implements Constraint {
-               final Constraint negated;
-
-               public Not(Constraint negated) {
-                       this.negated = negated;
-               }
-
-               public Constraint getNegated() {
-                       return negated;
-               }
-
-       }
-
-       public static class Eq implements Constraint {
-               final QName prop;
-               final Object value;
-
-               public Eq(QName prop, Object value) {
-                       super();
-                       this.prop = prop;
-                       this.value = value;
-               }
-
-               public QName getProp() {
-                       return prop;
-               }
-
-               public Object getValue() {
-                       return value;
-               }
-
-       }
-
-       public static class IsContentClass implements Constraint {
-               final QName[] contentClasses;
-
-               public IsContentClass(QName[] contentClasses) {
-                       this.contentClasses = contentClasses;
-               }
-
-               public IsContentClass(QNamed[] contentClasses) {
-                       this.contentClasses = new QName[contentClasses.length];
-                       for (int i = 0; i < contentClasses.length; i++)
-                               this.contentClasses[i] = contentClasses[i].qName();
-               }
-
-               public QName[] getContentClasses() {
-                       return contentClasses;
-               }
-
-       }
-
 //     public static void main(String[] args) {
 //             AndFilter filter = new AndFilter();
 //             filter.eq(new QName("test"), "test").and().not().eq(new QName("type"), "integer");
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/search/Eq.java b/org.argeo.api.acr/src/org/argeo/api/acr/search/Eq.java
new file mode 100644 (file)
index 0000000..73402a2
--- /dev/null
@@ -0,0 +1,10 @@
+package org.argeo.api.acr.search;
+
+import javax.xml.namespace.QName;
+
+/** Whether this property equals this value. */
+public class Eq extends PropertyValueContraint {
+       public Eq(QName prop, Object value) {
+               super(prop, value);
+       }
+}
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/search/Gt.java b/org.argeo.api.acr/src/org/argeo/api/acr/search/Gt.java
new file mode 100644 (file)
index 0000000..4ab454e
--- /dev/null
@@ -0,0 +1,10 @@
+package org.argeo.api.acr.search;
+
+import javax.xml.namespace.QName;
+
+/** Whether this property is strictly greater than this value. */
+public class Gt extends PropertyValueContraint {
+       public Gt(QName prop, Object value) {
+               super(prop, value);
+       }
+}
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/search/Gte.java b/org.argeo.api.acr/src/org/argeo/api/acr/search/Gte.java
new file mode 100644 (file)
index 0000000..4a51fc8
--- /dev/null
@@ -0,0 +1,10 @@
+package org.argeo.api.acr.search;
+
+import javax.xml.namespace.QName;
+
+/** Whether this property is greater than this value or equal. */
+public class Gte extends PropertyValueContraint {
+       public Gte(QName prop, Object value) {
+               super(prop, value);
+       }
+}
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/search/IsContentClass.java b/org.argeo.api.acr/src/org/argeo/api/acr/search/IsContentClass.java
new file mode 100644 (file)
index 0000000..cd01f7b
--- /dev/null
@@ -0,0 +1,25 @@
+package org.argeo.api.acr.search;
+
+import javax.xml.namespace.QName;
+
+import org.argeo.api.acr.QNamed;
+
+/** Whether the content is all these content classes. */
+public class IsContentClass implements Constraint {
+       final QName[] contentClasses;
+
+       public IsContentClass(QName[] contentClasses) {
+               this.contentClasses = contentClasses;
+       }
+
+       public IsContentClass(QNamed[] contentClasses) {
+               this.contentClasses = new QName[contentClasses.length];
+               for (int i = 0; i < contentClasses.length; i++)
+                       this.contentClasses[i] = contentClasses[i].qName();
+       }
+
+       public QName[] getContentClasses() {
+               return contentClasses;
+       }
+
+}
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/search/IsDefined.java b/org.argeo.api.acr/src/org/argeo/api/acr/search/IsDefined.java
new file mode 100644 (file)
index 0000000..a9c8d09
--- /dev/null
@@ -0,0 +1,16 @@
+package org.argeo.api.acr.search;
+
+import javax.xml.namespace.QName;
+
+/** Whether this property is defined. */
+public class IsDefined implements Constraint {
+       final QName prop;
+
+       public IsDefined(QName prop) {
+               this.prop = prop;
+       }
+
+       public QName getProp() {
+               return prop;
+       }
+}
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/search/Like.java b/org.argeo.api.acr/src/org/argeo/api/acr/search/Like.java
new file mode 100644 (file)
index 0000000..5af5278
--- /dev/null
@@ -0,0 +1,10 @@
+package org.argeo.api.acr.search;
+
+import javax.xml.namespace.QName;
+
+/** Whether this property equals this value. */
+public class Like extends PropertyValueContraint {
+       public Like(QName prop, String pattern) {
+               super(prop, pattern);
+       }
+}
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/search/Lt.java b/org.argeo.api.acr/src/org/argeo/api/acr/search/Lt.java
new file mode 100644 (file)
index 0000000..25b502e
--- /dev/null
@@ -0,0 +1,10 @@
+package org.argeo.api.acr.search;
+
+import javax.xml.namespace.QName;
+
+/** Whether this property is strictly less than this value. */
+public class Lt extends PropertyValueContraint {
+       public Lt(QName prop, Object value) {
+               super(prop, value);
+       }
+}
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/search/Lte.java b/org.argeo.api.acr/src/org/argeo/api/acr/search/Lte.java
new file mode 100644 (file)
index 0000000..330f29f
--- /dev/null
@@ -0,0 +1,10 @@
+package org.argeo.api.acr.search;
+
+import javax.xml.namespace.QName;
+
+/** Whether this property is less than this value or equal. */
+public class Lte extends PropertyValueContraint {
+       public Lte(QName prop, Object value) {
+               super(prop, value);
+       }
+}
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/search/Not.java b/org.argeo.api.acr/src/org/argeo/api/acr/search/Not.java
new file mode 100644 (file)
index 0000000..41a97da
--- /dev/null
@@ -0,0 +1,15 @@
+package org.argeo.api.acr.search;
+
+/** Negates the provided constraint. */
+public class Not implements Constraint {
+       final Constraint negated;
+
+       public Not(Constraint negated) {
+               this.negated = negated;
+       }
+
+       public Constraint getNegated() {
+               return negated;
+       }
+
+}
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/search/PropertyValueContraint.java b/org.argeo.api.acr/src/org/argeo/api/acr/search/PropertyValueContraint.java
new file mode 100644 (file)
index 0000000..7fc07bc
--- /dev/null
@@ -0,0 +1,23 @@
+package org.argeo.api.acr.search;
+
+import javax.xml.namespace.QName;
+
+/** Whether this property equals this value. */
+public abstract class PropertyValueContraint implements Constraint {
+       final QName prop;
+       final Object value;
+
+       public PropertyValueContraint(QName prop, Object value) {
+               this.prop = prop;
+               this.value = value;
+       }
+
+       public QName getProp() {
+               return prop;
+       }
+
+       public Object getValue() {
+               return value;
+       }
+
+}
index d557816cb1a3bc927d449c2ae7232b307fef726b..ca1082c7db5ab3c07d9118e8fa9545447ec424a5 100644 (file)
@@ -1,9 +1,14 @@
 package org.argeo.api.cms;
 
+import java.util.concurrent.CompletionStage;
+
+import com.sun.net.httpserver.HttpServer;
+
 /** A configured node deployment. */
 public interface CmsDeployment {
-
-//     void addFactoryDeployConfig(String factoryPid, Dictionary<String, Object> props);
-//
-//     Dictionary<String, Object> getProps(String factoryPid, String cn);
+       /** The local HTTP server, or null if none is expected. */
+       CompletionStage<HttpServer> getHttpServer();
+       
+       /** The local SSH server, or null if none is expected. */
+       CompletionStage<CmsSshd> getCmsSshd();
 }
diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/CmsSshd.java b/org.argeo.api.cms/src/org/argeo/api/cms/CmsSshd.java
new file mode 100644 (file)
index 0000000..ec44e57
--- /dev/null
@@ -0,0 +1,11 @@
+package org.argeo.api.cms;
+
+import java.net.InetSocketAddress;
+
+/** A local SSH server. */
+public interface CmsSshd {
+       final static String NODE_USERNAME_ALIAS = "user.name";
+       final static String DEFAULT_SSH_HOST_KEY_PATH = "private/" + CmsConstants.NODE + ".ser";
+
+       InetSocketAddress getAddress();
+}
index a36baf8e08b642cd2c7b625162494ae9710b9d14..121e4bdcbf601a654fc608aeda89ec895f4b4e3b 100644 (file)
@@ -1,5 +1,6 @@
 package org.argeo.api.cms.ux;
 
+import java.net.URI;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.Callable;
@@ -9,7 +10,7 @@ import javax.security.auth.login.LoginContext;
 
 import org.argeo.api.cms.CmsSession;
 
-/** Provides interaction with the CMS system. */
+/** Provides UX interactions with the CMS system. */
 public interface CmsView {
        final static String CMS_VIEW_UID_PROPERTY = "argeo.cms.view.uid";
        // String KEY = "org.argeo.cms.ui.view";
@@ -31,10 +32,18 @@ public interface CmsView {
        // SERVICES
        void exception(Throwable e);
 
-       <V,M> CmsImageManager<V, M> getImageManager();
+       <V, M> CmsImageManager<V, M> getImageManager();
 
        boolean isAnonymous();
 
+       /**
+        * Translates to an URL that can be reached by a client, depending on its type.
+        * Typically, if a web interface asks for /path/on/the/web/server it will be
+        * returned without modifications; but a thin client will probably need to add a
+        * server and a port.
+        */
+       URI toBackendUri(String url);
+
        /**
         * Send an event to this topic. Does nothing by default., but if implemented it
         * MUST set the {@link #CMS_VIEW_UID_PROPERTY} in the properties.
index 7cf8c13917b1b1fda75f0af1f029630156cae913..401cda9557399409b3981cdf19459f2ac913b2fd 100644 (file)
@@ -11,52 +11,57 @@ org.argeo.api.uuid.2.1.jar \
 org.argeo.cms.2.1.jar \
 org.argeo.cms.cli.2.1.jar \
 org.argeo.cms.ee.2.1.jar \
+org.argeo.cms.jshell.2.1.jar \
 org.argeo.cms.lib.dbus.2.1.jar \
 org.argeo.cms.lib.jetty.2.1.jar \
+org.argeo.cms.lib.json.2.1.jar \
 org.argeo.cms.lib.sshd.2.1.jar \
 org.argeo.cms.ux.2.1.jar \
 org.argeo.init.2.1.jar \
 ../log/syslogger/org.argeo.tp/org.argeo.tp.syslogger.2.1.jar \
-../org.argeo.tp/com.fasterxml.jackson.core.jackson.annotations.2.14.jar \
-../org.argeo.tp/com.fasterxml.jackson.core.jackson.core.2.14.jar \
-../org.argeo.tp/com.fasterxml.jackson.core.jackson.databind.2.14.jar \
+../org.argeo.tp/com.apicatalog.jsonld.1.3.jar \
+../org.argeo.tp/com.fasterxml.jackson.core.jackson.annotations.2.15.jar \
+../org.argeo.tp/com.fasterxml.jackson.core.jackson.core.2.15.jar \
+../org.argeo.tp/com.fasterxml.jackson.core.jackson.databind.2.15.jar \
 ../org.argeo.tp/javax.servlet.4.0.jar \
 ../org.argeo.tp/javax.websocket.1.1.jar \
-../org.argeo.tp/org.apache.batik.1.16.jar \
-../org.argeo.tp/org.apache.batik.constants.1.16.jar \
-../org.argeo.tp/org.apache.batik.css.1.16.jar \
-../org.argeo.tp/org.apache.batik.i18n.1.16.jar \
-../org.argeo.tp/org.apache.batik.util.1.16.jar \
+../org.argeo.tp/org.apache.batik.1.17.jar \
+../org.argeo.tp/org.apache.batik.constants.1.17.jar \
+../org.argeo.tp/org.apache.batik.css.1.17.jar \
+../org.argeo.tp/org.apache.batik.i18n.1.17.jar \
+../org.argeo.tp/org.apache.batik.util.1.17.jar \
 ../org.argeo.tp/org.apache.commons.cli.1.5.jar \
-../org.argeo.tp/org.apache.commons.codec.1.15.jar \
-../org.argeo.tp/org.apache.commons.compress.1.22.jar \
+../org.argeo.tp/org.apache.commons.codec.1.16.jar \
+../org.argeo.tp/org.apache.commons.compress.1.24.jar \
 ../org.argeo.tp/org.apache.commons.fileupload.1.5.jar \
-../org.argeo.tp/org.apache.commons.io.2.11.jar \
+../org.argeo.tp/org.apache.commons.io.2.14.jar \
 ../org.argeo.tp/org.apache.httpcomponents.httpclient.4.5.jar \
 ../org.argeo.tp/org.apache.httpcomponents.httpcore.4.4.jar \
 ../org.argeo.tp/org.apache.httpcomponents.httpmime.4.5.jar \
-../org.argeo.tp/org.apache.sshd.2.9.jar \
-../org.argeo.tp/org.apache.sshd.cli.2.9.jar \
-../org.argeo.tp/org.apache.sshd.git.2.9.jar \
-../org.argeo.tp/org.apache.sshd.putty.2.9.jar \
-../org.argeo.tp/org.apache.sshd.scp.2.9.jar \
-../org.argeo.tp/org.apache.sshd.sftp.2.9.jar \
+../org.argeo.tp/org.apache.sshd.2.10.jar \
+../org.argeo.tp/org.apache.sshd.cli.2.10.jar \
+../org.argeo.tp/org.apache.sshd.git.2.10.jar \
+../org.argeo.tp/org.apache.sshd.putty.2.10.jar \
+../org.argeo.tp/org.apache.sshd.scp.2.10.jar \
+../org.argeo.tp/org.apache.sshd.sftp.2.10.jar \
 ../org.argeo.tp/org.apache.tomcat.jni.9.0.jar \
-../org.argeo.tp/org.apache.xalan.2.7.jar \
 ../org.argeo.tp/org.apache.xerces.2.12.jar \
-../org.argeo.tp/org.apache.xmlgraphics.2.8.jar \
+../org.argeo.tp/org.apache.xmlgraphics.2.9.jar \
 ../org.argeo.tp/org.apache.xml.resolver.1.2.jar \
-../org.argeo.tp/org.freeedesktop.dbus.4.2.jar \
+../org.argeo.tp/org.eclipse.parsson.1.1.jar \
+../org.argeo.tp/org.freeedesktop.dbus.4.3.jar \
 ../org.argeo.tp/org.h2.2.1.jar \
-../org.argeo.tp/org.postgresql.jdbc.42.5.jar \
+../org.argeo.tp/org.jline.3.23.jar \
+../org.argeo.tp/org.mozilla.universalchardet.2.4.jar \
+../org.argeo.tp/org.postgresql.jdbc.42.6.jar \
 ../org.argeo.tp/org.tukaani.xz.1.9.jar \
 ../org.argeo.tp/org.w3c.css.sac.1.3.jar \
 ../org.argeo.tp/org.w3c.dom.smil.1.0.jar \
 ../org.argeo.tp/org.w3c.dom.svg.1.1.jar \
+../crypto/fips/org.argeo.tp.crypto/bc-fips.1.0.jar \
+../crypto/fips/org.argeo.tp.crypto/bc-fips.1.0.src.jar \
 ../crypto/fips/org.argeo.tp.crypto/bcmail-fips.1.0.jar \
 ../crypto/fips/org.argeo.tp.crypto/bcmail-fips.1.0.src.jar \
-../crypto/fips/org.argeo.tp.crypto/bc-noncert.1.0.jar \
-../crypto/fips/org.argeo.tp.crypto/bc-noncert.1.0.src.jar \
 ../crypto/fips/org.argeo.tp.crypto/bcpg-fips.1.0.jar \
 ../crypto/fips/org.argeo.tp.crypto/bcpg-fips.1.0.src.jar \
 ../crypto/fips/org.argeo.tp.crypto/bcpkix-fips.1.0.jar \
index f5e9c03945f4877c62f36369d264d75ed5bd1aea..85553f01cd9f1fd1be76a348d1d0a90c6434c15f 100644 (file)
@@ -5,9 +5,14 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.InetSocketAddress;
 import java.net.URI;
+import java.net.URLEncoder;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Enumeration;
 import java.util.List;
+import java.util.Map;
+import java.util.StringJoiner;
 
 import javax.net.ssl.SSLSession;
 import javax.servlet.http.HttpServletRequest;
@@ -72,7 +77,17 @@ class ServletHttpExchange extends HttpsExchange {
 
        @Override
        public URI getRequestURI() {
-               return URI.create(httpServletRequest.getRequestURI());
+               // TODO properly deal with charset?
+               Charset encoding = StandardCharsets.UTF_8;
+               Map<String, String[]> parameters = httpServletRequest.getParameterMap();
+               StringJoiner sb = new StringJoiner("&");
+               for (String key : parameters.keySet()) {
+                       for (String value : parameters.get(key)) {
+                               String pair = URLEncoder.encode(key, encoding) + '=' + URLEncoder.encode(value, encoding);
+                               sb.add(pair);
+                       }
+               }
+               return URI.create(httpServletRequest.getRequestURI() + (sb.length() != 0 ? '?' + sb.toString() : ""));
        }
 
        @Override
index 2b2ffcb10f084ff397a38df47354a72bb7dc32cd..ca4b6f74d70f9c27603fb7aaa9cefcc60f29bfde 100644 (file)
@@ -2,7 +2,9 @@ package org.argeo.cms.servlet.internal;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.net.FileNameMap;
 import java.net.URL;
+import java.net.URLConnection;
 import java.util.Collection;
 import java.util.SortedMap;
 import java.util.TreeMap;
@@ -26,9 +28,15 @@ import org.osgi.framework.wiring.BundleWiring;
 import org.osgi.framework.wiring.FrameworkWiring;
 import org.osgi.resource.Requirement;
 
+/**
+ * Publishes client-side web resources (JavaScript, HTML, CSS, images, etc.)
+ * from the OSGi runtime.
+ */
 public class PkgServlet extends HttpServlet {
        private static final long serialVersionUID = 7660824185145214324L;
 
+       private static FileNameMap fileNameMap = URLConnection.getFileNameMap();
+
        private BundleContext bundleContext = FrameworkUtil.getBundle(PkgServlet.class).getBundleContext();
 
        @Override
@@ -50,6 +58,10 @@ public class PkgServlet extends HttpServlet {
                        throw new IllegalArgumentException("Unsupported path length " + pathInfo);
                }
 
+               // content type
+               String contentType = fileNameMap.getContentTypeFor(file);
+               resp.setContentType(contentType);
+
                FrameworkWiring frameworkWiring = bundleContext.getBundle(0).adapt(FrameworkWiring.class);
                String filter;
                if (versionStr == null) {
index 91a11ee71d3a2ec59fadffe8fb513a53dedc4e4e..d84ce7212ebcbcacd387618c83ca72e7601bb4ec 100644 (file)
@@ -16,6 +16,7 @@ import java.util.UUID;
 import org.argeo.api.cms.CmsLog;
 import org.argeo.api.cms.CmsState;
 import org.argeo.api.uuid.UuidFactory;
+import org.argeo.cms.util.FsUtils;
 import org.argeo.cms.util.OS;
 import org.argeo.internal.cms.jshell.osgi.OsgiExecutionControlProvider;
 import org.osgi.framework.Bundle;
@@ -142,7 +143,8 @@ public class CmsJShell {
                String symbolicName = bundleSnDir.getFileName().toString();
                Bundle fromBundle = OsgiExecutionControlProvider.getBundleFromSn(symbolicName);
                if (fromBundle == null) {
-                       log.error("Ignoring bundle " + symbolicName + " because it was not found");
+                       log.error("Removing directory for bundle " + symbolicName + " because it was not found in runtime...");
+                       FsUtils.delete(bundleSnDir);
                        return;
                }
                Long bundleId = fromBundle.getBundleId();
index 87e88b77fe2d994014b488b84f5927e143ac4f1a..f090ed0684a85b3be957b9da3af309f46f0ec67b 100644 (file)
@@ -11,6 +11,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
+import java.io.PrintStream;
 import java.lang.System.Logger;
 import java.lang.management.ManagementFactory;
 import java.net.StandardSocketOptions;
@@ -155,34 +156,101 @@ public class JShellClient {
 
        }
 
-       public static void main(String[] args) throws IOException, InterruptedException {
-               if (benchmark)
-                       System.err.println(ManagementFactory.getRuntimeMXBean().getUptime());
-               List<String> plainArgs = new ArrayList<>();
-               Map<String, List<String>> options = new HashMap<>();
-               String currentOption = null;
-               for (int i = 0; i < args.length; i++) {
-                       if (args[i].startsWith("-")) {
-                               currentOption = args[i];
-                               if (!options.containsKey(currentOption))
-                                       options.put(currentOption, new ArrayList<>());
-                               i++;
-                               options.get(currentOption).add(args[i]);
-                       } else {
-                               plainArgs.add(args[i]);
+       public static void main(String[] args) {
+               try {
+                       if (benchmark)
+                               System.err.println(ManagementFactory.getRuntimeMXBean().getUptime());
+                       List<String> plainArgs = new ArrayList<>();
+                       Map<String, List<String>> options = new HashMap<>();
+                       String currentOption = null;
+                       for (int i = 0; i < args.length; i++) {
+                               if (args[i].startsWith("-")) {
+                                       currentOption = args[i];
+                                       if ("-h".equals(currentOption) || "--help".equals(currentOption)) {
+                                               printHelp(System.out);
+                                               return;
+                                       }
+                                       if (!options.containsKey(currentOption))
+                                               options.put(currentOption, new ArrayList<>());
+                                       i++;
+                                       options.get(currentOption).add(args[i]);
+                               } else {
+                                       plainArgs.add(args[i]);
+                               }
                        }
+
+                       List<String> dir = opt(options, "-d", "--sockets-dir");
+                       if (dir.size() > 1)
+                               throw new IllegalArgumentException("Only one run directory can be specified");
+                       Path targetStateDirectory;
+                       if (dir.isEmpty())
+                               targetStateDirectory = Paths.get(System.getProperty("user.dir"));
+                       else {
+                               targetStateDirectory = Paths.get(dir.get(0));
+                               if (!Files.exists(targetStateDirectory)) {
+                                       // we assume argument is the application id
+                                       targetStateDirectory = getRunDir().resolve(dir.get(0));
+                               }
+                       }
+
+                       List<String> bundle = opt(options, "-b", "--bundle");
+                       if (bundle.size() > 1)
+                               throw new IllegalArgumentException("Only one bundle can be specified");
+                       String symbolicName = bundle.isEmpty() ? "org.argeo.cms.cli" : bundle.get(0);
+
+                       Path script = plainArgs.isEmpty() ? null : Paths.get(plainArgs.get(0));
+                       List<String> scriptArgs = new ArrayList<>();
+                       for (int i = 1; i < plainArgs.size(); i++)
+                               scriptArgs.add(plainArgs.get(i));
+
+                       JShellClient client = new JShellClient(targetStateDirectory, symbolicName, script, scriptArgs);
+                       client.run();
+               } catch (Exception e) {
+                       e.printStackTrace();
+                       printHelp(System.err);
                }
+       }
 
-               Path targetStateDirectory = Paths.get(options.get("-d").get(0));
-               String symbolicName = options.get("-b").get(0);
+       /** Guaranteed to return a non-null list (which may be empty). */
+       private static List<String> opt(Map<String, List<String>> options, String shortOpt, String longOpt) {
+               List<String> res = new ArrayList<>();
+               if (options.get(shortOpt) != null)
+                       res.addAll(options.get(shortOpt));
+               if (options.get(longOpt) != null)
+                       res.addAll(options.get(longOpt));
+               return res;
+       }
 
-               Path script = plainArgs.isEmpty() ? null : Paths.get(plainArgs.get(0));
-               List<String> scriptArgs = new ArrayList<>();
-               for (int i = 1; i < plainArgs.size(); i++)
-                       scriptArgs.add(plainArgs.get(i));
+       public static void printHelp(PrintStream out) {
+               out.println("Start a JShell terminal or execute a JShell script in a local Argeo CMS instance");
+               out.println("Usage: jshc -d <sockets directory> -b <bundle> [JShell script] [script arguments...]");
+               out.println("  -d, --sockets-dir  app directory with UNIX sockets (default to current dir)");
+               out.println("  -b, --bundle       bundle to activate and use as context (default to org.argeo.cms.cli)");
+               out.println("  -h, --help         this help message");
+       }
 
-               JShellClient client = new JShellClient(targetStateDirectory, symbolicName, script, scriptArgs);
-               client.run();
+       // Copied from org.argeo.cms.util.OS
+       private static Path getRunDir() {
+               Path runDir;
+               String xdgRunDir = System.getenv("XDG_RUNTIME_DIR");
+               if (xdgRunDir != null) {
+                       // TODO support multiple names
+                       runDir = Paths.get(xdgRunDir);
+               } else {
+                       String username = System.getProperty("user.name");
+                       if (username.equals("root")) {
+                               runDir = Paths.get("/run");
+                       } else {
+                               Path homeDir = Paths.get(System.getProperty("user.home"));
+                               if (!Files.isWritable(homeDir)) {
+                                       // typically, dameon's home (/usr/sbin) is not writable
+                                       runDir = Paths.get("/tmp/" + username + "/run");
+                               } else {
+                                       runDir = homeDir.resolve(".cache/argeo");
+                               }
+                       }
+               }
+               return runDir;
        }
 
        /*
index fbca085861759a1410b743555d8eb1d817db6367..9ebf97ed07be957c25203e409a9272986a9badf5 100644 (file)
@@ -22,6 +22,7 @@ import org.argeo.api.cms.CmsLog;
 import org.argeo.cms.jshell.CmsExecutionControl;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
 import org.osgi.framework.FrameworkUtil;
 import org.osgi.framework.Version;
 import org.osgi.framework.namespace.PackageNamespace;
@@ -89,6 +90,25 @@ public class OsgiExecutionControlProvider implements ExecutionControlProvider {
        public static Path getBundleStartupScript(Long bundleId) {
                BundleContext bc = FrameworkUtil.getBundle(OsgiExecutionControlProvider.class).getBundleContext();
                Bundle fromBundle = bc.getBundle(bundleId);
+
+               int bundleState = fromBundle.getState();
+               if (Bundle.INSTALLED == bundleState)
+                       throw new IllegalStateException("Bundle " + fromBundle.getSymbolicName() + " is not resolved");
+               if (Bundle.RESOLVED == bundleState) {
+                       try {
+                               fromBundle.start();
+                       } catch (BundleException e) {
+                               throw new IllegalStateException("Cannot start bundle " + fromBundle.getSymbolicName(), e);
+                       }
+                       while (Bundle.ACTIVE != fromBundle.getState())
+                               try {
+                                       Thread.sleep(100);
+                               } catch (InterruptedException e) {
+                                       // we assume the session has been closed
+                                       throw new RuntimeException("Bundle " + fromBundle.getSymbolicName() + " is not active", e);
+                               }
+               }
+
                Path bundleStartupScript = fromBundle.getDataFile("BUNDLE.jsh").toPath();
 
                BundleWiring fromBundleWiring = fromBundle.adapt(BundleWiring.class);
@@ -101,8 +121,13 @@ public class OsgiExecutionControlProvider implements ExecutionControlProvider {
                        packagesToImport.add(pkg.getName());
                }
 
-               List<BundleWire> bundleWires = fromBundleWiring.getRequiredWires(BundleRevision.PACKAGE_NAMESPACE);
-               for (BundleWire bw : bundleWires) {
+//             List<BundleWire> exportedWires = fromBundleWiring.getProvidedWires(BundleRevision.PACKAGE_NAMESPACE);
+//             for (BundleWire bw : exportedWires) {
+//                     packagesToImport.add(bw.getCapability().getAttributes().get(PackageNamespace.PACKAGE_NAMESPACE).toString());
+//             }
+
+               List<BundleWire> importedWires = fromBundleWiring.getRequiredWires(BundleRevision.PACKAGE_NAMESPACE);
+               for (BundleWire bw : importedWires) {
                        packagesToImport.add(bw.getCapability().getAttributes().get(PackageNamespace.PACKAGE_NAMESPACE).toString());
                }
 
@@ -154,9 +179,9 @@ public class OsgiExecutionControlProvider implements ExecutionControlProvider {
        }
 
        public static String getBundleClasspath(Long bundleId) throws IOException {
-               String framework = System.getProperty("osgi.framework");
-               Path frameworkLocation = Paths.get(URI.create(framework)).toAbsolutePath();
                BundleContext bc = FrameworkUtil.getBundle(OsgiExecutionControlProvider.class).getBundleContext();
+               String framework = bc.getProperty("osgi.framework");
+               Path frameworkLocation = Paths.get(URI.create(framework)).toAbsolutePath();
                Bundle fromBundle = bc.getBundle(bundleId);
 
                BundleWiring fromBundleWiring = fromBundle.adapt(BundleWiring.class);
@@ -178,7 +203,8 @@ public class OsgiExecutionControlProvider implements ExecutionControlProvider {
                                continue bundles;
                        }
                        Path p = bundleToPath(frameworkLocation, b);
-                       classpath.add(p.toString());
+                       if (p != null)
+                               classpath.add(p.toString());
                }
 
                return classpath.toString();
@@ -188,11 +214,17 @@ public class OsgiExecutionControlProvider implements ExecutionControlProvider {
                String location = bundle.getLocation();
                if (location.startsWith("initial@reference:file:")) {
                        location = location.substring("initial@reference:file:".length());
-                       Path p = frameworkLocation.getParent().resolve(location).toRealPath();
-                       // TODO load dev.properties from OSGi configuration directory
-                       if (Files.isDirectory(p))
-                               p = p.resolve("bin");
-                       return p;
+                       Path p = frameworkLocation.getParent().resolve(location).toAbsolutePath();
+                       if (Files.exists(p)) {
+                               p = p.toRealPath();
+                               // TODO load dev.properties from OSGi configuration directory
+                               if (Files.isDirectory(p))
+                                       p = p.resolve("bin");
+                               return p;
+                       } else {
+                               log.warn("Ignore bundle " + p + " as it does not exist");
+                               return null;
+                       }
                }
                Path p = Paths.get(location);
                return p;
index e994463f9606e00d530efef01af0c184b37ad684..98975c3c843ad7c152a4c1815f738a812c6ca130 100644 (file)
@@ -2,11 +2,13 @@ package org.argeo.cms.jetty;
 
 import java.io.IOException;
 import java.net.InetSocketAddress;
+import java.security.NoSuchAlgorithmException;
 import java.util.Map;
 import java.util.TreeMap;
 import java.util.concurrent.Executor;
 import java.util.concurrent.ThreadPoolExecutor;
 
+import javax.net.ssl.SSLContext;
 import javax.servlet.ServletException;
 import javax.websocket.server.ServerContainer;
 
@@ -170,6 +172,16 @@ public class JettyHttpServer extends HttpsServer {
                        }
 
                        if (httpsEnabled) {
+                               if (httpsConfigurator == null) {
+                                       // we make sure that an HttpSConfigurator is set, so that clients can detect
+                                       // whether this server is HTTP or HTTPS
+                                       try {
+                                               httpsConfigurator = new HttpsConfigurator(SSLContext.getDefault());
+                                       } catch (NoSuchAlgorithmException e) {
+                                               throw new IllegalStateException("Cannot initalise SSL Context", e);
+                                       }
+                               }
+
                                SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
                                // sslContextFactory.setKeyStore(KeyS)
 
@@ -259,12 +271,17 @@ public class JettyHttpServer extends HttpsServer {
 
        @Override
        public synchronized void removeContext(String path) throws IllegalArgumentException {
+               if (!path.endsWith("/"))
+                       path = path + "/";
                if (!contexts.containsKey(path))
                        throw new IllegalArgumentException("Context " + path + " does not exist");
                JettyHttpContext httpContext = contexts.remove(path);
                if (httpContext instanceof ContextHandlerHttpContext contextHandlerHttpContext) {
                        // TODO stop handler first?
                        contextHandlerCollection.removeHandler(contextHandlerHttpContext.getServletContextHandler());
+               } else {
+                       // FIXME apparently servlets cannot be removed in Jetty, we should replace the
+                       // handler
                }
        }
 
index 33611941d9d51e1559a574b398bf80aa1569d7e6..b2a472b449910ef7276fd8ac5434032eea3feb59 100644 (file)
@@ -6,7 +6,6 @@ import java.util.Map;
 
 import javax.websocket.DeploymentException;
 import javax.websocket.server.ServerContainer;
-import javax.websocket.server.ServerEndpointConfig;
 
 import org.argeo.api.cms.CmsLog;
 import org.argeo.cms.servlet.httpserver.HttpContextServlet;
diff --git a/org.argeo.cms.lib.json/.classpath b/org.argeo.cms.lib.json/.classpath
new file mode 100644 (file)
index 0000000..81fe078
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17"/>
+       <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/org.argeo.cms.lib.json/.project b/org.argeo.cms.lib.json/.project
new file mode 100644 (file)
index 0000000..cd9a550
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.cms.lib.json</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.jdt.core.javabuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.ManifestBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.SchemaBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/org.argeo.cms.lib.json/.settings/org.eclipse.core.resources.prefs b/org.argeo.cms.lib.json/.settings/org.eclipse.core.resources.prefs
new file mode 100644 (file)
index 0000000..99f26c0
--- /dev/null
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+encoding/<project>=UTF-8
diff --git a/org.argeo.cms.lib.json/.settings/org.eclipse.jdt.core.prefs b/org.argeo.cms.lib.json/.settings/org.eclipse.jdt.core.prefs
new file mode 100644 (file)
index 0000000..62ef348
--- /dev/null
@@ -0,0 +1,9 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
+org.eclipse.jdt.core.compiler.compliance=17
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
+org.eclipse.jdt.core.compiler.release=enabled
+org.eclipse.jdt.core.compiler.source=17
diff --git a/org.argeo.cms.lib.json/.settings/org.eclipse.pde.core.prefs b/org.argeo.cms.lib.json/.settings/org.eclipse.pde.core.prefs
new file mode 100644 (file)
index 0000000..f29e940
--- /dev/null
@@ -0,0 +1,3 @@
+eclipse.preferences.version=1
+pluginProject.extensions=false
+resolve.requirebundle=false
diff --git a/org.argeo.cms.lib.json/bnd.bnd b/org.argeo.cms.lib.json/bnd.bnd
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/org.argeo.cms.lib.json/build.properties b/org.argeo.cms.lib.json/build.properties
new file mode 100644 (file)
index 0000000..34d2e4d
--- /dev/null
@@ -0,0 +1,4 @@
+source.. = src/
+output.. = bin/
+bin.includes = META-INF/,\
+               .
diff --git a/org.argeo.cms.lib.json/src/org/argeo/cms/acr/json/AcrJsonUtils.java b/org.argeo.cms.lib.json/src/org/argeo/cms/acr/json/AcrJsonUtils.java
new file mode 100644 (file)
index 0000000..db42d00
--- /dev/null
@@ -0,0 +1,61 @@
+package org.argeo.cms.acr.json;
+
+import javax.xml.namespace.QName;
+
+import org.argeo.api.acr.Content;
+import org.argeo.api.acr.DName;
+import org.argeo.api.acr.NamespaceUtils;
+import org.argeo.api.acr.QNamed;
+
+import jakarta.json.stream.JsonGenerator;
+
+/** Utilities around ACR and the JSON format. */
+public class AcrJsonUtils {
+       public static void writeAttr(JsonGenerator g, Content content, String attr) {
+               writeAttr(g, content, NamespaceUtils.parsePrefixedName(attr));
+       }
+
+       public static void writeAttr(JsonGenerator g, Content content, QNamed attr) {
+               writeAttr(g, content, attr.qName());
+       }
+
+       public static void writeAttr(JsonGenerator g, Content content, QName attr) {
+               // String value = content.attr(attr);
+               Object value = content.get(attr);
+               if (value != null) {
+                       // TODO specify NamespaceContext
+                       String key = NamespaceUtils.toPrefixedName(attr);
+                       if (value instanceof Double v)
+                               g.write(key, v);
+                       else if (value instanceof Long v)
+                               g.write(key, v);
+                       else if (value instanceof Integer v)
+                               g.write(key, v);
+                       else if (value instanceof Boolean v)
+                               g.write(key, v);
+                       else
+                               g.write(key, value.toString());
+               }
+       }
+
+       /** singleton */
+       private AcrJsonUtils() {
+       }
+
+//     private final QName JCR_CREATED = NamespaceUtils.parsePrefixedName("jcr:created");
+//
+//     private final QName JCR_LAST_MODIFIED = NamespaceUtils.parsePrefixedName("jcr:lastModified");
+
+       public static void writeTimeProperties(JsonGenerator g, Content content) {
+                       String creationDate = content.attr(DName.creationdate);
+       //              if (creationDate == null)
+       //                      creationDate = content.attr(JCR_CREATED);
+                       if (creationDate != null)
+                               g.write(DName.creationdate.get(), creationDate);
+                       String lastModified = content.attr(DName.getlastmodified);
+       //              if (lastModified == null)
+       //                      lastModified = content.attr(JCR_LAST_MODIFIED);
+                       if (lastModified != null)
+                               g.write(DName.getlastmodified.get(), lastModified);
+               }
+}
index 987b977459aef7473db12180e7afa7f78628eb25..8117b03b863a6aca0e3ba4feee89b0cc5142bfbd 100644 (file)
@@ -3,6 +3,6 @@
    <implementation class="org.argeo.cms.ssh.CmsSshServer"/>
    <reference bind="setCmsState" cardinality="1..1" interface="org.argeo.api.cms.CmsState" name="CmsState" policy="static"/>
    <service>
-      <provide interface="org.argeo.cms.CmsSshd"/>
+      <provide interface="org.argeo.api.cms.CmsSshd"/>
    </service>
 </scr:component>
index f5609a37d5f70e2786153c567f96e0d0759a5684..8a6def33ac631c2c88eaf8f8daa308c27394d4a4 100644 (file)
@@ -3,6 +3,7 @@ package org.argeo.cms.ssh;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.Writer;
+import java.net.InetSocketAddress;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -39,9 +40,9 @@ import org.apache.sshd.sftp.server.SftpSubsystemFactory;
 import org.argeo.api.cms.CmsAuth;
 import org.argeo.api.cms.CmsConstants;
 import org.argeo.api.cms.CmsLog;
+import org.argeo.api.cms.CmsSshd;
 import org.argeo.api.cms.CmsState;
 import org.argeo.cms.CmsDeployProperty;
-import org.argeo.cms.CmsSshd;
 
 public class CmsSshServer implements CmsSshd {
        private final static CmsLog log = CmsLog.getLog(CmsSshServer.class);
@@ -215,4 +216,9 @@ public class CmsSshServer implements CmsSshd {
                this.cmsState = cmsState;
        }
 
+       @Override
+       public InetSocketAddress getAddress() {
+               return new InetSocketAddress(host, port);
+       }
+
 }
index 66541827db71f0e9af08059420aa7186fe8d9140..ee86ad59a720cebb083cfda96b48b48c40042f4b 100644 (file)
@@ -2,7 +2,7 @@
 <scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" activate="start" deactivate="stop" immediate="true" name="CMS Deployment">
    <implementation class="org.argeo.cms.internal.runtime.CmsDeploymentImpl"/>
    <reference bind="setCmsState" cardinality="1..1" interface="org.argeo.api.cms.CmsState" name="CmsState" policy="static"/>
-   <reference bind="setCmsSshd" cardinality="0..1" interface="org.argeo.cms.CmsSshd" policy="dynamic"/>
+   <reference bind="setCmsSshd" cardinality="0..1" interface="org.argeo.api.cms.CmsSshd" policy="dynamic"/>
    <reference bind="setHttpServer" cardinality="0..1" interface="com.sun.net.httpserver.HttpServer" policy="dynamic"/>
    <reference bind="addHttpHandler" unbind="removeHttpHandler" cardinality="0..n" interface="com.sun.net.httpserver.HttpHandler" policy="dynamic"/>
    <service>
diff --git a/org.argeo.cms/OSGI-INF/cmsFileSystemProvider.xml b/org.argeo.cms/OSGI-INF/cmsFileSystemProvider.xml
new file mode 100644 (file)
index 0000000..b1402fa
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" activate="start" deactivate="stop" name="org.argeo.cms.cmsFileSystemProvider">
+   <implementation class="org.argeo.cms.file.provider.CmsFileSystemProvider"/>
+   <reference bind="setContentRepository" cardinality="1..1" interface="org.argeo.api.acr.spi.ProvidedRepository" name="ProvidedRepository" policy="static"/>
+   <service>
+      <provide interface="java.nio.file.spi.FileSystemProvider"/>
+   </service>
+</scr:component>
index ade2f3aa94f72ac1f1d2eee2616dffb48a30a4ef..01443b5e8150215f77435c4f4a8ce56850f3895e 100644 (file)
@@ -16,4 +16,5 @@ OSGI-INF/cmsContentRepository.xml,\
 OSGI-INF/cmsAcrHttpHandler.xml,\
 OSGI-INF/cmsDeployment.xml,\
 OSGI-INF/cmsContext.xml,\
+OSGI-INF/cmsFileSystemProvider.xml,\
 
index 6ca041a2a19c39c0db563da5d0bc0ab660494744..04d4cb94a07bb3fb1817dd5540142b7f48758353 100644 (file)
@@ -2,6 +2,7 @@ bin.includes = META-INF/,\
                .,\
                bin/,\
                OSGI-INF/,\
-               OSGI-INF/cmsEventBus.xml
+               OSGI-INF/cmsEventBus.xml,\
+               OSGI-INF/cmsFileSystemProvider.xml
 source.. = src/
 output.. = bin/
diff --git a/org.argeo.cms/src/org/argeo/cms/CmsSshd.java b/org.argeo.cms/src/org/argeo/cms/CmsSshd.java
deleted file mode 100644 (file)
index 41968be..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-package org.argeo.cms;
-
-import org.argeo.api.cms.CmsConstants;
-
-/** Just a marker interface for the time being. */
-public interface CmsSshd {
-       final static String NODE_USERNAME_ALIAS = "user.name";
-       final static String DEFAULT_SSH_HOST_KEY_PATH = "private/" + CmsConstants.NODE + ".ser";
-}
index 8aca8768a04d9e3af196ce9690ea449ba1d7d651..a09bf5a934187a1b7e878d95fdfca89a1748cfc6 100644 (file)
@@ -77,6 +77,8 @@ public class LocaleUtils {
 
        /** Lead transformation on the translated string. */
        public static String toLead(String raw, Locale locale) {
+               if ("".equals(raw))
+                       return "";
                return raw.substring(0, 1).toUpperCase(locale) + raw.substring(1);
        }
 
index daefe9835e371c4210bf6487ce108ac04700e05d..c782256e282acb94b97a7233ce0ddb4ad9d2cf0c 100644 (file)
@@ -206,10 +206,12 @@ class CmsContentSession implements ProvidedSession, UuidIdentified {
                        NavigableMap<String, ContentProvider> contentProviders = contentRepository.getMountManager()
                                        .findContentProviders(scopePath);
                        for (Map.Entry<String, ContentProvider> contentProvider : contentProviders.entrySet()) {
+                               assert scopePath.startsWith(contentProvider.getKey())
+                                               : "scopePath=" + scopePath + ", contentProvider path=" + contentProvider.getKey();
                                // TODO deal with depth
                                String relPath;
-                               if (scopePath.startsWith(contentProvider.getKey())) {
-                                       relPath = scopePath.substring(contentProvider.getKey().length());
+                               if (!scopePath.equals(contentProvider.getKey())) {
+                                       relPath = scopePath.substring(contentProvider.getKey().length() + 1, scopePath.length());
                                } else {
                                        relPath = null;
                                }
@@ -217,6 +219,8 @@ class CmsContentSession implements ProvidedSession, UuidIdentified {
                                searchPartitions.put(contentProvider.getKey(), searchPartition);
                        }
                }
+               if (searchPartitions.isEmpty())
+                       return Stream.empty();
                return StreamSupport.stream(new SearchPartitionsSpliterator(searchPartitions), true);
        }
 
index bf5954411bc157a32003bf17d3e4363e8ee9d5bc..facb5933bf3ebe743ce99eeed8e57c300d0cb11b 100644 (file)
@@ -1,10 +1,13 @@
 package org.argeo.cms.acr;
 
 import java.io.PrintStream;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
 import java.util.StringJoiner;
+import java.util.StringTokenizer;
 import java.util.function.BiConsumer;
 
 import javax.security.auth.login.LoginContext;
@@ -16,6 +19,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.CmsConstants;
 import org.argeo.api.cms.CmsSession;
 import org.argeo.api.cms.directory.CmsDirectory;
 import org.argeo.api.cms.directory.CmsUserManager;
@@ -124,6 +128,16 @@ public class ContentUtils {
                        throw new IllegalArgumentException("Path " + path + " contains //");
        }
 
+       /** The last element of a path. */
+       public static String lastPathElement(String path) {
+               if (path.charAt(path.length() - 1) == '/')
+                       throw new IllegalArgumentException("Path " + path + " cannot end with '/'");
+               int index = path.lastIndexOf('/');
+               if (index < 0)
+                       return path;
+               return path.substring(index + 1);
+       }
+
        /*
         * DIRECTORY
         */
@@ -217,6 +231,33 @@ public class ContentUtils {
                return relativePath;
        }
 
+       /** A path in the node repository */
+       public static String getDataPath(Content node) {
+               // TODO make it more configurable?
+               StringBuilder buf = new StringBuilder(CmsConstants.PATH_API_ACR);
+               buf.append(node.getPath());
+               return buf.toString();
+       }
+
+       /** A path in the node repository */
+       public static String getDataPathForUrl(Content node) {
+               return cleanPathForUrl(getDataPath(node));
+       }
+
+       /** Clean reserved URL characters for use in HTTP links. */
+       public static String cleanPathForUrl(String path) {
+               StringTokenizer st = new StringTokenizer(path, "/");
+               StringBuilder sb = new StringBuilder();
+               while (st.hasMoreElements()) {
+                       sb.append('/');
+                       String encoded = URLEncoder.encode(st.nextToken(), StandardCharsets.UTF_8);
+                       encoded = encoded.replace("+", "%20");
+                       sb.append(encoded);
+
+               }
+               return sb.toString();
+       }
+
        /** Singleton. */
        private ContentUtils() {
 
index 75ca427c7e25bdee4d96e7bbd768c65c959b3e90..90d621b761e57359ac14b02d2aeed2de47526146 100644 (file)
@@ -51,10 +51,10 @@ class MountManager {
        synchronized ContentProvider findContentProvider(String path) {
 //             if (ContentUtils.EMPTY.equals(path))
 //                     return partitions.firstEntry().getValue();
-               Map.Entry<String, ContentProvider> entry = partitions.floorEntry(path);
-               if (entry == null)
-                       throw new IllegalArgumentException("No entry provider found for path '" + path + "'");
-               String mountPath = entry.getKey();
+               Map.Entry<String, ContentProvider> floorEntry = partitions.floorEntry(path);
+               if (floorEntry == null)
+                       throw new IllegalArgumentException("No floor entry provider found for path '" + path + "'");
+               String mountPath = floorEntry.getKey();
                if (!path.startsWith(mountPath)) {
                        // FIXME make it more robust and find when there is no content provider
                        String[] parent = ContentUtils.getParentPath(path);
@@ -62,15 +62,22 @@ class MountManager {
                        // throw new IllegalArgumentException("Path " + path + " doesn't have a content
                        // provider");
                }
-               ContentProvider contentProvider = entry.getValue();
+               ContentProvider contentProvider = floorEntry.getValue();
                assert mountPath.equals(contentProvider.getMountPath());
                return contentProvider;
        }
 
-       /** All content provider under this path. */
+       /** All content providers under this path. */
        synchronized NavigableMap<String, ContentProvider> findContentProviders(String path) {
+               Map.Entry<String, ContentProvider> floorEntry = partitions.floorEntry(path);
+               if (floorEntry == null)
+                       throw new IllegalArgumentException("No floor entry provider found for path '" + path + "'");
+               // we first find the parent provider
+               String parentProviderPath = floorEntry.getKey();
+               // then gather all sub-providers
                NavigableMap<String, ContentProvider> res = new TreeMap<>();
-               tail: for (Map.Entry<String, ContentProvider> provider : partitions.tailMap(path).entrySet()) {
+               res.put(floorEntry.getKey(), floorEntry.getValue());
+               tail: for (Map.Entry<String, ContentProvider> provider : partitions.tailMap(parentProviderPath).entrySet()) {
                        if (!provider.getKey().startsWith(path))
                                break tail;
                        res.put(provider.getKey(), provider.getValue());
index a5abe8dd47f165f170d98154ee559d80987bec97..845a6ab4c22a23629194b6bab0e3a0609d8ed2d9 100644 (file)
@@ -77,7 +77,7 @@ public class DomContentProvider implements ContentProvider, NamespaceContext {
                        throw new IllegalArgumentException("Relative path cannot start with /");
                String xPathExpression = '/' + relativePath;
                if (Content.ROOT_PATH.equals(mountPath)) // repository root
-                       xPathExpression = "/" + CrName.root.qName() + xPathExpression;
+                       xPathExpression = "/" + CrName.root.get() + xPathExpression;
                try {
                        NodeList nodes = (NodeList) xPath.get().evaluate(xPathExpression, document, XPathConstants.NODESET);
                        return nodes;
index 10e091ead0a4953e2236f2ad4860c5d04ba277c2..cfffb6eea74e43a50dde96ff3e87e0288beb82b3 100644 (file)
@@ -69,8 +69,14 @@ public class SingleUserLoginModule implements LoginModule {
                        locale = request.getLocale();
                if (locale == null)
                        locale = Locale.getDefault();
-               Authorization authorization = new SingleUserAuthorization(authorizationName);
-               CmsAuthUtils.addAuthorization(subject, authorization);
+
+               Authorization authorization = null;
+               if (kerberosPrincipal != null) {
+                       authorization = new SingleUserAuthorization(authorizationName);
+                       CmsAuthUtils.addAuthorization(subject, authorization);
+               } else {
+                       // next step with user admin will properly populate
+               }
 
                // Add standard Java OS login
                OsUserUtils.loginAsSystemUser(subject);
@@ -81,7 +87,8 @@ public class SingleUserLoginModule implements LoginModule {
 //             principals.add(new ImpliedByPrincipal(NodeConstants.ROLE_ADMIN, principal));
 //             principals.add(new DataAdminPrincipal());
 
-               CmsAuthUtils.registerSessionAuthorization(request, subject, authorization, locale);
+               if (authorization != null)
+                       CmsAuthUtils.registerSessionAuthorization(request, subject, authorization, locale);
 
                return true;
        }
index a4da893b6d627259c11d1a839604c46021bd1a57..31fe9c73c3a6f05a505515957793afc7e4009a87 100644 (file)
@@ -5,24 +5,30 @@ import java.nio.file.attribute.FileAttributeView;
 import java.nio.file.attribute.FileStoreAttributeView;
 
 import org.argeo.api.acr.fs.AbstractFsStore;
+import org.argeo.api.acr.spi.ContentProvider;
 
 public class CmsFileStore extends AbstractFsStore {
+       private final ContentProvider contentProvider;
+
+       public CmsFileStore(ContentProvider contentProvider) {
+               this.contentProvider = contentProvider;
+       }
 
        @Override
        public String name() {
-               // TODO Auto-generated method stub
-               return null;
+               // TODO return an URI
+               String name = contentProvider.getMountPath();
+               return name;
        }
 
        @Override
        public String type() {
-               // TODO Auto-generated method stub
-               return null;
+               String type = contentProvider.getClass().getName();
+               return type;
        }
 
        @Override
        public boolean isReadOnly() {
-               // TODO Auto-generated method stub
                return false;
        }
 
@@ -46,13 +52,15 @@ public class CmsFileStore extends AbstractFsStore {
 
        @Override
        public boolean supportsFileAttributeView(Class<? extends FileAttributeView> type) {
-               // TODO Auto-generated method stub
+               if (ContentFileAttributeView.class.isAssignableFrom(type))
+                       return true;
                return false;
        }
 
        @Override
        public boolean supportsFileAttributeView(String name) {
-               // TODO Auto-generated method stub
+               if (ContentFileAttributeView.NAME.equals(name))
+                       return true;
                return false;
        }
 
index 6d4eea279c85f42f040ca5c6d599c881106f8617..0b5d5defe39aa34fb8fd04f556cb19d23f1116b5 100644 (file)
@@ -7,94 +7,131 @@ import java.nio.file.PathMatcher;
 import java.nio.file.WatchService;
 import java.nio.file.attribute.UserPrincipalLookupService;
 import java.nio.file.spi.FileSystemProvider;
+import java.util.Collections;
 import java.util.Set;
 
 import org.argeo.api.acr.fs.AbstractFsSystem;
+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.cms.CmsSession;
+import org.argeo.cms.acr.ContentUtils;
 
 public class CmsFileSystem extends AbstractFsSystem<CmsFileStore> {
+       private final CmsFileSystemProvider provider;
+//     private final ProvidedRepository contentRepository;
+       private final CmsSession cmsSession;
+       private final ProvidedSession contentSession;
+
+       private final CmsPath rootPath;
+       private final CmsFileStore baseFileStore;
+
+       public CmsFileSystem(CmsFileSystemProvider provider, ProvidedRepository contentRepository, CmsSession cmsSession) {
+               this.provider = provider;
+//             this.contentRepository = contentRepository;
+               this.cmsSession = cmsSession;
+               this.contentSession = (ProvidedSession) ContentUtils.openSession(contentRepository, cmsSession);
+
+               rootPath = new CmsPath(this, ProvidedContent.ROOT_PATH);
+               baseFileStore = new CmsFileStore(rootPath.getContent().getProvider());
+       }
 
        @Override
        public CmsFileStore getBaseFileStore() {
-               // TODO Auto-generated method stub
-               return null;
+               return baseFileStore;
        }
 
        @Override
        public CmsFileStore getFileStore(String path) {
-               // TODO Auto-generated method stub
-               return null;
+               ProvidedContent c = (ProvidedContent) contentSession.get(path);
+               return new CmsFileStore(c.getProvider());
        }
 
        @Override
        public FileSystemProvider provider() {
-               // TODO Auto-generated method stub
-               return null;
+               return provider;
        }
 
        @Override
        public void close() throws IOException {
-               // TODO Auto-generated method stub
-
+               // TODO close content session?
+               provider.close(this);
        }
 
        @Override
        public boolean isOpen() {
-               // TODO Auto-generated method stub
-               return false;
+               // TODO check provider
+               return true;
        }
 
        @Override
        public boolean isReadOnly() {
-               // TODO Auto-generated method stub
                return false;
        }
 
        @Override
        public String getSeparator() {
-               // TODO Auto-generated method stub
-               return null;
+               return CmsPath.SEPARATOR;
        }
 
        @Override
        public Iterable<Path> getRootDirectories() {
-               // TODO Auto-generated method stub
-               return null;
+               return Collections.singleton(rootPath);
        }
 
        @Override
        public Iterable<FileStore> getFileStores() {
-               // TODO Auto-generated method stub
-               return null;
+               // TODO return all mount points
+               return Collections.singleton(baseFileStore);
        }
 
        @Override
        public Set<String> supportedFileAttributeViews() {
-               // TODO Auto-generated method stub
-               return null;
+               return Collections.singleton(ContentFileAttributeView.NAME);
        }
 
        @Override
        public Path getPath(String first, String... more) {
-               // TODO Auto-generated method stub
-               return null;
+               StringBuilder sb = new StringBuilder(first);
+               // TODO Make it more robust
+               for (String part : more)
+                       sb.append('/').append(part);
+               return new CmsPath(this, sb.toString());
        }
 
        @Override
        public PathMatcher getPathMatcher(String syntaxAndPattern) {
-               // TODO Auto-generated method stub
                return null;
        }
 
        @Override
        public UserPrincipalLookupService getUserPrincipalLookupService() {
-               // TODO Auto-generated method stub
                return null;
        }
 
        @Override
        public WatchService newWatchService() throws IOException {
-               // TODO Auto-generated method stub
                return null;
        }
 
+       /*
+        * ACR
+        */
+
+       ProvidedContent getContent(String acrPath) {
+               return (ProvidedContent) contentSession.get(acrPath);
+       }
+
+       ProvidedSession getContentSession() {
+               return contentSession;
+       }
+
+       /*
+        * CMS
+        */
+
+       CmsSession getCmsSession() {
+               return cmsSession;
+       }
+
 }
index 51eb84e713204f057f509ae5613949192436dfe4..602441e2fc3d3a3d8e8ecf64e749c3171e96582b 100644 (file)
@@ -1,25 +1,51 @@
 package org.argeo.cms.file.provider;
 
 import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.net.URI;
 import java.nio.channels.SeekableByteChannel;
 import java.nio.file.AccessMode;
 import java.nio.file.CopyOption;
 import java.nio.file.DirectoryStream;
 import java.nio.file.DirectoryStream.Filter;
+import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.FileStore;
 import java.nio.file.FileSystem;
+import java.nio.file.FileSystemAlreadyExistsException;
 import java.nio.file.LinkOption;
+import java.nio.file.NoSuchFileException;
 import java.nio.file.OpenOption;
 import java.nio.file.Path;
 import java.nio.file.attribute.BasicFileAttributes;
 import java.nio.file.attribute.FileAttribute;
 import java.nio.file.attribute.FileAttributeView;
 import java.nio.file.spi.FileSystemProvider;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
 
+import javax.xml.namespace.QName;
+
+import org.argeo.api.acr.DName;
+import org.argeo.api.acr.NamespaceUtils;
+import org.argeo.api.acr.spi.ProvidedRepository;
+import org.argeo.api.acr.spi.ProvidedSession;
+import org.argeo.api.cms.CmsSession;
+import org.argeo.cms.CurrentUser;
+
 public class CmsFileSystemProvider extends FileSystemProvider {
+       private Map<CmsSession, CmsFileSystem> fileSystems = Collections.synchronizedMap(new HashMap<>());
+
+       private ProvidedRepository contentRepository;
+
+       public void start() {
+
+       }
+
+       public void stop() {
+
+       }
 
        @Override
        public String getScheme() {
@@ -28,20 +54,48 @@ public class CmsFileSystemProvider extends FileSystemProvider {
 
        @Override
        public FileSystem newFileSystem(URI uri, Map<String, ?> env) throws IOException {
-               // TODO Auto-generated method stub
-               return null;
+               CmsSession cmsSession = CurrentUser.getCmsSession();
+               if (cmsSession.isAnonymous()) {
+                       // TODO deal with anonymous
+                       return null;
+               }
+               if (fileSystems.containsKey(cmsSession))
+                       throw new FileSystemAlreadyExistsException("CMS file system already exists for user " + cmsSession);
+
+               String host = uri.getHost();
+               if (host != null && !host.trim().equals("")) {
+//                             URI repoUri = new URI("http", uri.getUserInfo(), uri.getHost(), uri.getPort(), "/jcr/node", null, null);
+                       // FIXME deal with remote
+                       CmsFileSystem fileSystem = null;
+                       fileSystems.put(cmsSession, fileSystem);
+                       return fileSystem;
+               } else {
+                       // FIXME send exception if it exists already
+                       CmsFileSystem fileSystem = new CmsFileSystem(this, contentRepository, cmsSession);
+                       fileSystems.put(cmsSession, fileSystem);
+                       cmsSession.addOnCloseCallback((s) -> {
+                               fileSystems.remove(s);
+                       });
+                       return fileSystem;
+               }
        }
 
        @Override
        public FileSystem getFileSystem(URI uri) {
-               // TODO Auto-generated method stub
-               return null;
+               return currentUserFileSystem();
        }
 
        @Override
        public Path getPath(URI uri) {
-               // TODO Auto-generated method stub
-               return null;
+               CmsFileSystem fileSystem = currentUserFileSystem();
+               String path = uri.getPath();
+               if (fileSystem == null)
+                       try {
+                               fileSystem = (CmsFileSystem) newFileSystem(uri, new HashMap<String, Object>());
+                       } catch (IOException e) {
+                               throw new UncheckedIOException("Could not autocreate file system for " + uri, e);
+                       }
+               return fileSystem.getPath(path);
        }
 
        @Override
@@ -53,20 +107,34 @@ public class CmsFileSystemProvider extends FileSystemProvider {
 
        @Override
        public DirectoryStream<Path> newDirectoryStream(Path dir, Filter<? super Path> filter) throws IOException {
-               // TODO Auto-generated method stub
-               return null;
+               CmsPath cmsPath = (CmsPath) dir;
+               return new ContentDirectoryStream(cmsPath, filter);
        }
 
        @Override
        public void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException {
-               // TODO Auto-generated method stub
+               CmsPath cmsPath = (CmsPath) dir;
+               ProvidedSession contentSession = cmsPath.getContentSession();
+               if (contentSession.exists(dir.toString()))
+                       throw new FileAlreadyExistsException(dir.toString());
 
+               CmsPath parent = (CmsPath) cmsPath.getParent();
+               if (!contentSession.exists(parent.toString()))
+                       throw new NoSuchFileException(parent.toString());
+               // TODO use a proper naming context
+               QName fileName = NamespaceUtils.parsePrefixedName(dir.getFileName().toString());
+               parent.getContent().add(fileName, DName.collection);
        }
 
        @Override
        public void delete(Path path) throws IOException {
-               // TODO Auto-generated method stub
-
+               CmsPath cmsPath = (CmsPath) path;
+               ProvidedSession contentSession = cmsPath.getContentSession();
+               if (!contentSession.exists(cmsPath.toString()))
+                       throw new NoSuchFileException(cmsPath.toString());
+               contentSession.edit((s) -> {
+                       cmsPath.getContent().remove();
+               });
        }
 
        @Override
@@ -83,39 +151,42 @@ public class CmsFileSystemProvider extends FileSystemProvider {
 
        @Override
        public boolean isSameFile(Path path, Path path2) throws IOException {
-               // TODO Auto-generated method stub
-               return false;
+               // TODO make it smarter
+               return path.toString().equals(path2.toString());
        }
 
        @Override
        public boolean isHidden(Path path) throws IOException {
-               // TODO Auto-generated method stub
                return false;
        }
 
        @Override
        public FileStore getFileStore(Path path) throws IOException {
-               // TODO Auto-generated method stub
-               return null;
+               CmsFileSystem fileSystem = (CmsFileSystem) path.getFileSystem();
+               return fileSystem.getFileStore(path.toString());
        }
 
        @Override
        public void checkAccess(Path path, AccessMode... modes) throws IOException {
-               // TODO Auto-generated method stub
-
        }
 
+       @SuppressWarnings("unchecked")
        @Override
        public <V extends FileAttributeView> V getFileAttributeView(Path path, Class<V> type, LinkOption... options) {
-               // TODO Auto-generated method stub
+               CmsPath cmsPath = (CmsPath) path;
+               if (BasicFileAttributes.class.isAssignableFrom(type))
+                       return (V) new ContentFileAttributeView(cmsPath.getContent());
+               else if (ContentFileAttributeView.class.isAssignableFrom(type))
+                       return (V) new ContentFileAttributeView(cmsPath.getContent());
                return null;
        }
 
+       @SuppressWarnings("unchecked")
        @Override
        public <A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type, LinkOption... options)
                        throws IOException {
-               // TODO Auto-generated method stub
-               return null;
+               CmsPath cmsPath = (CmsPath) path;
+               return (A) new ContentAttributes(cmsPath.getContent());
        }
 
        @Override
@@ -130,4 +201,26 @@ public class CmsFileSystemProvider extends FileSystemProvider {
 
        }
 
+       /*
+        * UTILITIES
+        */
+
+       CmsFileSystem currentUserFileSystem() {
+               CmsSession cmsSession = CurrentUser.getCmsSession();
+               return fileSystems.get(cmsSession);
+       }
+
+       void close(CmsFileSystem fileSystem) {
+               CmsSession cmsSession = fileSystem.getCmsSession();
+               CmsFileSystem ref = fileSystems.remove(cmsSession);
+               assert ref == fileSystem;
+       }
+
+       /*
+        * DEPENDENCY INJECTION
+        */
+       public void setContentRepository(ProvidedRepository contentRepository) {
+               this.contentRepository = contentRepository;
+       }
+
 }
index 504e69bff99c2e86e5438faa9bb1401f07e0d0d4..ce5e4b9e549c7dd78f3efefa99c1d14ac72b350b 100644 (file)
@@ -1,8 +1,27 @@
 package org.argeo.cms.file.provider;
 
+import org.argeo.api.acr.Content;
 import org.argeo.api.acr.fs.AbstractFsPath;
+import org.argeo.api.acr.spi.ProvidedContent;
+import org.argeo.api.acr.spi.ProvidedSession;
 
 public class CmsPath extends AbstractFsPath<CmsFileSystem, CmsFileStore> {
+       final static String SEPARATOR = "/";
+
+       // lazy loaded
+       private ProvidedContent content;
+
+       ProvidedContent getContent() {
+               if (content == null) {
+                       content = getFileSystem().getContent(toString());
+               }
+               return content;
+       }
+
+       CmsPath(CmsFileSystem fileSystem, Content content) {
+               this(fileSystem, content.getPath());
+               this.content = (ProvidedContent) content;
+       }
 
        public CmsPath(CmsFileSystem filesSystem, CmsFileStore fileStore, String[] segments, boolean absolute) {
                super(filesSystem, fileStore, segments, absolute);
@@ -22,4 +41,8 @@ public class CmsPath extends AbstractFsPath<CmsFileSystem, CmsFileStore> {
                return new CmsPath(getFileSystem(), getFileStore(), segments, absolute);
        }
 
+       ProvidedSession getContentSession() {
+               return getFileSystem().getContentSession();
+       }
+
 }
diff --git a/org.argeo.cms/src/org/argeo/cms/file/provider/ContentAttributes.java b/org.argeo.cms/src/org/argeo/cms/file/provider/ContentAttributes.java
new file mode 100644 (file)
index 0000000..827bb2a
--- /dev/null
@@ -0,0 +1,80 @@
+package org.argeo.cms.file.provider;
+
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileTime;
+import java.time.Instant;
+
+import org.argeo.api.acr.Content;
+import org.argeo.api.acr.DName;
+
+public class ContentAttributes implements BasicFileAttributes {
+       // TODO optimise for FS-based content
+       private final Content content;
+
+       public ContentAttributes(Content content) {
+               assert content != null;
+               this.content = content;
+       }
+
+       @Override
+       public FileTime lastModifiedTime() {
+               Instant t = content.get(DName.getlastmodified, Instant.class).orElseThrow();
+               return FileTime.from(t);
+       }
+
+       @Override
+       public FileTime lastAccessTime() {
+               // TODO implement the concept in ACR ?
+               return FileTime.fromMillis(0l);
+       }
+
+       @Override
+       public FileTime creationTime() {
+               Instant t = content.get(DName.getlastmodified, Instant.class).orElseThrow();
+               return FileTime.from(t);
+       }
+
+       @Override
+       public boolean isRegularFile() {
+               return isRegularFile(content);
+       }
+
+       @Override
+       public boolean isDirectory() {
+               return isDirectory(content);
+       }
+
+       @Override
+       public boolean isSymbolicLink() {
+               // TODO supports links in ACR
+               return false;
+       }
+
+       @Override
+       public boolean isOther() {
+               return !isDirectory() && !isRegularFile() && !isSymbolicLink();
+       }
+
+       @Override
+       public long size() {
+               long size = content.get(DName.getcontentlength, Long.class).orElse(-1l);
+               return size;
+       }
+
+       @Override
+       public Object fileKey() {
+               // TODO check for UUIDs, etc.
+               return null;
+       }
+
+       static boolean isDirectory(Content c) {
+               return !isRegularFile(c);
+//             return c.isContentClass(DName.collection);
+       }
+
+       static boolean isRegularFile(Content c) {
+//             return c.containsKey(DName.getcontenttype.qName());
+               return !c.get(DName.getcontenttype, String.class).isEmpty();
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/file/provider/ContentDirectoryStream.java b/org.argeo.cms/src/org/argeo/cms/file/provider/ContentDirectoryStream.java
new file mode 100644 (file)
index 0000000..55db443
--- /dev/null
@@ -0,0 +1,79 @@
+package org.argeo.cms.file.provider;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Path;
+import java.util.Iterator;
+
+import org.argeo.api.acr.Content;
+
+public class ContentDirectoryStream implements DirectoryStream<Path> {
+       private final CmsPath dir;
+       private final Filter<? super Path> filter;
+
+       private FilesAndCollectionsIterator iterator;
+
+       public ContentDirectoryStream(CmsPath dir, Filter<? super Path> filter) {
+               this.dir = dir;
+               this.filter = filter;
+       }
+
+       @Override
+       public void close() throws IOException {
+       }
+
+       @Override
+       public Iterator<Path> iterator() {
+               if (iterator == null)
+                       iterator = new FilesAndCollectionsIterator();
+               return iterator;
+       }
+
+       class FilesAndCollectionsIterator implements Iterator<Path> {
+               private Content next;
+               private final Iterator<Content> it;
+
+               public FilesAndCollectionsIterator() {
+                       Content content = dir.getContent();
+                       if (!ContentAttributes.isDirectory(content))
+                               throw new IllegalStateException("Content " + content + " is not a collection");
+                       it = content.iterator();
+                       findNext();
+               }
+
+               private void findNext() {
+                       next = null;
+                       while (it.hasNext() && next == null) {
+                               Content n = it.next();
+                               if (ContentAttributes.isRegularFile(n) || ContentAttributes.isDirectory(n)) {
+                                       if (filter != null) {
+                                               try {
+                                                       if (filter.accept(new CmsPath(dir.getFileSystem(), n)))
+                                                               next = n;
+                                               } catch (IOException e) {
+                                                       throw new UncheckedIOException("Cannot filter " + dir, e);
+                                               }
+                                       } else {
+                                               next = n;
+                                       }
+                               }
+                       }
+               }
+
+               @Override
+               public boolean hasNext() {
+                       return next != null;
+               }
+
+               @Override
+               public Path next() {
+                       if (next == null)
+                               throw new IllegalStateException("Iterator doesn't have more elements");
+                       CmsPath p = new CmsPath(dir.getFileSystem(), next);
+                       findNext();
+                       return p;
+               }
+
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/file/provider/ContentFileAttributeView.java b/org.argeo.cms/src/org/argeo/cms/file/provider/ContentFileAttributeView.java
new file mode 100644 (file)
index 0000000..adbe8d0
--- /dev/null
@@ -0,0 +1,83 @@
+package org.argeo.cms.file.provider;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.file.attribute.BasicFileAttributeView;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.attribute.UserDefinedFileAttributeView;
+import java.util.List;
+
+import org.argeo.api.acr.Content;
+import org.argeo.api.acr.DName;
+
+public class ContentFileAttributeView implements BasicFileAttributeView, UserDefinedFileAttributeView {
+       final static String NAME = "content";
+
+       private final Content content;
+
+       public ContentFileAttributeView(Content content) {
+               this.content = content;
+       }
+
+       @Override
+       public String name() {
+               return NAME;
+       }
+
+       /*
+        * BasicFileAttributeView
+        */
+
+       @Override
+       public BasicFileAttributes readAttributes() throws IOException {
+               return new ContentAttributes(content);
+       }
+
+       @Override
+       public void setTimes(FileTime lastModifiedTime, FileTime lastAccessTime, FileTime createTime) throws IOException {
+               if (lastModifiedTime != null)
+                       content.put(DName.getlastmodified, lastModifiedTime.toInstant());
+               if (createTime != null)
+                       content.put(DName.getlastmodified, createTime.toInstant());
+               // ignore last accessed time
+       }
+
+       /*
+        * UserDefinedFileAttributeView
+        */
+
+       @Override
+       public List<String> list() throws IOException {
+//             List<String> res = new ArrayList<>();
+               return null;
+       }
+
+       @Override
+       public int size(String name) throws IOException {
+               // TODO Auto-generated method stub
+               return 0;
+       }
+
+       @Override
+       public int read(String name, ByteBuffer dst) throws IOException {
+               // TODO Auto-generated method stub
+               return 0;
+       }
+
+       @Override
+       public int write(String name, ByteBuffer src) throws IOException {
+               // TODO Auto-generated method stub
+               return 0;
+       }
+
+       @Override
+       public void delete(String name) throws IOException {
+               // TODO Auto-generated method stub
+
+       }
+
+       public Content getContent() {
+               return content;
+       }
+}
index ef7385d1d5516899100c4c30648582bf593c3ff7..67eec9172048164800d112a81c2fef74c672ce9b 100644 (file)
@@ -6,6 +6,10 @@ public enum HttpHeader {
        WWW_AUTHENTICATE("WWW-Authenticate"), //
        ALLOW("Allow"), //
        VIA("Via"), //
+       CONTENT_TYPE("Content-Type"), //
+       CONTENT_LENGTH("Content-Length"), //
+       CONTENT_DISPOSITION("Content-Disposition"), //
+       DATE("Date"), //
 
        // WebDav
        DAV("DAV"), //
@@ -15,10 +19,15 @@ public enum HttpHeader {
        X_FORWARDED_HOST("X-Forwarded-Host"), //
        ;
 
+       // WWW-Authenticate related constants
        public final static String BASIC = "Basic";
        public final static String REALM = "realm";
        public final static String NEGOTIATE = "Negotiate";
 
+       // Content-Disposition related constants
+       public final static String ATTACHMENT = "attachment";
+       public final static String FILENAME = "filename";
+
        private final String name;
 
        private HttpHeader(String headerName) {
diff --git a/org.argeo.cms/src/org/argeo/cms/http/RemoteAuthHttpExchange.java b/org.argeo.cms/src/org/argeo/cms/http/RemoteAuthHttpExchange.java
new file mode 100644 (file)
index 0000000..7f34a52
--- /dev/null
@@ -0,0 +1,89 @@
+package org.argeo.cms.http;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+
+import org.argeo.cms.auth.RemoteAuthRequest;
+import org.argeo.cms.auth.RemoteAuthResponse;
+import org.argeo.cms.auth.RemoteAuthSession;
+
+import com.sun.net.httpserver.HttpExchange;
+
+/**
+ * Implementation of {@link RemoteAuthRequest} and {@link RemoteAuthResponse}
+ * based on {@link HttpExchange}.
+ */
+public class RemoteAuthHttpExchange implements RemoteAuthRequest, RemoteAuthResponse {
+       private final HttpExchange httpExchange;
+       private RemoteAuthSession remoteAuthSession;
+
+       public RemoteAuthHttpExchange(HttpExchange httpExchange) {
+               this.httpExchange = httpExchange;
+               this.remoteAuthSession = (RemoteAuthSession) httpExchange.getAttribute(RemoteAuthSession.class.getName());
+               Objects.requireNonNull(this.remoteAuthSession);
+       }
+
+       @Override
+       public void setHeader(String headerName, String value) {
+               httpExchange.getResponseHeaders().put(headerName, Collections.singletonList(value));
+       }
+
+       @Override
+       public void addHeader(String headerName, String value) {
+               List<String> values = httpExchange.getResponseHeaders().getOrDefault(headerName, new ArrayList<>());
+               values.add(value);
+       }
+
+       @Override
+       public RemoteAuthSession getSession() {
+               return remoteAuthSession;
+       }
+
+       @Override
+       public RemoteAuthSession createSession() {
+               throw new UnsupportedOperationException("Cannot create remote session");
+       }
+
+       @Override
+       public Locale getLocale() {
+               // TODO Auto-generated method stub
+               return null;
+       }
+
+       @Override
+       public Object getAttribute(String key) {
+               return httpExchange.getAttribute(key);
+       }
+
+       @Override
+       public void setAttribute(String key, Object object) {
+               httpExchange.setAttribute(key, object);
+       }
+
+       @Override
+       public String getHeader(String key) {
+               List<String> lst = httpExchange.getRequestHeaders().get(key);
+               if (lst == null || lst.size() == 0)
+                       return null;
+               return lst.get(0);
+       }
+
+       @Override
+       public int getLocalPort() {
+               return httpExchange.getLocalAddress().getPort();
+       }
+
+       @Override
+       public String getRemoteAddr() {
+               return httpExchange.getRemoteAddress().getHostName();
+       }
+
+       @Override
+       public int getRemotePort() {
+               return httpExchange.getRemoteAddress().getPort();
+       }
+
+}
index ab033f0ce068fa36a383769b84f56367e4e5fc37..9d2c1930ae09e7cfd0f400553b5372054bca2508 100644 (file)
@@ -1,8 +1,25 @@
 package org.argeo.cms.http.server;
 
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.UncheckedIOException;
 import java.net.URI;
+import java.net.URLDecoder;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 
+import org.argeo.api.acr.ContentRepository;
+import org.argeo.api.acr.ContentSession;
+import org.argeo.cms.auth.RemoteAuthUtils;
+import org.argeo.cms.http.HttpMethod;
+import org.argeo.cms.http.RemoteAuthHttpExchange;
+
 import com.sun.net.httpserver.HttpContext;
 import com.sun.net.httpserver.HttpExchange;
 
@@ -39,6 +56,64 @@ public class HttpServerUtils {
                return extractPathWithingContext(httpContext, uri.getPath(), true);
        }
 
+       /** Returns content session consistent with this HTTP context. */
+       public static ContentSession getContentSession(ContentRepository contentRepository, HttpExchange exchange) {
+               ContentSession session = RemoteAuthUtils.doAs(() -> contentRepository.get(),
+                               new RemoteAuthHttpExchange(exchange));
+               return session;
+       }
+
+       /*
+        * QUERY PARAMETERS
+        */
+       /** Returns the HTTP parameters form an {@link HttpExchange}. */
+       public static Map<String, List<String>> parseParameters(HttpExchange exchange) {
+               // TODO check encoding?
+               Charset encoding = StandardCharsets.UTF_8;
+
+               Map<String, List<String>> parameters = new HashMap<>();
+               URI requestedUri = exchange.getRequestURI();
+               String query = requestedUri.getRawQuery();
+               parseQuery(query, parameters, encoding);
+
+               // TODO do we really want to support POST?
+               if (HttpMethod.POST.name().equalsIgnoreCase(exchange.getRequestMethod())) {
+                       String postQuery;
+                       try {
+                               // We do not close the stream on purpose, since the body still needs to be read
+                               BufferedReader br = new BufferedReader(new InputStreamReader(exchange.getRequestBody(), encoding));
+                               postQuery = br.readLine();
+                       } catch (IOException e) {
+                               throw new UncheckedIOException("Cannot read exchange body", e);
+                       }
+                       parseQuery(postQuery, parameters, encoding);
+               }
+               return parameters;
+       }
+
+       private static void parseQuery(String query, Map<String, List<String>> parameters, Charset encoding) {
+               if (query == null)
+                       return;
+               String pairs[] = query.split("[&]");
+               for (String pair : pairs) {
+                       String param[] = pair.split("[=]");
+
+                       String key = null;
+                       String value = null;
+                       if (param.length > 0) {
+                               key = URLDecoder.decode(param[0], encoding);
+                       }
+
+                       if (param.length > 1) {
+                               value = URLDecoder.decode(param[1], encoding);
+                       }
+
+                       if (!parameters.containsKey(key))
+                               parameters.put(key, new ArrayList<>());
+                       parameters.get(key).add(value);
+               }
+       }
+
        /** singleton */
        private HttpServerUtils() {
 
diff --git a/org.argeo.cms/src/org/argeo/cms/http/server/StaticHttpHandler.java b/org.argeo.cms/src/org/argeo/cms/http/server/StaticHttpHandler.java
new file mode 100644 (file)
index 0000000..49cc242
--- /dev/null
@@ -0,0 +1,177 @@
+package org.argeo.cms.http.server;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
+import java.lang.System.Logger;
+import java.lang.System.Logger.Level;
+import java.net.FileNameMap;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.TreeMap;
+
+import org.argeo.cms.acr.ContentUtils;
+import org.argeo.cms.http.HttpHeader;
+import org.argeo.cms.http.HttpStatus;
+import org.argeo.cms.util.StreamUtils;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+
+/** A simple {@link HttpHandler} which just serves or proxy resources. */
+public class StaticHttpHandler implements HttpHandler {
+       private final static Logger logger = System.getLogger(StaticHttpHandler.class.getName());
+
+       private static FileNameMap fileNameMap = URLConnection.getFileNameMap();
+
+       private NavigableMap<String, Object> binds = new TreeMap<>();
+
+       @Override
+       public void handle(HttpExchange exchange) throws IOException {
+               try {
+                       String path = HttpServerUtils.subPath(exchange);
+                       Map.Entry<String, Object> bindEntry = findBind(path);
+                       boolean isRoot = "/".equals(bindEntry.getKey());
+
+                       String relPath = isRoot ? path.substring(bindEntry.getKey().length())
+                                       : path.substring(bindEntry.getKey().length() + 1);
+                       process(bindEntry.getValue(), exchange, relPath);
+               } catch (Exception e) {
+                       logger.log(Level.ERROR, exchange.getRequestURI().toString(), e);
+               }
+       }
+
+       public void addBind(String path, Object bind) {
+               if (binds.containsKey(path))
+                       throw new IllegalStateException("Path '" + path + "' is already bound");
+               Object bindToUse = checkBindSupport(bind);
+               binds.put(path, bindToUse);
+       }
+
+       protected Map.Entry<String, Object> findBind(String path) {
+               Map.Entry<String, Object> entry = binds.floorEntry(path);
+               if (entry == null)
+                       return null;
+               String mountPath = entry.getKey();
+               if (!path.startsWith(mountPath)) {
+                       // FIXME make it more robust and find when there is no content provider
+                       String[] parent = ContentUtils.getParentPath(path);
+                       return findBind(parent[0]);
+               }
+               return entry;
+       }
+
+       protected void process(Object bind, HttpExchange httpExchange, String relativePath) throws IOException {
+               OutputStream out = null;
+
+               try {
+                       String contentType = fileNameMap.getContentTypeFor(relativePath);
+                       if (contentType != null)
+                               httpExchange.getResponseHeaders().set(HttpHeader.CONTENT_TYPE.getHeaderName(), contentType);
+
+                       if (bind instanceof Path bindPath) {
+                               Path path = bindPath.resolve(relativePath);
+                               if (!Files.exists(path)) {
+                                       httpExchange.sendResponseHeaders(HttpStatus.NOT_FOUND.getCode(), -1);
+                                       return;
+                               }
+                               long size = Files.size(path);
+                               httpExchange.sendResponseHeaders(HttpStatus.OK.getCode(), size);
+                               out = httpExchange.getResponseBody();
+                               Files.copy(path, out);
+                       } else if (bind instanceof URL bindUrl) {
+                               URL url = new URL(bindUrl.toString() + relativePath);
+                               URLConnection urlConnection;
+                               try {
+                                       urlConnection = url.openConnection();
+                                       urlConnection.connect();
+                               } catch (IOException e) {
+                                       httpExchange.sendResponseHeaders(HttpStatus.NOT_FOUND.getCode(), -1);
+                                       return;
+                               }
+                               // TODO check other headers?
+                               // TODO use Proxy?
+                               String contentLengthStr = urlConnection.getHeaderField(HttpHeader.CONTENT_LENGTH.getHeaderName());
+                               httpExchange.sendResponseHeaders(HttpStatus.OK.getCode(),
+                                               contentLengthStr != null ? Long.parseLong(contentLengthStr) : 0);
+                               try (InputStream in = urlConnection.getInputStream()) {
+                                       out = httpExchange.getResponseBody();
+                                       StreamUtils.copy(in, out);
+                               } finally {
+                               }
+                       }
+                       // make sure everything is flushed
+                       httpExchange.getResponseBody().flush();
+               } catch (RuntimeException e) {
+                       try {
+                               httpExchange.sendResponseHeaders(HttpStatus.INTERNAL_SERVER_ERROR.getCode(), -1);
+                       } catch (IOException e1) {
+                               // silent
+                       }
+                       throw e;
+               } finally {
+                       if (out != null) {
+                               try {
+                                       out.close();
+                               } catch (IOException e) {
+                                       throw e;
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Checks whether this bind type is supported. This can be overridden in order
+        * to ass new bind type.
+        * 
+        * @see #process(Object, HttpExchange, String) for overriding the actual
+        *      implementation.
+        * 
+        * @param bind the bind to check
+        * @return the bind object to actually use (an URI will have been converted to
+        *         URL)
+        * @throws UnsupportedOperationException if this bind type is not supported
+        */
+       protected Object checkBindSupport(Object bind) throws UnsupportedOperationException {
+               if (bind instanceof Path)
+                       return bind;
+               if (bind instanceof URL)
+                       return bind;
+               if (bind instanceof URI uri) {
+                       try {
+                               return uri.toURL();
+                       } catch (MalformedURLException e) {
+                               throw new UnsupportedOperationException("URI " + uri + " cannot be connverted to URL.", e);
+                       }
+               }
+               // TODO string as a path within the server?
+               throw new UnsupportedOperationException("Bind " + bind + " type " + bind.getClass() + " is not supported.");
+       }
+
+       public static void main(String... args) {
+               try {
+                       HttpServer httpServer = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 6060), 0);
+
+                       StaticHttpHandler staticHttpHandler = new StaticHttpHandler();
+                       staticHttpHandler.addBind("/", Paths.get("/home/mbaudier/dev/workspaces/test-node-js/test-static"));
+                       staticHttpHandler.addBind("/js",
+                                       Paths.get("/home/mbaudier/dev/workspaces/test-node-js/test-static/node_modules"));
+
+                       httpServer.createContext("/", staticHttpHandler);
+                       httpServer.start();
+               } catch (IOException e) {
+                       throw new UncheckedIOException(e);
+               }
+       }
+}
index e17a089fe05e15db48924216a5f3f6c1f998b2c1..a66a7397ec790cb9ed96f693d19dbb62d860cfa9 100644 (file)
@@ -10,6 +10,7 @@ import org.argeo.cms.auth.RemoteAuthCallbackHandler;
 import org.argeo.cms.auth.RemoteAuthRequest;
 import org.argeo.cms.auth.RemoteAuthResponse;
 import org.argeo.cms.auth.RemoteAuthUtils;
+import org.argeo.cms.http.RemoteAuthHttpExchange;
 
 import com.sun.net.httpserver.Authenticator;
 import com.sun.net.httpserver.HttpExchange;
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/http/RemoteAuthHttpExchange.java b/org.argeo.cms/src/org/argeo/cms/internal/http/RemoteAuthHttpExchange.java
deleted file mode 100644 (file)
index b7e670c..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-package org.argeo.cms.internal.http;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Locale;
-import java.util.Objects;
-
-import org.argeo.cms.auth.RemoteAuthRequest;
-import org.argeo.cms.auth.RemoteAuthResponse;
-import org.argeo.cms.auth.RemoteAuthSession;
-
-import com.sun.net.httpserver.HttpExchange;
-
-public class RemoteAuthHttpExchange implements RemoteAuthRequest, RemoteAuthResponse {
-       private final HttpExchange httpExchange;
-       private RemoteAuthSession remoteAuthSession;
-
-       public RemoteAuthHttpExchange(HttpExchange httpExchange) {
-               this.httpExchange = httpExchange;
-               this.remoteAuthSession = (RemoteAuthSession) httpExchange.getAttribute(RemoteAuthSession.class.getName());
-               Objects.requireNonNull(this.remoteAuthSession);
-       }
-
-       @Override
-       public void setHeader(String headerName, String value) {
-               httpExchange.getResponseHeaders().put(headerName, Collections.singletonList(value));
-       }
-
-       @Override
-       public void addHeader(String headerName, String value) {
-               List<String> values = httpExchange.getResponseHeaders().getOrDefault(headerName, new ArrayList<>());
-               values.add(value);
-       }
-
-       @Override
-       public RemoteAuthSession getSession() {
-               return remoteAuthSession;
-       }
-
-       @Override
-       public RemoteAuthSession createSession() {
-               throw new UnsupportedOperationException("Cannot create remote session");
-       }
-
-       @Override
-       public Locale getLocale() {
-               // TODO Auto-generated method stub
-               return null;
-       }
-
-       @Override
-       public Object getAttribute(String key) {
-               return httpExchange.getAttribute(key);
-       }
-
-       @Override
-       public void setAttribute(String key, Object object) {
-               httpExchange.setAttribute(key, object);
-       }
-
-       @Override
-       public String getHeader(String key) {
-               List<String> lst = httpExchange.getRequestHeaders().get(key);
-               if (lst == null || lst.size() == 0)
-                       return null;
-               return lst.get(0);
-       }
-
-       @Override
-       public int getLocalPort() {
-               return httpExchange.getLocalAddress().getPort();
-       }
-
-       @Override
-       public String getRemoteAddr() {
-               return httpExchange.getRemoteAddress().getHostName();
-       }
-
-       @Override
-       public int getRemotePort() {
-               return httpExchange.getRemoteAddress().getPort();
-       }
-
-}
index c80933a559753203cac8e50ebc95b98afd79fd42..2847cb32f36594c098e4d866e4b5c863d4b45087 100644 (file)
@@ -26,7 +26,7 @@ import org.argeo.cms.dav.DavHttpHandler;
 import org.argeo.cms.dav.DavPropfind;
 import org.argeo.cms.dav.DavResponse;
 import org.argeo.cms.http.HttpStatus;
-import org.argeo.cms.internal.http.RemoteAuthHttpExchange;
+import org.argeo.cms.http.RemoteAuthHttpExchange;
 import org.argeo.cms.util.StreamUtils;
 
 import com.sun.net.httpserver.HttpExchange;
index e2d1fb97a592d38e6a64e85918353dbb5b49ce6a..f9a1dc36832dab593fb28d53017679f8db5f400c 100644 (file)
@@ -3,14 +3,17 @@ package org.argeo.cms.internal.runtime;
 import static org.argeo.api.cms.CmsConstants.CONTEXT_PATH;
 
 import java.util.Map;
+import java.util.Objects;
 import java.util.TreeMap;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
 
 import org.argeo.api.cms.CmsConstants;
 import org.argeo.api.cms.CmsDeployment;
 import org.argeo.api.cms.CmsLog;
+import org.argeo.api.cms.CmsSshd;
 import org.argeo.api.cms.CmsState;
 import org.argeo.cms.CmsDeployProperty;
-import org.argeo.cms.CmsSshd;
 import org.argeo.cms.internal.http.CmsAuthenticator;
 import org.argeo.cms.internal.http.PublicCmsAuthenticator;
 
@@ -29,12 +32,12 @@ public class CmsDeploymentImpl implements CmsDeployment {
        private boolean sshdExpected = false;
 
        // HTTP
-       private HttpServer httpServer;
+       private CompletableFuture<HttpServer> httpServer = new CompletableFuture<>();
        private Map<String, HttpHandler> httpHandlers = new TreeMap<>();
        private Map<String, CmsAuthenticator> httpAuthenticators = new TreeMap<>();
 
        // SSHD
-       private CmsSshd cmsSshd;
+       private CompletableFuture<CmsSshd> cmsSshd = new CompletableFuture<>();
 
        public void start() {
                log.debug(() -> "CMS deployment available");
@@ -49,13 +52,18 @@ public class CmsDeploymentImpl implements CmsDeployment {
                String httpPort = this.cmsState.getDeployProperty(CmsDeployProperty.HTTP_PORT.getProperty());
                String httpsPort = this.cmsState.getDeployProperty(CmsDeployProperty.HTTPS_PORT.getProperty());
                httpExpected = httpPort != null || httpsPort != null;
+               if (!httpExpected)
+                       httpServer.complete(null);
 
                String sshdPort = this.cmsState.getDeployProperty(CmsDeployProperty.SSHD_PORT.getProperty());
                sshdExpected = sshdPort != null;
+               if (!sshdExpected)
+                       cmsSshd.complete(null);
        }
 
        public void setHttpServer(HttpServer httpServer) {
-               this.httpServer = httpServer;
+               Objects.requireNonNull(httpServer);
+               this.httpServer.complete(httpServer);
                // create contexts whose handles had already been published
                for (String contextPath : httpHandlers.keySet()) {
                        HttpHandler httpHandler = httpHandlers.get(contextPath);
@@ -82,7 +90,12 @@ public class CmsDeploymentImpl implements CmsDeployment {
        }
 
        public void createHttpContext(String contextPath, HttpHandler httpHandler, CmsAuthenticator authenticator) {
-               HttpContext httpContext = httpServer.createContext(contextPath);
+               if (!httpExpected) {
+                       if (log.isTraceEnabled())
+                               log.warn("Ignore HTTP context " + contextPath + " as we don't provide an HTTP server");
+                       return;
+               }
+               HttpContext httpContext = httpServer.join().createContext(contextPath);
                // we want to set the authenticator BEFORE the handler actually becomes active
                httpContext.setAuthenticator(authenticator);
                httpContext.setHandler(httpHandler);
@@ -96,7 +109,7 @@ public class CmsDeploymentImpl implements CmsDeployment {
                httpHandlers.remove(contextPath);
                if (httpServer == null)
                        return;
-               httpServer.removeContext(contextPath);
+               httpServer.join().removeContext(contextPath);
                log.debug(() -> "Removed handler " + contextPath + " : " + httpHandler.getClass().getName());
        }
 
@@ -109,7 +122,18 @@ public class CmsDeploymentImpl implements CmsDeployment {
        }
 
        public void setCmsSshd(CmsSshd cmsSshd) {
-               this.cmsSshd = cmsSshd;
+               Objects.requireNonNull(cmsSshd);
+               this.cmsSshd.complete(cmsSshd);
+       }
+
+       @Override
+       public CompletionStage<HttpServer> getHttpServer() {
+               return httpServer.minimalCompletionStage();
+       }
+
+       @Override
+       public CompletionStage<CmsSshd> getCmsSshd() {
+               return cmsSshd.minimalCompletionStage();
        }
 
 }
index 4d1d698453015bd0e2f82e122b6899ec0a84f1e9..60a51b44f7f8a8f913f8008e11460b2a956114f1 100644 (file)
@@ -6,6 +6,7 @@ import java.io.Reader;
 import java.net.InetAddress;
 import java.net.URL;
 import java.net.UnknownHostException;
+import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -93,6 +94,9 @@ public class CmsStateImpl implements CmsState {
        }
 
        public void start() {
+               Charset defaultCharset = Charset.defaultCharset();
+               if (!StandardCharsets.UTF_8.equals(defaultCharset))
+                       log.error("Default JVM charset is " + defaultCharset + " and not " + StandardCharsets.UTF_8);
                try {
                        // First init check
                        Path privateBase = getDataPath(KernelConstants.DIR_PRIVATE);
diff --git a/org.argeo.cms/src/org/argeo/cms/util/AsyncPipedOutputStream.java b/org.argeo.cms/src/org/argeo/cms/util/AsyncPipedOutputStream.java
new file mode 100644 (file)
index 0000000..636f5e3
--- /dev/null
@@ -0,0 +1,71 @@
+package org.argeo.cms.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.io.UncheckedIOException;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+/**
+ * An output stream whose {@link #close()} method will wait for read actions to
+ * be completed. It is meant to be used transparently as an
+ * {@link OutputStream}, fulfilling the expectation that everything has been
+ * done when the {@link #close()} method has returned.
+ */
+public class AsyncPipedOutputStream extends PipedOutputStream {
+//     private final static Logger logger = System.getLogger(AsyncPipedOutputStream.class.getName());
+
+       private CompletableFuture<Void> readingDone;
+
+       private long timeout = 60 * 1000;
+
+       /**
+        * Provides the actions which will read (and close) the related piped input
+        * stream. Reading starts immediately asynchronously, but the provided
+        * {@link InputStream} will block until data starts to be written to this output
+        * stream.
+        */
+       public void asyncRead(Consumer<InputStream> readActions) {
+               try {
+                       PipedInputStream in = new PipedInputStream(this);
+                       readingDone = CompletableFuture.runAsync(() -> {
+                               readActions.accept(in);
+                       });
+               } catch (IOException e) {
+                       throw new UncheckedIOException("Cannot create piped input stream", e);
+               }
+       }
+
+       /**
+        * Closes this output stream immediately but then wait for the reading of the
+        * related input stream to be completed.
+        */
+       @Override
+       public void close() throws IOException {
+               Objects.requireNonNull(readingDone, "Async read must have started");
+               super.flush();
+               super.close();
+               readingDone.orTimeout(timeout, TimeUnit.MILLISECONDS).join();
+//             logger.log(Logger.Level.DEBUG, "OUT waiting " + timeout);
+//             try {
+//                     readingDone.get(timeout, TimeUnit.MILLISECONDS);
+//             } catch (InterruptedException | ExecutionException | TimeoutException e) {
+//                     logger.log(Logger.Level.ERROR, "Reading was not completed", e);
+//             }
+//             logger.log(Logger.Level.DEBUG, "OUT closed");
+       }
+
+       /**
+        * Sets the timeout in milliseconds when waiting for reading to be completed
+        * before returning in the {@link #close()} method.
+        */
+       public void setTimeout(long timeout) {
+               this.timeout = timeout;
+       }
+
+}
index 0e214271d7a612642b35e0af3f1b971a56f89280..e4cc607d666c2777c94680222c5abcb4f1476421 100644 (file)
@@ -12,13 +12,17 @@ import java.time.temporal.ChronoUnit;
 import java.time.temporal.Temporal;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.Dictionary;
 import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.Hashtable;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Properties;
 
 import javax.naming.InvalidNameException;
@@ -279,6 +283,24 @@ public class LangUtils {
                throw new IllegalArgumentException("Index " + index + " is not available (size is " + i + ")");
        }
 
+       public static <K, V extends Comparable<? super V>> Map<K, V> sortByValue(Map<K, V> map) {
+               return sortByValue(map, false);
+       }
+
+       public static <K, V extends Comparable<? super V>> Map<K, V> sortByValue(Map<K, V> map, boolean descending) {
+               List<Entry<K, V>> list = new ArrayList<>(map.entrySet());
+               list.sort(Entry.comparingByValue());
+               if (descending)
+                       Collections.reverse(list);
+
+               Map<K, V> result = new LinkedHashMap<>();
+               for (Entry<K, V> entry : list) {
+                       result.put(entry.getKey(), entry.getValue());
+               }
+
+               return result;
+       }
+
        /*
         * EXCEPTIONS
         */
index 56a5fdfd46d3691704ca15d886ba2ba6dec96a55..8d7e693e9f85841d7dcb4bc9045c179cf0fb886f 100644 (file)
@@ -1,6 +1,7 @@
 package org.argeo.cms.util;
 
 import java.io.File;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 
@@ -62,10 +63,17 @@ public class OS {
                        // TODO support multiple names
                        runDir = Paths.get(xdgRunDir);
                } else {
-                       if (System.getProperty("user.name").equals("root")) {
+                       String username = System.getProperty("user.name");
+                       if (username.equals("root")) {
                                runDir = Paths.get("/run");
                        } else {
-                               runDir = Paths.get(System.getProperty("user.home"), ".cache/argeo");
+                               Path homeDir = Paths.get(System.getProperty("user.home"));
+                               if (!Files.isWritable(homeDir)) {
+                                       // typically, dameon's home (/usr/sbin) is not writable
+                                       runDir = Paths.get("/tmp/" + username + "/run");
+                               } else {
+                                       runDir = homeDir.resolve(".cache/argeo");
+                               }
                        }
                }
                return runDir;
index a589e739af389b3bf0ef22b5cc1f811acd124a62..5fbef6b31a6889f9ba3276150032988214d0e293 100644 (file)
@@ -6,7 +6,10 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.Reader;
+import java.io.UncheckedIOException;
 import java.io.Writer;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
 import java.util.StringJoiner;
 
 /** Stream utilities to be used when Apache Commons IO is not available. */
@@ -88,6 +91,22 @@ public class StreamUtils {
                        }
        }
 
+       public static String toString(Class<?> clss, String resource) {
+               return toString(clss.getResourceAsStream(resource), StandardCharsets.UTF_8);
+       }
+
+       public static String toString(InputStream in) {
+               return toString(in, StandardCharsets.UTF_8);
+       }
+
+       public static String toString(InputStream in, Charset encoding) {
+               try {
+                       return new String(in.readAllBytes(), encoding);
+               } catch (IOException e) {
+                       throw new UncheckedIOException(e);
+               }
+       }
+
        public static String toString(BufferedReader reader) throws IOException {
                StringJoiner sn = new StringJoiner("\n");
                String line = null;
index 617e7887806f451a9eb89cd874610ccf8bab0344..f946add692a062940f0d64f3e2ef44f10e529be5 100644 (file)
@@ -47,17 +47,22 @@ public abstract class AbstractProvisioningSource implements ProvisioningSource {
                                Bundle bundle = bc.installBundle(referenceUrl);
                                return bundle;
                        } else {
-
-                               Path tempJar = null;
-                               if (locator instanceof Path && Files.isDirectory((Path) locator))
-                                       tempJar = toTempJar((Path) locator);
+                               Path locatorPath = (Path) locator;
+                               Path pathToUse;
+                               boolean isTemp = false;
+                               if (locator instanceof Path && Files.isDirectory(locatorPath)) {
+                                       pathToUse = toTempJar(locatorPath);
+                                       isTemp = true;
+                               } else {
+                                       pathToUse = locatorPath;
+                               }
                                Bundle bundle;
-                               try (InputStream in = newInputStream(tempJar != null ? tempJar : locator)) {
-                                       bundle = bc.installBundle(module.getBranch().getCoordinates(), in);
+                               try (InputStream in = newInputStream(pathToUse)) {
+                                       bundle = bc.installBundle(locatorPath.toAbsolutePath().toString(), in);
                                }
 
-                               if (tempJar != null)
-                                       Files.deleteIfExists(tempJar);
+                               if (isTemp && pathToUse != null)
+                                       Files.deleteIfExists(pathToUse);
                                return bundle;
                        }
                } catch (BundleException | IOException e) {
@@ -74,14 +79,20 @@ public abstract class AbstractProvisioningSource implements ProvisioningSource {
                                        bundle.update(in);
                                }
                        } else {
-                               Path tempJar = null;
-                               if (locator instanceof Path && Files.isDirectory((Path) locator))
-                                       tempJar = toTempJar((Path) locator);
-                               try (InputStream in = newInputStream(tempJar != null ? tempJar : locator)) {
+                               Path locatorPath = (Path) locator;
+                               Path pathToUse;
+                               boolean isTemp = false;
+                               if (locator instanceof Path && Files.isDirectory(locatorPath)) {
+                                       pathToUse = toTempJar(locatorPath);
+                                       isTemp = true;
+                               } else {
+                                       pathToUse = locatorPath;
+                               }
+                               try (InputStream in = newInputStream(pathToUse)) {
                                        bundle.update(in);
                                }
-                               if (tempJar != null)
-                                       Files.deleteIfExists(tempJar);
+                               if (isTemp && pathToUse != null)
+                                       Files.deleteIfExists(pathToUse);
                        }
                } catch (BundleException | IOException e) {
                        throw new A2Exception("Cannot update module " + module, e);
index 5b2c93924505112c9d4f4bade1ac7b6fcf88152f..c2ce215288171bf94543a5fd0a949013b25b6bf2 100644 (file)
@@ -74,12 +74,13 @@ class ThinLogging implements Consumer<Map<String, Object>> {
        private final boolean synchronous;
 
        ThinLogging() {
-               publisher = new LogEntryPublisher();
                synchronous = Boolean.parseBoolean(System.getProperty(PROP_ARGEO_LOGGING_SYNCHRONOUS, "false"));
                if (synchronous) {
                        synchronousSubscriber = new PrintStreamSubscriber();
+                       publisher = null;
                } else {
                        PrintStreamSubscriber subscriber = new PrintStreamSubscriber();
+                       publisher = new LogEntryPublisher();
                        publisher.subscribe(subscriber);
                }
                Runtime.getRuntime().addShutdownHook(new Thread(() -> close(), "Log shutdown"));
@@ -220,6 +221,45 @@ class ThinLogging implements Consumer<Map<String, Object>> {
                return Collections.unmodifiableNavigableMap(levels);
        }
 
+       private void dispatchLogEntry(ThinLogger logger, Level level, ResourceBundle bundle, String msg, Instant instant,
+                       Thread thread, Throwable thrown, StackTraceElement callLocation) {
+               assert level != null;
+               assert logger != null;
+//             assert msg != null;
+               assert instant != null;
+               assert thread != null;
+
+               if (msg == null)
+                       msg = "null";
+
+               final long sequence = nextEntry.incrementAndGet();
+
+               Map<String, Serializable> logEntry = new LogEntryMap(sequence);
+
+               // same object as key class name
+               logEntry.put(KEY_LEVEL, level);
+               logEntry.put(KEY_MSG, msg);
+               logEntry.put(KEY_INSTANT, instant);
+               if (thrown != null)
+                       logEntry.put(KEY_THROWABLE, thrown);
+               if (callLocation != null)
+                       logEntry.put(KEY_CALL_LOCATION, callLocation);
+
+               // object is a string
+               logEntry.put(KEY_LOGGER, logger.getName());
+               logEntry.put(KEY_THREAD, thread.getName());
+
+               // should be unmodifiable for security reasons
+               if (synchronous) {
+                       assert synchronousSubscriber != null;
+                       synchronousSubscriber.onNext(logEntry);
+               } else {
+                       if (!publisher.isClosed())
+                               publisher.submit(Collections.unmodifiableMap(logEntry));
+               }
+
+       }
+
        /*
         * INTERNAL CLASSES
         */
@@ -268,7 +308,7 @@ class ThinLogging implements Consumer<Map<String, Object>> {
                        // measure timestamp first
                        Instant now = Instant.now();
                        Thread thread = Thread.currentThread();
-                       publisher.log(this, level, bundle, msg, now, thread, thrown, findCallLocation(level, thread));
+                       dispatchLogEntry(ThinLogger.this, level, bundle, msg, now, thread, thrown, findCallLocation(level, thread));
                }
 
                @Override
@@ -295,7 +335,8 @@ class ThinLogging implements Consumer<Map<String, Object>> {
                                format = sb.toString();
                        }
                        String msg = params == null ? format : MessageFormat.format(format, params);
-                       publisher.log(this, level, bundle, msg, now, thread, (Throwable) null, findCallLocation(level, thread));
+                       dispatchLogEntry(ThinLogger.this, level, bundle, msg, now, thread, (Throwable) null,
+                                       findCallLocation(level, thread));
                }
 
                private void setLevel(Level level) {
@@ -353,40 +394,10 @@ class ThinLogging implements Consumer<Map<String, Object>> {
                        super();
                }
 
-               private void log(ThinLogger logger, Level level, ResourceBundle bundle, String msg, Instant instant,
-                               Thread thread, Throwable thrown, StackTraceElement callLocation) {
-                       assert level != null;
-                       assert logger != null;
-                       assert msg != null;
-                       assert instant != null;
-                       assert thread != null;
-
-                       final long sequence = nextEntry.incrementAndGet();
-
-                       Map<String, Serializable> logEntry = new LogEntryMap(sequence);
-
-                       // same object as key class name
-                       logEntry.put(KEY_LEVEL, level);
-                       logEntry.put(KEY_MSG, msg);
-                       logEntry.put(KEY_INSTANT, instant);
-                       if (thrown != null)
-                               logEntry.put(KEY_THROWABLE, thrown);
-                       if (callLocation != null)
-                               logEntry.put(KEY_CALL_LOCATION, callLocation);
-
-                       // object is a string
-                       logEntry.put(KEY_LOGGER, logger.getName());
-                       logEntry.put(KEY_THREAD, thread.getName());
-
-                       // should be unmodifiable for security reasons
-                       if (synchronous) {
-                               assert synchronousSubscriber != null;
-                               synchronousSubscriber.onNext(logEntry);
-                       } else {
-                               if (!isClosed())
-                                       submit(Collections.unmodifiableMap(logEntry));
-                       }
-               }
+//             private void log(ThinLogger logger, Level level, ResourceBundle bundle, String msg, Instant instant,
+//                             Thread thread, Throwable thrown, StackTraceElement callLocation) {
+//
+//             }
 
        }
 
diff --git a/osgi/equinox/org.argeo.cms.lib.equinox/OSGI-INF/equinoxJettyServer.xml b/osgi/equinox/org.argeo.cms.lib.equinox/OSGI-INF/equinoxJettyServer.xml
new file mode 100644 (file)
index 0000000..1b75ea2
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" immediate="true" activate="start" deactivate="stop" name="Jetty Service Factory">
+   <implementation class="org.argeo.cms.equinox.http.jetty.EquinoxJettyServer"/>
+   <property name="service.pid" type="String" value="org.argeo.equinox.jetty.config"/>
+   <reference bind="setCmsState" cardinality="1..1" interface="org.argeo.api.cms.CmsState" name="CmsState" policy="static"/>
+   <service>
+      <provide interface="com.sun.net.httpserver.HttpServer"/>
+      <provide interface="com.sun.net.httpserver.HttpsServer"/>
+   </service>
+</scr:component>
diff --git a/osgi/equinox/org.argeo.cms.lib.equinox/OSGI-INF/jettyServiceFactory.xml b/osgi/equinox/org.argeo.cms.lib.equinox/OSGI-INF/jettyServiceFactory.xml
deleted file mode 100644 (file)
index 6a13362..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" immediate="true" activate="start" deactivate="stop" name="Jetty Service Factory">
-   <implementation class="org.argeo.cms.equinox.http.jetty.EquinoxJettyServer"/>
-   <property name="service.pid" type="String" value="org.argeo.equinox.jetty.config"/>
-   <reference bind="setCmsState" cardinality="1..1" interface="org.argeo.api.cms.CmsState" name="CmsState" policy="static"/>
-   <service>
-      <provide interface="com.sun.net.httpserver.HttpServer"/>
-   </service>
-</scr:component>
index 2c83158e243cf69e0176afc5a7cc914c1f512806..2ced2a26c0d496cc2ba14301025a6dcf39bbee5f 100644 (file)
@@ -1,2 +1,2 @@
 Service-Component: \
-OSGI-INF/jettyServiceFactory.xml,\
+OSGI-INF/equinoxJettyServer.xml,\
index d9cae87d811258d5a13e43eea8492f3792377ce4..d5943f556d6fba9db0dd63d4c4cfceef89e4888e 160000 (submodule)
@@ -1 +1 @@
-Subproject commit d9cae87d811258d5a13e43eea8492f3792377ce4
+Subproject commit d5943f556d6fba9db0dd63d4c4cfceef89e4888e
index 1203fef8dc90116b9622583f6eb9f09638697b80..16c51ab9777dc35cff0632073a3cfd3579d14fc9 100644 (file)
@@ -1,6 +1,6 @@
 major=2
 minor=3
-micro=18
+micro=23
 qualifier=
 
 Bundle-Copyright= \
@@ -10,4 +10,4 @@ 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
+OR LicenseRef-argeo2-GPL-2.0-or-later-with-EPL-and-Apache-and-JCR-permissions
index df8363b76aead1107005e4e0995afd6e05757d06..da2e68a436ab853cf5eb1fff834a9b7ecc378514 100644 (file)
@@ -1,21 +1,20 @@
-argeo.osgi.start.2.node=\
-org.eclipse.equinox.metatype,\
-org.eclipse.equinox.cm,\
-org.eclipse.equinox.ds,\
+argeo.osgi.start.2=\
+org.apache.felix.scr,\
 org.argeo.init
 
-argeo.osgi.start.3.node=\
+argeo.osgi.start.3=\
 org.argeo.cms,\
-org.argeo.cms.jcr,\
-org.argeo.cms.ui.rcp
+org.argeo.cms.swt.rcp,\
+org.argeo.cms.ee,\
+org.argeo.cms.lib.dbus,\
 
+argeo.osgi.start.5=\
+org.argeo.app.profile.acr.fs,\
 
-# Local
-argeo.node.repo.type=h2
-org.osgi.service.http.port=7070
-#org.osgi.service.http.port.secure=7073
 
-argeo.node.useradmin.uris=os:///
+
+#argeo.node.useradmin.uris=os:///
+argeo.directory=ipa:///
 
 #argeo.node.useradmin.uris=ldap://cn=Directory%20Manager:argeoargeo@localhost:10389/dc=example,dc=com
 
@@ -31,7 +30,10 @@ log.org.argeo=DEBUG
 
 # DON'T CHANGE BELOW
 org.eclipse.equinox.http.jetty.autostart=false
-org.osgi.framework.bootdelegation=com.sun.jndi.ldap,\
+org.osgi.framework.bootdelegation=\
+sun.security.internal.spec,\
+sun.security.provider,\
+com.sun.jndi.ldap,\
 com.sun.jndi.ldap.sasl,\
 com.sun.security.jgss,\
 com.sun.jndi.dns,\
diff --git a/sdk/deploy/argeo-cms/usr/bin/jshc b/sdk/deploy/argeo-cms/usr/bin/jshc
new file mode 100755 (executable)
index 0000000..f29a38c
--- /dev/null
@@ -0,0 +1,2 @@
+#!/usr/bin/sh
+java -Xms32m -Xmx64m -jar /usr/share/a2/org.argeo.cms/org.argeo.cms.jshell.2.3.jar "$@"
\ No newline at end of file
index 4f2a405d58994d8521bc2a826d9ae19df3e0a5c1..b656accf449bcc01a6c725a59eef0d6a2ae8851a 100644 (file)
@@ -7,4 +7,5 @@
    </service>
    <reference bind="setCmsContext" cardinality="1..1" interface="org.argeo.api.cms.CmsContext" name="CmsContext" policy="static"/>
    <reference bind="setContentRepository" cardinality="1..1" interface="org.argeo.api.acr.ContentRepository" name="ContentRepository" policy="static"/>
+   <reference bind="setCmsFileSystemProvider" cardinality="1..1" interface="java.nio.file.spi.FileSystemProvider" name="CmsFileSystemProvider" policy="static"/>
 </scr:component>
index 5d964090b9abbd908ff1fc3f007c85afe11a76f8..3a91dbc23dc5db5db2c7569f75d00a272b3f8d42 100644 (file)
@@ -1,10 +1,7 @@
 package org.argeo.cms.swt;
 
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.StringTokenizer;
 
 import org.argeo.api.cms.ux.CmsIcon;
 import org.argeo.api.cms.ux.CmsStyle;
@@ -294,20 +291,6 @@ public class CmsSwtUtils {
                        child.dispose();
        }
 
-       /** Clean reserved URL characters for use in HTTP links. */
-       public static String cleanPathForUrl(String path) {
-               StringTokenizer st = new StringTokenizer(path, "/");
-               StringBuilder sb = new StringBuilder();
-               while (st.hasMoreElements()) {
-                       sb.append('/');
-                       String encoded = URLEncoder.encode(st.nextToken(), StandardCharsets.UTF_8);
-                       encoded = encoded.replace("+", "%20");
-                       sb.append(encoded);
-
-               }
-               return sb.toString();
-       }
-
        /** Singleton. */
        private CmsSwtUtils() {
        }
index 0984f57fb2786a5366f833ac9f551c3ba9e31348..3c825c1ffd79a457521bead97ded6e20cbf12ae3 100644 (file)
@@ -5,11 +5,10 @@ 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.ContentUtils;
 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.ImageData;
 
@@ -44,15 +43,7 @@ public class AcrSwtImageManager extends AbstractSwtImageManager<Content> {
        }
 
        protected String getDataPathForUrl(Content content) {
-               return CmsSwtUtils.cleanPathForUrl(getDataPath(content));
-       }
-
-       /** A path in the node repository */
-       protected String getDataPath(Content node) {
-               // TODO make it more configurable?
-               StringBuilder buf = new StringBuilder(CmsConstants.PATH_API_ACR);
-               buf.append(node.getPath());
-               return buf.toString();
+               return ContentUtils.getDataPathForUrl(content);
        }
 
        @Override
index 6a75dfb2c440c43740b6210da663e18e17c6f866..4333f48df309cefa847005d417e3211d3711ccb4 100644 (file)
@@ -3,7 +3,7 @@ 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.acr.ContentUtils;
 import org.argeo.cms.swt.widgets.StyledControl;
 import org.eclipse.swt.widgets.Composite;
 
@@ -56,7 +56,7 @@ public abstract class LinkedControl extends StyledControl {
                if (plainUri != null)
                        return plainUri;
                if (linkedContent != null)
-                       return URI.create("#" + CmsSwtUtils.cleanPathForUrl(linkedContent.getPath()));
+                       return URI.create("#" + ContentUtils.cleanPathForUrl(linkedContent.getPath()));
                return null;
 
        }
index 4988fc6b897b6300fcc601c87918200fe71ddd46..403bc8e871efcd646c7afe12c36df2ea4fdce032 100644 (file)
@@ -3,8 +3,31 @@ package org.argeo.cms.swt.acr;
 import org.argeo.api.acr.Content;
 import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Layout;
 
+/**
+ * Creates SWT UI parts based on a given view/model context. For simple UIs, the
+ * view and controller can usually be implemented directly in the method, while
+ * more complex UI components may require a dedicated object (either an internal
+ * or external class). The main purpose of this factory pattern is to ease the
+ * dependency injection of other services used.
+ */
 @FunctionalInterface
 public interface SwtUiProvider {
+       /**
+        * Populates the provided {@link Composite} with SWT UI components (view) and
+        * listeners (controller), based on the provided {@link Content}. For a typical
+        * view or editor, the context will be the data to display/edit, but it can also
+        * just be used to access the underlying data session.
+        * 
+        * @param parent  the SWT {@link Composite} to use as parent for the widgets.
+        *                Implementations should not assume that a {@link Layout} has
+        *                been set on it, and should therefore set the appropriate
+        *                layout themselves.
+        * @param context the data to display or a generic data context (typically a
+        *                user home area).
+        * @return a {@link Control} within the parent on which to focus if needed, Can
+        *         be null, so client should always check.
+        */
        Control createUiPart(Composite parent, Content context);
 }
index add6e9edb933a6f97d556961c35af4862cf39653..ca98f69beda3ee9f0e21addb445d9fc3a3990980 100644 (file)
@@ -1,23 +1,28 @@
 package org.argeo.cms.swt.app;
 
+import java.net.URI;
+import java.nio.file.Path;
+import java.nio.file.spi.FileSystemProvider;
 import java.util.HashSet;
 import java.util.Set;
 
 import org.argeo.api.acr.Content;
 import org.argeo.api.acr.ContentRepository;
-import org.argeo.api.cms.CmsContext;
 import org.argeo.api.cms.ux.CmsUi;
 import org.argeo.api.cms.ux.CmsView;
 import org.argeo.cms.AbstractCmsApp;
 import org.argeo.cms.swt.CmsSwtUi;
 import org.argeo.cms.swt.CmsSwtUtils;
 import org.argeo.cms.swt.auth.CmsLogin;
+import org.argeo.eclipse.ui.fs.SimpleFsBrowser;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.widgets.Composite;
 
 public class CmsUserApp extends AbstractCmsApp {
        private ContentRepository contentRepository;
 
+       private FileSystemProvider cmsFileSystemProvider;
+
        @Override
        public Set<String> getUiNames() {
                Set<String> uiNames = new HashSet<>();
@@ -41,6 +46,12 @@ public class CmsUserApp extends AbstractCmsApp {
                        AcrContentTreeView view = new AcrContentTreeView(cmsUi, 0, rootContent);
                        view.setLayoutData(CmsSwtUtils.fillAll());
 
+               } else if ("app".equals(uiName)) {
+                       Path rootPath = cmsFileSystemProvider.getPath(URI.create("cms:///"));
+                       SimpleFsBrowser view = new SimpleFsBrowser(cmsUi, 0);
+                       view.setInput(rootPath);
+                       view.setLayoutData(CmsSwtUtils.fillAll());
+
                }
                return cmsUi;
        }
@@ -59,4 +70,8 @@ public class CmsUserApp extends AbstractCmsApp {
                this.contentRepository = contentRepository;
        }
 
+       public void setCmsFileSystemProvider(FileSystemProvider cmsFileSystemProvider) {
+               this.cmsFileSystemProvider = cmsFileSystemProvider;
+       }
+
 }
\ No newline at end of file
diff --git a/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/app/SimpleSwtApp.java b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/app/SimpleSwtApp.java
new file mode 100644 (file)
index 0000000..169c2d0
--- /dev/null
@@ -0,0 +1,52 @@
+package org.argeo.cms.swt.app;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.argeo.api.cms.CmsApp;
+import org.argeo.api.cms.ux.CmsUi;
+import org.argeo.cms.AbstractCmsApp;
+import org.argeo.cms.swt.CmsSwtUi;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+
+/** Simplifies creating a simple {@link CmsApp} based on SWT. */
+public class SimpleSwtApp extends AbstractCmsApp {
+       protected final static String DEFAULT_UI_NAME = "app";
+
+       protected void createDefaultUi(Composite parent) {
+
+       }
+
+       protected void createUi(String uiName, Composite parent) {
+               if (DEFAULT_UI_NAME.equals(uiName)) {
+                       createDefaultUi(parent);
+               }
+       }
+
+       @Override
+       public Set<String> getUiNames() {
+               Set<String> uiNames = new HashSet<>();
+               uiNames.add(DEFAULT_UI_NAME);
+               return uiNames;
+       }
+
+       @Override
+       public CmsUi initUi(Object uiParent) {
+               Composite parent = (Composite) uiParent;
+               String uiName = parent.getData(UI_NAME_PROPERTY) != null ? parent.getData(UI_NAME_PROPERTY).toString() : null;
+               CmsSwtUi cmsUi = new CmsSwtUi(parent, SWT.NONE);
+               if (uiName != null)
+                       createUi(uiName, cmsUi);
+               return cmsUi;
+       }
+
+       @Override
+       public void refreshUi(CmsUi cmsUi, String state) {
+       }
+
+       @Override
+       public void setState(CmsUi cmsUi, String state) {
+       }
+
+}
index 216dc3654100b45f82ac2b5a07d33b558209916d..2389755432e6ccc6b381543f2cbab3a3fe45cd37 100644 (file)
@@ -2,6 +2,8 @@ package org.argeo.cms.web;
 
 import static org.eclipse.rap.rwt.internal.service.ContextProvider.getApplicationContext;
 
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.security.PrivilegedAction;
 import java.util.Locale;
 import java.util.UUID;
@@ -241,6 +243,15 @@ public class CmsWebEntryPoint extends AbstractSwtCmsView implements EntryPoint,
                return cmsSession;
        }
 
+       @Override
+       public URI toBackendUri(String url) {
+               try {
+                       return new URI(url);
+               } catch (URISyntaxException e) {
+                       throw new IllegalArgumentException("Cannot convert " + url, e);
+               }
+       }
+
        /*
         * EntryPoint IMPLEMENTATION
         */
index a89b921cdf6a9012bd38107ed647f046d07644f2..84ab27679b17451832d2af2fffcd2e7f98fef1b5 100644 (file)
@@ -11,7 +11,8 @@ import org.eclipse.swt.widgets.Widget;
 public class EclipseUiSpecificUtils {
 
        public static void setStyleData(Widget widget, Object data) {
-               widget.setData(RWT.CUSTOM_VARIANT, data);
+               if (!widget.isDisposed())
+                       widget.setData(RWT.CUSTOM_VARIANT, data);
        }
 
        public static Object getStyleData(Widget widget) {
index e8bfefb3f0594d8e5156e4bce95246a1002c7e5a..f66bf4b42138ffef67ba910e02dbe214380b7fa0 100644 (file)
@@ -3,4 +3,5 @@
    <implementation class="org.argeo.cms.ui.rcp.dbus.CmsRcpDBusLauncher"/>
    <reference bind="setCmsDBus" cardinality="1..1" interface="org.argeo.cms.dbus.CmsDBus" name="CmsDBus" policy="static"/>
    <reference bind="addCmsApp" cardinality="0..n" interface="org.argeo.api.cms.CmsApp" name="CmsApp" policy="dynamic" unbind="removeCmsApp"/>
+   <reference bind="setCmsRcpDisplayFactory" cardinality="1..1" interface="org.argeo.cms.ui.rcp.CmsRcpDisplayFactory" name="CmsRcpDisplayFactory" policy="static"/>
 </scr:component>
index 8b1d1468478f9a786ab3037ee89eaec06ad0a833..41cae5393e4f4ede3104e1b4d1ebc9a1d4c72a6f 100644 (file)
@@ -1,4 +1,8 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" activate="init" deactivate="destroy" immediate="true" name="CMS RCP Display Factory">
    <implementation class="org.argeo.cms.ui.rcp.CmsRcpDisplayFactory"/>
+   <service>
+      <provide interface="org.argeo.cms.ui.rcp.CmsRcpDisplayFactory"/>
+   </service>
+   <reference bind="setCmsDeployment" cardinality="1..1" interface="org.argeo.api.cms.CmsDeployment" name="CmsDeployment" policy="static"/>
 </scr:component>
index 6f37582501b419080fd7a893f8e3c5bd9025fb6b..80b5bcd33fcb93fc87f1465d67202aabd161ac64 100644 (file)
@@ -2,6 +2,7 @@ Bundle-SymbolicName: org.argeo.cms.swt.rcp;singleton=true
 
 Import-Package:\
 org.argeo.cms.auth,\
+org.argeo.api.acr,\
 org.eclipse.swt,\
 org.eclipse.swt.widgets,\
 org.eclipse.swt.graphics,\
index a88ff3824fa07c2ee2d02d093104ce0ad93060fd..77aeae061fc780a80786477864f8a0391760fe18 100644 (file)
@@ -2,6 +2,8 @@ package org.argeo.cms.ui.rcp;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.security.PrivilegedAction;
 import java.util.Map;
 import java.util.UUID;
@@ -15,10 +17,12 @@ import org.argeo.api.cms.CmsAuth;
 import org.argeo.api.cms.CmsEventBus;
 import org.argeo.api.cms.CmsLog;
 import org.argeo.api.cms.CmsSession;
+import org.argeo.api.cms.ux.CmsImageManager;
 import org.argeo.api.cms.ux.CmsTheme;
 import org.argeo.api.cms.ux.CmsView;
 import org.argeo.cms.swt.AbstractSwtCmsView;
 import org.argeo.cms.swt.CmsSwtUtils;
+import org.argeo.cms.swt.acr.AcrSwtImageManager;
 import org.eclipse.e4.ui.css.core.engine.CSSEngine;
 import org.eclipse.e4.ui.css.core.engine.CSSErrorHandler;
 import org.eclipse.e4.ui.css.swt.engine.CSSSWTEngineImpl;
@@ -36,12 +40,17 @@ public class CmsRcpApp extends AbstractSwtCmsView implements CmsView {
 
        private CSSEngine cssEngine;
 
+       private String httpServerBase;
+
        public CmsRcpApp(String uiName) {
                super(uiName);
                uid = UUID.randomUUID().toString();
        }
 
+       @SuppressWarnings("rawtypes")
        public void initRcpApp() {
+               imageManager = (CmsImageManager) new AcrSwtImageManager();
+
                display = Display.getCurrent();
                shell = new Shell(display);
                shell.setText("Argeo CMS");
@@ -153,6 +162,24 @@ public class CmsRcpApp extends AbstractSwtCmsView implements CmsView {
                return cmsApp;
        }
 
+       @Override
+       public URI toBackendUri(String url) {
+               try {
+                       URI u = new URI(url);
+                       if (u.getHost() == null) {
+                               // TODO make it more robust
+                               u = new URI(httpServerBase + url);
+                       }
+                       return u;
+               } catch (URISyntaxException e) {
+                       throw new IllegalArgumentException("Cannot convert " + url, e);
+               }
+       }
+
+       public void setHttpServerBase(String httpServerBase) {
+               this.httpServerBase = httpServerBase;
+       }
+
        /*
         * DEPENDENCY INJECTION
         */
index 63a1fd84ebb76ce6dc7f9da3d9d924e807871143..cd554de9d1ec09f63616a11dd1510b99ecd5bc67 100644 (file)
@@ -2,12 +2,15 @@ package org.argeo.cms.ui.rcp;
 
 import java.lang.System.Logger;
 import java.lang.System.Logger.Level;
+import java.net.InetSocketAddress;
 import java.nio.file.Path;
 
 import org.argeo.api.cms.CmsApp;
+import org.argeo.api.cms.CmsDeployment;
 import org.argeo.cms.util.OS;
 import org.eclipse.swt.events.DisposeListener;
 import org.eclipse.swt.widgets.Display;
+import com.sun.net.httpserver.HttpsServer;
 
 /** Creates the SWT {@link Display} in a dedicated thread. */
 public class CmsRcpDisplayFactory {
@@ -17,12 +20,14 @@ public class CmsRcpDisplayFactory {
        private final static String ARGEO_RCP_URL = "argeo.rcp.url";
 
        /** There is only one display in RCP mode */
-       private static Display display;
+       private Display display;
 
        private CmsUiThread uiThread;
 
        private boolean shutdown = false;
 
+       private CmsDeployment cmsDeployment;
+
        public void init() {
                uiThread = new CmsUiThread();
                uiThread.start();
@@ -74,21 +79,38 @@ public class CmsRcpDisplayFactory {
                }
        }
 
-       public static Display getDisplay() {
+       public Display getDisplay() {
                return display;
        }
 
-       public static void openCmsApp(CmsApp cmsApp, String uiName, DisposeListener disposeListener) {
-               CmsRcpDisplayFactory.getDisplay().syncExec(() -> {
-                       CmsRcpApp cmsRcpApp = new CmsRcpApp(uiName);
-                       cmsRcpApp.setCmsApp(cmsApp, null);
-                       cmsRcpApp.initRcpApp();
-                       if (disposeListener != null)
-                               cmsRcpApp.getShell().addDisposeListener(disposeListener);
+       public void openCmsApp(CmsApp cmsApp, String uiName, DisposeListener disposeListener) {
+               cmsDeployment.getHttpServer().thenAccept((httpServer) -> {
+                       getDisplay().syncExec(() -> {
+                               CmsRcpApp cmsRcpApp = new CmsRcpApp(uiName);
+                               cmsRcpApp.setCmsApp(cmsApp, null);
+                               if (httpServer != null) {
+                                       InetSocketAddress addr = httpServer.getAddress();
+                                       String scheme = "http";
+                                       if (httpServer instanceof HttpsServer httpsServer) {
+                                               if (httpsServer.getHttpsConfigurator() != null)
+                                                       scheme = "https";
+                                       }
+                                       String httpServerBase = scheme + "://" + addr.getHostString() + ":" + addr.getPort();
+                                       cmsRcpApp.setHttpServerBase(httpServerBase);
+                               }
+                               cmsRcpApp.initRcpApp();
+                               if (disposeListener != null)
+                                       cmsRcpApp.getShell().addDisposeListener(disposeListener);
+                       });
                });
        }
 
        public static Path getUrlRunFile() {
                return OS.getRunDir().resolve(CmsRcpDisplayFactory.ARGEO_RCP_URL);
        }
+
+       public void setCmsDeployment(CmsDeployment cmsDeployment) {
+               this.cmsDeployment = cmsDeployment;
+       }
+
 }
index 6246b0d0d13661316f546fb18e9bb74750f4b67f..8b81e6698ff5d5a6d3373ca074702e0dd62d31a9 100644 (file)
@@ -25,6 +25,8 @@ public class CmsRcpHttpLauncher {
        private final static Logger logger = System.getLogger(CmsRcpHttpLauncher.class.getName());
        private CompletableFuture<HttpServer> httpServer = new CompletableFuture<>();
 
+       private CmsRcpDisplayFactory cmsRcpDisplayFactory;
+       
        public void init() {
 
        }
@@ -50,7 +52,7 @@ public class CmsRcpHttpLauncher {
                                        public void handle(HttpExchange exchange) throws IOException {
                                                String path = exchange.getRequestURI().getPath();
                                                String uiName = path != null ? path.substring(path.lastIndexOf('/') + 1) : "";
-                                               CmsRcpDisplayFactory.openCmsApp(cmsApp, uiName, null);
+                                               cmsRcpDisplayFactory.openCmsApp(cmsApp, uiName, null);
                                                exchange.sendResponseHeaders(200, -1);
                                                logger.log(Level.DEBUG, "Opened RCP UI  " + uiName + " of  CMS App /" + contextName);
                                        }
index ea6905757adf6536914ce0b3514edd1aede6a12e..34190b005f14ac6f68526bb0be35a06409631537 100644 (file)
@@ -7,12 +7,15 @@ import java.util.concurrent.CompletableFuture;
 
 import org.argeo.api.cms.CmsApp;
 import org.argeo.cms.dbus.CmsDBus;
+import org.argeo.cms.ui.rcp.CmsRcpDisplayFactory;
 
 public class CmsRcpDBusLauncher {
        private CompletableFuture<CmsDBus> cmsDBus = new CompletableFuture<>();
 
        private Map<String, CmsRcpFreeDesktopApplication> apps = new HashMap<>();
 
+       private CmsRcpDisplayFactory cmsRcpDisplayFactory;
+
        public void start() {
 
        }
@@ -24,7 +27,8 @@ public class CmsRcpDBusLauncher {
        public void addCmsApp(CmsApp cmsApp, Map<String, String> properties) {
                final String contextName = properties.get(CmsApp.CONTEXT_NAME_PROPERTY);
                cmsDBus.thenAcceptAsync((cmsDBus) -> {
-                       CmsRcpFreeDesktopApplication application = new CmsRcpFreeDesktopApplication(cmsDBus, contextName, cmsApp);
+                       CmsRcpFreeDesktopApplication application = new CmsRcpFreeDesktopApplication(cmsRcpDisplayFactory, cmsDBus,
+                                       contextName, cmsApp);
                        apps.put(contextName, application);
                });
        }
@@ -45,4 +49,8 @@ public class CmsRcpDBusLauncher {
                this.cmsDBus.complete(cmsDBus);
        }
 
+       public void setCmsRcpDisplayFactory(CmsRcpDisplayFactory cmsRcpDisplayFactory) {
+               this.cmsRcpDisplayFactory = cmsRcpDisplayFactory;
+       }
+
 }
index 98c84fa41db214da95b445dc17fe883181b947dc..f1962f70a4eaf2d7f85639e68e23dd7ed5bd8a3a 100644 (file)
@@ -20,7 +20,11 @@ public class CmsRcpFreeDesktopApplication implements FreeDesktopApplication, Clo
 
        private DBusConnection dBusConnection;
 
-       public CmsRcpFreeDesktopApplication(CmsDBus cmsDBus, String contextName, CmsApp cmsApp) {
+       private CmsRcpDisplayFactory cmsRcpDisplayFactory;
+
+       public CmsRcpFreeDesktopApplication(CmsRcpDisplayFactory cmsRcpDisplayFactory, CmsDBus cmsDBus, String contextName,
+                       CmsApp cmsApp) {
+               this.cmsRcpDisplayFactory = cmsRcpDisplayFactory;
                // TODO find a better prefix and/or make it customisable
                this.path = "/org/argeo/cms/" + contextName;
                this.cmsApp = cmsApp;
@@ -50,7 +54,7 @@ public class CmsRcpFreeDesktopApplication implements FreeDesktopApplication, Clo
                // String uiName = path != null ? path.substring(path.lastIndexOf('/') + 1) :
                // "";
                String uiName = "app";
-               CmsRcpDisplayFactory.openCmsApp(cmsApp, uiName, null);
+               cmsRcpDisplayFactory.openCmsApp(cmsApp, uiName, null);
        }
 
        @Override
diff --git a/swt/rcp/org.argeo.swt.specific.rcp/src/org/eclipse/rap/fileupload/DiskFileUploadReceiver.java b/swt/rcp/org.argeo.swt.specific.rcp/src/org/eclipse/rap/fileupload/DiskFileUploadReceiver.java
new file mode 100644 (file)
index 0000000..adf6b64
--- /dev/null
@@ -0,0 +1,15 @@
+package org.eclipse.rap.fileupload;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class DiskFileUploadReceiver extends FileUploadReceiver {
+       public File[] getTargetFiles() {
+               return null;
+       }
+
+       @Override
+       public void receive(InputStream stream, FileDetails details) throws IOException {
+       }
+}
index cbf1449e0ed3a04336983126c55f67d4e428850f..eec769132dce148e642391fc45b6edededc8a426 100644 (file)
@@ -34,4 +34,7 @@ public class FileUpload extends Composite {
                return null;
        }
 
+       public void setFilterExtensions(String[] exts) {
+
+       }
 }