Merge tag 'v2.3.15' into testing
authorMathieu Baudier <mbaudier@argeo.org>
Thu, 29 Jun 2023 03:35:15 +0000 (05:35 +0200)
committerMathieu Baudier <mbaudier@argeo.org>
Thu, 29 Jun 2023 03:35:15 +0000 (05:35 +0200)
18 files changed:
Makefile
org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContent.java
org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContentProvider.java
org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContentUtils.java
org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrSessionAdapter.java
org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/KernelConstants.java
org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/CmsSessionProvider.java
org.argeo.cms.jcr/src/org/argeo/jcr/Jcr.java
org.argeo.cms.jcr/src/org/argeo/security/jackrabbit/ArgeoAuthContext.java
sdk/branches/testing.bnd
sdk/branches/unstable.bnd
swt/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/CmsUiProvider.java
swt/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/FormConstants.java
swt/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/FormPageViewer.java
swt/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/DefaultImageManager.java
swt/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/JcrVersionCmsEditable.java
swt/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/widgets/EditableImage.java
swt/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/widgets/Img.java

index ca20e89d4bcf725a091dddcae6818ce253e7c5f5..6fde8b6d626ac3f2b14a1f4b78a5305b1173cab2 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -15,7 +15,7 @@ swt/org.argeo.cms.jcr.ui \
 DEP_CATEGORIES = \
 org.argeo.tp \
 org.argeo.tp.jcr \
-osgi/api/org.argeo.tp.osgi \
+osgi/equinox/org.argeo.tp.osgi \
 osgi/equinox/org.argeo.tp.eclipse \
 swt/rap/org.argeo.tp.swt \
 swt/rap/org.argeo.tp.swt.workbench \
index 612eea89123e785a746ef6358951701a11b0691d..9e662ecc96ab8dcbf128c452ba1c1f42acfc661f 100644 (file)
@@ -5,14 +5,18 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.PipedInputStream;
 import java.io.PipedOutputStream;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
 import java.util.TreeSet;
+import java.util.UUID;
 import java.util.concurrent.ForkJoinPool;
 
 import javax.jcr.Node;
@@ -21,15 +25,18 @@ import javax.jcr.Property;
 import javax.jcr.PropertyIterator;
 import javax.jcr.PropertyType;
 import javax.jcr.RepositoryException;
+import javax.jcr.Session;
 import javax.jcr.Value;
+import javax.jcr.ValueFactory;
 import javax.jcr.nodetype.NodeType;
+import javax.jcr.nodetype.NodeTypeManager;
 import javax.xml.namespace.QName;
 import javax.xml.transform.Source;
 import javax.xml.transform.stream.StreamSource;
 
 import org.argeo.api.acr.Content;
+import org.argeo.api.acr.CrAttributeType;
 import org.argeo.api.acr.NamespaceUtils;
-import org.argeo.api.acr.spi.ContentProvider;
 import org.argeo.api.acr.spi.ProvidedSession;
 import org.argeo.api.cms.CmsConstants;
 import org.argeo.cms.acr.AbstractContent;
