From: Mathieu Baudier Date: Thu, 9 Nov 2023 10:34:18 +0000 (+0100) Subject: Merge tag 'v2.3.23' into testing X-Git-Tag: v2.1.114~3 X-Git-Url: https://git.argeo.org/?a=commitdiff_plain;h=9e6715d360fb29bb817ecaf6d49f0c8a3cc014f5;hp=e8607512775d76612e5d58460f3d862feb69dcc2;p=lgpl%2Fargeo-commons.git Merge tag 'v2.3.23' into testing --- diff --git a/Makefile b/Makefile index ff2c9ccad..ec06bd782 100644 --- 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 0db946a93..afa7583ab 100644 --- 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 diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/Content.java b/org.argeo.api.acr/src/org/argeo/api/acr/Content.java index df5c149e6..7ec295947 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/Content.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/Content.java @@ -89,12 +89,33 @@ public interface Content extends Iterable, Map { /* * 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 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 attrs, QName... classes) { + return add(unqualified(name), attrs, classes); + } + void remove(); /* @@ -118,10 +139,7 @@ public interface Content extends Iterable, Map { /** AND */ default boolean isContentClass(QNamed... contentClass) { - List 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, Map { /** OR */ default boolean hasContentClass(QNamed... contentClass) { - List 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, Map { 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(); } diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/CrAttributeType.java b/org.argeo.api.acr/src/org/argeo/api/acr/CrAttributeType.java index 3e0dddee4..888d376c4 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/CrAttributeType.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/CrAttributeType.java @@ -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)); } diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/CrName.java b/org.argeo.api.acr/src/org/argeo/api/acr/CrName.java index ead47377b..62a21fbd3 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/CrName.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/CrName.java @@ -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 diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/DName.java b/org.argeo.api.acr/src/org/argeo/api/acr/DName.java index be065a8d9..d39f35d86 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/DName.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/DName.java @@ -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; diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/QNamed.java b/org.argeo.api.acr/src/org/argeo/api/acr/QNamed.java index 73ae4f02e..9852a602a 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/QNamed.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/QNamed.java @@ -15,23 +15,52 @@ public interface QNamed extends Supplier { 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 diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/search/BasicSearch.java b/org.argeo.api.acr/src/org/argeo/api/acr/search/BasicSearch.java index 8cbdebf7e..7aae8cca0 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/search/BasicSearch.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/search/BasicSearch.java @@ -57,11 +57,11 @@ public class BasicSearch { } public BasicSearch where(Consumer 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 getWhere() { + if (where == null) + where = new AndFilter(); return where; } diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/search/ContentFilter.java b/org.argeo.api.acr/src/org/argeo/api/acr/search/ContentFilter.java index 45f2d848c..66c7559e9 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/search/ContentFilter.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/search/ContentFilter.java @@ -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 implements Constraint { - private Set constraintss = new HashSet<>(); + // even though not necessary, we use a list in order to have a predictable order + private List constraints = new ArrayList<>(); private COMPOSITION composition; @@ -74,10 +76,67 @@ public abstract class ContentFilter 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 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 getConstraints() { - return constraintss; + public Collection getConstraints() { + return constraints; } public boolean isUnion() { @@ -115,58 +174,6 @@ public abstract class ContentFilter 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 index 000000000..73402a2f4 --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/search/Eq.java @@ -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 index 000000000..4ab454e74 --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/search/Gt.java @@ -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 index 000000000..4a51fc8fb --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/search/Gte.java @@ -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 index 000000000..cd01f7b81 --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/search/IsContentClass.java @@ -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 index 000000000..a9c8d09d1 --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/search/IsDefined.java @@ -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 index 000000000..5af527891 --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/search/Like.java @@ -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 index 000000000..25b502eb1 --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/search/Lt.java @@ -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 index 000000000..330f29f91 --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/search/Lte.java @@ -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 index 000000000..41a97da67 --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/search/Not.java @@ -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 index 000000000..7fc07bc28 --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/search/PropertyValueContraint.java @@ -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; + } + +} diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/CmsDeployment.java b/org.argeo.api.cms/src/org/argeo/api/cms/CmsDeployment.java index d557816cb..ca1082c7d 100644 --- a/org.argeo.api.cms/src/org/argeo/api/cms/CmsDeployment.java +++ b/org.argeo.api.cms/src/org/argeo/api/cms/CmsDeployment.java @@ -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 props); -// -// Dictionary getProps(String factoryPid, String cn); + /** The local HTTP server, or null if none is expected. */ + CompletionStage getHttpServer(); + + /** The local SSH server, or null if none is expected. */ + CompletionStage 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 index 000000000..ec44e57ec --- /dev/null +++ b/org.argeo.api.cms/src/org/argeo/api/cms/CmsSshd.java @@ -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(); +} diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/ux/CmsView.java b/org.argeo.api.cms/src/org/argeo/api/cms/ux/CmsView.java index a36baf8e0..121e4bdcb 100644 --- a/org.argeo.api.cms/src/org/argeo/api/cms/ux/CmsView.java +++ b/org.argeo.api.cms/src/org/argeo/api/cms/ux/CmsView.java @@ -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); - CmsImageManager getImageManager(); + CmsImageManager 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. diff --git a/org.argeo.cms.cli/bnd.bnd b/org.argeo.cms.cli/bnd.bnd index 7cf8c1391..401cda955 100644 --- a/org.argeo.cms.cli/bnd.bnd +++ b/org.argeo.cms.cli/bnd.bnd @@ -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 \ diff --git a/org.argeo.cms.ee/src/org/argeo/cms/servlet/httpserver/ServletHttpExchange.java b/org.argeo.cms.ee/src/org/argeo/cms/servlet/httpserver/ServletHttpExchange.java index f5e9c0394..85553f01c 100644 --- a/org.argeo.cms.ee/src/org/argeo/cms/servlet/httpserver/ServletHttpExchange.java +++ b/org.argeo.cms.ee/src/org/argeo/cms/servlet/httpserver/ServletHttpExchange.java @@ -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 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 diff --git a/org.argeo.cms.ee/src/org/argeo/cms/servlet/internal/PkgServlet.java b/org.argeo.cms.ee/src/org/argeo/cms/servlet/internal/PkgServlet.java index 2b2ffcb10..ca4b6f74d 100644 --- a/org.argeo.cms.ee/src/org/argeo/cms/servlet/internal/PkgServlet.java +++ b/org.argeo.cms.ee/src/org/argeo/cms/servlet/internal/PkgServlet.java @@ -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) { diff --git a/org.argeo.cms.jshell/src/org/argeo/cms/jshell/CmsJShell.java b/org.argeo.cms.jshell/src/org/argeo/cms/jshell/CmsJShell.java index 91a11ee71..d84ce7212 100644 --- a/org.argeo.cms.jshell/src/org/argeo/cms/jshell/CmsJShell.java +++ b/org.argeo.cms.jshell/src/org/argeo/cms/jshell/CmsJShell.java @@ -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(); diff --git a/org.argeo.cms.jshell/src/org/argeo/cms/jshell/JShellClient.java b/org.argeo.cms.jshell/src/org/argeo/cms/jshell/JShellClient.java index 87e88b77f..f090ed068 100644 --- a/org.argeo.cms.jshell/src/org/argeo/cms/jshell/JShellClient.java +++ b/org.argeo.cms.jshell/src/org/argeo/cms/jshell/JShellClient.java @@ -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 plainArgs = new ArrayList<>(); - Map> 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 plainArgs = new ArrayList<>(); + Map> 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 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 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 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 opt(Map> options, String shortOpt, String longOpt) { + List 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 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 -b [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; } /* diff --git a/org.argeo.cms.jshell/src/org/argeo/internal/cms/jshell/osgi/OsgiExecutionControlProvider.java b/org.argeo.cms.jshell/src/org/argeo/internal/cms/jshell/osgi/OsgiExecutionControlProvider.java index fbca08586..9ebf97ed0 100644 --- a/org.argeo.cms.jshell/src/org/argeo/internal/cms/jshell/osgi/OsgiExecutionControlProvider.java +++ b/org.argeo.cms.jshell/src/org/argeo/internal/cms/jshell/osgi/OsgiExecutionControlProvider.java @@ -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 bundleWires = fromBundleWiring.getRequiredWires(BundleRevision.PACKAGE_NAMESPACE); - for (BundleWire bw : bundleWires) { +// List exportedWires = fromBundleWiring.getProvidedWires(BundleRevision.PACKAGE_NAMESPACE); +// for (BundleWire bw : exportedWires) { +// packagesToImport.add(bw.getCapability().getAttributes().get(PackageNamespace.PACKAGE_NAMESPACE).toString()); +// } + + List 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; diff --git a/org.argeo.cms.lib.jetty/src/org/argeo/cms/jetty/JettyHttpServer.java b/org.argeo.cms.lib.jetty/src/org/argeo/cms/jetty/JettyHttpServer.java index e994463f9..98975c3c8 100644 --- a/org.argeo.cms.lib.jetty/src/org/argeo/cms/jetty/JettyHttpServer.java +++ b/org.argeo.cms.lib.jetty/src/org/argeo/cms/jetty/JettyHttpServer.java @@ -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 } } diff --git a/org.argeo.cms.lib.jetty/src/org/argeo/cms/jetty/ServletHttpContext.java b/org.argeo.cms.lib.jetty/src/org/argeo/cms/jetty/ServletHttpContext.java index 33611941d..b2a472b44 100644 --- a/org.argeo.cms.lib.jetty/src/org/argeo/cms/jetty/ServletHttpContext.java +++ b/org.argeo.cms.lib.jetty/src/org/argeo/cms/jetty/ServletHttpContext.java @@ -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 index 000000000..81fe078c2 --- /dev/null +++ b/org.argeo.cms.lib.json/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/org.argeo.cms.lib.json/.project b/org.argeo.cms.lib.json/.project new file mode 100644 index 000000000..cd9a5505e --- /dev/null +++ b/org.argeo.cms.lib.json/.project @@ -0,0 +1,28 @@ + + + org.argeo.cms.lib.json + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + 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 index 000000000..99f26c020 --- /dev/null +++ b/org.argeo.cms.lib.json/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=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 index 000000000..62ef3488c --- /dev/null +++ b/org.argeo.cms.lib.json/.settings/org.eclipse.jdt.core.prefs @@ -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 index 000000000..f29e940a0 --- /dev/null +++ b/org.argeo.cms.lib.json/.settings/org.eclipse.pde.core.prefs @@ -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 index 000000000..e69de29bb diff --git a/org.argeo.cms.lib.json/build.properties b/org.argeo.cms.lib.json/build.properties new file mode 100644 index 000000000..34d2e4d2d --- /dev/null +++ b/org.argeo.cms.lib.json/build.properties @@ -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 index 000000000..db42d000f --- /dev/null +++ b/org.argeo.cms.lib.json/src/org/argeo/cms/acr/json/AcrJsonUtils.java @@ -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); + } +} diff --git a/org.argeo.cms.lib.sshd/OSGI-INF/cmsSshServer.xml b/org.argeo.cms.lib.sshd/OSGI-INF/cmsSshServer.xml index 987b97745..8117b03b8 100644 --- a/org.argeo.cms.lib.sshd/OSGI-INF/cmsSshServer.xml +++ b/org.argeo.cms.lib.sshd/OSGI-INF/cmsSshServer.xml @@ -3,6 +3,6 @@ - + diff --git a/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/CmsSshServer.java b/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/CmsSshServer.java index f5609a37d..8a6def33a 100644 --- a/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/CmsSshServer.java +++ b/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/CmsSshServer.java @@ -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); + } + } diff --git a/org.argeo.cms/OSGI-INF/cmsDeployment.xml b/org.argeo.cms/OSGI-INF/cmsDeployment.xml index 66541827d..ee86ad59a 100644 --- a/org.argeo.cms/OSGI-INF/cmsDeployment.xml +++ b/org.argeo.cms/OSGI-INF/cmsDeployment.xml @@ -2,7 +2,7 @@ - + diff --git a/org.argeo.cms/OSGI-INF/cmsFileSystemProvider.xml b/org.argeo.cms/OSGI-INF/cmsFileSystemProvider.xml new file mode 100644 index 000000000..b1402fae8 --- /dev/null +++ b/org.argeo.cms/OSGI-INF/cmsFileSystemProvider.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/org.argeo.cms/bnd.bnd b/org.argeo.cms/bnd.bnd index ade2f3aa9..01443b5e8 100644 --- a/org.argeo.cms/bnd.bnd +++ b/org.argeo.cms/bnd.bnd @@ -16,4 +16,5 @@ OSGI-INF/cmsContentRepository.xml,\ OSGI-INF/cmsAcrHttpHandler.xml,\ OSGI-INF/cmsDeployment.xml,\ OSGI-INF/cmsContext.xml,\ +OSGI-INF/cmsFileSystemProvider.xml,\ diff --git a/org.argeo.cms/build.properties b/org.argeo.cms/build.properties index 6ca041a2a..04d4cb94a 100644 --- a/org.argeo.cms/build.properties +++ b/org.argeo.cms/build.properties @@ -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 index 41968be7e..000000000 --- a/org.argeo.cms/src/org/argeo/cms/CmsSshd.java +++ /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"; -} diff --git a/org.argeo.cms/src/org/argeo/cms/LocaleUtils.java b/org.argeo.cms/src/org/argeo/cms/LocaleUtils.java index 8aca8768a..a09bf5a93 100644 --- a/org.argeo.cms/src/org/argeo/cms/LocaleUtils.java +++ b/org.argeo.cms/src/org/argeo/cms/LocaleUtils.java @@ -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); } diff --git a/org.argeo.cms/src/org/argeo/cms/acr/CmsContentSession.java b/org.argeo.cms/src/org/argeo/cms/acr/CmsContentSession.java index daefe9835..c782256e2 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/CmsContentSession.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/CmsContentSession.java @@ -206,10 +206,12 @@ class CmsContentSession implements ProvidedSession, UuidIdentified { NavigableMap contentProviders = contentRepository.getMountManager() .findContentProviders(scopePath); for (Map.Entry 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); } diff --git a/org.argeo.cms/src/org/argeo/cms/acr/ContentUtils.java b/org.argeo.cms/src/org/argeo/cms/acr/ContentUtils.java index bf5954411..facb5933b 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/ContentUtils.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/ContentUtils.java @@ -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() { diff --git a/org.argeo.cms/src/org/argeo/cms/acr/MountManager.java b/org.argeo.cms/src/org/argeo/cms/acr/MountManager.java index 75ca427c7..90d621b76 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/MountManager.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/MountManager.java @@ -51,10 +51,10 @@ class MountManager { synchronized ContentProvider findContentProvider(String path) { // if (ContentUtils.EMPTY.equals(path)) // return partitions.firstEntry().getValue(); - Map.Entry entry = partitions.floorEntry(path); - if (entry == null) - throw new IllegalArgumentException("No entry provider found for path '" + path + "'"); - String mountPath = entry.getKey(); + Map.Entry 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 findContentProviders(String path) { + Map.Entry 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 res = new TreeMap<>(); - tail: for (Map.Entry provider : partitions.tailMap(path).entrySet()) { + res.put(floorEntry.getKey(), floorEntry.getValue()); + tail: for (Map.Entry provider : partitions.tailMap(parentProviderPath).entrySet()) { if (!provider.getKey().startsWith(path)) break tail; res.put(provider.getKey(), provider.getValue()); diff --git a/org.argeo.cms/src/org/argeo/cms/acr/xml/DomContentProvider.java b/org.argeo.cms/src/org/argeo/cms/acr/xml/DomContentProvider.java index a5abe8dd4..845a6ab4c 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/xml/DomContentProvider.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/xml/DomContentProvider.java @@ -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; diff --git a/org.argeo.cms/src/org/argeo/cms/auth/SingleUserLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/SingleUserLoginModule.java index 10e091ead..cfffb6eea 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/SingleUserLoginModule.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/SingleUserLoginModule.java @@ -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; } diff --git a/org.argeo.cms/src/org/argeo/cms/file/provider/CmsFileStore.java b/org.argeo.cms/src/org/argeo/cms/file/provider/CmsFileStore.java index a4da893b6..31fe9c73c 100644 --- a/org.argeo.cms/src/org/argeo/cms/file/provider/CmsFileStore.java +++ b/org.argeo.cms/src/org/argeo/cms/file/provider/CmsFileStore.java @@ -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 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; } diff --git a/org.argeo.cms/src/org/argeo/cms/file/provider/CmsFileSystem.java b/org.argeo.cms/src/org/argeo/cms/file/provider/CmsFileSystem.java index 6d4eea279..0b5d5defe 100644 --- a/org.argeo.cms/src/org/argeo/cms/file/provider/CmsFileSystem.java +++ b/org.argeo.cms/src/org/argeo/cms/file/provider/CmsFileSystem.java @@ -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 { + 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 getRootDirectories() { - // TODO Auto-generated method stub - return null; + return Collections.singleton(rootPath); } @Override public Iterable getFileStores() { - // TODO Auto-generated method stub - return null; + // TODO return all mount points + return Collections.singleton(baseFileStore); } @Override public Set 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; + } + } diff --git a/org.argeo.cms/src/org/argeo/cms/file/provider/CmsFileSystemProvider.java b/org.argeo.cms/src/org/argeo/cms/file/provider/CmsFileSystemProvider.java index 51eb84e71..602441e2f 100644 --- a/org.argeo.cms/src/org/argeo/cms/file/provider/CmsFileSystemProvider.java +++ b/org.argeo.cms/src/org/argeo/cms/file/provider/CmsFileSystemProvider.java @@ -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 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 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()); + } 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 newDirectoryStream(Path dir, Filter 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 getFileAttributeView(Path path, Class 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 readAttributes(Path path, Class 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; + } + } diff --git a/org.argeo.cms/src/org/argeo/cms/file/provider/CmsPath.java b/org.argeo.cms/src/org/argeo/cms/file/provider/CmsPath.java index 504e69bff..ce5e4b9e5 100644 --- a/org.argeo.cms/src/org/argeo/cms/file/provider/CmsPath.java +++ b/org.argeo.cms/src/org/argeo/cms/file/provider/CmsPath.java @@ -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 { + 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 { 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 index 000000000..827bb2a76 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/file/provider/ContentAttributes.java @@ -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 index 000000000..55db44360 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/file/provider/ContentDirectoryStream.java @@ -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 { + private final CmsPath dir; + private final Filter filter; + + private FilesAndCollectionsIterator iterator; + + public ContentDirectoryStream(CmsPath dir, Filter filter) { + this.dir = dir; + this.filter = filter; + } + + @Override + public void close() throws IOException { + } + + @Override + public Iterator iterator() { + if (iterator == null) + iterator = new FilesAndCollectionsIterator(); + return iterator; + } + + class FilesAndCollectionsIterator implements Iterator { + private Content next; + private final Iterator 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 index 000000000..adbe8d079 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/file/provider/ContentFileAttributeView.java @@ -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 list() throws IOException { +// List 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; + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/http/HttpHeader.java b/org.argeo.cms/src/org/argeo/cms/http/HttpHeader.java index ef7385d1d..67eec9172 100644 --- a/org.argeo.cms/src/org/argeo/cms/http/HttpHeader.java +++ b/org.argeo.cms/src/org/argeo/cms/http/HttpHeader.java @@ -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 index 000000000..7f34a5230 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/http/RemoteAuthHttpExchange.java @@ -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 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 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(); + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/http/server/HttpServerUtils.java b/org.argeo.cms/src/org/argeo/cms/http/server/HttpServerUtils.java index ab033f0ce..9d2c1930a 100644 --- a/org.argeo.cms/src/org/argeo/cms/http/server/HttpServerUtils.java +++ b/org.argeo.cms/src/org/argeo/cms/http/server/HttpServerUtils.java @@ -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> parseParameters(HttpExchange exchange) { + // TODO check encoding? + Charset encoding = StandardCharsets.UTF_8; + + Map> 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> 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 index 000000000..49cc242c7 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/http/server/StaticHttpHandler.java @@ -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 binds = new TreeMap<>(); + + @Override + public void handle(HttpExchange exchange) throws IOException { + try { + String path = HttpServerUtils.subPath(exchange); + Map.Entry 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 findBind(String path) { + Map.Entry 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); + } + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/internal/http/CmsAuthenticator.java b/org.argeo.cms/src/org/argeo/cms/internal/http/CmsAuthenticator.java index e17a089fe..a66a7397e 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/http/CmsAuthenticator.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/http/CmsAuthenticator.java @@ -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 index b7e670c79..000000000 --- a/org.argeo.cms/src/org/argeo/cms/internal/http/RemoteAuthHttpExchange.java +++ /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 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 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(); - } - -} diff --git a/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsAcrHttpHandler.java b/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsAcrHttpHandler.java index c80933a55..2847cb32f 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsAcrHttpHandler.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsAcrHttpHandler.java @@ -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; diff --git a/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsDeploymentImpl.java b/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsDeploymentImpl.java index e2d1fb97a..f9a1dc368 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsDeploymentImpl.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsDeploymentImpl.java @@ -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 = new CompletableFuture<>(); private Map httpHandlers = new TreeMap<>(); private Map httpAuthenticators = new TreeMap<>(); // SSHD - private CmsSshd cmsSshd; + private CompletableFuture 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 getHttpServer() { + return httpServer.minimalCompletionStage(); + } + + @Override + public CompletionStage getCmsSshd() { + return cmsSshd.minimalCompletionStage(); } } diff --git a/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsStateImpl.java b/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsStateImpl.java index 4d1d69845..60a51b44f 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsStateImpl.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsStateImpl.java @@ -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 index 000000000..636f5e33a --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/util/AsyncPipedOutputStream.java @@ -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 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 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; + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/util/LangUtils.java b/org.argeo.cms/src/org/argeo/cms/util/LangUtils.java index 0e214271d..e4cc607d6 100644 --- a/org.argeo.cms/src/org/argeo/cms/util/LangUtils.java +++ b/org.argeo.cms/src/org/argeo/cms/util/LangUtils.java @@ -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 > Map sortByValue(Map map) { + return sortByValue(map, false); + } + + public static > Map sortByValue(Map map, boolean descending) { + List> list = new ArrayList<>(map.entrySet()); + list.sort(Entry.comparingByValue()); + if (descending) + Collections.reverse(list); + + Map result = new LinkedHashMap<>(); + for (Entry entry : list) { + result.put(entry.getKey(), entry.getValue()); + } + + return result; + } + /* * EXCEPTIONS */ diff --git a/org.argeo.cms/src/org/argeo/cms/util/OS.java b/org.argeo.cms/src/org/argeo/cms/util/OS.java index 56a5fdfd4..8d7e693e9 100644 --- a/org.argeo.cms/src/org/argeo/cms/util/OS.java +++ b/org.argeo.cms/src/org/argeo/cms/util/OS.java @@ -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; diff --git a/org.argeo.cms/src/org/argeo/cms/util/StreamUtils.java b/org.argeo.cms/src/org/argeo/cms/util/StreamUtils.java index a589e739a..5fbef6b31 100644 --- a/org.argeo.cms/src/org/argeo/cms/util/StreamUtils.java +++ b/org.argeo.cms/src/org/argeo/cms/util/StreamUtils.java @@ -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; diff --git a/org.argeo.init/src/org/argeo/init/a2/AbstractProvisioningSource.java b/org.argeo.init/src/org/argeo/init/a2/AbstractProvisioningSource.java index 617e78878..f946add69 100644 --- a/org.argeo.init/src/org/argeo/init/a2/AbstractProvisioningSource.java +++ b/org.argeo.init/src/org/argeo/init/a2/AbstractProvisioningSource.java @@ -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); diff --git a/org.argeo.init/src/org/argeo/init/logging/ThinLogging.java b/org.argeo.init/src/org/argeo/init/logging/ThinLogging.java index 5b2c93924..c2ce21528 100644 --- a/org.argeo.init/src/org/argeo/init/logging/ThinLogging.java +++ b/org.argeo.init/src/org/argeo/init/logging/ThinLogging.java @@ -74,12 +74,13 @@ class ThinLogging implements Consumer> { 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> { 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 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> { // 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> { 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> { 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 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 index 000000000..1b75ea2a4 --- /dev/null +++ b/osgi/equinox/org.argeo.cms.lib.equinox/OSGI-INF/equinoxJettyServer.xml @@ -0,0 +1,10 @@ + + + + + + + + + + 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 index 6a1336220..000000000 --- a/osgi/equinox/org.argeo.cms.lib.equinox/OSGI-INF/jettyServiceFactory.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/osgi/equinox/org.argeo.cms.lib.equinox/bnd.bnd b/osgi/equinox/org.argeo.cms.lib.equinox/bnd.bnd index 2c83158e2..2ced2a26c 100644 --- a/osgi/equinox/org.argeo.cms.lib.equinox/bnd.bnd +++ b/osgi/equinox/org.argeo.cms.lib.equinox/bnd.bnd @@ -1,2 +1,2 @@ Service-Component: \ -OSGI-INF/jettyServiceFactory.xml,\ +OSGI-INF/equinoxJettyServer.xml,\ diff --git a/sdk/argeo-build b/sdk/argeo-build index d9cae87d8..d5943f556 160000 --- a/sdk/argeo-build +++ b/sdk/argeo-build @@ -1 +1 @@ -Subproject commit d9cae87d811258d5a13e43eea8492f3792377ce4 +Subproject commit d5943f556d6fba9db0dd63d4c4cfceef89e4888e diff --git a/sdk/branches/unstable.bnd b/sdk/branches/unstable.bnd index 1203fef8d..16c51ab97 100644 --- a/sdk/branches/unstable.bnd +++ b/sdk/branches/unstable.bnd @@ -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 diff --git a/sdk/cms-rcp.properties b/sdk/cms-rcp.properties index df8363b76..da2e68a43 100644 --- a/sdk/cms-rcp.properties +++ b/sdk/cms-rcp.properties @@ -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 index 000000000..f29a38c91 --- /dev/null +++ b/sdk/deploy/argeo-cms/usr/bin/jshc @@ -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 diff --git a/swt/org.argeo.cms.swt/OSGI-INF/cmsUserApp.xml b/swt/org.argeo.cms.swt/OSGI-INF/cmsUserApp.xml index 4f2a405d5..b656accf4 100644 --- a/swt/org.argeo.cms.swt/OSGI-INF/cmsUserApp.xml +++ b/swt/org.argeo.cms.swt/OSGI-INF/cmsUserApp.xml @@ -7,4 +7,5 @@ + diff --git a/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/CmsSwtUtils.java b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/CmsSwtUtils.java index 5d964090b..3a91dbc23 100644 --- a/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/CmsSwtUtils.java +++ b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/CmsSwtUtils.java @@ -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() { } diff --git a/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/AcrSwtImageManager.java b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/AcrSwtImageManager.java index 0984f57fb..3c825c1ff 100644 --- a/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/AcrSwtImageManager.java +++ b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/AcrSwtImageManager.java @@ -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 { } 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 diff --git a/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/LinkedControl.java b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/LinkedControl.java index 6a75dfb2c..4333f48df 100644 --- a/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/LinkedControl.java +++ b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/LinkedControl.java @@ -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; } diff --git a/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/SwtUiProvider.java b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/SwtUiProvider.java index 4988fc6b8..403bc8e87 100644 --- a/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/SwtUiProvider.java +++ b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/SwtUiProvider.java @@ -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); } diff --git a/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/app/CmsUserApp.java b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/app/CmsUserApp.java index add6e9edb..ca98f69be 100644 --- a/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/app/CmsUserApp.java +++ b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/app/CmsUserApp.java @@ -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 getUiNames() { Set 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 index 000000000..169c2d0c1 --- /dev/null +++ b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/app/SimpleSwtApp.java @@ -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 getUiNames() { + Set 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) { + } + +} diff --git a/swt/rap/org.argeo.cms.swt.rap/src/org/argeo/cms/web/CmsWebEntryPoint.java b/swt/rap/org.argeo.cms.swt.rap/src/org/argeo/cms/web/CmsWebEntryPoint.java index 216dc3654..238975543 100644 --- a/swt/rap/org.argeo.cms.swt.rap/src/org/argeo/cms/web/CmsWebEntryPoint.java +++ b/swt/rap/org.argeo.cms.swt.rap/src/org/argeo/cms/web/CmsWebEntryPoint.java @@ -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 */ diff --git a/swt/rap/org.argeo.swt.specific.rap/src/org/argeo/eclipse/ui/specific/EclipseUiSpecificUtils.java b/swt/rap/org.argeo.swt.specific.rap/src/org/argeo/eclipse/ui/specific/EclipseUiSpecificUtils.java index a89b921cd..84ab27679 100644 --- a/swt/rap/org.argeo.swt.specific.rap/src/org/argeo/eclipse/ui/specific/EclipseUiSpecificUtils.java +++ b/swt/rap/org.argeo.swt.specific.rap/src/org/argeo/eclipse/ui/specific/EclipseUiSpecificUtils.java @@ -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) { diff --git a/swt/rcp/org.argeo.cms.swt.rcp/OSGI-INF/cmsRcpDBusLauncher.xml b/swt/rcp/org.argeo.cms.swt.rcp/OSGI-INF/cmsRcpDBusLauncher.xml index e8bfefb3f..f66bf4b42 100644 --- a/swt/rcp/org.argeo.cms.swt.rcp/OSGI-INF/cmsRcpDBusLauncher.xml +++ b/swt/rcp/org.argeo.cms.swt.rcp/OSGI-INF/cmsRcpDBusLauncher.xml @@ -3,4 +3,5 @@ + diff --git a/swt/rcp/org.argeo.cms.swt.rcp/OSGI-INF/cmsRcpDisplayFactory.xml b/swt/rcp/org.argeo.cms.swt.rcp/OSGI-INF/cmsRcpDisplayFactory.xml index 8b1d14684..41cae5393 100644 --- a/swt/rcp/org.argeo.cms.swt.rcp/OSGI-INF/cmsRcpDisplayFactory.xml +++ b/swt/rcp/org.argeo.cms.swt.rcp/OSGI-INF/cmsRcpDisplayFactory.xml @@ -1,4 +1,8 @@ + + + + diff --git a/swt/rcp/org.argeo.cms.swt.rcp/bnd.bnd b/swt/rcp/org.argeo.cms.swt.rcp/bnd.bnd index 6f3758250..80b5bcd33 100644 --- a/swt/rcp/org.argeo.cms.swt.rcp/bnd.bnd +++ b/swt/rcp/org.argeo.cms.swt.rcp/bnd.bnd @@ -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,\ diff --git a/swt/rcp/org.argeo.cms.swt.rcp/src/org/argeo/cms/ui/rcp/CmsRcpApp.java b/swt/rcp/org.argeo.cms.swt.rcp/src/org/argeo/cms/ui/rcp/CmsRcpApp.java index a88ff3824..77aeae061 100644 --- a/swt/rcp/org.argeo.cms.swt.rcp/src/org/argeo/cms/ui/rcp/CmsRcpApp.java +++ b/swt/rcp/org.argeo.cms.swt.rcp/src/org/argeo/cms/ui/rcp/CmsRcpApp.java @@ -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 */ diff --git a/swt/rcp/org.argeo.cms.swt.rcp/src/org/argeo/cms/ui/rcp/CmsRcpDisplayFactory.java b/swt/rcp/org.argeo.cms.swt.rcp/src/org/argeo/cms/ui/rcp/CmsRcpDisplayFactory.java index 63a1fd84e..cd554de9d 100644 --- a/swt/rcp/org.argeo.cms.swt.rcp/src/org/argeo/cms/ui/rcp/CmsRcpDisplayFactory.java +++ b/swt/rcp/org.argeo.cms.swt.rcp/src/org/argeo/cms/ui/rcp/CmsRcpDisplayFactory.java @@ -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; + } + } diff --git a/swt/rcp/org.argeo.cms.swt.rcp/src/org/argeo/cms/ui/rcp/CmsRcpHttpLauncher.java b/swt/rcp/org.argeo.cms.swt.rcp/src/org/argeo/cms/ui/rcp/CmsRcpHttpLauncher.java index 6246b0d0d..8b81e6698 100644 --- a/swt/rcp/org.argeo.cms.swt.rcp/src/org/argeo/cms/ui/rcp/CmsRcpHttpLauncher.java +++ b/swt/rcp/org.argeo.cms.swt.rcp/src/org/argeo/cms/ui/rcp/CmsRcpHttpLauncher.java @@ -25,6 +25,8 @@ public class CmsRcpHttpLauncher { private final static Logger logger = System.getLogger(CmsRcpHttpLauncher.class.getName()); private CompletableFuture 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); } diff --git a/swt/rcp/org.argeo.cms.swt.rcp/src/org/argeo/cms/ui/rcp/dbus/CmsRcpDBusLauncher.java b/swt/rcp/org.argeo.cms.swt.rcp/src/org/argeo/cms/ui/rcp/dbus/CmsRcpDBusLauncher.java index ea6905757..34190b005 100644 --- a/swt/rcp/org.argeo.cms.swt.rcp/src/org/argeo/cms/ui/rcp/dbus/CmsRcpDBusLauncher.java +++ b/swt/rcp/org.argeo.cms.swt.rcp/src/org/argeo/cms/ui/rcp/dbus/CmsRcpDBusLauncher.java @@ -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 = new CompletableFuture<>(); private Map apps = new HashMap<>(); + private CmsRcpDisplayFactory cmsRcpDisplayFactory; + public void start() { } @@ -24,7 +27,8 @@ public class CmsRcpDBusLauncher { public void addCmsApp(CmsApp cmsApp, Map 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; + } + } diff --git a/swt/rcp/org.argeo.cms.swt.rcp/src/org/argeo/cms/ui/rcp/dbus/CmsRcpFreeDesktopApplication.java b/swt/rcp/org.argeo.cms.swt.rcp/src/org/argeo/cms/ui/rcp/dbus/CmsRcpFreeDesktopApplication.java index 98c84fa41..f1962f70a 100644 --- a/swt/rcp/org.argeo.cms.swt.rcp/src/org/argeo/cms/ui/rcp/dbus/CmsRcpFreeDesktopApplication.java +++ b/swt/rcp/org.argeo.cms.swt.rcp/src/org/argeo/cms/ui/rcp/dbus/CmsRcpFreeDesktopApplication.java @@ -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 index 000000000..adf6b6481 --- /dev/null +++ b/swt/rcp/org.argeo.swt.specific.rcp/src/org/eclipse/rap/fileupload/DiskFileUploadReceiver.java @@ -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 { + } +} diff --git a/swt/rcp/org.argeo.swt.specific.rcp/src/org/eclipse/rap/rwt/widgets/FileUpload.java b/swt/rcp/org.argeo.swt.specific.rcp/src/org/eclipse/rap/rwt/widgets/FileUpload.java index cbf1449e0..eec769132 100644 --- a/swt/rcp/org.argeo.swt.specific.rcp/src/org/eclipse/rap/rwt/widgets/FileUpload.java +++ b/swt/rcp/org.argeo.swt.specific.rcp/src/org/eclipse/rap/rwt/widgets/FileUpload.java @@ -34,4 +34,7 @@ public class FileUpload extends Composite { return null; } + public void setFilterExtensions(String[] exts) { + + } }