Improve edition
authorMathieu Baudier <mbaudier@argeo.org>
Thu, 22 Jun 2023 05:57:27 +0000 (07:57 +0200)
committerMathieu Baudier <mbaudier@argeo.org>
Thu, 22 Jun 2023 05:57:27 +0000 (07:57 +0200)
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/JcrSessionAdapter.java

index 2b7676adcc09e8c00785a5814a2a99176ea41d43..864fbfb7eb8efead51c7475d7b0ce8453ba15fc3 100644 (file)
@@ -37,7 +37,6 @@ 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;
@@ -48,8 +47,6 @@ 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;
@@ -67,7 +64,7 @@ public class JcrContent extends AbstractContent {
        }
 
        /*
-        * READ/WRITE
+        * READ
         */
 
        @Override
@@ -85,9 +82,6 @@ public class JcrContent extends AbstractContent {
        @Override
        public <A> Optional<A> get(QName key, Class<A> clss) {
                Object value = get(getJcrNode(), key.toString());
-//             if (isDefaultAttrTypeRequested(clss)) {
-//                     return Optional.ofNullable((A) value);
-//             }
                return CrAttributeType.cast(clss, value);
        }
 
@@ -139,89 +133,6 @@ public class JcrContent extends AbstractContent {
                }
        }
 
-       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 boolean isMultiple(QName key) {
                Node node = getJcrNode();
@@ -239,35 +150,6 @@ public class JcrContent extends AbstractContent {
                }
        }
 
-       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 String getPath() {
                try {
@@ -302,11 +184,27 @@ public class JcrContent extends AbstractContent {
                return new JcrContent(getSession(), provider, jcrWorkspace, Jcr.getParentPath(getJcrNode()));
        }
 
+       @Override
+       public int getSiblingIndex() {
+               return Jcr.getIndex(getJcrNode());
+       }
+
+       /*
+        * 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());
@@ -323,12 +221,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();
@@ -343,12 +243,12 @@ public class JcrContent extends AbstractContent {
        public Object put(QName key, Object value) {
                try {
                        String property = NamespaceUtils.toFullyQualified(key);
-                       Node node = getJcrNode();
+                       Node node = openForEdit();
                        Object old = null;
                        if (node.hasProperty(property)) {
                                old = convertSingleValue(node.getProperty(property).getValue());
                        }
-                       Value newValue = convertSingleObject(getJcrSession().getValueFactory(), value);
+                       Value newValue = convertSingleObject(node.getSession().getValueFactory(), value);
                        node.setProperty(property, newValue);
                        // FIXME proper edition
                        node.getSession().save();
@@ -358,6 +258,29 @@ public class JcrContent extends AbstractContent {
                }
        }
 
+       @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
         */
@@ -435,7 +358,7 @@ public class JcrContent extends AbstractContent {
        }
 
        @Override
-       public ContentProvider getProvider() {
+       public JcrContentProvider getProvider() {
                return provider;
        }
 
@@ -451,16 +374,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<>();
@@ -481,15 +481,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) {
@@ -503,34 +494,6 @@ public class JcrContent extends AbstractContent {
                // return QName.valueOf(name);
        }
 
-       @Override
-       public void addContentClasses(QName... contentClass) throws IllegalArgumentException, JcrException {
-               try {
-                       NodeTypeManager ntm = getJcrSession().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);
-                       }
-                       Node node = getJcrNode();
-                       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);
-               }
-       }
-
-       @Override
-       public int getSiblingIndex() {
-               return Jcr.getIndex(getJcrNode());
-       }
-
        /*
         * COMMON UTILITIES
         */
@@ -565,4 +528,37 @@ public class JcrContent extends AbstractContent {
                }
        }
 
+       /*
+        * 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 0e641b19a7c0f8bc06522dae41f68a997530a6c3..258facc6c1b6c3658fc786ef4a45339eaaab8eab 100644 (file)
@@ -73,7 +73,7 @@ public class JcrContentProvider implements ContentProvider, NamespaceContext {
                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,
@@ -82,7 +82,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;
        }
@@ -91,6 +95,32 @@ public class JcrContentProvider implements ContentProvider, NamespaceContext {
                return getJcrSession(((ProvidedContent) content).getSession(), jcrWorkspace);
        }
 
+       /*
+        * WRITE
+        */
+       public 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 void persist(ProvidedSession contentSession) {
+               try {
+                       JcrSessionAdapter sessionAdapter = getJcrSessionAdapter(contentSession);
+                       sessionAdapter.persist();
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot persist " + contentSession, e);
+               }
+       }
+
        @Override
        public String getMountPath() {
                return mountPath;
index ae8ae80f29867636005d0c8428da88f3c769986d..e1ded7d1f8aab9141e05ef9e21cf409ad2a533a0 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,84 @@ 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 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);
+                       }
+               });
+       }
+
 }