From 795d4a749c7b9c809b163538d7af695478e3fbd5 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Tue, 28 Jul 2026 09:49:55 +0200 Subject: [PATCH 1/6] ACL with no auto-commit --- .../java/dev/vml/es/acm/core/acl/Acl.java | 7 ++++++- .../dev/vml/es/acm/core/acl/AclContext.java | 9 +++++++-- .../core/acl/utils/AuthorizableManager.java | 14 ++++++++++++-- .../core/acl/utils/PermissionsManager.java | 19 ++++++++++++++++--- .../dev/vml/es/acm/core/code/CodeContext.java | 2 +- 5 files changed, 42 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/dev/vml/es/acm/core/acl/Acl.java b/core/src/main/java/dev/vml/es/acm/core/acl/Acl.java index ca4019dc5..301f801ca 100644 --- a/core/src/main/java/dev/vml/es/acm/core/acl/Acl.java +++ b/core/src/main/java/dev/vml/es/acm/core/acl/Acl.java @@ -5,6 +5,7 @@ import dev.vml.es.acm.core.acl.authorizable.AclUser; import dev.vml.es.acm.core.util.GroovyUtils; import groovy.lang.Closure; +import java.util.function.Supplier; import org.apache.jackrabbit.api.security.user.Authorizable; import org.apache.jackrabbit.api.security.user.Group; import org.apache.jackrabbit.api.security.user.User; @@ -17,7 +18,11 @@ public class Acl { private final AclChecker checker; public Acl(ResourceResolver resourceResolver) { - this.context = new AclContext(resourceResolver); + this(resourceResolver, () -> true); + } + + public Acl(ResourceResolver resourceResolver, Supplier autoCommit) { + this.context = new AclContext(resourceResolver, autoCommit); this.checker = new AclChecker(context); } diff --git a/core/src/main/java/dev/vml/es/acm/core/acl/AclContext.java b/core/src/main/java/dev/vml/es/acm/core/acl/AclContext.java index 3e60f0c3d..36ec6112a 100644 --- a/core/src/main/java/dev/vml/es/acm/core/acl/AclContext.java +++ b/core/src/main/java/dev/vml/es/acm/core/acl/AclContext.java @@ -7,6 +7,7 @@ import dev.vml.es.acm.core.acl.utils.PermissionsManager; import dev.vml.es.acm.core.repo.Repo; import java.util.Optional; +import java.util.function.Supplier; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.ValueFactory; @@ -31,6 +32,10 @@ public class AclContext { private final PermissionsManager permissionsManager; public AclContext(ResourceResolver resourceResolver) { + this(resourceResolver, () -> true); + } + + public AclContext(ResourceResolver resourceResolver, Supplier autoCommit) { try { this.logger = LoggerFactory.getLogger(AclContext.class); JackrabbitSession session = (JackrabbitSession) resourceResolver.adaptTo(Session.class); @@ -38,8 +43,8 @@ public AclContext(ResourceResolver resourceResolver) { AccessControlManager accessControlManager = session.getAccessControlManager(); ValueFactory valueFactory = session.getValueFactory(); this.resourceResolver = resourceResolver; - this.authorizableManager = new AuthorizableManager(session, userManager, valueFactory); - this.permissionsManager = new PermissionsManager(session, accessControlManager, valueFactory); + this.authorizableManager = new AuthorizableManager(session, userManager, valueFactory, autoCommit); + this.permissionsManager = new PermissionsManager(session, accessControlManager, valueFactory, autoCommit); } catch (RepositoryException e) { throw new AclException("Cannot access repository while obtaining ACL context!", e); } diff --git a/core/src/main/java/dev/vml/es/acm/core/acl/utils/AuthorizableManager.java b/core/src/main/java/dev/vml/es/acm/core/acl/utils/AuthorizableManager.java index ce64364da..3d3acb1ea 100644 --- a/core/src/main/java/dev/vml/es/acm/core/acl/utils/AuthorizableManager.java +++ b/core/src/main/java/dev/vml/es/acm/core/acl/utils/AuthorizableManager.java @@ -5,6 +5,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.function.Supplier; import javax.jcr.Credentials; import javax.jcr.LoginException; import javax.jcr.Repository; @@ -27,10 +28,17 @@ public class AuthorizableManager { private final ValueFactory valueFactory; - public AuthorizableManager(JackrabbitSession session, UserManager userManager, ValueFactory valueFactory) { + private final Supplier autoCommit; + + public AuthorizableManager( + JackrabbitSession session, + UserManager userManager, + ValueFactory valueFactory, + Supplier autoCommit) { this.session = session; this.userManager = userManager; this.valueFactory = valueFactory; + this.autoCommit = autoCommit; } public User createUser(String id, String password, String path) { @@ -229,6 +237,8 @@ public boolean removeProperty(Authorizable authorizable, String relPath) { } private void save() throws RepositoryException { - session.save(); + if (autoCommit.get()) { + session.save(); + } } } diff --git a/core/src/main/java/dev/vml/es/acm/core/acl/utils/PermissionsManager.java b/core/src/main/java/dev/vml/es/acm/core/acl/utils/PermissionsManager.java index d846a128a..cd1ed0dcf 100644 --- a/core/src/main/java/dev/vml/es/acm/core/acl/utils/PermissionsManager.java +++ b/core/src/main/java/dev/vml/es/acm/core/acl/utils/PermissionsManager.java @@ -11,6 +11,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.Supplier; import java.util.stream.Collectors; import javax.jcr.Node; import javax.jcr.NodeIterator; @@ -46,11 +47,23 @@ public class PermissionsManager { private final ValueFactory valueFactory; + private final Supplier autoCommit; + public PermissionsManager( - JackrabbitSession session, AccessControlManager accessControlManager, ValueFactory valueFactory) { + JackrabbitSession session, + AccessControlManager accessControlManager, + ValueFactory valueFactory, + Supplier autoCommit) { this.session = session; this.accessControlManager = accessControlManager; this.valueFactory = valueFactory; + this.autoCommit = autoCommit; + } + + private void save() throws RepositoryException { + if (autoCommit.get()) { + session.save(); + } } public void apply( @@ -71,7 +84,7 @@ public void apply( updateAccessControlList( authorizable.getPrincipal(), path, modifyPermissions, modifyRestrictions, allow); } - session.save(); + save(); } catch (RepositoryException e) { throw new AclException( String.format("Failed to apply permissions for authorizable '%s' at path '%s'", id, path), e); @@ -215,7 +228,7 @@ private boolean removeAll(Authorizable authorizable, String path) { } if (result) { accessControlManager.setPolicy(path, jackrabbitAcl); - session.save(); + save(); } return result; } catch (RepositoryException e) { diff --git a/core/src/main/java/dev/vml/es/acm/core/code/CodeContext.java b/core/src/main/java/dev/vml/es/acm/core/code/CodeContext.java index b5b1b03c9..93bd87aeb 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/CodeContext.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/CodeContext.java @@ -46,7 +46,7 @@ public CodeContext(OsgiContext osgiContext, ResourceResolver resourceResolver) { this.log = LoggerFactory.getLogger(getClass()); this.repo = new Repo(resourceResolver); - this.acl = new Acl(resourceResolver); + this.acl = new Acl(resourceResolver, repo::isAutoCommit); this.activator = new Activator(resourceResolver, osgiContext); this.formatter = new Formatter(); this.notifier = osgiContext.getService(NotificationManager.class); From a8a7aca2e50a0e7cbfc43ec5351ced0dbf4b635f Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Tue, 28 Jul 2026 09:51:43 +0200 Subject: [PATCH 2/6] Repo batch --- .../main/java/dev/vml/es/acm/core/repo/Repo.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/core/src/main/java/dev/vml/es/acm/core/repo/Repo.java b/core/src/main/java/dev/vml/es/acm/core/repo/Repo.java index d5ee350d9..15ffbb7f6 100644 --- a/core/src/main/java/dev/vml/es/acm/core/repo/Repo.java +++ b/core/src/main/java/dev/vml/es/acm/core/repo/Repo.java @@ -130,6 +130,21 @@ public void dryRun(boolean enabled, Runnable operation) { } } + public void batch(Runnable operation) { + boolean autoCommitInitial = this.autoCommit; + if (autoCommitInitial) { + this.autoCommit = false; + getLogger().info("Batch started. Changes will be committed once at the end."); + } + try { + operation.run(); + commit(); + getLogger().info("Batch completed. Changes committed."); + } finally { + this.autoCommit = autoCommitInitial; + } + } + public void quiet(Runnable operation) { quiet(true, operation); } From e4e2a4724bd930ea218d3fdafbb2f057b461e677 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Tue, 28 Jul 2026 10:48:38 +0200 Subject: [PATCH 3/6] Commit policy --- .../java/dev/vml/es/acm/core/acl/Acl.java | 9 ++--- .../dev/vml/es/acm/core/acl/AclContext.java | 10 +++--- .../core/acl/utils/AuthorizableManager.java | 34 +++++++++---------- .../core/acl/utils/PermissionsManager.java | 20 +++++------ .../dev/vml/es/acm/core/code/CodeContext.java | 2 +- .../vml/es/acm/core/repo/CommitPolicy.java | 8 +++++ .../java/dev/vml/es/acm/core/repo/Locker.java | 19 +++++------ .../java/dev/vml/es/acm/core/repo/Repo.java | 4 +-- 8 files changed, 56 insertions(+), 50 deletions(-) create mode 100644 core/src/main/java/dev/vml/es/acm/core/repo/CommitPolicy.java diff --git a/core/src/main/java/dev/vml/es/acm/core/acl/Acl.java b/core/src/main/java/dev/vml/es/acm/core/acl/Acl.java index 301f801ca..9eb282581 100644 --- a/core/src/main/java/dev/vml/es/acm/core/acl/Acl.java +++ b/core/src/main/java/dev/vml/es/acm/core/acl/Acl.java @@ -3,9 +3,10 @@ import dev.vml.es.acm.core.acl.authorizable.AclAuthorizable; import dev.vml.es.acm.core.acl.authorizable.AclGroup; import dev.vml.es.acm.core.acl.authorizable.AclUser; +import dev.vml.es.acm.core.repo.CommitPolicy; +import dev.vml.es.acm.core.repo.Repo; import dev.vml.es.acm.core.util.GroovyUtils; import groovy.lang.Closure; -import java.util.function.Supplier; import org.apache.jackrabbit.api.security.user.Authorizable; import org.apache.jackrabbit.api.security.user.Group; import org.apache.jackrabbit.api.security.user.User; @@ -18,11 +19,11 @@ public class Acl { private final AclChecker checker; public Acl(ResourceResolver resourceResolver) { - this(resourceResolver, () -> true); + this(resourceResolver, new Repo(resourceResolver)); } - public Acl(ResourceResolver resourceResolver, Supplier autoCommit) { - this.context = new AclContext(resourceResolver, autoCommit); + public Acl(ResourceResolver resourceResolver, CommitPolicy commitPolicy) { + this.context = new AclContext(resourceResolver, commitPolicy); this.checker = new AclChecker(context); } diff --git a/core/src/main/java/dev/vml/es/acm/core/acl/AclContext.java b/core/src/main/java/dev/vml/es/acm/core/acl/AclContext.java index 36ec6112a..558b7ff11 100644 --- a/core/src/main/java/dev/vml/es/acm/core/acl/AclContext.java +++ b/core/src/main/java/dev/vml/es/acm/core/acl/AclContext.java @@ -5,9 +5,9 @@ import dev.vml.es.acm.core.acl.authorizable.AclUser; import dev.vml.es.acm.core.acl.utils.AuthorizableManager; import dev.vml.es.acm.core.acl.utils.PermissionsManager; +import dev.vml.es.acm.core.repo.CommitPolicy; import dev.vml.es.acm.core.repo.Repo; import java.util.Optional; -import java.util.function.Supplier; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.ValueFactory; @@ -32,10 +32,10 @@ public class AclContext { private final PermissionsManager permissionsManager; public AclContext(ResourceResolver resourceResolver) { - this(resourceResolver, () -> true); + this(resourceResolver, new Repo(resourceResolver)); } - public AclContext(ResourceResolver resourceResolver, Supplier autoCommit) { + public AclContext(ResourceResolver resourceResolver, CommitPolicy commitPolicy) { try { this.logger = LoggerFactory.getLogger(AclContext.class); JackrabbitSession session = (JackrabbitSession) resourceResolver.adaptTo(Session.class); @@ -43,8 +43,8 @@ public AclContext(ResourceResolver resourceResolver, Supplier autoCommi AccessControlManager accessControlManager = session.getAccessControlManager(); ValueFactory valueFactory = session.getValueFactory(); this.resourceResolver = resourceResolver; - this.authorizableManager = new AuthorizableManager(session, userManager, valueFactory, autoCommit); - this.permissionsManager = new PermissionsManager(session, accessControlManager, valueFactory, autoCommit); + this.authorizableManager = new AuthorizableManager(session, userManager, valueFactory, commitPolicy); + this.permissionsManager = new PermissionsManager(session, accessControlManager, valueFactory, commitPolicy); } catch (RepositoryException e) { throw new AclException("Cannot access repository while obtaining ACL context!", e); } diff --git a/core/src/main/java/dev/vml/es/acm/core/acl/utils/AuthorizableManager.java b/core/src/main/java/dev/vml/es/acm/core/acl/utils/AuthorizableManager.java index 3d3acb1ea..77dc804bb 100644 --- a/core/src/main/java/dev/vml/es/acm/core/acl/utils/AuthorizableManager.java +++ b/core/src/main/java/dev/vml/es/acm/core/acl/utils/AuthorizableManager.java @@ -1,11 +1,11 @@ package dev.vml.es.acm.core.acl.utils; import dev.vml.es.acm.core.acl.AclException; +import dev.vml.es.acm.core.repo.CommitPolicy; import java.security.Principal; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.function.Supplier; import javax.jcr.Credentials; import javax.jcr.LoginException; import javax.jcr.Repository; @@ -28,17 +28,17 @@ public class AuthorizableManager { private final ValueFactory valueFactory; - private final Supplier autoCommit; + private final CommitPolicy commitPolicy; public AuthorizableManager( JackrabbitSession session, UserManager userManager, ValueFactory valueFactory, - Supplier autoCommit) { + CommitPolicy commitPolicy) { this.session = session; this.userManager = userManager; this.valueFactory = valueFactory; - this.autoCommit = autoCommit; + this.commitPolicy = commitPolicy; } public User createUser(String id, String password, String path) { @@ -48,7 +48,7 @@ public User createUser(String id, String password, String path) { password = PasswordUtils.generateRandomPassword(); } User user = userManager.createUser(id, password, principal, path); - save(); + save(String.format("creating user '%s'", id)); return user; } catch (RepositoryException e) { throw new AclException(String.format("Failed to create user '%s'", id), e); @@ -62,7 +62,7 @@ public Group createGroup(String id, String path, String externalId) { if (StringUtils.isNotEmpty(externalId)) { group.setProperty("rep:externalId", valueFactory.createValue(externalId)); } - save(); + save(String.format("creating group '%s'", id)); return group; } catch (RepositoryException e) { throw new AclException(String.format("Failed to create group '%s'", id), e); @@ -72,7 +72,7 @@ public Group createGroup(String id, String path, String externalId) { public User createSystemUser(String id, String path) { try { User user = userManager.createSystemUser(id, path); - save(); + save(String.format("creating system user '%s'", id)); return user; } catch (RepositoryException e) { throw new AclException(String.format("Failed to create system user '%s'", id), e); @@ -95,7 +95,7 @@ public void deleteAuthorizable(Authorizable authorizable) { try { id = authorizable.getID(); authorizable.remove(); - save(); + save(String.format("deleting authorizable '%s'", id)); } catch (RepositoryException e) { throw new AclException(String.format("Failed to delete authorizable '%s'", id), e); } @@ -112,7 +112,7 @@ public boolean addMember(Group group, Authorizable member) { result = group.addMember(member); } if (result) { - save(); + save(String.format("adding member '%s' to group '%s'", memberId, groupId)); } return result; } catch (RepositoryException e) { @@ -131,11 +131,11 @@ public boolean removeMember(Group group, Authorizable member) { result = group.removeMember(member); } if (result) { - save(); + save(String.format("removing member '%s' from group '%s'", memberId, groupId)); } return result; } catch (RepositoryException e) { - throw new AclException(String.format("Failed to remove member '%s' to group '%s'", memberId, groupId), e); + throw new AclException(String.format("Failed to remove member '%s' from group '%s'", memberId, groupId), e); } } @@ -185,7 +185,7 @@ public void changePassword(User user, String password) { try { userId = user.getID(); user.changePassword(password); - save(); + save(String.format("changing password for user '%s'", userId)); } catch (RepositoryException e) { throw new AclException(String.format("Failed to set password for user '%s'", userId), e); } @@ -214,7 +214,7 @@ public void setProperty(Authorizable authorizable, String relPath, String value) try { id = authorizable.getID(); authorizable.setProperty(relPath, valueFactory.createValue(value)); - save(); + save(String.format("setting property '%s' for authorizable '%s'", relPath, id)); } catch (RepositoryException e) { throw new AclException(String.format("Failed to set property '%s' for authorizable '%s'", relPath, id), e); } @@ -226,7 +226,7 @@ public boolean removeProperty(Authorizable authorizable, String relPath) { id = authorizable.getID(); if (authorizable.hasProperty(relPath)) { authorizable.removeProperty(relPath); - save(); + save(String.format("removing property '%s' for authorizable '%s'", relPath, id)); return true; } return false; @@ -236,9 +236,7 @@ public boolean removeProperty(Authorizable authorizable, String relPath) { } } - private void save() throws RepositoryException { - if (autoCommit.get()) { - session.save(); - } + private void save(String context) { + commitPolicy.commit(context); } } diff --git a/core/src/main/java/dev/vml/es/acm/core/acl/utils/PermissionsManager.java b/core/src/main/java/dev/vml/es/acm/core/acl/utils/PermissionsManager.java index cd1ed0dcf..3053f279b 100644 --- a/core/src/main/java/dev/vml/es/acm/core/acl/utils/PermissionsManager.java +++ b/core/src/main/java/dev/vml/es/acm/core/acl/utils/PermissionsManager.java @@ -1,6 +1,7 @@ package dev.vml.es.acm.core.acl.utils; import dev.vml.es.acm.core.acl.AclException; +import dev.vml.es.acm.core.repo.CommitPolicy; import java.security.Principal; import java.util.Arrays; import java.util.Collection; @@ -11,7 +12,6 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.function.Supplier; import java.util.stream.Collectors; import javax.jcr.Node; import javax.jcr.NodeIterator; @@ -47,23 +47,21 @@ public class PermissionsManager { private final ValueFactory valueFactory; - private final Supplier autoCommit; + private final CommitPolicy commitPolicy; public PermissionsManager( JackrabbitSession session, AccessControlManager accessControlManager, ValueFactory valueFactory, - Supplier autoCommit) { + CommitPolicy commitPolicy) { this.session = session; this.accessControlManager = accessControlManager; this.valueFactory = valueFactory; - this.autoCommit = autoCommit; + this.commitPolicy = commitPolicy; } - private void save() throws RepositoryException { - if (autoCommit.get()) { - session.save(); - } + private void save(String context) { + commitPolicy.commit(context); } public void apply( @@ -84,7 +82,9 @@ public void apply( updateAccessControlList( authorizable.getPrincipal(), path, modifyPermissions, modifyRestrictions, allow); } - save(); + save(String.format( + "applying permissions for authorizable '%s' at path '%s' with permissions '%s' and restrictions '%s'", + id, path, permissions, restrictions)); } catch (RepositoryException e) { throw new AclException( String.format("Failed to apply permissions for authorizable '%s' at path '%s'", id, path), e); @@ -228,7 +228,7 @@ private boolean removeAll(Authorizable authorizable, String path) { } if (result) { accessControlManager.setPolicy(path, jackrabbitAcl); - save(); + save(String.format("removing all permissions for authorizable '%s' at path '%s'", id, path)); } return result; } catch (RepositoryException e) { diff --git a/core/src/main/java/dev/vml/es/acm/core/code/CodeContext.java b/core/src/main/java/dev/vml/es/acm/core/code/CodeContext.java index 93bd87aeb..f0212f3b5 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/CodeContext.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/CodeContext.java @@ -46,7 +46,7 @@ public CodeContext(OsgiContext osgiContext, ResourceResolver resourceResolver) { this.log = LoggerFactory.getLogger(getClass()); this.repo = new Repo(resourceResolver); - this.acl = new Acl(resourceResolver, repo::isAutoCommit); + this.acl = new Acl(resourceResolver, repo); this.activator = new Activator(resourceResolver, osgiContext); this.formatter = new Formatter(); this.notifier = osgiContext.getService(NotificationManager.class); diff --git a/core/src/main/java/dev/vml/es/acm/core/repo/CommitPolicy.java b/core/src/main/java/dev/vml/es/acm/core/repo/CommitPolicy.java new file mode 100644 index 000000000..5f505924a --- /dev/null +++ b/core/src/main/java/dev/vml/es/acm/core/repo/CommitPolicy.java @@ -0,0 +1,8 @@ +package dev.vml.es.acm.core.repo; + +public interface CommitPolicy { + + boolean isAutoCommit(); + + void commit(String context); +} diff --git a/core/src/main/java/dev/vml/es/acm/core/repo/Locker.java b/core/src/main/java/dev/vml/es/acm/core/repo/Locker.java index 67a9b15af..10a976cad 100644 --- a/core/src/main/java/dev/vml/es/acm/core/repo/Locker.java +++ b/core/src/main/java/dev/vml/es/acm/core/repo/Locker.java @@ -7,7 +7,6 @@ import java.util.Calendar; import java.util.HashMap; import java.util.Map; -import java.util.function.Supplier; import java.util.stream.Stream; import org.apache.commons.lang3.StringUtils; import org.apache.jackrabbit.JcrConstants; @@ -34,15 +33,15 @@ public class Locker { private final ResourceResolver resolver; - private final Supplier autoCommit; + private final CommitPolicy commitPolicy; public Locker(ResourceResolver resolver) { - this(resolver, () -> true); + this(resolver, new Repo(resolver)); } - public Locker(ResourceResolver resolver, Supplier autoCommit) { + public Locker(ResourceResolver resolver, CommitPolicy commitPolicy) { this.resolver = resolver; - this.autoCommit = autoCommit; + this.commitPolicy = commitPolicy; } public boolean isLocked(String lockName) { @@ -106,13 +105,13 @@ public void lock(String lockName, Duration ttl) { props.put(LOCKED_UNTIL_PROP, lockedUntil); } resolver.create(dirResource, nodeName, props); - if (autoCommit.get()) { + if (commitPolicy.isAutoCommit()) { resolver.commit(); } LOG.debug("Created lock '{}'", name); return; } catch (PersistenceException e) { - if (autoCommit.get()) { + if (commitPolicy.isAutoCommit()) { resolver.revert(); resolver.refresh(); exceptionLast = e; @@ -142,13 +141,13 @@ public void unlock(String lockName) { return; } resolver.delete(lockCurrent); - if (autoCommit.get()) { + if (commitPolicy.isAutoCommit()) { resolver.commit(); } LOG.debug("Deleted lock '{}'", name); return; } catch (PersistenceException e) { - if (autoCommit.get()) { + if (commitPolicy.isAutoCommit()) { resolver.revert(); resolver.refresh(); exceptionLast = e; @@ -169,7 +168,7 @@ public void unlockAll() { } try { resolver.delete(root); - if (autoCommit.get()) { + if (commitPolicy.isAutoCommit()) { resolver.commit(); } LOG.debug("Deleted all locks"); diff --git a/core/src/main/java/dev/vml/es/acm/core/repo/Repo.java b/core/src/main/java/dev/vml/es/acm/core/repo/Repo.java index 15ffbb7f6..7d86d4d82 100644 --- a/core/src/main/java/dev/vml/es/acm/core/repo/Repo.java +++ b/core/src/main/java/dev/vml/es/acm/core/repo/Repo.java @@ -15,7 +15,7 @@ import org.slf4j.LoggerFactory; import org.slf4j.helpers.NOPLogger; -public class Repo { +public class Repo implements CommitPolicy { private static final Logger LOG = LoggerFactory.getLogger(Repo.class); @@ -32,7 +32,7 @@ public class Repo { public Repo(ResourceResolver resourceResolver) { this.resourceResolver = resourceResolver; this.session = resourceResolver.adaptTo(Session.class); - this.locker = new Locker(resourceResolver, this::isAutoCommit); + this.locker = new Locker(resourceResolver, this); } public static Repo quiet(ResourceResolver resourceResolver) { From 486581e4e4c12ceedf1aa7636514c89dd92c8510 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Tue, 28 Jul 2026 11:00:25 +0200 Subject: [PATCH 4/6] Commit policy factory --- .../java/dev/vml/es/acm/core/acl/Acl.java | 3 +-- .../dev/vml/es/acm/core/acl/AclContext.java | 2 +- .../vml/es/acm/core/repo/CommitPolicy.java | 25 +++++++++++++++++++ .../java/dev/vml/es/acm/core/repo/Locker.java | 2 +- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/dev/vml/es/acm/core/acl/Acl.java b/core/src/main/java/dev/vml/es/acm/core/acl/Acl.java index 9eb282581..bc7668932 100644 --- a/core/src/main/java/dev/vml/es/acm/core/acl/Acl.java +++ b/core/src/main/java/dev/vml/es/acm/core/acl/Acl.java @@ -4,7 +4,6 @@ import dev.vml.es.acm.core.acl.authorizable.AclGroup; import dev.vml.es.acm.core.acl.authorizable.AclUser; import dev.vml.es.acm.core.repo.CommitPolicy; -import dev.vml.es.acm.core.repo.Repo; import dev.vml.es.acm.core.util.GroovyUtils; import groovy.lang.Closure; import org.apache.jackrabbit.api.security.user.Authorizable; @@ -19,7 +18,7 @@ public class Acl { private final AclChecker checker; public Acl(ResourceResolver resourceResolver) { - this(resourceResolver, new Repo(resourceResolver)); + this(resourceResolver, CommitPolicy.of(resourceResolver, true)); } public Acl(ResourceResolver resourceResolver, CommitPolicy commitPolicy) { diff --git a/core/src/main/java/dev/vml/es/acm/core/acl/AclContext.java b/core/src/main/java/dev/vml/es/acm/core/acl/AclContext.java index 558b7ff11..7f5259933 100644 --- a/core/src/main/java/dev/vml/es/acm/core/acl/AclContext.java +++ b/core/src/main/java/dev/vml/es/acm/core/acl/AclContext.java @@ -32,7 +32,7 @@ public class AclContext { private final PermissionsManager permissionsManager; public AclContext(ResourceResolver resourceResolver) { - this(resourceResolver, new Repo(resourceResolver)); + this(resourceResolver, CommitPolicy.of(resourceResolver, true)); } public AclContext(ResourceResolver resourceResolver, CommitPolicy commitPolicy) { diff --git a/core/src/main/java/dev/vml/es/acm/core/repo/CommitPolicy.java b/core/src/main/java/dev/vml/es/acm/core/repo/CommitPolicy.java index 5f505924a..79c3eb847 100644 --- a/core/src/main/java/dev/vml/es/acm/core/repo/CommitPolicy.java +++ b/core/src/main/java/dev/vml/es/acm/core/repo/CommitPolicy.java @@ -1,8 +1,33 @@ package dev.vml.es.acm.core.repo; +import org.apache.sling.api.resource.PersistenceException; +import org.apache.sling.api.resource.ResourceResolver; + public interface CommitPolicy { boolean isAutoCommit(); void commit(String context); + + static CommitPolicy of(ResourceResolver resourceResolver, boolean autoCommit) { + return new CommitPolicy() { + @Override + public boolean isAutoCommit() { + return autoCommit; + } + + @Override + public void commit(String context) { + if (!autoCommit) { + return; + } + try { + resourceResolver.commit(); + } catch (PersistenceException e) { + throw new RepoException( + String.format("Cannot commit changes to repository while %s!", context), e); + } + } + }; + } } diff --git a/core/src/main/java/dev/vml/es/acm/core/repo/Locker.java b/core/src/main/java/dev/vml/es/acm/core/repo/Locker.java index 10a976cad..fc1948608 100644 --- a/core/src/main/java/dev/vml/es/acm/core/repo/Locker.java +++ b/core/src/main/java/dev/vml/es/acm/core/repo/Locker.java @@ -36,7 +36,7 @@ public class Locker { private final CommitPolicy commitPolicy; public Locker(ResourceResolver resolver) { - this(resolver, new Repo(resolver)); + this(resolver, CommitPolicy.of(resolver, true)); } public Locker(ResourceResolver resolver, CommitPolicy commitPolicy) { From 23c78980d3e7ca7fd534f42bbb185ed9f3db1834 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Tue, 28 Jul 2026 11:22:18 +0200 Subject: [PATCH 5/6] Batch guard --- core/src/main/java/dev/vml/es/acm/core/repo/Repo.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/dev/vml/es/acm/core/repo/Repo.java b/core/src/main/java/dev/vml/es/acm/core/repo/Repo.java index 7d86d4d82..2f00c4dc1 100644 --- a/core/src/main/java/dev/vml/es/acm/core/repo/Repo.java +++ b/core/src/main/java/dev/vml/es/acm/core/repo/Repo.java @@ -131,17 +131,17 @@ public void dryRun(boolean enabled, Runnable operation) { } public void batch(Runnable operation) { - boolean autoCommitInitial = this.autoCommit; - if (autoCommitInitial) { - this.autoCommit = false; - getLogger().info("Batch started. Changes will be committed once at the end."); + if (!autoCommit) { + throw new RepoException("Cannot start a batch: already inside a batch or dry run scope."); } + this.autoCommit = false; + getLogger().info("Batch started. Changes will be committed once at the end."); try { operation.run(); commit(); getLogger().info("Batch completed. Changes committed."); } finally { - this.autoCommit = autoCommitInitial; + this.autoCommit = true; } } From 98c483bff4d6f79585c981cf5bc33bd0be4bef8e Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Tue, 28 Jul 2026 11:26:58 +0200 Subject: [PATCH 6/6] Docs --- .../snippet/available/core/repo/batch.yml | 30 +++++++++++++++++++ .../snippet/available/core/repo/dry_run.yml | 4 +++ 2 files changed, 34 insertions(+) create mode 100644 ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/repo/batch.yml diff --git a/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/repo/batch.yml b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/repo/batch.yml new file mode 100644 index 000000000..3f1b19d9d --- /dev/null +++ b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/repo/batch.yml @@ -0,0 +1,30 @@ +group: Repo +name: repo_batch +content: | + repo.batch { + // operations + } +documentation: | + Group multiple repository operations into a single commit. + + By default every operation commits on its own (`autoCommit` is enabled). + Within a batch, auto-committing is turned off and all changes are committed once, at the end of the block. + This is useful for bulk changes, where a single save is more efficient than many individual commits. + + Batches cannot be nested and cannot be used inside a dry run - doing so throws an exception, + so that the "commit at the end" guarantee always holds. + + The `autoCommit` control applies only to operations performed via the `repo` and `acl` services. + Since everything shares the same session, calling vanilla Sling/AEM/JCR APIs directly (e.g. `session.save()`) + may trigger an accidental commit of the pending changes and break the "commit at the end" guarantee. + + For example: + ```groovy + void doRun() { + repo.batch { + acl.createUser("alice.doe") + acl.allow(userId: "alice.doe", path: "/content/acme", permissions: ["jcr:read"]) + repo.get("/content/acme").ensureRegularFolder() + } + } + ``` diff --git a/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/repo/dry_run.yml b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/repo/dry_run.yml index ad1c10b7b..b22f5d7f6 100644 --- a/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/repo/dry_run.yml +++ b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/repo/dry_run.yml @@ -11,6 +11,10 @@ documentation: | When exception is thrown during dry run, the revert operation is performed, so no changes are made to the repository. Useful especially when combined with `inputs.bool("dryRun")` to allow user to choose whether to perform dry run or not. + + The `autoCommit` control applies only to operations performed via the `repo` and `acl` services. + Since everything shares the same session, calling vanilla Sling/AEM/JCR APIs directly (e.g. `session.save()`) + may trigger an accidental commit of the pending changes and defeat the dry run. For example: ```groovy