diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d633cfbe3..1b5dd499b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -55,16 +55,27 @@ jobs: retention-days: 3 test: - name: 'Test ACM Package' + name: 'Test ACM Package (AEM ${{ matrix.aem }})' runs-on: ubuntu-latest needs: build timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + - aem: '6.5.0' + quickstart_prefix: 'cq-quickstart-6.5.0.jar' + java_download_url: 'https://github.com/adoptium/temurin8-binaries/releases/download/jdk8u432-b06/OpenJDK8U-jdk_x64_linux_hotspot_8u432b06.tar.gz' + - aem: '6.6.x' + quickstart_prefix: 'cq-quickstart-6.6.' + java_download_url: 'https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21%2B35/OpenJDK21U-jdk_x64_linux_hotspot_21_35.tar.gz' permissions: contents: read actions: read checks: write env: AEM_OUTPUT_LOG_MODE: none + AEM_VENDOR_JAVA_DOWNLOAD_URL: ${{ matrix.java_download_url }} FORCE_COLOR: 1 steps: - name: Checkout Code @@ -110,7 +121,13 @@ jobs: echo "Preparing AEM instance" echo "AEM_PUBLISH_ACTIVE=false" >> .env mkdir -p aem/home/lib - aws s3 cp --no-progress --only-show-errors s3://${{ secrets.AWS_S3_BUCKET_NAME }}/aem/on-prem/cq-quickstart-6.5.0.jar aem/home/lib/cq-quickstart-6.5.0.jar + JAR_FILE=$(aws s3 ls "s3://${{ secrets.AWS_S3_BUCKET_NAME }}/aem/on-prem/${{ matrix.quickstart_prefix }}" | awk '{print $4}' | sort -V | tail -n 1) + if [ -z "$JAR_FILE" ]; then + echo "::error::No AEM quickstart matching '${{ matrix.quickstart_prefix }}' found in S3!" + exit 1 + fi + echo "Downloading AEM quickstart '$JAR_FILE'" + aws s3 cp --no-progress --only-show-errors "s3://${{ secrets.AWS_S3_BUCKET_NAME }}/aem/on-prem/$JAR_FILE" "aem/home/lib/$JAR_FILE" aws s3 cp --no-progress --only-show-errors s3://${{ secrets.AWS_S3_BUCKET_NAME }}/aem/on-prem/license.properties aem/home/lib/license.properties echo "Prepared AEM instance" echo "Launching AEM instance" @@ -141,6 +158,6 @@ jobs: uses: actions/upload-artifact@v7 if: ${{ !cancelled() }} with: - name: acm-playwright-report + name: acm-playwright-report-${{ matrix.aem }} path: ./test/e2e/playwright-report/ retention-days: 30 \ No newline at end of file diff --git a/aem/default/etc/aem.yml b/aem/default/etc/aem.yml index 14f058863..7727f1b64 100755 --- a/aem/default/etc/aem.yml +++ b/aem/default/etc/aem.yml @@ -260,10 +260,10 @@ vendor: # Auto-installed JDK options download: # Source URL with template vars support - url: "https://github.com/adoptium/temurin8-binaries/releases/download/jdk8u432-b06/OpenJDK8U-jdk_[[.Arch]]_[[.Os]]_hotspot_8u432b06.[[.ArchiveExt]]" + #url: "https://github.com/adoptium/temurin8-binaries/releases/download/jdk8u432-b06/OpenJDK8U-jdk_[[.Arch]]_[[.Os]]_hotspot_8u432b06.[[.ArchiveExt]]" #url: "https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.18%2B10/OpenJDK11U-jdk_[[.Arch]]_[[.Os]]_hotspot_11.0.18_10.[[.ArchiveExt]]" #url: "https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.8%2B7/OpenJDK17U-jdk_[[.Arch]]_[[.Os]]_hotspot_17.0.8_7.[[.ArchiveExt]]" - #url: "https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21%2B35/OpenJDK21U-jdk_[[.Arch]]_[[.Os]]_hotspot_21_35.[[.ArchiveExt]]" + url: "https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21%2B35/OpenJDK21U-jdk_[[.Arch]]_[[.Os]]_hotspot_21_35.[[.ArchiveExt]]" # Map source URL template vars to be compatible with Adoptium Java replacements: # Var 'Os' (GOOS) diff --git a/core/pom.xml b/core/pom.xml index fc7457906..ad84f5b68 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -95,7 +95,9 @@ DynamicImport-Package: * src/main/java/**/*.java src/test/java/**/*.java - + + ${palantir.java.format.version} + @@ -168,12 +170,6 @@ DynamicImport-Package: * org.slf4j slf4j-api - - org.apache.sling - org.apache.sling.commons.classloader - 1.4.2 - provided - javax.jcr jcr diff --git a/core/src/main/java/dev/vml/es/acm/core/acl/Acl.java b/core/src/main/java/dev/vml/es/acm/core/acl/Acl.java index bc7668932..c3f8563f9 100644 --- a/core/src/main/java/dev/vml/es/acm/core/acl/Acl.java +++ b/core/src/main/java/dev/vml/es/acm/core/acl/Acl.java @@ -668,4 +668,8 @@ public AclAuthorizable authorizable(Authorizable authorizable) { public AclChecker getChecker() { return checker; } + + public AclContext getContext() { + return context; + } } diff --git a/core/src/main/java/dev/vml/es/acm/core/acl/AclContext.java b/core/src/main/java/dev/vml/es/acm/core/acl/AclContext.java index 7f5259933..b8e0b08ac 100644 --- a/core/src/main/java/dev/vml/es/acm/core/acl/AclContext.java +++ b/core/src/main/java/dev/vml/es/acm/core/acl/AclContext.java @@ -23,7 +23,9 @@ public class AclContext { - private final Logger logger; + private static final Logger LOG = LoggerFactory.getLogger(AclContext.class); + + private Logger logger = LOG; private final ResourceResolver resourceResolver; @@ -37,7 +39,6 @@ public AclContext(ResourceResolver resourceResolver) { public AclContext(ResourceResolver resourceResolver, CommitPolicy commitPolicy) { try { - this.logger = LoggerFactory.getLogger(AclContext.class); JackrabbitSession session = (JackrabbitSession) resourceResolver.adaptTo(Session.class); UserManager userManager = session.getUserManager(); AccessControlManager accessControlManager = session.getAccessControlManager(); @@ -54,6 +55,10 @@ public Logger getLogger() { return logger; } + public void setLogger(Logger logger) { + this.logger = logger != null ? logger : LOG; + } + public ResourceResolver getResourceResolver() { return resourceResolver; } diff --git a/core/src/main/java/dev/vml/es/acm/core/code/CodePrintStream.java b/core/src/main/java/dev/vml/es/acm/core/code/CodePrintStream.java index 76efe5dd1..d7c58c459 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/CodePrintStream.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/CodePrintStream.java @@ -1,6 +1,7 @@ package dev.vml.es.acm.core.code; import dev.vml.es.acm.core.AcmException; +import dev.vml.es.acm.core.code.log.CodePrintLogger; import dev.vml.es.acm.core.code.log.LogInterceptor; import dev.vml.es.acm.core.code.log.LogInterceptorManager; import dev.vml.es.acm.core.code.log.LogMessage; @@ -36,6 +37,8 @@ public class CodePrintStream extends PrintStream { private final Set loggerNames; + private final Set bridgedLoggerNames; + private boolean loggerTimestamps; private LogInterceptor.Handle interceptorHandle; @@ -51,17 +54,30 @@ public CodePrintStream(OutputStream output, String id, LogInterceptorManager log this.logInterceptorManager = logInterceptorManager; this.loggerNames = new HashSet<>(); + this.bridgedLoggerNames = new HashSet<>(); this.loggerTimestamps = true; - this.logger = LoggerFactory.getLogger(id); + this.logger = bridgeLogger(LoggerFactory.getLogger(id)); this.printerTimestamps = true; } + Logger bridgeLogger(Logger delegate) { + bridgedLoggerNames.add(delegate.getName()); + return new CodePrintLogger(delegate, this::handleLogMessage); + } + + private boolean isBridgedLogger(String loggerName) { + return LOGGER_NAME_ACL.equals(loggerName) + || LOGGER_NAME_REPO.equals(loggerName) + || bridgedLoggerNames.contains(loggerName); + } + public void fromLogger(String loggerName) { if (StringUtils.isBlank(loggerName)) { throw new AcmException("Logger name cannot be blank!"); } - loggerNames.add(loggerName); - updateInterceptor(); + if (loggerNames.add(loggerName.trim())) { + updateInterceptor(); + } } @Override @@ -73,8 +89,11 @@ public void close() { private void updateInterceptor() { detachInterceptor(); if (!loggerNames.isEmpty() && logInterceptorManager != null) { - interceptorHandle = - logInterceptorManager.attach(this::handleLogMessage, loggerNames.toArray(new String[0])); + String[] externalLoggers = + loggerNames.stream().filter(name -> !isBridgedLogger(name)).toArray(String[]::new); + if (externalLoggers.length > 0) { + interceptorHandle = logInterceptorManager.attach(this::handleLogMessage, externalLoggers); + } } } @@ -134,13 +153,24 @@ public void fromRecommendedLoggers() { } public void fromLoggers(String... loggerNames) { + if (loggerNames == null) { + return; + } + boolean changed = false; for (String loggerName : loggerNames) { - fromLogger(loggerName); + if (StringUtils.isNotBlank(loggerName) && this.loggerNames.add(loggerName.trim())) { + changed = true; + } + } + if (changed) { + updateInterceptor(); } } public void fromLoggers(List loggerNames) { - loggerNames.forEach(this::fromLogger); + if (loggerNames != null) { + fromLoggers(loggerNames.toArray(new String[0])); + } } public void setPrinterTimestamps(boolean flag) { diff --git a/core/src/main/java/dev/vml/es/acm/core/code/ExecutionContext.java b/core/src/main/java/dev/vml/es/acm/core/code/ExecutionContext.java index 7a19538a8..40b021c90 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/ExecutionContext.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/ExecutionContext.java @@ -221,6 +221,15 @@ public Conditions getConditions() { } private void customizeBinding() { + codeContext + .getRepo() + .setLogger(printStream.bridgeLogger(codeContext.getRepo().getLogger())); + codeContext + .getAcl() + .getContext() + .setLogger(printStream.bridgeLogger( + codeContext.getAcl().getContext().getLogger())); + Binding binding = getCodeContext().getBinding(); binding.setVariable("context", this); diff --git a/core/src/main/java/dev/vml/es/acm/core/code/log/CodePrintLogger.java b/core/src/main/java/dev/vml/es/acm/core/code/log/CodePrintLogger.java new file mode 100644 index 000000000..70fe42a36 --- /dev/null +++ b/core/src/main/java/dev/vml/es/acm/core/code/log/CodePrintLogger.java @@ -0,0 +1,441 @@ +package dev.vml.es.acm.core.code.log; + +import java.util.function.Consumer; +import org.slf4j.Logger; +import org.slf4j.Marker; +import org.slf4j.helpers.MessageFormatter; + +/** + * SLF4J {@link Logger} decorator that forwards log messages directly to a {@link LogMessage} listener + * in addition to the underlying SLF4J logger, without relying on Logback internals or reflection. + */ +public class CodePrintLogger implements Logger { + + private static final String LEVEL_TRACE = "TRACE"; + private static final String LEVEL_DEBUG = "DEBUG"; + private static final String LEVEL_INFO = "INFO"; + private static final String LEVEL_WARN = "WARN"; + private static final String LEVEL_ERROR = "ERROR"; + + private final Logger delegate; + private final Consumer listener; + + public CodePrintLogger(Logger delegate, Consumer listener) { + this.delegate = delegate; + this.listener = listener; + } + + private void emit(String level, String message) { + if (listener != null) { + listener.accept(new LogMessage( + delegate.getName(), level, message != null ? message : "", System.currentTimeMillis())); + } + } + + private void emit(String level, String format, Object arg) { + emit(level, MessageFormatter.format(format, arg).getMessage()); + } + + private void emit(String level, String format, Object arg1, Object arg2) { + emit(level, MessageFormatter.format(format, arg1, arg2).getMessage()); + } + + private void emit(String level, String format, Object[] arguments) { + emit(level, MessageFormatter.arrayFormat(format, arguments).getMessage()); + } + + @Override + public String getName() { + return delegate.getName(); + } + + @Override + public boolean isTraceEnabled() { + return delegate.isTraceEnabled(); + } + + @Override + public void trace(String msg) { + delegate.trace(msg); + if (delegate.isTraceEnabled()) { + emit(LEVEL_TRACE, msg); + } + } + + @Override + public void trace(String format, Object arg) { + delegate.trace(format, arg); + if (delegate.isTraceEnabled()) { + emit(LEVEL_TRACE, format, arg); + } + } + + @Override + public void trace(String format, Object arg1, Object arg2) { + delegate.trace(format, arg1, arg2); + if (delegate.isTraceEnabled()) { + emit(LEVEL_TRACE, format, arg1, arg2); + } + } + + @Override + public void trace(String format, Object... arguments) { + delegate.trace(format, arguments); + if (delegate.isTraceEnabled()) { + emit(LEVEL_TRACE, format, arguments); + } + } + + @Override + public void trace(String msg, Throwable t) { + delegate.trace(msg, t); + if (delegate.isTraceEnabled()) { + emit(LEVEL_TRACE, msg); + } + } + + @Override + public boolean isTraceEnabled(Marker marker) { + return delegate.isTraceEnabled(marker); + } + + @Override + public void trace(Marker marker, String msg) { + delegate.trace(marker, msg); + if (delegate.isTraceEnabled(marker)) { + emit(LEVEL_TRACE, msg); + } + } + + @Override + public void trace(Marker marker, String format, Object arg) { + delegate.trace(marker, format, arg); + if (delegate.isTraceEnabled(marker)) { + emit(LEVEL_TRACE, format, arg); + } + } + + @Override + public void trace(Marker marker, String format, Object arg1, Object arg2) { + delegate.trace(marker, format, arg1, arg2); + if (delegate.isTraceEnabled(marker)) { + emit(LEVEL_TRACE, format, arg1, arg2); + } + } + + @Override + public void trace(Marker marker, String format, Object... argArray) { + delegate.trace(marker, format, argArray); + if (delegate.isTraceEnabled(marker)) { + emit(LEVEL_TRACE, format, argArray); + } + } + + @Override + public void trace(Marker marker, String msg, Throwable t) { + delegate.trace(marker, msg, t); + if (delegate.isTraceEnabled(marker)) { + emit(LEVEL_TRACE, msg); + } + } + + @Override + public boolean isDebugEnabled() { + return delegate.isDebugEnabled(); + } + + @Override + public void debug(String msg) { + delegate.debug(msg); + if (delegate.isDebugEnabled()) { + emit(LEVEL_DEBUG, msg); + } + } + + @Override + public void debug(String format, Object arg) { + delegate.debug(format, arg); + if (delegate.isDebugEnabled()) { + emit(LEVEL_DEBUG, format, arg); + } + } + + @Override + public void debug(String format, Object arg1, Object arg2) { + delegate.debug(format, arg1, arg2); + if (delegate.isDebugEnabled()) { + emit(LEVEL_DEBUG, format, arg1, arg2); + } + } + + @Override + public void debug(String format, Object... arguments) { + delegate.debug(format, arguments); + if (delegate.isDebugEnabled()) { + emit(LEVEL_DEBUG, format, arguments); + } + } + + @Override + public void debug(String msg, Throwable t) { + delegate.debug(msg, t); + if (delegate.isDebugEnabled()) { + emit(LEVEL_DEBUG, msg); + } + } + + @Override + public boolean isDebugEnabled(Marker marker) { + return delegate.isDebugEnabled(marker); + } + + @Override + public void debug(Marker marker, String msg) { + delegate.debug(marker, msg); + if (delegate.isDebugEnabled(marker)) { + emit(LEVEL_DEBUG, msg); + } + } + + @Override + public void debug(Marker marker, String format, Object arg) { + delegate.debug(marker, format, arg); + if (delegate.isDebugEnabled(marker)) { + emit(LEVEL_DEBUG, format, arg); + } + } + + @Override + public void debug(Marker marker, String format, Object arg1, Object arg2) { + delegate.debug(marker, format, arg1, arg2); + if (delegate.isDebugEnabled(marker)) { + emit(LEVEL_DEBUG, format, arg1, arg2); + } + } + + @Override + public void debug(Marker marker, String format, Object... arguments) { + delegate.debug(marker, format, arguments); + if (delegate.isDebugEnabled(marker)) { + emit(LEVEL_DEBUG, format, arguments); + } + } + + @Override + public void debug(Marker marker, String msg, Throwable t) { + delegate.debug(marker, msg, t); + if (delegate.isDebugEnabled(marker)) { + emit(LEVEL_DEBUG, msg); + } + } + + @Override + public boolean isInfoEnabled() { + return delegate.isInfoEnabled(); + } + + @Override + public void info(String msg) { + delegate.info(msg); + emit(LEVEL_INFO, msg); + } + + @Override + public void info(String format, Object arg) { + delegate.info(format, arg); + emit(LEVEL_INFO, format, arg); + } + + @Override + public void info(String format, Object arg1, Object arg2) { + delegate.info(format, arg1, arg2); + emit(LEVEL_INFO, format, arg1, arg2); + } + + @Override + public void info(String format, Object... arguments) { + delegate.info(format, arguments); + emit(LEVEL_INFO, format, arguments); + } + + @Override + public void info(String msg, Throwable t) { + delegate.info(msg, t); + emit(LEVEL_INFO, msg); + } + + @Override + public boolean isInfoEnabled(Marker marker) { + return delegate.isInfoEnabled(marker); + } + + @Override + public void info(Marker marker, String msg) { + delegate.info(marker, msg); + emit(LEVEL_INFO, msg); + } + + @Override + public void info(Marker marker, String format, Object arg) { + delegate.info(marker, format, arg); + emit(LEVEL_INFO, format, arg); + } + + @Override + public void info(Marker marker, String format, Object arg1, Object arg2) { + delegate.info(marker, format, arg1, arg2); + emit(LEVEL_INFO, format, arg1, arg2); + } + + @Override + public void info(Marker marker, String format, Object... arguments) { + delegate.info(marker, format, arguments); + emit(LEVEL_INFO, format, arguments); + } + + @Override + public void info(Marker marker, String msg, Throwable t) { + delegate.info(marker, msg, t); + emit(LEVEL_INFO, msg); + } + + @Override + public boolean isWarnEnabled() { + return delegate.isWarnEnabled(); + } + + @Override + public void warn(String msg) { + delegate.warn(msg); + emit(LEVEL_WARN, msg); + } + + @Override + public void warn(String format, Object arg) { + delegate.warn(format, arg); + emit(LEVEL_WARN, format, arg); + } + + @Override + public void warn(String format, Object... arguments) { + delegate.warn(format, arguments); + emit(LEVEL_WARN, format, arguments); + } + + @Override + public void warn(String format, Object arg1, Object arg2) { + delegate.warn(format, arg1, arg2); + emit(LEVEL_WARN, format, arg1, arg2); + } + + @Override + public void warn(String msg, Throwable t) { + delegate.warn(msg, t); + emit(LEVEL_WARN, msg); + } + + @Override + public boolean isWarnEnabled(Marker marker) { + return delegate.isWarnEnabled(marker); + } + + @Override + public void warn(Marker marker, String msg) { + delegate.warn(marker, msg); + emit(LEVEL_WARN, msg); + } + + @Override + public void warn(Marker marker, String format, Object arg) { + delegate.warn(marker, format, arg); + emit(LEVEL_WARN, format, arg); + } + + @Override + public void warn(Marker marker, String format, Object arg1, Object arg2) { + delegate.warn(marker, format, arg1, arg2); + emit(LEVEL_WARN, format, arg1, arg2); + } + + @Override + public void warn(Marker marker, String format, Object... arguments) { + delegate.warn(marker, format, arguments); + emit(LEVEL_WARN, format, arguments); + } + + @Override + public void warn(Marker marker, String msg, Throwable t) { + delegate.warn(marker, msg, t); + emit(LEVEL_WARN, msg); + } + + @Override + public boolean isErrorEnabled() { + return delegate.isErrorEnabled(); + } + + @Override + public void error(String msg) { + delegate.error(msg); + emit(LEVEL_ERROR, msg); + } + + @Override + public void error(String format, Object arg) { + delegate.error(format, arg); + emit(LEVEL_ERROR, format, arg); + } + + @Override + public void error(String format, Object arg1, Object arg2) { + delegate.error(format, arg1, arg2); + emit(LEVEL_ERROR, format, arg1, arg2); + } + + @Override + public void error(String format, Object... arguments) { + delegate.error(format, arguments); + emit(LEVEL_ERROR, format, arguments); + } + + @Override + public void error(String msg, Throwable t) { + delegate.error(msg, t); + emit(LEVEL_ERROR, msg); + } + + @Override + public boolean isErrorEnabled(Marker marker) { + return delegate.isErrorEnabled(marker); + } + + @Override + public void error(Marker marker, String msg) { + delegate.error(marker, msg); + emit(LEVEL_ERROR, msg); + } + + @Override + public void error(Marker marker, String format, Object arg) { + delegate.error(marker, format, arg); + emit(LEVEL_ERROR, format, arg); + } + + @Override + public void error(Marker marker, String format, Object arg1, Object arg2) { + delegate.error(marker, format, arg1, arg2); + emit(LEVEL_ERROR, format, arg1, arg2); + } + + @Override + public void error(Marker marker, String format, Object... arguments) { + delegate.error(marker, format, arguments); + emit(LEVEL_ERROR, format, arguments); + } + + @Override + public void error(Marker marker, String msg, Throwable t) { + delegate.error(marker, msg, t); + emit(LEVEL_ERROR, msg); + } +} diff --git a/core/src/main/java/dev/vml/es/acm/core/code/log/LogbackAppenderFactory.java b/core/src/main/java/dev/vml/es/acm/core/code/log/LogbackAppenderFactory.java index 6da5f3416..693101e15 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/log/LogbackAppenderFactory.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/log/LogbackAppenderFactory.java @@ -4,7 +4,9 @@ import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import java.util.function.Consumer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -19,44 +21,70 @@ class LogbackAppenderFactory { private static final String LOGBACK_LOGGING_EVENT = "ch.qos.logback.classic.spi.ILoggingEvent"; private final ClassLoader classLoader; + private final Object loggerContext; private final Class loggerContextClass; private final Class appenderClass; private final Class contextClass; private final Class eventClass; - LogbackAppenderFactory(ClassLoader classLoader) throws ClassNotFoundException { + LogbackAppenderFactory(ClassLoader classLoader, Object loggerContext) throws ClassNotFoundException { this.classLoader = classLoader; this.loggerContextClass = classLoader.loadClass(LOGBACK_LOGGER_CONTEXT); + if (loggerContext == null || !loggerContextClass.isInstance(loggerContext)) { + throw new IllegalStateException("SLF4J is not using Logback"); + } + this.loggerContext = loggerContext; this.appenderClass = classLoader.loadClass(LOGBACK_APPENDER); this.contextClass = classLoader.loadClass(LOGBACK_CONTEXT); this.eventClass = classLoader.loadClass(LOGBACK_LOGGING_EVENT); } - Object attach(String name, Consumer listener, List loggerNames) + Attachment attach(String name, Consumer listener, List loggerNames) throws ReflectiveOperationException { Object appender = createAppender(name, listener, loggerNames); + List targetLoggers = resolveTargetLoggers(loggerNames); List attached = new ArrayList<>(); try { - Object loggerContext = getLoggerContext(); initAppender(appender, loggerContext); - for (String loggerName : loggerNames) { + for (String loggerName : targetLoggers) { addAppenderToLogger(loggerContext, appender, loggerName); attached.add(loggerName); } - return appender; + return new Attachment(appender, attached); } catch (ReflectiveOperationException e) { detach(appender, attached); throw e; } } + private List resolveTargetLoggers(List loggerNames) { + Set targets = new LinkedHashSet<>(loggerNames); + try { + List existingLoggers = + (List) loggerContextClass.getMethod("getLoggerList").invoke(loggerContext); + if (existingLoggers != null) { + for (Object logger : existingLoggers) { + String existingName = + (String) logger.getClass().getMethod("getName").invoke(logger); + Boolean additive = + (Boolean) logger.getClass().getMethod("isAdditive").invoke(logger); + if (Boolean.FALSE.equals(additive) && matchesPrefix(existingName, loggerNames)) { + targets.add(existingName); + } + } + } + } catch (Exception e) { + LOG.debug("Cannot inspect existing loggers for non-additive children", e); + } + return new ArrayList<>(targets); + } + void detach(Object appender, List loggerNames) { if (appender == null) { return; } try { if (loggerNames != null) { - Object loggerContext = getLoggerContext(); for (String loggerName : loggerNames) { try { detachAppenderFromLogger(loggerContext, appender, loggerName); @@ -65,8 +93,6 @@ void detach(Object appender, List loggerNames) { } } } - } catch (Exception e) { - LOG.warn("Failed to obtain logger context for detach", e); } finally { try { stopAppender(appender); @@ -84,20 +110,12 @@ private Object createAppender(String name, Consumer listener, List loggerPrefixes) { + if (loggerName == null || loggerPrefixes == null) { + return false; + } + for (String prefix : loggerPrefixes) { + if (prefix != null && loggerName.startsWith(prefix)) { + return true; + } + } + return false; + } + + class Attachment { + private final Object appender; + private final List attachedLoggers; + + Attachment(Object appender, List attachedLoggers) { + this.appender = appender; + this.attachedLoggers = attachedLoggers; + } + + List getAttachedLoggers() { + return attachedLoggers; + } + + void detach() { + LogbackAppenderFactory.this.detach(appender, attachedLoggers); + } + } + private static class AppenderHandler implements InvocationHandler { private final String appenderName; @@ -119,7 +167,8 @@ private static class AppenderHandler implements InvocationHandler { private final Consumer listener; private final List loggerPrefixes; private String name; - private boolean started; + private boolean started = true; + private volatile Object lastEvent; AppenderHandler(String name, Class eventClass, Consumer listener, List loggerPrefixes) { this.appenderName = name; @@ -166,9 +215,13 @@ public Object invoke(Object proxy, Method method, Object[] args) { private void processEvent(Object event) { try { + if (event == lastEvent) { + return; + } + lastEvent = event; String loggerName = (String) eventClass.getMethod("getLoggerName").invoke(event); - if (!matchesPrefix(loggerName)) { + if (!matchesPrefix(loggerName, loggerPrefixes)) { return; } String message = @@ -177,21 +230,9 @@ private void processEvent(Object event) { Long ts = (Long) eventClass.getMethod("getTimeStamp").invoke(event); listener.accept( new LogMessage(loggerName, level, message, ts != null ? ts : System.currentTimeMillis())); - } catch (Exception ignored) { - // Never disrupt logging - } - } - - private boolean matchesPrefix(String loggerName) { - if (loggerName == null) { - return false; - } - for (String prefix : loggerPrefixes) { - if (prefix != null && loggerName.startsWith(prefix)) { - return true; - } + } catch (Exception e) { + LOG.trace("Failed to process log event", e); } - return false; } private Object defaultValue(Class type) { diff --git a/core/src/main/java/dev/vml/es/acm/core/code/log/LogbackLogInterceptor.java b/core/src/main/java/dev/vml/es/acm/core/code/log/LogbackLogInterceptor.java index a343dbff0..e5c99f246 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/log/LogbackLogInterceptor.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/log/LogbackLogInterceptor.java @@ -3,9 +3,7 @@ import java.util.Arrays; import java.util.List; import java.util.function.Consumer; -import org.apache.sling.commons.classloader.DynamicClassLoaderManager; import org.osgi.service.component.annotations.Component; -import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,9 +14,6 @@ public class LogbackLogInterceptor implements LogInterceptor { private static final String APPENDER_NAME = "ACM-LogbackLogInterceptor"; - @Reference - private DynamicClassLoaderManager classLoaderManager; - @Override public boolean isAvailable() { return getFactory() != null; @@ -36,9 +31,9 @@ public Handle attach(Consumer listener, String... loggerNames) { } try { List loggerList = Arrays.asList(loggerNames); - Object appender = factory.attach(APPENDER_NAME, listener, loggerList); - LOG.debug("Attached appender to loggers: {}", loggerList); - return () -> factory.detach(appender, loggerList); + LogbackAppenderFactory.Attachment attachment = factory.attach(APPENDER_NAME, listener, loggerList); + LOG.debug("Attached appender to loggers: {}", attachment.getAttachedLoggers()); + return attachment::detach; } catch (Exception e) { LOG.error("Failed to attach log interceptor", e); return () -> {}; @@ -46,16 +41,22 @@ public Handle attach(Consumer listener, String... loggerNames) { } private LogbackAppenderFactory getFactory() { - if (classLoaderManager == null) { - return null; - } try { - ClassLoader cl = classLoaderManager.getDynamicClassLoader(); + Object loggerContext = LoggerFactory.getILoggerFactory(); + if (loggerContext == null) { + LOG.debug("Cannot obtain SLF4J ILoggerFactory"); + return null; + } + ClassLoader cl = loggerContext.getClass().getClassLoader(); if (cl == null) { + LOG.debug( + "SLF4J ILoggerFactory '{}' has no class loader", + loggerContext.getClass().getName()); return null; } - return new LogbackAppenderFactory(cl); + return new LogbackAppenderFactory(cl, loggerContext); } catch (Exception e) { + LOG.debug("Logback log interceptor is not available", e); return null; } } diff --git a/core/src/main/java/dev/vml/es/acm/core/repo/Repo.java b/core/src/main/java/dev/vml/es/acm/core/repo/Repo.java index 2f00c4dc1..c1102f4b5 100644 --- a/core/src/main/java/dev/vml/es/acm/core/repo/Repo.java +++ b/core/src/main/java/dev/vml/es/acm/core/repo/Repo.java @@ -19,6 +19,8 @@ public class Repo implements CommitPolicy { private static final Logger LOG = LoggerFactory.getLogger(Repo.class); + private Logger logger = LOG; + private final ResourceResolver resourceResolver; private final Session session; @@ -62,7 +64,11 @@ public void setQuiet(boolean quiet) { } public Logger getLogger() { - return quiet ? NOPLogger.NOP_LOGGER : LOG; + return quiet ? NOPLogger.NOP_LOGGER : logger; + } + + public void setLogger(Logger logger) { + this.logger = logger != null ? logger : LOG; } public void autoCommit(boolean enabled) { diff --git a/core/src/test/java/dev/vml/es/acm/core/code/CodePrintStreamTest.java b/core/src/test/java/dev/vml/es/acm/core/code/CodePrintStreamTest.java index 50c697962..3bca597cb 100644 --- a/core/src/test/java/dev/vml/es/acm/core/code/CodePrintStreamTest.java +++ b/core/src/test/java/dev/vml/es/acm/core/code/CodePrintStreamTest.java @@ -1,9 +1,19 @@ package dev.vml.es.acm.core.code; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import dev.vml.es.acm.core.code.log.LogInterceptor; +import dev.vml.es.acm.core.code.log.LogInterceptorManager; +import dev.vml.es.acm.core.code.log.LogMessage; import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; class CodePrintStreamTest { @@ -80,4 +90,64 @@ void shouldPrintMultipleMessagesWithDifferentLevels() { assertTrue(output.contains("[DEBUG] Debug message")); assertTrue(output.contains("[SUCCESS] Success message")); } + + @Test + void shouldBridgeSelfAndRecommendedLoggersWithoutInterceptor() { + List interceptedLoggers = new ArrayList<>(); + LogInterceptorManager manager = new LogInterceptorManager() { + @Override + public LogInterceptor.Handle attach(Consumer listener, String... loggerNames) { + interceptedLoggers.addAll(Arrays.asList(loggerNames)); + return () -> {}; + } + }; + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (CodePrintStream out = new CodePrintStream(outputStream, "console|1", manager)) { + Logger repoLogger = out.bridgeLogger(LoggerFactory.getLogger(CodePrintStream.LOGGER_NAME_REPO)); + out.fromSelfLogger(); + out.fromRecommendedLoggers(); + + out.getLogger().info("Self message {}", 1); + repoLogger.info("Repo message {}", 2); + } + + assertTrue(interceptedLoggers.isEmpty()); + String output = outputStream.toString(); + assertTrue(output.contains("[INFO] Self message 1")); + assertTrue(output.contains("[INFO] Repo message 2")); + } + + @Test + void shouldKeepOutputEmptyWhenLogPrintingNotEnabled() { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (CodePrintStream out = new CodePrintStream(outputStream, "console|1", null)) { + Logger repoLogger = out.bridgeLogger(LoggerFactory.getLogger(CodePrintStream.LOGGER_NAME_REPO)); + out.getLogger().info("Should not be printed"); + repoLogger.info("Should not be printed either"); + } + + assertEquals("", outputStream.toString()); + } + + @Test + void shouldAttachInterceptorOnlyForExternalLoggers() { + List interceptedLoggers = new ArrayList<>(); + LogInterceptorManager manager = new LogInterceptorManager() { + @Override + public LogInterceptor.Handle attach(Consumer listener, String... loggerNames) { + interceptedLoggers.addAll(Arrays.asList(loggerNames)); + return () -> {}; + } + }; + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (CodePrintStream out = new CodePrintStream(outputStream, "console|1", manager)) { + out.fromSelfLogger(); + out.fromRecommendedLoggers(); + out.fromLogger("com.acme.custom"); + } + + assertEquals(Arrays.asList("com.acme.custom"), interceptedLoggers); + } } diff --git a/mvnw b/mvnw index 88a9d2383..05820d275 100755 --- a/mvnw +++ b/mvnw @@ -60,6 +60,13 @@ if [ "${AEM_JAVA_ENABLED}" = true ]; then JAVA_HOME="$AEM_JAVA_HOME"; export JAVA_HOME fi +if [ -n "$JAVA_HOME" ] && [ -x "$JAVA_HOME/bin/java" ]; then + if "$JAVA_HOME/bin/java" --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED -version >/dev/null 2>&1; then + MAVEN_OPTS="${MAVEN_OPTS} --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED" + export MAVEN_OPTS + fi +fi + # ---------------------------------------------------------------------------- # Apache Maven Wrapper startup batch script, version 3.2.0 # diff --git a/pom.xml b/pom.xml index 9068ab8df..9a1cb7875 100644 --- a/pom.xml +++ b/pom.xml @@ -71,6 +71,8 @@ admin admin 5.1.2 + 2.30.0 + 1.1.0 UTF-8 UTF-8 false @@ -203,7 +205,7 @@ com.diffplug.spotless spotless-maven-plugin - 2.30.0 + ${spotless.version} @@ -688,6 +690,25 @@ Private-Package: com.fasterxml.jackson.datatype.jsr310 + + java11-spotless + + [11,17) + + + 2.28.0 + + + + java17plus-spotless + + [17,) + + + 2.43.0 + 2.39.0 + + diff --git a/test/e2e/002-console.spec.ts b/test/e2e/002-console.spec.ts index e38831b3c..4d8f464c9 100644 --- a/test/e2e/002-console.spec.ts +++ b/test/e2e/002-console.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from '@playwright/test'; -import { expectCompilationSucceeded, expectExecutionProgressBarSucceeded, expectToHaveMultilineText } from './utils/expect' +import { expectCompilationSucceeded, expectExecutionProgressBarSucceeded } from './utils/expect' import { readFromCodeEditor, writeToCodeEditor } from './utils/editor'; import { attachScreenshot } from './utils/page'; @@ -16,6 +16,7 @@ test.describe('Console', () => { void doRun() { println "Hello World!" + log.info "Hello Logs!" } `); await expectCompilationSucceeded(page); @@ -27,9 +28,8 @@ test.describe('Console', () => { await expectExecutionProgressBarSucceeded(page); const output = await readFromCodeEditor(page, 'Console Output'); - expectToHaveMultilineText(output, ` - Hello World! - `); + expect(output).toContain('Hello World!'); + expect(output).toContain('[INFO] Hello Logs!'); await attachScreenshot(page, testInfo, 'Console Output'); }); }); diff --git a/tools/shared/src/catalog/api.json b/tools/shared/src/catalog/api.json index f45ff4192..8251b4859 100644 --- a/tools/shared/src/catalog/api.json +++ b/tools/shared/src/catalog/api.json @@ -1262,6 +1262,11 @@ "name": "getChecker", "returns": "AclChecker", "params": [] + }, + { + "name": "getContext", + "returns": "AclContext", + "params": [] } ] }, @@ -1672,6 +1677,152 @@ } ] }, + "dev.vml.es.acm.core.acl.AclContext": { + "name": "AclContext", + "methods": [ + { + "name": "getLogger", + "returns": "Logger", + "params": [] + }, + { + "name": "setLogger", + "returns": "void", + "params": [ + { + "type": "Logger", + "name": "logger" + } + ] + }, + { + "name": "getResourceResolver", + "returns": "ResourceResolver", + "params": [] + }, + { + "name": "getAuthorizableManager", + "returns": "AuthorizableManager", + "params": [] + }, + { + "name": "getPermissionsManager", + "returns": "PermissionsManager", + "params": [] + }, + { + "name": "isCompositeNodeStore", + "returns": "boolean", + "params": [] + }, + { + "name": "determineUser", + "returns": "AclUser", + "params": [ + { + "type": "User", + "name": "user" + } + ] + }, + { + "name": "determineUser", + "returns": "AclUser", + "params": [ + { + "type": "String", + "name": "id" + } + ] + }, + { + "name": "determineUser", + "returns": "AclUser", + "params": [ + { + "type": "AclUser", + "name": "user" + }, + { + "type": "String", + "name": "id" + } + ] + }, + { + "name": "determineGroup", + "returns": "AclGroup", + "params": [ + { + "type": "Group", + "name": "group" + } + ] + }, + { + "name": "determineGroup", + "returns": "AclGroup", + "params": [ + { + "type": "String", + "name": "id" + } + ] + }, + { + "name": "determineGroup", + "returns": "AclGroup", + "params": [ + { + "type": "AclGroup", + "name": "group" + }, + { + "type": "String", + "name": "id" + } + ] + }, + { + "name": "determineAuthorizable", + "returns": "AclAuthorizable", + "params": [ + { + "type": "AclAuthorizable", + "name": "authorizable" + }, + { + "type": "String", + "name": "id" + } + ] + }, + { + "name": "determineAuthorizable", + "returns": "AclAuthorizable", + "params": [ + { + "type": "Authorizable", + "name": "authorizable" + } + ] + }, + { + "name": "determineId", + "returns": "String", + "params": [ + { + "type": "AclAuthorizable", + "name": "authorizable" + }, + { + "type": "String", + "name": "id" + } + ] + } + ] + }, "dev.vml.es.acm.core.acl.authorizable.AclAuthorizable": { "name": "AclAuthorizable", "methods": [ @@ -4028,6 +4179,324 @@ } ] }, + "dev.vml.es.acm.core.acl.utils.AuthorizableManager": { + "name": "AuthorizableManager", + "methods": [ + { + "name": "createUser", + "returns": "User", + "params": [ + { + "type": "String", + "name": "id" + }, + { + "type": "String", + "name": "password" + }, + { + "type": "String", + "name": "path" + } + ] + }, + { + "name": "createGroup", + "returns": "Group", + "params": [ + { + "type": "String", + "name": "id" + }, + { + "type": "String", + "name": "path" + }, + { + "type": "String", + "name": "externalId" + } + ] + }, + { + "name": "createSystemUser", + "returns": "User", + "params": [ + { + "type": "String", + "name": "id" + }, + { + "type": "String", + "name": "path" + } + ] + }, + { + "name": "updateUser", + "returns": "void", + "params": [ + { + "type": "User", + "name": "user" + }, + { + "type": "String", + "name": "password" + }, + { + "type": "Map", + "name": "properties" + } + ] + }, + { + "name": "updateGroup", + "returns": "void", + "params": [ + { + "type": "Group", + "name": "group" + }, + { + "type": "Map", + "name": "properties" + } + ] + }, + { + "name": "deleteAuthorizable", + "returns": "void", + "params": [ + { + "type": "Authorizable", + "name": "authorizable" + } + ] + }, + { + "name": "addMember", + "returns": "boolean", + "params": [ + { + "type": "Group", + "name": "group" + }, + { + "type": "Authorizable", + "name": "member" + } + ] + }, + { + "name": "removeMember", + "returns": "boolean", + "params": [ + { + "type": "Group", + "name": "group" + }, + { + "type": "Authorizable", + "name": "member" + } + ] + }, + { + "name": "getAuthorizable", + "returns": "Authorizable", + "params": [ + { + "type": "String", + "name": "id" + } + ] + }, + { + "name": "getAuthorizable", + "returns": "T", + "params": [ + { + "type": "String", + "name": "id" + }, + { + "type": "Class", + "name": "clazz" + } + ] + }, + { + "name": "getUser", + "returns": "User", + "params": [ + { + "type": "String", + "name": "id" + } + ] + }, + { + "name": "getGroup", + "returns": "Group", + "params": [ + { + "type": "String", + "name": "id" + } + ] + }, + { + "name": "testPassword", + "returns": "boolean", + "params": [ + { + "type": "User", + "name": "user" + }, + { + "type": "String", + "name": "password" + } + ] + }, + { + "name": "changePassword", + "returns": "void", + "params": [ + { + "type": "User", + "name": "user" + }, + { + "type": "String", + "name": "password" + } + ] + }, + { + "name": "getProperty", + "returns": "List", + "params": [ + { + "type": "Authorizable", + "name": "authorizable" + }, + { + "type": "String", + "name": "relPath" + } + ] + }, + { + "name": "setProperty", + "returns": "void", + "params": [ + { + "type": "Authorizable", + "name": "authorizable" + }, + { + "type": "String", + "name": "relPath" + }, + { + "type": "String", + "name": "value" + } + ] + }, + { + "name": "removeProperty", + "returns": "boolean", + "params": [ + { + "type": "Authorizable", + "name": "authorizable" + }, + { + "type": "String", + "name": "relPath" + } + ] + } + ] + }, + "dev.vml.es.acm.core.acl.utils.PermissionsManager": { + "name": "PermissionsManager", + "methods": [ + { + "name": "apply", + "returns": "void", + "params": [ + { + "type": "Authorizable", + "name": "authorizable" + }, + { + "type": "String", + "name": "path" + }, + { + "type": "List", + "name": "permissions" + }, + { + "type": "Map", + "name": "restrictions" + }, + { + "type": "boolean", + "name": "allow" + } + ] + }, + { + "name": "clear", + "returns": "boolean", + "params": [ + { + "type": "Authorizable", + "name": "authorizable" + }, + { + "type": "String", + "name": "path" + }, + { + "type": "boolean", + "name": "strict" + } + ] + }, + { + "name": "check", + "returns": "boolean", + "params": [ + { + "type": "Authorizable", + "name": "authorizable" + }, + { + "type": "String", + "name": "path" + }, + { + "type": "List", + "name": "permissions" + }, + { + "type": "Map", + "name": "restrictions" + }, + { + "type": "boolean", + "name": "allow" + } + ] + } + ] + }, "dev.vml.es.acm.core.code.CodeContext": { "name": "CodeContext", "methods": [ @@ -11023,6 +11492,16 @@ "returns": "Logger", "params": [] }, + { + "name": "setLogger", + "returns": "void", + "params": [ + { + "type": "Logger", + "name": "logger" + } + ] + }, { "name": "autoCommit", "returns": "void", diff --git a/tools/skills/acm-groovy-script/references/api.md b/tools/skills/acm-groovy-script/references/api.md index db1ecbcea..428053105 100644 --- a/tools/skills/acm-groovy-script/references/api.md +++ b/tools/skills/acm-groovy-script/references/api.md @@ -166,6 +166,7 @@ AclUser user(User user) AclGroup group(Group group) AclAuthorizable authorizable(Authorizable authorizable) AclChecker getChecker() +AclContext getContext() ``` ### AclChecker @@ -206,6 +207,28 @@ boolean password(String id, String password) boolean property(String id, String relPath, String value) ``` +### AclContext + +`dev.vml.es.acm.core.acl.AclContext` + +```groovy +Logger getLogger() +void setLogger(Logger logger) +ResourceResolver getResourceResolver() +AuthorizableManager getAuthorizableManager() +PermissionsManager getPermissionsManager() +boolean isCompositeNodeStore() +AclUser determineUser(User user) +AclUser determineUser(String id) +AclUser determineUser(AclUser user, String id) +AclGroup determineGroup(Group group) +AclGroup determineGroup(String id) +AclGroup determineGroup(AclGroup group, String id) +AclAuthorizable determineAuthorizable(AclAuthorizable authorizable, String id) +AclAuthorizable determineAuthorizable(Authorizable authorizable) +String determineId(AclAuthorizable authorizable, String id) +``` + ### AclAuthorizable `dev.vml.es.acm.core.acl.authorizable.AclAuthorizable` @@ -690,6 +713,40 @@ String getAuthorizableId() void setAuthorizableId(String authorizableId) ``` +### AuthorizableManager + +`dev.vml.es.acm.core.acl.utils.AuthorizableManager` + +```groovy +User createUser(String id, String password, String path) +Group createGroup(String id, String path, String externalId) +User createSystemUser(String id, String path) +void updateUser(User user, String password, Map properties) +void updateGroup(Group group, Map properties) +void deleteAuthorizable(Authorizable authorizable) +boolean addMember(Group group, Authorizable member) +boolean removeMember(Group group, Authorizable member) +Authorizable getAuthorizable(String id) +T getAuthorizable(String id, Class clazz) +User getUser(String id) +Group getGroup(String id) +boolean testPassword(User user, String password) +void changePassword(User user, String password) +List getProperty(Authorizable authorizable, String relPath) +void setProperty(Authorizable authorizable, String relPath, String value) +boolean removeProperty(Authorizable authorizable, String relPath) +``` + +### PermissionsManager + +`dev.vml.es.acm.core.acl.utils.PermissionsManager` + +```groovy +void apply(Authorizable authorizable, String path, List permissions, Map restrictions, boolean allow) +boolean clear(Authorizable authorizable, String path, boolean strict) +boolean check(Authorizable authorizable, String path, List permissions, Map restrictions, boolean allow) +``` + ### CodeContext `dev.vml.es.acm.core.code.CodeContext` @@ -1989,6 +2046,7 @@ void setAutoCommit(boolean autoCommit) boolean isQuiet() void setQuiet(boolean quiet) Logger getLogger() +void setLogger(Logger logger) void autoCommit(boolean enabled) void commit() void commit(String context)