From 630fdddcf3e784b6a7243de9b83b86fa7677e0ed Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Wed, 29 Oct 2025 10:16:24 +0100 Subject: [PATCH 1/5] Next schedule info --- .../java/dev/vml/es/acm/core/acl/Acl.java | 21 +++++-- .../acl/authorizable/AclAuthorizable.java | 62 ++++++++++++++----- .../acm/core/acl/authorizable/AclGroup.java | 14 +++-- .../es/acm/core/script/ScriptSchedule.java | 30 +++++++++ .../es/acm/core/script/ScriptScheduler.java | 23 ++++++- .../es/acm/core/servlet/ScriptServlet.java | 14 ++++- .../core/servlet/output/ScriptListOutput.java | 10 ++- ui.frontend/src/components/DateExplained.tsx | 6 +- .../src/components/ScriptAutomaticList.tsx | 22 +++++-- ui.frontend/src/types/main.ts | 3 +- ui.frontend/src/types/script.ts | 5 ++ 11 files changed, 171 insertions(+), 39 deletions(-) create mode 100644 core/src/main/java/dev/vml/es/acm/core/script/ScriptSchedule.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 f3030e60..ca4019dc 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 @@ -204,12 +204,12 @@ private void deleteAuthorizable(AclAuthorizable authorizable, String id) { if (authorizable == null) { context.getLogger().info("Skipped deleting authorizable '{}' (already deleted or never existed)", id); return; - } + } if (context.getAuthorizableManager().getAuthorizable(id) == null) { context.getLogger().info("Skipped deleting authorizable '{}' (already deleted)", id); return; } - + purge(authorizable); context.getAuthorizableManager().deleteAuthorizable(authorizable.get()); } @@ -259,7 +259,10 @@ public void addToGroup(GroupOptions options) { String authorizableId = context.determineId(options.getAuthorizable(), options.getAuthorizableId()); String groupId = context.determineId(options.getGroup(), options.getGroupId()); context.getLogger() - .info("Skipped adding authorizable '{}' to group '{}' (authorizable not found)", authorizableId, groupId); + .info( + "Skipped adding authorizable '{}' to group '{}' (authorizable not found)", + authorizableId, + groupId); return; } authorizable.addToGroup(options); @@ -300,7 +303,10 @@ public void removeFromGroup(GroupOptions options) { String authorizableId = context.determineId(options.getAuthorizable(), options.getAuthorizableId()); String groupId = context.determineId(options.getGroup(), options.getGroupId()); context.getLogger() - .info("Skipped removing authorizable '{}' from group '{}' (authorizable not found)", authorizableId, groupId); + .info( + "Skipped removing authorizable '{}' from group '{}' (authorizable not found)", + authorizableId, + groupId); return; } authorizable.removeFromGroup(options); @@ -340,7 +346,9 @@ public void removeFromAllGroups(AuthorizableOptions options) { if (authorizable == null) { String authorizableId = context.determineId(options.getAuthorizable(), options.getAuthorizableId()); context.getLogger() - .info("Skipped removing authorizable '{}' from all groups (authorizable not found)", authorizableId); + .info( + "Skipped removing authorizable '{}' from all groups (authorizable not found)", + authorizableId); return; } authorizable.removeFromAllGroups(); @@ -402,7 +410,8 @@ public void removeMember(MemberOptions options) { if (group == null) { String memberId = context.determineId(options.getMember(), options.getMemberId()); String groupId = context.determineId(options.getGroup(), options.getGroupId()); - context.getLogger().info("Skipped removing member '{}' from group '{}' (group not found)", memberId, groupId); + context.getLogger() + .info("Skipped removing member '{}' from group '{}' (group not found)", memberId, groupId); return; } group.removeMember(options); diff --git a/core/src/main/java/dev/vml/es/acm/core/acl/authorizable/AclAuthorizable.java b/core/src/main/java/dev/vml/es/acm/core/acl/authorizable/AclAuthorizable.java index ba2744f9..bac91753 100644 --- a/core/src/main/java/dev/vml/es/acm/core/acl/authorizable/AclAuthorizable.java +++ b/core/src/main/java/dev/vml/es/acm/core/acl/authorizable/AclAuthorizable.java @@ -63,12 +63,12 @@ public void removeProperty(Closure closure) { public void addToGroup(GroupOptions options) { AclGroup group = context.determineGroup(options.getGroup(), options.getGroupId()); String groupId = context.determineId(options.getGroup(), options.getGroupId()); - + if (group == null) { context.getLogger().info("Skipped adding authorizable '{}' to group '{}' (group not found)", id, groupId); return; } - + boolean changed = context.getAuthorizableManager().addMember(group.get(), authorizable); if (changed) { context.getLogger().info("Added authorizable '{}' to group '{}'", id, groupId); @@ -92,12 +92,13 @@ public void addToGroup(AclGroup group) { public void removeFromGroup(GroupOptions options) { AclGroup group = context.determineGroup(options.getGroup(), options.getGroupId()); String groupId = context.determineId(options.getGroup(), options.getGroupId()); - + if (group == null) { - context.getLogger().info("Skipped removing authorizable '{}' from group '{}' (group not found)", id, groupId); + context.getLogger() + .info("Skipped removing authorizable '{}' from group '{}' (group not found)", id, groupId); return; } - + boolean changed = context.getAuthorizableManager().removeMember(group.get(), authorizable); if (changed) { context.getLogger().info("Removed authorizable '{}' from group '{}'", id, groupId); @@ -132,7 +133,8 @@ public void removeFromAllGroups() { if (anyChanged) { context.getLogger().info("Removed authorizable '{}' from all groups", id); } else { - context.getLogger().info("Skipped removing authorizable '{}' from all groups (not a member of any group)", id); + context.getLogger() + .info("Skipped removing authorizable '{}' from all groups (not a member of any group)", id); } } catch (RepositoryException e) { throw new AclException(String.format("Cannot remove authorizable '%s' from all groups!", id), e); @@ -145,18 +147,27 @@ public void clear(ClearOptions options) { public void clear(String path, boolean strict) { if (context.isCompositeNodeStore() && isAppsOrLibsPath(path)) { - context.getLogger().info("Skipped clearing permissions for authorizable '{}' at path '{}' (composite node store)", id, path); + context.getLogger() + .info( + "Skipped clearing permissions for authorizable '{}' at path '{}' (composite node store)", + id, + path); return; } if (context.getResourceResolver().getResource(path) == null) { - context.getLogger().info("Skipped clearing permissions for authorizable '{}' at path '{}' (path not found)", id, path); + context.getLogger() + .info("Skipped clearing permissions for authorizable '{}' at path '{}' (path not found)", id, path); return; } boolean changed = context.getPermissionsManager().clear(authorizable, path, strict); if (changed) { context.getLogger().info("Cleared permissions for authorizable '{}' at path '{}'", id, path); } else { - context.getLogger().info("Skipped clearing permissions for authorizable '{}' at path '{}' (no permissions to clear)", id, path); + context.getLogger() + .info( + "Skipped clearing permissions for authorizable '{}' at path '{}' (no permissions to clear)", + id, + path); } } @@ -173,25 +184,41 @@ private void apply(PermissionsOptions options, boolean allow) { List permissions = options.determineAllPermissions(); Map restrictions = options.determineAllRestrictions(); PermissionsOptions.Mode mode = options.getMode(); - + if (context.isCompositeNodeStore() && isAppsOrLibsPath(path)) { String actionDescription = allow ? "allow permissions" : "deny permissions"; - context.getLogger().info("Skipped setting {} for authorizable '{}' at path '{}' (composite node store)", actionDescription, id, path); + context.getLogger() + .info( + "Skipped setting {} for authorizable '{}' at path '{}' (composite node store)", + actionDescription, + id, + path); return; } - + if (context.getResourceResolver().getResource(path) == null) { if (mode == PermissionsOptions.Mode.FAIL) { - throw new AclException(String.format("Cannot apply permissions for authorizable '%s' at path '%s'! (path not found)", id, path)); + throw new AclException(String.format( + "Cannot apply permissions for authorizable '%s' at path '%s'! (path not found)", id, path)); } String actionDescription = allow ? "allow permissions" : "deny permissions"; - context.getLogger().info("Skipped setting {} for authorizable '{}' at path '{}' (path not found)", actionDescription, id, path); + context.getLogger() + .info( + "Skipped setting {} for authorizable '{}' at path '{}' (path not found)", + actionDescription, + id, + path); return; } - + if (context.getPermissionsManager().check(authorizable, path, permissions, restrictions, allow)) { String actionDescription = allow ? "allow permissions" : "deny permissions"; - context.getLogger().info("Skipped setting {} for authorizable '{}' at path '{}' (already set)", actionDescription, id, path); + context.getLogger() + .info( + "Skipped setting {} for authorizable '{}' at path '{}' (already set)", + actionDescription, + id, + path); } else { context.getPermissionsManager().apply(authorizable, path, permissions, restrictions, allow); String actionDescription = allow ? "allow permissions" : "deny permissions"; @@ -230,7 +257,8 @@ public void removeProperty(String relPath) { if (changed) { context.getLogger().info("Removed property '{}' for authorizable '{}'", relPath, id); } else { - context.getLogger().info("Skipped removing property '{}' for authorizable '{}' (property not set)", relPath, id); + context.getLogger() + .info("Skipped removing property '{}' for authorizable '{}' (property not set)", relPath, id); } } diff --git a/core/src/main/java/dev/vml/es/acm/core/acl/authorizable/AclGroup.java b/core/src/main/java/dev/vml/es/acm/core/acl/authorizable/AclGroup.java index 04c55546..3d46de98 100644 --- a/core/src/main/java/dev/vml/es/acm/core/acl/authorizable/AclGroup.java +++ b/core/src/main/java/dev/vml/es/acm/core/acl/authorizable/AclGroup.java @@ -36,12 +36,12 @@ public void removeMember(Closure closure) { public void addMember(MemberOptions options) { AclAuthorizable member = context.determineAuthorizable(options.getMember(), options.getMemberId()); String memberId = context.determineId(options.getMember(), options.getMemberId()); - + if (member == null) { context.getLogger().info("Skipped adding member '{}' to group '{}' (member not found)", memberId, getId()); return; } - + boolean changed = context.getAuthorizableManager().addMember(group, member.get()); if (changed) { context.getLogger().info("Added member '{}' to group '{}'", memberId, getId()); @@ -65,12 +65,13 @@ public void addMember(AclAuthorizable member) { public void removeMember(MemberOptions options) { AclAuthorizable member = context.determineAuthorizable(options.getMember(), options.getMemberId()); String memberId = context.determineId(options.getMember(), options.getMemberId()); - + if (member == null) { - context.getLogger().info("Skipped removing member '{}' from group '{}' (member not found)", memberId, getId()); + context.getLogger() + .info("Skipped removing member '{}' from group '{}' (member not found)", memberId, getId()); return; } - + boolean changed = context.getAuthorizableManager().removeMember(group, member.get()); if (changed) { context.getLogger().info("Removed member '{}' from group '{}'", memberId, getId()); @@ -102,7 +103,8 @@ public void removeAllMembers() { if (anyChanged) { context.getLogger().info("Removed all members from group '{}'", getId()); } else { - context.getLogger().info("Skipped removing all members from group '{}' (no members to remove)", getId()); + context.getLogger() + .info("Skipped removing all members from group '{}' (no members to remove)", getId()); } } catch (RepositoryException e) { throw new AclException(String.format("Cannot remove all members from group '%s'!", getId()), e); diff --git a/core/src/main/java/dev/vml/es/acm/core/script/ScriptSchedule.java b/core/src/main/java/dev/vml/es/acm/core/script/ScriptSchedule.java new file mode 100644 index 00000000..249d5638 --- /dev/null +++ b/core/src/main/java/dev/vml/es/acm/core/script/ScriptSchedule.java @@ -0,0 +1,30 @@ +package dev.vml.es.acm.core.script; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import org.apache.sling.event.jobs.JobManager; +import org.apache.sling.event.jobs.ScheduledJobInfo; + +public class ScriptSchedule implements Serializable { + + private final String path; + + private final Date nextExecution; + + public ScriptSchedule(String path, Date nextExecution) { + this.path = path; + this.nextExecution = nextExecution; + } + + public String getPath() { + return path; + } + + public Date getNextExecution() { + return nextExecution; + } +} diff --git a/core/src/main/java/dev/vml/es/acm/core/script/ScriptScheduler.java b/core/src/main/java/dev/vml/es/acm/core/script/ScriptScheduler.java index af176a45..2689f9ea 100644 --- a/core/src/main/java/dev/vml/es/acm/core/script/ScriptScheduler.java +++ b/core/src/main/java/dev/vml/es/acm/core/script/ScriptScheduler.java @@ -43,7 +43,7 @@ import org.slf4j.LoggerFactory; @Component( - service = {ResourceChangeListener.class, EventListener.class, JobConsumer.class}, + service = {ScriptScheduler.class, ResourceChangeListener.class, EventListener.class, JobConsumer.class}, immediate = true, property = { ResourceChangeListener.PATHS + "=glob:" + ScriptRepository.ROOT + "/automatic/**/*.groovy", @@ -458,4 +458,23 @@ private boolean awaitInstanceHealthy(String operation, long retryMaxCount, long } return true; } -} + + public Optional findScriptSchedule(String path) { + Map filterProps = new HashMap<>(); + filterProps.put(ScriptScheduler.JOB_PROP_TYPE, ScriptScheduler.JobType.CRON.name()); + filterProps.put(ScriptScheduler.JOB_PROP_SCRIPT_PATH, path); + + @SuppressWarnings("unchecked") + Collection jobInfos = jobManager.getScheduledJobs(ScriptScheduler.JOB_TOPIC, -1, filterProps); + + if (jobInfos.isEmpty()) { + return Optional.empty(); + } + + ScheduledJobInfo jobInfo = jobInfos.iterator().next(); + Date nextExecution = jobInfo.getNextScheduledExecution(); + + return Optional.of(new ScriptSchedule(path, nextExecution)); + } + +} \ No newline at end of file diff --git a/core/src/main/java/dev/vml/es/acm/core/servlet/ScriptServlet.java b/core/src/main/java/dev/vml/es/acm/core/servlet/ScriptServlet.java index d7aab102..bb4ed383 100644 --- a/core/src/main/java/dev/vml/es/acm/core/servlet/ScriptServlet.java +++ b/core/src/main/java/dev/vml/es/acm/core/servlet/ScriptServlet.java @@ -9,6 +9,8 @@ import dev.vml.es.acm.core.replication.Activator; import dev.vml.es.acm.core.script.Script; import dev.vml.es.acm.core.script.ScriptRepository; +import dev.vml.es.acm.core.script.ScriptSchedule; +import dev.vml.es.acm.core.script.ScriptScheduler; import dev.vml.es.acm.core.script.ScriptStats; import dev.vml.es.acm.core.script.ScriptType; import dev.vml.es.acm.core.servlet.input.ScriptInput; @@ -25,6 +27,7 @@ import org.apache.sling.api.SlingHttpServletResponse; import org.apache.sling.api.servlets.ServletResolverConstants; import org.apache.sling.api.servlets.SlingAllMethodsServlet; +import org.apache.sling.event.jobs.JobManager; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; @@ -70,6 +73,9 @@ public static Optional of(String name) { @Reference private transient SpaSettings spaSettings; + @Reference + private transient ScriptScheduler scriptScheduler; + @Override protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException { long statsLimit = @@ -101,8 +107,14 @@ protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse r .map(Script::getPath) .map(path -> ScriptStats.forCompletedByPath(request.getResourceResolver(), path, statsLimit)) .collect(Collectors.toList()); + List schedules = scripts.stream() + .map(Script::getPath) + .map(path -> scriptScheduler.findScriptSchedule(path)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); - ScriptListOutput output = new ScriptListOutput(scripts, stats); + ScriptListOutput output = new ScriptListOutput(scripts, stats, schedules); respondJson(response, ok("Scripts listed successfully", output)); } catch (Exception e) { LOG.error("Scripts cannot be read!", e); diff --git a/core/src/main/java/dev/vml/es/acm/core/servlet/output/ScriptListOutput.java b/core/src/main/java/dev/vml/es/acm/core/servlet/output/ScriptListOutput.java index 709977a4..c9eedeb9 100644 --- a/core/src/main/java/dev/vml/es/acm/core/servlet/output/ScriptListOutput.java +++ b/core/src/main/java/dev/vml/es/acm/core/servlet/output/ScriptListOutput.java @@ -1,6 +1,7 @@ package dev.vml.es.acm.core.servlet.output; import dev.vml.es.acm.core.script.Script; +import dev.vml.es.acm.core.script.ScriptSchedule; import dev.vml.es.acm.core.script.ScriptStats; import java.io.Serializable; import java.util.List; @@ -11,9 +12,12 @@ public class ScriptListOutput implements Serializable { private final List stats; - public ScriptListOutput(List