@@ -40,20 +47,36 @@ import org.argeo.jcr.JcrUtils;
 
 /** A JCR {@link Node} accessed as {@link Content}. */
 public class JcrContent extends AbstractContent {
-//     private Node jcrNode;
-
        private JcrContentProvider provider;
 
        private String jcrWorkspace;
        private String jcrPath;
 
+       private final boolean isMountBase;
+
+       /* OPTIMISATIONS */
+       /**
+        * While we want to support thread-safe access, it is very likely that only
+        * thread and only one sesssion will be used (typically from a single-threaded
+        * UI). We therefore cache was long as the same thread is calling.
+        */
+       private Thread lastRetrievingThread = null;
+       private Node cachedNode = null;
+       private boolean caching = true;
+
        protected JcrContent(ProvidedSession session, JcrContentProvider provider, String jcrWorkspace, String jcrPath) {
                super(session);
                this.provider = provider;
                this.jcrWorkspace = jcrWorkspace;
                this.jcrPath = jcrPath;
+
+               this.isMountBase = ContentUtils.SLASH_STRING.equals(jcrPath);
        }
 
+       /*
+        * READ
+        */
+
        @Override
        public QName getName() {
                String name = Jcr.getName(getJcrNode());
@@ -68,10 +91,11 @@ public class JcrContent extends AbstractContent {
        @SuppressWarnings("unchecked")
        @Override
        public <A> Optional<A> get(QName key, Class<A> clss) {
-               if (isDefaultAttrTypeRequested(clss)) {
-                       return Optional.of((A) get(getJcrNode(), key.toString()));
-               }
-               return Optional.of((A) Jcr.get(getJcrNode(), key.toString()));
+               Object value = get(getJcrNode(), key.toString());
+               if (value instanceof List<?> lst)
+                       return Optional.of((A) lst);
+               // TODO check other collections?
+               return CrAttributeType.cast(clss, value);
        }
 
        @Override
@@ -100,18 +124,11 @@ public class JcrContent extends AbstractContent {
                }
        }
 
-       public Node getJcrNode() {
-               try {
-                       // TODO caching?
-                       return provider.getJcrSession(getSession(), jcrWorkspace).getNode(jcrPath);
-               } catch (RepositoryException e) {
-                       throw new JcrException("Cannot retrieve " + jcrPath + " from workspace " + jcrWorkspace, e);
-               }
-       }
-
        /** Cast to a standard Java object. */
        static Object get(Node node, String property) {
                try {
+                       if (!node.hasProperty(property))
+                               return null;
                        Property p = node.getProperty(property);
                        if (p.isMultiple()) {
                                Value[] values = p.getValues();
@@ -125,57 +142,25 @@ public class JcrContent extends AbstractContent {
                                return convertSingleValue(value);
                        }
                } catch (RepositoryException e) {
-                       throw new JcrException("Cannot cast value from " + property + " of node " + node, e);
+                       throw new JcrException("Cannot cast value from " + property + " of " + node, e);
                }
        }
 
-       static Object convertSingleValue(Value value) throws RepositoryException {
-               switch (value.getType()) {
-               case PropertyType.STRING:
-                       return value.getString();
-               case PropertyType.DOUBLE:
-                       return (Double) value.getDouble();
-               case PropertyType.LONG:
-                       return (Long) value.getLong();
-               case PropertyType.BOOLEAN:
-                       return (Boolean) value.getBoolean();
-               case PropertyType.DATE:
-                       Calendar calendar = value.getDate();
-                       return calendar.toInstant();
-               case PropertyType.BINARY:
-                       throw new IllegalArgumentException("Binary is not supported as an attribute");
-               default:
-                       return value.getString();
-               }
-       }
-
-       class JcrContentIterator implements Iterator<Content> {
-               private final NodeIterator nodeIterator;
-               // we keep track in order to be able to delete it
-               private JcrContent current = null;
-
-               protected JcrContentIterator(NodeIterator nodeIterator) {
-                       this.nodeIterator = nodeIterator;
-               }
-
-               @Override
-               public boolean hasNext() {
-                       return nodeIterator.hasNext();
-               }
-
-               @Override
-               public Content next() {
-                       current = new JcrContent(getSession(), provider, jcrWorkspace, Jcr.getPath(nodeIterator.nextNode()));
-                       return current;
-               }
-
-               @Override
-               public void remove() {
-                       if (current != null) {
-                               Jcr.remove(current.getJcrNode());
+       @Override
+       public boolean isMultiple(QName key) {
+               Node node = getJcrNode();
+               String p = NamespaceUtils.toFullyQualified(key);
+               try {
+                       if (node.hasProperty(p)) {
+                               Property property = node.getProperty(p);
+                               return property.isMultiple();
+                       } else {
+                               return false;
                        }
+               } catch (RepositoryException e) {
+                       throw new JcrException(
+                                       "Cannot check multiplicityof property " + p + " of " + jcrPath + " in " + jcrWorkspace, e);
                }
-
        }
 
        @Override
@@ -183,7 +168,7 @@ public class JcrContent extends AbstractContent {
                try {
                        // Note: it is important to to use the default way (recursing through parents),
                        // since the session may not have access to parent nodes
-                       return ContentUtils.ROOT_SLASH + jcrWorkspace + getJcrNode().getPath();
+                       return Content.ROOT_PATH + jcrWorkspace + getJcrNode().getPath();
                } catch (RepositoryException e) {
                        throw new JcrException("Cannot get depth of " + getJcrNode(), e);
                }
@@ -200,16 +185,47 @@ public class JcrContent extends AbstractContent {
 
        @Override
        public Content getParent() {
-               if (Jcr.isRoot(getJcrNode())) // root
-                       return null;
+               if (isMountBase) {
+                       String mountPath = provider.getMountPath();
+                       if (mountPath == null || mountPath.equals("/"))
+                               return null;
+                       String[] parent = ContentUtils.getParentPath(mountPath);
+                       return getSession().get(parent[0]);
+               }
+//             if (Jcr.isRoot(getJcrNode())) // root
+//                     return null;
                return new JcrContent(getSession(), provider, jcrWorkspace, Jcr.getParentPath(getJcrNode()));
        }
 
+       @Override
+       public int getSiblingIndex() {
+               return Jcr.getIndex(getJcrNode());
+       }
+
+       /*
+        * MAP OPTIMISATIONS
+        */
+       @Override
+       public boolean containsKey(Object key) {
+               return Jcr.hasProperty(getJcrNode(), key.toString());
+       }
+
+       /*
+        * WRITE
+        */
+
+       protected Node openForEdit() {
+               Node node = getProvider().openForEdit(getSession(), jcrWorkspace, jcrPath);
+               getSession().notifyModification(this);
+               return node;
+       }
+
        @Override
        public Content add(QName name, QName... classes) {
                if (classes.length > 0) {
                        QName primaryType = classes[0];
-                       Node child = Jcr.addNode(getJcrNode(), name.toString(), primaryType.toString());
+                       Node node = openForEdit();
+                       Node child = Jcr.addNode(node, name.toString(), primaryType.toString());
                        for (int i = 1; i < classes.length; i++) {
                                try {
                                        child.addMixin(classes[i].toString());
@@ -226,12 +242,14 @@ public class JcrContent extends AbstractContent {
 
        @Override
        public void remove() {
-               Jcr.remove(getJcrNode());
+               Node node = openForEdit();
+               Jcr.remove(node);
        }
 
        @Override
        protected void removeAttr(QName key) {
-               Property property = Jcr.getProperty(getJcrNode(), key.toString());
+               Node node = openForEdit();
+               Property property = Jcr.getProperty(node, key.toString());
                if (property != null) {
                        try {
                                property.remove();
@@ -242,20 +260,79 @@ public class JcrContent extends AbstractContent {
 
        }
 
-       boolean exists() {
+       @Override
+       public Object put(QName key, Object value) {
+               try {
+                       String property = NamespaceUtils.toFullyQualified(key);
+                       Node node = openForEdit();
+                       Object old = null;
+                       if (node.hasProperty(property)) {
+                               old = convertSingleValue(node.getProperty(property).getValue());
+                       }
+                       Value newValue = convertSingleObject(node.getSession().getValueFactory(), value);
+                       node.setProperty(property, newValue);
+                       // FIXME proper edition
+                       node.getSession().save();
+                       return old;
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot set property " + key + " on " + jcrPath + " in " + jcrWorkspace, e);
+               }
+       }
+
+       @Override
+       public void addContentClasses(QName... contentClass) throws IllegalArgumentException, JcrException {
+               try {
+                       Node node = openForEdit();
+                       NodeTypeManager ntm = node.getSession().getWorkspace().getNodeTypeManager();
+                       List<NodeType> nodeTypes = new ArrayList<>();
+                       for (QName clss : contentClass) {
+                               NodeType nodeType = ntm.getNodeType(NamespaceUtils.toFullyQualified(clss));
+                               if (!nodeType.isMixin())
+                                       throw new IllegalArgumentException(clss + " is not a mixin");
+                               nodeTypes.add(nodeType);
+                       }
+                       for (NodeType nodeType : nodeTypes) {
+                               node.addMixin(nodeType.getName());
+                       }
+                       // FIXME proper edition
+                       node.getSession().save();
+               } catch (RepositoryException e) {
+                       throw new JcrException(
+                                       "Cannot add content classes " + contentClass + " to " + jcrPath + " in " + jcrWorkspace, e);
+               }
+       }
+
+       /*
+        * ACCESS
+        */
+       protected boolean exists() {
                try {
-                       return provider.getJcrSession(getSession(), jcrWorkspace).itemExists(jcrPath);
+                       return getJcrSession().itemExists(jcrPath);
                } catch (RepositoryException e) {
                        throw new JcrException("Cannot check whether " + jcrPath + " exists", e);
                }
        }
 
+       @Override
+       public boolean isParentAccessible() {
+               String jcrParentPath = ContentUtils.getParentPath(jcrPath)[0];
+               if ("".equals(jcrParentPath)) // JCR root node
+                       jcrParentPath = ContentUtils.SLASH_STRING;
+               try {
+                       return getJcrSession().hasPermission(jcrParentPath, Session.ACTION_READ);
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot check whether parent " + jcrParentPath + " is accessible", e);
+               }
+       }
+
        /*
         * ADAPTERS
         */
        @SuppressWarnings("unchecked")
        public <A> A adapt(Class<A> clss) {
-               if (Source.class.isAssignableFrom(clss)) {
+               if (Node.class.isAssignableFrom(clss)) {
+                       return (A) getJcrNode();
+               } else if (Source.class.isAssignableFrom(clss)) {
 //                     try {
                        PipedOutputStream out = new PipedOutputStream();
                        PipedInputStream in;
@@ -268,7 +345,7 @@ public class JcrContent extends AbstractContent {
                        ForkJoinPool.commonPool().execute(() -> {
 //                             try (PipedOutputStream out = new PipedOutputStream(in)) {
                                try {
-                                       provider.getJcrSession(getSession(), jcrWorkspace).exportDocumentView(jcrPath, out, true, false);
+                                       getJcrSession().exportDocumentView(jcrPath, out, true, false);
                                        out.flush();
                                        out.close();
                                } catch (IOException | RepositoryException e) {
@@ -280,9 +357,9 @@ public class JcrContent extends AbstractContent {
 //                     } catch (IOException e) {
 //                             throw new RuntimeException("Cannot adapt " + JcrContent.this + " to " + clss, e);
 //                     }
-               } else
-
+               } else {
                        return super.adapt(clss);
+               }
        }
 
        @SuppressWarnings("unchecked")
@@ -302,7 +379,7 @@ public class JcrContent extends AbstractContent {
        }
 
        @Override
-       public ContentProvider getProvider() {
+       public JcrContentProvider getProvider() {
                return provider;
        }
 
@@ -318,16 +395,93 @@ public class JcrContent extends AbstractContent {
        /*
         * TYPING
         */
+
+       static Object convertSingleValue(Value value) throws JcrException, IllegalArgumentException {
+               try {
+                       switch (value.getType()) {
+                       case PropertyType.STRING:
+                               return value.getString();
+                       case PropertyType.DOUBLE:
+                               return (Double) value.getDouble();
+                       case PropertyType.LONG:
+                               return (Long) value.getLong();
+                       case PropertyType.BOOLEAN:
+                               return (Boolean) value.getBoolean();
+                       case PropertyType.DATE:
+                               Calendar calendar = value.getDate();
+                               return calendar.toInstant();
+                       case PropertyType.BINARY:
+                               throw new IllegalArgumentException("Binary is not supported as an attribute");
+                       default:
+                               return value.getString();
+                       }
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot convert " + value + " to an object.", e);
+               }
+       }
+
+       static Value convertSingleObject(ValueFactory factory, Object value) {
+               if (value instanceof String string) {
+                       return factory.createValue(string);
+               } else if (value instanceof Double dbl) {
+                       return factory.createValue(dbl);
+               } else if (value instanceof Float flt) {
+                       return factory.createValue(flt);
+               } else if (value instanceof Long lng) {
+                       return factory.createValue(lng);
+               } else if (value instanceof Integer intg) {
+                       return factory.createValue(intg);
+               } else if (value instanceof Boolean bool) {
+                       return factory.createValue(bool);
+               } else if (value instanceof Instant instant) {
+                       GregorianCalendar calendar = new GregorianCalendar();
+                       calendar.setTime(Date.from(instant));
+                       return factory.createValue(calendar);
+               } else {
+                       // TODO or use String by default?
+                       throw new IllegalArgumentException("Unsupported value " + value.getClass());
+               }
+       }
+
+       @Override
+       public Class<?> getType(QName key) {
+               Node node = getJcrNode();
+               String p = NamespaceUtils.toFullyQualified(key);
+               try {
+                       if (node.hasProperty(p)) {
+                               Property property = node.getProperty(p);
+                               return switch (property.getType()) {
+                               case PropertyType.STRING:
+                               case PropertyType.NAME:
+                               case PropertyType.PATH:
+                               case PropertyType.DECIMAL:
+                                       yield String.class;
+                               case PropertyType.LONG:
+                                       yield Long.class;
+                               case PropertyType.DOUBLE:
+                                       yield Double.class;
+                               case PropertyType.BOOLEAN:
+                                       yield Boolean.class;
+                               case PropertyType.DATE:
+                                       yield Instant.class;
+                               case PropertyType.WEAKREFERENCE:
+                               case PropertyType.REFERENCE:
+                                       yield UUID.class;
+                               default:
+                                       yield Object.class;
+                               };
+                       } else {
+                               // TODO does it make sense?
+                               return Object.class;
+                       }
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot get type of property " + p + " of " + jcrPath + " in " + jcrWorkspace, e);
+               }
+       }
+
        @Override
        public List<QName> getContentClasses() {
                try {
-//                     Node node = getJcrNode();
-//                     List<QName> res = new ArrayList<>();
-//                     res.add(nodeTypeToQName(node.getPrimaryNodeType()));
-//                     for (NodeType mixin : node.getMixinNodeTypes()) {
-//                             res.add(nodeTypeToQName(mixin));
-//                     }
-//                     return res;
                        Node context = getJcrNode();
 
                        List<QName> res = new ArrayList<>();
@@ -348,15 +502,6 @@ public class JcrContent extends AbstractContent {
                                        secondaryTypes.add(nodeTypeToQName(superType));
                                }
                        }
-//             // entity type
-//             if (context.isNodeType(EntityType.entity.get())) {
-//                     if (context.hasProperty(EntityNames.ENTITY_TYPE)) {
-//                             String entityTypeName = context.getProperty(EntityNames.ENTITY_TYPE).getString();
-//                             if (byType.containsKey(entityTypeName)) {
-//                                     types.add(entityTypeName);
-//                             }
-//                     }
-//             }
                        res.addAll(secondaryTypes);
                        return res;
                } catch (RepositoryException e) {
@@ -367,12 +512,32 @@ public class JcrContent extends AbstractContent {
        private QName nodeTypeToQName(NodeType nodeType) {
                String name = nodeType.getName();
                return NamespaceUtils.parsePrefixedName(provider, name);
-               //return QName.valueOf(name);
+               // return QName.valueOf(name);
        }
 
-       @Override
-       public int getSiblingIndex() {
-               return Jcr.getIndex(getJcrNode());
+       /*
+        * COMMON UTILITIES
+        */
+       protected Session getJcrSession() {
+               return provider.getJcrSession(getSession(), jcrWorkspace);
+       }
+
+       protected Node getJcrNode() {
+               try {
+                       if (caching) {
+                               synchronized (this) {
+                                       if (lastRetrievingThread != Thread.currentThread()) {
+                                               cachedNode = getJcrSession().getNode(jcrPath);
+                                               lastRetrievingThread = Thread.currentThread();
+                                       }
+                                       return cachedNode;
+                               }
+                       } else {
+                               return getJcrSession().getNode(jcrPath);
+                       }
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot retrieve " + jcrPath + " from workspace " + jcrWorkspace, e);
+               }
        }
 
        /*
@@ -387,10 +552,43 @@ public class JcrContent extends AbstractContent {
                        if (contentSession == null)
                                throw new IllegalArgumentException(
                                                "Cannot adapt " + node + " to content, because it was not loaded from a content session");
-                       return contentSession.get(ContentUtils.SLASH + CmsConstants.SYS_WORKSPACE + node.getPath());
+                       return contentSession.get(Content.ROOT_PATH + CmsConstants.SYS_WORKSPACE + node.getPath());
                } catch (RepositoryException e) {
                        throw new JcrException("Cannot adapt " + node + " to a content", e);
                }
        }
 
+       /*
+        * CONTENT ITERATOR
+        */
+
+       class JcrContentIterator implements Iterator<Content> {
+               private final NodeIterator nodeIterator;
+               // we keep track in order to be able to delete it
+               private JcrContent current = null;
+
+               protected JcrContentIterator(NodeIterator nodeIterator) {
+                       this.nodeIterator = nodeIterator;
+               }
+
+               @Override
+               public boolean hasNext() {
+                       return nodeIterator.hasNext();
+               }
+
+               @Override
+               public Content next() {
+                       current = new JcrContent(getSession(), provider, jcrWorkspace, Jcr.getPath(nodeIterator.nextNode()));
+                       return current;
+               }
+
+               @Override
+               public void remove() {
+                       if (current != null) {
+                               Jcr.remove(current.getJcrNode());
+                       }
+               }
+
+       }
+
 }
index bc4aa5be418b33c966e87e95eb2a955bd2670996..5fc7d7c241d2496b040a13d2f2a0b68fa1fc7ed8 100644 (file)
@@ -61,20 +61,27 @@ public class JcrContentProvider implements ContentProvider, NamespaceContext {
                this.jcrRepository = jcrRepository;
        }
 
+       @Override
+       public String getMountPath() {
+               return mountPath;
+       }
+
+       /*
+        * READ
+        */
+
        @Override
        public ProvidedContent get(ProvidedSession contentSession, String relativePath) {
-               String jcrPath = "/" + relativePath;
-               return new JcrContent(contentSession, this, jcrWorkspace, jcrPath);
+               return new JcrContent(contentSession, this, jcrWorkspace, toJcrPath(relativePath));
        }
 
        @Override
        public boolean exists(ProvidedSession contentSession, String relativePath) {
-               String jcrWorkspace = ContentUtils.getParentPath(mountPath)[1];
-               String jcrPath = "/" + relativePath;
+               String jcrPath = ContentUtils.SLASH + relativePath;
                return new JcrContent(contentSession, this, jcrWorkspace, jcrPath).exists();
        }
 
-       public Session getJcrSession(ProvidedSession contentSession, String jcrWorkspace) {
+       protected JcrSessionAdapter getJcrSessionAdapter(ProvidedSession contentSession) {
                JcrSessionAdapter sessionAdapter = sessionAdapters.get(contentSession);
                if (sessionAdapter == null) {
                        final JcrSessionAdapter newSessionAdapter = new JcrSessionAdapter(jcrRepository, contentSession,
@@ -83,7 +90,11 @@ public class JcrContentProvider implements ContentProvider, NamespaceContext {
                        contentSession.onClose().thenAccept((s) -> newSessionAdapter.close());
                        sessionAdapter = newSessionAdapter;
                }
+               return sessionAdapter;
+       }
 
+       public Session getJcrSession(ProvidedSession contentSession, String jcrWorkspace) {
+               JcrSessionAdapter sessionAdapter = getJcrSessionAdapter(contentSession);
                Session jcrSession = sessionAdapter.getSession(jcrWorkspace);
                return jcrSession;
        }
@@ -92,21 +103,63 @@ public class JcrContentProvider implements ContentProvider, NamespaceContext {
                return getJcrSession(((ProvidedContent) content).getSession(), jcrWorkspace);
        }
 
+       /*
+        * WRITE
+        */
+       Node openForEdit(ProvidedSession contentSession, String jcrWorkspace, String jcrPath) {
+               try {
+                       if (contentSession.isEditing()) {
+                               JcrSessionAdapter sessionAdapter = getJcrSessionAdapter(contentSession);
+                               return sessionAdapter.openForEdit(jcrWorkspace, jcrPath);
+                       } else {
+                               return getJcrSession(contentSession, jcrWorkspace).getNode(jcrPath);
+                       }
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot open for edit " + jcrPath + " in workspace " + jcrWorkspace, e);
+               }
+       }
+
        @Override
-       public String getMountPath() {
-               return mountPath;
+       public void persist(ProvidedSession contentSession) {
+               try {
+                       JcrSessionAdapter sessionAdapter = getJcrSessionAdapter(contentSession);
+                       sessionAdapter.persist();
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot persist " + contentSession, e);
+               }
        }
 
-       public synchronized <T> T doInAdminSession(Function<Session, T> toDo) {
+       /*
+        * EDITING
+        */
+
+       @Override
+       public void openForEdit(ProvidedSession session, String relativePath) {
+               openForEdit(session, relativePath, toJcrPath(relativePath));
+       }
+
+       @Override
+       public void freeze(ProvidedSession session, String relativePath) {
                try {
-                       return toDo.apply(adminSession);
-               } finally {
-                       try {
-                               if (adminSession.hasPendingChanges())
-                                       adminSession.save();
-                       } catch (RepositoryException e) {
-                               throw new JcrException("Cannot save admin session", e);
+                       String jcrPath = toJcrPath(relativePath);
+                       if (session.isEditing()) {
+                               JcrSessionAdapter sessionAdapter = getJcrSessionAdapter(session);
+                               sessionAdapter.freeze(jcrWorkspace, jcrPath);
                        }
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot freeze " + relativePath + " in workspace " + jcrWorkspace, e);
+               }
+       }
+
+       @Override
+       public boolean isOpenForEdit(ProvidedSession session, String relativePath) {
+               try {
+                       String jcrPath = toJcrPath(relativePath);
+                       JcrSessionAdapter sessionAdapter = getJcrSessionAdapter(session);
+                       return sessionAdapter.isOpenForEdit(jcrWorkspace, jcrPath);
+               } catch (RepositoryException e) {
+                       throw new JcrException(
+                                       "Cannot check whether " + relativePath + " is open for edit in workspace " + jcrWorkspace, e);
                }
        }
 
@@ -199,4 +252,33 @@ public class JcrContentProvider implements ContentProvider, NamespaceContext {
                }
 
        }
+
+       /*
+        * UTILITIES
+        */
+       /**
+        * Just adds a '/' so that it becomes an absolute JCR path within the JCR
+        * workspace of this provider.
+        */
+       private String toJcrPath(String relativePath) {
+               return ContentUtils.SLASH + relativePath;
+       }
+
+       /*
+        * TRANSITIONAL, WHILE MIGRATING FROM JCR TO ACR
+        */
+       @Deprecated
+       public synchronized <T> T doInAdminSession(Function<Session, T> toDo) {
+               try {
+                       return toDo.apply(adminSession);
+               } finally {
+                       try {
+                               if (adminSession.hasPendingChanges())
+                                       adminSession.save();
+                       } catch (RepositoryException e) {
+                               throw new JcrException("Cannot save admin session", e);
+                       }
+               }
+       }
+
 }
index 25e54b302678b602c334cd22f7bb3ce78e9a8b7a..071eec4b8e95869117ebe587a5db712a42737780 100644 (file)
@@ -30,7 +30,6 @@ import javax.xml.transform.dom.DOMSource;
 
 import org.argeo.api.acr.Content;
 import org.argeo.api.acr.ContentName;
-import org.argeo.api.acr.CrName;
 import org.argeo.api.acr.DName;
 import org.argeo.api.acr.NamespaceUtils;
 import org.argeo.api.acr.spi.ProvidedContent;
index ae8ae80f29867636005d0c8428da88f3c769986d..0fc46b8c9cc734f344ecc2233922a006734d20a5 100644 (file)
@@ -4,14 +4,20 @@ import java.security.PrivilegedAction;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
 
+import javax.jcr.Node;
 import javax.jcr.Repository;
 import javax.jcr.RepositoryException;
 import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.version.VersionManager;
 import javax.security.auth.Subject;
 
 import org.apache.jackrabbit.core.SessionImpl;
 import org.argeo.api.acr.spi.ProvidedSession;
+import org.argeo.jcr.Jcr;
 import org.argeo.jcr.JcrException;
 import org.argeo.jcr.JcrUtils;
 
@@ -28,6 +34,14 @@ class JcrSessionAdapter {
 
        private Thread lastRetrievingThread = null;
 
+//     private Thread writeThread;
+       private Map<String, Session> writeSessions = new HashMap<>();
+       /**
+        * Path of versionable nodes which have been modified during an edition cycle.
+        */
+       private Map<String, Set<String>> checkedInModified = new HashMap<>();
+       private Map<String, Set<String>> checkedOutModified = new HashMap<>();
+
        public JcrSessionAdapter(Repository repository, ProvidedSession contentSession, Subject subject) {
                this.repository = repository;
                this.contentSession = contentSession;
@@ -61,19 +75,7 @@ class JcrSessionAdapter {
 
                Session session = threadSession.get(workspace);
                if (session == null) {
-                       session = Subject.doAs(subject, (PrivilegedAction<Session>) () -> {
-                               try {
-//                                     String username = CurrentUser.getUsername(subject);
-//                                     SimpleCredentials credentials = new SimpleCredentials(username, new char[0]);
-//                                     credentials.setAttribute(ProvidedSession.class.getName(), contentSession);
-                                       Session sess = repository.login(workspace);
-                                       // Jackrabbit specific:
-                                       ((SessionImpl)sess).setAttribute(ProvidedSession.class.getName(), contentSession);
-                                       return sess;
-                               } catch (RepositoryException e) {
-                                       throw new IllegalStateException("Cannot log in to " + workspace, e);
-                               }
-                       });
+                       session = login(workspace);
                        threadSession.put(workspace, session);
                }
 
@@ -88,4 +90,102 @@ class JcrSessionAdapter {
                return session;
        }
 
+       protected synchronized Session getWriteSession(String workspace) throws RepositoryException {
+               Session session = writeSessions.get(workspace);
+               if (session == null) {
+                       session = login(workspace);
+                       writeSessions.put(workspace, session);
+               } else {
+//                     if ((writeThread != Thread.currentThread()) && session.hasPendingChanges()) {
+//                             throw new IllegalStateException("Session " + contentSession + " is currently being written to");
+//                     }
+//                     writeThread = Thread.currentThread();
+               }
+               return session;
+       }
+
+       public synchronized Node openForEdit(String workspace, String jcrPath) throws RepositoryException {
+               Session session = getWriteSession(workspace);
+               Node node = session.getNode(jcrPath);
+               if (node.isNodeType(NodeType.MIX_SIMPLE_VERSIONABLE)) {
+                       VersionManager versionManager = session.getWorkspace().getVersionManager();
+                       if (versionManager.isCheckedOut(jcrPath)) {
+                               if (!checkedOutModified.containsKey(workspace))
+                                       checkedOutModified.put(workspace, new TreeSet<>());
+                               checkedOutModified.get(workspace).add(jcrPath);
+                       } else {
+                               if (!checkedInModified.containsKey(workspace))
+                                       checkedInModified.put(workspace, new TreeSet<>());
+                               checkedInModified.get(workspace).add(jcrPath);
+                               versionManager.checkout(jcrPath);
+                       }
+               }
+               return node;
+       }
+
+       public synchronized Node freeze(String workspace, String jcrPath) throws RepositoryException {
+               Session session = getWriteSession(workspace);
+               Node node = session.getNode(jcrPath);
+               if (node.isNodeType(NodeType.MIX_SIMPLE_VERSIONABLE)) {
+                       VersionManager versionManager = session.getWorkspace().getVersionManager();
+                       if (versionManager.isCheckedOut(jcrPath)) {
+                               versionManager.checkin(jcrPath);
+                       }
+               }
+               return node;
+       }
+
+       public synchronized boolean isOpenForEdit(String workspace, String jcrPath) throws RepositoryException {
+               Session session = getWriteSession(workspace);
+               VersionManager versionManager = session.getWorkspace().getVersionManager();
+               return versionManager.isCheckedOut(jcrPath);
+       }
+
+       public synchronized void persist() throws RepositoryException {
+               for (String workspace : writeSessions.keySet()) {
+                       Session session = writeSessions.get(workspace);
+                       if (session == null) {
+//                             assert writeThread == null;
+                               assert !checkedOutModified.containsKey(workspace);
+                               assert !checkedInModified.containsKey(workspace);
+                               return; // nothing to do
+                       }
+                       session.save();
+                       VersionManager versionManager = session.getWorkspace().getVersionManager();
+                       if (checkedOutModified.containsKey(workspace))
+                               for (String jcrPath : checkedOutModified.get(workspace)) {
+                                       versionManager.checkpoint(jcrPath);
+                               }
+                       if (checkedInModified.containsKey(workspace))
+                               for (String jcrPath : checkedInModified.get(workspace)) {
+                                       versionManager.checkin(jcrPath);
+                               }
+                       Jcr.logout(session);
+               }
+
+               for (Map<String, Session> m : threadSessions.values())
+                       for (Session session : m.values())
+                               session.refresh(true);
+//                     writeThread = null;
+               writeSessions.clear();
+               checkedOutModified.clear();
+               checkedInModified.clear();
+       }
+
+       protected Session login(String workspace) {
+               return Subject.doAs(subject, (PrivilegedAction<Session>) () -> {
+                       try {
+//                             String username = CurrentUser.getUsername(subject);
+//                             SimpleCredentials credentials = new SimpleCredentials(username, new char[0]);
+//                             credentials.setAttribute(ProvidedSession.class.getName(), contentSession);
+                               Session sess = repository.login(workspace);
+                               // Jackrabbit specific:
+                               ((SessionImpl) sess).setAttribute(ProvidedSession.class.getName(), contentSession);
+                               return sess;
+                       } catch (RepositoryException e) {
+                               throw new IllegalStateException("Cannot log in to " + workspace, e);
+                       }
+               });
+       }
+
 }
index 93f29fbe8fda068c825be89c35a9fbda494d8415..3b8be28faadbca30fb17fdfd9e4b30f4cbf78f46 100644 (file)
@@ -4,7 +4,7 @@ import org.argeo.api.cms.CmsConstants;
 
 /** Internal CMS constants. */
 @Deprecated
-public interface KernelConstants {
+interface KernelConstants {
        // Directories
        String DIR_NODE = "node";
        String DIR_REPOS = "repos";
index 4e067eea25a01653e1e7a0fbb318687628497091..7c8e95f1e2f8fb786bd990b219e013909ddd2b44 100644 (file)
@@ -159,7 +159,7 @@ public class CmsSessionProvider implements SessionProvider, Serializable {
                private void checkValid() {
                        if (!cmsSession.isValid())
                                throw new IllegalStateException(
-                                               "CMS session " + cmsSession.getUuid() + " is not valid since " + cmsSession.getEnd());
+                                               "CMS session " + cmsSession.uuid() + " is not valid since " + cmsSession.getEnd());
                }
 
                protected void close() {
index 88f534503d5a142e5a4d87d4deec57a0e5c24979..bdeaac707511d554403fcea465b8fc80780f9de5 100644 (file)
@@ -431,6 +431,20 @@ public class Jcr {
                }
        }
 
+       /**
+        * Whether this node has this property.
+        * 
+        * @see Node#hasProperty(String)
+        * @throws JcrException caused by {@link RepositoryException}
+        */
+       public static boolean hasProperty(Node node, String property) {
+               try {
+                       return node.hasProperty(property);
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot check whether " + node + " has property " + property, e);
+               }
+       }
+
        /**
         * Set a property to the given value, or remove it if the value is
         * <code>null</code>.
@@ -603,6 +617,7 @@ public class Jcr {
                }
        }
 
+       @SuppressWarnings("unchecked")
        public static <T> T getAs(Node node, String property, Class<T> clss) {
                if (String.class.isAssignableFrom(clss)) {
                        return (T) get(node, property);
index d679c45f96ff8fcc59c99d06e191983ed3256884..97dfdf5dcdd39a56597e4ef463f5de45f2c4b1d9 100644 (file)
@@ -11,17 +11,32 @@ import org.apache.jackrabbit.core.security.authentication.AuthContext;
 class ArgeoAuthContext implements AuthContext {
        private LoginContext lc;
 
+       private String loginContextName;
+
        public ArgeoAuthContext(String appName, Subject subject, CallbackHandler callbackHandler) {
+               this.loginContextName = appName;
+               // Context class loader for login context is set when it is created.
+               // we make sure that it uses our won class loader
+               ClassLoader currentContextCl = Thread.currentThread().getContextClassLoader();
                try {
-                       lc = new LoginContext(appName, subject, callbackHandler);
+                       Thread.currentThread().setContextClassLoader(SystemJackrabbitLoginModule.class.getClassLoader());
+                       lc = new LoginContext(loginContextName, subject, callbackHandler);
                } catch (LoginException e) {
                        throw new IllegalStateException("Cannot configure Jackrabbit login context", e);
+               } finally {
+                       Thread.currentThread().setContextClassLoader(currentContextCl);
                }
        }
 
        @Override
        public void login() throws LoginException {
-               lc.login();
+               try {
+                       lc.login();
+               } catch (LoginException e) {
+                       // we force a runtime exception since Jackrabbit swallows LoginException
+                       // and still create a session
+                       throw new IllegalStateException("Login context " + loginContextName + " failed", e);
+               }
        }
 
        @Override
index 0bffadd6140ffd6fa6af59ccb0a8825e60931110..19d097d5ad838c411c344252f8161ad54b28eaea 100644 (file)
@@ -2,3 +2,11 @@ major=2
 minor=1
 micro=2
 qualifier=.next
+
+Bundle-Copyright= \
+Copyright 2009-2023 Mathieu Baudier, \
+Copyright 2012-2023 Argeo GmbH
+
+SPDX-License-Identifier= \
+GPL-2.0-or-later \
+OR LicenseRef-argeo2-GPL-2.0-or-later-with-EPL-and-JCR-permissions
index 8fc9c6646a0fa4528c3ea2f4ab236264aac5144b..29a2ca76a98f0ba967738211ca53acc44728da1b 100644 (file)
@@ -1,6 +1,6 @@
 major=2
 minor=3
-micro=14
+micro=15
 qualifier=
 
 Bundle-Copyright= \
index 5f2377be566f05a6ad618f1e30cea3c8e943b3e4..cc6f9a71a8b65d351398237b72b5d91e44249fb2 100644 (file)
@@ -36,7 +36,7 @@ public interface CmsUiProvider extends SwtUiProvider {
                if (context == null)
                        return createUiPart(parent, (Node) null);
                if (context instanceof JcrContent) {
-                       Node node = ((JcrContent) context).getJcrNode();
+                       Node node = context.adapt(Node.class);
                        return createUiPart(parent, node);
                } else {
 //                     CmsLog.getLog(CmsUiProvider.class)
index fe9f7e7d7537062c4541f4d652a6d3ae62829c9d..2ea824c20b4f278139b3e06f693de81803ad2579 100644 (file)
@@ -1,7 +1,8 @@
 package org.argeo.cms.ui.forms;
 
 /** Constants used in the various CMS Forms */
-public interface FormConstants {
+@Deprecated
+interface FormConstants {
        // DATAKEYS
        public final static String LINKED_VALUE = "LinkedValue";
 }
index 1888055fccef9c0cbcb9a462200ca2aec8a64e3e..eaeab95b43d7613e574930486a34ece2455ad13d 100644 (file)
@@ -188,7 +188,7 @@ public class FormPageViewer extends AbstractPageViewer {
                        // use control AFTER setting style, since it may have been reset
                        if (part instanceof EditableImage) {
                                EditableImage editableImage = (EditableImage) part;
-                               imageManager().load(partNode, part.getControl(), editableImage.getPreferredImageSize());
+                               imageManager().load(partNode, part.getControl(), editableImage.getPreferredImageSize(), null);
                        }
                }
        }
@@ -305,7 +305,7 @@ public class FormPageViewer extends AbstractPageViewer {
 
        protected CmsImageManager<Control, Node> imageManager() {
                if (imageManager == null)
-                       imageManager = (CmsImageManager<Control, Node>) CmsSwtUtils.getCmsView(mainSection).getImageManager();
+                       imageManager = CmsSwtUtils.getCmsView(mainSection).getImageManager();
                return imageManager;
        }
 
index b431fc3c9618c428dd14219c3923ef7d06381d21..405642955a5e6ce3ef5b9f490fec12869b4b256f 100644 (file)
@@ -24,9 +24,7 @@ import org.argeo.jcr.JcrException;
 import org.argeo.jcr.JcrUtils;
 import org.eclipse.rap.rwt.RWT;
 import org.eclipse.rap.rwt.service.ResourceManager;
-import org.eclipse.swt.graphics.Image;
 import org.eclipse.swt.graphics.ImageData;
-import org.eclipse.swt.widgets.Display;
 
 /** Manages only public images so far. */
 public class DefaultImageManager extends AbstractSwtImageManager<Node> {
@@ -62,14 +60,14 @@ public class DefaultImageManager extends AbstractSwtImageManager<Node> {
                }
        }
 
-       public Image getSwtImage(Node node) {
+       public ImageData getSwtImageData(Node node) {
                InputStream inputStream = null;
                Binary binary = getImageBinary(node);
                if (binary == null)
                        return null;
                try {
                        inputStream = binary.getStream();
-                       return new Image(Display.getCurrent(), inputStream);
+                       return new ImageData(inputStream);
                } catch (RepositoryException e) {
                        throw new JcrException(e);
                } finally {
index 256831468d5ab9334da2bf68d58981e873b6cc7b..91224490a7aa4d8c54fffd03f3986ab0183bca90 100644 (file)
@@ -30,10 +30,10 @@ public class JcrVersionCmsEditable extends AbstractCmsEditable {
                if (node.getSession().hasPermission(node.getPath(), Session.ACTION_SET_PROPERTY)) {
                        // was Session.ACTION_ADD_NODE
                        canEdit = true;
-                       if (!node.isNodeType(NodeType.MIX_VERSIONABLE)) {
-                               node.addMixin(NodeType.MIX_VERSIONABLE);
-                               node.getSession().save();
-                       }
+//                     if (!node.isNodeType(NodeType.MIX_SIMPLE_VERSIONABLE)) {
+//                             node.addMixin(NodeType.MIX_SIMPLE_VERSIONABLE);
+//                             node.getSession().save();
+//                     }
                        versionManager = node.getSession().getWorkspace().getVersionManager();
                } else {
                        canEdit = false;
index 95d9e8ee9a0d5d957ec54b46dcecec9d761f9b90..85ccec912cf700149363e512e8a414da0cfd26b9 100644 (file)
@@ -83,7 +83,7 @@ public abstract class EditableImage extends StyledControl {
                if (control != null) {
                        ((Label) control).setText(imgTag);
                        control.setSize(preferredImageSize != null
-                                       ? new Point(preferredImageSize.getWidth(), preferredImageSize.getHeight())
+                                       ? new Point(preferredImageSize.width(), preferredImageSize.height())
                                        : getSize());
                } else {
                        loaded = false;
index 41063fa47239d3ffb86166bf58de2d6e2d3e5c12..f5cae6ae9de70e0abca05b398af96a8468c2e311 100644 (file)
@@ -53,8 +53,7 @@ public class Img extends EditableImage implements SectionPart, NodePart {
                        CmsImageManager<Control, Node> imageManager) throws RepositoryException {
                super(parent, swtStyle, imgNode, false, preferredImageSize);
                this.section = section;
-               this.imageManager = imageManager != null ? imageManager
-                               : (CmsImageManager<Control, Node>) CmsSwtUtils.getCmsView(section).getImageManager();
+               this.imageManager = imageManager != null ? imageManager : CmsSwtUtils.getCmsView(section).getImageManager();
                CmsSwtUtils.style(this, TextStyles.TEXT_IMG);
        }
 
@@ -80,7 +79,7 @@ public class Img extends EditableImage implements SectionPart, NodePart {
        @Override
        protected synchronized Boolean load(Control lbl) {
                Node imgNode = getNode();
-               boolean loaded = imageManager.load(imgNode, lbl, getPreferredImageSize());
+               boolean loaded = imageManager.load(imgNode, lbl, getPreferredImageSize(), null);
                // getParent().layout();
                return loaded;
        }