From 311d6e47ad278fd00d1ad15fe9d59be47e23e385 Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Thu, 22 Jun 2023 07:57:27 +0200 Subject: [PATCH] Improve edition --- .../src/org/argeo/cms/jcr/acr/JcrContent.java | 334 +++++++++--------- .../argeo/cms/jcr/acr/JcrContentProvider.java | 32 +- .../argeo/cms/jcr/acr/JcrSessionAdapter.java | 108 +++++- 3 files changed, 291 insertions(+), 183 deletions(-) diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContent.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContent.java index 2b7676a..864fbfb 100644 --- a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContent.java +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContent.java @@ -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 Optional get(QName key, Class 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 { - 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 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 getContentClasses() { try { -// Node node = getJcrNode(); -// List res = new ArrayList<>(); -// res.add(nodeTypeToQName(node.getPrimaryNodeType())); -// for (NodeType mixin : node.getMixinNodeTypes()) { -// res.add(nodeTypeToQName(mixin)); -// } -// return res; Node context = getJcrNode(); List 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 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 { + 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()); + } + } + + } + } diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContentProvider.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContentProvider.java index 0e641b1..258facc 100644 --- a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContentProvider.java +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContentProvider.java @@ -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; diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrSessionAdapter.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrSessionAdapter.java index ae8ae80..e1ded7d 100644 --- a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrSessionAdapter.java +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrSessionAdapter.java @@ -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 writeSessions = new HashMap<>(); + /** + * Path of versionable nodes which have been modified during an edition cycle. + */ + private Map> checkedInModified = new HashMap<>(); + private Map> 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) () -> { - 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 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) () -> { + 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); + } + }); + } + } -- 2.30.2