diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/Configurator1Test.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/Configurator1Test.java index e46d4cb3ae7..3c500e01af0 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/Configurator1Test.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/Configurator1Test.java @@ -460,7 +460,6 @@ void testRolling() { builder.add(builder.newRootLogger(Level.DEBUG).add(builder.newAppenderRef("rolling"))); final Configuration config = builder.build(); - config.initialize(); assertNotNull(config.getAppender("rolling"), "No rolling file appender"); assertEquals("RollingBuilder", config.getName(), "Unexpected Configuration"); // Initialize the new configuration diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/builder/CustomBuiltConfigurationTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/builder/CustomBuiltConfigurationTest.java new file mode 100644 index 00000000000..246ffe2c432 --- /dev/null +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/builder/CustomBuiltConfigurationTest.java @@ -0,0 +1,360 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.logging.log4j.core.config.builder; + +import static org.assertj.core.api.Assertions.assertThatCollection; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.lang.reflect.Constructor; +import java.util.Arrays; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.appender.ConsoleAppender; +import org.apache.logging.log4j.core.appender.mom.kafka.KafkaAppender; +import org.apache.logging.log4j.core.config.ConfigurationSource; +import org.apache.logging.log4j.core.config.CustomLevelConfig; +import org.apache.logging.log4j.core.config.Property; +import org.apache.logging.log4j.core.config.builder.api.AppenderComponentBuilder; +import org.apache.logging.log4j.core.config.builder.api.Component; +import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilder; +import org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration; +import org.apache.logging.log4j.core.config.builder.impl.DefaultConfigurationBuilder; +import org.apache.logging.log4j.core.filter.ThresholdFilter; +import org.junit.jupiter.api.Test; + +class CustomBuiltConfigurationTest { + + private static final String CONFIG_NAME = "FooBar Configuration"; + + private static final FooBar FOOBAR = new FooBar("wingding"); + + /** Test that the build configuration contains the intended attributes. */ + @Test + void testCustomBuiltConfiguration_Attributes() { + + final LoggerContext loggerContext = new LoggerContext("CustomBuiltConfigurationTest"); + final FoobarConfigurationBuilder builder = createTestBuilder(loggerContext); + final FooBarConfiguration config = builder.build(false); + + try { + + // build the configuration and set it in the context to start the configuration + loggerContext.setConfiguration(config); + + assertNotNull(config); + assertEquals(CONFIG_NAME, config.getName()); + assertEquals(10, config.getMonitorInterval()); + assertEquals(5000, config.getShutdownTimeoutMillis()); + assertThatCollection(config.getPluginPackages()).containsExactlyInAnyOrder("foo", "bar"); + + } finally { + + loggerContext.stop(); + } + } + + /** Test that the custom constructor of the custom configuration was called and the test object is accessible. */ + @Test + void testCustomBuiltConfiguration_CustomObject() { + + final LoggerContext loggerContext = new LoggerContext("CustomBuiltConfigurationTest"); + final FoobarConfigurationBuilder builder = createTestBuilder(loggerContext); + final FooBarConfiguration config = builder.build(false); + + try { + + // build the configuration and set it in the context to start the configuration + loggerContext.setConfiguration(config); + + assertNotNull(config); + + final FooBar fb = config.getFooBar(); + assertNotNull(fb); + assertEquals(FOOBAR, fb); + assertEquals(FOOBAR.getValue(), fb.getValue()); + + } finally { + + loggerContext.stop(); + } + } + + /** Test that the build configuration contains the intended appenders. */ + @Test + void testCustomBuiltConfiguration_Appenders() { + + final LoggerContext loggerContext = new LoggerContext("CustomBuiltConfigurationTest"); + final FoobarConfigurationBuilder builder = createTestBuilder(loggerContext); + final FooBarConfiguration config = builder.build(false); + + try { + + // build the configuration and set it in the context to start the configuration + loggerContext.setConfiguration(config); + + assertNotNull(config); + assertThatCollection(config.getAppenders().keySet()).containsExactlyInAnyOrder("Stdout", "Kafka"); + + } finally { + + loggerContext.stop(); + } + } + + /** Test that the build configuration contains the custom levels. */ + @Test + void testCustomBuiltConfiguration_CustomLevels() { + + final LoggerContext loggerContext = new LoggerContext("CustomBuiltConfigurationTest"); + final FoobarConfigurationBuilder builder = createTestBuilder(loggerContext); + final FooBarConfiguration config = builder.build(false); + + try { + + // build the configuration and set it in the context to start the configuration + loggerContext.setConfiguration(config); + + assertNotNull(config); + assertThatCollection(config.getCustomLevels().stream() + .map(CustomLevelConfig::getLevelName) + .collect(Collectors.toList())) + .containsExactlyInAnyOrder("Panic"); + + } finally { + + loggerContext.stop(); + } + } + + /** Test that the build configuration contains the intended filter. */ + @Test + void testCustomBuiltConfiguration_Filter() { + + final LoggerContext loggerContext = new LoggerContext("CustomBuiltConfigurationTest"); + final FoobarConfigurationBuilder builder = createTestBuilder(loggerContext); + final FooBarConfiguration config = builder.build(false); + + try { + + // build the configuration and set it in the context to start the configuration + loggerContext.setConfiguration(config); + + assertNotNull(config); + Filter filter = config.getFilter(); + assertNotNull(filter); + assertInstanceOf(ThresholdFilter.class, filter); + + } finally { + + loggerContext.stop(); + } + } + + /** Test that the build configuration contains the intended loggers. */ + @Test + void testCustomBuiltConfiguration_Loggers() { + + final LoggerContext loggerContext = new LoggerContext("CustomBuiltConfigurationTest"); + final FoobarConfigurationBuilder builder = createTestBuilder(loggerContext); + final FooBarConfiguration config = builder.build(false); + + try { + + // build the configuration and set it in the context to start the configuration + loggerContext.setConfiguration(config); + + assertNotNull(config); + assertThatCollection(config.getLoggers().keySet()) + .containsExactlyInAnyOrder("", "org.apache.logging.log4j", "org.apache.logging.log4j.core"); + + } finally { + + loggerContext.stop(); + } + } + + /** Test that the build configuration correctly registers and resolves properties. */ + @Test + void testCustomBuiltConfiguration_PropertyResolution() { + + final LoggerContext loggerContext = new LoggerContext("CustomBuiltConfigurationTest"); + final FoobarConfigurationBuilder builder = createTestBuilder(loggerContext); + final FooBarConfiguration config = builder.build(false); + + try { + + // build the configuration and set it in the context to start the configuration + loggerContext.setConfiguration(config); + + assertNotNull(config); + assertEquals("Wing", config.getConfigurationStrSubstitutor().replace("${P1}")); + assertEquals("WingDing", config.getConfigurationStrSubstitutor().replace("${P2}")); + + Appender kafkaAppender = config.getAppender("Kafka"); + assertNotNull(kafkaAppender); + assertInstanceOf(KafkaAppender.class, kafkaAppender); + final Property p2Property = Arrays.stream(((KafkaAppender) kafkaAppender).getPropertyArray()) + .collect(Collectors.toMap(Property::getName, Function.identity())) + .get("P2"); + assertNotNull(p2Property); + assertEquals("WingDing", p2Property.getValue()); + + } finally { + + loggerContext.stop(); + } + } + + /** + * Creates, preconfigures and returns a test builder instance. + * @param loggerContext the logger context to use + * @return the created configuration builder + */ + private FoobarConfigurationBuilder createTestBuilder(final LoggerContext loggerContext) { + final FoobarConfigurationBuilder builder = new FoobarConfigurationBuilder(); + builder.setLoggerContext(loggerContext); + addTestFixtures(builder); + return builder; + } + + /** + * Populates the given configuration-builder. + * @param builder the builder + */ + private void addTestFixtures(final ConfigurationBuilder builder) { + builder.setConfigurationName(CONFIG_NAME); + builder.setStatusLevel(Level.ERROR); + builder.setMonitorInterval(10); + builder.setShutdownTimeout(5000, TimeUnit.MILLISECONDS); + builder.add(builder.newScriptFile("target/test-classes/scripts/filter.groovy") + .addIsWatched(true)); + builder.add(builder.newFilter("ThresholdFilter", Filter.Result.ACCEPT, Filter.Result.NEUTRAL) + .addAttribute("level", Level.DEBUG)); + + final AppenderComponentBuilder appenderBuilder = + builder.newAppender("Stdout", "CONSOLE").addAttribute("target", ConsoleAppender.Target.SYSTEM_OUT); + appenderBuilder.add( + builder.newLayout("PatternLayout").addAttribute("pattern", "%d [%t] %-5level: %msg%n%throwable")); + appenderBuilder.add(builder.newFilter("MarkerFilter", Filter.Result.DENY, Filter.Result.NEUTRAL) + .addAttribute("marker", "FLOW")); + builder.add(appenderBuilder); + + final AppenderComponentBuilder appenderBuilder2 = + builder.newAppender("Kafka", "Kafka").addAttribute("topic", "my-topic"); + appenderBuilder2.addComponent(builder.newProperty("bootstrap.servers", "localhost:9092")); + appenderBuilder2.addComponent(builder.newProperty("P2", "${P2}")); + appenderBuilder2.add(builder.newLayout("GelfLayout") + .addAttribute("host", "my-host") + .addComponent(builder.newKeyValuePair("extraField", "extraValue"))); + builder.add(appenderBuilder2); + + builder.add(builder.newLogger("org.apache.logging.log4j", Level.DEBUG, true) + .add(builder.newAppenderRef("Stdout")) + .addAttribute("additivity", false)); + builder.add(builder.newLogger("org.apache.logging.log4j.core").add(builder.newAppenderRef("Stdout"))); + builder.add(builder.newRootLogger(Level.ERROR).add(builder.newAppenderRef("Stdout"))); + + builder.addProperty("P1", "Wing"); + builder.addProperty("P2", "${P1}Ding"); + builder.add(builder.newCustomLevel("Panic", 17)); + builder.setPackages("foo,bar"); + } + + // + // Test implementations + // + + /** A custom {@link DefaultConfigurationBuilder} implementation that generates a {@link FooBarConfiguration}. */ + public static class FoobarConfigurationBuilder extends DefaultConfigurationBuilder { + + public FoobarConfigurationBuilder() { + super(FooBarConfiguration.class); + } + + /** {@inheritDoc} */ + @Override + protected FooBarConfiguration createNewConfigurationInstance(Class configurationClass) { + Objects.requireNonNull(configurationClass, "The 'configurationClass' argument must not be null."); + try { + final Constructor constructor = FooBarConfiguration.class.getConstructor( + LoggerContext.class, ConfigurationSource.class, Component.class, FooBar.class); + return constructor.newInstance( + getLoggerContext().orElse(null), + getConfigurationSource().orElse(null), + getRootComponent(), + FOOBAR); + } catch (final Exception ex) { + throw new IllegalStateException( + "Configuration class '" + configurationClass.getName() + "' cannot be instantiated.", ex); + } + } + } + + /** + * A custom {@link BuiltConfiguration} implementation with a custom constructor that takes + * an additional {@code FooBar} argument. + */ + public static class FooBarConfiguration extends BuiltConfiguration { + + private int monitorInterval; + + private final FooBar fooBar; + + public FooBarConfiguration( + LoggerContext loggerContext, ConfigurationSource source, Component rootComponent, FooBar fooBar) { + super(loggerContext, source, rootComponent); + this.fooBar = Objects.requireNonNull(fooBar, "fooBar"); + } + + public FooBar getFooBar() { + return fooBar; + } + + public int getMonitorInterval() { + return this.monitorInterval; + } + + /** {@inheritDoc} */ + @Override + public void setMonitorInterval(int seconds) { + super.setMonitorInterval(seconds); + this.monitorInterval = seconds; + } + } + + /** Test object used by custom configuration-builder and configuration. */ + public static class FooBar { + + private final String value; + + public FooBar(final String value) { + this.value = value; + } + + public String getValue() { + return this.value; + } + } +} diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/api/ConfigurationBuilder.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/api/ConfigurationBuilder.java index 3b9fc489614..b8c174f39e5 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/api/ConfigurationBuilder.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/api/ConfigurationBuilder.java @@ -23,34 +23,43 @@ import org.apache.logging.log4j.core.Filter; import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.ConfigurationException; import org.apache.logging.log4j.core.config.ConfigurationSource; import org.apache.logging.log4j.core.util.Builder; +import org.osgi.annotation.versioning.ProviderType; /** * Interface for building logging configurations. * @param The Configuration type created by this builder. * @since 2.4 */ +@ProviderType public interface ConfigurationBuilder extends Builder { /** * Adds a ScriptComponent. - * @param builder The ScriptComponentBuilder with all of its attributes and sub components set. + * @param builder The ScriptComponentBuilder with all of its attributes and subcomponents set. * @return this builder instance. + * @throws ConfigurationException if an error occurs while building the given builder + * @throws NullPointerException if the builder argument is null */ ConfigurationBuilder add(ScriptComponentBuilder builder); /** * Adds a ScriptFileComponent. - * @param builder The ScriptFileComponentBuilder with all of its attributes and sub components set. + * @param builder The ScriptFileComponentBuilder with all of its attributes and subcomponents set. * @return this builder instance. + * @throws ConfigurationException if an error occurs while building the given builder + * @throws NullPointerException if the builder argument is null */ ConfigurationBuilder add(ScriptFileComponentBuilder builder); /** * Adds an AppenderComponent. - * @param builder The AppenderComponentBuilder with all of its attributes and sub components set. + * @param builder The AppenderComponentBuilder with all of its attributes and subcomponents set. * @return this builder instance. + * @throws ConfigurationException if an error occurs while building the given builder + * @throws NullPointerException if the builder argument is null */ ConfigurationBuilder add(AppenderComponentBuilder builder); @@ -58,38 +67,61 @@ public interface ConfigurationBuilder extends Builder add(CustomLevelComponentBuilder builder); /** * Adds a Filter component. - * @param builder the FilterComponentBuilder with all of its attributes and sub components set. + * @param builder the FilterComponentBuilder with all of its attributes and subcomponents set. * @return this builder instance. + * @throws ConfigurationException if an error occurs while building the given builder + * @throws NullPointerException if the builder argument is null */ ConfigurationBuilder add(FilterComponentBuilder builder); /** * Adds a Logger component. - * @param builder The LoggerComponentBuilder with all of its attributes and sub components set. + * @param builder The LoggerComponentBuilder with all of its attributes and subcomponents set. * @return this builder instance. + * @throws ConfigurationException if an error occurs while building the given builder + * @throws NullPointerException if the builder argument is null */ ConfigurationBuilder add(LoggerComponentBuilder builder); /** * Adds the root Logger component. - * @param builder The RootLoggerComponentBuilder with all of its attributes and sub components set. + * @param builder The RootLoggerComponentBuilder with all of its attributes and subcomponents set. * @return this builder instance. + * @throws ConfigurationException if an error occurs while building the given builder + * @throws NullPointerException if the builder argument is null */ ConfigurationBuilder add(RootLoggerComponentBuilder builder); /** * Adds a Property key and value. + *

+ * This is a convenience method which creates, configures and immediately adds + * a {@code PropertyComponentBuilder}. + *

* @param key The property key. * @param value The property value. * @return this builder instance. + * @throws ConfigurationException if an error occurs while building the created {@code PropertyComponentBuilder} + * @throws NullPointerException if the builder argument is null */ ConfigurationBuilder addProperty(String key, String value); + /** + * Adds a Property component. + * @param builder the PropertyComponentBuilder with all of its attributes and subcomponents set + * @return this builder instance + * @throws ConfigurationException if an error occurs while building the given builder + * @throws NullPointerException if the builder argument is null + */ + ConfigurationBuilder addProperty(PropertyComponentBuilder builder); + /** * Returns a builder for creating Async Loggers. * @param name The name of the Logger. @@ -392,6 +424,9 @@ public interface ConfigurationBuilder extends Builder + * If the given value is {@code null}, any previous value will be cleared. + *

* @param advertiser The Advertiser Plugin name. * @return this builder instance. */ @@ -399,13 +434,19 @@ public interface ConfigurationBuilder extends Builder + * If the given value is {@code null}, any previous value will be cleared. + *

+ * @param name the name of the {@link Configuration}. By default, the value is {@code "Built"}. * @return this builder instance. */ ConfigurationBuilder setConfigurationName(String name); /** * Sets the configuration source, if one exists. + *

+ * If the given value is {@code null}, any previous value will be cleared. + *

* @param configurationSource the ConfigurationSource. * @return this builder instance. */ @@ -413,13 +454,29 @@ public interface ConfigurationBuilder extends Builder + * If the given value is {@code null}, any previous value will be cleared. + *

* @param intervalSeconds The number of seconds that should pass between checks of the configuration file. * @return this builder instance. */ ConfigurationBuilder setMonitorInterval(String intervalSeconds); + /** + * Sets the interval at which the configuration file should be checked for changes. + *

+ * If the given value is {@code null}, any previous value will be cleared. + *

+ * @param intervalSeconds the number of seconds that should pass between checks of the configuraion file + * @return this builder instance. + */ + ConfigurationBuilder setMonitorInterval(int intervalSeconds); + /** * Sets the list of packages to search for plugins. + *

+ * If the given value is {@code null}, any previous value will be cleared. + *

* @param packages The comma separated list of packages. * @return this builder instance. */ @@ -427,30 +484,78 @@ public interface ConfigurationBuilder extends Builder + * If the given value is {@code null}, any previous value will be cleared. + *

* @param flag "disable" will prevent the shutdown hook from being set. * @return this builder instance. */ ConfigurationBuilder setShutdownHook(String flag); /** - * How long appenders and background tasks will get to shutdown when the JVM shuts down. - * Default is zero which mean that each appender uses its default timeout, and don't wait for background + * How long appenders and background tasks will get to shut down when the JVM shuts down. + * The default is zero which mean that each appender uses its default timeout, and don't wait for background * tasks. Not all appenders will honor this, it is a hint and not an absolute guarantee that the shutdown * procedure will not take longer. Setting this too low increase the risk of losing outstanding log events * not yet written to the final destination. (Not used if {@link #setShutdownHook(String)} is set to "disable".) + *

+ * If the timeout value is set to {@code null} any previously configured value will be cleared. + *

* @return this builder instance. - * + * @throws NullPointerException if the given {@code timeUnit} is {@code null} * @see LoggerContext#stop(long, TimeUnit) */ ConfigurationBuilder setShutdownTimeout(long timeout, TimeUnit timeUnit); + /** + * Sets the shutdown timeout for appenders and background tasks (in the specified time-unit) to shut down + * when the JVM is shutdown + *

+ * If the given value is {@code null}, any previous value will be cleared. + *

+ * @param timeout the timeout (in the given time-unit + * @param timeUnit the time-unit of the {@code timeout} value (i.e. SECONDS, MILLISECONDS) + * @return this builder instance + * @throws IllegalArgumentException if the {@code timeout} can not be converted to a valid {@code Long} + * @throws NullPointerException if the {@code timeUnit} is {@code null} + */ + ConfigurationBuilder setShutdownTimeout(String timeout, TimeUnit timeUnit); + + /** + * Sets the shutdown timeout (in milliseconds) for appenders and background tasks to shut down when the JVM + * is shutdown. + *

+ * If the given value is {@code null}, any previous value will be cleared. + *

+ * @param timeoutMillis the timeout in milliseconds + * @return this builder instance + */ + ConfigurationBuilder setShutdownTimeout(long timeoutMillis); + /** * Sets the level of the StatusLogger. + *

+ * If the given value is {@code null}, any previous value will be cleared. + *

* @param level The logging level. * @return this builder instance. */ ConfigurationBuilder setStatusLevel(Level level); + /** + * Sets the level of the StatusLogger. + *

+ * If the given value is {@code null}, any previous value will be cleared. + *

+ *

+ * If the given value is not {@code null}, it must be resolvable to a valid configured level + *

+ * @param level The logging level + * @return this builder instance + * @throws IllegalArgumentException if the given argument is not a valid level + */ + ConfigurationBuilder setStatusLevel(String level); + /** * Sets whether the logging should include constructing Plugins. * @param verbosity "disable" will hide messages from plugin construction. diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/api/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/api/package-info.java index 273906a5d48..6d91c2d89d8 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/api/package-info.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/api/package-info.java @@ -20,7 +20,7 @@ * @since 2.4 */ @Export -@Version("2.20.1") +@Version("2.25.0") package org.apache.logging.log4j.core.config.builder.api; import org.osgi.annotation.bundle.Export; diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/impl/BuiltConfiguration.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/impl/BuiltConfiguration.java index f1ec35e06a0..07414399108 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/impl/BuiltConfiguration.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/impl/BuiltConfiguration.java @@ -20,103 +20,183 @@ import java.io.InputStream; import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.config.AbstractConfiguration; +import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.ConfigurationSource; import org.apache.logging.log4j.core.config.Node; import org.apache.logging.log4j.core.config.Reconfigurable; import org.apache.logging.log4j.core.config.builder.api.Component; -import org.apache.logging.log4j.core.config.plugins.util.PluginManager; +import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilder; import org.apache.logging.log4j.core.config.plugins.util.PluginType; import org.apache.logging.log4j.core.config.status.StatusConfiguration; +import org.apache.logging.log4j.core.net.Advertiser; import org.apache.logging.log4j.core.util.Patterns; /** - * This is the general version of the Configuration created by the Builder. It may be extended to - * enhance its functionality. + * A {@link Configuration} that is built (assembled) via a {@link ConfigurationBuilder}. + *

+ * This base implementation may be extended to enhance its functionality. + *

+ *

Important Notes for Extension:

+ *

+ *

    + *
  • This class is designed to be extended for custom configurations. Subclasses should maintain + * the contract of returning valid and non-{@code null} configuration components.
  • + *
  • Fields such as {@code rootComponent} are intentionally made accessible to subclasses. However, + * direct modification of such fields is discouraged to prevent inconsistencies.
  • + *
  • Where possible, subclasses should adhere to the public accessor methods and override provided hooks + * like {@link #setup()} to implement additional configuration logic.
  • + *
+ *

+ *

Backward Compatibility

+ *

+ * Certain design decisions in this class, such as the use of + * a {@code protected} field for {@code rootComponent}, have been retained for historical reasons and due to + * guarantees of binary compatibility for users of this class. Users of this API should avoid directly manipulating + * internal fields where possible and instead rely on public or protected accessor methods where available. + *

+ *

+ * This implementation is neither immutable nor thread-safe. + *

* * @since 2.4 */ public class BuiltConfiguration extends AbstractConfiguration { + private final StatusConfiguration statusConfig; - protected Component rootComponent; - private Component loggersComponent; - private Component appendersComponent; - private Component filtersComponent; - private Component propertiesComponent; - private Component customLevelsComponent; - private Component scriptsComponent; + + protected Component rootComponent; // should be private and accessed via getter, but backwards-compatibility... + private String contentType = "text"; + /** + * Constructs a new instance. + *

+ * The configuration assembled using the {@link ConfigurationBuilder} is transported via the given + * {@code rootComponent} and used to populate this configuration's root {@code Node} in the + * {@link #setup} method implementation. + *

+ *

+ * This constructor is called via reflection from the {@link DefaultConfigurationBuilder}. + *

+ * + * @param loggerContext the logger-context (can be {@code null}) + * @param configurationSource the configuration-source + * @param rootComponent the root-component created by the builder + * @throws NullPointerException if the {@code source} or {@code rootComponent} argument is {@code null} + */ public BuiltConfiguration( - final LoggerContext loggerContext, final ConfigurationSource source, final Component rootComponent) { - super(loggerContext, source); - statusConfig = new StatusConfiguration().withStatus(getDefaultStatus()); - for (final Component component : rootComponent.getComponents()) { - switch (component.getPluginType()) { - case "Scripts": { - scriptsComponent = component; - break; - } - case "Loggers": { - loggersComponent = component; - break; - } - case "Appenders": { - appendersComponent = component; - break; - } - case "Filters": { - filtersComponent = component; - break; - } - case "Properties": { - propertiesComponent = component; - break; - } - case "CustomLevels": { - customLevelsComponent = component; - break; - } + final LoggerContext loggerContext, + final ConfigurationSource configurationSource, + final Component rootComponent) { + + super( + loggerContext, + Objects.requireNonNull(configurationSource, "The 'configurationSource' argument cannot be null")); + + Objects.requireNonNull(rootComponent, "The 'rootComponent' argument cannot be null"); + + this.rootComponent = rootComponent; + this.statusConfig = new StatusConfiguration().withStatus(getDefaultStatus()); + + // process each of the root-component's attributes, dealing with special cases and assigning all to root node + for (Map.Entry entry : rootComponent.getAttributes().entrySet()) { + final String key = entry.getKey().trim(); + final String value = entry.getValue(); + this.getRootNode().getAttributes().put(key, value); // all attributes get passed to root-node + if ("advertise".equalsIgnoreCase(key)) { + createAdvertiser(value, getConfigurationSource()); + } else if ("dest".equalsIgnoreCase(key)) { + statusConfig.withDestination(value); + } else if ("monitorInterval".equalsIgnoreCase(key)) { + setMonitorInterval(value, TimeUnit.SECONDS); + } else if ("name".equalsIgnoreCase(key)) { + setName(value); + } else if ("packages".equalsIgnoreCase(key)) { + setPluginPackages(value); + } else if ("shutdownHook".equalsIgnoreCase(key)) { + setShutdownHook(value); + } else if ("shutdownTimeout".equalsIgnoreCase(key)) { + setShutdownTimeout(value, TimeUnit.MILLISECONDS); + } else if ("status".equalsIgnoreCase(key)) { + statusConfig.withStatus(value); } } - this.rootComponent = rootComponent; + + this.statusConfig.initialize(); } + /** + * {@inheritDoc} + *

+ * Converts the {@code Component} children of the root component to Log4j configuration nodes and + * subsequently invalidates the root component. + *

+ */ @Override public void setup() { + final List children = rootNode.getChildren(); - if (propertiesComponent.getComponents().size() > 0) { - children.add(convertToNode(rootNode, propertiesComponent)); - } - if (scriptsComponent.getComponents().size() > 0) { - children.add(convertToNode(rootNode, scriptsComponent)); - } - if (customLevelsComponent.getComponents().size() > 0) { - children.add(convertToNode(rootNode, customLevelsComponent)); - } - children.add(convertToNode(rootNode, loggersComponent)); - children.add(convertToNode(rootNode, appendersComponent)); - if (filtersComponent.getComponents().size() > 0) { - if (filtersComponent.getComponents().size() == 1) { - children.add( - convertToNode(rootNode, filtersComponent.getComponents().get(0))); - } else { - children.add(convertToNode(rootNode, filtersComponent)); - } + + if (this.rootComponent != null) { + + getChildComponent("Properties") + .filter(c -> !c.getComponents().isEmpty()) + .ifPresent(c -> children.add(convertToNode(rootNode, c))); + getChildComponent("Scripts") + .filter(c -> !c.getComponents().isEmpty()) + .ifPresent(c -> children.add(convertToNode(rootNode, c))); + getChildComponent("CustomLevels") + .filter(c -> !c.getComponents().isEmpty()) + .ifPresent(c -> children.add(convertToNode(rootNode, c))); + + children.add(convertToNode(rootNode, getChildComponent("Loggers").orElse(new Component("Loggers")))); + children.add(convertToNode(rootNode, getChildComponent("Appenders").orElse(new Component("Appenders")))); + + getChildComponent("Filters") + .filter(c -> !c.getComponents().isEmpty()) + .ifPresent(c -> { + if (c.getComponents().size() == 1) { + children.add( + convertToNode(rootNode, c.getComponents().get(0))); + } else { + children.add(convertToNode(rootNode, c)); + } + }); } - rootComponent = null; + + this.rootComponent = null; } + /** + * Gets the content-type of the built configuration. + * @return the content-type + */ public String getContentType() { return this.contentType; } + /** + * Sets the content-type of the build configuration. + * @param contentType the content-type + */ public void setContentType(final String contentType) { this.contentType = contentType; } - public void createAdvertiser(final String advertiserString, final ConfigurationSource configSource) { + /** + * Creates and sets this configuration's {@link Advertiser} for the given configuration-source. + * @param name the name of the advertiser + * @param configSource the configuration-source + */ + public void createAdvertiser(final String name, final ConfigurationSource configSource) { byte[] buffer = null; try { if (configSource != null) { @@ -126,39 +206,146 @@ public void createAdvertiser(final String advertiserString, final ConfigurationS } } } catch (final IOException ioe) { - LOGGER.warn("Unable to read configuration source " + configSource.toString()); + LOGGER.warn("Unable to read configuration source {}", configSource); } - super.createAdvertiser(advertiserString, configSource, buffer, contentType); + super.createAdvertiser(name, configSource, buffer, contentType); } + /** + * Returns the status-logger fallback listener configuration. + *

+ * Implementations should always return a non-{@code null} value. + *

+ * @return the status configuration + */ public StatusConfiguration getStatusConfiguration() { return statusConfig; } + /** + * Sets the packages to search for plugin implementations. + * @param packages a comma-separated list of package names + */ public void setPluginPackages(final String packages) { - pluginPackages.addAll(Arrays.asList(packages.split(Patterns.COMMA_SEPARATOR))); + Objects.requireNonNull(packages, "The 'packages' argument cannot be null"); + List packageList = Arrays.stream(packages.split(Patterns.COMMA_SEPARATOR)) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .collect(Collectors.toList()); + pluginPackages.addAll(packageList); } + /** + * Sets the shutdown hook flag. + * @param flag if "disable" the shutdown hook will be disabled; otherwise, all other values enable it + */ public void setShutdownHook(final String flag) { - isShutdownHookEnabled = !"disable".equalsIgnoreCase(flag); + setShutdownHook(!"disable".equalsIgnoreCase(flag)); } - public void setShutdownTimeoutMillis(final long shutdownTimeoutMillis) { - this.shutdownTimeoutMillis = shutdownTimeoutMillis; + /** + * Sets the enablement of the shutdown hook. + * @param flag {@code true} to enable the shutdown hook (default); otherwise, {@code false} to disable + */ + public void setShutdownHook(final boolean flag) { + isShutdownHookEnabled = flag; } - public void setMonitorInterval(final int intervalSeconds) { - if (this instanceof Reconfigurable && intervalSeconds > 0) { - initializeWatchers((Reconfigurable) this, getConfigurationSource(), intervalSeconds); + /** + * Sets the shutdown timeout to the given value converted to milliseconds using the provided time-unit. + * @param value the value + * @param timeUnit the time-unit of the value (i.e. MILLISECONDS, SECONDS, MINUTES, etc.) + */ + public void setShutdownTimeout(final String value, final TimeUnit timeUnit) { + Objects.requireNonNull(value, "The 'value' argument cannot be null"); + Objects.requireNonNull(timeUnit, "The 'timeUnit' argument cannot be null"); + try { + setShutdownTimeoutMillis(timeUnit.toMillis(Long.parseLong(value))); + } catch (final Exception ex) { + LOGGER.error("The given shudown timeoutt is invalid '{}'. Reason: {}", value, ex.getMessage()); } } - @Override - public PluginManager getPluginManager() { - return pluginManager; + /** + * Sets the shutdown timeout in milliseconds. + * @param millis the number of milliseconds to set + */ + public void setShutdownTimeoutMillis(final long millis) { + this.shutdownTimeoutMillis = millis; + } + + /** + * Sets the monitor interval to the given value converted to seconds using the provided time-unit. + * @param value the value + * @param timeUnit the time-unit of the value (i.e. MILLISECONDS, SECONDS, MINUTES, etc.) + */ + public void setMonitorInterval(final String value, final TimeUnit timeUnit) { + Objects.requireNonNull(value, "The 'value' argument cannot be null"); + Objects.requireNonNull(timeUnit, "The 'timeUnit' argument cannot be null"); + try { + setMonitorInterval(Math.toIntExact(timeUnit.toSeconds(Integer.parseInt(value)))); + } catch (final Exception ex) { + LOGGER.error("The given monitor interval is invalid '{}'. Reason: {}", value, ex.getMessage()); + } + } + + /** + * Sets the monitor interval to the given number of seconds. + *

+ * If the given value is greater than 0 and this instance implements {@link Reconfigurable}, + * this method will trigger the {@link #initializeWatchers(Reconfigurable, ConfigurationSource, int)} + * method. + *

+ * @param seconds the number of seconds to set + */ + public void setMonitorInterval(final int seconds) { + if (this instanceof Reconfigurable && seconds > 0) { + initializeWatchers((Reconfigurable) this, getConfigurationSource(), seconds); + } + } + + /** + * Gets the root component. + *

+ * NOTE: After {@link #setup()} has been called this will always return an empty optional. + *

+ * @return an optional containing the root component or that is empty if undefined + */ + protected final Optional getRootComponent() { + return Optional.ofNullable(rootComponent); + } + + /** + * Gets the child component of the root component with the given plugin-type. + * @param pluginType the plugin-type to lookup + * @return an optional containing the resolved child component or that is empty if not found + */ + protected final Optional getChildComponent(final String pluginType) { + Objects.requireNonNull(pluginType, "The 'pluginType' argument cannot be null"); + return Optional.ofNullable(getChildComponents().get(pluginType)); + } + + /** + * Returns a map of all "named" child components. + * @return an immutable map of child components keyed by name + */ + protected final Map getChildComponents() { + + return this.rootComponent.getComponents().stream() + .filter(c -> Objects.nonNull(c.getPluginType())) + .collect(Collectors.toMap(Component::getPluginType, Function.identity())); } + /** + * Converts the given configuration {@link Component} to a runtime configuration {@link Node} + * with the given parent node. + * @param parent the target parent node + * @param component the component to convert + * @return the constructed node + */ protected Node convertToNode(final Node parent, final Component component) { + Objects.requireNonNull(parent, "The 'parent' argument cannot be null"); + Objects.requireNonNull(component, "The 'component' argument cannot be null"); final String name = component.getPluginType(); final PluginType pluginType = pluginManager.getPluginType(name); final Node node = new Node(parent, name, pluginType); diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/impl/DefaultConfigurationBuilder.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/impl/DefaultConfigurationBuilder.java index 633e619b281..593a0efb5b9 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/impl/DefaultConfigurationBuilder.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/impl/DefaultConfigurationBuilder.java @@ -22,8 +22,9 @@ import java.io.StringReader; import java.io.StringWriter; import java.lang.reflect.Constructor; -import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.concurrent.TimeUnit; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; @@ -32,7 +33,6 @@ import javax.xml.transform.Result; import javax.xml.transform.Source; import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.TransformerFactoryConfigurationError; @@ -41,7 +41,6 @@ import org.apache.logging.log4j.Level; import org.apache.logging.log4j.core.Filter; import org.apache.logging.log4j.core.LoggerContext; -import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.ConfigurationException; import org.apache.logging.log4j.core.config.ConfigurationSource; import org.apache.logging.log4j.core.config.LoggerConfig; @@ -63,555 +62,930 @@ import org.apache.logging.log4j.core.util.Throwables; /** + * A default ConfigurationBuilder that is used to build a BuiltConfiguration instance. + * * @param The BuiltConfiguration type. * @since 2.4 */ public class DefaultConfigurationBuilder implements ConfigurationBuilder { - private static final String INDENT = " "; + /** Indentation prefix used when generating an XML representation of this builder's configuration. */ + private static final String XML_INDENT = " "; + + /** The class of the configuration instance being built. */ + private final Class configurationClass; + /** The root component. */ private final Component root = new Component(); - private Component loggers; - private Component appenders; - private Component filters; - private Component properties; - private Component customLevels; - private Component scripts; - private final Class clazz; + + /** Standard component: "Appenders". */ + private final Component appenders; + + /** Standard component: "CustomLevels". */ + private final Component customLevels; + + /** Standard component: "Filters". */ + private final Component filters; + + /** Standard component: "Loggers". */ + private final Component loggers; + + /** Standard component: "Properties". */ + private final Component properties; + + /** Standard component: "Scripts". */ + private final Component scripts; + + /** The configuration source passed to the constructor of the built configuration instance. */ private ConfigurationSource source; - private int monitorInterval; - private Level level; - private String destination; - private String packages; - private String shutdownFlag; - private long shutdownTimeoutMillis; - private String advertiser; - private LoggerContext loggerContext; - private String name; - @SuppressFBWarnings( - value = {"XXE_DTD_TRANSFORM_FACTORY", "XXE_XSLT_TRANSFORM_FACTORY"}, - justification = "This method only uses internally generated data.") - public static void formatXml(final Source source, final Result result) - throws TransformerConfigurationException, TransformerFactoryConfigurationError, TransformerException { - final Transformer transformer = TransformerFactory.newInstance().newTransformer(); - transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", Integer.toString(INDENT.length())); - transformer.setOutputProperty(OutputKeys.INDENT, "yes"); - transformer.transform(source, result); - } + /** The logger context passed to the constructor of the built configuration instance. */ + private LoggerContext loggerContext; + /** + * Constructs a new instance with a standard {@link BuiltConfiguration} target type. + */ @SuppressWarnings("unchecked") public DefaultConfigurationBuilder() { this((Class) BuiltConfiguration.class); root.addAttribute("name", "Built"); } + /** + * Constructs a new instance with the given {@link BuiltConfiguration} implementation class. + * @param clazz the {@code Class} of the {@code BuiltConfiguration} implementation to build + * @throws NullPointerException if the argument is null + */ public DefaultConfigurationBuilder(final Class clazz) { - if (clazz == null) { - throw new IllegalArgumentException("A Configuration class must be provided"); - } - this.clazz = clazz; - final List components = root.getComponents(); - properties = new Component("Properties"); - components.add(properties); - scripts = new Component("Scripts"); - components.add(scripts); - customLevels = new Component("CustomLevels"); - components.add(customLevels); - filters = new Component("Filters"); - components.add(filters); - appenders = new Component("Appenders"); - components.add(appenders); - loggers = new Component("Loggers"); - components.add(loggers); + + super(); + + this.configurationClass = Objects.requireNonNull(clazz, "The 'configurationClass' argument must not be null."); + this.root.addAttribute("name", "Built"); + + properties = getOrCreateComponent("Properties"); + scripts = getOrCreateComponent("Scripts"); + customLevels = getOrCreateComponent("CustomLevels"); + filters = getOrCreateComponent("Filters"); + appenders = getOrCreateComponent("Appenders"); + loggers = getOrCreateComponent("Loggers"); } + // + // ACCESSORS / MUTATORS + // + + /** + * Adds the component built by the provided builder as a child component of the given parent component. + * + * @param parent the parent component to add to + * @param builder the builder to generate the child component from + * @throws ConfigurationException if an error occurs while building the given builder + * @throws NullPointerException if either argument is null + */ protected ConfigurationBuilder add(final Component parent, final ComponentBuilder builder) { + Objects.requireNonNull(parent, "The 'parent' component must not be null."); + Objects.requireNonNull(builder, "The 'builder' component must not be null."); parent.getComponents().add(builder.build()); return this; } + /** {@inheritDoc} */ @Override public ConfigurationBuilder add(final AppenderComponentBuilder builder) { - return add(appenders, builder); + add(this.appenders, builder); + return this; } + /** {@inheritDoc} */ @Override public ConfigurationBuilder add(final CustomLevelComponentBuilder builder) { - return add(customLevels, builder); + add(this.customLevels, builder); + return this; } + /** {@inheritDoc} */ @Override public ConfigurationBuilder add(final FilterComponentBuilder builder) { - return add(filters, builder); + add(this.filters, builder); + return this; } + /** {@inheritDoc} */ @Override public ConfigurationBuilder add(final ScriptComponentBuilder builder) { - return add(scripts, builder); + add(this.scripts, builder); + return this; } + /** {@inheritDoc} */ @Override public ConfigurationBuilder add(final ScriptFileComponentBuilder builder) { - return add(scripts, builder); + add(this.scripts, builder); + return this; } + /** {@inheritDoc} */ @Override public ConfigurationBuilder add(final LoggerComponentBuilder builder) { - return add(loggers, builder); + add(this.loggers, builder); + return this; } + /** {@inheritDoc} */ @Override - public ConfigurationBuilder add(final RootLoggerComponentBuilder builder) { + public ConfigurationBuilder add(RootLoggerComponentBuilder builder) { for (final Component c : loggers.getComponents()) { if (c.getPluginType().equals(LoggerConfig.ROOT)) { throw new ConfigurationException("Root Logger was previously defined"); } } - return add(loggers, builder); + add(this.loggers, builder); + return this; } + /** {@inheritDoc} */ @Override public ConfigurationBuilder addProperty(final String key, final String value) { - properties.addComponent(newComponent(key, "Property", value).build()); + add(this.properties, newProperty(key, value)); return this; } + /** {@inheritDoc} */ @Override - public T build() { - return build(true); + public ConfigurationBuilder addProperty(final PropertyComponentBuilder builder) { + add(this.properties, builder); + return this; } + /** {@inheritDoc} */ @Override - public T build(final boolean initialize) { - T configuration; + public ConfigurationBuilder addRootProperty(String key, String value) { + Objects.requireNonNull(key, "The 'key' argument must not be null."); + if (key.isEmpty()) { + throw new IllegalArgumentException("The 'key' argument must not be empty."); + } + if (value != null) { + this.getRootComponent().getAttributes().put(key, value); + } else { + this.getRootComponent().getAttributes().remove(key); + } + return this; + } + + /** + * Gets the value of a property (attribute) on the root component. + * @param key the property key + * @return an optional containing the property value or that is empty if not set + */ + protected Optional getRootProperty(String key) { + Objects.requireNonNull(key, "The 'key' argument must not be null."); + if (key.isEmpty()) { + throw new IllegalArgumentException("The 'key' argument must not be empty."); + } + return Optional.ofNullable(this.getRootComponent().getAttributes().get(key)); + } + + /** + * Gets the child component with the given plugin-type. + * @param pluginType the plugin-type to lookup + * @return an optional containing the resolved component or that is empty if not found + */ + protected Optional getComponent(final String pluginType) { + Objects.requireNonNull(pluginType, "The 'pluginType' argument must not be null."); + return this.root.getComponents().stream() + .filter(c -> c.getPluginType().equals(pluginType)) + .findFirst(); + } + + /** + * Gets the root component. + * + * @return the root component (will never be null) + */ + protected Component getRootComponent() { + return this.root; + } + + /** + * Gets or creates the child component with the given plugin-type. + *

+ * The lookup is case-sensitive. + *

+ * + * @param pluginType the plugin-type to lookup + * @return either the existing component or a new component with the given plugin-type if not found (never null) + */ + protected Component getOrCreateComponent(String pluginType) { + Objects.requireNonNull(pluginType, "The 'pluginType' must not be null."); + return getComponent(pluginType).orElseGet(() -> { + Component c = new Component(pluginType); + this.root.getComponents().add(c); + return c; + }); + } + + /** + * Gets the value of the "advertiser" property. + * @return an optional containing the property value or that is empty if undefined + */ + protected Optional getAdvertiser() { + return getRootProperty("advertiser"); + } + + /** + * Gets the value of the "name" property. + * @return an optional containing the property value or that is empty if undefined + */ + protected Optional getConfigurationName() { + return getRootProperty("name"); + } + + /** + * Gets the configuration-source. + * @return an optional containing the configuration-source or that is empy if undefined + */ + protected Optional getConfigurationSource() { + return Optional.ofNullable(this.source); + } + + /** + * Gets the value of the "dest" property. + * @return an optional containing the property value or that is empty if undefined + */ + protected Optional getDestination() { + return getRootProperty("dest"); + } + + /** + * Gets the logger-context. + * + * @return an optional containing the logger-context or that is empty if undefined + */ + protected Optional getLoggerContext() { + return Optional.ofNullable(this.loggerContext); + } + + /** + * Returns the configured monitor interval (in seconds) as an {@code Integer}. + * @return an optional containing the configured monitor interval or that is empty if undefined (or invalid) + */ + protected Optional getMonitorInterval() { try { - if (source == null) { - source = ConfigurationSource.NULL_SOURCE; - } - final Constructor constructor = - clazz.getConstructor(LoggerContext.class, ConfigurationSource.class, Component.class); - configuration = constructor.newInstance(loggerContext, source, root); - configuration.getRootNode().getAttributes().putAll(root.getAttributes()); - if (name != null) { - configuration.setName(name); - } - if (level != null) { - configuration.getStatusConfiguration().withStatus(level); - } - if (destination != null) { - configuration.getStatusConfiguration().withDestination(destination); - } - if (packages != null) { - configuration.setPluginPackages(packages); - } - if (shutdownFlag != null) { - configuration.setShutdownHook(shutdownFlag); - } - if (shutdownTimeoutMillis > 0) { - configuration.setShutdownTimeoutMillis(shutdownTimeoutMillis); - } - if (advertiser != null) { - configuration.createAdvertiser(advertiser, source); - } - configuration.setMonitorInterval(monitorInterval); + return getRootProperty("monitorInterval").map(Integer::parseInt); } catch (final Exception ex) { - throw new IllegalArgumentException("Invalid Configuration class specified", ex); + return Optional.empty(); } - configuration.getStatusConfiguration().initialize(); - if (initialize) { - configuration.initialize(); + } + + /** + * Gets the value of the "packages" property. + * @return an optional containing the property value or that is empty if undefined + */ + protected Optional getPackages() { + return getRootProperty("packages"); + } + + /** + * Gets the value of the "shutdownHook" property. + * @return an optional containing the property value or that is empty if undefined + */ + protected Optional getShutdownHook() { + return getRootProperty("shutdownHook"); + } + + /** + * Gets the value of the "shutdownTimeout" property as a {@code Long}. + * @return an optional containing the property value or that is empty if undefined + */ + protected Optional getShutdownTimeout() { + try { + return getRootProperty("shutdownTimeout").map(Long::parseLong); + } catch (final Exception ex) { + return Optional.empty(); } - return configuration; } - private String formatXml(final String xml) - throws TransformerConfigurationException, TransformerException, TransformerFactoryConfigurationError { - final StringWriter writer = new StringWriter(); - formatXml(new StreamSource(new StringReader(xml)), new StreamResult(writer)); - return writer.toString(); + /** + * Gets the value of the "status" property as a {@code Level}. + * @return an optional containing the property value or that is empty if undefined + */ + protected Optional getStatusLevel() { + return getRootProperty("status").map(Level::getLevel); } + /** + * Gets the "Appenders" component. + * + * @return the appenders component + */ + protected final Component getAppenders() { + return this.appenders; + } + + /** + * Gets the "CustomLevels" component. + * + * @return the custom-levels component + */ + protected final Component getCustomLevels() { + return this.customLevels; + } + + /** + * Gets the "Filters" component. + * + * @return the filters component + */ + protected final Component getFilters() { + return this.filters; + } + + /** + * Gets the "Loggers" component. + * + * @return the loggers component + */ + protected final Component getLoggers() { + return this.loggers; + } + + /** + * Gets the "Properties" component. + * + * @return the properties component + */ + protected final Component getProperties() { + return this.properties; + } + + /** + * Gets the "Scripts" component. + * + * @return the scripts component + */ + protected final Component getScripts() { + return this.scripts; + } + + /** {@inheritDoc} */ @Override - public void writeXmlConfiguration(final OutputStream output) throws IOException { - try { - final XMLStreamWriter xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(output); - writeXmlConfiguration(xmlWriter); - xmlWriter.close(); - } catch (final XMLStreamException e) { - if (e.getNestedException() instanceof IOException) { - throw (IOException) e.getNestedException(); + public ConfigurationBuilder setAdvertiser(final String advertiser) { + this.addRootProperty("advertiser", advertiser); + return this; + } + + /** {@inheritDoc} */ + @Override + public ConfigurationBuilder setConfigurationName(final String name) { + this.addRootProperty("name", name); + return this; + } + + /** {@inheritDoc} */ + @Override + public ConfigurationBuilder setConfigurationSource(final ConfigurationSource configurationSource) { + source = configurationSource; + return this; + } + + /** {@inheritDoc} */ + @Override + public ConfigurationBuilder setDestination(final String destination) { + this.addRootProperty("dest", (destination == null) ? null : destination.trim()); + return this; + } + + /** {@inheritDoc} */ + @Override + public ConfigurationBuilder setMonitorInterval(final String intervalSeconds) { + int iMonitorInterval = 0; + if (intervalSeconds != null && !intervalSeconds.isEmpty() && !"0".equals(intervalSeconds.trim())) { + try { + iMonitorInterval = Integers.parseInt(intervalSeconds.trim()); + } catch (final Exception ex) { + throw new IllegalArgumentException("Invalid monitor interval: " + intervalSeconds, ex); } - Throwables.rethrow(e); } + return this.setMonitorInterval(iMonitorInterval); } + /** {@inheritDoc} */ @Override - public String toXmlConfiguration() { - final StringWriter writer = new StringWriter(); - try { - final XMLStreamWriter xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(writer); - writeXmlConfiguration(xmlWriter); - xmlWriter.close(); - return formatXml(writer.toString()); - } catch (final XMLStreamException | TransformerException e) { - Throwables.rethrow(e); - } - return writer.toString(); + public ConfigurationBuilder setMonitorInterval(final int intervalSeconds) { + addRootProperty("monitorInterval", intervalSeconds <= 0 ? null : String.valueOf(intervalSeconds)); + return this; } - private void writeXmlConfiguration(final XMLStreamWriter xmlWriter) throws XMLStreamException { - xmlWriter.writeStartDocument(); - xmlWriter.writeStartElement("Configuration"); - if (name != null) { - xmlWriter.writeAttribute("name", name); - } - if (level != null) { - xmlWriter.writeAttribute("status", level.name()); - } - if (destination != null) { - xmlWriter.writeAttribute("dest", destination); - } - if (packages != null) { - xmlWriter.writeAttribute("packages", packages); - } - if (shutdownFlag != null) { - xmlWriter.writeAttribute("shutdownHook", shutdownFlag); - } - if (shutdownTimeoutMillis > 0) { - xmlWriter.writeAttribute("shutdownTimeout", String.valueOf(shutdownTimeoutMillis)); - } - if (advertiser != null) { - xmlWriter.writeAttribute("advertiser", advertiser); - } - if (monitorInterval > 0) { - xmlWriter.writeAttribute("monitorInterval", String.valueOf(monitorInterval)); - } + /** {@inheritDoc} */ + @Override + public ConfigurationBuilder setPackages(final String packages) { + addRootProperty("packages", packages); + return this; + } - writeXmlSection(xmlWriter, properties); - writeXmlSection(xmlWriter, scripts); - writeXmlSection(xmlWriter, customLevels); - if (filters.getComponents().size() == 1) { - writeXmlComponent(xmlWriter, filters.getComponents().get(0)); - } else if (filters.getComponents().size() > 1) { - writeXmlSection(xmlWriter, filters); + /** {@inheritDoc} */ + @Override + public ConfigurationBuilder setShutdownHook(final String flag) { + addRootProperty("shutdownHook", flag); + return this; + } + + /** {@inheritDoc} */ + @Override + public ConfigurationBuilder setShutdownTimeout(final String timeout, final TimeUnit timeUnit) { + Objects.requireNonNull(timeUnit, "The 'timeUnit' argument must not be null."); + long shutdownTimeoutMillis = 0L; + if (timeout != null && !timeout.isEmpty()) { + try { + shutdownTimeoutMillis = timeUnit.toMillis(Long.parseLong(timeout)); + } catch (final Exception ex) { + throw new IllegalArgumentException("Invalid shutdown timeout: " + timeout, ex); + } } - writeXmlSection(xmlWriter, appenders); - writeXmlSection(xmlWriter, loggers); + return this.setShutdownTimeout(shutdownTimeoutMillis, timeUnit); + } - xmlWriter.writeEndElement(); // "Configuration" - xmlWriter.writeEndDocument(); + /** {@inheritDoc} */ + @Override + public ConfigurationBuilder setShutdownTimeout(final long timeout, final TimeUnit timeUnit) { + Objects.requireNonNull(timeUnit, "The 'timeUnit' argument must not be null."); + return this.setShutdownTimeout(timeUnit.toMillis(timeout)); } - private void writeXmlSection(final XMLStreamWriter xmlWriter, final Component component) throws XMLStreamException { - if (!component.getAttributes().isEmpty() - || !component.getComponents().isEmpty() - || component.getValue() != null) { - writeXmlComponent(xmlWriter, component); - } + /** {@inheritDoc} */ + @Override + public ConfigurationBuilder setShutdownTimeout(final long timeoutMillis) { + this.addRootProperty("shutdownTimeout", timeoutMillis <= 0L ? null : String.valueOf(timeoutMillis)); + return this; } - private void writeXmlComponent(final XMLStreamWriter xmlWriter, final Component component) - throws XMLStreamException { - if (!component.getComponents().isEmpty() || component.getValue() != null) { - xmlWriter.writeStartElement(component.getPluginType()); - writeXmlAttributes(xmlWriter, component); - for (final Component subComponent : component.getComponents()) { - writeXmlComponent(xmlWriter, subComponent); - } - if (component.getValue() != null) { - xmlWriter.writeCharacters(component.getValue()); - } - xmlWriter.writeEndElement(); - } else { - xmlWriter.writeEmptyElement(component.getPluginType()); - writeXmlAttributes(xmlWriter, component); + /** {@inheritDoc} */ + @Override + public ConfigurationBuilder setStatusLevel(final Level level) { + this.addRootProperty("status", (level != null) ? level.toString() : null); + return this; + } + + /** {@inheritDoc} */ + @Override + public ConfigurationBuilder setStatusLevel(final String level) { + return this.setStatusLevel((level != null) ? Level.getLevel(level.trim()) : null); + } + + /** + * {@inheritDoc} + * @deprecated This method is ineffective and only kept for binary backward compatibility. + */ + @Override + @Deprecated + public ConfigurationBuilder setVerbosity(final String verbosity) { + return this; + } + + /** {@inheritDoc} */ + @Override + public void setLoggerContext(final LoggerContext loggerContext) { + this.loggerContext = loggerContext; + } + + // + // BUILD + // + + /** {@inheritDoc} */ + @Override + public T build() { + return build(true); + } + + /** {@inheritDoc} */ + @Override + public T build(final boolean initialize) { + if (source == null) { + source = ConfigurationSource.NULL_SOURCE; } + T configuration = createNewConfigurationInstance(this.configurationClass); + if (initialize) { + configuration.initialize(); + } + return configuration; } - private void writeXmlAttributes(final XMLStreamWriter xmlWriter, final Component component) - throws XMLStreamException { - for (final Map.Entry attribute : - component.getAttributes().entrySet()) { - xmlWriter.writeAttribute(attribute.getKey(), attribute.getValue()); + /** + * Instantiate a new instance of the {@code Configuration} implementation. + *

+ * Subclasses may override this if they need to provide some non-standard behaviour. + *

+ * + * @return the new configuration instance + * @throws IllegalStateException if the configuration cannot be instantiated + */ + protected T createNewConfigurationInstance(Class configurationClass) { + Objects.requireNonNull(configurationClass, "The 'configurationClass' argument must not be null."); + try { + final Constructor constructor = + configurationClass.getConstructor(LoggerContext.class, ConfigurationSource.class, Component.class); + return constructor.newInstance(loggerContext, source, root); + } catch (final Exception ex) { + throw new IllegalStateException( + "Configuration class '" + configurationClass.getName() + "' cannot be instantiated.", ex); } } + // + // BUILDER FACTORY METHODS + // + + /** {@inheritDoc} */ @Override public ScriptComponentBuilder newScript(final String name, final String language, final String text) { return new DefaultScriptComponentBuilder(this, name, language, text); } + /** {@inheritDoc} */ @Override public ScriptFileComponentBuilder newScriptFile(final String path) { return new DefaultScriptFileComponentBuilder(this, path, path); } + /** {@inheritDoc} */ @Override public ScriptFileComponentBuilder newScriptFile(final String name, final String path) { return new DefaultScriptFileComponentBuilder(this, name, path); } + /** {@inheritDoc} */ @Override public AppenderComponentBuilder newAppender(final String name, final String type) { return new DefaultAppenderComponentBuilder(this, name, type); } + /** {@inheritDoc} */ @Override public AppenderRefComponentBuilder newAppenderRef(final String ref) { return new DefaultAppenderRefComponentBuilder(this, ref); } + /** {@inheritDoc} */ @Override public LoggerComponentBuilder newAsyncLogger(final String name) { return new DefaultLoggerComponentBuilder(this, name, null, "AsyncLogger"); } + /** {@inheritDoc} */ @Override public LoggerComponentBuilder newAsyncLogger(final String name, final boolean includeLocation) { return new DefaultLoggerComponentBuilder(this, name, null, "AsyncLogger", includeLocation); } + /** {@inheritDoc} */ @Override public LoggerComponentBuilder newAsyncLogger(final String name, final Level level) { return new DefaultLoggerComponentBuilder(this, name, level.toString(), "AsyncLogger"); } + /** {@inheritDoc} */ @Override public LoggerComponentBuilder newAsyncLogger(final String name, final Level level, final boolean includeLocation) { return new DefaultLoggerComponentBuilder(this, name, level.toString(), "AsyncLogger", includeLocation); } + /** {@inheritDoc} */ @Override public LoggerComponentBuilder newAsyncLogger(final String name, final String level) { return new DefaultLoggerComponentBuilder(this, name, level, "AsyncLogger"); } + /** {@inheritDoc} */ @Override public LoggerComponentBuilder newAsyncLogger(final String name, final String level, final boolean includeLocation) { return new DefaultLoggerComponentBuilder(this, name, level, "AsyncLogger", includeLocation); } + /** {@inheritDoc} */ @Override public RootLoggerComponentBuilder newAsyncRootLogger() { return new DefaultRootLoggerComponentBuilder(this, "AsyncRoot"); } + /** {@inheritDoc} */ @Override public RootLoggerComponentBuilder newAsyncRootLogger(final boolean includeLocation) { return new DefaultRootLoggerComponentBuilder(this, null, "AsyncRoot", includeLocation); } + /** {@inheritDoc} */ @Override public RootLoggerComponentBuilder newAsyncRootLogger(final Level level) { return new DefaultRootLoggerComponentBuilder(this, level.toString(), "AsyncRoot"); } + /** {@inheritDoc} */ @Override public RootLoggerComponentBuilder newAsyncRootLogger(final Level level, final boolean includeLocation) { return new DefaultRootLoggerComponentBuilder(this, level.toString(), "AsyncRoot", includeLocation); } + /** {@inheritDoc} */ @Override public RootLoggerComponentBuilder newAsyncRootLogger(final String level) { return new DefaultRootLoggerComponentBuilder(this, level, "AsyncRoot"); } + /** {@inheritDoc} */ @Override public RootLoggerComponentBuilder newAsyncRootLogger(final String level, final boolean includeLocation) { return new DefaultRootLoggerComponentBuilder(this, level, "AsyncRoot", includeLocation); } + /** {@inheritDoc} */ @Override public > ComponentBuilder newComponent(final String type) { return new DefaultComponentBuilder<>(this, type); } + /** {@inheritDoc} */ @Override public > ComponentBuilder newComponent(final String name, final String type) { return new DefaultComponentBuilder<>(this, name, type); } + /** {@inheritDoc} */ @Override public > ComponentBuilder newComponent( final String name, final String type, final String value) { return new DefaultComponentBuilder<>(this, name, type, value); } + /** {@inheritDoc} */ @Override public PropertyComponentBuilder newProperty(final String name, final String value) { return new DefaultPropertyComponentBuilder(this, name, value); } + /** {@inheritDoc} */ @Override public KeyValuePairComponentBuilder newKeyValuePair(final String key, final String value) { return new DefaultKeyValuePairComponentBuilder(this, key, value); } + /** {@inheritDoc} */ @Override public CustomLevelComponentBuilder newCustomLevel(final String name, final int level) { return new DefaultCustomLevelComponentBuilder(this, name, level); } + /** {@inheritDoc} */ @Override public FilterComponentBuilder newFilter( final String type, final Filter.Result onMatch, final Filter.Result onMismatch) { return new DefaultFilterComponentBuilder(this, type, onMatch.name(), onMismatch.name()); } + /** {@inheritDoc} */ @Override public FilterComponentBuilder newFilter(final String type, final String onMatch, final String onMismatch) { return new DefaultFilterComponentBuilder(this, type, onMatch, onMismatch); } + /** {@inheritDoc} */ @Override public LayoutComponentBuilder newLayout(final String type) { return new DefaultLayoutComponentBuilder(this, type); } + /** {@inheritDoc} */ @Override public LoggerComponentBuilder newLogger(final String name) { return new DefaultLoggerComponentBuilder(this, name, null); } + /** {@inheritDoc} */ @Override public LoggerComponentBuilder newLogger(final String name, final boolean includeLocation) { return new DefaultLoggerComponentBuilder(this, name, null, includeLocation); } + /** {@inheritDoc} */ @Override public LoggerComponentBuilder newLogger(final String name, final Level level) { return new DefaultLoggerComponentBuilder(this, name, level.toString()); } + /** {@inheritDoc} */ @Override public LoggerComponentBuilder newLogger(final String name, final Level level, final boolean includeLocation) { return new DefaultLoggerComponentBuilder(this, name, level.toString(), includeLocation); } + /** {@inheritDoc} */ @Override public LoggerComponentBuilder newLogger(final String name, final String level) { return new DefaultLoggerComponentBuilder(this, name, level); } + /** {@inheritDoc} */ @Override public LoggerComponentBuilder newLogger(final String name, final String level, final boolean includeLocation) { return new DefaultLoggerComponentBuilder(this, name, level, includeLocation); } + /** {@inheritDoc} */ @Override public RootLoggerComponentBuilder newRootLogger() { return new DefaultRootLoggerComponentBuilder(this, null); } + /** {@inheritDoc} */ @Override public RootLoggerComponentBuilder newRootLogger(final boolean includeLocation) { return new DefaultRootLoggerComponentBuilder(this, null, includeLocation); } + /** {@inheritDoc} */ @Override public RootLoggerComponentBuilder newRootLogger(final Level level) { return new DefaultRootLoggerComponentBuilder(this, level.toString()); } + /** {@inheritDoc} */ @Override public RootLoggerComponentBuilder newRootLogger(final Level level, final boolean includeLocation) { return new DefaultRootLoggerComponentBuilder(this, level.toString(), includeLocation); } + /** {@inheritDoc} */ @Override public RootLoggerComponentBuilder newRootLogger(final String level) { return new DefaultRootLoggerComponentBuilder(this, level); } + /** {@inheritDoc} */ @Override public RootLoggerComponentBuilder newRootLogger(final String level, final boolean includeLocation) { return new DefaultRootLoggerComponentBuilder(this, level, includeLocation); } - @Override - public ConfigurationBuilder setAdvertiser(final String advertiser) { - this.advertiser = advertiser; - return this; - } + // + // XML SERIALIZATION + // - /** - * Set the name of the configuration. - * - * @param name the name of the {@link Configuration}. By default is {@code "Assembled"}. - * @return this builder instance - */ - @Override - public ConfigurationBuilder setConfigurationName(final String name) { - this.name = name; - return this; + private String formatXml(final String xml) throws TransformerException, TransformerFactoryConfigurationError { + final StringWriter writer = new StringWriter(); + formatXml(new StreamSource(new StringReader(xml)), new StreamResult(writer)); + return writer.toString(); } - /** - * Set the ConfigurationSource. - * - * @param configurationSource the {@link ConfigurationSource} - * @return this builder instance - */ - @Override - public ConfigurationBuilder setConfigurationSource(final ConfigurationSource configurationSource) { - source = configurationSource; - return this; + @SuppressFBWarnings( + value = {"XXE_DTD_TRANSFORM_FACTORY", "XXE_XSLT_TRANSFORM_FACTORY"}, + justification = "This method only uses internally generated data.") + public static void formatXml(final Source source, final Result result) + throws TransformerFactoryConfigurationError, TransformerException { + final Transformer transformer = TransformerFactory.newInstance().newTransformer(); + transformer.setOutputProperty( + "{http://xml.apache.org/xslt}indent-amount", Integer.toString(XML_INDENT.length())); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.transform(source, result); } + /** {@inheritDoc} */ @Override - public ConfigurationBuilder setMonitorInterval(final String intervalSeconds) { - monitorInterval = Integers.parseInt(intervalSeconds); - return this; + public void writeXmlConfiguration(final OutputStream output) throws IOException { + try { + final XMLStreamWriter xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(output); + writeXmlConfiguration(xmlWriter); + xmlWriter.close(); + } catch (final XMLStreamException e) { + if (e.getNestedException() instanceof IOException) { + throw (IOException) e.getNestedException(); + } + Throwables.rethrow(e); + } } + /** {@inheritDoc} */ @Override - public ConfigurationBuilder setPackages(final String packages) { - this.packages = packages; - return this; + public String toXmlConfiguration() { + final StringWriter writer = new StringWriter(); + try { + final XMLStreamWriter xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(writer); + writeXmlConfiguration(xmlWriter); + xmlWriter.close(); + return formatXml(writer.toString()); + } catch (final XMLStreamException | TransformerException e) { + Throwables.rethrow(e); + } + return writer.toString(); } - @Override - public ConfigurationBuilder setShutdownHook(final String flag) { - this.shutdownFlag = flag; - return this; + private void writeXmlConfiguration(final XMLStreamWriter xmlWriter) throws XMLStreamException { + xmlWriter.writeStartDocument(); + xmlWriter.writeStartElement("Configuration"); + writeXmlAttributes(xmlWriter); + writeXmlSections(xmlWriter); + xmlWriter.writeEndElement(); // "Configuration" + xmlWriter.writeEndDocument(); } - @Override - public ConfigurationBuilder setShutdownTimeout(final long timeout, final TimeUnit timeUnit) { - this.shutdownTimeoutMillis = timeUnit.toMillis(timeout); - return this; + /** + * Writes the top-level attributes of the main "Configuration" root element. + * + * @param xmlWriter the XML writer to write to + * @throws XMLStreamException if an error occurs while writing the XML document + */ + protected void writeXmlAttributes(final XMLStreamWriter xmlWriter) throws XMLStreamException { + writeXmlAttribute(xmlWriter, "name", this.getConfigurationName().orElse(null)); + writeXmlAttribute( + xmlWriter, "status", this.getStatusLevel().map(Level::toString).orElse(null)); + writeXmlAttribute(xmlWriter, "dest", this.getDestination().orElse(null)); + writeXmlAttribute(xmlWriter, "packages", this.getPackages().orElse(null)); + writeXmlAttribute(xmlWriter, "shutdownHook", this.getShutdownHook().orElse(null)); + writeXmlAttribute( + xmlWriter, + "shutdownTimeout", + this.getShutdownTimeout().map(String::valueOf).orElse(null)); + writeXmlAttribute(xmlWriter, "advertiser", this.getAdvertiser().orElse(null)); + writeXmlAttribute( + xmlWriter, + "monitorInterval", + this.getMonitorInterval().map(String::valueOf).orElse(null)); } - @Override - public ConfigurationBuilder setStatusLevel(final Level level) { - this.level = level; - return this; + /** + * Writes the sections (sub-elements) of the main "Configuration" root element. + * @param xmlWriter the XML writer to write to + * @throws XMLStreamException if an error occurs while writing the XML document + */ + protected void writeXmlSections(final XMLStreamWriter xmlWriter) throws XMLStreamException { + writeXmlSection(xmlWriter, properties); + writeXmlSection(xmlWriter, scripts); + writeXmlSection(xmlWriter, customLevels); + + if (filters.getComponents().size() == 1) { + writeXmlComponent(xmlWriter, filters.getComponents().get(0)); + } else if (filters.getComponents().size() > 1) { + writeXmlSection(xmlWriter, filters); + } + + writeXmlSection(xmlWriter, appenders); + writeXmlSection(xmlWriter, loggers); } /** - * @deprecated This method is ineffective and only kept for binary backward compatibility. + * Writes an attribute to the given writer. + *

+ * If the provided value is {@code null} no attribute will be written. + *

+ * @param xmlWriter the writer to write to + * @param name the attribute name + * @param value the attribute value (if {@code null} no action will be taken + * @throws NullPointerException if either the {@code xmlWriter} or {@code name} attributes are {@code null} + * @throws IllegalArgumentException if the {@code name} attribute is blank + * @throws XMLStreamException if an error occurs while writing the attribute */ - @Override - @Deprecated - public ConfigurationBuilder setVerbosity(final String verbosity) { - return this; + private void writeXmlAttribute(XMLStreamWriter xmlWriter, String name, String value) throws XMLStreamException { + Objects.requireNonNull(xmlWriter, "The 'xmlWriter' argument must not be null."); + Objects.requireNonNull(name, "The 'name' argument must not be null."); + if (name.trim().isEmpty()) { + throw new IllegalArgumentException("The 'name' argument must not be blank."); + } + if (value != null) { + xmlWriter.writeAttribute(name, value); + } } - @Override - public ConfigurationBuilder setDestination(final String destination) { - this.destination = destination; - return this; + private void writeXmlSection(final XMLStreamWriter xmlWriter, final Component component) throws XMLStreamException { + if (!component.getAttributes().isEmpty() + || !component.getComponents().isEmpty() + || component.getValue() != null) { + writeXmlComponent(xmlWriter, component); + } } - @Override - public void setLoggerContext(final LoggerContext loggerContext) { - this.loggerContext = loggerContext; + private void writeXmlComponent(final XMLStreamWriter xmlWriter, final Component component) + throws XMLStreamException { + if (!component.getComponents().isEmpty() || component.getValue() != null) { + xmlWriter.writeStartElement(component.getPluginType()); + writeXmlAttributes(xmlWriter, component); + for (final Component subComponent : component.getComponents()) { + writeXmlComponent(xmlWriter, subComponent); + } + if (component.getValue() != null) { + xmlWriter.writeCharacters(component.getValue()); + } + xmlWriter.writeEndElement(); + } else { + xmlWriter.writeEmptyElement(component.getPluginType()); + writeXmlAttributes(xmlWriter, component); + } } - @Override - public ConfigurationBuilder addRootProperty(final String key, final String value) { - root.getAttributes().put(key, value); - return this; + private void writeXmlAttributes(final XMLStreamWriter xmlWriter, final Component component) + throws XMLStreamException { + for (final Map.Entry attribute : + component.getAttributes().entrySet()) { + xmlWriter.writeAttribute(attribute.getKey(), attribute.getValue()); + } } } diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/impl/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/impl/package-info.java index c56f92230f3..32a69487d75 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/impl/package-info.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/impl/package-info.java @@ -20,7 +20,7 @@ * @since 2.4 */ @Export -@Version("2.20.2") +@Version("2.25.0") package org.apache.logging.log4j.core.config.builder.impl; import org.osgi.annotation.bundle.Export; diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/properties/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/properties/package-info.java index 1563a62e0ec..fae889a28ff 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/properties/package-info.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/properties/package-info.java @@ -18,7 +18,7 @@ * Configuration using Properties files. */ @Export -@Version("2.20.1") +@Version("2.25.0") package org.apache.logging.log4j.core.config.properties; import org.osgi.annotation.bundle.Export; diff --git a/src/changelog/2.25.0/3441_default_configuration_builder_extensibility.xml b/src/changelog/2.25.0/3441_default_configuration_builder_extensibility.xml new file mode 100644 index 00000000000..7757a5184f3 --- /dev/null +++ b/src/changelog/2.25.0/3441_default_configuration_builder_extensibility.xml @@ -0,0 +1,11 @@ + + + + + Makes DefaultConfigurationBuilder more extensible and updates BuiltConfiguration + to more closely match behavior of other Configuration implementations. + +