diff --git a/core/src/main/java/org/jboss/logmanager/ConfiguratorFactory.java b/core/src/main/java/org/jboss/logmanager/ConfiguratorFactory.java new file mode 100644 index 00000000..9e8863f8 --- /dev/null +++ b/core/src/main/java/org/jboss/logmanager/ConfiguratorFactory.java @@ -0,0 +1,46 @@ +/* + * JBoss, Home of Professional Open Source. + * + * Copyright 2018 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed 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.jboss.logmanager; + +/** + * Used to create a {@link LogContextConfigurator}. The {@linkplain #priority() priority} is used to determine which + * factory should be used. The lowest priority factory is used. If two factories have the same priority the second + * factory will not be used. The order of loading the factories for determining priority is done via the + * {@link java.util.ServiceLoader#load(Class, ClassLoader)}. + * + * @author James R. Perkins + */ +public interface ConfiguratorFactory { + + /** + * Creates the {@link LogContextConfigurator}. + * + * @return the log context configurator + */ + LogContextConfigurator create(); + + /** + * The priority for the factory which is used to determine which factory should be used to create a + * {@link LogContextConfigurator}. The lowest priority factory will be used. + * + * @return the priority for this factory + */ + int priority(); +} diff --git a/core/src/main/java/org/jboss/logmanager/LogManager.java b/core/src/main/java/org/jboss/logmanager/LogManager.java index 6e5f2264..6c314cc0 100644 --- a/core/src/main/java/org/jboss/logmanager/LogManager.java +++ b/core/src/main/java/org/jboss/logmanager/LogManager.java @@ -115,17 +115,23 @@ private void doConfigure(InputStream inputStream) { synchronized (configuratorRef) { configurator = configuratorRef.get(); if (configurator == null) { - final ServiceLoader serviceLoader = ServiceLoader.load(LogContextConfigurator.class); - final Iterator iterator = serviceLoader.iterator(); + int best = Integer.MAX_VALUE; + ConfiguratorFactory factory = null; + final ServiceLoader serviceLoader = ServiceLoader.load(ConfiguratorFactory.class); + final Iterator iterator = serviceLoader.iterator(); List problems = null; for (;;) try { if (! iterator.hasNext()) break; - configurator = iterator.next(); - break; + final ConfiguratorFactory f = iterator.next(); + if (f.priority() < best || factory == null) { + best = f.priority(); + factory = f; + } } catch (Throwable t) { if (problems == null) problems = new ArrayList<>(4); problems.add(t); } + configurator = factory == null ? null : factory.create(); if (configurator == null) { if (problems == null) { configuratorRef.set(configurator = LogContextConfigurator.EMPTY); diff --git a/core/src/test/java/org/jboss/logmanager/TestFileHandler.java b/core/src/test/java/org/jboss/logmanager/TestFileHandler.java deleted file mode 100644 index 0a46d857..00000000 --- a/core/src/test/java/org/jboss/logmanager/TestFileHandler.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * JBoss, Home of Professional Open Source. - * - * Copyright 2014 Red Hat, Inc., and individual contributors - * as indicated by the @author tags. - * - * Licensed 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.jboss.logmanager; - -import java.io.File; -import java.io.FileNotFoundException; - -import org.jboss.logmanager.handlers.FileHandler; - -/** - * @author James R. Perkins - */ -public class TestFileHandler extends FileHandler { - private static final String BASE_LOG_DIR; - - static { - BASE_LOG_DIR = System.getProperty("test.log.dir"); - } - - public TestFileHandler() { - super(); - } - - public TestFileHandler(final String fileName, final boolean append) throws FileNotFoundException { - super(fileName, append); - } - - @Override - public void setFileName(final String fileName) throws FileNotFoundException { - final String name = fileName == null ? null : BASE_LOG_DIR.concat(fileName); - super.setFileName(name); - } -} diff --git a/ext/pom.xml b/ext/pom.xml new file mode 100644 index 00000000..b2f6373d --- /dev/null +++ b/ext/pom.xml @@ -0,0 +1,80 @@ + + + + + + jboss-logmanager-parent + org.jboss.logmanager + 3.0.0.Final-SNAPSHOT + + 4.0.0 + + jboss-logmanager-ext + + + + org.jboss.logging + jboss-logging + + + org.jboss.logmanager + jboss-logmanager + ${project.version} + + + org.jboss.modules + jboss-modules + + + org.kohsuke.metainf-services + metainf-services + + true + provided + + + org.wildfly.common + wildfly-common + + + + + junit + junit + test + + + + + + + maven-surefire-plugin + + + ${project.build.testOutputDirectory}${file.separator}configs + ${project.build.directory}${file.separator}logs + + + + + + \ No newline at end of file diff --git a/ext/src/main/java/org/jboss/logmanager/ext/DefaultConfiguratorFactory.java b/ext/src/main/java/org/jboss/logmanager/ext/DefaultConfiguratorFactory.java new file mode 100644 index 00000000..d95aaac8 --- /dev/null +++ b/ext/src/main/java/org/jboss/logmanager/ext/DefaultConfiguratorFactory.java @@ -0,0 +1,43 @@ +/* + * JBoss, Home of Professional Open Source. + * + * Copyright 2018 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed 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.jboss.logmanager.ext; + +import org.jboss.logmanager.ConfiguratorFactory; +import org.jboss.logmanager.LogContextConfigurator; +import org.kohsuke.MetaInfServices; + +/** + * The default configuration factory which has a priority of 100. + * + * @author James R. Perkins + */ +@MetaInfServices +public class DefaultConfiguratorFactory implements ConfiguratorFactory { + + @Override + public LogContextConfigurator create() { + return new DefaultLogContextConfigurator(); + } + + @Override + public int priority() { + return 100; + } +} diff --git a/ext/src/main/java/org/jboss/logmanager/ext/DefaultLogContextConfigurator.java b/ext/src/main/java/org/jboss/logmanager/ext/DefaultLogContextConfigurator.java new file mode 100644 index 00000000..dc603590 --- /dev/null +++ b/ext/src/main/java/org/jboss/logmanager/ext/DefaultLogContextConfigurator.java @@ -0,0 +1,107 @@ +/* + * JBoss, Home of Professional Open Source. + * + * Copyright 2018 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed 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.jboss.logmanager.ext; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Iterator; +import java.util.Properties; +import java.util.ServiceLoader; +import java.util.logging.Logger; + +import org.jboss.logmanager.Level; +import org.jboss.logmanager.LogContext; +import org.jboss.logmanager.LogContextConfigurator; +import org.jboss.logmanager.StandardOutputStreams; +import org.jboss.logmanager.formatters.PatternFormatter; +import org.jboss.logmanager.handlers.ConsoleHandler; + +/** + * A default log context configuration. + *

+ * If the {@linkplain #configure(LogContext, InputStream) input stream} is {@code null} an attempt is made to find a + * {@code logging.properties} file. If the file is not found, a {@linkplain java.util.ServiceLoader service loader} is + * used to find the first implementation of a {@link LogContextConfigurator}. If that fails a default + * {@link ConsoleHandler} is configured with the pattern {@code %d{yyyy-MM-dd'T'HH:mm:ssXXX} %-5p [%c] (%t) %s%e%n}. + *

+ * + *

+ * Locating the {@code logging.properties} happens in the following order: + *

    + *
  • The {@code logging.configuration} system property is checked
  • + *
  • The current threads {@linkplain ClassLoader#getResourceAsStream(String)} class loader} for a {@code logging.properties}
  • + *
  • Finally {@link Class#getResourceAsStream(String)} is used to locate a {@code logging.properties}
  • + *
+ *

+ * + * @author James R. Perkins + */ +public class DefaultLogContextConfigurator implements LogContextConfigurator { + + @Override + public void configure(final LogContext logContext, final InputStream inputStream) { + final InputStream configIn = inputStream != null ? inputStream : findConfiguration(); + + // Configure the log context based on a property file + if (configIn != null) { + final Properties properties = new Properties(); + try (Reader reader = new InputStreamReader(configIn, StandardCharsets.UTF_8)) { + properties.load(reader); + } catch (IOException e) { + throw new RuntimeException("Failed to configure log manager with configuration file.", e); + } + PropertyConfigurator.configure(logContext, properties); + } else { + // Next check the service loader + final Iterator serviceLoader = ServiceLoader.load(LogContextConfigurator.class).iterator(); + if (serviceLoader.hasNext()) { + serviceLoader.next().configure(logContext, inputStream); + } else { + // Configure a default console handler, pattern formatter and associated with the root logger + final ConsoleHandler handler = new ConsoleHandler(new PatternFormatter("%d{yyyy-MM-dd'T'HH:mm:ssXXX} %-5p [%c] (%t) %s%e%n")); + handler.setLevel(Level.INFO); + handler.setAutoFlush(true); + final Logger rootLogger = logContext.getLogger(""); + rootLogger.setLevel(Level.INFO); + rootLogger.addHandler(handler); + } + } + } + + private static InputStream findConfiguration() { + final String propLoc = System.getProperty("logging.configuration"); + if (propLoc != null) try { + return new URL(propLoc).openStream(); + } catch (IOException e) { + StandardOutputStreams.printError("Unable to read the logging configuration from '%s' (%s)%n", propLoc, e); + } + final ClassLoader tccl = Thread.currentThread().getContextClassLoader(); + if (tccl != null) try { + final InputStream stream = tccl.getResourceAsStream("logging.properties"); + if (stream != null) return stream; + } catch (Exception ignore) { + } + return DefaultLogContextConfigurator.class.getResourceAsStream("logging.properties"); + } +} diff --git a/ext/src/main/java/org/jboss/logmanager/ext/ObjectBuilder.java b/ext/src/main/java/org/jboss/logmanager/ext/ObjectBuilder.java new file mode 100644 index 00000000..0779e8ed --- /dev/null +++ b/ext/src/main/java/org/jboss/logmanager/ext/ObjectBuilder.java @@ -0,0 +1,389 @@ +/* + * JBoss, Home of Professional Open Source. + * + * Copyright 2018 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed 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.jboss.logmanager.ext; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.nio.charset.Charset; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.regex.Pattern; + +import org.jboss.logmanager.LogContext; +import org.jboss.modules.Module; +import org.jboss.modules.ModuleLoader; + +/** + * Helper to lazily build an object. + * + * @author James R. Perkins + */ +@SuppressWarnings({"UnusedReturnValue"}) +class ObjectBuilder { + + private final LogContext logContext; + private final Class baseClass; + private final String className; + private final Map constructorProperties; + private final Map properties; + private final Set definedProperties; + private final Set postConstructMethods; + private String moduleName; + + private ObjectBuilder(final LogContext logContext, final Class baseClass, final String className) { + this.logContext = logContext; + this.baseClass = baseClass; + this.className = className; + constructorProperties = new LinkedHashMap<>(); + properties = new LinkedHashMap<>(); + definedProperties = new LinkedHashSet<>(); + postConstructMethods = new LinkedHashSet<>(); + } + + /** + * Create a new {@link ObjectBuilder}. + * + * @param logContext the log context being configured + * @param baseClass the base type + * @param className the name of the class to create + * @param the type being created + * + * @return a new {@link ObjectBuilder} + */ + static ObjectBuilder of(final LogContext logContext, final Class baseClass, final String className) { + return new ObjectBuilder<>(logContext, baseClass, className); + } + + /** + * Adds a property used for constructing the object. + *

+ * The {@code name} must be the base name for a getter or setter so the type can be determined. + *

+ * + * @param name the name of the property + * @param value a string representing the value + * + * @return this builder + */ + ObjectBuilder addConstructorProperty(final String name, final String value) { + constructorProperties.put(name, value); + return this; + } + + /** + * Adds a method name to be executed after the object is created and the properties are set. The method must not + * have any parameters. + * + * @param methodNames the name of the method to execute + * + * @return this builder + */ + ObjectBuilder addPostConstructMethods(final String... methodNames) { + if (methodNames != null) { + Collections.addAll(postConstructMethods, methodNames); + } + return this; + } + + /** + * Adds a property to be set on the object after it has been created. + * + * @param name the name of the property + * @param value a string representing the value + * + * @return this builder + */ + ObjectBuilder addProperty(final String name, final String value) { + properties.put(name, value); + return this; + } + + /** + * Adds a defined property to be set after the object is created. + * + * @param name the name of the property + * @param type the type of the property + * @param value a supplier for the property value + * + * @return this builder + */ + ObjectBuilder addDefinedProperty(final String name, final Class type, final Supplier value) { + definedProperties.add(new PropertyValue(name, type, value)); + return this; + } + + /** + * Sets the name of the module used to load the object being created. + * + * @param moduleName the module name or {@code null} to use this class loader + * + * @return this builder + */ + ObjectBuilder setModuleName(final String moduleName) { + this.moduleName = moduleName; + return this; + } + + /** + * Creates a the object when the {@linkplain Supplier#get() supplier} value is accessed. + * + * @param pojos a map of objects to be used when the value of a property is a name of a POJO object + * + * @return a supplier which can create the object + */ + Supplier build(final Map> pojos) { + if (className == null) { + throw new IllegalArgumentException("className is null"); + } + final Map constructorProperties = new LinkedHashMap<>(this.constructorProperties); + final Map properties = new LinkedHashMap<>(this.properties); + final Set postConstructMethods = new LinkedHashSet<>(this.postConstructMethods); + final Map> objects = new LinkedHashMap<>(pojos); + final String moduleName = this.moduleName; + return () -> { + final ClassLoader classLoader; + if (moduleName != null) { + try { + classLoader = ModuleFinder.getClassLoader(moduleName); + } catch (Throwable e) { + throw new IllegalArgumentException(String.format("Failed to load module \"%s\"", moduleName), e); + } + } else { + classLoader = getClass().getClassLoader(); + } + final Class actualClass; + try { + actualClass = Class.forName(className, true, classLoader).asSubclass(baseClass); + } catch (Exception e) { + throw new IllegalArgumentException(String.format("Failed to load class \"%s\"", className), e); + } + final int length = constructorProperties.size(); + final Class[] paramTypes = new Class[length]; + final Object[] params = new Object[length]; + int i = 0; + for (Map.Entry entry : constructorProperties.entrySet()) { + final String property = entry.getKey(); + final Class type = getConstructorPropertyType(actualClass, property); + if (type == null) { + throw new IllegalArgumentException(String.format("No property named \"%s\" in \"%s\"", property, className)); + } + paramTypes[i] = type; + params[i] = getValue(actualClass, property, type, entry.getValue(), objects); + i++; + } + final Constructor constructor; + try { + constructor = actualClass.getConstructor(paramTypes); + } catch (Exception e) { + throw new IllegalArgumentException(String.format("Failed to locate constructor in class \"%s\"", className), e); + } + + // Get all the setters + final Map setters = new LinkedHashMap<>(); + for (Map.Entry entry : properties.entrySet()) { + final Method method = getPropertySetter(actualClass, entry.getKey()); + if (method == null) { + throw new IllegalArgumentException(String.format("Failed to locate setter for property \"%s\" on type \"%s\"", entry.getKey(), className)); + } + // Get the value type for the setter + Class type = getPropertyType(method); + if (type == null) { + throw new IllegalArgumentException(String.format("Failed to determine type for setter \"%s\" on type \"%s\"", method.getName(), className)); + } + setters.put(method, getValue(actualClass, entry.getKey(), type, entry.getValue(), objects)); + } + + // Define known type parameters + for (PropertyValue value : definedProperties) { + final String methodName = getPropertySetterName(value.name); + try { + final Method method = actualClass.getMethod(methodName, value.type); + setters.put(method, value.value.get()); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException(String.format("Failed to find setter method for property \"%s\" on type \"%s\"", value.name, className), e); + } + } + + // Get all the post construct methods + final Set postConstruct = new LinkedHashSet<>(); + for (String methodName : postConstructMethods) { + try { + postConstruct.add(actualClass.getMethod(methodName)); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException(String.format("Failed to find post construct method \"%s\" on type \"%s\"", methodName, className), e); + } + } + try { + T instance = constructor.newInstance(params); + + // Execute setters + for (Map.Entry entry : setters.entrySet()) { + entry.getKey().invoke(instance, entry.getValue()); + } + + // Execute post construct methods + for (Method method : postConstruct) { + method.invoke(instance); + } + + return instance; + } catch (Exception e) { + throw new IllegalArgumentException(String.format("Failed to instantiate class \"%s\"", className), e); + } + }; + } + + @SuppressWarnings("unchecked") + private Object getValue(final Class objClass, final String propertyName, final Class paramType, final String value, final Map> objects) { + if (value == null) { + if (paramType.isPrimitive()) { + throw new IllegalArgumentException(String.format("Cannot assign null value to primitive property \"%s\" of %s", propertyName, objClass)); + } + return null; + } + if (paramType == String.class) { + // Don't use the trimmed value for strings + return value; + } else if (paramType == java.util.logging.Level.class) { + return logContext.getLevelForName(value); + } else if (paramType == java.util.logging.Logger.class) { + return logContext.getLogger(value); + } else if (paramType == boolean.class || paramType == Boolean.class) { + return Boolean.valueOf(value); + } else if (paramType == byte.class || paramType == Byte.class) { + return Byte.valueOf(value); + } else if (paramType == short.class || paramType == Short.class) { + return Short.valueOf(value); + } else if (paramType == int.class || paramType == Integer.class) { + return Integer.valueOf(value); + } else if (paramType == long.class || paramType == Long.class) { + return Long.valueOf(value); + } else if (paramType == float.class || paramType == Float.class) { + return Float.valueOf(value); + } else if (paramType == double.class || paramType == Double.class) { + return Double.valueOf(value); + } else if (paramType == char.class || paramType == Character.class) { + return value.length() > 0 ? value.charAt(0) : 0; + } else if (paramType == TimeZone.class) { + return TimeZone.getTimeZone(value); + } else if (paramType == Charset.class) { + return Charset.forName(value); + } else if (paramType.isAssignableFrom(Level.class)) { + return Level.parse(value); + } else if (paramType.isEnum()) { + return Enum.valueOf(paramType.asSubclass(Enum.class), value); + } else if (objects.containsKey(value)) { + return objects.get(value).get(); + } else if (definedPropertiesContains(propertyName)) { + return findDefinedProperty(propertyName).value.get(); + } else { + throw new IllegalArgumentException("Unknown parameter type for property " + propertyName + " on " + objClass); + } + } + + private boolean definedPropertiesContains(final String name) { + return findDefinedProperty(name) != null; + } + + private PropertyValue findDefinedProperty(final String name) { + for (PropertyValue value : definedProperties) { + if (name.equals(value.name)) { + return value; + } + } + return null; + } + + private static Class getPropertyType(Class clazz, String propertyName) { + return getPropertyType(getPropertySetter(clazz, propertyName)); + } + + private static Class getPropertyType(final Method setter) { + return setter != null ? setter.getParameterTypes()[0] : null; + } + + private static Class getConstructorPropertyType(Class clazz, String propertyName) { + final Method getter = getPropertyGetter(clazz, propertyName); + return getter != null ? getter.getReturnType() : getPropertyType(clazz, propertyName); + } + + private static Method getPropertySetter(Class clazz, String propertyName) { + final String set = getPropertySetterName(propertyName); + for (Method method : clazz.getMethods()) { + if ((method.getName().equals(set) && Modifier.isPublic(method.getModifiers())) && method.getParameterTypes().length == 1) { + return method; + } + } + return null; + } + + private static Method getPropertyGetter(Class clazz, String propertyName) { + final String upperPropertyName = Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1); + final Pattern pattern = Pattern.compile("(get|has|is)(" + Pattern.quote(upperPropertyName) + ")"); + for (Method method : clazz.getMethods()) { + if ((pattern.matcher(method.getName()).matches() && Modifier.isPublic(method.getModifiers())) && method.getParameterTypes().length == 0) { + return method; + } + } + return null; + } + + private static String getPropertySetterName(final String propertyName) { + return "set" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1); + } + + static class ModuleFinder { + + private ModuleFinder() { + } + + static ClassLoader getClassLoader(final String moduleName) throws Exception { + ModuleLoader moduleLoader = ModuleLoader.forClass(ModuleFinder.class); + if (moduleLoader == null) { + moduleLoader = Module.getBootModuleLoader(); + } + return moduleLoader.loadModule(moduleName).getClassLoader(); + } + } + + private static class PropertyValue implements Comparable { + final String name; + final Class type; + final Supplier value; + + private PropertyValue(final String name, final Class type, final Supplier value) { + this.name = name; + this.type = type; + this.value = value; + } + + @Override + public int compareTo(final PropertyValue o) { + return name.compareTo(o.name); + } + } +} diff --git a/ext/src/main/java/org/jboss/logmanager/ext/PropertyConfigurator.java b/ext/src/main/java/org/jboss/logmanager/ext/PropertyConfigurator.java new file mode 100644 index 00000000..12fd334a --- /dev/null +++ b/ext/src/main/java/org/jboss/logmanager/ext/PropertyConfigurator.java @@ -0,0 +1,368 @@ +/* + * JBoss, Home of Professional Open Source. + * + * Copyright 2018 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed 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.jboss.logmanager.ext; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.function.Supplier; +import java.util.logging.ErrorManager; +import java.util.logging.Filter; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.jboss.logmanager.LogContext; +import org.jboss.logmanager.StandardOutputStreams; +import org.jboss.logmanager.ext.filters.FilterExpressions; +import org.jboss.logmanager.filters.AcceptAllFilter; +import org.jboss.logmanager.filters.DenyAllFilter; +import org.wildfly.common.Assert; +import org.wildfly.common.expression.Expression; + +/** + * A utility to parse a {@code logging.properties} file and configure a {@link LogContext}. + * + * @author James R. Perkins + */ +@SuppressWarnings("WeakerAccess") +public class PropertyConfigurator { + + private static final String[] EMPTY_STRINGS = new String[0]; + + private final LogContext logContext; + private final Properties properties; + private final Map> errorManagers = new HashMap<>(); + private final Map> filters = new HashMap<>(); + private final Map> formatters = new HashMap<>(); + private final Map> handlers = new HashMap<>(); + private final Map> pojos = new HashMap<>(); + + private PropertyConfigurator(final LogContext logContext, final Properties properties) { + this.logContext = logContext; + this.properties = properties; + } + + /** + * Configures the {@link LogContext} based on the properties. + * + * @param logContext the log context to configure + * @param properties the properties used to configure the log context + */ + public static void configure(final LogContext logContext, final Properties properties) { + final PropertyConfigurator config = new PropertyConfigurator( + Assert.checkNotNullParam("logContext", logContext), + Assert.checkNotNullParam("properties", properties) + ); + config.doConfigure(); + } + + private void doConfigure() { + // POJO's must be configured first so other + for (String pojoName : getStringCsvArray("pojos")) { + configurePojos(pojoName); + } + // Start with the list of loggers to configure. The root logger is always on the list. + configureLogger(""); + // And, for each logger name, configure any filters, handlers, etc. + for (String loggerName : getStringCsvArray("loggers")) { + configureLogger(loggerName); + } + // Configure any declared handlers. + for (String handlerName : getStringCsvArray("handlers")) { + configureHandler(handlerName); + } + // Configure any declared filters. + for (String filterName : getStringCsvArray("filters")) { + configureFilter(filterName); + } + // Configure any declared formatters. + for (String formatterName : getStringCsvArray("formatters")) { + configureFormatter(formatterName); + } + // Configure any declared error managers. + for (String errorManagerName : getStringCsvArray("errorManagers")) { + configureErrorManager(errorManagerName); + } + } + + @SuppressWarnings({"ConstantConditions", "CastCanBeRemovedNarrowingVariableType"}) + private void configureLogger(final String loggerName) { + /*if (logContext.getLoggerIfExists(loggerName) != null) { + // duplicate + return; + } */ + final Logger logger = logContext.getLogger(loggerName); + + // Get logger level + final String levelName = getStringProperty(getKey("logger", loggerName, "level")); + if (levelName != null) { + logger.setLevel(Level.parse(levelName)); + } + + // Get logger filters + final String filterName = getStringProperty(getKey("logger", loggerName, "filter")); + if (filterName != null) { + if (configureFilter(filterName)) { + logger.setFilter(filters.get(filterName).get()); + } + } + + // Get logger handlers + final String[] handlerNames = getStringCsvArray(getKey("logger", loggerName, "handlers")); + for (String name : handlerNames) { + if (configureHandler(name)) { + logger.addHandler(handlers.get(name).get()); + } + } + + // Get logger properties + final String useParentHandlersString = getStringProperty(getKey("logger", loggerName, "useParentHandlers")); + if (useParentHandlersString != null) { + logger.setUseParentHandlers(resolveBooleanExpression(useParentHandlersString)); + } + final String useParentFiltersString = getStringProperty(getKey("logger", loggerName, "useParentFilters")); + if (useParentFiltersString != null) { + if (logger instanceof org.jboss.logmanager.Logger) { + ((org.jboss.logmanager.Logger) logger).setUseParentFilters(resolveBooleanExpression(useParentHandlersString)); + } + } + } + + private boolean configureHandler(final String handlerName) { + if (handlers.containsKey(handlerName)) { + // already configured! + return true; + } + final String className = getStringProperty(getKey("handler", handlerName), true, false); + if (className == null) { + StandardOutputStreams.printError("Handler %s is not defined%n", handlerName); + return false; + } + + final ObjectBuilder handlerBuilder = ObjectBuilder.of(logContext, Handler.class, className) + .setModuleName(getStringProperty(getKey("handler", handlerName, "module"))) + .addPostConstructMethods(getStringCsvArray(getKey("handler", handlerName, "postConfiguration"))); + + // Configure the constructor properties + configureProperties(handlerBuilder, "handler", handlerName); + final String encoding = getStringProperty(getKey("handler", handlerName, "encoding")); + if (encoding != null) { + handlerBuilder.addProperty("encoding", encoding); + } + + final String filter = getStringProperty(getKey("handler", handlerName, "filter")); + if (filter != null) { + if (configureFilter(filter)) { + handlerBuilder.addDefinedProperty("filter", Filter.class, filters.get(filter)); + } + } + final String levelName = getStringProperty(getKey("handler", handlerName, "level")); + if (levelName != null) { + handlerBuilder.addProperty("level", levelName); + } + final String formatterName = getStringProperty(getKey("handler", handlerName, "formatter")); + if (formatterName != null) { + if (configureFormatter(formatterName)) { + handlerBuilder.addDefinedProperty("formatter", Formatter.class, formatters.get(formatterName)); + } + } + final String errorManagerName = getStringProperty(getKey("handler", handlerName, "errorManager")); + if (errorManagerName != null) { + if (configureErrorManager(errorManagerName)) { + handlerBuilder.addDefinedProperty("errorManager", ErrorManager.class, errorManagers.get(errorManagerName)); + } + } + + final String[] handlerNames = getStringCsvArray(getKey("handler", handlerName, "handlers")); + if (handlerNames.length > 0) { + final List> subhandlers = new ArrayList<>(); + for (String name : handlerNames) { + if (configureHandler(name)) { + subhandlers.add(handlers.get(name)); + } + } + handlerBuilder.addDefinedProperty("handlers", Handler[].class, new Supplier() { + + @Override + public Handler[] get() { + if (subhandlers.isEmpty()) { + return new Handler[0]; + } + final Handler[] result = new Handler[subhandlers.size()]; + int i = 0; + for (Supplier supplier : subhandlers) { + result[i++] = supplier.get(); + } + return result; + } + }); + } + handlers.putIfAbsent(handlerName, handlerBuilder.build(pojos)); + return true; + } + + private boolean configureFormatter(final String formatterName) { + if (filters.containsKey(formatterName)) { + // already configured! + return true; + } + final String className = getStringProperty(getKey("formatter", formatterName), true, false); + if (className == null) { + StandardOutputStreams.printError("Formatter %s is not defined%n", formatterName); + return false; + } + final ObjectBuilder formatterBuilder = ObjectBuilder.of(logContext, Formatter.class, className) + .setModuleName(getStringProperty(getKey("formatter", formatterName, "module"))) + .addPostConstructMethods(getStringCsvArray(getKey("formatter", formatterName, "postConfiguration"))); + configureProperties(formatterBuilder, "formatter", formatterName); + formatters.putIfAbsent(formatterName, formatterBuilder.build(pojos)); + return true; + } + + private boolean configureErrorManager(final String errorManagerName) { + if (errorManagers.containsKey(errorManagerName)) { + // already configured! + return true; + } + final String className = getStringProperty(getKey("errorManager", errorManagerName), true, false); + if (className == null) { + StandardOutputStreams.printError("Error manager %s is not defined%n", errorManagerName); + return false; + } + final ObjectBuilder errorManagerBuilder = ObjectBuilder.of(logContext, ErrorManager.class, className) + .setModuleName(getStringProperty(getKey("errorManager", errorManagerName, "module"))) + .addPostConstructMethods(getStringCsvArray(getKey("errorManager", errorManagerName, "postConfiguration"))); + configureProperties(errorManagerBuilder, "errorManager", errorManagerName); + errorManagers.putIfAbsent(errorManagerName, errorManagerBuilder.build(pojos)); + return true; + } + + private boolean configureFilter(final String filterName) { + if (filters.containsKey(filterName)) { + return true; + } + // First determine if we're using a defined filters or filters expression. We assume a defined filters if there is + // a filters.NAME property. + String filterValue = getStringProperty(getKey("filter", filterName), true, false); + if (filterValue == null) { + // We are a filters expression, parse the expression and create a filters + filters.putIfAbsent(filterName, () -> FilterExpressions.parse(logContext, filterName)); + } else { + // The AcceptAllFilter and DenyAllFilter are singletons. + if (AcceptAllFilter.class.getName().equals(filterValue)) { + filters.putIfAbsent(filterName, AcceptAllFilter::getInstance); + } else if (DenyAllFilter.class.getName().equals(filterValue)) { + filters.putIfAbsent(filterName, DenyAllFilter::getInstance); + } else { + // We assume we're a defined filter + final ObjectBuilder filterBuilder = ObjectBuilder.of(logContext, Filter.class, filterValue) + .setModuleName(getStringProperty(getKey("filter", filterName, "module"))) + .addPostConstructMethods(getStringCsvArray(getKey("filter", filterName, "postConfiguration"))); + configureProperties(filterBuilder, "errorManager", filterName); + filters.putIfAbsent(filterName, filterBuilder.build(pojos)); + } + } + return true; + } + + private void configurePojos(final String pojoName) { + if (pojos.containsKey(pojoName)) { + // already configured! + return; + } + final String className = getStringProperty(getKey("pojo", pojoName), true, false); + if (className == null) { + StandardOutputStreams.printError("POJO %s is not defined%n", pojoName); + return; + } + final ObjectBuilder pojoBuilder = ObjectBuilder.of(logContext, Object.class, className) + .setModuleName(getStringProperty(getKey("pojo", pojoName, "module"))) + .addPostConstructMethods(getStringCsvArray(getKey("pojo", pojoName, "postConfiguration"))); + configureProperties(pojoBuilder, "pojo", pojoName); + pojos.putIfAbsent(pojoName, pojoBuilder.build(pojos)); + } + + private String getStringProperty(final String key) { + return getStringProperty(key, true, true); + } + + private String getStringProperty(final String key, final boolean trim, final boolean resolveExpression) { + String value = properties.getProperty(key); + if (resolveExpression && value != null) { + value = resolveExpression(value); + } else if (value != null && trim) { + value = value.trim(); + } + return value; + } + + private String[] getStringCsvArray(final String key) { + final String property = properties.getProperty(key, ""); + if (property == null) { + return EMPTY_STRINGS; + } + final String value = property.trim(); + if (value.length() == 0) { + return EMPTY_STRINGS; + } + return value.split("\\s*,\\s*"); + } + + private void configureProperties(final ObjectBuilder builder, final String prefix, final String name) { + // First configure constructor properties + final String[] constructorPropertyNames = getStringCsvArray(getKey(prefix, name, "constructorProperties")); + for (String propertyName : constructorPropertyNames) { + final String valueString = getStringProperty(getKey(prefix, name, propertyName), false, true); + if (valueString != null) builder.addConstructorProperty(propertyName, valueString); + } + + // Next configure setter properties + final String[] propertyNames = getStringCsvArray(getKey(prefix, name, "properties")); + for (String propertyName : propertyNames) { + final String valueString = getStringProperty(getKey(prefix, name, propertyName), false, true); + if (valueString != null) builder.addProperty(propertyName, valueString); + } + } + + private static String getKey(final String prefix, final String objectName) { + return objectName.length() > 0 ? prefix + "." + objectName : prefix; + } + + private static String getKey(final String prefix, final String objectName, final String key) { + return objectName.length() > 0 ? prefix + "." + objectName + "." + key : prefix + "." + key; + } + + private static boolean resolveBooleanExpression(final String possibleExpression) { + final String value = resolveExpression(possibleExpression); + return !value.toLowerCase(Locale.ROOT).equals("false"); + } + + private static String resolveExpression(final String possibleExpression) { + final EnumSet flags = EnumSet.noneOf(Expression.Flag.class); + final Expression expression = Expression.compile(possibleExpression, flags); + return expression.evaluateWithPropertiesAndEnvironment(false); + } +} diff --git a/ext/src/main/java/org/jboss/logmanager/ext/filters/FilterExpressions.java b/ext/src/main/java/org/jboss/logmanager/ext/filters/FilterExpressions.java new file mode 100644 index 00000000..19764f2d --- /dev/null +++ b/ext/src/main/java/org/jboss/logmanager/ext/filters/FilterExpressions.java @@ -0,0 +1,267 @@ +/* + * JBoss, Home of Professional Open Source. + * + * Copyright 2018 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed 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.jboss.logmanager.ext.filters; + +import static java.lang.Character.isJavaIdentifierPart; +import static java.lang.Character.isJavaIdentifierStart; +import static java.lang.Character.isWhitespace; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.logging.Filter; +import java.util.logging.Level; + +import org.jboss.logmanager.LogContext; +import org.jboss.logmanager.filters.AcceptAllFilter; +import org.jboss.logmanager.filters.AllFilter; +import org.jboss.logmanager.filters.AnyFilter; +import org.jboss.logmanager.filters.DenyAllFilter; +import org.jboss.logmanager.filters.InvertFilter; +import org.jboss.logmanager.filters.LevelChangingFilter; +import org.jboss.logmanager.filters.LevelFilter; +import org.jboss.logmanager.filters.LevelRangeFilter; +import org.jboss.logmanager.filters.RegexFilter; +import org.jboss.logmanager.filters.SubstituteFilter; + +/** + * Helper class to parse filter expressions. + * + * @author James R. Perkins + */ +public class FilterExpressions { + + private static final String ACCEPT = "accept"; + private static final String ALL = "all"; + private static final String ANY = "any"; + private static final String DENY = "deny"; + private static final String LEVELS = "levels"; + private static final String LEVEL_CHANGE = "levelChange"; + private static final String LEVEL_RANGE = "levelRange"; + private static final String MATCH = "match"; + private static final String NOT = "not"; + private static final String SUBSTITUTE = "substitute"; + private static final String SUBSTITUTE_ALL = "substituteAll"; + + /** + * Pareses a filter expression and returns the parsed filter. + * + * @param logContext the log context this filter is for + * @param expression the filter expression + * + * @return the created filter + */ + public static Filter parse(final LogContext logContext, final String expression) { + final Iterator iterator = tokens(expression).iterator(); + return parseFilterExpression(logContext, iterator, true); + } + + private static Filter parseFilterExpression(final LogContext logContext, final Iterator iterator, final boolean outermost) { + if (!iterator.hasNext()) { + if (outermost) { + return null; + } + throw endOfExpression(); + } + final String token = iterator.next(); + if (ACCEPT.equals(token)) { + return AcceptAllFilter.getInstance(); + } else if (DENY.equals(token)) { + return DenyAllFilter.getInstance(); + } else if (NOT.equals(token)) { + expect("(", iterator); + final Filter nested = parseFilterExpression(logContext, iterator, false); + expect(")", iterator); + return new InvertFilter(nested); + } else if (ALL.equals(token)) { + expect("(", iterator); + final List filters = new ArrayList<>(); + do { + filters.add(parseFilterExpression(logContext, iterator, false)); + } while (expect(",", ")", iterator)); + return new AllFilter(filters); + } else if (ANY.equals(token)) { + expect("(", iterator); + final List filters = new ArrayList<>(); + do { + filters.add(parseFilterExpression(logContext, iterator, false)); + } while (expect(",", ")", iterator)); + return new AnyFilter(filters); + } else if (LEVEL_CHANGE.equals(token)) { + expect("(", iterator); + final Level level = logContext.getLevelForName(expectName(iterator)); + expect(")", iterator); + return new LevelChangingFilter(level); + } else if (LEVELS.equals(token)) { + expect("(", iterator); + final Set levels = new HashSet<>(); + do { + levels.add(logContext.getLevelForName(expectName(iterator))); + } while (expect(",", ")", iterator)); + return new LevelFilter(levels); + } else if (LEVEL_RANGE.equals(token)) { + final boolean minInclusive = expect("[", "(", iterator); + final Level minLevel = logContext.getLevelForName(expectName(iterator)); + expect(",", iterator); + final Level maxLevel = logContext.getLevelForName(expectName(iterator)); + final boolean maxInclusive = expect("]", ")", iterator); + return new LevelRangeFilter(minLevel, minInclusive, maxLevel, maxInclusive); + } else if (MATCH.equals(token)) { + expect("(", iterator); + final String pattern = expectString(iterator); + expect(")", iterator); + return new RegexFilter(pattern); + } else if (SUBSTITUTE.equals(token)) { + expect("(", iterator); + final String pattern = expectString(iterator); + expect(",", iterator); + final String replacement = expectString(iterator); + expect(")", iterator); + return new SubstituteFilter(pattern, replacement, false); + } else if (SUBSTITUTE_ALL.equals(token)) { + expect("(", iterator); + final String pattern = expectString(iterator); + expect(",", iterator); + final String replacement = expectString(iterator); + expect(")", iterator); + return new SubstituteFilter(pattern, replacement, true); + } else { + final String name = expectName(iterator); + throw new IllegalArgumentException(String.format("No filter named \"%s\" is defined", name)); + } + } + + private static String expectName(Iterator iterator) { + if (iterator.hasNext()) { + final String next = iterator.next(); + if (isJavaIdentifierStart(next.codePointAt(0))) { + return next; + } + } + throw new IllegalArgumentException("Expected identifier next in filter expression"); + } + + private static String expectString(final Iterator iterator) { + if (iterator.hasNext()) { + final String next = iterator.next(); + if (next.codePointAt(0) == '"') { + return next.substring(1); + } + } + throw new IllegalArgumentException("Expected string next in filter expression"); + } + + private static boolean expect(final String trueToken, final String falseToken, final Iterator iterator) { + final boolean hasNext = iterator.hasNext(); + final String next = hasNext ? iterator.next() : null; + final boolean result; + if (!hasNext || !((result = trueToken.equals(next)) || falseToken.equals(next))) { + throw new IllegalArgumentException("Expected '" + trueToken + "' or '" + falseToken + "' next in filter expression"); + } + return result; + } + + private static void expect(String token, Iterator iterator) { + if (!iterator.hasNext() || !token.equals(iterator.next())) { + throw new IllegalArgumentException("Expected '" + token + "' next in filter expression"); + } + } + + private static IllegalArgumentException endOfExpression() { + return new IllegalArgumentException("Unexpected end of filter expression"); + } + + + @SuppressWarnings("UnusedAssignment") + private static List tokens(final String source) { + final List tokens = new ArrayList<>(); + final int length = source.length(); + int idx = 0; + while (idx < length) { + int ch; + ch = source.codePointAt(idx); + if (isWhitespace(ch)) { + ch = source.codePointAt(idx); + idx = source.offsetByCodePoints(idx, 1); + } else if (isJavaIdentifierStart(ch)) { + int start = idx; + do { + idx = source.offsetByCodePoints(idx, 1); + } while (idx < length && isJavaIdentifierPart(ch = source.codePointAt(idx))); + tokens.add(source.substring(start, idx)); + } else if (ch == '"') { + final StringBuilder b = new StringBuilder(); + // tag token as a string + b.append('"'); + idx = source.offsetByCodePoints(idx, 1); + while (idx < length && (ch = source.codePointAt(idx)) != '"') { + ch = source.codePointAt(idx); + if (ch == '\\') { + idx = source.offsetByCodePoints(idx, 1); + if (idx == length) { + throw new IllegalArgumentException("Truncated filter expression string"); + } + ch = source.codePointAt(idx); + switch (ch) { + case '\\': + b.append('\\'); + break; + case '\'': + b.append('\''); + break; + case '"': + b.append('"'); + break; + case 'b': + b.append('\b'); + break; + case 'f': + b.append('\f'); + break; + case 'n': + b.append('\n'); + break; + case 'r': + b.append('\r'); + break; + case 't': + b.append('\t'); + break; + default: + throw new IllegalArgumentException("Invalid escape found in filter expression string"); + } + } else { + b.appendCodePoint(ch); + } + idx = source.offsetByCodePoints(idx, 1); + } + idx = source.offsetByCodePoints(idx, 1); + tokens.add(b.toString()); + } else { + int start = idx; + idx = source.offsetByCodePoints(idx, 1); + tokens.add(source.substring(start, idx)); + } + } + return tokens; + } +} diff --git a/ext/src/test/java/org/jboss/logmanager/ext/PropertyConfigurationTests.java b/ext/src/test/java/org/jboss/logmanager/ext/PropertyConfigurationTests.java new file mode 100644 index 00000000..62b0b9de --- /dev/null +++ b/ext/src/test/java/org/jboss/logmanager/ext/PropertyConfigurationTests.java @@ -0,0 +1,412 @@ +/* + * JBoss, Home of Professional Open Source. + * + * Copyright 2018 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed 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.jboss.logmanager.ext; + +import java.io.PrintWriter; +import java.io.Reader; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Properties; +import java.util.logging.ErrorManager; +import java.util.logging.Filter; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +import org.jboss.logmanager.ExtHandler; +import org.jboss.logmanager.ExtLogRecord; +import org.jboss.logmanager.Level; +import org.jboss.logmanager.LogContext; +import org.jboss.logmanager.formatters.PatternFormatter; +import org.jboss.logmanager.handlers.ConsoleHandler; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +/** + * @author James R. Perkins + */ +public class PropertyConfigurationTests { + private static final String DEFAULT_PATTERN = "%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n"; + + private LogContext logContext; + + @Before + public void setup() { + logContext = LogContext.create(); + TestHandler.INITIALIZED = false; + TestFormatter.INITIALIZED = false; + TestErrorManager.INITIALIZED = false; + TestFilter.INITIALIZED = false; + } + + @After + public void tearDown() throws Exception { + logContext.close(); + } + + @Test + public void readConfigs() throws Exception { + final Path configDir = Paths.get(System.getProperty("config.dir")); + Assert.assertTrue("Missing config dir: " + configDir, Files.exists(configDir)); + Files.walk(configDir) + .filter(Files::isRegularFile) + .forEach((configFile) -> { + try ( + LogContext logContext = LogContext.create(); + Reader reader = Files.newBufferedReader(configFile, StandardCharsets.UTF_8) + ) { + final Properties properties = new Properties(); + properties.load(reader); + PropertyConfigurator.configure(logContext, properties); + } catch (Exception e) { + final StringWriter writer = new StringWriter(); + writer.append("Failed to configure ") + .append(configFile.getFileName().toString()) + .append(System.lineSeparator()); + e.printStackTrace(new PrintWriter(writer)); + Assert.fail(writer.toString()); + } + }); + } + + @Test + public void testSimple() { + final Properties config = defaultProperties(); + PropertyConfigurator.configure(logContext, config); + testDefault(2, 1); + } + + @Test + public void testLogger() { + final String loggerName = PropertyConfigurationTests.class.getName(); + final Properties config = defaultProperties(loggerName); + + config.setProperty(String.join(",", "logger", loggerName, "level"), "INFO"); + config.setProperty(String.join(".", "logger", loggerName, "handlers"), "TEST"); + + config.setProperty("handler.TEST", TestHandler.class.getName()); + + PropertyConfigurator.configure(logContext, config); + testDefault(3, 1); + + final Logger logger = logContext.getLoggerIfExists(loggerName); + Assert.assertNotNull(logger); + final TestHandler testHandler = findType(TestHandler.class, logger.getHandlers()); + Assert.assertNotNull("Failed to find TestHandler", testHandler); + Assert.assertTrue(TestHandler.INITIALIZED); + } + + @Test + public void testErrorManager() { + final Properties config = defaultProperties(); + String handlers = config.getProperty("logger.handlers"); + if (handlers == null) { + handlers = "TEST"; + } else { + handlers = handlers + ",TEST"; + } + config.setProperty("logger.handlers", handlers); + + config.setProperty("handler.TEST", TestHandler.class.getName()); + config.setProperty("handler.TEST.errorManager", "TEST"); + + config.setProperty("errorManager.TEST", TestErrorManager.class.getName()); + + PropertyConfigurator.configure(logContext, config); + testDefault(2, 2); + + final Logger rootLogger = logContext.getLogger(""); + final TestHandler handler = findType(TestHandler.class, rootLogger.getHandlers()); + Assert.assertNotNull(handler); + Assert.assertTrue(TestHandler.INITIALIZED); + + final ErrorManager errorManager = handler.getErrorManager(); + Assert.assertTrue(errorManager instanceof TestErrorManager); + Assert.assertTrue(TestErrorManager.INITIALIZED); + } + + @Test + public void testPojo() { + final Properties config = defaultProperties(); + addPojoConfiguration(config); + PropertyConfigurator.configure(logContext, config); + testDefault(2, 2); + + final Logger rootLogger = logContext.getLogger(""); + final Handler[] handlers = rootLogger.getHandlers(); + + // Find the POJO handler + final PojoHandler handler = findType(PojoHandler.class, handlers); + Assert.assertNotNull("POJO handler was not found: " + Arrays.toString(handlers), handler); + Assert.assertNotNull(handler.pojoObject); + Assert.assertTrue(handler.pojoObject.initialized); + Assert.assertEquals("testValue", handler.pojoObject.value); + + } + + @Test + public void testUnusedHandler() { + final Properties config = defaultProperties(); + config.setProperty("handler.TEST", TestHandler.class.getName()); + + PropertyConfigurator.configure(logContext, config); + testDefault(2, 1); + + // The test handler should not have been activated + Assert.assertFalse("The handler should not have been initialized", TestHandler.INITIALIZED); + } + + @Test + public void testUnusedFormatter() { + final Properties config = defaultProperties(); + config.setProperty("formatter.TEST", TestFormatter.class.getName()); + + PropertyConfigurator.configure(logContext, config); + testDefault(2, 1); + + // The test handler should not have been activated + Assert.assertFalse("The formatter should not have been initialized", TestFormatter.INITIALIZED); + } + + @Test + public void testUnusedErrorManager() { + final Properties config = defaultProperties(); + config.setProperty("errorManager.TEST", TestErrorManager.class.getName()); + + PropertyConfigurator.configure(logContext, config); + testDefault(2, 1); + + // The test handler should not have been activated + Assert.assertFalse("The error manager should not have been initialized", TestErrorManager.INITIALIZED); + } + + @Test + public void testUnusedFilter() { + final Properties config = defaultProperties(); + config.setProperty("filter.TEST", TestFilter.class.getName()); + + PropertyConfigurator.configure(logContext, config); + testDefault(2, 1); + + // The test handler should not have been activated + Assert.assertFalse("The filter should not have been initialized", TestFilter.INITIALIZED); + } + + private void testDefault(final int expectedLoggers, final int expectedRootHandlers) { + final Collection loggerNames = Collections.list(logContext.getLoggerNames()); + // We should have two defined loggers + Assert.assertEquals("Expected two loggers to be defined found: " + loggerNames, expectedLoggers, loggerNames.size()); + + // Test the configured root logger + final Logger rootLogger = logContext.getLoggerIfExists(""); + Assert.assertNotNull("Root logger was not configured", rootLogger); + Assert.assertEquals(Level.DEBUG.intValue(), rootLogger.getLevel().intValue()); + + // There should only be a console handler, check that it's configured + final Handler[] handlers = rootLogger.getHandlers(); + Assert.assertNotNull("Expected handles to be defined", handlers); + Assert.assertEquals(String.format("Expected %d handlers found %d: %s", expectedRootHandlers, handlers.length, + Arrays.toString(handlers)), expectedRootHandlers, handlers.length); + final Handler handler = findType(ConsoleHandler.class, handlers); + Assert.assertNotNull("Failed to find the console handler", handler); + Assert.assertEquals(ConsoleHandler.class, handler.getClass()); + Assert.assertEquals(Level.TRACE.intValue(), handler.getLevel().intValue()); + } + + private static Properties defaultProperties(final String... additionalLoggers) { + final Properties config = new Properties(); + // Configure some default loggers + final StringBuilder sb = new StringBuilder("org.jboss.logmanager.ext"); + for (String additionalLogger : additionalLoggers) { + sb.append(',').append(additionalLogger); + } + config.setProperty("loggers", sb.toString()); + config.setProperty("logger.level", "DEBUG"); + config.setProperty("logger.handlers", "CONSOLE"); + + // Configure a handler + config.setProperty("handler.CONSOLE", ConsoleHandler.class.getName()); + config.setProperty("handler.CONSOLE.level", "TRACE"); + config.setProperty("handler.CONSOLE.formatter", "PATTERN"); + + // Configure a formatter + config.setProperty("formatter.PATTERN", PatternFormatter.class.getName()); + config.setProperty("formatter.PATTERN.properties", "pattern"); + config.setProperty("formatter.PATTERN.pattern", DEFAULT_PATTERN); + + return config; + } + + private static void addPojoConfiguration(final Properties config) { + String handlers = config.getProperty("logger.handlers"); + if (handlers == null) { + handlers = "POJO"; + } else { + handlers = handlers + ",POJO"; + } + config.setProperty("logger.handlers", handlers); + + // Configure the POJO handler + config.setProperty("handler.POJO", PojoHandler.class.getName()); + config.setProperty("handler.POJO.properties", "pojoObject"); + config.setProperty("handler.POJO.pojoObject", "POJO_OBJECT"); + + // Configure the POJO object + config.setProperty("pojos", "POJO_OBJECT"); + config.setProperty("pojo.POJO_OBJECT", PojoObject.class.getName()); + config.setProperty("pojo.POJO_OBJECT.properties", "value"); + config.setProperty("pojo.POJO_OBJECT.value", "testValue"); + config.setProperty("pojo.POJO_OBJECT.postConfiguration", "init,checkInitialized"); + + } + + private static T findType(final Class type, final Object[] array) { + if (array != null) { + for (Object obj : array) { + if (obj.getClass().isAssignableFrom(type)) { + return type.cast(obj); + } + } + } + return null; + } + + @SuppressWarnings("unused") + public static class PojoObject { + String name; + String value; + boolean initialized; + + public PojoObject() { + initialized = false; + } + + public void init() { + initialized = true; + } + + public void checkInitialized() { + if (!initialized) { + throw new IllegalStateException("Not initialized"); + } + } + + public String getName() { + return name; + } + + public void setName(final String name) { + this.name = name; + } + + public String getValue() { + return value; + } + + public void setValue(final String value) { + this.value = value; + } + } + + @SuppressWarnings("unused") + public static class PojoHandler extends ExtHandler { + PojoObject pojoObject; + + public PojoHandler() { + pojoObject = null; + } + + @Override + protected void doPublish(final ExtLogRecord record) { + super.doPublish(record); + } + + public PojoObject getPojoObject() { + return pojoObject; + } + + public void setPojoObject(final PojoObject pojoObject) { + this.pojoObject = pojoObject; + } + } + + public static class TestHandler extends Handler { + static boolean INITIALIZED; + + public TestHandler() { + INITIALIZED = true; + } + + @Override + public void publish(final LogRecord record) { + } + + @Override + public void flush() { + } + + @Override + public void close() throws SecurityException { + } + } + + public static class TestFormatter extends Formatter { + static boolean INITIALIZED; + + public TestFormatter() { + INITIALIZED = true; + } + + @Override + public String format(final LogRecord record) { + return record.getMessage(); + } + } + + public static class TestErrorManager extends ErrorManager { + static boolean INITIALIZED; + + public TestErrorManager() { + INITIALIZED = true; + } + } + + public static class TestFilter implements Filter { + static boolean INITIALIZED; + + public TestFilter() { + INITIALIZED = true; + } + + @Override + public boolean isLoggable(final LogRecord record) { + return true; + } + } +} diff --git a/core/src/test/resources/org/jboss/logmanager/default-logging.properties b/ext/src/test/resources/configs/default-logging.properties similarity index 86% rename from core/src/test/resources/org/jboss/logmanager/default-logging.properties rename to ext/src/test/resources/configs/default-logging.properties index bab27258..d270ad1c 100644 --- a/core/src/test/resources/org/jboss/logmanager/default-logging.properties +++ b/ext/src/test/resources/configs/default-logging.properties @@ -1,7 +1,7 @@ # # JBoss, Home of Professional Open Source. # -# Copyright 2014 Red Hat, Inc., and individual contributors +# Copyright 2018 Red Hat, Inc., and individual contributors # as indicated by the @author tags. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -43,24 +43,24 @@ handler.CONSOLE.autoFlush=true handler.CONSOLE.target=SYSTEM_OUT handler.CONSOLE.filter=FILTER -handler.FILE=org.jboss.logmanager.TestFileHandler +handler.FILE=org.jboss.logmanager.handlers.FileHandler handler.FILE.formatter=PATTERN handler.FILE.level=TRACE handler.FILE.properties=autoFlush,append,fileName handler.FILE.constructorProperties=fileName,append handler.FILE.autoFlush=true handler.FILE.append=false -handler.FILE.fileName=test.log +handler.FILE.fileName=${log.dir:target}/test.log handler.FILE.encoding=UTF-8 handler.FILE.filter=match(".*") handler.FILE.errorManager=DFT handlers=FILE -filter.FILTER=org.jboss.logmanager.AcceptFilter +filter.FILTER=org.jboss.logmanager.filters.AcceptAllFilter filters=FILTER2 -filter.FILTER2=org.jboss.logmanager.AcceptFilter +filter.FILTER2=org.jboss.logmanager.filters.AcceptAllFilter errorManager.DFT=org.jboss.logmanager.errormanager.OnlyOnceErrorManager @@ -77,11 +77,11 @@ formatter.OTHER.properties=pattern formatter.OTHER.pattern=%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%E%n pojos=filePojo -pojo.filePojo=org.jboss.logmanager.TestFileHandler +pojo.filePojo=org.jboss.logmanager.handlers.FileHandler pojo.filePojo.properties=autoFlush,append,fileName,encoding pojo.filePojo.constructorProperties=fileName,append pojo.filePojo.autoFlush=true pojo.filePojo.append=false -pojo.filePojo.fileName=test.log +pojo.filePojo.fileName=${log.dir:target}/test.log pojo.filePojo.encoding=UTF-8 pojo.filePojo.postConfiguration=flush \ No newline at end of file diff --git a/core/src/test/resources/org/jboss/logmanager/expected-spaced-value-logging.properties b/ext/src/test/resources/configs/expected-spaced-value-logging.properties similarity index 88% rename from core/src/test/resources/org/jboss/logmanager/expected-spaced-value-logging.properties rename to ext/src/test/resources/configs/expected-spaced-value-logging.properties index f403a8cc..0948211b 100644 --- a/core/src/test/resources/org/jboss/logmanager/expected-spaced-value-logging.properties +++ b/ext/src/test/resources/configs/expected-spaced-value-logging.properties @@ -1,7 +1,7 @@ # # JBoss, Home of Professional Open Source. # -# Copyright 2014 Red Hat, Inc., and individual contributors +# Copyright 2018 Red Hat, Inc., and individual contributors # as indicated by the @author tags. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,7 +35,7 @@ handler.CONSOLE.filter=FILTER handler.CONSOLE.encoding=UTF-8 handler.CONSOLE.errorManager=DFT -filter.FILTER=org.jboss.logmanager.AcceptFilter +filter.FILTER=org.jboss.logmanager.filters.AcceptAllFilter errorManager.DFT=org.jboss.logmanager.errormanager.OnlyOnceErrorManager @@ -44,11 +44,11 @@ formatter.PATTERN.properties=pattern formatter.PATTERN.pattern='\u0020'%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%E%n pojos=filePojo -pojo.filePojo=org.jboss.logmanager.TestFileHandler +pojo.filePojo=org.jboss.logmanager.handlers.FileHandler pojo.filePojo.properties=autoFlush,append,fileName,encoding pojo.filePojo.constructorProperties=fileName,append pojo.filePojo.autoFlush=true pojo.filePojo.append=false -pojo.filePojo.fileName=test.log +pojo.filePojo.fileName=${log.dir:target}/test.log pojo.filePojo.encoding=UTF-8 pojo.filePojo.postConfiguration=flush \ No newline at end of file diff --git a/core/src/test/resources/org/jboss/logmanager/expression-logging.properties b/ext/src/test/resources/configs/expression-logging.properties similarity index 88% rename from core/src/test/resources/org/jboss/logmanager/expression-logging.properties rename to ext/src/test/resources/configs/expression-logging.properties index cae0adef..ff0a3832 100644 --- a/core/src/test/resources/org/jboss/logmanager/expression-logging.properties +++ b/ext/src/test/resources/configs/expression-logging.properties @@ -1,7 +1,7 @@ # # JBoss, Home of Professional Open Source. # -# Copyright 2014 Red Hat, Inc., and individual contributors +# Copyright 2018 Red Hat, Inc., and individual contributors # as indicated by the @author tags. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,14 +34,14 @@ handler.CONSOLE.target=${handler.console.target:SYSTEM_OUT} handler.CONSOLE.filter=${handler.console.filter:FILTER} handler.CONSOLE.encoding=${handler.encoding:UTF-8} -handler.FILE=org.jboss.logmanager.TestFileHandler +handler.FILE=org.jboss.logmanager.handlers.FileHandler handler.FILE.formatter=PATTERN handler.FILE.level=${handler.level:TRACE} handler.FILE.properties=autoFlush,append,fileName,errorManager,encoding,filter handler.FILE.constructorProperties=fileName,append handler.FILE.autoFlush=${handler.autoFlush:true} handler.FILE.append=${handler.file.append:false} -handler.FILE.fileName=${handler.file.fileName:test.log} +handler.FILE.fileName=${log.dir:target}/${handler.file.fileName:test.log} handler.FILE.encoding=${handler.encoding:UTF-8} handler.FILE.filter=${handler.file.filter:match(".*")} handler.FILE.errorManager=${handler.errorManager:DFT} @@ -52,4 +52,4 @@ formatter.PATTERN=org.jboss.logmanager.formatters.PatternFormatter formatter.PATTERN.properties=pattern formatter.PATTERN.pattern=${format.pattern:%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%E%n} -filter.FILTER=org.jboss.logmanager.AcceptFilter \ No newline at end of file +filter.FILTER=org.jboss.logmanager.filters.AcceptAllFilter \ No newline at end of file diff --git a/core/src/test/resources/org/jboss/logmanager/invalid-logging.properties b/ext/src/test/resources/configs/invalid-logging.properties similarity index 86% rename from core/src/test/resources/org/jboss/logmanager/invalid-logging.properties rename to ext/src/test/resources/configs/invalid-logging.properties index df571bbf..b7a3c7c0 100644 --- a/core/src/test/resources/org/jboss/logmanager/invalid-logging.properties +++ b/ext/src/test/resources/configs/invalid-logging.properties @@ -1,7 +1,7 @@ # # JBoss, Home of Professional Open Source. # -# Copyright 2014 Red Hat, Inc., and individual contributors +# Copyright 2018 Red Hat, Inc., and individual contributors # as indicated by the @author tags. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -37,21 +37,21 @@ handler.CONSOLE.target=SYSTEM_OUT # handler.CONSOLE.filter=INVALID TODO (jrp) filters need to be revisited handler.CONSOLE.errorManager=INVALID -handler.FILE=org.jboss.logmanager.TestFileHandler +handler.FILE=org.jboss.logmanager.handlers.FileHandler handler.FILE.formatter=INVALID handler.FILE.level=TRACE handler.FILE.properties=autoFlush,append,fileName handler.FILE.constructorProperties=fileName,append handler.FILE.autoFlush=true handler.FILE.append=false -handler.FILE.fileName=test.log +handler.FILE.fileName=${log.dir:target}/test.log handler.FILE.encoding=UTF-8 handler.FILE.filter=match(".*") handler.FILE.errorManager=DFT handlers=FILE -filter.FILTER=org.jboss.logmanager.AcceptFilter +filter.FILTER=org.jboss.logmanager.filters.AcceptAllFilter errorManager.DFT=org.jboss.logmanager.errormanager.OnlyOnceErrorManager @@ -60,11 +60,11 @@ formatter.PATTERN.properties=pattern formatter.PATTERN.pattern=%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%E%n pojos=filePojo -pojo.filePojo=org.jboss.logmanager.TestFileHandler +pojo.filePojo=org.jboss.logmanager.handlers.FileHandler pojo.filePojo.properties=autoFlush,append,fileName,encoding pojo.filePojo.constructorProperties=fileName,append pojo.filePojo.autoFlush=true pojo.filePojo.append=false -pojo.filePojo.fileName=test.log +pojo.filePojo.fileName=${log.dir:target}/test.log pojo.filePojo.encoding=UTF-8 pojo.filePojo.postConfiguration=flush \ No newline at end of file diff --git a/core/src/test/resources/org/jboss/logmanager/simple-logging.properties b/ext/src/test/resources/configs/simple-logging.properties similarity index 100% rename from core/src/test/resources/org/jboss/logmanager/simple-logging.properties rename to ext/src/test/resources/configs/simple-logging.properties diff --git a/core/src/test/resources/org/jboss/logmanager/spaced-value-logging.properties b/ext/src/test/resources/configs/spaced-value-logging.properties similarity index 88% rename from core/src/test/resources/org/jboss/logmanager/spaced-value-logging.properties rename to ext/src/test/resources/configs/spaced-value-logging.properties index de6f3384..b70cc6c8 100644 --- a/core/src/test/resources/org/jboss/logmanager/spaced-value-logging.properties +++ b/ext/src/test/resources/configs/spaced-value-logging.properties @@ -1,7 +1,7 @@ # # JBoss, Home of Professional Open Source. # -# Copyright 2014 Red Hat, Inc., and individual contributors +# Copyright 2018 Red Hat, Inc., and individual contributors # as indicated by the @author tags. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,7 +35,7 @@ handler.CONSOLE.filter=FILTER\u0020 handler.CONSOLE.encoding=\u0020UTF-8\u0020 handler.CONSOLE.errorManager=\u0020DFT\u0020 -filter.FILTER=org.jboss.logmanager.AcceptFilter +filter.FILTER=org.jboss.logmanager.filters.AcceptAllFilter errorManager.DFT=org.jboss.logmanager.errormanager.OnlyOnceErrorManager @@ -44,11 +44,11 @@ formatter.PATTERN.properties=pattern formatter.PATTERN.pattern='\u0020'%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%E%n pojos=filePojo -pojo.filePojo=org.jboss.logmanager.TestFileHandler +pojo.filePojo=org.jboss.logmanager.handlers.FileHandler pojo.filePojo.properties=autoFlush,append,fileName,encoding pojo.filePojo.constructorProperties=fileName,append pojo.filePojo.autoFlush=true pojo.filePojo.append=false -pojo.filePojo.fileName=test.log +pojo.filePojo.fileName=${log.dir:target}/test.log pojo.filePojo.encoding=UTF-8 pojo.filePojo.postConfiguration=flush \ No newline at end of file diff --git a/pom.xml b/pom.xml index 17440b94..18ca34de 100644 --- a/pom.xml +++ b/pom.xml @@ -47,6 +47,7 @@ core + ext @@ -54,7 +55,9 @@ 1.0 4.0.4 1.0.4 + 3.3.2.Final 1.7.0.Final + 1.8 1.2.0.Final 4.12 @@ -71,11 +74,25 @@ + + + org.jboss.logging + jboss-logging + ${version.org.jboss.logging.jboss-logging} + org.jboss.modules jboss-modules ${version.org.jboss.modules.jboss-modules} + + org.kohsuke.metainf-services + metainf-services + ${version.org.kohsuke.metainf-services} + + true + provided + org.wildfly.common wildfly-common