Add unit tests
diff --git a/README.adoc b/README.adoc index e8df97e..eb361d2 100644 --- a/README.adoc +++ b/README.adoc
@@ -30,7 +30,7 @@ while (i < args.length) { if ("--logLevel".equals(args[i]) && ++i < args.length) { LoggingAdmin admin = LoggingAdmin.getInstance(TOKEN); - admin.setLoggerLevel("", args[i]); + admin.setLevel("", args[i]); } i++; }
diff --git a/pom.xml b/pom.xml index ac6eef5..93fd52e 100644 --- a/pom.xml +++ b/pom.xml
@@ -70,12 +70,44 @@ <!-- project version --> <revision>0.1.0-SNAPSHOT</revision> + <!-- Ignore BND Baseline until the first release --> + <bnd.baseline.skip>true</bnd.baseline.skip> + + <bnd-module-name>org.apache.logging.admin</bnd-module-name> + <bnd-extra-package-options> + <!-- Optional dependencies --> + ch.qos.logback.*;resolution:=optional, + java.util.logging;resolution:=optional, + org.slf4j;resolution:=optional, + org.apache.logging.log4j.*;resolution:=optional, + </bnd-extra-package-options> + <!-- disable `maven-site-plugin`--> <maven.site.skip>true</maven.site.skip> <maven.site.deploy.skip>true</maven.site.deploy.skip> </properties> + <dependencyManagement> + <dependencies> + <dependency> + <groupId>org.assertj</groupId> + <artifactId>assertj-bom</artifactId> + <version>3.26.3</version> + <type>pom</type> + <scope>import</scope> + </dependency> + + <dependency> + <groupId>org.junit</groupId> + <artifactId>junit-bom</artifactId> + <version>5.11.3</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> + </dependencyManagement> + <dependencies> <dependency> @@ -100,12 +132,6 @@ </dependency> <dependency> - <groupId>com.github.spotbugs</groupId> - <artifactId>spotbugs-annotations</artifactId> - <scope>provided</scope> - </dependency> - - <dependency> <groupId>org.jspecify</groupId> <artifactId>jspecify</artifactId> <version>1.0.0</version> @@ -125,5 +151,87 @@ <optional>true</optional> </dependency> + <dependency> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-api</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-params</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <executions> + <!-- Disable default execution to properly name each execution --> + <execution> + <id>default-test</id> + <phase>none</phase> + </execution> + <execution> + <id>jul-test</id> + <goals> + <goal>test</goal> + </goals> + <configuration> + <classpathDependencyExcludes> + <exclude>ch.qos.logback:*</exclude> + <exclude>org.apache.logging.log4j:*</exclude> + <exclude>org.slf4j:*</exclude> + </classpathDependencyExcludes> + <systemPropertyVariables> + <admin.implementation>jul</admin.implementation> + <java.util.logging.config.file>${project.basedir}/src/test/resources/logging.properties</java.util.logging.config.file> + </systemPropertyVariables> + </configuration> + </execution> + <execution> + <id>log4j-core-test</id> + <goals> + <goal>test</goal> + </goals> + <configuration> + <classpathDependencyExcludes> + <exclude>ch.qos.logback:*</exclude> + <exclude>org.slf4j:*</exclude> + </classpathDependencyExcludes> + <systemPropertyVariables> + <admin.implementation>log4j-core</admin.implementation> + </systemPropertyVariables> + </configuration> + </execution> + <execution> + <id>logback-test</id> + <goals> + <goal>test</goal> + </goals> + <configuration> + <classpathDependencyExcludes> + <exclude>org.apache.logging.log4j:*</exclude> + </classpathDependencyExcludes> + <systemPropertyVariables> + <admin.implementation>logback</admin.implementation> + </systemPropertyVariables> + </configuration> + </execution> + </executions> + </plugin> + + </plugins> + </build> </project>
diff --git a/src/main/java/org/apache/logging/admin/LoggingAdmin.java b/src/main/java/org/apache/logging/admin/LoggingAdmin.java index ce2be20..0954b05 100644 --- a/src/main/java/org/apache/logging/admin/LoggingAdmin.java +++ b/src/main/java/org/apache/logging/admin/LoggingAdmin.java
@@ -16,11 +16,11 @@ */ package org.apache.logging.admin; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.Set; -import org.apache.logging.admin.internal.FactoryHolder; +import org.apache.logging.admin.internal.FactoryUtil; import org.jspecify.annotations.Nullable; /** @@ -48,7 +48,7 @@ * while (i < args.length) { * if ("--logLevel".equals(args[i]) && ++i < args.length) { * LoggingAdmin admin = LoggingAdmin.getInstance(TOKEN); - * admin.setLoggerLevel("", args[i]); + * admin.setLevel("", args[i]); * } * i++; * } @@ -57,26 +57,23 @@ * </pre> */ public interface LoggingAdmin { + /** + * Implementation independent name for the root logger. + */ + String ROOT_LOGGER_NAME = ""; /** - * The logger context that this interface will be configuring. - * - * @return A logger context. + * The names of the available log levels in decreasing severity order. */ - Object getLoggerContext(); - - /** - * The names of the available log levels. - */ - Set<String> getSupportedLevels(); + List<String> getSupportedLevels(); /** * A map associating logger names with the configured log levels. * <p> - * Loggers that inherit their configuration from the parent logger will be associated with {@link Optional#empty()}. + * Loggers that inherit their configuration from the parent logger will be associated with {@code null}. * </p> */ - Map<String, Optional<String>> getLoggerLevels(); + Map<String, @Nullable String> getLevels(); /** * The configured log level for the given logger. @@ -85,7 +82,8 @@ * </p> * @param loggerName The name of the logger. */ - Optional<String> getLoggerLevel(String loggerName); + @Nullable + String getLevel(String loggerName); /** * Sets the level for a logger. @@ -93,41 +91,21 @@ * @param loggerName The name of the logger. * @param level The level to use or {@code null} to inherit the level of the parent logger. */ - void setLoggerLevel(String loggerName, @Nullable String level); + void setLevel(String loggerName, @Nullable String level); /** - * Retrieves the logging configuration admin for the given classloader. + * Retrieves the logging configuration admin appropriate for the caller * <p> - * If {@code token} is not null, it also sets a security token for the appropriate logger context. + * The {@code token} parameter sets a security token for the appropriate logger context. * All future invocations of this method will need to use the same token. * Tokens are compared using object equality. * </p> - * @param token The security token. - * @param classLoader A class loader. + * @param token Any Java object. * @return A logging configuration admin. - * @throws IllegalStateException If no implementation of the Apache Logging Admin API is present on the classpath. - * @throws SecurityException If a security token is set for the associated logger context and the provided token - * does not match. - */ - static LoggingAdmin getInstance(Object token, ClassLoader classLoader) { - return FactoryHolder.getLoggingConfigurationAdmin( - Objects.requireNonNull(token), Objects.requireNonNull(classLoader)); - } - - /** - * Retrieves the logging configuration admin for the classloader that loaded this class. - * <p> - * If {@code token} is not null, it also sets a security token for the appropriate logger context. - * All future invocations of this method will need to use the same token. - * Tokens are compared using object equality. - * </p> - * @return A logging configuration admin. - * @throws IllegalStateException If no implementation of the Apache Logging Admin API is present on the classpath. * @throws SecurityException If a security token is set for the associated logger context and the provided token * does not match. */ static LoggingAdmin getInstance(Object token) { - return FactoryHolder.getLoggingConfigurationAdmin( - Objects.requireNonNull(token), LoggingAdmin.class.getClassLoader()); + return FactoryUtil.getLoggingAdmin(Objects.requireNonNull(token)); } }
diff --git a/src/main/java/org/apache/logging/admin/internal/FactoryHolder.java b/src/main/java/org/apache/logging/admin/internal/FactoryHolder.java deleted file mode 100644 index 08a94c3..0000000 --- a/src/main/java/org/apache/logging/admin/internal/FactoryHolder.java +++ /dev/null
@@ -1,79 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to you under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.logging.admin.internal; - -import aQute.bnd.annotation.Cardinality; -import aQute.bnd.annotation.Resolution; -import aQute.bnd.annotation.spi.ServiceConsumer; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Map; -import java.util.ServiceLoader; -import java.util.WeakHashMap; -import java.util.concurrent.locks.ReentrantLock; -import org.apache.logging.admin.LoggingAdmin; -import org.apache.logging.admin.spi.LoggingAdminFactory; - -@ServiceConsumer( - value = LoggingAdminFactory.class, - cardinality = Cardinality.MULTIPLE, - resolution = Resolution.MANDATORY) -public final class FactoryHolder { - - private static final Collection<LoggingAdminFactory> factories = new ArrayList<>(); - private static final ReentrantLock lock = new ReentrantLock(); - private static final Map<ClassLoader, LoggingAdmin> instances = new WeakHashMap<>(); - private static final Map<Object, Object> tokensByLoggerContext = new WeakHashMap<>(); - - static { - ServiceLoader.load(LoggingAdminFactory.class, FactoryHolder.class.getClassLoader()) - .forEach(factories::add); - } - - private static void checkSecurityToken(Object token, Object loggerContext) { - Object oldToken = tokensByLoggerContext.get(loggerContext); - if (oldToken == null) { - tokensByLoggerContext.put(loggerContext, token); - return; - } - if (token != oldToken) { - throw new SecurityException("Security token changed from " + oldToken + " to " + token); - } - } - - public static LoggingAdmin getLoggingConfigurationAdmin(Object token, ClassLoader classLoader) { - lock.lock(); - try { - LoggingAdmin instance = instances.get(classLoader); - if (instance == null) { - instance = factories.stream() - .filter(f -> f.isActive(classLoader)) - .findAny() - .map(f -> f.createLoggingConfigurationAdmin(classLoader)) - .orElseThrow(() -> - new IllegalStateException("No " + LoggingAdminFactory.class.getName() + " found.")); - instances.put(classLoader, instance); - } - checkSecurityToken(token, instance.getLoggerContext()); - return instance; - } finally { - lock.unlock(); - } - } - - private FactoryHolder() {} -}
diff --git a/src/main/java/org/apache/logging/admin/internal/FactoryUtil.java b/src/main/java/org/apache/logging/admin/internal/FactoryUtil.java new file mode 100644 index 0000000..5719b57 --- /dev/null +++ b/src/main/java/org/apache/logging/admin/internal/FactoryUtil.java
@@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.logging.admin.internal; + +import aQute.bnd.annotation.Cardinality; +import aQute.bnd.annotation.Resolution; +import aQute.bnd.annotation.spi.ServiceConsumer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.ServiceLoader; +import org.apache.logging.admin.LoggingAdmin; +import org.apache.logging.admin.spi.LoggingAdminFactory; + +@ServiceConsumer( + value = LoggingAdminFactory.class, + cardinality = Cardinality.MULTIPLE, + resolution = Resolution.MANDATORY) +public final class FactoryUtil { + + private static final Collection<LoggingAdminFactory> factories = new ArrayList<>(); + + static { + ServiceLoader.load(LoggingAdminFactory.class, FactoryUtil.class.getClassLoader()) + .forEach(factories::add); + } + + public static LoggingAdmin getLoggingAdmin(Object token) { + LoggingAdminFactory factory = factories.stream() + .filter(LoggingAdminFactory::isActive) + .sorted(Comparator.comparing(LoggingAdminFactory::getPriority)) + .findAny() + .orElseThrow(() -> + new IllegalStateException("No active " + LoggingAdminFactory.class.getName() + " found.")); + return factory.getLoggingAdmin(token); + } + + private FactoryUtil() {} +}
diff --git a/src/main/java/org/apache/logging/admin/internal/JulAdmin.java b/src/main/java/org/apache/logging/admin/internal/JulAdmin.java new file mode 100644 index 0000000..5d49388 --- /dev/null +++ b/src/main/java/org/apache/logging/admin/internal/JulAdmin.java
@@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.logging.admin.internal; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.WeakHashMap; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.logging.admin.LoggingAdmin; +import org.jspecify.annotations.Nullable; + +class JulAdmin implements LoggingAdmin { + + private static final ReentrantLock lock = new ReentrantLock(); + private static final Map<Logger, Object> tokensByRootLogger = new WeakHashMap<>(); + + private static final List<String> levels = Collections.unmodifiableList(Stream.of( + Level.OFF, + Level.SEVERE, + Level.WARNING, + Level.INFO, + Level.CONFIG, + Level.FINE, + Level.FINER, + Level.FINEST, + Level.ALL) + .map(Level::toString) + .collect(Collectors.toList())); + + private final LogManager logManager = LogManager.getLogManager(); + private final Logger rootLogger; + + JulAdmin(Logger rootLogger) { + this.rootLogger = rootLogger; + } + + @Override + public List<String> getSupportedLevels() { + return levels; + } + + @Override + public Map<String, @Nullable String> getLevels() { + final Map<String, @Nullable String> loggerLevels = new HashMap<>(); + Collections.list(logManager.getLoggerNames()).forEach(loggerName -> fillLoggerLevels(loggerName, loggerLevels)); + return loggerLevels; + } + + @Override + public @Nullable String getLevel(String loggerName) { + return Optional.ofNullable(logManager.getLogger(loggerName)) + .map(Logger::getLevel) + .map(Level::getName) + .orElse(null); + } + + @Override + public void setLevel(String loggerName, @Nullable String level) { + Logger logger = logManager.getLogger(loggerName); + Logger rootLogger = findRootLogger(logger); + // Prevents setting the log level of a different "logger context" + if (this.rootLogger.equals(rootLogger)) { + logger.setLevel(level != null ? Level.parse(level) : null); + } + } + + private void fillLoggerLevels(String loggerName, Map<String, @Nullable String> loggerLevels) { + String currentName = loggerName; + while (loggerLevels.putIfAbsent(currentName, getLevel(currentName)) == null) { + if (currentName.isEmpty()) { + break; + } + int idx = currentName.lastIndexOf('.'); + currentName = idx == -1 ? "" : currentName.substring(0, idx); + } + } + + private static Logger findRootLogger(Logger logger) { + Logger current = logger; + while (current != null) { + if ("".equals(current.getName()) || current.getParent() == null) { + return current; + } + current = current.getParent(); + } + throw new IllegalStateException("Unable to find root logger for " + logger); + } + + /** + * Checks if the `java.logging` module is present. + * @throws LinkageError If `java.logging` is absent. + */ + static boolean isActive() { + return LogManager.getLogManager() != null; + } + + static LoggingAdmin newInstance(Object token) { + lock.lock(); + try { + Logger rootLogger = Logger.getLogger(""); + if (tokensByRootLogger.computeIfAbsent(rootLogger, k -> token) != token) { + throw new SecurityException("The security token does not match: " + token); + } + return new JulAdmin(rootLogger); + } finally { + lock.unlock(); + } + } +}
diff --git a/src/main/java/org/apache/logging/admin/internal/jul/JulFactory.java b/src/main/java/org/apache/logging/admin/internal/JulAdminFactory.java similarity index 67% rename from src/main/java/org/apache/logging/admin/internal/jul/JulFactory.java rename to src/main/java/org/apache/logging/admin/internal/JulAdminFactory.java index 7c257df..d0d22dc 100644 --- a/src/main/java/org/apache/logging/admin/internal/jul/JulFactory.java +++ b/src/main/java/org/apache/logging/admin/internal/JulAdminFactory.java
@@ -14,22 +14,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.logging.admin.internal.jul; +package org.apache.logging.admin.internal; import aQute.bnd.annotation.spi.ServiceProvider; import org.apache.logging.admin.LoggingAdmin; import org.apache.logging.admin.spi.LoggingAdminFactory; +/** + * Logging admin factory for `java.util.logging`. + */ @ServiceProvider(LoggingAdminFactory.class) -public class JulFactory implements LoggingAdminFactory { - +public class JulAdminFactory implements LoggingAdminFactory { @Override - public boolean isActive(ClassLoader classLoader) { - return true; + public int getPriority() { + return Integer.MAX_VALUE; } @Override - public LoggingAdmin createLoggingConfigurationAdmin(ClassLoader classLoader) { - return new JulAdmin(classLoader); + public boolean isActive() { + try { + return JulAdmin.isActive(); + } catch (LinkageError e) { + return false; + } + } + + @Override + public LoggingAdmin getLoggingAdmin(Object token) { + return JulAdmin.newInstance(token); } }
diff --git a/src/main/java/org/apache/logging/admin/internal/Log4jCoreAdmin.java b/src/main/java/org/apache/logging/admin/internal/Log4jCoreAdmin.java new file mode 100644 index 0000000..3ed3a99 --- /dev/null +++ b/src/main/java/org/apache/logging/admin/internal/Log4jCoreAdmin.java
@@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.logging.admin.internal; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.WeakHashMap; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.logging.admin.LoggingAdmin; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.LoggerConfig; +import org.apache.logging.log4j.core.util.NameUtil; +import org.jspecify.annotations.Nullable; + +class Log4jCoreAdmin implements LoggingAdmin { + + private static final ReentrantLock lock = new ReentrantLock(); + private static final Map<LoggerContext, Object> tokensByLoggerContext = new WeakHashMap<>(); + + private final LoggerContext loggerContext; + + Log4jCoreAdmin(LoggerContext loggerContext) { + this.loggerContext = loggerContext; + } + + @Override + public List<String> getSupportedLevels() { + return Stream.of(Level.values()).sorted().map(Level::name).collect(Collectors.toList()); + } + + @Override + public Map<String, @Nullable String> getLevels() { + Map<String, @Nullable String> loggerLevels = new HashMap<>(); + // Insert the ancestors of all existing loggers + loggerContext.getLoggers().forEach(logger -> fillLoggerLevels(logger.getName(), loggerLevels)); + // Insert the ancestors of all existing logger configurations + loggerContext + .getConfiguration() + .getLoggers() + .keySet() + .forEach(loggerName -> fillLoggerLevels(loggerName, loggerLevels)); + return loggerLevels; + } + + @Override + public @Nullable String getLevel(String loggerName) { + Configuration config = loggerContext.getConfiguration(); + return Optional.of(config.getLoggerConfig(loggerName)) + .filter(lc -> loggerName.equals(lc.getName())) + .map(LoggerConfig::getLevel) + .map(Level::name) + .orElse(null); + } + + @Override + public void setLevel(String loggerName, @Nullable String level) { + boolean changed; + Configuration config = loggerContext.getConfiguration(); + Level levelObj = level != null ? Level.valueOf(level) : null; + LoggerConfig loggerConfig = config.getLoggerConfig(loggerName); + if (!loggerName.equals(loggerConfig.getName())) { + loggerConfig = new LoggerConfig(loggerName, levelObj, true); + config.addLogger(loggerName, loggerConfig); + changed = true; + } else { + changed = Objects.equals(levelObj, loggerConfig.getLevel()); + loggerConfig.setLevel(levelObj); + } + if (changed) { + loggerContext.updateLoggers(); + } + } + + private void fillLoggerLevels(String loggerName, Map<String, @Nullable String> loggerLevels) { + String currentName = loggerName; + while (currentName != null && loggerLevels.putIfAbsent(currentName, getLevel(currentName)) == null) { + currentName = NameUtil.getSubName(currentName); + } + } + + static boolean isActive() { + org.apache.logging.log4j.spi.LoggerContext loggerContext = PrivateLogManager.getContext(); + return loggerContext instanceof org.apache.logging.log4j.core.LoggerContext; + } + + static LoggingAdmin newInstance(Object token) { + lock.lock(); + try { + LoggerContext loggerContext = (LoggerContext) PrivateLogManager.getContext(); + if (tokensByLoggerContext.computeIfAbsent(loggerContext, k -> token) != token) { + throw new SecurityException("The security token does not match: " + token); + } + return new Log4jCoreAdmin(loggerContext); + } finally { + lock.unlock(); + } + } + + private static final class PrivateLogManager extends LogManager { + private PrivateLogManager() {} + + public static org.apache.logging.log4j.spi.LoggerContext getContext() { + return LogManager.getContext(LoggingAdmin.class.getName(), false); + } + } +}
diff --git a/src/main/java/org/apache/logging/admin/internal/jul/JulFactory.java b/src/main/java/org/apache/logging/admin/internal/Log4jCoreAdminFactory.java similarity index 66% copy from src/main/java/org/apache/logging/admin/internal/jul/JulFactory.java copy to src/main/java/org/apache/logging/admin/internal/Log4jCoreAdminFactory.java index 7c257df..8f46127 100644 --- a/src/main/java/org/apache/logging/admin/internal/jul/JulFactory.java +++ b/src/main/java/org/apache/logging/admin/internal/Log4jCoreAdminFactory.java
@@ -14,22 +14,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.logging.admin.internal.jul; +package org.apache.logging.admin.internal; import aQute.bnd.annotation.spi.ServiceProvider; import org.apache.logging.admin.LoggingAdmin; import org.apache.logging.admin.spi.LoggingAdminFactory; -@ServiceProvider(LoggingAdminFactory.class) -public class JulFactory implements LoggingAdminFactory { - +@ServiceProvider(value = LoggingAdminFactory.class) +public class Log4jCoreAdminFactory implements LoggingAdminFactory { @Override - public boolean isActive(ClassLoader classLoader) { - return true; + public int getPriority() { + return 512; } @Override - public LoggingAdmin createLoggingConfigurationAdmin(ClassLoader classLoader) { - return new JulAdmin(classLoader); + public boolean isActive() { + try { + return Log4jCoreAdmin.isActive(); + } catch (LinkageError e) { + return false; + } + } + + @Override + public LoggingAdmin getLoggingAdmin(Object token) { + return Log4jCoreAdmin.newInstance(token); } }
diff --git a/src/main/java/org/apache/logging/admin/internal/LogbackAdmin.java b/src/main/java/org/apache/logging/admin/internal/LogbackAdmin.java new file mode 100644 index 0000000..9aa03a6 --- /dev/null +++ b/src/main/java/org/apache/logging/admin/internal/LogbackAdmin.java
@@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.logging.admin.internal; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.WeakHashMap; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.logging.admin.LoggingAdmin; +import org.jspecify.annotations.Nullable; +import org.slf4j.LoggerFactory; + +class LogbackAdmin implements LoggingAdmin { + + private static final ReentrantLock lock = new ReentrantLock(); + private static final Map<LoggerContext, Object> tokensByLoggerContext = new WeakHashMap<>(); + + private static final List<String> levels = Collections.unmodifiableList( + Stream.of(Level.OFF, Level.ERROR, Level.WARN, Level.INFO, Level.DEBUG, Level.TRACE) + .map(Level::toString) + .collect(Collectors.toList())); + + private final LoggerContext loggerContext; + + LogbackAdmin(final LoggerContext loggerContext) { + this.loggerContext = loggerContext; + } + + @Override + public List<String> getSupportedLevels() { + return levels; + } + + @Override + public Map<String, @Nullable String> getLevels() { + Map<String, @Nullable String> loggerLevels = new HashMap<>(); + loggerContext + .getLoggerList() + .forEach(logger -> loggerLevels.put(rootToEmpty(logger.getName()), getLevel(logger))); + return loggerLevels; + } + + @Override + public @Nullable String getLevel(String loggerName) { + return getLevel(loggerContext.getLogger(emptyToRoot(loggerName))); + } + + private static @Nullable String getLevel(Logger logger) { + return Optional.ofNullable(logger.getLevel()).map(Level::toString).orElse(null); + } + + @Override + public void setLevel(String loggerName, @Nullable String level) { + loggerContext.getLogger(emptyToRoot(loggerName)).setLevel(level != null ? Level.valueOf(level) : null); + } + + private String rootToEmpty(String loggerName) { + return Logger.ROOT_LOGGER_NAME.equals(loggerName) ? "" : loggerName; + } + + private String emptyToRoot(String loggerName) { + return loggerName.isEmpty() ? Logger.ROOT_LOGGER_NAME : loggerName; + } + + static boolean isActive() { + return LoggerFactory.getILoggerFactory() instanceof ch.qos.logback.classic.LoggerContext; + } + + static LoggingAdmin newInstance(Object token) { + lock.lock(); + try { + LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); + if (tokensByLoggerContext.computeIfAbsent(loggerContext, k -> token) != token) { + throw new SecurityException("The security token does not match: " + token); + } + return new LogbackAdmin(loggerContext); + } finally { + lock.unlock(); + } + } +}
diff --git a/src/main/java/org/apache/logging/admin/internal/logback/LogbackAdminFactory.java b/src/main/java/org/apache/logging/admin/internal/LogbackAdminFactory.java similarity index 67% rename from src/main/java/org/apache/logging/admin/internal/logback/LogbackAdminFactory.java rename to src/main/java/org/apache/logging/admin/internal/LogbackAdminFactory.java index 3b3492a..b05a03c 100644 --- a/src/main/java/org/apache/logging/admin/internal/logback/LogbackAdminFactory.java +++ b/src/main/java/org/apache/logging/admin/internal/LogbackAdminFactory.java
@@ -14,21 +14,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.logging.admin.internal.logback; +package org.apache.logging.admin.internal; -import ch.qos.logback.classic.LoggerContext; +import aQute.bnd.annotation.spi.ServiceProvider; import org.apache.logging.admin.LoggingAdmin; import org.apache.logging.admin.spi.LoggingAdminFactory; -import org.slf4j.LoggerFactory; +@ServiceProvider(value = LoggingAdminFactory.class) public class LogbackAdminFactory implements LoggingAdminFactory { @Override - public boolean isActive(ClassLoader classLoader) { - return LoggerFactory.getILoggerFactory() instanceof ch.qos.logback.classic.LoggerContext; + public int getPriority() { + return 1024; } @Override - public LoggingAdmin createLoggingConfigurationAdmin(ClassLoader classLoader) { - return new LogbackAdmin((LoggerContext) LoggerFactory.getILoggerFactory()); + public boolean isActive() { + try { + return LogbackAdmin.isActive(); + } catch (LinkageError e) { + return false; + } + } + + @Override + public LoggingAdmin getLoggingAdmin(Object token) { + return LogbackAdmin.newInstance(token); } }
diff --git a/src/main/java/org/apache/logging/admin/internal/jul/JulAdmin.java b/src/main/java/org/apache/logging/admin/internal/jul/JulAdmin.java deleted file mode 100644 index 54fc6a8..0000000 --- a/src/main/java/org/apache/logging/admin/internal/jul/JulAdmin.java +++ /dev/null
@@ -1,117 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to you under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.logging.admin.internal.jul; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.logging.Level; -import java.util.logging.LogManager; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.apache.logging.admin.LoggingAdmin; -import org.jspecify.annotations.Nullable; - -class JulAdmin implements LoggingAdmin { - - private static final Set<String> levels = Stream.of( - Level.ALL, - Level.FINEST, - Level.FINER, - Level.FINE, - Level.CONFIG, - Level.INFO, - Level.WARNING, - Level.SEVERE, - Level.OFF) - .map(Level::toString) - .collect(Collectors.toSet()); - - private final LogManager logManager = LogManager.getLogManager(); - private final ClassLoader classLoader; - - JulAdmin(final ClassLoader classLoader) { - this.classLoader = classLoader; - } - - @Override - public Object getLoggerContext() { - // Implementations that support multiple logger contexts can be recognized by having multiple root loggers. - ClassLoader oldClassLoader = updateThreadContextClassLoader(classLoader); - try { - return Logger.getLogger(""); - } finally { - updateThreadContextClassLoader(oldClassLoader); - } - } - - @Override - public Set<String> getSupportedLevels() { - return levels; - } - - @Override - public Map<String, Optional<String>> getLoggerLevels() { - ClassLoader oldClassLoader = updateThreadContextClassLoader(classLoader); - try { - final Map<String, Optional<String>> loggerLevels = new HashMap<>(); - for (final String loggerName : Collections.list(logManager.getLoggerNames())) { - loggerLevels.computeIfAbsent(loggerName, this::getLoggerLevel); - } - return loggerLevels; - } finally { - updateThreadContextClassLoader(oldClassLoader); - } - } - - @Override - public Optional<String> getLoggerLevel(String loggerName) { - ClassLoader oldClassLoader = updateThreadContextClassLoader(classLoader); - try { - return Optional.ofNullable(logManager.getLogger(loggerName)) - .map(Logger::getLevel) - .map(Level::getName); - } finally { - updateThreadContextClassLoader(oldClassLoader); - } - } - - private Optional<String> doGetLoggerLevel(String loggerName) { - return Optional.ofNullable(logManager.getLogger(loggerName)) - .map(Logger::getLevel) - .map(Level::getName); - } - - @Override - public void setLoggerLevel(String loggerName, @Nullable String level) { - ClassLoader oldClassLoader = updateThreadContextClassLoader(classLoader); - try { - logManager.getLogger(loggerName).setLevel(level != null ? Level.parse(level) : null); - } finally { - updateThreadContextClassLoader(oldClassLoader); - } - } - - private static ClassLoader updateThreadContextClassLoader(final ClassLoader classLoader) { - ClassLoader old = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(classLoader); - return old; - } -}
diff --git a/src/main/java/org/apache/logging/admin/internal/log4j/CoreAdmin.java b/src/main/java/org/apache/logging/admin/internal/log4j/CoreAdmin.java deleted file mode 100644 index 7edd685..0000000 --- a/src/main/java/org/apache/logging/admin/internal/log4j/CoreAdmin.java +++ /dev/null
@@ -1,100 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to you under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.logging.admin.internal.log4j; - -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.apache.logging.admin.LoggingAdmin; -import org.apache.logging.log4j.Level; -import org.apache.logging.log4j.core.LoggerContext; -import org.apache.logging.log4j.core.config.Configuration; -import org.apache.logging.log4j.core.config.LoggerConfig; -import org.apache.logging.log4j.core.util.NameUtil; -import org.jspecify.annotations.Nullable; - -class CoreAdmin implements LoggingAdmin { - - private final LoggerContext loggerContext; - - CoreAdmin(LoggerContext loggerContext) { - this.loggerContext = loggerContext; - } - - @Override - public Object getLoggerContext() { - return loggerContext; - } - - @Override - public Set<String> getSupportedLevels() { - return Stream.of(Level.values()).map(Level::name).collect(Collectors.toSet()); - } - - @Override - public Map<String, Optional<String>> getLoggerLevels() { - Map<String, Optional<String>> loggerLevels = new HashMap<>(); - // Insert the ancestors of all existing loggers - loggerContext.getLoggers().forEach(logger -> fillLoggerLevels(logger.getName(), loggerLevels)); - // Insert the ancestors of all existing logger configurations - loggerContext - .getConfiguration() - .getLoggers() - .keySet() - .forEach(loggerName -> fillLoggerLevels(loggerName, loggerLevels)); - return loggerLevels; - } - - @Override - public Optional<String> getLoggerLevel(String loggerName) { - Configuration config = loggerContext.getConfiguration(); - return Optional.of(config.getLoggerConfig(loggerName)) - .filter(lc -> loggerName.equals(lc.getName())) - .map(LoggerConfig::getLevel) - .map(Level::name); - } - - @Override - public void setLoggerLevel(String loggerName, @Nullable String level) { - boolean changed; - Configuration config = loggerContext.getConfiguration(); - Level levelObj = level != null ? Level.valueOf(level) : null; - LoggerConfig loggerConfig = config.getLoggerConfig(loggerName); - if (!loggerName.equals(loggerConfig.getName())) { - loggerConfig = new LoggerConfig(loggerName, levelObj, true); - config.addLogger(loggerName, loggerConfig); - changed = true; - } else { - changed = Objects.equals(levelObj, loggerConfig.getLevel()); - loggerConfig.setLevel(levelObj); - } - if (changed) { - loggerContext.updateLoggers(); - } - } - - private void fillLoggerLevels(String loggerName, Map<String, Optional<String>> loggerLevels) { - String currentName = loggerName; - while (currentName != null && loggerLevels.computeIfAbsent(currentName, this::getLoggerLevel) == null) { - currentName = NameUtil.getSubName(currentName); - } - } -}
diff --git a/src/main/java/org/apache/logging/admin/internal/log4j/CoreFactory.java b/src/main/java/org/apache/logging/admin/internal/log4j/CoreFactory.java deleted file mode 100644 index 9b9c82a..0000000 --- a/src/main/java/org/apache/logging/admin/internal/log4j/CoreFactory.java +++ /dev/null
@@ -1,39 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to you under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.logging.admin.internal.log4j; - -import aQute.bnd.annotation.spi.ServiceProvider; -import org.apache.logging.admin.LoggingAdmin; -import org.apache.logging.admin.spi.LoggingAdminFactory; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.spi.LoggerContext; - -@ServiceProvider(value = LoggingAdminFactory.class) -public class CoreFactory implements LoggingAdminFactory { - - @Override - public boolean isActive(ClassLoader classLoader) { - LoggerContext loggerContext = LogManager.getContext(classLoader, false); - return loggerContext instanceof org.apache.logging.log4j.core.LoggerContext; - } - - @Override - public LoggingAdmin createLoggingConfigurationAdmin(ClassLoader classLoader) { - LoggerContext loggerContext = LogManager.getContext(classLoader, false); - return new CoreAdmin((org.apache.logging.log4j.core.LoggerContext) loggerContext); - } -}
diff --git a/src/main/java/org/apache/logging/admin/internal/logback/LogbackAdmin.java b/src/main/java/org/apache/logging/admin/internal/logback/LogbackAdmin.java deleted file mode 100644 index 3dd1fe3..0000000 --- a/src/main/java/org/apache/logging/admin/internal/logback/LogbackAdmin.java +++ /dev/null
@@ -1,72 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to you under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.logging.admin.internal.logback; - -import ch.qos.logback.classic.Level; -import ch.qos.logback.classic.Logger; -import ch.qos.logback.classic.LoggerContext; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.apache.logging.admin.LoggingAdmin; -import org.jspecify.annotations.Nullable; - -class LogbackAdmin implements LoggingAdmin { - - private static final Set<String> levels = Stream.of( - Level.TRACE, Level.DEBUG, Level.INFO, Level.WARN, Level.ERROR, Level.OFF) - .map(Level::toString) - .collect(Collectors.toSet()); - - private final LoggerContext loggerContext; - - @Override - public Object getLoggerContext() { - return loggerContext; - } - - LogbackAdmin(final LoggerContext loggerContext) { - this.loggerContext = loggerContext; - } - - @Override - public Set<String> getSupportedLevels() { - return levels; - } - - @Override - public Map<String, Optional<String>> getLoggerLevels() { - return loggerContext.getLoggerList().stream() - .collect(Collectors.toMap(Logger::getName, LogbackAdmin::getLoggerLevel)); - } - - @Override - public Optional<String> getLoggerLevel(String loggerName) { - return getLoggerLevel(loggerContext.getLogger(loggerName)); - } - - private static Optional<String> getLoggerLevel(ch.qos.logback.classic.Logger logger) { - return Optional.ofNullable(logger.getLevel()).map(Level::toString); - } - - @Override - public void setLoggerLevel(String loggerName, @Nullable String level) { - loggerContext.getLogger(loggerName).setLevel(level != null ? Level.valueOf(level) : null); - } -}
diff --git a/src/main/java/org/apache/logging/admin/package-info.java b/src/main/java/org/apache/logging/admin/package-info.java index be36b44..8c0bc7e 100644 --- a/src/main/java/org/apache/logging/admin/package-info.java +++ b/src/main/java/org/apache/logging/admin/package-info.java
@@ -14,7 +14,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@Export +@Version("0.1.0") @NullMarked package org.apache.logging.admin; import org.jspecify.annotations.NullMarked; +import org.osgi.annotation.bundle.Export; +import org.osgi.annotation.versioning.Version;
diff --git a/src/main/java/org/apache/logging/admin/spi/LoggingAdminFactory.java b/src/main/java/org/apache/logging/admin/spi/LoggingAdminFactory.java index b6a79c4..6d11a8c 100644 --- a/src/main/java/org/apache/logging/admin/spi/LoggingAdminFactory.java +++ b/src/main/java/org/apache/logging/admin/spi/LoggingAdminFactory.java
@@ -27,17 +27,29 @@ public interface LoggingAdminFactory { /** - * Determines whether the logging system handled by this factory is used. - * - * @param classLoader The class loader associated with the logger context. + * Provides the order in which this factory should be evaluated. + * <p> + * Lower numerical values are evaluated first. + * </p> */ - boolean isActive(ClassLoader classLoader); + default int getPriority() { + return 0; + } + + /** + * Determines whether the logging system handled by this factory is used. + */ + boolean isActive(); /** * Creates a new {@link LoggingAdmin} instance associated with the given classloader. - * - * @param classLoader The class loader associated with the logger context. - * @return A new instance of {@link LoggingAdmin}. + * <p> + * The {@code token} parameter sets a security token for the appropriate logger context. + * All future invocations of this method will need to use the same token. + * Tokens are compared using object equality. + * </p> + * @param token Any Java object. + * @return An instance of {@link LoggingAdmin}. */ - LoggingAdmin createLoggingConfigurationAdmin(ClassLoader classLoader); + LoggingAdmin getLoggingAdmin(Object token) throws SecurityException; }
diff --git a/src/main/java/org/apache/logging/admin/spi/package-info.java b/src/main/java/org/apache/logging/admin/spi/package-info.java index ab19b30..2751805 100644 --- a/src/main/java/org/apache/logging/admin/spi/package-info.java +++ b/src/main/java/org/apache/logging/admin/spi/package-info.java
@@ -14,7 +14,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@Export +@Version("0.1.0") @NullMarked package org.apache.logging.admin.spi; import org.jspecify.annotations.NullMarked; +import org.osgi.annotation.bundle.Export; +import org.osgi.annotation.versioning.Version;
diff --git a/src/test/java/org/apache/logging/admin/LoggingAdminTest.java b/src/test/java/org/apache/logging/admin/LoggingAdminTest.java new file mode 100644 index 0000000..02612ec --- /dev/null +++ b/src/test/java/org/apache/logging/admin/LoggingAdminTest.java
@@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.logging.admin; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.AbstractMap; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; +import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class LoggingAdminTest { + + private static final Map<String, List<String>> EXPECTED_LEVELS; + + static { + final Map<String, List<String>> expectedLevels = new HashMap<>(); + expectedLevels.put( + "jul", Arrays.asList("OFF", "SEVERE", "WARNING", "INFO", "CONFIG", "FINE", "FINER", "FINEST", "ALL")); + expectedLevels.put( + "log4j-core", Arrays.asList("OFF", "FATAL", "ERROR", "WARN", "INFO", "DEBUG", "TRACE", "ALL")); + expectedLevels.put("logback", Arrays.asList("OFF", "ERROR", "WARN", "INFO", "DEBUG", "TRACE")); + EXPECTED_LEVELS = Collections.unmodifiableMap(expectedLevels); + } + + private static final Object TOKEN = new Object(); + private static LoggingAdmin admin; + + // Log4j Core is the default in the IDE + private static final String type = System.getProperty("admin.implementation", "log4j-core"); + + private static String debugLevel() { + return "jul".equals(type) ? "FINE" : "DEBUG"; + } + + @BeforeAll + static void setup() { + admin = LoggingAdmin.getInstance(TOKEN); + // JUL creates loggers lazily, so we force the creation of `foo.bar` + Logger.getLogger("foo.bar"); + } + + @Test + void should_return_correct_supported_levels() { + assertThat(admin.getSupportedLevels()).containsExactlyElementsOf(EXPECTED_LEVELS.get(type)); + } + + static Stream<Arguments> should_return_correct_configured_levels() { + return Stream.of( + Arguments.of(LoggingAdmin.ROOT_LOGGER_NAME, "INFO"), + Arguments.of("foo", null), + Arguments.of("foo.bar", debugLevel())); + } + + @ParameterizedTest + @MethodSource + void should_return_correct_configured_levels(String loggerName, @Nullable String expectedLevel) { + assertThat(admin.getLevel(loggerName)) + .as("Level of logger `%s`.", loggerName) + .isEqualTo(expectedLevel); + } + + @Test + void should_return_correct_configured_level_map() { + assertThat(admin.getLevels()) + .contains( + entry(LoggingAdmin.ROOT_LOGGER_NAME, "INFO"), + entry("foo", null), + entry("foo.bar", debugLevel())); + } + + @Test + void should_return_admin_if_token_correct() { + assertDoesNotThrow(() -> LoggingAdmin.getInstance(TOKEN)); + } + + @Test + void should_throw_if_token_incorrect() { + assertThrows(SecurityException.class, () -> LoggingAdmin.getInstance(new Object())); + } + + private static Map.Entry<String, @Nullable String> entry(String loggerName, @Nullable String level) { + return new AbstractMap.SimpleImmutableEntry<>(loggerName, level); + } +}
diff --git a/src/test/resources/log4j2.xml b/src/test/resources/log4j2.xml new file mode 100644 index 0000000..23eb4be --- /dev/null +++ b/src/test/resources/log4j2.xml
@@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to you under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<Configuration xmlns="https://logging.apache.org/xml/ns" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation=" + https://logging.apache.org/xml/ns + https://logging.apache.org/xml/ns/log4j-config-2.xsd"> + <Appenders/> + <Loggers> + <Root level="INFO"/> + <Logger name="foo.bar" level="DEBUG"/> + </Loggers> +</Configuration>
diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml new file mode 100644 index 0000000..5e98a3e --- /dev/null +++ b/src/test/resources/logback.xml
@@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to you under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<!DOCTYPE configuration> +<configuration> + <root level="INFO"/> + <logger name="foo.bar" level="DEBUG"/> +</configuration>
diff --git a/src/test/resources/logging.properties b/src/test/resources/logging.properties new file mode 100644 index 0000000..adcc875 --- /dev/null +++ b/src/test/resources/logging.properties
@@ -0,0 +1,19 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to you under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +handlers = +.level = INFO +foo.bar.level = FINE