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 extends T> 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 extends T> 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 extends T> 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 extends T> 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 extends T> 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