From 7f0a14ad5c55e08019ce9d9089b5289209f5d3a7 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Fri, 9 May 2025 09:40:08 +0200 Subject: [PATCH 01/26] Mocks PoC --- .../core/code/script/ContentScriptSyntax.java | 15 +- .../code/script/ExtensionScriptSyntax.java | 15 +- .../aem/acm/core/code/script/MockScript.java | 111 +++++++++++++++ .../core/code/script/MockScriptSyntax.java | 42 ++++++ .../aem/acm/core/code/script/ScriptUtils.java | 18 +++ .../com/vml/es/aem/acm/core/mock/Mock.java | 15 ++ .../es/aem/acm/core/mock/MockException.java | 12 ++ .../vml/es/aem/acm/core/mock/MockFilter.java | 133 ++++++++++++++++++ .../vml/es/aem/acm/core/mock/MockManager.java | 55 ++++++++ .../es/aem/acm/core/mock/MockRepository.java | 55 ++++++++ .../acm/core/mock/MockRequestException.java | 8 ++ .../acm/core/mock/MockResponseException.java | 12 ++ .../es/aem/acm/core/script/ScriptType.java | 1 + .../settings/script/mock/hello-world.groovy | 11 ++ .../settings/script/mock/odd-numbers.groovy | 20 +++ ui.frontend/src/utils/api.types.ts | 1 + 16 files changed, 496 insertions(+), 28 deletions(-) create mode 100644 core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScript.java create mode 100644 core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScriptSyntax.java create mode 100644 core/src/main/java/com/vml/es/aem/acm/core/mock/Mock.java create mode 100644 core/src/main/java/com/vml/es/aem/acm/core/mock/MockException.java create mode 100644 core/src/main/java/com/vml/es/aem/acm/core/mock/MockFilter.java create mode 100644 core/src/main/java/com/vml/es/aem/acm/core/mock/MockManager.java create mode 100644 core/src/main/java/com/vml/es/aem/acm/core/mock/MockRepository.java create mode 100644 core/src/main/java/com/vml/es/aem/acm/core/mock/MockRequestException.java create mode 100644 core/src/main/java/com/vml/es/aem/acm/core/mock/MockResponseException.java create mode 100644 ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/hello-world.groovy create mode 100644 ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/odd-numbers.groovy diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ContentScriptSyntax.java b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ContentScriptSyntax.java index 4dd8c0fe..99949edb 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ContentScriptSyntax.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ContentScriptSyntax.java @@ -19,21 +19,8 @@ public void visit(ASTNode[] nodes, SourceUnit source) { if (source == null) { return; } - this.sourceUnit = source; - - ClassNode mainClass = requireMainClass(source.getAST().getClasses(), MAIN_CLASS); - for (Method methodValue : Method.values()) { - if (methodValue.required || hasMethod(mainClass, methodValue.givenName)) { - if (!isMethodValid(mainClass, methodValue.givenName, methodValue.returnType, 0)) { - addError( - String.format( - "Top-level '%s %s()' method not found or has incorrect signature!", - methodValue.returnType, methodValue.givenName), - mainClass); - } - } - } + ScriptUtils.visit(this, nodes, source, MAIN_CLASS); } enum Method { diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ExtensionScriptSyntax.java b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ExtensionScriptSyntax.java index bdafeabd..6e46900c 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ExtensionScriptSyntax.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ExtensionScriptSyntax.java @@ -19,21 +19,8 @@ public void visit(ASTNode[] nodes, SourceUnit source) { if (source == null) { return; } - this.sourceUnit = source; - - ClassNode mainClass = ScriptUtils.requireMainClass(source.getAST().getClasses(), MAIN_CLASS); - for (Method methodValue : Method.values()) { - if (methodValue.required || hasMethod(mainClass, methodValue.givenName)) { - if (!isMethodValid(mainClass, methodValue.givenName, methodValue.returnType, methodValue.paramCount)) { - addError( - String.format( - "Top-level '%s %s()' method not found or has incorrect signature!", - methodValue.returnType, methodValue.givenName), - mainClass); - } - } - } + ScriptUtils.visit(this, nodes, source, MAIN_CLASS); } enum Method { diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScript.java b/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScript.java new file mode 100644 index 00000000..48cb1eb1 --- /dev/null +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScript.java @@ -0,0 +1,111 @@ +package com.vml.es.aem.acm.core.code.script; + +import com.vml.es.aem.acm.core.AcmException; +import com.vml.es.aem.acm.core.code.ExecutionContext; +import com.vml.es.aem.acm.core.mock.Mock; +import com.vml.es.aem.acm.core.mock.MockRequestException; +import com.vml.es.aem.acm.core.mock.MockResponseException; +import groovy.lang.GroovyShell; +import groovy.lang.Script; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class MockScript implements Mock { + + private static final Logger LOG = org.slf4j.LoggerFactory.getLogger(MockScript.class); + + private final ExecutionContext executionContext; + + private final Script script; + + public MockScript(ExecutionContext executionContext) { + this.executionContext = executionContext; + this.script = parseScript(); + } + + private Script parseScript() { + GroovyShell shell = ScriptUtils.createShell(new MockScriptSyntax()); + Script script = shell.parse( + executionContext.getExecutable().getContent(), + ContentScriptSyntax.MAIN_CLASS, + executionContext.getBinding()); + if (script == null) { + throw new AcmException(String.format( + "Mock script '%s' cannot be parsed!", + executionContext.getExecutable().getId())); + } + return script; + } + + @Override + public String getId() { + return executionContext.getExecutable().getId(); + } + + @Override + public boolean request(HttpServletRequest request) throws MockRequestException { + try { + LOG.info("Mock '{}' is matching request '{} {}'", getId(), request.getMethod(), request.getRequestURI()); + Boolean result = (Boolean) script.invokeMethod("request", new Object[] {request}); + if (BooleanUtils.isTrue(result)) { + LOG.info("Mock '{}' matched request '{} {}'", getId(), request.getMethod(), request.getRequestURI()); + } else { + LOG.info( + "Mock '{}' did not match request '{} {}'", + getId(), + request.getMethod(), + request.getRequestURI()); + } + return result; + } catch (Exception e) { + throw new MockRequestException(String.format("Mock script '%s' cannot match request properly", getId()), e); + } + } + + @Override + public void respond(HttpServletRequest request, HttpServletResponse response) throws MockResponseException { + try { + LOG.info( + "Mock '{}' is responding to request '{} {}'", + getId(), + request.getMethod(), + request.getRequestURI()); + script.invokeMethod("respond", new Object[] {request, response}); + LOG.info("Mock '{}' responded to request '{} {}'", getId(), request.getMethod(), request.getRequestURI()); + } catch (Exception e) { + throw new MockResponseException(String.format("Mock script '%s' cannot respond properly", getId()), e); + } + } + + @Override + public void fail(HttpServletRequest request, HttpServletResponse response, Exception exception) + throws MockResponseException { + try { + LOG.info( + "Mock '{}' is handling failed request '{} {}'", + getId(), + request.getMethod(), + request.getRequestURI()); + script.invokeMethod("fail", new Object[] {request, response, exception}); + LOG.info("Mock '{}' handled failed request '{} {}'", getId(), request.getMethod(), request.getRequestURI()); + } catch (Exception e) { + throw new MockResponseException( + String.format("Mock script '%s' cannot handle failed request properly", getId()), e); + } + } + + public String getDirPath() { + return StringUtils.substringBeforeLast(getId(), "/"); + } + + public String resolvePath(String path) { + if (path.startsWith("/")) { + return path; + } + return getDirPath() + "/" + path; + } +} diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScriptSyntax.java b/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScriptSyntax.java new file mode 100644 index 00000000..d1a7bcbe --- /dev/null +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScriptSyntax.java @@ -0,0 +1,42 @@ +package com.vml.es.aem.acm.core.code.script; + +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.control.CompilePhase; +import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.transform.AbstractASTTransformation; +import org.codehaus.groovy.transform.GroovyASTTransformation; + +import static com.vml.es.aem.acm.core.code.script.ScriptUtils.*; + +@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS) +public class MockScriptSyntax extends AbstractASTTransformation { + + public static final String MAIN_CLASS = "AcmMockScript"; + + @Override + public void visit(ASTNode[] nodes, SourceUnit source) { + if (source == null) { + return; + } + this.sourceUnit = source; + ScriptUtils.visit(this, nodes, source, MAIN_CLASS); + } + + enum Method { + REQUEST("request", "boolean", true), + RESPOND("respond", "void", true); + + final String givenName; + + final String returnType; + + final boolean required; + + Method(String givenName, String returnType, boolean required) { + this.givenName = givenName; + this.returnType = returnType; + this.required = required; + } + } +} diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ScriptUtils.java b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ScriptUtils.java index 8f6a8f45..33442189 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ScriptUtils.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ScriptUtils.java @@ -4,12 +4,15 @@ import groovy.lang.GroovyShell; import java.util.List; import org.codehaus.groovy.GroovyBugError; +import org.codehaus.groovy.ast.ASTNode; import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.ast.MethodNode; import org.codehaus.groovy.control.CompilerConfiguration; +import org.codehaus.groovy.control.SourceUnit; import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer; import org.codehaus.groovy.control.customizers.ImportCustomizer; import org.codehaus.groovy.transform.ASTTransformation; +import org.codehaus.groovy.transform.AbstractASTTransformation; public final class ScriptUtils { @@ -17,6 +20,21 @@ private ScriptUtils() { // intentionally empty } + public static void visit(AbstractASTTransformation transformation, ASTNode[] nodes, SourceUnit source, String mainClassName) { + ClassNode mainClass = requireMainClass(source.getAST().getClasses(), mainClassName); + for (MockScriptSyntax.Method methodValue : MockScriptSyntax.Method.values()) { + if (methodValue.required || hasMethod(mainClass, methodValue.givenName)) { + if (!isMethodValid(mainClass, methodValue.givenName, methodValue.returnType, 0)) { + transformation.addError( + String.format( + "Top-level '%s %s()' method not found or has incorrect signature!", + methodValue.returnType, methodValue.givenName), + mainClass); + } + } + } + } + public static GroovyShell createShell(ASTTransformation codeSyntax) { CompilerConfiguration compiler = new CompilerConfiguration(); compiler.addCompilationCustomizers(new ImportCustomizer()); diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/Mock.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/Mock.java new file mode 100644 index 00000000..19538201 --- /dev/null +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/Mock.java @@ -0,0 +1,15 @@ +package com.vml.es.aem.acm.core.mock; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public interface Mock { + + String getId(); + + boolean request(HttpServletRequest request) throws MockRequestException; + + void respond(HttpServletRequest request, HttpServletResponse response) throws MockResponseException; + + void fail(HttpServletRequest request, HttpServletResponse response, Exception e) throws MockResponseException; +} \ No newline at end of file diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockException.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockException.java new file mode 100644 index 00000000..9e3eb9d7 --- /dev/null +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockException.java @@ -0,0 +1,12 @@ +package com.vml.es.aem.acm.core.mock; + +public class MockException extends Exception { + + public MockException(String message) { + super(message); + } + + public MockException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockFilter.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockFilter.java new file mode 100644 index 00000000..73d0cf06 --- /dev/null +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockFilter.java @@ -0,0 +1,133 @@ +package com.vml.es.aem.acm.core.mock; + +import java.io.IOException; +import java.util.Iterator; +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.vml.es.aem.acm.core.code.Executor; +import com.vml.es.aem.acm.core.util.ResourceUtils; +import org.apache.sling.api.resource.LoginException; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.api.resource.ResourceResolverFactory; +import org.osgi.service.component.annotations.*; +import org.osgi.service.http.whiteboard.HttpWhiteboardConstants; +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.Designate; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Component( + service = Filter.class, + property = { + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT + "=(" + + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME + "=*)" + }, + configurationPolicy = ConfigurationPolicy.REQUIRE) +@Designate(ocd = MockFilter.Config.class) +public class MockFilter implements Filter { + + private static final Logger LOG = LoggerFactory.getLogger(MockFilter.class); + + @Reference + private ResourceResolverFactory resolverFactory; + + @Reference + private MockManager manager; + + private Config config; + + @ObjectClassDefinition(name = "AEM Content Manager - Mock HTTP Filter") + public @interface Config { + + @AttributeDefinition(name = "Enabled") + boolean enabled() default true; + + @AttributeDefinition(name = "Whiteboard Filter Regex") + String[] osgi_http_whiteboard_filter_regex() default { "/mock/.*" }; + } + + @Reference + private Executor executor; + + @Activate + @Modified + protected void activate(Config config) { + this.config = config; + } + + @Override + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) + throws IOException, ServletException { + if (!config.enabled()) { + chain.doFilter(req, res); + return; + } + + HttpServletRequest request = (HttpServletRequest) req; + HttpServletResponse response = (HttpServletResponse) res; + + try (ResourceResolver resolver = ResourceUtils.serviceResolver(resolverFactory, null)) { + MockRepository repository = new MockRepository(manager, resolver); + try { + Iterator it = repository.findStubs().iterator(); + while (it.hasNext()) { + Resource mock = it.next(); + if (!manager.isSpecial(mock)) { + // TODO connect somehow MockScript with executor.createContext(mock.getPath(), ExecutionMode.RUN, mock, resolver); + if (mock.request(request)) { + mock.respond(request, response); + } + return; + } + } + } catch (MockException e) { + LOG.error("Stubs error!", e); + + Mock mock = repository.findSpecialStub(MockManager.FAIL_PATH).orElse(null); + if (mock != null) { + try { + mock.fail(request, response, e); + } catch (MockException e2) { + LOG.error("Stubs fail error!", e2); + response.sendError( + HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Stubs fail error. " + e.getMessage()); + } + } else { + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Stubs error. " + e.getMessage()); + } + return; + } + + Mock mock = repository.findSpecialStub(MockManager.MISSING_PATH).orElse(null); + if (mock != null) { + try { + mock.respond(request, response); + } catch (MockException e) { + LOG.error("Stubs missing error!", e); + response.sendError( + HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Stubs missing error. " + e.getMessage()); + } + } else { + response.sendError(HttpServletResponse.SC_NOT_FOUND, "Mock not found!"); + } + return; + } catch (LoginException e) { + LOG.error("Stubs repository error!", e); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Stubs error. " + e.getMessage()); + } + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + // do nothing + } + + @Override + public void destroy() { + // do nothing + } +} \ No newline at end of file diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockManager.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockManager.java new file mode 100644 index 00000000..84251eb7 --- /dev/null +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockManager.java @@ -0,0 +1,55 @@ +package com.vml.es.aem.acm.core.mock; + +import org.apache.commons.lang3.StringUtils; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Modified; +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +import java.util.Arrays; +import java.util.List; + +@Component(service = MockManager.class, immediate = true) +public class MockManager { + + public static final String INTERNAL_DIR = "internal"; + + public static final String FAIL_PATH = INTERNAL_DIR + "/fail.groovy"; + + public static final String MISSING_PATH = INTERNAL_DIR + "/missing.groovy"; + + public static final List SPECIAL_PATHS = Arrays.asList(FAIL_PATH, MISSING_PATH); + + private Config config; + + @ObjectClassDefinition(name = "AEM Content Manager - Mock Manager") + public @interface Config { + + @AttributeDefinition(name = "Search paths", description = "JCR repository paths to search for mock resources.") + String[] searchPaths() default {"/conf/stubs"}; + + @AttributeDefinition( + name = "Classifier", + description = "Resource name part used to distinguish stubs from other files.") + String classifier() default "stub"; + } + + @Activate + @Modified + protected void activate(Config config) { + this.config = config; + } + + public List getSearchPaths() { + return Arrays.asList(config.searchPaths()); + } + + public String getClassifier() { + return config.classifier(); + } + + public boolean isSpecial(Mock mock) { + return SPECIAL_PATHS.stream().anyMatch(n -> StringUtils.endsWith(mock.getId(), "/" + n)); + } +} diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockRepository.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockRepository.java new file mode 100644 index 00000000..10e446c8 --- /dev/null +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockRepository.java @@ -0,0 +1,55 @@ +package com.vml.es.aem.acm.core.mock; + +import java.util.Optional; +import java.util.stream.Stream; + +import com.vml.es.aem.acm.core.util.ResourceSpliterator; +import org.apache.jackrabbit.JcrConstants; +import org.apache.sling.api.resource.*; + +public class MockRepository { + + private final MockManager manager; + + private final ResourceResolver resolver; + + public MockRepository(MockManager manager, ResourceResolver resolver) { + this.manager = manager; + this.resolver = resolver; + } + + public Stream findStubs() throws MockException { + Stream result = Stream.empty(); + for (String path : manager.getSearchPaths()) { + Resource root = resolver.getResource(path); + if (root == null) { + throw new MockException(String.format("Cannot read stubs search path '%s'!", path)); + } + Stream stream = ResourceSpliterator.stream(root, this::isStub); + result = Stream.concat(result, stream); + } + return result; + } + + private boolean isStub(Resource resource) { + return resource.isResourceType(JcrConstants.NT_FILE) && resource.getName().endsWith(manager.getClassifier()); + } + + public Optional findResource(String subPath) { + for (String path : manager.getSearchPaths()) { + Resource result = resolver.getResource(String.format("%s/%s", path, subPath)); + if (result != null) { + return Optional.of(result); + } + } + return Optional.empty(); + } + + public Optional findStub(String subPath) { + return findResource(subPath).filter(this::isStub); + } + + public Optional findSpecialStub(String subPath) { + return findResource(subPath); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockRequestException.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockRequestException.java new file mode 100644 index 00000000..1f55348a --- /dev/null +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockRequestException.java @@ -0,0 +1,8 @@ +package com.vml.es.aem.acm.core.mock; + +public class MockRequestException extends MockException { + + public MockRequestException(String message, Exception e) { + super(message, e); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockResponseException.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockResponseException.java new file mode 100644 index 00000000..41d3ad32 --- /dev/null +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockResponseException.java @@ -0,0 +1,12 @@ +package com.vml.es.aem.acm.core.mock; + +public class MockResponseException extends MockException { + + public MockResponseException(String message, Exception e) { + super(message, e); + } + + public MockResponseException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/vml/es/aem/acm/core/script/ScriptType.java b/core/src/main/java/com/vml/es/aem/acm/core/script/ScriptType.java index 84db2ff1..08ddadca 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/script/ScriptType.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/script/ScriptType.java @@ -8,6 +8,7 @@ public enum ScriptType { MANUAL(ScriptRepository.ROOT + "/manual"), ENABLED(ScriptRepository.ROOT + "/auto/enabled"), DISABLED(ScriptRepository.ROOT + "/auto/disabled"), + MOCK(ScriptRepository.ROOT + "/stub"), EXTENSION(ScriptRepository.ROOT + "/extension"); private final String root; diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/hello-world.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/hello-world.groovy new file mode 100644 index 00000000..f9b46da6 --- /dev/null +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/hello-world.groovy @@ -0,0 +1,11 @@ +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +boolean request(HttpServletRequest request) { + return request.requestURI == "/mock/hello-world" +} + +void respond(HttpServletRequest request, HttpServletResponse response) { + def message = StringUtils.defaultIfBlank(request.getParameter("message"), "Hello, World!") + response.writer.write("""{"message": "$message"}""") +} \ No newline at end of file diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/odd-numbers.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/odd-numbers.groovy new file mode 100644 index 00000000..1ead2c07 --- /dev/null +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/odd-numbers.groovy @@ -0,0 +1,20 @@ +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + + +boolean request(HttpServletRequest request) { + return request.getRequestURI() == "/mock/odd-numbers" +} + +void respond(HttpServletRequest request, HttpServletResponse response) { + def start = StringUtils.defaultIfBlank(request.getParameter("start"), "1") as int + def end = StringUtils.defaultIfBlank(request.getParameter("end"), "100") as int + def numbers = (start..end).findAll { it % 2 != 0 } + + response.setContentType("application/json; charset=UTF-8") + response.getWriter().write(gson.toJson([ + "start": start, + "end": end, + "result": numbers + ])) +} \ No newline at end of file diff --git a/ui.frontend/src/utils/api.types.ts b/ui.frontend/src/utils/api.types.ts index f41744d3..f98126f9 100644 --- a/ui.frontend/src/utils/api.types.ts +++ b/ui.frontend/src/utils/api.types.ts @@ -191,6 +191,7 @@ export enum ScriptType { ENABLED = 'ENABLED', DISABLED = 'DISABLED', EXTENSION = 'EXTENSION', + MOCK = 'MOCK', } export type Script = { From a9dbda5fcf0f829464ad818bc3dcfcdef94c87e0 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Fri, 9 May 2025 09:44:37 +0200 Subject: [PATCH 02/26] Rename --- .../com/vml/es/aem/acm/core/mock/MockFilter.java | 16 ++++++++-------- .../vml/es/aem/acm/core/mock/MockManager.java | 7 ++++--- .../vml/es/aem/acm/core/mock/MockRepository.java | 2 +- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockFilter.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockFilter.java index 73d0cf06..4a22bc5d 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockFilter.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockFilter.java @@ -85,19 +85,19 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) } } } catch (MockException e) { - LOG.error("Stubs error!", e); + LOG.error("Mock error!", e); Mock mock = repository.findSpecialStub(MockManager.FAIL_PATH).orElse(null); if (mock != null) { try { mock.fail(request, response, e); } catch (MockException e2) { - LOG.error("Stubs fail error!", e2); + LOG.error("Mock fail error!", e2); response.sendError( - HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Stubs fail error. " + e.getMessage()); + HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Mock fail error. " + e.getMessage()); } } else { - response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Stubs error. " + e.getMessage()); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Mock error. " + e.getMessage()); } return; } @@ -107,17 +107,17 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) try { mock.respond(request, response); } catch (MockException e) { - LOG.error("Stubs missing error!", e); + LOG.error("Mock missing error!", e); response.sendError( - HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Stubs missing error. " + e.getMessage()); + HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Mock missing error. " + e.getMessage()); } } else { response.sendError(HttpServletResponse.SC_NOT_FOUND, "Mock not found!"); } return; } catch (LoginException e) { - LOG.error("Stubs repository error!", e); - response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Stubs error. " + e.getMessage()); + LOG.error("Mock repository error!", e); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Mock error. " + e.getMessage()); } } diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockManager.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockManager.java index 84251eb7..0078fa31 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockManager.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockManager.java @@ -26,13 +26,14 @@ public class MockManager { @ObjectClassDefinition(name = "AEM Content Manager - Mock Manager") public @interface Config { + // TODO hardcode it @AttributeDefinition(name = "Search paths", description = "JCR repository paths to search for mock resources.") - String[] searchPaths() default {"/conf/stubs"}; + String[] searchPaths() default {"/conf/acm/settings/script/mock"}; @AttributeDefinition( name = "Classifier", - description = "Resource name part used to distinguish stubs from other files.") - String classifier() default "stub"; + description = "Resource name part used to distinguish mocks from other files.") + String classifier() default "mock"; } @Activate diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockRepository.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockRepository.java index 10e446c8..796d5ddd 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockRepository.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockRepository.java @@ -23,7 +23,7 @@ public Stream findStubs() throws MockException { for (String path : manager.getSearchPaths()) { Resource root = resolver.getResource(path); if (root == null) { - throw new MockException(String.format("Cannot read stubs search path '%s'!", path)); + throw new MockException(String.format("Cannot read mock search path '%s'!", path)); } Stream stream = ResourceSpliterator.stream(root, this::isStub); result = Stream.concat(result, stream); From 4e8c857c115d4d73fb47ef79c9ad472580ac3e20 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Mon, 12 May 2025 10:26:48 +0200 Subject: [PATCH 03/26] Mock WIP --- .../core/code/script/ContentScriptSyntax.java | 1 - .../code/script/ExtensionScriptSyntax.java | 1 - .../aem/acm/core/code/script/MockScript.java | 5 +- .../core/code/script/MockScriptSyntax.java | 5 +- .../aem/acm/core/code/script/ScriptUtils.java | 3 +- .../com/vml/es/aem/acm/core/mock/Mock.java | 2 +- .../es/aem/acm/core/mock/MockException.java | 2 +- .../{MockFilter.java => MockHttpFilter.java} | 55 +++++++------ .../vml/es/aem/acm/core/mock/MockManager.java | 56 -------------- .../es/aem/acm/core/mock/MockRepository.java | 55 ------------- .../acm/core/mock/MockRequestException.java | 2 +- .../acm/core/mock/MockResponseException.java | 2 +- .../acm/core/mock/MockScriptExecutable.java | 31 ++++++++ .../acm/core/mock/MockScriptRepository.java | 60 ++++++++++++++ .../main/content/META-INF/vault/filter.xml | 1 + .../acm/settings/script/mock/.content.xml | 3 + .../settings/script/mock/example/.content.xml | 3 + .../mock/{ => example}/hello-world.groovy | 0 .../mock/{ => example}/odd-numbers.groovy | 0 .../main/content/META-INF/vault/filter.xml | 1 + .../conf/acm/settings/mock/.content.xml | 3 + .../conf/acm/settings/mock/core/.content.xml | 3 + .../conf/acm/settings/mock/core/fail.groovy | 16 ++++ .../conf/acm/settings/mock/core/fail.html | 73 ++++++++++++++++++ .../conf/acm/settings/mock/core/logo-text.png | Bin 0 -> 23514 bytes .../acm/settings/mock/core/missing.groovy | 12 +++ .../conf/acm/settings/mock/core/missing.html | 58 ++++++++++++++ 27 files changed, 306 insertions(+), 147 deletions(-) rename core/src/main/java/com/vml/es/aem/acm/core/mock/{MockFilter.java => MockHttpFilter.java} (68%) delete mode 100644 core/src/main/java/com/vml/es/aem/acm/core/mock/MockManager.java delete mode 100644 core/src/main/java/com/vml/es/aem/acm/core/mock/MockRepository.java create mode 100644 core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptExecutable.java create mode 100644 core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptRepository.java create mode 100644 ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/.content.xml create mode 100644 ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/example/.content.xml rename ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/{ => example}/hello-world.groovy (100%) rename ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/{ => example}/odd-numbers.groovy (100%) create mode 100644 ui.content/src/main/content/jcr_root/conf/acm/settings/mock/.content.xml create mode 100644 ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/.content.xml create mode 100644 ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/fail.groovy create mode 100644 ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/fail.html create mode 100644 ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/logo-text.png create mode 100644 ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/missing.groovy create mode 100644 ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/missing.html diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ContentScriptSyntax.java b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ContentScriptSyntax.java index 99949edb..e30b92ca 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ContentScriptSyntax.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ContentScriptSyntax.java @@ -3,7 +3,6 @@ import static com.vml.es.aem.acm.core.code.script.ScriptUtils.*; import org.codehaus.groovy.ast.ASTNode; -import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.control.CompilePhase; import org.codehaus.groovy.control.SourceUnit; import org.codehaus.groovy.transform.AbstractASTTransformation; diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ExtensionScriptSyntax.java b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ExtensionScriptSyntax.java index 6e46900c..f7d819a1 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ExtensionScriptSyntax.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ExtensionScriptSyntax.java @@ -3,7 +3,6 @@ import static com.vml.es.aem.acm.core.code.script.ScriptUtils.*; import org.codehaus.groovy.ast.ASTNode; -import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.control.CompilePhase; import org.codehaus.groovy.control.SourceUnit; import org.codehaus.groovy.transform.AbstractASTTransformation; diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScript.java b/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScript.java index 48cb1eb1..600261c5 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScript.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScript.java @@ -7,13 +7,12 @@ import com.vml.es.aem.acm.core.mock.MockResponseException; import groovy.lang.GroovyShell; import groovy.lang.Script; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - public class MockScript implements Mock { private static final Logger LOG = org.slf4j.LoggerFactory.getLogger(MockScript.class); diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScriptSyntax.java b/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScriptSyntax.java index d1a7bcbe..bb9c1ab3 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScriptSyntax.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScriptSyntax.java @@ -1,14 +1,13 @@ package com.vml.es.aem.acm.core.code.script; +import static com.vml.es.aem.acm.core.code.script.ScriptUtils.*; + import org.codehaus.groovy.ast.ASTNode; -import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.control.CompilePhase; import org.codehaus.groovy.control.SourceUnit; import org.codehaus.groovy.transform.AbstractASTTransformation; import org.codehaus.groovy.transform.GroovyASTTransformation; -import static com.vml.es.aem.acm.core.code.script.ScriptUtils.*; - @GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS) public class MockScriptSyntax extends AbstractASTTransformation { diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ScriptUtils.java b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ScriptUtils.java index 33442189..80b8f9b8 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ScriptUtils.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ScriptUtils.java @@ -20,7 +20,8 @@ private ScriptUtils() { // intentionally empty } - public static void visit(AbstractASTTransformation transformation, ASTNode[] nodes, SourceUnit source, String mainClassName) { + public static void visit( + AbstractASTTransformation transformation, ASTNode[] nodes, SourceUnit source, String mainClassName) { ClassNode mainClass = requireMainClass(source.getAST().getClasses(), mainClassName); for (MockScriptSyntax.Method methodValue : MockScriptSyntax.Method.values()) { if (methodValue.required || hasMethod(mainClass, methodValue.givenName)) { diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/Mock.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/Mock.java index 19538201..ea848d99 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/mock/Mock.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/Mock.java @@ -12,4 +12,4 @@ public interface Mock { void respond(HttpServletRequest request, HttpServletResponse response) throws MockResponseException; void fail(HttpServletRequest request, HttpServletResponse response, Exception e) throws MockResponseException; -} \ No newline at end of file +} diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockException.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockException.java index 9e3eb9d7..03d1971d 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockException.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockException.java @@ -9,4 +9,4 @@ public MockException(String message) { public MockException(String message, Throwable cause) { super(message, cause); } -} \ No newline at end of file +} diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockFilter.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockHttpFilter.java similarity index 68% rename from core/src/main/java/com/vml/es/aem/acm/core/mock/MockFilter.java rename to core/src/main/java/com/vml/es/aem/acm/core/mock/MockHttpFilter.java index 4a22bc5d..1999840c 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockFilter.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockHttpFilter.java @@ -1,15 +1,17 @@ package com.vml.es.aem.acm.core.mock; +import com.vml.es.aem.acm.core.code.ExecutionContext; +import com.vml.es.aem.acm.core.code.ExecutionId; +import com.vml.es.aem.acm.core.code.ExecutionMode; +import com.vml.es.aem.acm.core.code.Executor; +import com.vml.es.aem.acm.core.code.script.MockScript; +import com.vml.es.aem.acm.core.util.ResourceUtils; import java.io.IOException; import java.util.Iterator; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; - -import com.vml.es.aem.acm.core.code.Executor; -import com.vml.es.aem.acm.core.util.ResourceUtils; import org.apache.sling.api.resource.LoginException; -import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.api.resource.ResourceResolverFactory; import org.osgi.service.component.annotations.*; @@ -25,19 +27,15 @@ property = { HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT + "=(" + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME + "=*)" - }, - configurationPolicy = ConfigurationPolicy.REQUIRE) -@Designate(ocd = MockFilter.Config.class) -public class MockFilter implements Filter { + }) +@Designate(ocd = MockHttpFilter.Config.class) +public class MockHttpFilter implements Filter { - private static final Logger LOG = LoggerFactory.getLogger(MockFilter.class); + private static final Logger LOG = LoggerFactory.getLogger(MockHttpFilter.class); @Reference private ResourceResolverFactory resolverFactory; - @Reference - private MockManager manager; - private Config config; @ObjectClassDefinition(name = "AEM Content Manager - Mock HTTP Filter") @@ -47,7 +45,7 @@ public class MockFilter implements Filter { boolean enabled() default true; @AttributeDefinition(name = "Whiteboard Filter Regex") - String[] osgi_http_whiteboard_filter_regex() default { "/mock/.*" }; + String[] osgi_http_whiteboard_filter_regex() default {"/mock/.*"}; } @Reference @@ -71,13 +69,16 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) HttpServletResponse response = (HttpServletResponse) res; try (ResourceResolver resolver = ResourceUtils.serviceResolver(resolverFactory, null)) { - MockRepository repository = new MockRepository(manager, resolver); + MockScriptRepository repository = new MockScriptRepository(resolver); + try { - Iterator it = repository.findStubs().iterator(); + Iterator it = repository.findAll().iterator(); while (it.hasNext()) { - Resource mock = it.next(); - if (!manager.isSpecial(mock)) { - // TODO connect somehow MockScript with executor.createContext(mock.getPath(), ExecutionMode.RUN, mock, resolver); + MockScriptExecutable candidateScript = it.next(); + if (!repository.isSpecial(candidateScript.getId())) { + ExecutionContext executionContext = executor.createContext( + ExecutionId.generate(), ExecutionMode.RUN, candidateScript, resolver); + MockScript mock = new MockScript(executionContext); if (mock.request(request)) { mock.respond(request, response); } @@ -87,9 +88,13 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) } catch (MockException e) { LOG.error("Mock error!", e); - Mock mock = repository.findSpecialStub(MockManager.FAIL_PATH).orElse(null); - if (mock != null) { + MockScriptExecutable failScript = + repository.findSpecial(MockScriptRepository.FAIL_PATH).orElse(null); + if (failScript != null) { try { + ExecutionContext executionContext = + executor.createContext(ExecutionId.generate(), ExecutionMode.RUN, failScript, resolver); + MockScript mock = new MockScript(executionContext); mock.fail(request, response, e); } catch (MockException e2) { LOG.error("Mock fail error!", e2); @@ -102,9 +107,13 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) return; } - Mock mock = repository.findSpecialStub(MockManager.MISSING_PATH).orElse(null); - if (mock != null) { + MockScriptExecutable missingScript = + repository.findSpecial(MockScriptRepository.MISSING_PATH).orElse(null); + if (missingScript != null) { try { + ExecutionContext executionContext = + executor.createContext(ExecutionId.generate(), ExecutionMode.RUN, missingScript, resolver); + MockScript mock = new MockScript(executionContext); mock.respond(request, response); } catch (MockException e) { LOG.error("Mock missing error!", e); @@ -130,4 +139,4 @@ public void init(FilterConfig filterConfig) throws ServletException { public void destroy() { // do nothing } -} \ No newline at end of file +} diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockManager.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockManager.java deleted file mode 100644 index 0078fa31..00000000 --- a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockManager.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.vml.es.aem.acm.core.mock; - -import org.apache.commons.lang3.StringUtils; -import org.osgi.service.component.annotations.Activate; -import org.osgi.service.component.annotations.Component; -import org.osgi.service.component.annotations.Modified; -import org.osgi.service.metatype.annotations.AttributeDefinition; -import org.osgi.service.metatype.annotations.ObjectClassDefinition; - -import java.util.Arrays; -import java.util.List; - -@Component(service = MockManager.class, immediate = true) -public class MockManager { - - public static final String INTERNAL_DIR = "internal"; - - public static final String FAIL_PATH = INTERNAL_DIR + "/fail.groovy"; - - public static final String MISSING_PATH = INTERNAL_DIR + "/missing.groovy"; - - public static final List SPECIAL_PATHS = Arrays.asList(FAIL_PATH, MISSING_PATH); - - private Config config; - - @ObjectClassDefinition(name = "AEM Content Manager - Mock Manager") - public @interface Config { - - // TODO hardcode it - @AttributeDefinition(name = "Search paths", description = "JCR repository paths to search for mock resources.") - String[] searchPaths() default {"/conf/acm/settings/script/mock"}; - - @AttributeDefinition( - name = "Classifier", - description = "Resource name part used to distinguish mocks from other files.") - String classifier() default "mock"; - } - - @Activate - @Modified - protected void activate(Config config) { - this.config = config; - } - - public List getSearchPaths() { - return Arrays.asList(config.searchPaths()); - } - - public String getClassifier() { - return config.classifier(); - } - - public boolean isSpecial(Mock mock) { - return SPECIAL_PATHS.stream().anyMatch(n -> StringUtils.endsWith(mock.getId(), "/" + n)); - } -} diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockRepository.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockRepository.java deleted file mode 100644 index 796d5ddd..00000000 --- a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockRepository.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.vml.es.aem.acm.core.mock; - -import java.util.Optional; -import java.util.stream.Stream; - -import com.vml.es.aem.acm.core.util.ResourceSpliterator; -import org.apache.jackrabbit.JcrConstants; -import org.apache.sling.api.resource.*; - -public class MockRepository { - - private final MockManager manager; - - private final ResourceResolver resolver; - - public MockRepository(MockManager manager, ResourceResolver resolver) { - this.manager = manager; - this.resolver = resolver; - } - - public Stream findStubs() throws MockException { - Stream result = Stream.empty(); - for (String path : manager.getSearchPaths()) { - Resource root = resolver.getResource(path); - if (root == null) { - throw new MockException(String.format("Cannot read mock search path '%s'!", path)); - } - Stream stream = ResourceSpliterator.stream(root, this::isStub); - result = Stream.concat(result, stream); - } - return result; - } - - private boolean isStub(Resource resource) { - return resource.isResourceType(JcrConstants.NT_FILE) && resource.getName().endsWith(manager.getClassifier()); - } - - public Optional findResource(String subPath) { - for (String path : manager.getSearchPaths()) { - Resource result = resolver.getResource(String.format("%s/%s", path, subPath)); - if (result != null) { - return Optional.of(result); - } - } - return Optional.empty(); - } - - public Optional findStub(String subPath) { - return findResource(subPath).filter(this::isStub); - } - - public Optional findSpecialStub(String subPath) { - return findResource(subPath); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockRequestException.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockRequestException.java index 1f55348a..66cf842d 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockRequestException.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockRequestException.java @@ -5,4 +5,4 @@ public class MockRequestException extends MockException { public MockRequestException(String message, Exception e) { super(message, e); } -} \ No newline at end of file +} diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockResponseException.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockResponseException.java index 41d3ad32..c223b923 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockResponseException.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockResponseException.java @@ -9,4 +9,4 @@ public MockResponseException(String message, Exception e) { public MockResponseException(String message) { super(message); } -} \ No newline at end of file +} diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptExecutable.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptExecutable.java new file mode 100644 index 00000000..9292c325 --- /dev/null +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptExecutable.java @@ -0,0 +1,31 @@ +package com.vml.es.aem.acm.core.mock; + +import com.vml.es.aem.acm.core.AcmException; +import com.vml.es.aem.acm.core.code.ArgumentValues; +import com.vml.es.aem.acm.core.code.Executable; +import com.vml.es.aem.acm.core.repo.RepoResource; +import org.apache.sling.api.resource.Resource; + +public class MockScriptExecutable implements Executable { + + private final Resource resource; + + public MockScriptExecutable(Resource resource) { + this.resource = resource; + } + + @Override + public String getId() { + return resource.getPath(); + } + + @Override + public String getContent() throws AcmException { + return RepoResource.of(resource).readFileAsString(); + } + + @Override + public ArgumentValues getArguments() { + return new ArgumentValues(); + } +} diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptRepository.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptRepository.java new file mode 100644 index 00000000..1d3919ad --- /dev/null +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptRepository.java @@ -0,0 +1,60 @@ +package com.vml.es.aem.acm.core.mock; + +import com.vml.es.aem.acm.core.AcmException; +import com.vml.es.aem.acm.core.util.ResourceSpliterator; +import com.vml.es.aem.acm.core.util.ResourceUtils; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; +import org.apache.commons.lang3.StringUtils; +import org.apache.jackrabbit.JcrConstants; +import org.apache.sling.api.resource.*; + +public class MockScriptRepository { + + public static final String ROOT = "/conf/acm/settings/script/mock"; + + public static final String CORE_DIR = "core"; + + public static final String FAIL_PATH = CORE_DIR + "/fail.groovy"; + + public static final String MISSING_PATH = CORE_DIR + "/missing.groovy"; + + public static final List SPECIAL_PATHS = Arrays.asList(FAIL_PATH, MISSING_PATH); + + private final ResourceResolver resolver; + + public MockScriptRepository(ResourceResolver resolver) { + this.resolver = resolver; + } + + public Resource getOrCreateRoot() throws AcmException { + return ResourceUtils.makeFolders(resolver, ROOT); + } + + public boolean checkResource(Resource resource) { + return resource.getName().endsWith(".groovy") && resource.isResourceType(JcrConstants.NT_FILE); + } + + private Optional findResource(String subPath) { + return Optional.ofNullable(resolver.getResource(String.format("%s/%s", ROOT, subPath))); + } + + public Stream findAll() throws MockException { + return ResourceSpliterator.stream(getOrCreateRoot(), this::checkResource) + .map(s -> s.adaptTo(MockScriptExecutable.class)); + } + + public Optional find(String subPath) { + return findResource(subPath).filter(this::checkResource).map(s -> s.adaptTo(MockScriptExecutable.class)); + } + + public Optional findSpecial(String subPath) { + return findResource(subPath).map(s -> s.adaptTo(MockScriptExecutable.class)); + } + + public boolean isSpecial(String id) { + return SPECIAL_PATHS.stream().anyMatch(n -> StringUtils.endsWith(id, "/" + n)); + } +} diff --git a/ui.content.example/src/main/content/META-INF/vault/filter.xml b/ui.content.example/src/main/content/META-INF/vault/filter.xml index f606b7a0..fc6f1d9f 100644 --- a/ui.content.example/src/main/content/META-INF/vault/filter.xml +++ b/ui.content.example/src/main/content/META-INF/vault/filter.xml @@ -4,4 +4,5 @@ + diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/.content.xml b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/.content.xml new file mode 100644 index 00000000..491392d5 --- /dev/null +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/.content.xml @@ -0,0 +1,3 @@ + + diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/example/.content.xml b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/example/.content.xml new file mode 100644 index 00000000..491392d5 --- /dev/null +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/example/.content.xml @@ -0,0 +1,3 @@ + + diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/hello-world.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/example/hello-world.groovy similarity index 100% rename from ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/hello-world.groovy rename to ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/example/hello-world.groovy diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/odd-numbers.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/example/odd-numbers.groovy similarity index 100% rename from ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/odd-numbers.groovy rename to ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/example/odd-numbers.groovy diff --git a/ui.content/src/main/content/META-INF/vault/filter.xml b/ui.content/src/main/content/META-INF/vault/filter.xml index 32887b85..8a289a4d 100644 --- a/ui.content/src/main/content/META-INF/vault/filter.xml +++ b/ui.content/src/main/content/META-INF/vault/filter.xml @@ -2,4 +2,5 @@ + diff --git a/ui.content/src/main/content/jcr_root/conf/acm/settings/mock/.content.xml b/ui.content/src/main/content/jcr_root/conf/acm/settings/mock/.content.xml new file mode 100644 index 00000000..491392d5 --- /dev/null +++ b/ui.content/src/main/content/jcr_root/conf/acm/settings/mock/.content.xml @@ -0,0 +1,3 @@ + + diff --git a/ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/.content.xml b/ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/.content.xml new file mode 100644 index 00000000..491392d5 --- /dev/null +++ b/ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/.content.xml @@ -0,0 +1,3 @@ + + diff --git a/ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/fail.groovy b/ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/fail.groovy new file mode 100644 index 00000000..d9b1480a --- /dev/null +++ b/ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/fail.groovy @@ -0,0 +1,16 @@ +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse +import org.apache.commons.lang3.exception.ExceptionUtils + +void fail(HttpServletRequest request, HttpServletResponse response, Exception exception) { + response.setStatus(500) + template.render(response, "fail.html", [ + "request": request, + "response": response, + "exception": exception, + + "logo": repository.readAsBase64("logo-text.png"), + "stackTrace": ExceptionUtils.getStackTrace(exception), + "rootCause": ExceptionUtils.getRootCause(exception), + ]) +} \ No newline at end of file diff --git a/ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/fail.html b/ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/fail.html new file mode 100644 index 00000000..8228c856 --- /dev/null +++ b/ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/fail.html @@ -0,0 +1,73 @@ + + + + Stubs - Error + + + + +
+ AEM Stubs Logo +

Error (${response.status})

+
+ +
+ The requested resource cannot be processed by server properly. +
+ +
+

Details

+ + + + + + + + + + + + + + + + + + + + + +
Method${request.method}
URI${request.requestURI}
Query String${request.queryString ?: "(empty)" }
Exception${exception.class.name}: ${exception.message}
Root Cause${rootCause.class.name}: ${rootCause.message}
+
+ +
+

Stack Trace

+
${stackTrace}
+
+ +
+

Headers:

+ + <% request.headerNames.toList().sort().each { headerName -> %> + + + + + <% } %> +
${headerName}${request.getHeader(headerName)}
+
+ + + \ No newline at end of file diff --git a/ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/logo-text.png b/ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/logo-text.png new file mode 100644 index 0000000000000000000000000000000000000000..2e4614c49bb993e20d72bdd2d5e6023408ad8e6a GIT binary patch literal 23514 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 M07*qoM6N<$f;4M>`v3p{ literal 0 HcmV?d00001 diff --git a/ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/missing.groovy b/ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/missing.groovy new file mode 100644 index 00000000..c5e3371f --- /dev/null +++ b/ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/missing.groovy @@ -0,0 +1,12 @@ +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +void respond(HttpServletRequest request, HttpServletResponse response) { + response.setStatus(404) + template.render(response, "missing.html", [ + "request": request, + "response": response, + + "logo": repository.readAsBase64("logo-text.png"), + ]) +} \ No newline at end of file diff --git a/ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/missing.html b/ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/missing.html new file mode 100644 index 00000000..ffc1b232 --- /dev/null +++ b/ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/missing.html @@ -0,0 +1,58 @@ + + + + Stubs - 404 Not Found + + + + +
+ AEM Stubs Logo +

Not Found (${response.status})

+
+ +
+ The requested resource was not found on this server. +
+ +
+

Details

+ + + + + + + + + + + + + +
Method${request.method}
URI${request.requestURI}
Query String${request.queryString ?: "(empty)" }
+
+
+

Headers:

+ + <% request.headerNames.toList().sort().each { headerName -> %> + + + + + <% } %> +
${headerName}${request.getHeader(headerName)}
+
+ + + \ No newline at end of file From 6b06b53edfb8bba3598da9ecca034cbbdb779159 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Mon, 12 May 2025 16:10:29 +0200 Subject: [PATCH 04/26] Buggy syntax --- .../es/aem/acm/core/code/script/ContentScriptSyntax.java | 4 +--- .../aem/acm/core/code/script/ExtensionScriptSyntax.java | 2 -- .../com/vml/es/aem/acm/core/code/script/MockScript.java | 9 +++++---- .../es/aem/acm/core/code/script/MockScriptSyntax.java | 5 ++--- .../vml/es/aem/acm/core/mock/MockScriptRepository.java | 3 ++- 5 files changed, 10 insertions(+), 13 deletions(-) diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ContentScriptSyntax.java b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ContentScriptSyntax.java index e30b92ca..e6ec4b12 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ContentScriptSyntax.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ContentScriptSyntax.java @@ -1,7 +1,5 @@ package com.vml.es.aem.acm.core.code.script; -import static com.vml.es.aem.acm.core.code.script.ScriptUtils.*; - import org.codehaus.groovy.ast.ASTNode; import org.codehaus.groovy.control.CompilePhase; import org.codehaus.groovy.control.SourceUnit; @@ -11,7 +9,7 @@ @GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS) public class ContentScriptSyntax extends AbstractASTTransformation { - public static final String MAIN_CLASS = "AcmContentScript"; + public static final String MAIN_CLASS = "AcmExtensionScript"; @Override public void visit(ASTNode[] nodes, SourceUnit source) { diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ExtensionScriptSyntax.java b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ExtensionScriptSyntax.java index f7d819a1..7c9b82fb 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ExtensionScriptSyntax.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ExtensionScriptSyntax.java @@ -1,7 +1,5 @@ package com.vml.es.aem.acm.core.code.script; -import static com.vml.es.aem.acm.core.code.script.ScriptUtils.*; - import org.codehaus.groovy.ast.ASTNode; import org.codehaus.groovy.control.CompilePhase; import org.codehaus.groovy.control.SourceUnit; diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScript.java b/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScript.java index 600261c5..01a4d6eb 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScript.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScript.java @@ -30,7 +30,7 @@ private Script parseScript() { GroovyShell shell = ScriptUtils.createShell(new MockScriptSyntax()); Script script = shell.parse( executionContext.getExecutable().getContent(), - ContentScriptSyntax.MAIN_CLASS, + MockScriptSyntax.MAIN_CLASS, executionContext.getBinding()); if (script == null) { throw new AcmException(String.format( @@ -49,7 +49,8 @@ public String getId() { public boolean request(HttpServletRequest request) throws MockRequestException { try { LOG.info("Mock '{}' is matching request '{} {}'", getId(), request.getMethod(), request.getRequestURI()); - Boolean result = (Boolean) script.invokeMethod("request", new Object[] {request}); + Boolean result = + (Boolean) script.invokeMethod(MockScriptSyntax.Method.REQUEST.givenName, new Object[] {request}); if (BooleanUtils.isTrue(result)) { LOG.info("Mock '{}' matched request '{} {}'", getId(), request.getMethod(), request.getRequestURI()); } else { @@ -73,7 +74,7 @@ public void respond(HttpServletRequest request, HttpServletResponse response) th getId(), request.getMethod(), request.getRequestURI()); - script.invokeMethod("respond", new Object[] {request, response}); + script.invokeMethod(MockScriptSyntax.Method.RESPOND.givenName, new Object[] {request, response}); LOG.info("Mock '{}' responded to request '{} {}'", getId(), request.getMethod(), request.getRequestURI()); } catch (Exception e) { throw new MockResponseException(String.format("Mock script '%s' cannot respond properly", getId()), e); @@ -89,7 +90,7 @@ public void fail(HttpServletRequest request, HttpServletResponse response, Excep getId(), request.getMethod(), request.getRequestURI()); - script.invokeMethod("fail", new Object[] {request, response, exception}); + script.invokeMethod(MockScriptSyntax.Method.FAIL.givenName, new Object[] {request, response, exception}); LOG.info("Mock '{}' handled failed request '{} {}'", getId(), request.getMethod(), request.getRequestURI()); } catch (Exception e) { throw new MockResponseException( diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScriptSyntax.java b/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScriptSyntax.java index bb9c1ab3..34a192cf 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScriptSyntax.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScriptSyntax.java @@ -1,7 +1,5 @@ package com.vml.es.aem.acm.core.code.script; -import static com.vml.es.aem.acm.core.code.script.ScriptUtils.*; - import org.codehaus.groovy.ast.ASTNode; import org.codehaus.groovy.control.CompilePhase; import org.codehaus.groovy.control.SourceUnit; @@ -24,7 +22,8 @@ public void visit(ASTNode[] nodes, SourceUnit source) { enum Method { REQUEST("request", "boolean", true), - RESPOND("respond", "void", true); + RESPOND("respond", "void", true), + FAIL("fail", "void", false); final String givenName; diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptRepository.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptRepository.java index 1d3919ad..01c9fb48 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptRepository.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptRepository.java @@ -42,7 +42,8 @@ private Optional findResource(String subPath) { } public Stream findAll() throws MockException { - return ResourceSpliterator.stream(getOrCreateRoot(), this::checkResource) + return ResourceSpliterator.stream(getOrCreateRoot()) + .filter(this::checkResource) .map(s -> s.adaptTo(MockScriptExecutable.class)); } From adb3b813e9152546078272401101fd20d24099b1 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Mon, 12 May 2025 16:18:18 +0200 Subject: [PATCH 05/26] Minor --- .../com/vml/es/aem/acm/core/mock/MockScriptRepository.java | 7 +++---- .../java/com/vml/es/aem/acm/core/script/ScriptType.java | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptRepository.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptRepository.java index 01c9fb48..ed22111d 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptRepository.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptRepository.java @@ -1,6 +1,7 @@ package com.vml.es.aem.acm.core.mock; import com.vml.es.aem.acm.core.AcmException; +import com.vml.es.aem.acm.core.script.ScriptType; import com.vml.es.aem.acm.core.util.ResourceSpliterator; import com.vml.es.aem.acm.core.util.ResourceUtils; import java.util.Arrays; @@ -13,8 +14,6 @@ public class MockScriptRepository { - public static final String ROOT = "/conf/acm/settings/script/mock"; - public static final String CORE_DIR = "core"; public static final String FAIL_PATH = CORE_DIR + "/fail.groovy"; @@ -30,7 +29,7 @@ public MockScriptRepository(ResourceResolver resolver) { } public Resource getOrCreateRoot() throws AcmException { - return ResourceUtils.makeFolders(resolver, ROOT); + return ResourceUtils.makeFolders(resolver, ScriptType.MOCK.root()); } public boolean checkResource(Resource resource) { @@ -38,7 +37,7 @@ public boolean checkResource(Resource resource) { } private Optional findResource(String subPath) { - return Optional.ofNullable(resolver.getResource(String.format("%s/%s", ROOT, subPath))); + return Optional.ofNullable(resolver.getResource(String.format("%s/%s", ScriptType.MOCK.root(), subPath))); } public Stream findAll() throws MockException { diff --git a/core/src/main/java/com/vml/es/aem/acm/core/script/ScriptType.java b/core/src/main/java/com/vml/es/aem/acm/core/script/ScriptType.java index 08ddadca..684217ff 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/script/ScriptType.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/script/ScriptType.java @@ -8,7 +8,7 @@ public enum ScriptType { MANUAL(ScriptRepository.ROOT + "/manual"), ENABLED(ScriptRepository.ROOT + "/auto/enabled"), DISABLED(ScriptRepository.ROOT + "/auto/disabled"), - MOCK(ScriptRepository.ROOT + "/stub"), + MOCK(ScriptRepository.ROOT + "/mock"), EXTENSION(ScriptRepository.ROOT + "/extension"); private final String root; From bff52424ef523b5f87cdad5a61e8ffdef1f3fb28 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Mon, 12 May 2025 16:29:46 +0200 Subject: [PATCH 06/26] Little syntax revert --- .../vml/es/aem/acm/core/code/Extender.java | 1 + .../core/code/script/ContentScriptSyntax.java | 18 +++++++++++++++++- .../code/script/ExtensionScriptSyntax.java | 18 +++++++++++++++++- .../core/code/script/MockScriptSyntax.java | 18 +++++++++++++++++- .../aem/acm/core/code/script/ScriptUtils.java | 19 ------------------- 5 files changed, 52 insertions(+), 22 deletions(-) diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/Extender.java b/core/src/main/java/com/vml/es/aem/acm/core/code/Extender.java index 9e49ec4e..bd231b6d 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/Extender.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/Extender.java @@ -11,6 +11,7 @@ public class Extender { private final List scripts; public Extender(ExecutionContext contentContext) { + this.scripts = new ScriptRepository(contentContext.getResourceResolver()) .findAll(ScriptType.EXTENSION) .map(s -> new ExtensionScript(contentContext, s)) diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ContentScriptSyntax.java b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ContentScriptSyntax.java index e6ec4b12..f0f00085 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ContentScriptSyntax.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ContentScriptSyntax.java @@ -1,6 +1,9 @@ package com.vml.es.aem.acm.core.code.script; +import static com.vml.es.aem.acm.core.code.script.ScriptUtils.*; + import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.control.CompilePhase; import org.codehaus.groovy.control.SourceUnit; import org.codehaus.groovy.transform.AbstractASTTransformation; @@ -16,8 +19,21 @@ public void visit(ASTNode[] nodes, SourceUnit source) { if (source == null) { return; } + this.sourceUnit = source; - ScriptUtils.visit(this, nodes, source, MAIN_CLASS); + + ClassNode mainClass = requireMainClass(source.getAST().getClasses(), MAIN_CLASS); + for (Method methodValue : Method.values()) { + if (methodValue.required || hasMethod(mainClass, methodValue.givenName)) { + if (!isMethodValid(mainClass, methodValue.givenName, methodValue.returnType, 0)) { + addError( + String.format( + "Top-level '%s %s()' method not found or has incorrect signature!", + methodValue.returnType, methodValue.givenName), + mainClass); + } + } + } } enum Method { diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ExtensionScriptSyntax.java b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ExtensionScriptSyntax.java index 7c9b82fb..5c5f8c3a 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ExtensionScriptSyntax.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ExtensionScriptSyntax.java @@ -1,6 +1,9 @@ package com.vml.es.aem.acm.core.code.script; +import static com.vml.es.aem.acm.core.code.script.ScriptUtils.*; + import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.control.CompilePhase; import org.codehaus.groovy.control.SourceUnit; import org.codehaus.groovy.transform.AbstractASTTransformation; @@ -16,8 +19,21 @@ public void visit(ASTNode[] nodes, SourceUnit source) { if (source == null) { return; } + this.sourceUnit = source; - ScriptUtils.visit(this, nodes, source, MAIN_CLASS); + + ClassNode mainClass = requireMainClass(source.getAST().getClasses(), MAIN_CLASS); + for (Method methodValue : Method.values()) { + if (methodValue.required || hasMethod(mainClass, methodValue.givenName)) { + if (!isMethodValid(mainClass, methodValue.givenName, methodValue.returnType, 0)) { + addError( + String.format( + "Top-level '%s %s()' method not found or has incorrect signature!", + methodValue.returnType, methodValue.givenName), + mainClass); + } + } + } } enum Method { diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScriptSyntax.java b/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScriptSyntax.java index 34a192cf..55d97dd1 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScriptSyntax.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScriptSyntax.java @@ -1,6 +1,9 @@ package com.vml.es.aem.acm.core.code.script; +import static com.vml.es.aem.acm.core.code.script.ScriptUtils.*; + import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.control.CompilePhase; import org.codehaus.groovy.control.SourceUnit; import org.codehaus.groovy.transform.AbstractASTTransformation; @@ -16,8 +19,21 @@ public void visit(ASTNode[] nodes, SourceUnit source) { if (source == null) { return; } + this.sourceUnit = source; - ScriptUtils.visit(this, nodes, source, MAIN_CLASS); + + ClassNode mainClass = requireMainClass(source.getAST().getClasses(), MAIN_CLASS); + for (Method methodValue : Method.values()) { + if (methodValue.required || hasMethod(mainClass, methodValue.givenName)) { + if (!isMethodValid(mainClass, methodValue.givenName, methodValue.returnType, 0)) { + addError( + String.format( + "Top-level '%s %s()' method not found or has incorrect signature!", + methodValue.returnType, methodValue.givenName), + mainClass); + } + } + } } enum Method { diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ScriptUtils.java b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ScriptUtils.java index 80b8f9b8..8f6a8f45 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ScriptUtils.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ScriptUtils.java @@ -4,15 +4,12 @@ import groovy.lang.GroovyShell; import java.util.List; import org.codehaus.groovy.GroovyBugError; -import org.codehaus.groovy.ast.ASTNode; import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.ast.MethodNode; import org.codehaus.groovy.control.CompilerConfiguration; -import org.codehaus.groovy.control.SourceUnit; import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer; import org.codehaus.groovy.control.customizers.ImportCustomizer; import org.codehaus.groovy.transform.ASTTransformation; -import org.codehaus.groovy.transform.AbstractASTTransformation; public final class ScriptUtils { @@ -20,22 +17,6 @@ private ScriptUtils() { // intentionally empty } - public static void visit( - AbstractASTTransformation transformation, ASTNode[] nodes, SourceUnit source, String mainClassName) { - ClassNode mainClass = requireMainClass(source.getAST().getClasses(), mainClassName); - for (MockScriptSyntax.Method methodValue : MockScriptSyntax.Method.values()) { - if (methodValue.required || hasMethod(mainClass, methodValue.givenName)) { - if (!isMethodValid(mainClass, methodValue.givenName, methodValue.returnType, 0)) { - transformation.addError( - String.format( - "Top-level '%s %s()' method not found or has incorrect signature!", - methodValue.returnType, methodValue.givenName), - mainClass); - } - } - } - } - public static GroovyShell createShell(ASTTransformation codeSyntax) { CompilerConfiguration compiler = new CompilerConfiguration(); compiler.addCompilationCustomizers(new ImportCustomizer()); From 685cfaeca29ebbd657ddcfa5367aa2db1af9c494 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Mon, 12 May 2025 16:33:49 +0200 Subject: [PATCH 07/26] Minor --- .../vml/es/aem/acm/core/code/script/ContentScriptSyntax.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ContentScriptSyntax.java b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ContentScriptSyntax.java index f0f00085..e82b753b 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ContentScriptSyntax.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ContentScriptSyntax.java @@ -12,7 +12,7 @@ @GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS) public class ContentScriptSyntax extends AbstractASTTransformation { - public static final String MAIN_CLASS = "AcmExtensionScript"; + public static final String MAIN_CLASS = "AcmConentScript"; @Override public void visit(ASTNode[] nodes, SourceUnit source) { From ae82e7c4e1331339891b881e7f1dae2bf57db50f Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Mon, 12 May 2025 16:37:19 +0200 Subject: [PATCH 08/26] Syntaxes bug fix --- .../acm/core/code/script/ExtensionScriptSyntax.java | 2 +- .../aem/acm/core/code/script/MockScriptSyntax.java | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ExtensionScriptSyntax.java b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ExtensionScriptSyntax.java index 5c5f8c3a..b69c4004 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ExtensionScriptSyntax.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ExtensionScriptSyntax.java @@ -25,7 +25,7 @@ public void visit(ASTNode[] nodes, SourceUnit source) { ClassNode mainClass = requireMainClass(source.getAST().getClasses(), MAIN_CLASS); for (Method methodValue : Method.values()) { if (methodValue.required || hasMethod(mainClass, methodValue.givenName)) { - if (!isMethodValid(mainClass, methodValue.givenName, methodValue.returnType, 0)) { + if (!isMethodValid(mainClass, methodValue.givenName, methodValue.returnType, methodValue.paramCount)) { addError( String.format( "Top-level '%s %s()' method not found or has incorrect signature!", diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScriptSyntax.java b/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScriptSyntax.java index 55d97dd1..fe3ab7f6 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScriptSyntax.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScriptSyntax.java @@ -25,7 +25,7 @@ public void visit(ASTNode[] nodes, SourceUnit source) { ClassNode mainClass = requireMainClass(source.getAST().getClasses(), MAIN_CLASS); for (Method methodValue : Method.values()) { if (methodValue.required || hasMethod(mainClass, methodValue.givenName)) { - if (!isMethodValid(mainClass, methodValue.givenName, methodValue.returnType, 0)) { + if (!isMethodValid(mainClass, methodValue.givenName, methodValue.returnType, methodValue.paramCount)) { addError( String.format( "Top-level '%s %s()' method not found or has incorrect signature!", @@ -37,9 +37,9 @@ public void visit(ASTNode[] nodes, SourceUnit source) { } enum Method { - REQUEST("request", "boolean", true), - RESPOND("respond", "void", true), - FAIL("fail", "void", false); + REQUEST("request", "boolean", true, 1), + RESPOND("respond", "void", true, 2), + FAIL("fail", "void", false, 3); final String givenName; @@ -47,10 +47,13 @@ enum Method { final boolean required; - Method(String givenName, String returnType, boolean required) { + final int paramCount; + + Method(String givenName, String returnType, boolean required, int paramCount) { this.givenName = givenName; this.returnType = returnType; this.required = required; + this.paramCount = paramCount; } } } From 4d13b874943e2df9f00df95f90dce66257d252e9 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Mon, 12 May 2025 16:43:51 +0200 Subject: [PATCH 09/26] Almost work --- .../com/vml/es/aem/acm/core/code/script/MockContext.java | 5 +++++ .../com/vml/es/aem/acm/core/mock/MockScriptExecutable.java | 2 +- .../com/vml/es/aem/acm/core/mock/MockScriptRepository.java | 6 +++--- 3 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 core/src/main/java/com/vml/es/aem/acm/core/code/script/MockContext.java diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockContext.java b/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockContext.java new file mode 100644 index 00000000..0e65f36d --- /dev/null +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockContext.java @@ -0,0 +1,5 @@ +package com.vml.es.aem.acm.core.code.script; + +public class MockContext { + // TODO do the same binding like in content scripts +} diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptExecutable.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptExecutable.java index 9292c325..66d12bec 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptExecutable.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptExecutable.java @@ -6,7 +6,7 @@ import com.vml.es.aem.acm.core.repo.RepoResource; import org.apache.sling.api.resource.Resource; -public class MockScriptExecutable implements Executable { +public class MockScriptExecutable implements Executable { // TODO not executable! private final Resource resource; diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptRepository.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptRepository.java index ed22111d..12afe139 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptRepository.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptRepository.java @@ -43,15 +43,15 @@ private Optional findResource(String subPath) { public Stream findAll() throws MockException { return ResourceSpliterator.stream(getOrCreateRoot()) .filter(this::checkResource) - .map(s -> s.adaptTo(MockScriptExecutable.class)); + .map(MockScriptExecutable::new); } public Optional find(String subPath) { - return findResource(subPath).filter(this::checkResource).map(s -> s.adaptTo(MockScriptExecutable.class)); + return findResource(subPath).filter(this::checkResource).map(MockScriptExecutable::new); } public Optional findSpecial(String subPath) { - return findResource(subPath).map(s -> s.adaptTo(MockScriptExecutable.class)); + return findResource(subPath).map(MockScriptExecutable::new); } public boolean isSpecial(String id) { From ac22e58dd654ea2207340df0de2a105bc2d58c59 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Tue, 13 May 2025 07:24:38 +0200 Subject: [PATCH 10/26] Mocks work --- .../vml/es/aem/acm/core/format/Formatter.java | 6 +++++ .../es/aem/acm/core/format/YamlFormatter.java | 25 +++++++++++++++++++ .../es/aem/acm/core/mock/MockHttpFilter.java | 2 +- .../acm/core/snippet/SnippetDefinition.java | 2 +- .../vml/es/aem/acm/core/util/YamlUtils.java | 12 +++++++-- .../script/mock/example/hello-world.groovy | 1 + .../script/mock/example/odd-numbers.groovy | 6 ++--- 7 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 core/src/main/java/com/vml/es/aem/acm/core/format/YamlFormatter.java diff --git a/core/src/main/java/com/vml/es/aem/acm/core/format/Formatter.java b/core/src/main/java/com/vml/es/aem/acm/core/format/Formatter.java index a0e694a8..1914c576 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/format/Formatter.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/format/Formatter.java @@ -4,7 +4,13 @@ public class Formatter { private final JsonFormatter json = new JsonFormatter(); + private final YamlFormatter yaml = new YamlFormatter(); + public JsonFormatter getJson() { return json; } + + public YamlFormatter getYaml() { + return yaml; + } } diff --git a/core/src/main/java/com/vml/es/aem/acm/core/format/YamlFormatter.java b/core/src/main/java/com/vml/es/aem/acm/core/format/YamlFormatter.java new file mode 100644 index 00000000..12dba03d --- /dev/null +++ b/core/src/main/java/com/vml/es/aem/acm/core/format/YamlFormatter.java @@ -0,0 +1,25 @@ +package com.vml.es.aem.acm.core.format; + +import com.vml.es.aem.acm.core.util.YamlUtils; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class YamlFormatter { + + public T read(InputStream inputStream, Class clazz) throws IOException { + return YamlUtils.read(inputStream, clazz); + } + + public T readFromString(String json, Class clazz) throws IOException { + return YamlUtils.readFromString(json, clazz); + } + + public void write(OutputStream outputStream, Object data) throws IOException { + YamlUtils.write(outputStream, data); + } + + public String writeToString(Object data) throws IOException { + return YamlUtils.writeToString(data); + } +} diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockHttpFilter.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockHttpFilter.java index 1999840c..57b64e3d 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockHttpFilter.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockHttpFilter.java @@ -81,8 +81,8 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) MockScript mock = new MockScript(executionContext); if (mock.request(request)) { mock.respond(request, response); + return; } - return; } } } catch (MockException e) { diff --git a/core/src/main/java/com/vml/es/aem/acm/core/snippet/SnippetDefinition.java b/core/src/main/java/com/vml/es/aem/acm/core/snippet/SnippetDefinition.java index 10c1b0a2..7e366953 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/snippet/SnippetDefinition.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/snippet/SnippetDefinition.java @@ -20,7 +20,7 @@ public SnippetDefinition() { public static SnippetDefinition fromYaml(String path, InputStream inputStream) { try { - return YamlUtils.readYaml(inputStream, SnippetDefinition.class); + return YamlUtils.read(inputStream, SnippetDefinition.class); } catch (Exception e) { throw new IllegalArgumentException( String.format("Snippet definition at path '%s' cannot be parsed as YML!", path), e); diff --git a/core/src/main/java/com/vml/es/aem/acm/core/util/YamlUtils.java b/core/src/main/java/com/vml/es/aem/acm/core/util/YamlUtils.java index e44ec165..a81b6acc 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/util/YamlUtils.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/util/YamlUtils.java @@ -14,11 +14,19 @@ private YamlUtils() { // intentionally empty } - public static T readYaml(InputStream inputStream, Class clazz) throws IOException { + public static T read(InputStream inputStream, Class clazz) throws IOException { return YAML_MAPPER.readValue(inputStream, clazz); } - public static void writeYaml(OutputStream outputStream, Object data) throws IOException { + public static void write(OutputStream outputStream, Object data) throws IOException { YAML_MAPPER.writeValue(outputStream, data); } + + public static T readFromString(String yaml, Class clazz) throws IOException { + return YAML_MAPPER.readValue(yaml, clazz); + } + + public static String writeToString(Object data) throws IOException { + return YAML_MAPPER.writeValueAsString(data); + } } diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/example/hello-world.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/example/hello-world.groovy index f9b46da6..c610dd04 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/example/hello-world.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/example/hello-world.groovy @@ -1,5 +1,6 @@ import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse +import org.apache.commons.lang3.StringUtils; boolean request(HttpServletRequest request) { return request.requestURI == "/mock/hello-world" diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/example/odd-numbers.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/example/odd-numbers.groovy index 1ead2c07..03165a02 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/example/odd-numbers.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/mock/example/odd-numbers.groovy @@ -1,6 +1,6 @@ import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse - +import org.apache.commons.lang3.StringUtils; boolean request(HttpServletRequest request) { return request.getRequestURI() == "/mock/odd-numbers" @@ -12,9 +12,9 @@ void respond(HttpServletRequest request, HttpServletResponse response) { def numbers = (start..end).findAll { it % 2 != 0 } response.setContentType("application/json; charset=UTF-8") - response.getWriter().write(gson.toJson([ + formatter.json.write(response.outputStream, [ "start": start, "end": end, "result": numbers - ])) + ]) } \ No newline at end of file From 7d88cb208f99d09ddb29c5d502c99925ce756b01 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Tue, 13 May 2025 07:46:08 +0200 Subject: [PATCH 11/26] Mocks on script list --- Taskfile.yml | 6 ++- .../es/aem/acm/core/script/ScriptStats.java | 32 ++++++-------- .../es/aem/acm/core/script/ScriptType.java | 4 ++ ui.frontend/package.json | 2 +- .../src/components/CodeArgumentInput.tsx | 1 - ui.frontend/src/components/ScriptList.tsx | 2 +- ui.frontend/src/pages/ScriptView.tsx | 42 ++++++++++++++----- ui.frontend/src/pages/ScriptsPage.tsx | 8 ++++ .../utils/monaco/groovy/completions/core.ts | 4 +- ui.frontend/src/utils/monaco/java.d.ts | 2 + 10 files changed, 66 insertions(+), 37 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index 400fa689..e8d3a951 100755 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -253,10 +253,12 @@ tasks: - sh npmw install - sh npmw run dev - develop:frontend:format: + develop:frontend:fix: desc: format frontend code dir: ui.frontend - cmd: sh npmw run format + cmds: + - sh npmw run format + - sh npmw run lint release: desc: Release a new version diff --git a/core/src/main/java/com/vml/es/aem/acm/core/script/ScriptStats.java b/core/src/main/java/com/vml/es/aem/acm/core/script/ScriptStats.java index ac09c7c7..a08fd37c 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/script/ScriptStats.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/script/ScriptStats.java @@ -2,10 +2,7 @@ import com.vml.es.aem.acm.core.code.*; import java.io.Serializable; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import org.apache.sling.api.resource.ResourceResolver; @@ -29,6 +26,13 @@ public ScriptStats( } public static ScriptStats forCompletedByPath(ResourceResolver resourceResolver, String path, long limit) { + ScriptType scriptType = ScriptType.byPath(path) + .orElseThrow(() -> new IllegalStateException( + String.format("Script type for path '%s' cannot be determined!", path))); + if (!scriptType.statsSupported()) { + return new ScriptStats(path, Collections.emptyMap(), null, null); + } + AtomicReference lastExecution = new AtomicReference<>(); Map statusCount = ExecutionStatus.completed().stream().collect(HashMap::new, (m, s) -> m.put(s, 0L), HashMap::putAll); @@ -37,22 +41,10 @@ public static ScriptStats forCompletedByPath(ResourceResolver resourceResolver, ExecutionQuery query = new ExecutionQuery(); query.setStatuses(ExecutionStatus.completed()); - ScriptType scriptType = ScriptType.byPath(path) - .orElseThrow(() -> new IllegalStateException( - String.format("Script type for path '%s' cannot be determined!", path))); - switch (scriptType) { - case MANUAL: - case ENABLED: - query.setExecutableId(path); - break; - case DISABLED: - query.setExecutableId(ScriptType.ENABLED.enforcePath(path)); - break; - case EXTENSION: - return new ScriptStats(path, Collections.emptyMap(), null, null); - default: - throw new IllegalStateException(String.format( - "Script type '%s' for path '%s' is not supported to calculate stats!", scriptType, path)); + if (scriptType == ScriptType.MANUAL || scriptType == ScriptType.ENABLED) { + query.setExecutableId(path); + } else if (scriptType == ScriptType.DISABLED) { + query.setExecutableId(ScriptType.ENABLED.enforcePath(path)); } List executions = diff --git a/core/src/main/java/com/vml/es/aem/acm/core/script/ScriptType.java b/core/src/main/java/com/vml/es/aem/acm/core/script/ScriptType.java index 684217ff..7ef1f9c1 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/script/ScriptType.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/script/ScriptType.java @@ -37,6 +37,10 @@ public String enforcePath(String path) { return root + "/" + subPath; } + public boolean statsSupported() { + return this == MANUAL || this == ENABLED || this == DISABLED; + } + public String root() { return root; } diff --git a/ui.frontend/package.json b/ui.frontend/package.json index 6485d982..0080540c 100644 --- a/ui.frontend/package.json +++ b/ui.frontend/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", - "lint": "eslint .", + "lint": "eslint . --fix", "preview": "vite preview", "format": "prettier --write .", "postinstall": "patch-package" diff --git a/ui.frontend/src/components/CodeArgumentInput.tsx b/ui.frontend/src/components/CodeArgumentInput.tsx index 62db1a2a..7324f858 100644 --- a/ui.frontend/src/components/CodeArgumentInput.tsx +++ b/ui.frontend/src/components/CodeArgumentInput.tsx @@ -26,7 +26,6 @@ const CodeArgumentInput: React.FC = ({ arg }) => { } if (arg.validator) { try { - // eslint-disable-next-line no-eval const validator = eval(arg.validator); const allValues = getValues(); const errorMessage = validator(value, allValues); diff --git a/ui.frontend/src/components/ScriptList.tsx b/ui.frontend/src/components/ScriptList.tsx index 04949688..24f7994e 100644 --- a/ui.frontend/src/components/ScriptList.tsx +++ b/ui.frontend/src/components/ScriptList.tsx @@ -103,7 +103,7 @@ const ScriptList: React.FC = ({ type }) => { - {type === ScriptType.EXTENSION ? ( + {(type === ScriptType.EXTENSION || type === ScriptType.MOCK) ? ( navigate(`/scripts/view/${encodeURIComponent(key)}`)}> Name diff --git a/ui.frontend/src/pages/ScriptView.tsx b/ui.frontend/src/pages/ScriptView.tsx index dcf7fd51..e48c708b 100644 --- a/ui.frontend/src/pages/ScriptView.tsx +++ b/ui.frontend/src/pages/ScriptView.tsx @@ -1,18 +1,40 @@ -import { Button, ButtonGroup, Content, Flex, IllustratedMessage, Item, LabeledValue, ProgressBar, TabList, TabPanels, Tabs, Text, View } from '@adobe/react-spectrum'; -import { Field } from '@react-spectrum/label'; -import { ToastQueue } from '@react-spectrum/toast'; +import { + Button, + ButtonGroup, + Content, + Flex, + IllustratedMessage, + Item, + LabeledValue, + ProgressBar, + TabList, + TabPanels, + Tabs, + Text, + View +} from '@adobe/react-spectrum'; +import {Field} from '@react-spectrum/label'; +import {ToastQueue} from '@react-spectrum/toast'; import NotFound from '@spectrum-icons/illustrations/NotFound'; import Copy from '@spectrum-icons/workflow/Copy'; import FileCode from '@spectrum-icons/workflow/FileCode'; import History from '@spectrum-icons/workflow/History'; -import { useEffect, useState } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; +import {useEffect, useState} from 'react'; +import {useNavigate, useParams} from 'react-router-dom'; import CodeExecuteButton from '../components/CodeExecuteButton.tsx'; import ImmersiveEditor from '../components/ImmersiveEditor.tsx'; -import { NavigationSearchParams, useNavigationTab } from '../hooks/navigation.ts'; -import { toastRequest } from '../utils/api'; -import { ArgumentValues, Description, ExecutionQueryParams, QueueOutput, Script, ScriptOutput } from '../utils/api.types'; -import { Urls } from '../utils/url.ts'; +import {NavigationSearchParams, useNavigationTab} from '../hooks/navigation.ts'; +import {toastRequest} from '../utils/api'; +import { + ArgumentValues, + Description, + ExecutionQueryParams, + QueueOutput, + Script, + ScriptOutput, + ScriptType +} from '../utils/api.types'; +import {Urls} from '../utils/url.ts'; const toastTimeout = 3000; @@ -134,7 +156,7 @@ const ScriptView = () => { - {script.type !== 'EXTENSION' && ( + {(script.type !== ScriptType.EXTENSION && script.type !== ScriptType.MOCK) && ( - - - - by {lastExecution.userId} - - ) : ( - - )} - - - - {lastExecution ? formatter.duration(scriptStats.averageDuration) : <>—} - - - - - - ); - })} - - - )} - - ); -}; - -export default ScriptList; diff --git a/ui.frontend/src/components/ScriptListRich.tsx b/ui.frontend/src/components/ScriptListRich.tsx new file mode 100644 index 00000000..f4dab618 --- /dev/null +++ b/ui.frontend/src/components/ScriptListRich.tsx @@ -0,0 +1,172 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { Button, Cell, Column, ContextualHelp, Flex, ProgressBar, Heading, ButtonGroup, Row, StatusLight, TableBody, TableHeader, TableView, Text, View, IllustratedMessage, Content, Link } from '@adobe/react-spectrum'; +import Magnify from '@spectrum-icons/workflow/Magnify'; +import NotFound from '@spectrum-icons/illustrations/NotFound'; +import { useNavigate } from 'react-router-dom'; +import { useAppState } from '../hooks/app'; +import { useFormatter } from '../hooks/formatter'; +import { toastRequest } from '../utils/api'; +import { InstanceRole, isExecutionNegative, ScriptOutput, ScriptType } from '../utils/api.types'; +import DateExplained from './DateExplained'; +import ExecutionStatsBadge from './ExecutionStatsBadge'; +import ScriptSynchronizeButton from './ScriptSynchronizeButton'; +import ScriptToggleButton from './ScriptToggleButton'; +import ScriptsAutomaticHelpButton from './ScriptsAutomaticHelpButton'; +import ScriptsManualHelpButton from './ScriptsManualHelpButton'; +import { Key, Selection } from '@react-types/shared'; + +type ScriptListRichProps = { + type: ScriptType; +}; + +const ScriptListRich: React.FC = ({ type }) => { + const appState = useAppState(); + const navigate = useNavigate(); + const formatter = useFormatter(); + + const [scripts, setScripts] = useState(null); + const [loading, setLoading] = useState(true); + const [selectedKeys, setSelectedKeys] = useState(new Set()); + + const loadScripts = useCallback(() => { + setLoading(true); + toastRequest({ + method: 'GET', + url: `/apps/acm/api/script.json?type=${type}`, + operation: `Scripts loading (${type.toString().toLowerCase()})`, + positive: false, + }) + .then((data) => setScripts(data.data.data)) + .catch((error) => console.error(`Scripts loading (${type}) error:`, error)) + .finally(() => setLoading(false)); + }, [type]); + + useEffect(() => { + loadScripts(); + }, [type, loadScripts]); + + const selectedIds = (selectedKeys: Selection): string[] => { + if (selectedKeys === 'all') { + return scripts?.list.map((script) => script.id) || []; + } else { + return Array.from(selectedKeys as Set).map((key) => key.toString()); + } + }; + + const renderEmptyState = () => ( + + + No scripts found + + ); + + if (scripts === null || loading) { + return ( + + + + ); + } + + return ( + + + + + + {type === ScriptType.ENABLED || type === ScriptType.DISABLED ? ( + <> + + {appState.instanceSettings.role == InstanceRole.AUTHOR && } + + ) : null} + + + + + {appState.healthStatus.healthy ? ( + Executor active + ) : ( + <> + Executor paused +  —  + navigate('/maintenance?tab=health-checker')}> + See health issues + + + )} + + + + {type === ScriptType.MANUAL ? : } + + + + navigate(`/scripts/view/${encodeURIComponent(key)}`)} + > + + Name + Last Execution + + Average Duration + + Explanation + Duration is calculated based on the last {appState.spaSettings.scriptStatsLimit} completed executions (only succeeded or failed). + + + + Success Rate + + Explanation + Success rate is calculated based on the last {appState.spaSettings.scriptStatsLimit} completed executions (only succeeded or failed). + + + + + {(scripts.list || []).map((script) => { + const scriptStats = scripts.stats.find((stat) => stat.path === script.id)!; + const lastExecution = scriptStats?.lastExecution; + + return ( + + {script.name} + + + {lastExecution ? ( + <> + + + + + by {lastExecution.userId} + + ) : ( + + )} + + + + {lastExecution ? formatter.duration(scriptStats.averageDuration) : <>—} + + + + + + ); + })} + + + + ); +}; + +export default ScriptListRich; \ No newline at end of file diff --git a/ui.frontend/src/components/ScriptListSimple.tsx b/ui.frontend/src/components/ScriptListSimple.tsx new file mode 100644 index 00000000..d1f85c1e --- /dev/null +++ b/ui.frontend/src/components/ScriptListSimple.tsx @@ -0,0 +1,65 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { Flex, ProgressBar, TableBody, TableHeader, TableView, IllustratedMessage, Content, Row, Cell, Column } from '@adobe/react-spectrum'; +import NotFound from '@spectrum-icons/illustrations/NotFound'; +import { useNavigate } from 'react-router-dom'; +import { toastRequest } from '../utils/api'; +import { ScriptOutput, ScriptType } from '../utils/api.types'; + +type ScriptListSimpleProps = { + type: ScriptType; +}; + +const ScriptListSimple: React.FC = ({ type }) => { + const [scripts, setScripts] = useState(null); + const [loading, setLoading] = useState(true); + const navigate = useNavigate(); + + const loadScripts = useCallback(() => { + setLoading(true); + toastRequest({ + method: 'GET', + url: `/apps/acm/api/script.json?type=${type}`, + operation: `Scripts loading (${type.toString().toLowerCase()})`, + positive: false, + }) + .then((data) => setScripts(data.data.data)) + .catch((error) => console.error(`Scripts loading (${type}) error:`, error)) + .finally(() => setLoading(false)); + }, [type]); + + useEffect(() => { + loadScripts(); + }, [type, loadScripts]); + + const renderEmptyState = () => ( + + + No scripts found + + ); + + if (scripts === null || loading) { + return ( + + + + ); + } + + return ( + navigate(`/scripts/view/${encodeURIComponent(key)}`)}> + + Name + + + {(scripts.list || []).map((script) => ( + + {script.name} + + ))} + + + ); +}; + +export default ScriptListSimple; \ No newline at end of file diff --git a/ui.frontend/src/pages/ScriptsPage.module.css b/ui.frontend/src/pages/ScriptsPage.module.css new file mode 100644 index 00000000..c6b76486 --- /dev/null +++ b/ui.frontend/src/pages/ScriptsPage.module.css @@ -0,0 +1,14 @@ +.scriptTabs div[role="tab"]:nth-child(3) { + position: relative; +} + +.scriptTabs div[role="tab"]:nth-child(3)::after { + content: ''; + position: absolute; + right: -14px; + width: 1px; + height: 90%; + bottom: 0; + background-color: var(--spectrum-global-color-gray-300); + opacity: 0.75; +} \ No newline at end of file diff --git a/ui.frontend/src/pages/ScriptsPage.tsx b/ui.frontend/src/pages/ScriptsPage.tsx index 547b7f47..4a6a1b76 100644 --- a/ui.frontend/src/pages/ScriptsPage.tsx +++ b/ui.frontend/src/pages/ScriptsPage.tsx @@ -4,58 +4,60 @@ import Extension from '@spectrum-icons/workflow/Extension'; import FlashOn from '@spectrum-icons/workflow/FlashOn'; import Box from '@spectrum-icons/workflow/Box'; import Hand from '@spectrum-icons/workflow/Hand'; -import ScriptList from '../components/ScriptList'; +import ScriptListSimple from '../components/ScriptListSimple.tsx'; +import ScriptListRich from '../components/ScriptListRich.tsx'; import { useNavigationTab } from '../hooks/navigation'; import { ScriptType } from '../utils/api.types'; +import styles from './ScriptsPage.module.css'; const ScriptsPage = () => { const [selectedTab, handleTabChange] = useNavigationTab('manual'); return ( - - - - - - Manual - - - - Automatic - - - - Disabled - - - - Mock - - - - Extension - - - - - - - - - - - - - - - - - - - - - + + + + + + Manual + + + + Automatic + + + + Disabled + + + + Mock + + + + Extension + + + + + + + + + + + + + + + + + + + + + ); }; -export default ScriptsPage; +export default ScriptsPage; \ No newline at end of file From bd28d7d77f2840f30fc4b9a0af9d1488566870ef Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Tue, 13 May 2025 12:03:36 +0200 Subject: [PATCH 13/26] Mock list --- .../es/aem/acm/core/mock/MockHttpFilter.java | 6 +- .../vml/es/aem/acm/core/mock/MockStatus.java | 16 + .../es/aem/acm/core/servlet/StateServlet.java | 7 +- .../com/vml/es/aem/acm/core/state/State.java | 5 + ui.frontend/src/App.tsx | 3 + ui.frontend/src/components/HealthChecker.tsx | 10 +- ui.frontend/src/components/ScriptListRich.tsx | 288 +++++++++--------- .../src/components/ScriptListSimple.tsx | 129 +++++--- ui.frontend/src/pages/ScriptView.tsx | 42 +-- ui.frontend/src/pages/ScriptsPage.module.css | 24 +- ui.frontend/src/pages/ScriptsPage.tsx | 92 +++--- ui.frontend/src/utils/api.types.ts | 9 + 12 files changed, 342 insertions(+), 289 deletions(-) create mode 100644 core/src/main/java/com/vml/es/aem/acm/core/mock/MockStatus.java diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockHttpFilter.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockHttpFilter.java index 57b64e3d..83874efd 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockHttpFilter.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockHttpFilter.java @@ -23,7 +23,7 @@ import org.slf4j.LoggerFactory; @Component( - service = Filter.class, + service = {Filter.class, MockHttpFilter.class}, property = { HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT + "=(" + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME + "=*)" @@ -38,6 +38,10 @@ public class MockHttpFilter implements Filter { private Config config; + public MockStatus getMockStatus() { + return new MockStatus(config.enabled()); + } + @ObjectClassDefinition(name = "AEM Content Manager - Mock HTTP Filter") public @interface Config { diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockStatus.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockStatus.java new file mode 100644 index 00000000..65059020 --- /dev/null +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockStatus.java @@ -0,0 +1,16 @@ +package com.vml.es.aem.acm.core.mock; + +import java.io.Serializable; + +public class MockStatus implements Serializable { + + private final boolean enabled; + + public MockStatus(boolean enabled) { + this.enabled = enabled; + } + + public boolean isEnabled() { + return enabled; + } +} diff --git a/core/src/main/java/com/vml/es/aem/acm/core/servlet/StateServlet.java b/core/src/main/java/com/vml/es/aem/acm/core/servlet/StateServlet.java index 3c75938f..38ffe7d0 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/servlet/StateServlet.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/servlet/StateServlet.java @@ -9,6 +9,7 @@ import com.vml.es.aem.acm.core.gui.SpaSettings; import com.vml.es.aem.acm.core.instance.HealthChecker; import com.vml.es.aem.acm.core.instance.HealthStatus; +import com.vml.es.aem.acm.core.mock.MockHttpFilter; import com.vml.es.aem.acm.core.osgi.InstanceInfo; import com.vml.es.aem.acm.core.state.State; import java.io.IOException; @@ -46,6 +47,9 @@ public class StateServlet extends SlingAllMethodsServlet { @Reference private HealthChecker healthChecker; + @Reference + private MockHttpFilter mockHttpFilter; + @Reference private SpaSettings spaSettings; @@ -53,9 +57,10 @@ public class StateServlet extends SlingAllMethodsServlet { protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException { try { HealthStatus healthStatus = healthChecker.checkStatus(); + MockStatus mockStatus = mockHttpFilter.getMockStatus(); List queuedExecutions = executionQueue.findAllSummaries().collect(Collectors.toList()); - State state = new State(spaSettings, healthStatus, instanceInfo.getInstanceSettings(), queuedExecutions); + State state = new State(spaSettings, healthStatus, mockStatus, instanceInfo.getInstanceSettings(), queuedExecutions); respondJson(response, ok("State read successfully", state)); } catch (Exception e) { diff --git a/core/src/main/java/com/vml/es/aem/acm/core/state/State.java b/core/src/main/java/com/vml/es/aem/acm/core/state/State.java index 4a23061a..787a5852 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/state/State.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/state/State.java @@ -11,6 +11,8 @@ public class State implements Serializable { private final HealthStatus healthStatus; + private final MockStatus mockStatus; + private final InstanceSettings instanceSettings; private final List queuedExecutions; @@ -20,12 +22,15 @@ public class State implements Serializable { public State( SpaSettings spaSettings, HealthStatus healthStatus, + MockStatus mockStatus, InstanceSettings instanceSettings, List queuedExecutions) { this.spaSettings = spaSettings; this.healthStatus = healthStatus; + this.mockStatus = mockStatus; this.instanceSettings = instanceSettings; this.queuedExecutions = queuedExecutions; + } public HealthStatus getHealthStatus() { diff --git a/ui.frontend/src/App.tsx b/ui.frontend/src/App.tsx index 31ff2bf7..4d2b6065 100644 --- a/ui.frontend/src/App.tsx +++ b/ui.frontend/src/App.tsx @@ -22,6 +22,9 @@ function App() { healthy: true, issues: [], }, + mockStatus: { + enabled: true, + }, instanceSettings: { id: 'default', timezoneId: 'UTC', diff --git a/ui.frontend/src/components/HealthChecker.tsx b/ui.frontend/src/components/HealthChecker.tsx index 1a5dc8c7..92c12cd9 100644 --- a/ui.frontend/src/components/HealthChecker.tsx +++ b/ui.frontend/src/components/HealthChecker.tsx @@ -6,13 +6,11 @@ import Help from '@spectrum-icons/workflow/Help'; import Replay from '@spectrum-icons/workflow/Replay'; import Settings from '@spectrum-icons/workflow/Settings'; import { useAppState } from '../hooks/app.ts'; -import { HealthIssueSeverity, InstanceType } from '../utils/api.types'; -import { isProduction } from '../utils/node.ts'; +import { HealthIssueSeverity, instancePrefix, InstanceType } from '../utils/api.types'; const HealthChecker = () => { const appState = useAppState(); const healthIssues = appState.healthStatus.issues; - const prefix = isProduction() ? '' : 'http://localhost:4502'; const getSeverityVariant = (severity: HealthIssueSeverity): 'negative' | 'yellow' | 'neutral' => { switch (severity) { @@ -37,7 +35,11 @@ const HealthChecker = () => { - diff --git a/ui.frontend/src/components/ScriptListRich.tsx b/ui.frontend/src/components/ScriptListRich.tsx index f4dab618..629b441b 100644 --- a/ui.frontend/src/components/ScriptListRich.tsx +++ b/ui.frontend/src/components/ScriptListRich.tsx @@ -1,7 +1,8 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { Button, Cell, Column, ContextualHelp, Flex, ProgressBar, Heading, ButtonGroup, Row, StatusLight, TableBody, TableHeader, TableView, Text, View, IllustratedMessage, Content, Link } from '@adobe/react-spectrum'; -import Magnify from '@spectrum-icons/workflow/Magnify'; +import { Button, ButtonGroup, Cell, Column, Content, ContextualHelp, Flex, Heading, IllustratedMessage, Link, ProgressBar, Row, StatusLight, TableBody, TableHeader, TableView, Text, View } from '@adobe/react-spectrum'; +import { Key, Selection } from '@react-types/shared'; import NotFound from '@spectrum-icons/illustrations/NotFound'; +import Magnify from '@spectrum-icons/workflow/Magnify'; +import React, { useCallback, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useAppState } from '../hooks/app'; import { useFormatter } from '../hooks/formatter'; @@ -13,160 +14,159 @@ import ScriptSynchronizeButton from './ScriptSynchronizeButton'; import ScriptToggleButton from './ScriptToggleButton'; import ScriptsAutomaticHelpButton from './ScriptsAutomaticHelpButton'; import ScriptsManualHelpButton from './ScriptsManualHelpButton'; -import { Key, Selection } from '@react-types/shared'; type ScriptListRichProps = { - type: ScriptType; + type: ScriptType; }; const ScriptListRich: React.FC = ({ type }) => { - const appState = useAppState(); - const navigate = useNavigate(); - const formatter = useFormatter(); - - const [scripts, setScripts] = useState(null); - const [loading, setLoading] = useState(true); - const [selectedKeys, setSelectedKeys] = useState(new Set()); + const appState = useAppState(); + const navigate = useNavigate(); + const formatter = useFormatter(); - const loadScripts = useCallback(() => { - setLoading(true); - toastRequest({ - method: 'GET', - url: `/apps/acm/api/script.json?type=${type}`, - operation: `Scripts loading (${type.toString().toLowerCase()})`, - positive: false, - }) - .then((data) => setScripts(data.data.data)) - .catch((error) => console.error(`Scripts loading (${type}) error:`, error)) - .finally(() => setLoading(false)); - }, [type]); + const [scripts, setScripts] = useState(null); + const [loading, setLoading] = useState(true); + const [selectedKeys, setSelectedKeys] = useState(new Set()); - useEffect(() => { - loadScripts(); - }, [type, loadScripts]); + const loadScripts = useCallback(() => { + setLoading(true); + toastRequest({ + method: 'GET', + url: `/apps/acm/api/script.json?type=${type}`, + operation: `Scripts loading (${type.toString().toLowerCase()})`, + positive: false, + }) + .then((data) => setScripts(data.data.data)) + .catch((error) => console.error(`Scripts loading (${type}) error:`, error)) + .finally(() => setLoading(false)); + }, [type]); - const selectedIds = (selectedKeys: Selection): string[] => { - if (selectedKeys === 'all') { - return scripts?.list.map((script) => script.id) || []; - } else { - return Array.from(selectedKeys as Set).map((key) => key.toString()); - } - }; + useEffect(() => { + loadScripts(); + }, [type, loadScripts]); - const renderEmptyState = () => ( - - - No scripts found - - ); - - if (scripts === null || loading) { - return ( - - - - ); + const selectedIds = (selectedKeys: Selection): string[] => { + if (selectedKeys === 'all') { + return scripts?.list.map((script) => script.id) || []; + } else { + return Array.from(selectedKeys as Set).map((key) => key.toString()); } + }; + + const renderEmptyState = () => ( + + + No scripts found + + ); + if (scripts === null || loading) { return ( - - - - - - {type === ScriptType.ENABLED || type === ScriptType.DISABLED ? ( - <> - - {appState.instanceSettings.role == InstanceRole.AUTHOR && } - - ) : null} - - - - - {appState.healthStatus.healthy ? ( - Executor active - ) : ( - <> - Executor paused -  —  - navigate('/maintenance?tab=health-checker')}> - See health issues - - - )} - - - - {type === ScriptType.MANUAL ? : } - - - - navigate(`/scripts/view/${encodeURIComponent(key)}`)} - > - - Name - Last Execution - - Average Duration - - Explanation - Duration is calculated based on the last {appState.spaSettings.scriptStatsLimit} completed executions (only succeeded or failed). - - - - Success Rate - - Explanation - Success rate is calculated based on the last {appState.spaSettings.scriptStatsLimit} completed executions (only succeeded or failed). - - - - - {(scripts.list || []).map((script) => { - const scriptStats = scripts.stats.find((stat) => stat.path === script.id)!; - const lastExecution = scriptStats?.lastExecution; + + + + ); + } - return ( - - {script.name} - - - {lastExecution ? ( - <> - - - - - by {lastExecution.userId} - - ) : ( - - )} - - - - {lastExecution ? formatter.duration(scriptStats.averageDuration) : <>—} - - - - - - ); - })} - - + return ( + + + + + + {type === ScriptType.ENABLED || type === ScriptType.DISABLED ? ( + <> + + {appState.instanceSettings.role == InstanceRole.AUTHOR && } + + ) : null} + + + + + {appState.healthStatus.healthy ? ( + Executor active + ) : ( + <> + Executor paused +  —  + navigate('/maintenance?tab=health-checker')}> + See health issues + + + )} + + + + {type === ScriptType.MANUAL ? : } + - ); + + navigate(`/scripts/view/${encodeURIComponent(key)}`)} + > + + Name + Last Execution + + Average Duration + + Explanation + Duration is calculated based on the last {appState.spaSettings.scriptStatsLimit} completed executions (only succeeded or failed). + + + + Success Rate + + Explanation + Success rate is calculated based on the last {appState.spaSettings.scriptStatsLimit} completed executions (only succeeded or failed). + + + + + {(scripts.list || []).map((script) => { + const scriptStats = scripts.stats.find((stat) => stat.path === script.id)!; + const lastExecution = scriptStats?.lastExecution; + + return ( + + {script.name} + + + {lastExecution ? ( + <> + + + + + by {lastExecution.userId} + + ) : ( + + )} + + + + {lastExecution ? formatter.duration(scriptStats.averageDuration) : <>—} + + + + + + ); + })} + + + + ); }; -export default ScriptListRich; \ No newline at end of file +export default ScriptListRich; diff --git a/ui.frontend/src/components/ScriptListSimple.tsx b/ui.frontend/src/components/ScriptListSimple.tsx index d1f85c1e..39a57934 100644 --- a/ui.frontend/src/components/ScriptListSimple.tsx +++ b/ui.frontend/src/components/ScriptListSimple.tsx @@ -1,65 +1,96 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { Flex, ProgressBar, TableBody, TableHeader, TableView, IllustratedMessage, Content, Row, Cell, Column } from '@adobe/react-spectrum'; +import { Button, ButtonGroup, Cell, Column, Content, Flex, IllustratedMessage, ProgressBar, Row, StatusLight, TableBody, TableHeader, TableView, Text, View } from '@adobe/react-spectrum'; import NotFound from '@spectrum-icons/illustrations/NotFound'; +import Settings from '@spectrum-icons/workflow/Settings'; +import React, { useCallback, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; +import { useAppState } from '../hooks/app.ts'; import { toastRequest } from '../utils/api'; -import { ScriptOutput, ScriptType } from '../utils/api.types'; +import { InstanceType, ScriptOutput, ScriptType, instancePrefix } from '../utils/api.types'; +import ScriptsAutomaticHelpButton from './ScriptsAutomaticHelpButton.tsx'; +import ScriptsManualHelpButton from './ScriptsManualHelpButton.tsx'; type ScriptListSimpleProps = { - type: ScriptType; + type: ScriptType; }; const ScriptListSimple: React.FC = ({ type }) => { - const [scripts, setScripts] = useState(null); - const [loading, setLoading] = useState(true); - const navigate = useNavigate(); + const [scripts, setScripts] = useState(null); + const [loading, setLoading] = useState(true); + const navigate = useNavigate(); + const appState = useAppState(); - const loadScripts = useCallback(() => { - setLoading(true); - toastRequest({ - method: 'GET', - url: `/apps/acm/api/script.json?type=${type}`, - operation: `Scripts loading (${type.toString().toLowerCase()})`, - positive: false, - }) - .then((data) => setScripts(data.data.data)) - .catch((error) => console.error(`Scripts loading (${type}) error:`, error)) - .finally(() => setLoading(false)); - }, [type]); + const loadScripts = useCallback(() => { + setLoading(true); + toastRequest({ + method: 'GET', + url: `/apps/acm/api/script.json?type=${type}`, + operation: `Scripts loading (${type.toString().toLowerCase()})`, + positive: false, + }) + .then((data) => setScripts(data.data.data)) + .catch((error) => console.error(`Scripts loading (${type}) error:`, error)) + .finally(() => setLoading(false)); + }, [type]); - useEffect(() => { - loadScripts(); - }, [type, loadScripts]); + useEffect(() => { + loadScripts(); + }, [type, loadScripts]); - const renderEmptyState = () => ( - - - No scripts found - - ); - - if (scripts === null || loading) { - return ( - - - - ); - } + const renderEmptyState = () => ( + + + No scripts found + + ); + if (scripts === null || loading) { return ( - navigate(`/scripts/view/${encodeURIComponent(key)}`)}> - - Name - - - {(scripts.list || []).map((script) => ( - - {script.name} - - ))} - - + + + ); + } + + return ( + + + + + + {type === ScriptType.MOCK ? ( + + ) : null} + + + + {appState.mockStatus.enabled ? Mocks enabled : Mocks disabled} + + + {type === ScriptType.MANUAL ? : } + + + + navigate(`/scripts/view/${encodeURIComponent(key)}`)}> + + Name + + + {(scripts.list || []).map((script) => ( + + {script.name} + + ))} + + + + ); }; -export default ScriptListSimple; \ No newline at end of file +export default ScriptListSimple; diff --git a/ui.frontend/src/pages/ScriptView.tsx b/ui.frontend/src/pages/ScriptView.tsx index e48c708b..2a275585 100644 --- a/ui.frontend/src/pages/ScriptView.tsx +++ b/ui.frontend/src/pages/ScriptView.tsx @@ -1,40 +1,18 @@ -import { - Button, - ButtonGroup, - Content, - Flex, - IllustratedMessage, - Item, - LabeledValue, - ProgressBar, - TabList, - TabPanels, - Tabs, - Text, - View -} from '@adobe/react-spectrum'; -import {Field} from '@react-spectrum/label'; -import {ToastQueue} from '@react-spectrum/toast'; +import { Button, ButtonGroup, Content, Flex, IllustratedMessage, Item, LabeledValue, ProgressBar, TabList, TabPanels, Tabs, Text, View } from '@adobe/react-spectrum'; +import { Field } from '@react-spectrum/label'; +import { ToastQueue } from '@react-spectrum/toast'; import NotFound from '@spectrum-icons/illustrations/NotFound'; import Copy from '@spectrum-icons/workflow/Copy'; import FileCode from '@spectrum-icons/workflow/FileCode'; import History from '@spectrum-icons/workflow/History'; -import {useEffect, useState} from 'react'; -import {useNavigate, useParams} from 'react-router-dom'; +import { useEffect, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; import CodeExecuteButton from '../components/CodeExecuteButton.tsx'; import ImmersiveEditor from '../components/ImmersiveEditor.tsx'; -import {NavigationSearchParams, useNavigationTab} from '../hooks/navigation.ts'; -import {toastRequest} from '../utils/api'; -import { - ArgumentValues, - Description, - ExecutionQueryParams, - QueueOutput, - Script, - ScriptOutput, - ScriptType -} from '../utils/api.types'; -import {Urls} from '../utils/url.ts'; +import { NavigationSearchParams, useNavigationTab } from '../hooks/navigation.ts'; +import { toastRequest } from '../utils/api'; +import { ArgumentValues, Description, ExecutionQueryParams, QueueOutput, Script, ScriptOutput, ScriptType } from '../utils/api.types'; +import { Urls } from '../utils/url.ts'; const toastTimeout = 3000; @@ -156,7 +134,7 @@ const ScriptView = () => { - {(script.type !== ScriptType.EXTENSION && script.type !== ScriptType.MOCK) && ( + {script.type !== ScriptType.EXTENSION && script.type !== ScriptType.MOCK && ( {(close) => ( - Extension Script Capabilities + Extension Scripts

- Extension scripts allow you to define additional variables and utilities that can be used during script execution. This enables advanced customization and integration with project-specific logic. + Allows to define additional variables and utilities that can be used during script execution. This enables advanced customization and integration with project-specific logic.

These scripts can also handle post-execution operations, such as sending direct messages, emails, or other notifications in case of script failures. This ensures that critical issues are promptly diff --git a/ui.frontend/src/components/ScriptsMockHelpButton.tsx b/ui.frontend/src/components/ScriptsMockHelpButton.tsx new file mode 100644 index 00000000..e35f9137 --- /dev/null +++ b/ui.frontend/src/components/ScriptsMockHelpButton.tsx @@ -0,0 +1,41 @@ +import { Button, ButtonGroup, Content, Dialog, DialogTrigger, Divider, Heading, Text } from '@adobe/react-spectrum'; +import Checkmark from '@spectrum-icons/workflow/Checkmark'; +import Close from '@spectrum-icons/workflow/Close'; +import Help from '@spectrum-icons/workflow/Help'; +import Box from '@spectrum-icons/workflow/Box'; +import Notification from '@spectrum-icons/workflow/Messenger'; +import React from 'react'; + +const ScriptsMockHelpButton: React.FC = () => ( + + + {(close) => ( +

+ Mock Scripts + + +

+ Dedicated to simulate responses from 3rd-party systems during development and testing. They allow you to define static or generate on-the-fly responses for specific request paths. +

+

+ These scripts are matched to requests based on the filter's configured regular expressions. Only requests that pass the filter are evaluated by the mock scripts request() method. If some mock script is matched to the request, its response() method is called to generate the response. +

+

+ Mock scripts can handle various scenarios, including success responses, error cases, and edge conditions, ensuring robust testing coverage. +

+
+ + + +
+ )} + +); + +export default ScriptsMockHelpButton; \ No newline at end of file From d4569c05714897282efb1619005acf959a40bb02 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Tue, 13 May 2025 18:11:06 +0200 Subject: [PATCH 18/26] Minor --- .../main/java/com/vml/es/aem/acm/core/mock/MockHttpFilter.java | 3 +-- .../conf/acm/settings/snippet/available/core/repo/traverse.yml | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockHttpFilter.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockHttpFilter.java index 2f80b643..771936a2 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockHttpFilter.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockHttpFilter.java @@ -145,8 +145,7 @@ public void destroy() { @AttributeDefinition( name = "Whiteboard Filter Regex", - description = - "Expressions which narrow down the scope of requests that mock scripts can evaluate. Mock scripts then determine their ability to handle requests within this filtered scope.") + description = "Expressions which define the scope of requests that mock scripts can evaluate.") String[] osgi_http_whiteboard_filter_regex() default {"/mock/.*"}; } } diff --git a/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/repo/traverse.yml b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/repo/traverse.yml index 2e46bbcf..b263efe9 100644 --- a/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/repo/traverse.yml +++ b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/repo/traverse.yml @@ -3,7 +3,7 @@ name: repo_traverse content: | repo.get("${1:path}").traverse(${2:includeSelf}) documentation: | - Travers resources under the given path in the repository.
+ Traverse resources under the given path in the repository.
The result is a stream of resources that match the query.
To skip the current resource, use the `includeSelf` parameter by setting it to `false`.
From 9d509f6d6b50e3a51bf555b1cd73953464a2ff59 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Wed, 14 May 2025 07:22:42 +0200 Subject: [PATCH 19/26] Code context --- .../es/aem/acm/core/assist/Assistancer.java | 2 +- .../vml/es/aem/acm/core/code/CodeContext.java | 90 +++++++++++++++++++ .../vml/es/aem/acm/core/code/Condition.java | 43 +++++---- .../aem/acm/core/code/ExecutionContext.java | 84 ++++------------- .../vml/es/aem/acm/core/code/Executor.java | 9 +- .../vml/es/aem/acm/core/code/Extender.java | 34 ------- .../acm/core/code/HistoricalExecution.java | 5 +- .../aem/acm/core/code/ImmediateExecution.java | 2 +- .../acm/core/code/script/ContentScript.java | 2 +- .../core/code/script/ContentScriptSyntax.java | 2 +- .../acm/core/code/script/ExtensionScript.java | 38 +++++--- .../code/script/ExtensionScriptSyntax.java | 5 +- .../aem/acm/core/code/script/MockContext.java | 5 -- .../aem/acm/core/code/script/MockScript.java | 30 +++---- .../com/vml/es/aem/acm/core/mock/Mock.java | 21 +++-- .../vml/es/aem/acm/core/mock/MockContext.java | 39 ++++++++ .../es/aem/acm/core/mock/MockHttpFilter.java | 47 +++++----- ...iptRepository.java => MockRepository.java} | 16 ++-- .../acm/core/mock/MockScriptExecutable.java | 31 ------- .../example/ACME-300_extension.groovy | 5 ++ .../available/core/condition/is_date.yml | 26 +++--- ui.frontend/src/ErrorHandler.tsx | 4 +- 22 files changed, 295 insertions(+), 245 deletions(-) create mode 100644 core/src/main/java/com/vml/es/aem/acm/core/code/CodeContext.java delete mode 100644 core/src/main/java/com/vml/es/aem/acm/core/code/Extender.java delete mode 100644 core/src/main/java/com/vml/es/aem/acm/core/code/script/MockContext.java create mode 100644 core/src/main/java/com/vml/es/aem/acm/core/mock/MockContext.java rename core/src/main/java/com/vml/es/aem/acm/core/mock/{MockScriptRepository.java => MockRepository.java} (78%) delete mode 100644 core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptExecutable.java diff --git a/core/src/main/java/com/vml/es/aem/acm/core/assist/Assistancer.java b/core/src/main/java/com/vml/es/aem/acm/core/assist/Assistancer.java index 1f6c957e..8576a13e 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/assist/Assistancer.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/assist/Assistancer.java @@ -152,7 +152,7 @@ private synchronized void maybeUpdateVariablesCache(ResourceResolver resolver) { LOG.info("Variables cache - updating"); try (ExecutionContext context = executor.createContext( ExecutionId.generate(), ExecutionMode.PARSE, Code.consoleMinimal(), resolver)) { - variablesCache = context.getBindingVariables(); + variablesCache = context.getCodeContext().getBindingVariables(); variablesCacheTimestamp = currentTime; } LOG.info("Variables cache - updated"); diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/CodeContext.java b/core/src/main/java/com/vml/es/aem/acm/core/code/CodeContext.java new file mode 100644 index 00000000..505bc70c --- /dev/null +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/CodeContext.java @@ -0,0 +1,90 @@ +package com.vml.es.aem.acm.core.code; + +import com.vml.es.aem.acm.core.acl.Acl; +import com.vml.es.aem.acm.core.code.script.ExtensionScript; +import com.vml.es.aem.acm.core.format.Formatter; +import com.vml.es.aem.acm.core.mock.MockContext; +import com.vml.es.aem.acm.core.osgi.OsgiContext; +import com.vml.es.aem.acm.core.replication.Activator; +import com.vml.es.aem.acm.core.repo.Repo; +import com.vml.es.aem.acm.core.script.ScriptRepository; +import com.vml.es.aem.acm.core.script.ScriptType; +import groovy.lang.Binding; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.apache.sling.api.resource.ResourceResolver; +import org.slf4j.LoggerFactory; + +public class CodeContext { + + private final OsgiContext osgiContext; + + private final ResourceResolver resourceResolver; + + private final Binding binding; + + private final List scripts; + + public CodeContext(OsgiContext osgiContext, ResourceResolver resourceResolver) { + this.osgiContext = osgiContext; + this.resourceResolver = resourceResolver; + this.binding = createBinding(osgiContext, resourceResolver); + this.scripts = new ScriptRepository(resourceResolver) + .findAll(ScriptType.EXTENSION) + .map(s -> new ExtensionScript(this, s)) + .collect(Collectors.toList()); + } + + public void prepareRun(ExecutionContext executionContext) { + for (ExtensionScript script : scripts) { + script.prepareRun(executionContext); + } + } + + public void completeRun(Execution execution) { + for (ExtensionScript script : scripts) { + script.completeRun(execution); + } + } + + public void prepareMock(MockContext mockContext) { + for (ExtensionScript script : scripts) { + script.prepareMock(mockContext); + } + } + + public Binding createBinding(OsgiContext osgiContext, ResourceResolver resourceResolver) { + Binding result = new Binding(); + + result.setVariable("log", LoggerFactory.getLogger(getClass())); + result.setVariable("resourceResolver", resourceResolver); + result.setVariable("osgi", osgiContext); + result.setVariable("repo", new Repo(resourceResolver)); + result.setVariable("acl", new Acl(resourceResolver)); + result.setVariable("formatter", new Formatter()); + result.setVariable("activator", new Activator(resourceResolver, osgiContext.getReplicator())); + + return result; + } + + public Binding getBinding() { + return binding; + } + + @SuppressWarnings("unchecked") + public List getBindingVariables() { + Map variables = binding.getVariables(); + return variables.entrySet().stream() + .map(entry -> new Variable(entry.getKey(), entry.getValue().getClass())) + .collect(Collectors.toList()); + } + + public ResourceResolver getResourceResolver() { + return resourceResolver; + } + + public OsgiContext getOsgiContext() { + return osgiContext; + } +} diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/Condition.java b/core/src/main/java/com/vml/es/aem/acm/core/code/Condition.java index 998a94f3..9f4163e2 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/Condition.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/Condition.java @@ -1,5 +1,6 @@ package com.vml.es.aem.acm.core.code; +import com.vml.es.aem.acm.core.osgi.InstanceInfo; import com.vml.es.aem.acm.core.osgi.InstanceType; import com.vml.es.aem.acm.core.script.ScriptScheduler; import com.vml.es.aem.acm.core.util.DateUtils; @@ -17,7 +18,8 @@ public class Condition { public Condition(ExecutionContext executionContext) { this.executionContext = executionContext; - this.executionHistory = new ExecutionHistory(executionContext.getResourceResolver()); + this.executionHistory = + new ExecutionHistory(executionContext.getCodeContext().getResourceResolver()); } public boolean always() { @@ -59,15 +61,11 @@ public boolean idleSelf() { } public Stream queuedExecutions() { - return executionContext.getOsgiContext().getExecutionQueue().findAll().filter(e -> !isSelfExecution(e)); + return getExecutionQueue().findAll().filter(e -> !isSelfExecution(e)); } public Stream queuedSelfExecutions() { - return executionContext - .getOsgiContext() - .getExecutionQueue() - .findAll() - .filter(e -> !isSelfExecution(e) && isSameExecutable(e)); + return getExecutionQueue().findAll().filter(e -> !isSelfExecution(e) && isSameExecutable(e)); } public boolean isSelfExecution(Execution e) { @@ -79,6 +77,10 @@ public boolean isSameExecutable(Execution e) { e.getExecutable().getId(), executionContext.getExecutable().getId()); } + private ExecutionQueue getExecutionQueue() { + return executionContext.getCodeContext().getOsgiContext().getExecutionQueue(); + } + // Time period-based public boolean everyMinute() { @@ -319,13 +321,14 @@ public boolean isDate(ZonedDateTime zonedDateTime) { } public boolean isDate(LocalDateTime localDateTime) { - long intervalMillis = executionContext - .getOsgiContext() - .getService(ScriptScheduler.class) - .getIntervalMillis(); + long intervalMillis = getScriptScheduler().getIntervalMillis(); return DateUtils.isInRange(localDateTime, LocalDateTime.now(), intervalMillis); } + private ScriptScheduler getScriptScheduler() { + return executionContext.getCodeContext().getOsgiContext().getService(ScriptScheduler.class); + } + // Duration-based since the last execution public Duration passedDuration() { @@ -359,30 +362,34 @@ public boolean passedDays(long days) { // Instance-based public boolean isInstanceRunMode(String runMode) { - return executionContext.getOsgiContext().getInstanceInfo().isRunMode(runMode); + return getInstanceInfo().isRunMode(runMode); + } + + private InstanceInfo getInstanceInfo() { + return executionContext.getCodeContext().getOsgiContext().getInstanceInfo(); } public boolean isInstanceAuthor() { - return executionContext.getOsgiContext().getInstanceInfo().isAuthor(); + return getInstanceInfo().isAuthor(); } public boolean isInstancePublish() { - return executionContext.getOsgiContext().getInstanceInfo().isPublish(); + return getInstanceInfo().isPublish(); } public boolean isInstanceOnPrem() { - return executionContext.getOsgiContext().getInstanceInfo().getType() == InstanceType.ON_PREM; + return getInstanceInfo().getType() == InstanceType.ON_PREM; } public boolean isInstanceCloud() { - return executionContext.getOsgiContext().getInstanceInfo().getType().isCloud(); + return getInstanceInfo().getType().isCloud(); } public boolean isInstanceCloudContainer() { - return executionContext.getOsgiContext().getInstanceInfo().getType() == InstanceType.CLOUD_CONTAINER; + return getInstanceInfo().getType() == InstanceType.CLOUD_CONTAINER; } public boolean isInstanceCloudSdk() { - return executionContext.getOsgiContext().getInstanceInfo().getType() == InstanceType.CLOUD_SDK; + return getInstanceInfo().getType() == InstanceType.CLOUD_SDK; } } diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/ExecutionContext.java b/core/src/main/java/com/vml/es/aem/acm/core/code/ExecutionContext.java index ab96cf98..a8ba7c34 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/ExecutionContext.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/ExecutionContext.java @@ -1,16 +1,6 @@ package com.vml.es.aem.acm.core.code; -import com.vml.es.aem.acm.core.acl.Acl; -import com.vml.es.aem.acm.core.format.Formatter; -import com.vml.es.aem.acm.core.osgi.OsgiContext; -import com.vml.es.aem.acm.core.replication.Activator; -import com.vml.es.aem.acm.core.repo.Repo; import groovy.lang.Binding; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import org.apache.sling.api.resource.ResourceResolver; -import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ExecutionContext implements AutoCloseable { @@ -25,11 +15,7 @@ public class ExecutionContext implements AutoCloseable { private final Executable executable; - private final OsgiContext osgiContext; - - private final ResourceResolver resourceResolver; - - private final Extender extender; + private final CodeContext codeContext; private boolean history = true; @@ -37,24 +23,16 @@ public class ExecutionContext implements AutoCloseable { private final Arguments arguments = new Arguments(); - private Binding binding = new Binding(); - public ExecutionContext( - String id, - ExecutionMode mode, - Executor executor, - Executable executable, - OsgiContext osgiContext, - ResourceResolver resourceResolver) { + String id, ExecutionMode mode, Executor executor, Executable executable, CodeContext codeContext) { this.id = id; this.mode = mode; this.output = mode == ExecutionMode.RUN ? new OutputFile(id) : new OutputString(); this.executor = executor; this.executable = executable; - this.osgiContext = osgiContext; - this.resourceResolver = resourceResolver; - this.binding = createBinding(resourceResolver); - this.extender = new Extender(this); + this.codeContext = codeContext; + + customizeBinding(); } public String getId() { @@ -73,16 +51,8 @@ public Executable getExecutable() { return executable; } - public Extender getExtender() { - return extender; - } - - public ResourceResolver getResourceResolver() { - return resourceResolver; - } - - public OsgiContext getOsgiContext() { - return osgiContext; + public CodeContext getCodeContext() { + return codeContext; } public ExecutionMode getMode() { @@ -109,37 +79,15 @@ public Arguments getArguments() { return arguments; } - public Binding getBinding() { - return binding; - } - - @SuppressWarnings("unchecked") - public List getBindingVariables() { - Map variables = binding.getVariables(); - return variables.entrySet().stream() - .map(entry -> new Variable(entry.getKey(), entry.getValue().getClass())) - .collect(Collectors.toList()); - } - - private Binding createBinding(ResourceResolver resourceResolver) { - Binding result = new Binding(); - - result.setVariable("args", arguments); - result.setVariable("condition", new Condition(this)); - result.setVariable("log", createLogger(executable)); - result.setVariable("out", new CodePrintStream(this)); - result.setVariable("resourceResolver", resourceResolver); - result.setVariable("osgi", osgiContext); - result.setVariable("repo", new Repo(resourceResolver)); - result.setVariable("acl", new Acl(resourceResolver)); - result.setVariable("formatter", new Formatter()); - result.setVariable("activator", new Activator(resourceResolver, osgiContext.getReplicator())); - - return result; - } + private void customizeBinding() { + Binding binding = getCodeContext().getBinding(); - private Logger createLogger(Executable executable) { - return LoggerFactory.getLogger(String.format("%s(%s)", getClass().getName(), executable.getId())); + binding.setVariable("args", arguments); + binding.setVariable("condition", new Condition(this)); + binding.setVariable( + "log", + LoggerFactory.getLogger(String.format("%s(%s)", getClass().getName(), executable.getId()))); + binding.setVariable("out", new CodePrintStream(this)); } @Override @@ -148,6 +96,6 @@ public void close() { } public void variable(String name, Object value) { - binding.setVariable(name, value); + codeContext.getBinding().setVariable(name, value); } } diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/Executor.java b/core/src/main/java/com/vml/es/aem/acm/core/code/Executor.java index 68a4c712..d0ff897a 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/Executor.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/Executor.java @@ -50,7 +50,8 @@ protected void activate(Config config) { public ExecutionContext createContext( String id, ExecutionMode mode, Executable executable, ResourceResolver resourceResolver) { - ExecutionContext result = new ExecutionContext(id, mode, this, executable, osgiContext, resourceResolver); + CodeContext codeContext = new CodeContext(osgiContext, resourceResolver); + ExecutionContext result = new ExecutionContext(id, mode, this, executable, codeContext); result.setDebug(config.debug()); result.setHistory(config.history()); return result; @@ -69,13 +70,15 @@ public Execution execute(Executable executable, ExecutionContextOptions contextO } public Execution execute(ExecutionContext context) throws AcmException { + context.getCodeContext().prepareRun(context); ImmediateExecution execution = executeImmediately(context); if (context.getMode() == ExecutionMode.RUN) { if (context.isHistory() && (context.isDebug() || (execution.getStatus() != ExecutionStatus.SKIPPED))) { - ExecutionHistory history = new ExecutionHistory(context.getResourceResolver()); + ExecutionHistory history = + new ExecutionHistory(context.getCodeContext().getResourceResolver()); history.save(context, execution); } - context.getExtender().complete(execution); + context.getCodeContext().completeRun(execution); } return execution; } diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/Extender.java b/core/src/main/java/com/vml/es/aem/acm/core/code/Extender.java deleted file mode 100644 index bd231b6d..00000000 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/Extender.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.vml.es.aem.acm.core.code; - -import com.vml.es.aem.acm.core.code.script.ExtensionScript; -import com.vml.es.aem.acm.core.script.ScriptRepository; -import com.vml.es.aem.acm.core.script.ScriptType; -import java.util.List; -import java.util.stream.Collectors; - -public class Extender { - - private final List scripts; - - public Extender(ExecutionContext contentContext) { - - this.scripts = new ScriptRepository(contentContext.getResourceResolver()) - .findAll(ScriptType.EXTENSION) - .map(s -> new ExtensionScript(contentContext, s)) - .collect(Collectors.toList()); - - prepare(contentContext); - } - - public void prepare(ExecutionContext executionContext) { - for (ExtensionScript script : scripts) { - script.prepare(executionContext); - } - } - - public void complete(Execution execution) { - for (ExtensionScript script : scripts) { - script.complete(execution); - } - } -} diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/HistoricalExecution.java b/core/src/main/java/com/vml/es/aem/acm/core/code/HistoricalExecution.java index 98e8c0ed..7f5c24a4 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/HistoricalExecution.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/HistoricalExecution.java @@ -65,7 +65,10 @@ protected static Map toMap(ExecutionContext context, ImmediateEx Map props = new HashMap<>(); props.put("id", execution.getId()); - props.put("userId", ResourceUtils.serviceOrImpersonatedUserId(context.getResourceResolver())); + props.put( + "userId", + ResourceUtils.serviceOrImpersonatedUserId( + context.getCodeContext().getResourceResolver())); props.put("status", execution.getStatus().name()); props.put("startDate", DateUtils.toCalendar(execution.getStartDate())); props.put("endDate", DateUtils.toCalendar(execution.getEndDate())); diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/ImmediateExecution.java b/core/src/main/java/com/vml/es/aem/acm/core/code/ImmediateExecution.java index 27366d0f..4c22309f 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/ImmediateExecution.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/ImmediateExecution.java @@ -130,7 +130,7 @@ public ImmediateExecution end(ExecutionStatus status) { return new ImmediateExecution( context.getExecutable(), context.getId(), - context.getResourceResolver().getUserID(), + context.getCodeContext().getResourceResolver().getUserID(), status, startDate, endDate, diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ContentScript.java b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ContentScript.java index ad8fb743..53bfdbef 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ContentScript.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ContentScript.java @@ -22,7 +22,7 @@ private Script parseScript() { Script script = shell.parse( executionContext.getExecutable().getContent(), ContentScriptSyntax.MAIN_CLASS, - executionContext.getBinding()); + executionContext.getCodeContext().getBinding()); if (script == null) { throw new AcmException(String.format( "Content script '%s' cannot be parsed!", diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ContentScriptSyntax.java b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ContentScriptSyntax.java index e82b753b..4dd8c0fe 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ContentScriptSyntax.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ContentScriptSyntax.java @@ -12,7 +12,7 @@ @GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS) public class ContentScriptSyntax extends AbstractASTTransformation { - public static final String MAIN_CLASS = "AcmConentScript"; + public static final String MAIN_CLASS = "AcmContentScript"; @Override public void visit(ASTNode[] nodes, SourceUnit source) { diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ExtensionScript.java b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ExtensionScript.java index 060821c0..2accd7f4 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ExtensionScript.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ExtensionScript.java @@ -1,38 +1,56 @@ package com.vml.es.aem.acm.core.code.script; import com.vml.es.aem.acm.core.AcmException; +import com.vml.es.aem.acm.core.code.CodeContext; import com.vml.es.aem.acm.core.code.Executable; import com.vml.es.aem.acm.core.code.Execution; import com.vml.es.aem.acm.core.code.ExecutionContext; +import com.vml.es.aem.acm.core.mock.MockContext; import groovy.lang.Script; public class ExtensionScript { - private final Executable executable; + private final Executable extensionScript; private final Script script; - public ExtensionScript(ExecutionContext contentContext, Executable extensionScript) { - this.executable = extensionScript; + public ExtensionScript(CodeContext codeContext, Executable extensionScript) { + this.extensionScript = extensionScript; this.script = ScriptUtils.createShell(new ExtensionScriptSyntax()) - .parse(executable.getContent(), ExtensionScriptSyntax.MAIN_CLASS, contentContext.getBinding()); + .parse(this.extensionScript.getContent(), ExtensionScriptSyntax.MAIN_CLASS, codeContext.getBinding()); } - public void prepare(ExecutionContext executionContext) { + public void prepareRun(ExecutionContext executionContext) { try { - script.invokeMethod(ExtensionScriptSyntax.Method.PREPARE.givenName, executionContext); + script.invokeMethod(ExtensionScriptSyntax.Method.PREPARE_RUN.givenName, executionContext); } catch (Exception e) { throw new AcmException( - String.format("Cannot extend content script with extension script '%s'!", executable.getId()), e); + String.format( + "Cannot prepare content script context with extension script '%s'!", + extensionScript.getId()), + e); } } - public void complete(Execution execution) { + public void completeRun(Execution execution) { try { - script.invokeMethod(ExtensionScriptSyntax.Method.COMPLETE.givenName, execution); + script.invokeMethod(ExtensionScriptSyntax.Method.COMPLETE_RUN.givenName, execution); } catch (Exception e) { throw new AcmException( - String.format("Cannot complete execution with extension script '%s'", executable.getId()), e); + String.format( + "Cannot complete execution '%s' with extension script '%s'!", + execution.getId(), extensionScript.getId()), + e); + } + } + + public void prepareMock(MockContext mockContext) { + try { + script.invokeMethod(ExtensionScriptSyntax.Method.PREPARE_MOCK.givenName, mockContext); + } catch (Exception e) { + throw new AcmException( + String.format("Cannot prepare mock context with extension script '%s'!", extensionScript.getId()), + e); } } } diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ExtensionScriptSyntax.java b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ExtensionScriptSyntax.java index b69c4004..6e995754 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/script/ExtensionScriptSyntax.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/script/ExtensionScriptSyntax.java @@ -37,8 +37,9 @@ public void visit(ASTNode[] nodes, SourceUnit source) { } enum Method { - PREPARE("prepareRun", "void", true, 1), - COMPLETE("completeRun", "void", true, 1); + PREPARE_RUN("prepareRun", "void", true, 1), + COMPLETE_RUN("completeRun", "void", true, 1), + PREPARE_MOCK("prepareMock", "void", true, 1); final String givenName; final String returnType; diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockContext.java b/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockContext.java deleted file mode 100644 index 0e65f36d..00000000 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockContext.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.vml.es.aem.acm.core.code.script; - -public class MockContext { - // TODO do the same binding like in content scripts -} diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScript.java b/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScript.java index 01a4d6eb..92d357ea 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScript.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScript.java @@ -1,8 +1,7 @@ package com.vml.es.aem.acm.core.code.script; import com.vml.es.aem.acm.core.AcmException; -import com.vml.es.aem.acm.core.code.ExecutionContext; -import com.vml.es.aem.acm.core.mock.Mock; +import com.vml.es.aem.acm.core.mock.MockContext; import com.vml.es.aem.acm.core.mock.MockRequestException; import com.vml.es.aem.acm.core.mock.MockResponseException; import groovy.lang.GroovyShell; @@ -13,39 +12,32 @@ import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; -public class MockScript implements Mock { +public class MockScript { private static final Logger LOG = org.slf4j.LoggerFactory.getLogger(MockScript.class); - private final ExecutionContext executionContext; + private final MockContext context; private final Script script; - public MockScript(ExecutionContext executionContext) { - this.executionContext = executionContext; + public MockScript(MockContext context) { + this.context = context; this.script = parseScript(); } private Script parseScript() { GroovyShell shell = ScriptUtils.createShell(new MockScriptSyntax()); Script script = shell.parse( - executionContext.getExecutable().getContent(), + context.getMock().getContent(), MockScriptSyntax.MAIN_CLASS, - executionContext.getBinding()); + context.getCodeContext().getBinding()); if (script == null) { throw new AcmException(String.format( - "Mock script '%s' cannot be parsed!", - executionContext.getExecutable().getId())); + "Mock script '%s' cannot be parsed!", context.getMock().getId())); } return script; } - @Override - public String getId() { - return executionContext.getExecutable().getId(); - } - - @Override public boolean request(HttpServletRequest request) throws MockRequestException { try { LOG.info("Mock '{}' is matching request '{} {}'", getId(), request.getMethod(), request.getRequestURI()); @@ -66,7 +58,6 @@ public boolean request(HttpServletRequest request) throws MockRequestException { } } - @Override public void respond(HttpServletRequest request, HttpServletResponse response) throws MockResponseException { try { LOG.info( @@ -81,7 +72,6 @@ public void respond(HttpServletRequest request, HttpServletResponse response) th } } - @Override public void fail(HttpServletRequest request, HttpServletResponse response, Exception exception) throws MockResponseException { try { @@ -98,6 +88,10 @@ public void fail(HttpServletRequest request, HttpServletResponse response, Excep } } + public String getId() { + return context.getMock().getId(); + } + public String getDirPath() { return StringUtils.substringBeforeLast(getId(), "/"); } diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/Mock.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/Mock.java index ea848d99..0b6a7b84 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/mock/Mock.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/Mock.java @@ -1,15 +1,22 @@ package com.vml.es.aem.acm.core.mock; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import com.vml.es.aem.acm.core.AcmException; +import com.vml.es.aem.acm.core.repo.RepoResource; +import org.apache.sling.api.resource.Resource; -public interface Mock { +public class Mock { - String getId(); + private final Resource resource; - boolean request(HttpServletRequest request) throws MockRequestException; + public Mock(Resource resource) { + this.resource = resource; + } - void respond(HttpServletRequest request, HttpServletResponse response) throws MockResponseException; + public String getId() { + return resource.getPath(); + } - void fail(HttpServletRequest request, HttpServletResponse response, Exception e) throws MockResponseException; + public String getContent() throws AcmException { + return RepoResource.of(resource).readFileAsString(); + } } diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockContext.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockContext.java new file mode 100644 index 00000000..e4111cc0 --- /dev/null +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockContext.java @@ -0,0 +1,39 @@ +package com.vml.es.aem.acm.core.mock; + +import com.vml.es.aem.acm.core.code.*; +import groovy.lang.Binding; +import org.slf4j.LoggerFactory; + +public class MockContext { + + private final CodeContext codeContext; + + private final Mock mock; + + public MockContext(CodeContext codeContext, Mock mock) { + this.codeContext = codeContext; + this.mock = mock; + + customizeBinding(); + } + + public CodeContext getCodeContext() { + return codeContext; + } + + public Mock getMock() { + return mock; + } + + public void variable(String name, Object value) { + codeContext.getBinding().setVariable(name, value); + } + + private void customizeBinding() { + Binding binding = getCodeContext().getBinding(); + + binding.setVariable( + "log", + LoggerFactory.getLogger(String.format("%s(%s)", getClass().getName(), mock.getId()))); + } +} diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockHttpFilter.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockHttpFilter.java index 771936a2..110f829b 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockHttpFilter.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockHttpFilter.java @@ -1,10 +1,8 @@ package com.vml.es.aem.acm.core.mock; -import com.vml.es.aem.acm.core.code.ExecutionContext; -import com.vml.es.aem.acm.core.code.ExecutionId; -import com.vml.es.aem.acm.core.code.ExecutionMode; -import com.vml.es.aem.acm.core.code.Executor; +import com.vml.es.aem.acm.core.code.CodeContext; import com.vml.es.aem.acm.core.code.script.MockScript; +import com.vml.es.aem.acm.core.osgi.OsgiContext; import com.vml.es.aem.acm.core.util.ResourceUtils; import java.io.IOException; import java.util.Iterator; @@ -36,10 +34,10 @@ public class MockHttpFilter implements Filter { @Reference private ResourceResolverFactory resolverFactory; - private Config config; - @Reference - private Executor executor; + private OsgiContext osgiContext; + + private Config config; public MockStatus checkStatus() { return new MockStatus(config.enabled()); @@ -63,16 +61,17 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) HttpServletResponse response = (HttpServletResponse) res; try (ResourceResolver resolver = ResourceUtils.serviceResolver(resolverFactory, null)) { - MockScriptRepository repository = new MockScriptRepository(resolver); + CodeContext codeContext = new CodeContext(osgiContext, resolver); + MockRepository repository = new MockRepository(resolver); try { - Iterator it = repository.findAll().iterator(); + Iterator it = repository.findAll().iterator(); while (it.hasNext()) { - MockScriptExecutable candidateScript = it.next(); - if (!repository.isSpecial(candidateScript.getId())) { - ExecutionContext executionContext = executor.createContext( - ExecutionId.generate(), ExecutionMode.RUN, candidateScript, resolver); - MockScript mock = new MockScript(executionContext); + Mock candidateMock = it.next(); + if (!repository.isSpecial(candidateMock.getId())) { + MockContext mockContext = new MockContext(codeContext, candidateMock); + codeContext.prepareMock(mockContext); + MockScript mock = new MockScript(mockContext); if (mock.request(request)) { mock.respond(request, response); return; @@ -82,13 +81,13 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) } catch (MockException e) { LOG.error("Mock error!", e); - MockScriptExecutable failScript = - repository.findSpecial(MockScriptRepository.FAIL_PATH).orElse(null); + Mock failScript = + repository.findSpecial(MockRepository.FAIL_PATH).orElse(null); if (failScript != null) { try { - ExecutionContext executionContext = - executor.createContext(ExecutionId.generate(), ExecutionMode.RUN, failScript, resolver); - MockScript mock = new MockScript(executionContext); + MockContext mockContext = new MockContext(codeContext, failScript); + codeContext.prepareMock(mockContext); + MockScript mock = new MockScript(mockContext); mock.fail(request, response, e); } catch (MockException e2) { LOG.error("Mock fail error!", e2); @@ -101,13 +100,13 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) return; } - MockScriptExecutable missingScript = - repository.findSpecial(MockScriptRepository.MISSING_PATH).orElse(null); + Mock missingScript = + repository.findSpecial(MockRepository.MISSING_PATH).orElse(null); if (missingScript != null) { try { - ExecutionContext executionContext = - executor.createContext(ExecutionId.generate(), ExecutionMode.RUN, missingScript, resolver); - MockScript mock = new MockScript(executionContext); + MockContext mockContext = new MockContext(codeContext, missingScript); + codeContext.prepareMock(mockContext); + MockScript mock = new MockScript(mockContext); mock.respond(request, response); } catch (MockException e) { LOG.error("Mock missing error!", e); diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptRepository.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockRepository.java similarity index 78% rename from core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptRepository.java rename to core/src/main/java/com/vml/es/aem/acm/core/mock/MockRepository.java index 12afe139..4b4d3b38 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptRepository.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockRepository.java @@ -12,7 +12,7 @@ import org.apache.jackrabbit.JcrConstants; import org.apache.sling.api.resource.*; -public class MockScriptRepository { +public class MockRepository { public static final String CORE_DIR = "core"; @@ -24,7 +24,7 @@ public class MockScriptRepository { private final ResourceResolver resolver; - public MockScriptRepository(ResourceResolver resolver) { + public MockRepository(ResourceResolver resolver) { this.resolver = resolver; } @@ -40,18 +40,18 @@ private Optional findResource(String subPath) { return Optional.ofNullable(resolver.getResource(String.format("%s/%s", ScriptType.MOCK.root(), subPath))); } - public Stream findAll() throws MockException { + public Stream findAll() throws MockException { return ResourceSpliterator.stream(getOrCreateRoot()) .filter(this::checkResource) - .map(MockScriptExecutable::new); + .map(Mock::new); } - public Optional find(String subPath) { - return findResource(subPath).filter(this::checkResource).map(MockScriptExecutable::new); + public Optional find(String subPath) { + return findResource(subPath).filter(this::checkResource).map(Mock::new); } - public Optional findSpecial(String subPath) { - return findResource(subPath).map(MockScriptExecutable::new); + public Optional findSpecial(String subPath) { + return findResource(subPath).map(Mock::new); } public boolean isSpecial(String id) { diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptExecutable.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptExecutable.java deleted file mode 100644 index 66d12bec..00000000 --- a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockScriptExecutable.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.vml.es.aem.acm.core.mock; - -import com.vml.es.aem.acm.core.AcmException; -import com.vml.es.aem.acm.core.code.ArgumentValues; -import com.vml.es.aem.acm.core.code.Executable; -import com.vml.es.aem.acm.core.repo.RepoResource; -import org.apache.sling.api.resource.Resource; - -public class MockScriptExecutable implements Executable { // TODO not executable! - - private final Resource resource; - - public MockScriptExecutable(Resource resource) { - this.resource = resource; - } - - @Override - public String getId() { - return resource.getPath(); - } - - @Override - public String getContent() throws AcmException { - return RepoResource.of(resource).readFileAsString(); - } - - @Override - public ArgumentValues getArguments() { - return new ArgumentValues(); - } -} diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/extension/example/ACME-300_extension.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/extension/example/ACME-300_extension.groovy index aaf395a4..bbb35409 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/extension/example/ACME-300_extension.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/extension/example/ACME-300_extension.groovy @@ -1,5 +1,6 @@ import com.vml.es.aem.acm.core.code.ExecutionContext import com.vml.es.aem.acm.core.code.Execution +import com.vml.es.aem.acm.core.mock.MockContext void prepareRun(ExecutionContext executionContext) { executionContext.variable("acme", new AcmeFacade()) @@ -12,6 +13,10 @@ void completeRun(Execution execution) { } } +void prepareMock(MockContext mockContext) { + mockContext.variable("acme", new AcmeFacade()) +} + class AcmeFacade { def now() { return new Date() diff --git a/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/condition/is_date.yml b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/condition/is_date.yml index fd262276..f69eb766 100644 --- a/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/condition/is_date.yml +++ b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/condition/is_date.yml @@ -3,15 +3,21 @@ name: condition_is_date content: | condition.isDate(${1:date}) documentation: | - Execute the script only on the specified date. + Execute the script on a specific date. + + The date can be provided as a string in one of the following formats: - The input can be provided as a string in one of the following formats: - - **yyyy-MM-dd HH:mm:ss**: `condition.isDate("2025-04-16 12:30:00")` - - **yyyy-MM-dd'T'HH:mm:ss**: `condition.isDate("2025-04-16T12:30:00")` - - **yyyy-MM-dd'T'HH:mm:ss.SSSXXX**: `condition.isDate("2025-04-16T12:30:00.123+02:00")` - - **yyyy-MM-dd'T'HH:mm:ssXXX**: `condition.isDate("2025-04-16T12:30:00+02:00")`. + - **yyyy-MM-dd HH:mm:ss**: `condition.isDate("2025-04-16 12:30:00")` + - **yyyy-MM-dd'T'HH:mm:ss**: `condition.isDate("2025-04-16T12:30:00")` + - **yyyy-MM-dd'T'HH:mm:ss.SSSXXX**: `condition.isDate("2025-04-16T12:30:00.123+02:00")` + - **yyyy-MM-dd'T'HH:mm:ssXXX**: `condition.isDate("2025-04-16T12:30:00+02:00")` + + If the date string does not include timezone information, the server assumes the date is in its default timezone. In such cases, the date is adjusted to match the server's timezone if there is a discrepancy. - If the string does not include timezone information, the server assumes the date is in its own timezone. In such cases, the date value is recalculated to align with the server's timezone if there is a timezone difference. - Alternatively, you can pass a `ZonedDateTime` or `LocalDateTime` object, for example: `condition.isDate(ZonedDateTime.of(2025, 12, 25, 10, 0, 0, 0, ZoneId.of("Europe/Warsaw")))`. - - Using `ZonedDateTime` is recommended for precise timezone handling, as it eliminates ambiguity and ensures accurate date interpretation. \ No newline at end of file + Alternatively, you can provide a `ZonedDateTime` or `LocalDateTime` object, such as: + `condition.isDate(ZonedDateTime.of(2025, 12, 25, 10, 0, 0, 0, ZoneId.of("Europe/Warsaw")))`. + + Using `ZonedDateTime` is recommended for precise timezone handling, as it avoids ambiguity and ensures accurate date interpretation. + + **Note:** The script scheduler operates at regular intervals (default: 30 seconds). The provided date must be in the future and fall within the range of the current time and the next scheduler interval. + For example, if the current time is `12:00:00` and the scheduler runs every 30 seconds, the date must be between `12:00:00` and `12:00:30` (inclusive) to meet the condition. diff --git a/ui.frontend/src/ErrorHandler.tsx b/ui.frontend/src/ErrorHandler.tsx index 381b613b..bc69186f 100644 --- a/ui.frontend/src/ErrorHandler.tsx +++ b/ui.frontend/src/ErrorHandler.tsx @@ -26,8 +26,8 @@ const ErrorHandler: React.FC = () => { - Application encountered a problem. -
+ Application encountered a problem.
+ Verify your authorization status.
Ensure that AEM instance backend is available.
From f3edd0e03ed192bb4466ae1e5ccdce8a1219b0ce Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Wed, 14 May 2025 07:59:17 +0200 Subject: [PATCH 20/26] Mock UI fixes --- .../vml/es/aem/acm/core/code/CodeContext.java | 42 ++++++++++-------- .../aem/acm/core/code/script/MockScript.java | 12 ----- .../com/vml/es/aem/acm/core/mock/Mock.java | 16 +++++++ .../vml/es/aem/acm/core/mock/MockContext.java | 1 + .../example/ACME-300_extension.groovy | 8 ++-- .../main/content/META-INF/vault/filter.xml | 2 +- .../settings/{ => script}/mock/.content.xml | 0 .../{ => script}/mock/core/.content.xml | 0 .../{ => script}/mock/core/fail.groovy | 0 .../settings/{ => script}/mock/core/fail.html | 0 .../{ => script}/mock/core/logo-text.png | Bin .../{ => script}/mock/core/missing.groovy | 0 .../{ => script}/mock/core/missing.html | 0 ui.frontend/index.html | 2 +- .../src/components/ScriptListSimple.tsx | 16 ++++++- ui.frontend/src/pages/ScriptsPage.tsx | 4 +- 16 files changed, 63 insertions(+), 40 deletions(-) rename ui.content/src/main/content/jcr_root/conf/acm/settings/{ => script}/mock/.content.xml (100%) rename ui.content/src/main/content/jcr_root/conf/acm/settings/{ => script}/mock/core/.content.xml (100%) rename ui.content/src/main/content/jcr_root/conf/acm/settings/{ => script}/mock/core/fail.groovy (100%) rename ui.content/src/main/content/jcr_root/conf/acm/settings/{ => script}/mock/core/fail.html (100%) rename ui.content/src/main/content/jcr_root/conf/acm/settings/{ => script}/mock/core/logo-text.png (100%) rename ui.content/src/main/content/jcr_root/conf/acm/settings/{ => script}/mock/core/missing.groovy (100%) rename ui.content/src/main/content/jcr_root/conf/acm/settings/{ => script}/mock/core/missing.html (100%) diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/CodeContext.java b/core/src/main/java/com/vml/es/aem/acm/core/code/CodeContext.java index 505bc70c..ebe85e48 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/CodeContext.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/CodeContext.java @@ -24,50 +24,54 @@ public class CodeContext { private final Binding binding; - private final List scripts; + private final List extensionScripts; public CodeContext(OsgiContext osgiContext, ResourceResolver resourceResolver) { this.osgiContext = osgiContext; this.resourceResolver = resourceResolver; this.binding = createBinding(osgiContext, resourceResolver); - this.scripts = new ScriptRepository(resourceResolver) + this.extensionScripts = findExtensionScripts(resourceResolver); + } + + private List findExtensionScripts(ResourceResolver resourceResolver) { + return new ScriptRepository(resourceResolver) .findAll(ScriptType.EXTENSION) .map(s -> new ExtensionScript(this, s)) .collect(Collectors.toList()); } + public Binding createBinding(OsgiContext osgiContext, ResourceResolver resourceResolver) { + Binding result = new Binding(); + + result.setVariable("log", LoggerFactory.getLogger(getClass())); + result.setVariable("resourceResolver", resourceResolver); + result.setVariable("osgi", osgiContext); + result.setVariable("repo", new Repo(resourceResolver)); + result.setVariable("acl", new Acl(resourceResolver)); + result.setVariable("formatter", new Formatter()); + result.setVariable("activator", new Activator(resourceResolver, osgiContext.getReplicator())); + + return result; + } + public void prepareRun(ExecutionContext executionContext) { - for (ExtensionScript script : scripts) { + for (ExtensionScript script : extensionScripts) { script.prepareRun(executionContext); } } public void completeRun(Execution execution) { - for (ExtensionScript script : scripts) { + for (ExtensionScript script : extensionScripts) { script.completeRun(execution); } } public void prepareMock(MockContext mockContext) { - for (ExtensionScript script : scripts) { + for (ExtensionScript script : extensionScripts) { script.prepareMock(mockContext); } } - public Binding createBinding(OsgiContext osgiContext, ResourceResolver resourceResolver) { - Binding result = new Binding(); - - result.setVariable("log", LoggerFactory.getLogger(getClass())); - result.setVariable("resourceResolver", resourceResolver); - result.setVariable("osgi", osgiContext); - result.setVariable("repo", new Repo(resourceResolver)); - result.setVariable("acl", new Acl(resourceResolver)); - result.setVariable("formatter", new Formatter()); - result.setVariable("activator", new Activator(resourceResolver, osgiContext.getReplicator())); - - return result; - } - public Binding getBinding() { return binding; } diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScript.java b/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScript.java index 92d357ea..d87e8ef9 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScript.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/script/MockScript.java @@ -9,7 +9,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.BooleanUtils; -import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; public class MockScript { @@ -91,15 +90,4 @@ public void fail(HttpServletRequest request, HttpServletResponse response, Excep public String getId() { return context.getMock().getId(); } - - public String getDirPath() { - return StringUtils.substringBeforeLast(getId(), "/"); - } - - public String resolvePath(String path) { - if (path.startsWith("/")) { - return path; - } - return getDirPath() + "/" + path; - } } diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/Mock.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/Mock.java index 0b6a7b84..cca1aeae 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/mock/Mock.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/Mock.java @@ -2,6 +2,7 @@ import com.vml.es.aem.acm.core.AcmException; import com.vml.es.aem.acm.core.repo.RepoResource; +import org.apache.commons.lang3.StringUtils; import org.apache.sling.api.resource.Resource; public class Mock { @@ -13,10 +14,25 @@ public Mock(Resource resource) { } public String getId() { + return getPath(); + } + + public String getPath() { return resource.getPath(); } public String getContent() throws AcmException { return RepoResource.of(resource).readFileAsString(); } + + public String getDirPath() { + return StringUtils.substringBeforeLast(getId(), "/"); + } + + public String resolvePath(String path) { + if (path.startsWith("/")) { + return path; + } + return getDirPath() + "/" + path; + } } diff --git a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockContext.java b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockContext.java index e4111cc0..b9ed4934 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/mock/MockContext.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/mock/MockContext.java @@ -32,6 +32,7 @@ public void variable(String name, Object value) { private void customizeBinding() { Binding binding = getCodeContext().getBinding(); + binding.setVariable("mock", mock); binding.setVariable( "log", LoggerFactory.getLogger(String.format("%s(%s)", getClass().getName(), mock.getId()))); diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/extension/example/ACME-300_extension.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/extension/example/ACME-300_extension.groovy index bbb35409..9f5bad53 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/extension/example/ACME-300_extension.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/extension/example/ACME-300_extension.groovy @@ -2,8 +2,8 @@ import com.vml.es.aem.acm.core.code.ExecutionContext import com.vml.es.aem.acm.core.code.Execution import com.vml.es.aem.acm.core.mock.MockContext -void prepareRun(ExecutionContext executionContext) { - executionContext.variable("acme", new AcmeFacade()) +void prepareRun(ExecutionContext context) { + context.variable("acme", new AcmeFacade()) } void completeRun(Execution execution) { @@ -13,8 +13,8 @@ void completeRun(Execution execution) { } } -void prepareMock(MockContext mockContext) { - mockContext.variable("acme", new AcmeFacade()) +void prepareMock(MockContext context) { + context.variable("acme", new AcmeFacade()) } class AcmeFacade { diff --git a/ui.content/src/main/content/META-INF/vault/filter.xml b/ui.content/src/main/content/META-INF/vault/filter.xml index 8a289a4d..f3b722a7 100644 --- a/ui.content/src/main/content/META-INF/vault/filter.xml +++ b/ui.content/src/main/content/META-INF/vault/filter.xml @@ -2,5 +2,5 @@ - + diff --git a/ui.content/src/main/content/jcr_root/conf/acm/settings/mock/.content.xml b/ui.content/src/main/content/jcr_root/conf/acm/settings/script/mock/.content.xml similarity index 100% rename from ui.content/src/main/content/jcr_root/conf/acm/settings/mock/.content.xml rename to ui.content/src/main/content/jcr_root/conf/acm/settings/script/mock/.content.xml diff --git a/ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/.content.xml b/ui.content/src/main/content/jcr_root/conf/acm/settings/script/mock/core/.content.xml similarity index 100% rename from ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/.content.xml rename to ui.content/src/main/content/jcr_root/conf/acm/settings/script/mock/core/.content.xml diff --git a/ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/fail.groovy b/ui.content/src/main/content/jcr_root/conf/acm/settings/script/mock/core/fail.groovy similarity index 100% rename from ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/fail.groovy rename to ui.content/src/main/content/jcr_root/conf/acm/settings/script/mock/core/fail.groovy diff --git a/ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/fail.html b/ui.content/src/main/content/jcr_root/conf/acm/settings/script/mock/core/fail.html similarity index 100% rename from ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/fail.html rename to ui.content/src/main/content/jcr_root/conf/acm/settings/script/mock/core/fail.html diff --git a/ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/logo-text.png b/ui.content/src/main/content/jcr_root/conf/acm/settings/script/mock/core/logo-text.png similarity index 100% rename from ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/logo-text.png rename to ui.content/src/main/content/jcr_root/conf/acm/settings/script/mock/core/logo-text.png diff --git a/ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/missing.groovy b/ui.content/src/main/content/jcr_root/conf/acm/settings/script/mock/core/missing.groovy similarity index 100% rename from ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/missing.groovy rename to ui.content/src/main/content/jcr_root/conf/acm/settings/script/mock/core/missing.groovy diff --git a/ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/missing.html b/ui.content/src/main/content/jcr_root/conf/acm/settings/script/mock/core/missing.html similarity index 100% rename from ui.content/src/main/content/jcr_root/conf/acm/settings/mock/core/missing.html rename to ui.content/src/main/content/jcr_root/conf/acm/settings/script/mock/core/missing.html diff --git a/ui.frontend/index.html b/ui.frontend/index.html index 8675f7ed..694c1817 100644 --- a/ui.frontend/index.html +++ b/ui.frontend/index.html @@ -4,7 +4,7 @@ - AEM Content Manager + Content Manager