From abe16cbf4341676a47a24ce2c8e48d40b306fd7c Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Tue, 19 Nov 2024 18:23:51 +0100 Subject: [PATCH 1/5] Adds a Configuration Converter API Adds an API to perform automatic conversions between different logging configuration file formats and a basic implementation. The implementation supports the following configuration file formats: - Log4j Core 2 XML, - Log4j Core 2 JSON, - Log4j Core 2 YAML, - Log4j Core 2 Properties (read-only), - Log4j Core 3 Properties. The API is extensible through `ServiceLoader` and allows integrators to provide support for additional configuration file formats. Read-only support for the Log4j 1 Properties and Log4j 1 XML configuration formats will be provided in a separate PR. **Note**: Currently the API is only accessible from Java code. Its main purpose is to provide a "Migrate Log4j 1.x to Log4j Core 2.x", a "Migrate Logback to Log4j Core 2.x" and a "Migrate JUL to Log4j Core 2.x" [OpenRewrite recipe](https://docs.openrewrite.org/recipes/java/logging/log4j). A `picocli`-based `log4j-transform-cli` tool to access all the goodies in this repository, will be provided later. Closes apache/logging-log4j2#2080 --- log4j-codegen/pom.xml | 18 - log4j-converter-config/pom.xml | 110 +++++ .../config/ConfigurationConverter.java | 58 +++ .../ConfigurationConverterException.java | 33 ++ .../internal/ConfigurationNodeImpl.java | 130 ++++++ .../DefaultConfigurationConverter.java | 99 +++++ .../config/internal/package-info.java | 20 + .../AbstractJacksonConfigurationMapper.java | 187 +++++++++ .../internal/v2/JsonConfigurationMapper.java | 37 ++ .../v2/PropertiesV2ConfigurationParser.java | 377 ++++++++++++++++++ .../internal/v2/XmlConfigurationMapper.java | 281 +++++++++++++ .../internal/v2/YamlConfigurationMapper.java | 43 ++ .../config/internal/v2/package-info.java | 20 + .../v3/PropertiesV3ConfigurationMapper.java | 58 +++ .../config/internal/v3/package-info.java | 20 + .../converter/config/package-info.java | 42 ++ .../config/spi/ConfigurationMapper.java | 47 +++ .../config/spi/ConfigurationNode.java | 52 +++ .../config/spi/ConfigurationParser.java | 50 +++ .../config/spi/ConfigurationWriter.java | 50 +++ .../converter/config/spi/package-info.java | 33 ++ .../config/ConfigurationConverterTest.java | 101 +++++ .../AbstractConfigurationMapperTest.java | 250 ++++++++++++ .../v2/JsonConfigurationMapperTest.java | 53 +++ .../PropertiesV2ConfigurationParserTest.java | 37 ++ .../v2/XmlConfigurationMapperTest.java | 53 +++ .../v2/YamlConfigurationMapperTest.java | 53 +++ .../PropertiesV3ConfigurationMapperTest.java | 53 +++ .../src/test/resources/v2/log4j2.json | 103 +++++ .../src/test/resources/v2/log4j2.properties | 91 +++++ .../src/test/resources/v2/log4j2.xml | 70 ++++ .../src/test/resources/v2/log4j2.yaml | 81 ++++ .../src/test/resources/v3/log4j2.properties | 83 ++++ log4j-transform-parent/pom.xml | 28 ++ log4j-weaver/pom.xml | 12 - pom.xml | 7 + 36 files changed, 2810 insertions(+), 30 deletions(-) create mode 100644 log4j-converter-config/pom.xml create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/ConfigurationConverter.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/ConfigurationConverterException.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/ConfigurationNodeImpl.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/DefaultConfigurationConverter.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/package-info.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/AbstractJacksonConfigurationMapper.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/JsonConfigurationMapper.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/PropertiesV2ConfigurationParser.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/XmlConfigurationMapper.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/YamlConfigurationMapper.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/package-info.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v3/PropertiesV3ConfigurationMapper.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v3/package-info.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/package-info.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/ConfigurationMapper.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/ConfigurationNode.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/ConfigurationParser.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/ConfigurationWriter.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/package-info.java create mode 100644 log4j-converter-config/src/test/java/org/apache/logging/converter/config/ConfigurationConverterTest.java create mode 100644 log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/AbstractConfigurationMapperTest.java create mode 100644 log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v2/JsonConfigurationMapperTest.java create mode 100644 log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v2/PropertiesV2ConfigurationParserTest.java create mode 100644 log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v2/XmlConfigurationMapperTest.java create mode 100644 log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v2/YamlConfigurationMapperTest.java create mode 100644 log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v3/PropertiesV3ConfigurationMapperTest.java create mode 100644 log4j-converter-config/src/test/resources/v2/log4j2.json create mode 100644 log4j-converter-config/src/test/resources/v2/log4j2.properties create mode 100644 log4j-converter-config/src/test/resources/v2/log4j2.xml create mode 100644 log4j-converter-config/src/test/resources/v2/log4j2.yaml create mode 100644 log4j-converter-config/src/test/resources/v3/log4j2.properties diff --git a/log4j-codegen/pom.xml b/log4j-codegen/pom.xml index c53f6a7b..b63161a2 100644 --- a/log4j-codegen/pom.xml +++ b/log4j-codegen/pom.xml @@ -43,24 +43,6 @@ - - org.osgi - org.osgi.annotation.bundle - provided - - - - org.osgi - org.osgi.annotation.versioning - provided - - - - com.github.spotbugs - spotbugs-annotations - provided - - info.picocli diff --git a/log4j-converter-config/pom.xml b/log4j-converter-config/pom.xml new file mode 100644 index 00000000..fe2cf33d --- /dev/null +++ b/log4j-converter-config/pom.xml @@ -0,0 +1,110 @@ + + + + 4.0.0 + + org.apache.logging.log4j + log4j-transform-parent + ${revision} + ../log4j-transform-parent + + + log4j-converter-config + Apache Log4j Configuration converter + Converts various logging configuration formats to the Log4j Core 2.x format. + + + + false + + + 2.18.1 + 4.0.5 + + + + + + + com.fasterxml.jackson + jackson-bom + ${jackson.version} + pom + import + + + + + + + + + org.apache.logging.log4j + log4j-api + + + + org.jspecify + jspecify + + + + + com.fasterxml.jackson.core + jackson-databind + + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-properties + + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + + + org.glassfish.jaxb + txw2 + ${txw2.version} + + + + org.junit.jupiter + junit-jupiter-api + test + + + + org.junit.jupiter + junit-jupiter-params + test + + + + org.assertj + assertj-core + test + + + + diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/ConfigurationConverter.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/ConfigurationConverter.java new file mode 100644 index 00000000..15768d14 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/ConfigurationConverter.java @@ -0,0 +1,58 @@ +/* + * 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.converter.config; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Set; +import org.apache.logging.converter.config.internal.DefaultConfigurationConverter; + +/** + * Service class to convert between different logging configuration formats. + */ +public interface ConfigurationConverter { + + /** + * A default implementation of {@link ConfigurationConverter} that uses {@link java.util.ServiceLoader} to load additional formats. + * @see org.apache.logging.converter.config.spi.ConfigurationMapper + */ + static ConfigurationConverter newInstance() { + return new DefaultConfigurationConverter(); + } + + /** + * Converts a logging configuration file from one format to another. + * + * @param inputStream The input configuration file, never {@code null}. + * @param inputFormat The input format. Must be one of the formats returned by {@link #getSupportedInputFormats()}. + * @param outputStream The output configuration file, never {@code null}. + * @param outputFormat The output format. Must be one of the formats returned by {@link #getSupportedOutputFormats()}. + * @throws ConfigurationConverterException If any kind of error occurs during the conversion process. + */ + void convert(InputStream inputStream, String inputFormat, OutputStream outputStream, String outputFormat) + throws ConfigurationConverterException; + + /** + * Returns the list of supported input formats. + */ + Set getSupportedInputFormats(); + + /** + * Returns the list of supported output formats. + */ + Set getSupportedOutputFormats(); +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/ConfigurationConverterException.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/ConfigurationConverterException.java new file mode 100644 index 00000000..c43327da --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/ConfigurationConverterException.java @@ -0,0 +1,33 @@ +/* + * 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.converter.config; + +/** + * Exception thrown by {@link ConfigurationConverter}, when a problem occurs during the conversion. + */ +public class ConfigurationConverterException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public ConfigurationConverterException(final String message) { + super(message); + } + + public ConfigurationConverterException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/ConfigurationNodeImpl.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/ConfigurationNodeImpl.java new file mode 100644 index 00000000..07e01438 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/ConfigurationNodeImpl.java @@ -0,0 +1,130 @@ +/* + * 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.converter.config.internal; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.Supplier; +import org.apache.logging.converter.config.ConfigurationConverterException; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.jspecify.annotations.Nullable; + +public final class ConfigurationNodeImpl implements ConfigurationNode { + + private final String pluginName; + private final Map attributes = new TreeMap<>(); + private final List children = new ArrayList<>(); + + public static NodeBuilder newNodeBuilder() { + return new NodeBuilder(); + } + + private ConfigurationNodeImpl( + final String pluginName, + final Map attributes, + final Collection children) { + this.pluginName = pluginName; + this.attributes.putAll(attributes); + this.children.addAll(children); + } + + @Override + public String getPluginName() { + return pluginName; + } + + @Override + public Map getAttributes() { + return Collections.unmodifiableMap(attributes); + } + + @Override + public List getChildren() { + return Collections.unmodifiableList(children); + } + + private static void formatTo(ConfigurationNode node, StringBuilder builder, int indent) { + String indentation = getIndentation(indent); + builder.append(indentation).append("<").append(node.getPluginName()); + for (final Map.Entry entry : node.getAttributes().entrySet()) { + builder.append(" ") + .append(entry.getKey()) + .append("=\"") + .append(entry.getValue()) + .append("\""); + } + builder.append(">\n"); + for (ConfigurationNode child : node.getChildren()) { + formatTo(child, builder, indent + 1); + builder.append('\n'); + } + builder.append(indentation).append(""); + } + + private static String getIndentation(int indent) { + return String.join("", Collections.nCopies(indent, " ")); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + formatTo(this, builder, 0); + return builder.toString(); + } + + public static final class NodeBuilder implements Supplier { + + private @Nullable String pluginName; + private final Map attributes = new TreeMap<>(); + private final List children = new ArrayList<>(); + + private NodeBuilder() {} + + public NodeBuilder setPluginName(String pluginName) { + this.pluginName = pluginName; + return this; + } + + public NodeBuilder addAttribute(String key, @Nullable String value) { + if (value != null) { + attributes.put(key, value); + } + return this; + } + + public NodeBuilder addChild(ConfigurationNode child) { + children.add(child); + return this; + } + + public ConfigurationNode build() { + if (pluginName == null) { + throw new ConfigurationConverterException("No plugin name specified"); + } + return new ConfigurationNodeImpl(pluginName, attributes, children); + } + + @Override + public ConfigurationNode get() { + return build(); + } + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/DefaultConfigurationConverter.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/DefaultConfigurationConverter.java new file mode 100644 index 00000000..e4771ea2 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/DefaultConfigurationConverter.java @@ -0,0 +1,99 @@ +/* + * 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.converter.config.internal; + +import static java.util.Objects.requireNonNull; + +import aQute.bnd.annotation.Cardinality; +import aQute.bnd.annotation.Resolution; +import aQute.bnd.annotation.spi.ServiceConsumer; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.Set; +import org.apache.logging.converter.config.ConfigurationConverter; +import org.apache.logging.converter.config.ConfigurationConverterException; +import org.apache.logging.converter.config.spi.ConfigurationMapper; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.apache.logging.converter.config.spi.ConfigurationParser; +import org.apache.logging.converter.config.spi.ConfigurationWriter; + +@ServiceConsumer( + value = ConfigurationParser.class, + cardinality = Cardinality.MULTIPLE, + resolution = Resolution.OPTIONAL) +@ServiceConsumer( + value = ConfigurationWriter.class, + cardinality = Cardinality.MULTIPLE, + resolution = Resolution.OPTIONAL) +@ServiceConsumer( + value = ConfigurationMapper.class, + cardinality = Cardinality.MULTIPLE, + resolution = Resolution.OPTIONAL) +public class DefaultConfigurationConverter implements ConfigurationConverter { + + private final Map parsers = new HashMap<>(); + private final Map writers = new HashMap<>(); + + public DefaultConfigurationConverter() { + ServiceLoader.load(ConfigurationParser.class).forEach(parser -> parsers.put(parser.getInputFormat(), parser)); + ServiceLoader.load(ConfigurationWriter.class).forEach(writer -> writers.put(writer.getOutputFormat(), writer)); + ServiceLoader.load(ConfigurationMapper.class).forEach(mapper -> { + parsers.put(mapper.getInputFormat(), mapper); + writers.put(mapper.getOutputFormat(), mapper); + }); + } + + @Override + public void convert(InputStream inputStream, String inputFormat, OutputStream outputStream, String outputFormat) { + requireNonNull(inputStream, "inputStream"); + requireNonNull(inputFormat, "inputFormat"); + requireNonNull(outputStream, "outputStream"); + requireNonNull(outputFormat, "outputFormat"); + + ConfigurationParser parser = parsers.get(inputFormat); + if (parser == null) { + throw new ConfigurationConverterException("The input format `" + inputFormat + "` is not supported."); + } + ConfigurationWriter writer = writers.get(outputFormat); + if (writer == null) { + throw new ConfigurationConverterException("The output format `" + outputFormat + "` is not supported."); + } + + try { + ConfigurationNode configuration = parser.parse(inputStream); + writer.writeConfiguration(outputStream, configuration); + } catch (IOException e) { + throw new ConfigurationConverterException( + "Failed to convert configuration from format " + inputFormat + " to format " + outputFormat, e); + } + } + + @Override + public Set getSupportedInputFormats() { + return Collections.unmodifiableSet(parsers.keySet()); + } + + @Override + public Set getSupportedOutputFormats() { + return Collections.unmodifiableSet(writers.keySet()); + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/package-info.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/package-info.java new file mode 100644 index 00000000..b24aaa3a --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ +@NullMarked +package org.apache.logging.converter.config.internal; + +import org.jspecify.annotations.NullMarked; diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/AbstractJacksonConfigurationMapper.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/AbstractJacksonConfigurationMapper.java new file mode 100644 index 00000000..6540fd2e --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/AbstractJacksonConfigurationMapper.java @@ -0,0 +1,187 @@ +/* + * 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.converter.config.internal.v2; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import org.apache.logging.converter.config.internal.ConfigurationNodeImpl; +import org.apache.logging.converter.config.spi.ConfigurationMapper; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.jspecify.annotations.Nullable; + +public abstract class AbstractJacksonConfigurationMapper implements ConfigurationMapper { + + private static final String CONFIGURATION_FIELD_NAME = "Configuration"; + private static final String TYPE_ATTRIBUTE = "type"; + + private final ObjectMapper mapper; + private final boolean wrapRootObject; + + protected AbstractJacksonConfigurationMapper(ObjectMapper mapper, boolean wrapRootObject) { + this.mapper = mapper; + this.wrapRootObject = wrapRootObject; + } + + @Override + public ConfigurationNode parse(InputStream inputStream) throws IOException { + JsonNode documentNode = mapper.readTree(inputStream); + + final JsonNode configurationNode; + if (wrapRootObject) { + configurationNode = documentNode.get(CONFIGURATION_FIELD_NAME); + if (configurationNode == null || !configurationNode.isObject()) { + throw new IOException("Unable to find " + CONFIGURATION_FIELD_NAME + " field."); + } + } else { + configurationNode = documentNode; + } + return parseObjectNode((ObjectNode) configurationNode, CONFIGURATION_FIELD_NAME); + } + + @Override + public void writeConfiguration(OutputStream outputStream, ConfigurationNode configuration) throws IOException { + JsonNodeFactory nodeFactory = mapper.getNodeFactory(); + + ObjectNode configurationNode = convertToObjectNode(configuration, nodeFactory, false); + final ObjectNode documentNode = wrapRootObject + ? nodeFactory.objectNode().set(configuration.getPluginName(), configurationNode) + : configurationNode; + mapper.writeValue(outputStream, documentNode); + } + + private static ConfigurationNode parseObjectNode(ObjectNode objectNode, String fieldName) { + ConfigurationNodeImpl.NodeBuilder builder = ConfigurationNodeImpl.newNodeBuilder(); + objectNode.fields().forEachRemaining(entry -> { + String childFieldName = entry.getKey(); + JsonNode childNode = entry.getValue(); + if (childNode.isObject()) { + builder.addChild(parseObjectNode((ObjectNode) childNode, childFieldName)); + } + if (childNode.isArray()) { + processArrayNode((ArrayNode) childNode, childFieldName, builder); + } + if (childNode.isValueNode() && !TYPE_ATTRIBUTE.equals(childFieldName)) { + builder.addAttribute(childFieldName, childNode.asText()); + } + }); + return builder.setPluginName(getPluginName(objectNode, fieldName)).build(); + } + + private static void processArrayNode( + ArrayNode arrayNode, String fieldName, ConfigurationNodeImpl.NodeBuilder builder) { + arrayNode.elements().forEachRemaining(childNode -> { + if (childNode.isObject()) { + builder.addChild(parseObjectNode((ObjectNode) childNode, fieldName)); + } + }); + } + + private static String getPluginName(JsonNode jsonNode, String fieldName) { + JsonNode typeNode = jsonNode.get(TYPE_ATTRIBUTE); + String typeAttribute = typeNode != null ? typeNode.textValue() : null; + return typeAttribute != null ? typeAttribute : fieldName; + } + + /** + * Returns true, if the configuration node needs to be serialized with an {@code type} attribute. + * + * @param configurationNode A configuration node. + */ + protected boolean requiresExplicitTypeAttribute(ConfigurationNode configurationNode) { + return false; + } + + private ObjectNode convertToObjectNode( + ConfigurationNode configurationNode, JsonNodeFactory nodeFactory, boolean explicitTypeAttribute) { + ObjectNode objectNode = nodeFactory.objectNode(); + if (explicitTypeAttribute || requiresExplicitTypeAttribute(configurationNode)) { + objectNode.set(TYPE_ATTRIBUTE, nodeFactory.textNode(configurationNode.getPluginName())); + } + configurationNode.getAttributes().forEach(objectNode::put); + // If multiple nodes have the same name, we need to use JSON arrays or explicit type attributes. + // For a given plugin type, the strategy is: + // * For the first group of nodes with the given type we use an array. + // * If there is a second group of nodes with the same plugin name, we use an explicit type attribute. + // + // This strategy does not require us to reorder the children elements, which is not a functionally neutral + // operation + // (e.g., for filters). + Iterator childrenIterator = + configurationNode.getChildren().iterator(); + Collection visitedPluginNames = new HashSet<>(); + Collection similarChildren = new ArrayList<>(); + @Nullable String similarChildrenPluginName = null; + int counter = 1; + while (childrenIterator.hasNext()) { + ConfigurationNode child = childrenIterator.next(); + String childPluginName = child.getPluginName(); + // If the plugin name is the same, add to the current group + if (childPluginName.equals(similarChildrenPluginName)) { + similarChildren.add(child); + continue; + } + // We have a new group + if (!similarChildren.isEmpty()) { + // Flush the previous group + JsonNode childNode = convertToJsonNode(similarChildren, nodeFactory); + String childKey = childNode instanceof ObjectNode && childNode.get(TYPE_ATTRIBUTE) != null + ? "id" + counter++ + : similarChildrenPluginName; + objectNode.set(childKey, convertToJsonNode(similarChildren, nodeFactory)); + } + if (visitedPluginNames.add(childPluginName)) { + // 1. This is the first group with the given plugin name: + similarChildrenPluginName = childPluginName; + similarChildren.clear(); + similarChildren.add(child); + } else { + // 2. Use an explicit type attribute + objectNode.set("id" + counter++, convertToObjectNode(child, nodeFactory, true)); + } + } + // Flush the last group + if (!similarChildren.isEmpty()) { + JsonNode childNode = convertToJsonNode(similarChildren, nodeFactory); + String childKey = childNode instanceof ObjectNode && childNode.get(TYPE_ATTRIBUTE) != null + ? "id" + counter + : similarChildrenPluginName; + objectNode.set(childKey, convertToJsonNode(similarChildren, nodeFactory)); + } + return objectNode; + } + + private JsonNode convertToJsonNode(Collection node, JsonNodeFactory nodeFactory) { + if (node.size() == 1) { + return convertToObjectNode(node.iterator().next(), nodeFactory, false); + } + ArrayNode arrayNode = nodeFactory.arrayNode(); + for (ConfigurationNode child : node) { + arrayNode.add(convertToObjectNode(child, nodeFactory, false)); + } + return arrayNode; + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/JsonConfigurationMapper.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/JsonConfigurationMapper.java new file mode 100644 index 00000000..d97e9266 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/JsonConfigurationMapper.java @@ -0,0 +1,37 @@ +/* + * 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.converter.config.internal.v2; + +import aQute.bnd.annotation.Resolution; +import aQute.bnd.annotation.spi.ServiceProvider; +import com.fasterxml.jackson.databind.json.JsonMapper; +import org.apache.logging.converter.config.spi.ConfigurationMapper; + +@ServiceProvider(value = ConfigurationMapper.class, resolution = Resolution.MANDATORY) +public class JsonConfigurationMapper extends AbstractJacksonConfigurationMapper { + + private static final String LOG4J_V2_JSON_FORMAT = "v2:json"; + + public JsonConfigurationMapper() { + super(JsonMapper.builder().build(), true); + } + + @Override + public String getFormat() { + return LOG4J_V2_JSON_FORMAT; + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/PropertiesV2ConfigurationParser.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/PropertiesV2ConfigurationParser.java new file mode 100644 index 00000000..e6a22be1 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/PropertiesV2ConfigurationParser.java @@ -0,0 +1,377 @@ +/* + * 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.converter.config.internal.v2; + +import static org.apache.logging.log4j.util.PropertiesUtil.extractSubset; +import static org.apache.logging.log4j.util.PropertiesUtil.partitionOnCommonPrefixes; + +import aQute.bnd.annotation.Resolution; +import aQute.bnd.annotation.spi.ServiceProvider; +import java.io.IOException; +import java.io.InputStream; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Properties; +import java.util.TreeMap; +import java.util.function.Supplier; +import org.apache.logging.converter.config.ConfigurationConverterException; +import org.apache.logging.converter.config.internal.ConfigurationNodeImpl; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.apache.logging.converter.config.spi.ConfigurationParser; +import org.apache.logging.log4j.util.Strings; +import org.jspecify.annotations.Nullable; + +@ServiceProvider(value = ConfigurationParser.class, resolution = Resolution.MANDATORY) +public class PropertiesV2ConfigurationParser implements ConfigurationParser { + + private static final String LOG4J_V2_PROPERTIES_FORMAT = "v2:properties"; + + private static final String INT_LEVEL_ATTRIBUTE = "intLevel"; + private static final String LEVEL_AND_REFS_ATTRIBUTE = "levelAndRefs"; + private static final String NAME_ATTRIBUTE = "name"; + private static final String REF_ATTRIBUTE = "ref"; + private static final String TYPE_ATTRIBUTE = "type"; + private static final String VALUE_ATTRIBUTE = "value"; + + private static final String ROOT_LOGGER_NAME = "root"; + + private static final String APPENDERS_PLUGIN_NAME = "Appenders"; + private static final String CONFIGURATION_PLUGIN_NAME = "Configuration"; + private static final String CUSTOM_LEVEL_PLUGIN_NAME = "CustomLevel"; + private static final String CUSTOM_LEVELS_PLUGIN_NAME = "CustomLevels"; + private static final String APPENDER_REF_PLUGIN_NAME = "AppenderRef"; + private static final String ASYNC_LOGGER_PLUGIN_NAME = "AsyncLogger"; + private static final String ASYNC_ROOT_PLUGIN_NAME = "AsyncRoot"; + private static final String FILTERS_PLUGIN_NAME = "Filters"; + private static final String LOGGER_PLUGIN_NAME = "Logger"; + private static final String LOGGERS_PLUGIN_NAME = "Loggers"; + private static final String PROPERTIES_PLUGIN_NAME = "Properties"; + private static final String PROPERTY_PLUGIN_NAME = "Property"; + private static final String ROOT_PLUGIN_NAME = "Root"; + private static final String SCRIPTS_PLUGIN_NAME = "Scripts"; + + @Override + public String getInputFormat() { + return LOG4J_V2_PROPERTIES_FORMAT; + } + + @Override + public ConfigurationNode parse(InputStream inputStream) throws IOException { + Properties rootProperties = new Properties(); + rootProperties.load(inputStream); + ConfigurationNodeImpl.NodeBuilder builder = + ConfigurationNodeImpl.newNodeBuilder().setPluginName(CONFIGURATION_PLUGIN_NAME); + + for (final String key : rootProperties.stringPropertyNames()) { + if (!key.contains(".")) { + builder.addAttribute(key, remove(rootProperties, key)); + } + } + + Properties propertyPlaceholders = extractSubset(rootProperties, "property"); + if (!propertyPlaceholders.isEmpty()) { + builder.addChild(processPropertyPlaceholders(propertyPlaceholders)); + } + + Map scripts = extractSubsetAndPartition(rootProperties, "script"); + if (!scripts.isEmpty()) { + builder.addChild(processScripts(scripts)); + } + + Properties customLevels = extractSubset(rootProperties, "customLevel"); + if (!customLevels.isEmpty()) { + builder.addChild(processCustomLevels(customLevels)); + } + + Map filters = extractSubsetAndPartition(rootProperties, "filter", "filters"); + if (!filters.isEmpty()) { + builder.addChild(processFilters("", filters)); + } + + Map appenders = extractSubsetAndPartition(rootProperties, "appender", "appenders"); + if (!appenders.isEmpty()) { + builder.addChild(processAppenders(appenders)); + } + + ConfigurationNodeImpl.NodeBuilder loggersBuilder = + ConfigurationNodeImpl.newNodeBuilder().setPluginName(LOGGERS_PLUGIN_NAME); + // 1. Start with the root logger + Properties rootLoggerProperties = extractSubset(rootProperties, "rootLogger"); + String rootLoggerProperty = rootProperties.getProperty("rootLogger"); + if (rootLoggerProperty != null) { + rootLoggerProperties.put("", rootLoggerProperty); + } + loggersBuilder.addChild(processRootLogger(rootLoggerProperties)); + // 2. The remaining loggers + Map loggers = extractSubsetAndPartition(rootProperties, "logger", "loggers"); + for (Map.Entry entry : loggers.entrySet()) { + if (!ROOT_LOGGER_NAME.equals(entry.getKey())) { + loggersBuilder.addChild(processLogger(entry.getKey(), entry.getValue())); + } + } + // Add the `Loggers` plugin + builder.addChild(loggersBuilder.build()); + + return builder.build(); + } + + private static Map extractSubsetAndPartition(Properties rootProperties, String prefix) { + return new TreeMap<>(partitionOnCommonPrefixes(extractSubset(rootProperties, prefix))); + } + + private static Map extractSubsetAndPartition( + Properties rootProperties, String prefix, String keysProperty) { + String keysList = rootProperties.getProperty(keysProperty); + if (keysList != null) { + String[] keys = keysList.split(",", -1); + Map result = new LinkedHashMap<>(); + for (final String untrimmedKey : keys) { + String key = untrimmedKey.trim(); + result.put(key, extractSubset(rootProperties, prefix + "." + key)); + } + return result; + } + return extractSubsetAndPartition(rootProperties, prefix); + } + + private static ConfigurationNode processPropertyPlaceholders(final Properties propertyPlaceholders) { + ConfigurationNodeImpl.NodeBuilder builder = + ConfigurationNodeImpl.newNodeBuilder().setPluginName(PROPERTIES_PLUGIN_NAME); + for (final String key : propertyPlaceholders.stringPropertyNames()) { + builder.addChild(ConfigurationNodeImpl.newNodeBuilder() + .setPluginName(PROPERTY_PLUGIN_NAME) + .addAttribute(NAME_ATTRIBUTE, key) + .addAttribute(VALUE_ATTRIBUTE, propertyPlaceholders.getProperty(key)) + .build()); + } + return builder.build(); + } + + private ConfigurationNode processScripts(Map scripts) { + ConfigurationNodeImpl.NodeBuilder builder = + ConfigurationNodeImpl.newNodeBuilder().setPluginName(SCRIPTS_PLUGIN_NAME); + for (final Map.Entry entry : scripts.entrySet()) { + String scriptPrefix = "script." + entry.getKey(); + Properties scriptProperties = entry.getValue(); + builder.addChild(processGenericComponent(scriptPrefix, "Script", scriptProperties)); + } + return builder.build(); + } + + private ConfigurationNode processCustomLevels(Properties customLevels) { + ConfigurationNodeImpl.NodeBuilder builder = + ConfigurationNodeImpl.newNodeBuilder().setPluginName(CUSTOM_LEVELS_PLUGIN_NAME); + for (final String key : customLevels.stringPropertyNames()) { + String value = validateInteger("customLevel." + key, customLevels.getProperty(key)); + builder.addChild(ConfigurationNodeImpl.newNodeBuilder() + .setPluginName(CUSTOM_LEVEL_PLUGIN_NAME) + .addAttribute(NAME_ATTRIBUTE, key) + .addAttribute(INT_LEVEL_ATTRIBUTE, value) + .build()); + } + return builder.build(); + } + + private static String validateInteger(String key, String value) { + try { + Integer.parseInt(value); + return value; + } catch (NumberFormatException e) { + throw new ConfigurationConverterException("Invalid integer value `" + value + "` for key `" + key + "`", e); + } + } + + private static ConfigurationNode processFilters(String prefix, Map filters) { + if (filters.size() == 1) { + return processFilter(prefix, filters.entrySet().iterator().next()); + } + ConfigurationNodeImpl.NodeBuilder builder = + ConfigurationNodeImpl.newNodeBuilder().setPluginName(FILTERS_PLUGIN_NAME); + for (final Map.Entry filterEntry : filters.entrySet()) { + builder.addChild(processFilter(prefix, filterEntry)); + } + return builder.build(); + } + + private static ConfigurationNode processFilter(String prefix, Map.Entry filterEntry) { + String actualPrefix = prefix.isEmpty() ? prefix : prefix + "."; + String filterPrefix = actualPrefix + "filter." + filterEntry.getKey(); + Properties filterProperties = filterEntry.getValue(); + return processGenericComponent(filterPrefix, "Filter", filterProperties); + } + + private static ConfigurationNode processAppenders(Map appenders) { + ConfigurationNodeImpl.NodeBuilder builder = + ConfigurationNodeImpl.newNodeBuilder().setPluginName(APPENDERS_PLUGIN_NAME); + for (Map.Entry entry : appenders.entrySet()) { + builder.addChild(processAppender(entry.getKey(), entry.getValue())); + } + return builder.build(); + } + + private static ConfigurationNode processAppender(String key, Properties properties) { + String appenderPrefix = "appender." + key; + ConfigurationNodeImpl.NodeBuilder builder = ConfigurationNodeImpl.newNodeBuilder() + .setPluginName(getRequiredAttribute( + properties, TYPE_ATTRIBUTE, () -> "No type attribute provided for Appender " + appenderPrefix)) + .addAttribute( + NAME_ATTRIBUTE, + getRequiredAttribute( + properties, + NAME_ATTRIBUTE, + () -> "No name attribute provided for Appender " + appenderPrefix)); + + addFiltersToComponent(appenderPrefix, properties, builder); + processRemainingProperties(appenderPrefix, properties, builder); + + return builder.build(); + } + + private static ConfigurationNode processLogger(String key, Properties properties) { + ConfigurationNodeImpl.NodeBuilder builder = ConfigurationNodeImpl.newNodeBuilder() + .addAttribute(LEVEL_AND_REFS_ATTRIBUTE, remove(properties, "")) + .addAttribute( + NAME_ATTRIBUTE, + getRequiredAttribute( + properties, NAME_ATTRIBUTE, () -> "No name attribute provided for Logger " + key)); + + String type = remove(properties, TYPE_ATTRIBUTE); + if (ASYNC_LOGGER_PLUGIN_NAME.equalsIgnoreCase(type)) { + builder.setPluginName(ASYNC_LOGGER_PLUGIN_NAME); + } else if (type != null) { + throw new ConfigurationConverterException("Unknown logger type `" + type + "` for logger " + key); + } else { + builder.setPluginName(LOGGER_PLUGIN_NAME); + } + + String prefix = "logger." + key; + addAppenderRefsToComponent(prefix, properties, builder); + addFiltersToComponent(prefix, properties, builder); + processRemainingProperties(prefix, properties, builder); + + return builder.build(); + } + + private static void addAppenderRefsToComponent( + String prefix, Properties properties, ConfigurationNodeImpl.NodeBuilder builder) { + Map appenderRefs = extractSubsetAndPartition(properties, "appenderRef"); + for (final Map.Entry entry : appenderRefs.entrySet()) { + builder.addChild(processAppenderRef(prefix + ".appenderRef." + entry.getKey(), entry.getValue())); + } + } + + private static ConfigurationNode processAppenderRef(String prefix, Properties properties) { + ConfigurationNodeImpl.NodeBuilder builder = ConfigurationNodeImpl.newNodeBuilder() + .setPluginName(APPENDER_REF_PLUGIN_NAME) + .addAttribute( + REF_ATTRIBUTE, + getRequiredAttribute( + properties, + REF_ATTRIBUTE, + () -> "No ref attribute provided for AppenderRef " + prefix)); + + String level = Strings.trimToNull(remove(properties, "level")); + if (level != null) { + builder.addAttribute("level", level); + } + + addFiltersToComponent(prefix, properties, builder); + processRemainingProperties(prefix, properties, builder); + + return builder.build(); + } + + private static void addFiltersToComponent( + String prefix, Properties properties, ConfigurationNodeImpl.NodeBuilder builder) { + Map filters = extractSubsetAndPartition(properties, "filter"); + if (!filters.isEmpty()) { + builder.addChild(processFilters(prefix, filters)); + } + } + + private static ConfigurationNode processRootLogger(Properties properties) { + ConfigurationNodeImpl.NodeBuilder builder = + ConfigurationNodeImpl.newNodeBuilder().addAttribute(LEVEL_AND_REFS_ATTRIBUTE, remove(properties, "")); + + String type = remove(properties, TYPE_ATTRIBUTE); + if (ASYNC_ROOT_PLUGIN_NAME.equalsIgnoreCase(type)) { + builder.setPluginName(ASYNC_ROOT_PLUGIN_NAME); + } else if (type != null) { + throw new ConfigurationConverterException("Unknown logger type `" + type + "` for root logger."); + } else { + builder.setPluginName(ROOT_PLUGIN_NAME); + } + + String prefix = "rootLogger"; + addAppenderRefsToComponent(prefix, properties, builder); + addFiltersToComponent(prefix, properties, builder); + processRemainingProperties(prefix, properties, builder); + + return builder.build(); + } + + private static @Nullable String remove(final Properties properties, final String key) { + return (String) properties.remove(key); + } + + /** + * Standard mapping between a properties file and node tree. + *

+ * The component must have a {@code type} attribute. + *

+ * + * @param prefix Prefix of all the properties in the global prefix file. Used only for the exception message. + * @param componentCategory Type of expected component. Used only for the exception message. + * @param properties Component properties with names relative to {@code prefix}. + */ + private static ConfigurationNode processGenericComponent( + String prefix, String componentCategory, Properties properties) { + ConfigurationNodeImpl.NodeBuilder builder = ConfigurationNodeImpl.newNodeBuilder(); + + builder.setPluginName(getRequiredAttribute( + properties, + TYPE_ATTRIBUTE, + () -> "No type attribute provided for " + componentCategory + " " + prefix)); + processRemainingProperties(prefix, properties, builder); + return builder.build(); + } + + private static void processRemainingProperties( + String prefix, Properties properties, ConfigurationNodeImpl.NodeBuilder builder) { + while (!properties.isEmpty()) { + String propertyName = properties.stringPropertyNames().iterator().next(); + int index = propertyName.indexOf('.'); + if (index > 0) { + String localPrefix = propertyName.substring(0, index); + String globalPrefix = prefix + "." + propertyName.substring(0, index); + Properties componentProperties = extractSubset(properties, localPrefix); + builder.addChild(processGenericComponent(globalPrefix, "component", componentProperties)); + } else { + builder.addAttribute(propertyName, remove(properties, propertyName)); + } + } + } + + private static String getRequiredAttribute( + Properties properties, String propertyName, Supplier errorMessageSupplier) { + String value = remove(properties, propertyName); + if (Strings.isEmpty(value)) { + throw new ConfigurationConverterException(errorMessageSupplier.get()); + } + return value; + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/XmlConfigurationMapper.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/XmlConfigurationMapper.java new file mode 100644 index 00000000..580b94b2 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/XmlConfigurationMapper.java @@ -0,0 +1,281 @@ +/* + * 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.converter.config.internal.v2; + +import aQute.bnd.annotation.Resolution; +import aQute.bnd.annotation.spi.ServiceProvider; +import com.sun.xml.txw2.output.IndentingXMLStreamWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Map; +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import org.apache.logging.converter.config.ConfigurationConverterException; +import org.apache.logging.converter.config.internal.ConfigurationNodeImpl; +import org.apache.logging.converter.config.spi.ConfigurationMapper; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.jspecify.annotations.Nullable; +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.ErrorHandler; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +@ServiceProvider(value = ConfigurationMapper.class, resolution = Resolution.MANDATORY) +public class XmlConfigurationMapper implements ConfigurationMapper { + + private static final String DISABLE_DOCTYPE_DECLARATION = "http://apache.org/xml/features/disallow-doctype-decl"; + private static final String LOG4J_NAMESPACE_URI = "https://logging.apache.org/xml/ns"; + private static final String LOG4J_SCHEMA_LOCATION = + LOG4J_NAMESPACE_URI + " https://logging.apache.org/xml/ns/log4j-config-2.xsd"; + private static final String LOG4J_V2_XML_FORMAT = "v2:xml"; + + @Override + public ConfigurationNode parse(InputStream inputStream) throws IOException { + DocumentBuilder documentBuilder = createDocumentBuilder(); + // The default error handler pollutes `System.err` + documentBuilder.setErrorHandler(new ThrowingErrorHandler()); + try { + Document document = documentBuilder.parse(inputStream); + Element configurationElement = document.getDocumentElement(); + if (!isLog4jNamespace(configurationElement)) { + throw new ConfigurationConverterException("Wrong configuration file namespace: expecting `" + + LOG4J_NAMESPACE_URI + "`, found `" + configurationElement.getNamespaceURI() + "`"); + } + return parseComplexElement(configurationElement); + } catch (SAXException e) { + Throwable cause = e.getCause(); + if (cause instanceof IOException) { + throw (IOException) cause; + } + throw new IOException("Unable to parse configuration file.", e); + } + } + + @Override + public void writeConfiguration(OutputStream outputStream, ConfigurationNode configuration) throws IOException { + try { + XMLStreamWriter streamWriter = createStreamWriter(outputStream); + streamWriter.writeStartDocument(); + streamWriter.setDefaultNamespace(LOG4J_NAMESPACE_URI); + streamWriter.writeStartElement(LOG4J_NAMESPACE_URI, configuration.getPluginName()); + // Register the namespaces + streamWriter.writeDefaultNamespace(LOG4J_NAMESPACE_URI); + streamWriter.writeNamespace("xsi", XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI); + streamWriter.writeAttribute( + XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "schemaLocation", LOG4J_SCHEMA_LOCATION); + // Write the content of the file + writeNodeContentToStreamWriter(configuration, streamWriter); + streamWriter.writeEndElement(); + streamWriter.writeEndDocument(); + } catch (XMLStreamException e) { + throw new IOException("Unable to write configuration.", e); + } + } + + @Override + public String getFormat() { + return LOG4J_V2_XML_FORMAT; + } + + private static DocumentBuilder createDocumentBuilder() throws IOException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + enableFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING); + enableFeature(factory, DISABLE_DOCTYPE_DECLARATION); + try { + factory.setXIncludeAware(false); + } catch (UnsupportedOperationException e) { + throw new IOException( + "Failed to disable XInclude on DocumentBuilderFactory " + + factory.getClass().getName(), + e); + } + try { + return factory.newDocumentBuilder(); + } catch (ParserConfigurationException e) { + throw new IOException( + "Failed to create DocumentBuilder using DocumentBuilderFactory " + + factory.getClass().getName(), + e); + } + } + + private static XMLStreamWriter createStreamWriter(OutputStream outputStream) throws IOException { + XMLOutputFactory outputFactory = XMLOutputFactory.newFactory(); + try { + outputFactory.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, Boolean.TRUE); + return new IndentingXMLStreamWriter(outputFactory.createXMLStreamWriter(outputStream)); + } catch (XMLStreamException e) { + throw new IOException( + "Failed to create XMLStreamWriter using XMLOutputFactory " + + outputFactory.getClass().getName(), + e); + } + } + + private static void enableFeature(DocumentBuilderFactory factory, String feature) throws IOException { + try { + factory.setFeature(feature, true); + } catch (ParserConfigurationException e) { + throw new IOException( + "Failed to enable '" + feature + "' feature on DocumentBuilderFactory " + + factory.getClass().getName(), + e); + } + } + + /** + * Transforms an XML element into a Log4j configuration node. + */ + private static ConfigurationNode parseComplexElement(Element element) { + ConfigurationNodeImpl.NodeBuilder builder = ConfigurationNodeImpl.newNodeBuilder(); + // Handle child elements + NodeList childNodes = element.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + processChildElement(childNodes.item(i), builder); + } + // Handle child attributes + NamedNodeMap nodeMap = element.getAttributes(); + processAttributes(nodeMap, builder); + return builder.setPluginName(element.getTagName()).build(); + } + + /** + * Handles child nodes of an XML element. + * + * @param childNode An XML node. + * @param builder A {@link ConfigurationNode} builder. + */ + private static void processChildElement(org.w3c.dom.Node childNode, ConfigurationNodeImpl.NodeBuilder builder) { + if (isLog4jNamespace(childNode) && childNode instanceof Element) { + Element childElement = (Element) childNode; + if (isComplexElement(childElement)) { + builder.addChild(parseComplexElement(childElement)); + return; + } + String value = getSimpleElementValue(childElement); + // An empty Simple Element is probably a plugin, e.g. `` + if (value.isEmpty()) { + builder.addChild(parseComplexElement(childElement)); + } else { + builder.addAttribute(childElement.getNodeName(), value); + } + } + } + + /** + * Checks if an element is an XSD Complex Element + * + * @return {@code true} if the element has child elements or attributes. + */ + private static boolean isComplexElement(org.w3c.dom.Node element) { + NodeList nodeList = element.getChildNodes(); + for (int i = 0; i < nodeList.getLength(); i++) { + if (nodeList.item(i) instanceof Element) { + return true; + } + } + return element.getAttributes().getLength() > 0; + } + + private static String getSimpleElementValue(org.w3c.dom.Node element) { + String value = element.getTextContent(); + return value == null ? "" : value.trim(); + } + + /** + * Handles attributes of an XML element. + * + * @param nodeMap A collection of XML attributes. + * @param builder A {@link ConfigurationNode} builder. + */ + private static void processAttributes(NamedNodeMap nodeMap, ConfigurationNodeImpl.NodeBuilder builder) { + for (int i = 0; i < nodeMap.getLength(); i++) { + org.w3c.dom.Node item = nodeMap.item(i); + if (isLog4jNamespace(item) && item instanceof Attr) { + Attr attr = (Attr) item; + builder.addAttribute(attr.getName(), attr.getValue()); + } + } + } + + /** + * Writes the content of {@code node} to the {@link XMLStreamWriter}. + *

+ * This method assumes that the {@link XMLStreamWriter#writeStartElement} and {@link XMLStreamWriter#writeEndElement()} + * are handled by the caller. + *

+ */ + private static void writeNodeContentToStreamWriter( + ConfigurationNode configurationNode, XMLStreamWriter streamWriter) throws XMLStreamException { + for (Map.Entry attribute : + configurationNode.getAttributes().entrySet()) { + streamWriter.writeAttribute(attribute.getKey(), attribute.getValue()); + } + for (ConfigurationNode child : configurationNode.getChildren()) { + // Empty element with attributes + if (child.getChildren().isEmpty()) { + streamWriter.writeEmptyElement(LOG4J_NAMESPACE_URI, child.getPluginName()); + writeNodeContentToStreamWriter(child, streamWriter); + } else { + streamWriter.writeStartElement(LOG4J_NAMESPACE_URI, child.getPluginName()); + writeNodeContentToStreamWriter(child, streamWriter); + streamWriter.writeEndElement(); + } + } + } + + private static boolean isLog4jNamespace(Node node) { + @Nullable String namespaceUri = node.getNamespaceURI(); + return namespaceUri == null || namespaceUri.equals(LOG4J_NAMESPACE_URI); + } + + private static class ThrowingErrorHandler implements ErrorHandler { + @Override + public void warning(SAXParseException exception) {} + + @Override + public void error(SAXParseException exception) throws SAXException { + throwOnParseException(exception); + } + + @Override + public void fatalError(SAXParseException exception) throws SAXException { + throwOnParseException(exception); + } + + private void throwOnParseException(SAXParseException exception) throws SAXException { + IOException ioException = new IOException( + String.format( + "Invalid configuration file content at line %d, column %d: %s", + exception.getLineNumber(), exception.getColumnNumber(), exception.getMessage()), + exception); + throw new SAXException(ioException); + } + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/YamlConfigurationMapper.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/YamlConfigurationMapper.java new file mode 100644 index 00000000..6e2385b9 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/YamlConfigurationMapper.java @@ -0,0 +1,43 @@ +/* + * 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.converter.config.internal.v2; + +import aQute.bnd.annotation.Resolution; +import aQute.bnd.annotation.spi.ServiceProvider; +import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; +import org.apache.logging.converter.config.spi.ConfigurationMapper; + +@ServiceProvider(value = ConfigurationMapper.class, resolution = Resolution.MANDATORY) +public class YamlConfigurationMapper extends AbstractJacksonConfigurationMapper { + + private static final String LOG4J_V2_YAML_FORMAT = "v2:yaml"; + + public YamlConfigurationMapper() { + super( + YAMLMapper.builder() + .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER) + .disable(YAMLGenerator.Feature.SPLIT_LINES) + .build(), + true); + } + + @Override + public String getFormat() { + return LOG4J_V2_YAML_FORMAT; + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/package-info.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/package-info.java new file mode 100644 index 00000000..c465391b --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ +@NullMarked +package org.apache.logging.converter.config.internal.v2; + +import org.jspecify.annotations.NullMarked; diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v3/PropertiesV3ConfigurationMapper.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v3/PropertiesV3ConfigurationMapper.java new file mode 100644 index 00000000..090baf46 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v3/PropertiesV3ConfigurationMapper.java @@ -0,0 +1,58 @@ +/* + * 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.converter.config.internal.v3; + +import aQute.bnd.annotation.Resolution; +import aQute.bnd.annotation.spi.ServiceProvider; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.dataformat.javaprop.JavaPropsMapper; +import java.io.IOException; +import java.io.InputStream; +import org.apache.logging.converter.config.internal.v2.AbstractJacksonConfigurationMapper; +import org.apache.logging.converter.config.spi.ConfigurationMapper; +import org.apache.logging.converter.config.spi.ConfigurationNode; + +@ServiceProvider(value = ConfigurationMapper.class, resolution = Resolution.MANDATORY) +public class PropertiesV3ConfigurationMapper extends AbstractJacksonConfigurationMapper { + + private static final String LOG4J_V3_PROPERTIES_FORMAT = "v3:properties"; + + public PropertiesV3ConfigurationMapper() { + super( + JavaPropsMapper.builder() + .enable(JsonParser.Feature.ALLOW_COMMENTS) + .build(), + false); + } + + @Override + public String getFormat() { + return LOG4J_V3_PROPERTIES_FORMAT; + } + + @Override + public ConfigurationNode parse(InputStream inputStream) throws IOException { + return super.parse(inputStream); + } + + @Override + protected boolean requiresExplicitTypeAttribute(ConfigurationNode configurationNode) { + // Empty nodes need a `type` attribute in the Properties format + return configurationNode.getAttributes().isEmpty() + && configurationNode.getChildren().isEmpty(); + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v3/package-info.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v3/package-info.java new file mode 100644 index 00000000..72398667 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v3/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ +@NullMarked +package org.apache.logging.converter.config.internal.v3; + +import org.jspecify.annotations.NullMarked; diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/package-info.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/package-info.java new file mode 100644 index 00000000..7423ef6a --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/package-info.java @@ -0,0 +1,42 @@ +/* + * 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. + */ +/** + * Simple API to convert between different logging configuration formats. + *

+ * For example, to convert from the Log4j Core 2 Properties configuration format to the YAML configuration format, + * you can use the following code: + *

+ *
+ * 
+ * Path input = ...;
+ * Path output = ...;
+ * try (InputStream inputStream = Files.newInputStream(input);
+ *         OutputStream outputStream = Files.newOutputStream(output)) {
+ *     ConfigurationConverter converter = ConfigurationConverter.newInstance();
+ *     converter.convert(inputStream, "v2:properties", outputStream, "v2:yaml");
+ * }
+ * 
+ * 
+ */ +@NullMarked +@Export +@Version("0.3.0") +package org.apache.logging.converter.config; + +import org.jspecify.annotations.NullMarked; +import org.osgi.annotation.bundle.Export; +import org.osgi.annotation.versioning.Version; diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/ConfigurationMapper.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/ConfigurationMapper.java new file mode 100644 index 00000000..a143e99a --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/ConfigurationMapper.java @@ -0,0 +1,47 @@ +/* + * 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.converter.config.spi; + +import org.osgi.annotation.versioning.ProviderType; + +/** + * Interface for format provider classes that implement a parser and a writer at the same time. + *

+ * Implementations of this class should be registered with {@link java.util.ServiceLoader}. + *

+ */ +@ProviderType +public interface ConfigurationMapper extends ConfigurationParser, ConfigurationWriter { + + /** + * A short string that identifies the format supported by this mapper. + *

+ * For example, "v2:xml" or "v1:properties". + *

+ */ + String getFormat(); + + @Override + default String getInputFormat() { + return getFormat(); + } + + @Override + default String getOutputFormat() { + return getFormat(); + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/ConfigurationNode.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/ConfigurationNode.java new file mode 100644 index 00000000..71d1bd5e --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/ConfigurationNode.java @@ -0,0 +1,52 @@ +/* + * 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.converter.config.spi; + +import java.util.List; +import java.util.Map; +import org.osgi.annotation.versioning.ProviderType; + +/** + * A simplified view of a Log4j Core 2 configuration subtree. + *

+ * It provides a subset of the information contained in the + * o.a.l.l.core.config.Node + * class. + *

+ * @see Configuration Syntax + * for an explanation of the used terms. + */ +@ProviderType +public interface ConfigurationNode { + + /** + * The name of the plugin configured by this node. + * + * @see Log4j Core Plugins for more details + */ + String getPluginName(); + + /** + * The configuration attributes + */ + Map getAttributes(); + + /** + * The nested configuration elements. + */ + List getChildren(); +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/ConfigurationParser.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/ConfigurationParser.java new file mode 100644 index 00000000..d0a943fc --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/ConfigurationParser.java @@ -0,0 +1,50 @@ +/* + * 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.converter.config.spi; + +import java.io.IOException; +import java.io.InputStream; +import org.osgi.annotation.versioning.ProviderType; + +/** + * Interface to be implemented by format providers that can parse a configuration file format. + *

+ * Implementations of this class should be registered with {@link java.util.ServiceLoader}. + *

+ */ +@ProviderType +public interface ConfigurationParser { + + /** + * A short string that identifies the format supported by this parser. + *

+ * For example, "v2:xml" or "v1:properties". + *

+ */ + String getInputFormat(); + + /** + * Parses a configuration file into a tree of configuration nodes. + *

+ * The returned tree should represent a valid Log4j Core 2 configuration. + *

+ * @param inputStream The input configuration file. + * @return A tree of configuration nodes. + * @throws IOException If a parsing error occurs. + */ + ConfigurationNode parse(InputStream inputStream) throws IOException; +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/ConfigurationWriter.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/ConfigurationWriter.java new file mode 100644 index 00000000..9f2fa009 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/ConfigurationWriter.java @@ -0,0 +1,50 @@ +/* + * 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.converter.config.spi; + +import java.io.IOException; +import java.io.OutputStream; +import org.osgi.annotation.versioning.ProviderType; + +/** + * Interface to be implemented by format providers that can write a configuration file format. + *

+ * Implementations of this class should be registered with {@link java.util.ServiceLoader}. + *

+ */ +@ProviderType +public interface ConfigurationWriter { + + /** + * A short string that identifies the format supported by this writer. + *

+ * For example, "v2:xml" or "v1:properties". + *

+ */ + String getOutputFormat(); + + /** + * Write a tree of configuration nodes to a configuration file. + *

+ * The returned tree should represent a valid Log4j Core 2 configuration. + *

+ * @param outputStream The output configuration file. + * @param configuration A tree of configuration nodes. + * @throws IOException If a writing error occurs. + */ + void writeConfiguration(OutputStream outputStream, ConfigurationNode configuration) throws IOException; +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/package-info.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/package-info.java new file mode 100644 index 00000000..a21bfff6 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/package-info.java @@ -0,0 +1,33 @@ +/* + * 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. + */ +/** + * SPI interfaces for the Configuration Converter. + *

+ * Integrators of new logging configuration file formats, should implement + * {@link org.apache.logging.converter.config.spi.ConfigurationMapper} or + * {@link org.apache.logging.converter.config.spi.ConfigurationParser} and + * register it with {@link java.util.ServiceLoader}. + *

+ */ +@NullMarked +@Export +@Version("0.3.0") +package org.apache.logging.converter.config.spi; + +import org.jspecify.annotations.NullMarked; +import org.osgi.annotation.bundle.Export; +import org.osgi.annotation.versioning.Version; diff --git a/log4j-converter-config/src/test/java/org/apache/logging/converter/config/ConfigurationConverterTest.java b/log4j-converter-config/src/test/java/org/apache/logging/converter/config/ConfigurationConverterTest.java new file mode 100644 index 00000000..d86407bc --- /dev/null +++ b/log4j-converter-config/src/test/java/org/apache/logging/converter/config/ConfigurationConverterTest.java @@ -0,0 +1,101 @@ +/* + * 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.converter.config; + +import static org.apache.logging.converter.config.internal.AbstractConfigurationMapperTest.EXAMPLE_V2_CONFIGURATION; +import static org.apache.logging.converter.config.internal.AbstractConfigurationMapperTest.assertThat; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Objects; +import java.util.stream.Stream; +import org.apache.logging.converter.config.internal.v2.XmlConfigurationMapper; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.apache.logging.converter.config.spi.ConfigurationParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class ConfigurationConverterTest { + + private static final String DEFAULT_FORMAT = "v2:xml"; + + private final ConfigurationConverter converter = ConfigurationConverter.newInstance(); + private final ConfigurationParser parser = new XmlConfigurationMapper(); + + public static Stream conversionToXml() { + return Stream.of( + Arguments.of("/v2/log4j2.json", "v2:json"), + Arguments.of("/v2/log4j2.properties", "v2:properties"), + Arguments.of("/v2/log4j2.xml", DEFAULT_FORMAT), + Arguments.of("/v2/log4j2.yaml", "v2:yaml"), + Arguments.of("/v3/log4j2.properties", "v3:properties")); + } + + @ParameterizedTest + @MethodSource + void conversionToXml(String inputResource, String inputFormat) throws IOException { + try (InputStream inputStream = getClass().getResourceAsStream(inputResource)) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + converter.convert(Objects.requireNonNull(inputStream), inputFormat, outputStream, DEFAULT_FORMAT); + // Parse the result and check if it matches + ConfigurationNode actual = parser.parse(new ByteArrayInputStream(outputStream.toByteArray())); + assertThat(actual).ignoringOrder().isEqualTo(EXAMPLE_V2_CONFIGURATION); + } + } + + @Test + void supportedFormats() { + assertThat(converter.getSupportedInputFormats()) + .containsExactlyInAnyOrder("v2:json", "v2:properties", DEFAULT_FORMAT, "v2:yaml", "v3:properties"); + assertThat(converter.getSupportedOutputFormats()) + .containsExactlyInAnyOrder("v2:json", DEFAULT_FORMAT, "v2:yaml", "v3:properties"); + } + + @Test + void throwOnUnsupportedFormat() { + InputStream inputStream = new ByteArrayInputStream(new byte[0]); + OutputStream outputStream = new ByteArrayOutputStream(); + String unsupportedFormat = "unsupportedFormat"; + // Messages may vary, but should explain what happened + assertThatThrownBy(() -> converter.convert(inputStream, unsupportedFormat, outputStream, DEFAULT_FORMAT)) + .isInstanceOf(ConfigurationConverterException.class) + .hasMessageContaining("input format") + .hasMessageContaining(unsupportedFormat) + .hasMessageContaining("not supported"); + assertThatThrownBy(() -> converter.convert(inputStream, DEFAULT_FORMAT, outputStream, unsupportedFormat)) + .isInstanceOf(ConfigurationConverterException.class) + .hasMessageContaining("output format") + .hasMessageContaining(unsupportedFormat) + .hasMessageContaining("not supported"); + } + + @Test + void throwOnIOException() { + InputStream inputStream = new ByteArrayInputStream(new byte[0]); + OutputStream outputStream = new ByteArrayOutputStream(); + assertThatThrownBy(() -> converter.convert(inputStream, DEFAULT_FORMAT, outputStream, DEFAULT_FORMAT)) + .isInstanceOf(ConfigurationConverterException.class) + .hasCauseInstanceOf(IOException.class); + } +} diff --git a/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/AbstractConfigurationMapperTest.java b/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/AbstractConfigurationMapperTest.java new file mode 100644 index 00000000..d7edda06 --- /dev/null +++ b/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/AbstractConfigurationMapperTest.java @@ -0,0 +1,250 @@ +/* + * 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.converter.config.internal; + +import static org.apache.logging.converter.config.internal.ConfigurationNodeImpl.newNodeBuilder; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.assertj.core.api.AbstractObjectAssert; +import org.jspecify.annotations.Nullable; +import org.opentest4j.AssertionFailedError; + +public abstract class AbstractConfigurationMapperTest { + + public static final ConfigurationNode EXAMPLE_V2_CONFIGURATION = newNodeBuilder() + .setPluginName("Configuration") + .addAttribute("monitorInterval", "10") + .addAttribute("name", "Test Configuration") + .addChild(newNodeBuilder() + .setPluginName("Properties") + .addChild(newNodeBuilder() + .setPluginName("Property") + .addAttribute("name", "pattern") + .addAttribute("value", "%d [%t] %-5p %c - %m%n%ex") + .build()) + .build()) + .addChild(newNodeBuilder() + .setPluginName("CustomLevels") + .addChild(newNodeBuilder() + .setPluginName("CustomLevel") + .addAttribute("name", "CONFIG") + .addAttribute("intLevel", "450") + .build()) + .build()) + .addChild(newNodeBuilder() + .setPluginName("Appenders") + .addChild(newNodeBuilder() + .setPluginName("File") + .addAttribute("name", "MAIN") + .addAttribute("fileName", "main.log") + .addChild(newNodeBuilder() + .setPluginName("JsonTemplateLayout") + .build()) + .build()) + .addChild(newNodeBuilder() + .setPluginName("File") + .addAttribute("name", "AUDIT") + .addAttribute("fileName", "audit.log") + .addChild(newNodeBuilder() + .setPluginName("MarkerFilter") + .addAttribute("marker", "AUDIT") + .build()) + .addChild(newNodeBuilder() + .setPluginName("PatternLayout") + .addAttribute("pattern", "${pattern}") + .build()) + .build()) + .addChild(newNodeBuilder() + .setPluginName("Console") + .addAttribute("name", "CONSOLE") + .addChild(newNodeBuilder() + .setPluginName("Filters") + .addChild(newNodeBuilder() + .setPluginName("ThresholdFilter") + .addAttribute("level", "WARN") + .addAttribute("onMatch", "ACCEPT") + .addAttribute("onMismatch", "NEUTRAL") + .build()) + .addChild(newNodeBuilder() + .setPluginName("BurstFilter") + .build()) + .build()) + .addChild(newNodeBuilder() + .setPluginName("JsonTemplateLayout") + .build()) + .build()) + .addChild(newNodeBuilder() + .setPluginName("File") + .addAttribute("name", "DEBUG_LOG") + .addAttribute("fileName", "debug.log") + .addChild(newNodeBuilder() + .setPluginName("JsonTemplateLayout") + .build()) + .build()) + .build()) + .addChild(newNodeBuilder() + .setPluginName("Loggers") + .addChild(newNodeBuilder() + .setPluginName("Root") + .addAttribute("level", "INFO") + .addChild(newNodeBuilder() + .setPluginName("AppenderRef") + .addAttribute("ref", "MAIN") + .addChild(newNodeBuilder() + .setPluginName("MarkerFilter") + .addAttribute("marker", "PRIVATE") + .build()) + .build()) + .addChild(newNodeBuilder() + .setPluginName("AppenderRef") + .addAttribute("ref", "CONSOLE") + .addChild(newNodeBuilder() + .setPluginName("Filters") + .addChild(newNodeBuilder() + .setPluginName("ThresholdFilter") + .addAttribute("level", "WARN") + .addAttribute("onMatch", "ACCEPT") + .addAttribute("onMismatch", "NEUTRAL") + .build()) + .addChild(newNodeBuilder() + .setPluginName("BurstFilter") + .build()) + .build()) + .build()) + .addChild(newNodeBuilder() + .setPluginName("BurstFilter") + .build()) + .build()) + .addChild(newNodeBuilder() + .setPluginName("Logger") + .addAttribute("name", "org.apache.logging") + .addAttribute("additivity", "false") + .addAttribute("level", "DEBUG") + .addChild(newNodeBuilder() + .setPluginName("AppenderRef") + .addAttribute("ref", "AUDIT") + .build()) + .addChild(newNodeBuilder() + .setPluginName("AppenderRef") + .addAttribute("ref", "DEBUG_LOG") + .build()) + .addChild(newNodeBuilder() + .setPluginName("Filters") + .addChild(newNodeBuilder() + .setPluginName("ThresholdFilter") + .addAttribute("level", "DEBUG") + .addAttribute("onMatch", "ACCEPT") + .addAttribute("onMismatch", "NEUTRAL") + .build()) + .addChild(newNodeBuilder() + .setPluginName("BurstFilter") + .addAttribute("level", "TRACE") + .build()) + .build()) + .build()) + .build()) + .build(); + + public static ConfigurationNodeAssert assertThat(ConfigurationNode node) { + return new ConfigurationNodeAssert(node, false); + } + + public static final class ConfigurationNodeAssert + extends AbstractObjectAssert { + + final boolean ignoreOrder; + + private ConfigurationNodeAssert(ConfigurationNode configurationNode, boolean ignoreOrder) { + super(configurationNode, ConfigurationNodeAssert.class); + this.ignoreOrder = ignoreOrder; + } + + public ConfigurationNodeAssert ignoringOrder() { + return new ConfigurationNodeAssert(this.actual, true); + } + + public ConfigurationNodeAssert isEqualTo(ConfigurationNode expected) { + ConfigurationNodeDifference difference = compare("$", expected, actual); + if (difference != null) { + String message = String.format( + "Expecting configuration nodes to be equal, but actual node at path `%s`:\n%s\nwas different from expected node at that path:\n%s", + difference.prefix, difference.actual, difference.expected); + throw new AssertionFailedError(message, expected, actual); + } + return this; + } + + private @Nullable ConfigurationNodeDifference compare( + String prefix, @Nullable ConfigurationNode expected, @Nullable ConfigurationNode actual) { + if (expected == null + || actual == null + || !expected.getPluginName().equals(actual.getPluginName()) + || !expected.getAttributes().equals(actual.getAttributes())) { + return new ConfigurationNodeDifference(prefix, expected, actual); + } + List expectedChildren = new ArrayList<>(expected.getChildren()); + List actualChildren = new ArrayList<>(actual.getChildren()); + if (ignoreOrder) { + // This comparator is good enough for now + Comparator comparator = Comparator.comparing(ConfigurationNode::getPluginName) + .thenComparing( + node -> Objects.toString(node.getAttributes().get("name"), "")); + expectedChildren.sort(comparator); + actualChildren.sort(comparator); + } + Iterator expectedIterator = expectedChildren.iterator(); + Iterator actualIterator = actualChildren.iterator(); + ConfigurationNode currentExpected; + ConfigurationNode currentActual; + while (true) { + currentExpected = expectedIterator.hasNext() ? expectedIterator.next() : null; + currentActual = actualIterator.hasNext() ? actualIterator.next() : null; + if (currentExpected == null) { + return currentActual != null ? new ConfigurationNodeDifference(prefix, expected, actual) : null; + } + ConfigurationNodeDifference difference = + compare(prefix + "." + currentExpected.getPluginName(), currentExpected, currentActual); + if (difference != null) { + return difference; + } + } + } + } + + private static final class ConfigurationNodeDifference { + + private final String prefix; + + @Nullable + private final ConfigurationNode expected; + + @Nullable + private final ConfigurationNode actual; + + private ConfigurationNodeDifference( + String prefix, @Nullable ConfigurationNode expected, @Nullable ConfigurationNode actual) { + this.prefix = prefix; + this.expected = expected; + this.actual = actual; + } + } +} diff --git a/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v2/JsonConfigurationMapperTest.java b/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v2/JsonConfigurationMapperTest.java new file mode 100644 index 00000000..b2ce66cf --- /dev/null +++ b/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v2/JsonConfigurationMapperTest.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.logging.converter.config.internal.v2; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; +import org.apache.logging.converter.config.internal.AbstractConfigurationMapperTest; +import org.apache.logging.converter.config.spi.ConfigurationMapper; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.apache.logging.converter.config.spi.ConfigurationParser; +import org.junit.jupiter.api.Test; + +class JsonConfigurationMapperTest extends AbstractConfigurationMapperTest { + + @Test + void convertFromJson() throws IOException { + ConfigurationParser parser = new JsonConfigurationMapper(); + try (InputStream inputStream = getClass().getResourceAsStream("/v2/log4j2.json")) { + ConfigurationNode actual = parser.parse(Objects.requireNonNull(inputStream)); + assertThat(actual).isEqualTo(EXAMPLE_V2_CONFIGURATION); + } + } + + @Test + void convertToJsonAndBack() throws IOException { + ConfigurationMapper mapper = new JsonConfigurationMapper(); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + mapper.writeConfiguration(outputStream, EXAMPLE_V2_CONFIGURATION); + + ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray()); + ConfigurationNode actual = mapper.parse(inputStream); + + assertThat(actual).isEqualTo(EXAMPLE_V2_CONFIGURATION); + } +} diff --git a/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v2/PropertiesV2ConfigurationParserTest.java b/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v2/PropertiesV2ConfigurationParserTest.java new file mode 100644 index 00000000..7d21475a --- /dev/null +++ b/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v2/PropertiesV2ConfigurationParserTest.java @@ -0,0 +1,37 @@ +/* + * 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.converter.config.internal.v2; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; +import org.apache.logging.converter.config.internal.AbstractConfigurationMapperTest; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.apache.logging.converter.config.spi.ConfigurationParser; +import org.junit.jupiter.api.Test; + +class PropertiesV2ConfigurationParserTest extends AbstractConfigurationMapperTest { + + @Test + void convertFromProperties() throws IOException { + ConfigurationParser parser = new PropertiesV2ConfigurationParser(); + try (InputStream inputStream = getClass().getResourceAsStream("/v2/log4j2.properties")) { + ConfigurationNode actual = parser.parse(Objects.requireNonNull(inputStream)); + assertThat(actual).isEqualTo(EXAMPLE_V2_CONFIGURATION); + } + } +} diff --git a/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v2/XmlConfigurationMapperTest.java b/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v2/XmlConfigurationMapperTest.java new file mode 100644 index 00000000..6c582ac7 --- /dev/null +++ b/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v2/XmlConfigurationMapperTest.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.logging.converter.config.internal.v2; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; +import org.apache.logging.converter.config.internal.AbstractConfigurationMapperTest; +import org.apache.logging.converter.config.spi.ConfigurationMapper; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.apache.logging.converter.config.spi.ConfigurationParser; +import org.junit.jupiter.api.Test; + +class XmlConfigurationMapperTest extends AbstractConfigurationMapperTest { + + @Test + void convertFromXml() throws IOException { + ConfigurationParser parser = new XmlConfigurationMapper(); + try (InputStream inputStream = getClass().getResourceAsStream("/v2/log4j2.xml")) { + ConfigurationNode actual = parser.parse(Objects.requireNonNull(inputStream)); + assertThat(actual).isEqualTo(EXAMPLE_V2_CONFIGURATION); + } + } + + @Test + void convertToXmlAndBack() throws IOException { + ConfigurationMapper mapper = new XmlConfigurationMapper(); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + mapper.writeConfiguration(outputStream, EXAMPLE_V2_CONFIGURATION); + + ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray()); + ConfigurationNode actual = mapper.parse(inputStream); + + assertThat(actual).isEqualTo(EXAMPLE_V2_CONFIGURATION); + } +} diff --git a/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v2/YamlConfigurationMapperTest.java b/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v2/YamlConfigurationMapperTest.java new file mode 100644 index 00000000..e50984ae --- /dev/null +++ b/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v2/YamlConfigurationMapperTest.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.logging.converter.config.internal.v2; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; +import org.apache.logging.converter.config.internal.AbstractConfigurationMapperTest; +import org.apache.logging.converter.config.spi.ConfigurationMapper; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.apache.logging.converter.config.spi.ConfigurationParser; +import org.junit.jupiter.api.Test; + +class YamlConfigurationMapperTest extends AbstractConfigurationMapperTest { + + @Test + void convertFromYaml() throws IOException { + ConfigurationParser parser = new YamlConfigurationMapper(); + try (InputStream inputStream = getClass().getResourceAsStream("/v2/log4j2.yaml")) { + ConfigurationNode actual = parser.parse(Objects.requireNonNull(inputStream)); + assertThat(actual).isEqualTo(EXAMPLE_V2_CONFIGURATION); + } + } + + @Test + void convertToYamlAndBack() throws IOException { + ConfigurationMapper mapper = new YamlConfigurationMapper(); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + mapper.writeConfiguration(outputStream, EXAMPLE_V2_CONFIGURATION); + + ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray()); + ConfigurationNode actual = mapper.parse(inputStream); + + assertThat(actual).isEqualTo(EXAMPLE_V2_CONFIGURATION); + } +} diff --git a/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v3/PropertiesV3ConfigurationMapperTest.java b/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v3/PropertiesV3ConfigurationMapperTest.java new file mode 100644 index 00000000..6f93623c --- /dev/null +++ b/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v3/PropertiesV3ConfigurationMapperTest.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.logging.converter.config.internal.v3; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; +import org.apache.logging.converter.config.internal.AbstractConfigurationMapperTest; +import org.apache.logging.converter.config.spi.ConfigurationMapper; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.apache.logging.converter.config.spi.ConfigurationParser; +import org.junit.jupiter.api.Test; + +class PropertiesV3ConfigurationMapperTest extends AbstractConfigurationMapperTest { + + @Test + void convertFromProperties() throws IOException { + ConfigurationParser parser = new PropertiesV3ConfigurationMapper(); + try (InputStream inputStream = getClass().getResourceAsStream("/v3/log4j2.properties")) { + ConfigurationNode actual = parser.parse(Objects.requireNonNull(inputStream)); + assertThat(actual).ignoringOrder().isEqualTo(EXAMPLE_V2_CONFIGURATION); + } + } + + @Test + void convertToPropertiesAndBack() throws IOException { + ConfigurationMapper mapper = new PropertiesV3ConfigurationMapper(); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + mapper.writeConfiguration(outputStream, EXAMPLE_V2_CONFIGURATION); + + ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray()); + ConfigurationNode actual = mapper.parse(inputStream); + + assertThat(actual).ignoringOrder().isEqualTo(EXAMPLE_V2_CONFIGURATION); + } +} diff --git a/log4j-converter-config/src/test/resources/v2/log4j2.json b/log4j-converter-config/src/test/resources/v2/log4j2.json new file mode 100644 index 00000000..3be389a0 --- /dev/null +++ b/log4j-converter-config/src/test/resources/v2/log4j2.json @@ -0,0 +1,103 @@ +{ + "Configuration": { + "monitorInterval": "10", + "name": "Test Configuration", + "Properties": { + "Property": { + "name": "pattern", + "value": "%d [%t] %-5p %c - %m%n%ex" + } + }, + "CustomLevels": { + "CustomLevel": { + "intLevel": "450", + "name": "CONFIG" + } + }, + "Appenders": { + "File": [ + { + "fileName": "main.log", + "name": "MAIN", + "JsonTemplateLayout": {} + }, + { + "fileName": "audit.log", + "name": "AUDIT", + "MarkerFilter": { + "marker": "AUDIT" + }, + "PatternLayout": { + "pattern": "${pattern}" + } + } + ], + "Console": { + "name": "CONSOLE", + "Filters": { + "ThresholdFilter": { + "level": "WARN", + "onMatch": "ACCEPT", + "onMismatch": "NEUTRAL" + }, + "BurstFilter": {} + }, + "JsonTemplateLayout": {} + }, + "id1": { + "type": "File", + "fileName": "debug.log", + "name": "DEBUG_LOG", + "JsonTemplateLayout": {} + } + }, + "Loggers": { + "Root": { + "level": "INFO", + "AppenderRef": [ + { + "ref": "MAIN", + "MarkerFilter": { + "marker": "PRIVATE" + } + }, + { + "ref": "CONSOLE", + "Filters": { + "ThresholdFilter": { + "level": "WARN", + "onMatch": "ACCEPT", + "onMismatch": "NEUTRAL" + }, + "BurstFilter": {} + } + } + ], + "BurstFilter": {} + }, + "Logger": { + "additivity": "false", + "level": "DEBUG", + "name": "org.apache.logging", + "AppenderRef": [ + { + "ref": "AUDIT" + }, + { + "ref": "DEBUG_LOG" + } + ], + "Filters": { + "ThresholdFilter": { + "level": "DEBUG", + "onMatch": "ACCEPT", + "onMismatch": "NEUTRAL" + }, + "BurstFilter": { + "level": "TRACE" + } + } + } + } + } +} \ No newline at end of file diff --git a/log4j-converter-config/src/test/resources/v2/log4j2.properties b/log4j-converter-config/src/test/resources/v2/log4j2.properties new file mode 100644 index 00000000..ab8bdd8b --- /dev/null +++ b/log4j-converter-config/src/test/resources/v2/log4j2.properties @@ -0,0 +1,91 @@ +# +# 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. +# +## +# +monitorInterval = 10 +name = Test Configuration + +# +# Some properties +property.pattern = %d [%t] %-5p %c - %m%n%ex + +# +# Some custom levels +customLevel.CONFIG = 450 + +# +# Simple file appender +appender.id1.type = File +appender.id1.name = MAIN +appender.id1.fileName = main.log +appender.id1.layout.type = JsonTemplateLayout + +# +# File appender with a filter +appender.id2.type = File +appender.id2.name = AUDIT +appender.id2.fileName = audit.log +appender.id2.layout.type = PatternLayout +appender.id2.layout.pattern = ${pattern} +appender.id2.filter.id1.type = MarkerFilter +appender.id2.filter.id1.marker = AUDIT + +appender.id3.type = Console +appender.id3.name = CONSOLE +appender.id3.layout.type = JsonTemplateLayout +appender.id3.filter.id1.type = ThresholdFilter +appender.id3.filter.id1.level = WARN +appender.id3.filter.id1.onMatch = ACCEPT +appender.id3.filter.id1.onMismatch = NEUTRAL +appender.id3.filter.id2.type = BurstFilter + +# +# Another `File` appender that will require a `type` property in JSON/YAML +appender.id4.type = File +appender.id4.name = DEBUG_LOG +appender.id4.fileName = debug.log +appender.id4.layout.type = JsonTemplateLayout + +# +# Loggers +rootLogger.level = INFO +# Appender reference with one filter +rootLogger.appenderRef.id1.ref = MAIN +rootLogger.appenderRef.id1.filter.id1.type = MarkerFilter +rootLogger.appenderRef.id1.filter.id1.marker = PRIVATE +# Appender reference with two filters +rootLogger.appenderRef.id2.ref = CONSOLE +rootLogger.appenderRef.id2.filter.id1.type = ThresholdFilter +rootLogger.appenderRef.id2.filter.id1.level = WARN +rootLogger.appenderRef.id2.filter.id1.onMatch = ACCEPT +rootLogger.appenderRef.id2.filter.id1.onMismatch = NEUTRAL +rootLogger.appenderRef.id2.filter.id2.type = BurstFilter +# One filter +rootLogger.filter.id1.type = BurstFilter + +logger.apache.name = org.apache.logging +logger.apache.additivity = false +logger.apache.level = DEBUG +logger.apache.appenderRef.id1.ref = AUDIT +logger.apache.appenderRef.id2.ref = DEBUG_LOG +# Two filters +logger.apache.filter.id1.type = ThresholdFilter +logger.apache.filter.id1.level = DEBUG +logger.apache.filter.id1.onMatch = ACCEPT +logger.apache.filter.id1.onMismatch = NEUTRAL +logger.apache.filter.id2.type = BurstFilter +logger.apache.filter.id2.level = TRACE diff --git a/log4j-converter-config/src/test/resources/v2/log4j2.xml b/log4j-converter-config/src/test/resources/v2/log4j2.xml new file mode 100644 index 00000000..7078ce36 --- /dev/null +++ b/log4j-converter-config/src/test/resources/v2/log4j2.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/log4j-converter-config/src/test/resources/v2/log4j2.yaml b/log4j-converter-config/src/test/resources/v2/log4j2.yaml new file mode 100644 index 00000000..643032ff --- /dev/null +++ b/log4j-converter-config/src/test/resources/v2/log4j2.yaml @@ -0,0 +1,81 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to you under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +Configuration: + monitorInterval: "10" + name: "Test Configuration" + Properties: + Property: + name: "pattern" + value: "%d [%t] %-5p %c - %m%n%ex" + CustomLevels: + CustomLevel: + name: "CONFIG" + intLevel: "450" + Appenders: + File: + - name: "MAIN" + fileName: "main.log" + JsonTemplateLayout: { } + - name: "AUDIT" + fileName: "audit.log" + MarkerFilter: + marker: "AUDIT" + PatternLayout: + pattern: "${pattern}" + Console: + name: "CONSOLE" + Filters: + ThresholdFilter: + level: "WARN" + onMatch: "ACCEPT" + onMismatch: "NEUTRAL" + BurstFilter: { } + JsonTemplateLayout: { } + id1: + type: "File" + fileName: "debug.log" + name: "DEBUG_LOG" + JsonTemplateLayout: { } + Loggers: + Root: + level: "INFO" + AppenderRef: + - ref: "MAIN" + MarkerFilter: + marker: "PRIVATE" + - ref: "CONSOLE" + Filters: + ThresholdFilter: + level: "WARN" + onMatch: "ACCEPT" + onMismatch: "NEUTRAL" + BurstFilter: { } + BurstFilter: { } + Logger: + name: "org.apache.logging" + level: "DEBUG" + additivity: "false" + AppenderRef: + - ref: "AUDIT" + - ref: "DEBUG_LOG" + Filters: + ThresholdFilter: + level: "DEBUG" + onMatch: "ACCEPT" + onMismatch: "NEUTRAL" + BurstFilter: + level: "TRACE" diff --git a/log4j-converter-config/src/test/resources/v3/log4j2.properties b/log4j-converter-config/src/test/resources/v3/log4j2.properties new file mode 100644 index 00000000..c09cf46b --- /dev/null +++ b/log4j-converter-config/src/test/resources/v3/log4j2.properties @@ -0,0 +1,83 @@ +# +# 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. +# +## +# +monitorInterval = 10 +name = Test Configuration + +# +# Some properties +Properties.Property.name = pattern +Properties.Property.value = %d [%t] %-5p %c - %m%n%ex + +# +# Some custom levels +CustomLevels.CustomLevel.intLevel = 450 +CustomLevels.CustomLevel.name = CONFIG + +# +# Simple file appender +Appenders.File.1.fileName=main.log +Appenders.File.1.name=MAIN +Appenders.File.1.id1.type=JsonTemplateLayout + +# +# File appender with a filter +Appenders.File.2.fileName=audit.log +Appenders.File.2.name=AUDIT +Appenders.File.2.MarkerFilter.marker=AUDIT +Appenders.File.2.PatternLayout.pattern=${pattern} + +Appenders.Console.name=CONSOLE +Appenders.Console.Filters.ThresholdFilter.level=WARN +Appenders.Console.Filters.ThresholdFilter.onMatch=ACCEPT +Appenders.Console.Filters.ThresholdFilter.onMismatch=NEUTRAL +Appenders.Console.Filters.id1.type=BurstFilter +Appenders.Console.id1.type=JsonTemplateLayout + +# +# Another `File` appender that will require a `type` property in JSON/YAML +Appenders.id1.type=File +Appenders.id1.fileName=debug.log +Appenders.id1.name=DEBUG_LOG +Appenders.id1.id1.type=JsonTemplateLayout + +# +# Loggers +Loggers.Root.level=INFO +# Appender reference with one filter +Loggers.Root.AppenderRef.1.ref=MAIN +Loggers.Root.AppenderRef.1.MarkerFilter.marker=PRIVATE +# Appender reference with two filters +Loggers.Root.AppenderRef.2.ref=CONSOLE +Loggers.Root.AppenderRef.2.Filters.ThresholdFilter.level=WARN +Loggers.Root.AppenderRef.2.Filters.ThresholdFilter.onMatch=ACCEPT +Loggers.Root.AppenderRef.2.Filters.ThresholdFilter.onMismatch=NEUTRAL +Loggers.Root.AppenderRef.2.Filters.id1.type=BurstFilter +# One filter +Loggers.Root.id1.type=BurstFilter + +Loggers.Logger.name=org.apache.logging +Loggers.Logger.additivity=false +Loggers.Logger.level=DEBUG +Loggers.Logger.AppenderRef.1.ref=AUDIT +Loggers.Logger.AppenderRef.2.ref=DEBUG_LOG +# Two filters +Loggers.Logger.Filters.ThresholdFilter.level=DEBUG +Loggers.Logger.Filters.ThresholdFilter.onMatch=ACCEPT +Loggers.Logger.Filters.ThresholdFilter.onMismatch=NEUTRAL +Loggers.Logger.Filters.BurstFilter.level=TRACE diff --git a/log4j-transform-parent/pom.xml b/log4j-transform-parent/pom.xml index 61080112..07b41775 100644 --- a/log4j-transform-parent/pom.xml +++ b/log4j-transform-parent/pom.xml @@ -141,6 +141,34 @@
+ + + + biz.aQute.bnd + biz.aQute.bnd.annotation + provided + + + + org.osgi + org.osgi.annotation.bundle + provided + + + + org.osgi + org.osgi.annotation.versioning + provided + + + + com.github.spotbugs + spotbugs-annotations + provided + + + + diff --git a/log4j-weaver/pom.xml b/log4j-weaver/pom.xml index eeb968b0..3319aa29 100644 --- a/log4j-weaver/pom.xml +++ b/log4j-weaver/pom.xml @@ -53,18 +53,6 @@ commons-lang3 - - org.osgi - osgi.annotation - provided - - - - com.github.spotbugs - spotbugs-annotations - provided - - org.assertj assertj-core diff --git a/pom.xml b/pom.xml index e2209269..fecc2692 100644 --- a/pom.xml +++ b/pom.xml @@ -74,6 +74,7 @@ log4j-codegen + log4j-converter-config log4j-converter-plugin-descriptor log4j-transform-maven-plugin log4j-transform-maven-shade-plugin-extensions @@ -133,6 +134,12 @@ ${project.version} + + org.apache.logging.log4j + log4j-converter-config + ${project.version} + + org.apache.logging.log4j log4j-converter-plugin-descriptor From 201eb61bcf95fee7d8a623551f0c69a0a00bf2d8 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Tue, 19 Nov 2024 18:50:13 +0100 Subject: [PATCH 2/5] Close XMLStreamWriter --- .../config/internal/v2/XmlConfigurationMapper.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/XmlConfigurationMapper.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/XmlConfigurationMapper.java index 580b94b2..5956a035 100644 --- a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/XmlConfigurationMapper.java +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/XmlConfigurationMapper.java @@ -78,8 +78,8 @@ public ConfigurationNode parse(InputStream inputStream) throws IOException { @Override public void writeConfiguration(OutputStream outputStream, ConfigurationNode configuration) throws IOException { + XMLStreamWriter streamWriter = createStreamWriter(outputStream); try { - XMLStreamWriter streamWriter = createStreamWriter(outputStream); streamWriter.writeStartDocument(); streamWriter.setDefaultNamespace(LOG4J_NAMESPACE_URI); streamWriter.writeStartElement(LOG4J_NAMESPACE_URI, configuration.getPluginName()); @@ -92,8 +92,19 @@ public void writeConfiguration(OutputStream outputStream, ConfigurationNode conf writeNodeContentToStreamWriter(configuration, streamWriter); streamWriter.writeEndElement(); streamWriter.writeEndDocument(); + streamWriter.flush(); } catch (XMLStreamException e) { throw new IOException("Unable to write configuration.", e); + } finally { + closeStreamWriter(streamWriter); + } + } + + private static void closeStreamWriter(XMLStreamWriter streamWriter) throws IOException { + try { + streamWriter.close(); + } catch (XMLStreamException e) { + throw new IOException("Unable to close stream writer.", e); } } From dd18e90ec6fc6ba6dea89c7eca2545f0df058483 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Sat, 23 Nov 2024 12:29:27 +0100 Subject: [PATCH 3/5] Extract some common utils --- log4j-converter-config/pom.xml | 2 +- .../config/internal/ComponentUtils.java | 156 ++++++++++++++++++ .../internal/ConfigurationNodeImpl.java | 130 --------------- .../converter/config/internal/XmlUtils.java | 144 ++++++++++++++++ .../AbstractJacksonConfigurationMapper.java | 9 +- .../v2/PropertiesV2ConfigurationParser.java | 47 +++--- .../internal/v2/XmlConfigurationMapper.java | 77 +-------- .../AbstractConfigurationMapperTest.java | 2 +- .../resources/log4j2.component.properties | 19 +++ .../resources/log4j2.simplelog.properties | 19 +++ 10 files changed, 372 insertions(+), 233 deletions(-) create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/ComponentUtils.java delete mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/ConfigurationNodeImpl.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/XmlUtils.java create mode 100644 log4j-converter-config/src/test/resources/log4j2.component.properties create mode 100644 log4j-converter-config/src/test/resources/log4j2.simplelog.properties diff --git a/log4j-converter-config/pom.xml b/log4j-converter-config/pom.xml index fe2cf33d..6260560e 100644 --- a/log4j-converter-config/pom.xml +++ b/log4j-converter-config/pom.xml @@ -25,7 +25,7 @@ log4j-converter-config - Apache Log4j Configuration converter + Apache Log4j Configuration Converter Converts various logging configuration formats to the Log4j Core 2.x format. diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/ComponentUtils.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/ComponentUtils.java new file mode 100644 index 00000000..6940648c --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/ComponentUtils.java @@ -0,0 +1,156 @@ +/* + * 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.converter.config.internal; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.Supplier; +import org.apache.logging.converter.config.ConfigurationConverterException; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.jspecify.annotations.Nullable; + +public final class ComponentUtils { + + public static ConfigurationNodeBuilder newNodeBuilder() { + return new ConfigurationNodeBuilder(); + } + + public static ConfigurationNode createThresholdFilter(String level) { + return newNodeBuilder() + .setPluginName("ThresholdFilter") + .addAttribute("level", level) + .build(); + } + + public static ConfigurationNode createCompositeFilter(Iterable filters) { + ConfigurationNodeBuilder builder = newNodeBuilder().setPluginName("Filters"); + filters.forEach(builder::addChild); + return builder.build(); + } + + private ComponentUtils() {} + + public static class ConfigurationNodeBuilder implements Supplier { + + private @Nullable String pluginName; + private final Map attributes = new TreeMap<>(); + private final List children = new ArrayList<>(); + + protected ConfigurationNodeBuilder() {} + + public ConfigurationNodeBuilder setPluginName(String pluginName) { + this.pluginName = pluginName; + return this; + } + + public ConfigurationNodeBuilder addAttribute(String key, @Nullable String value) { + if (value != null) { + attributes.put(key, value); + } + return this; + } + + public ConfigurationNodeBuilder addAttribute(String key, boolean value) { + attributes.put(key, String.valueOf(value)); + return this; + } + + public ConfigurationNodeBuilder addChild(ConfigurationNode child) { + children.add(child); + return this; + } + + public ConfigurationNode build() { + if (pluginName == null) { + throw new ConfigurationConverterException("No plugin name specified"); + } + return new ConfigurationNodeImpl(pluginName, attributes, children); + } + + @Override + public ConfigurationNode get() { + return build(); + } + } + + private static final class ConfigurationNodeImpl implements ConfigurationNode { + + private final String pluginName; + private final Map attributes; + private final List children; + + private ConfigurationNodeImpl( + final String pluginName, + final Map attributes, + final Collection children) { + this.pluginName = pluginName; + this.attributes = Collections.unmodifiableMap(new TreeMap<>(attributes)); + this.children = Collections.unmodifiableList(new ArrayList<>(children)); + } + + @Override + public String getPluginName() { + return pluginName; + } + + @Override + public Map getAttributes() { + return attributes; + } + + @Override + public List getChildren() { + return children; + } + + private static void formatTo(ConfigurationNode node, StringBuilder builder, int indent) { + String indentation = getIndentation(indent); + builder.append(indentation).append("<").append(node.getPluginName()); + for (final Map.Entry entry : node.getAttributes().entrySet()) { + builder.append(" ") + .append(entry.getKey()) + .append("=\"") + .append(entry.getValue()) + .append("\""); + } + builder.append(">\n"); + for (ConfigurationNode child : node.getChildren()) { + formatTo(child, builder, indent + 1); + builder.append('\n'); + } + builder.append(indentation) + .append(""); + } + + private static String getIndentation(int indent) { + return String.join("", Collections.nCopies(indent, " ")); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + formatTo(this, builder, 0); + return builder.toString(); + } + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/ConfigurationNodeImpl.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/ConfigurationNodeImpl.java deleted file mode 100644 index 07e01438..00000000 --- a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/ConfigurationNodeImpl.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to you under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.logging.converter.config.internal; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; -import java.util.function.Supplier; -import org.apache.logging.converter.config.ConfigurationConverterException; -import org.apache.logging.converter.config.spi.ConfigurationNode; -import org.jspecify.annotations.Nullable; - -public final class ConfigurationNodeImpl implements ConfigurationNode { - - private final String pluginName; - private final Map attributes = new TreeMap<>(); - private final List children = new ArrayList<>(); - - public static NodeBuilder newNodeBuilder() { - return new NodeBuilder(); - } - - private ConfigurationNodeImpl( - final String pluginName, - final Map attributes, - final Collection children) { - this.pluginName = pluginName; - this.attributes.putAll(attributes); - this.children.addAll(children); - } - - @Override - public String getPluginName() { - return pluginName; - } - - @Override - public Map getAttributes() { - return Collections.unmodifiableMap(attributes); - } - - @Override - public List getChildren() { - return Collections.unmodifiableList(children); - } - - private static void formatTo(ConfigurationNode node, StringBuilder builder, int indent) { - String indentation = getIndentation(indent); - builder.append(indentation).append("<").append(node.getPluginName()); - for (final Map.Entry entry : node.getAttributes().entrySet()) { - builder.append(" ") - .append(entry.getKey()) - .append("=\"") - .append(entry.getValue()) - .append("\""); - } - builder.append(">\n"); - for (ConfigurationNode child : node.getChildren()) { - formatTo(child, builder, indent + 1); - builder.append('\n'); - } - builder.append(indentation).append(""); - } - - private static String getIndentation(int indent) { - return String.join("", Collections.nCopies(indent, " ")); - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - formatTo(this, builder, 0); - return builder.toString(); - } - - public static final class NodeBuilder implements Supplier { - - private @Nullable String pluginName; - private final Map attributes = new TreeMap<>(); - private final List children = new ArrayList<>(); - - private NodeBuilder() {} - - public NodeBuilder setPluginName(String pluginName) { - this.pluginName = pluginName; - return this; - } - - public NodeBuilder addAttribute(String key, @Nullable String value) { - if (value != null) { - attributes.put(key, value); - } - return this; - } - - public NodeBuilder addChild(ConfigurationNode child) { - children.add(child); - return this; - } - - public ConfigurationNode build() { - if (pluginName == null) { - throw new ConfigurationConverterException("No plugin name specified"); - } - return new ConfigurationNodeImpl(pluginName, attributes, children); - } - - @Override - public ConfigurationNode get() { - return build(); - } - } -} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/XmlUtils.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/XmlUtils.java new file mode 100644 index 00000000..6b4b85d8 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/XmlUtils.java @@ -0,0 +1,144 @@ +/* + * 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.converter.config.internal; + +import java.io.IOException; +import java.util.function.Consumer; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.ErrorHandler; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +public final class XmlUtils { + + private static final String DISABLE_DOCTYPE_DECLARATION = "http://apache.org/xml/features/disallow-doctype-decl"; + private static final String EXTERNAL_GENERAL_ENTITIES = "http://xml.org/sax/features/external-general-entities"; + private static final String EXTERNAL_PARAMETER_ENTITIES = "http://xml.org/sax/features/external-parameter-entities"; + private static final String LOAD_EXTERNAL_DTD = "http://apache.org/xml/features/nonvalidating/load-external-dtd"; + + public static DocumentBuilder createDocumentBuilderV1() throws IOException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(false); + enableFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING); + disableFeature(factory, EXTERNAL_GENERAL_ENTITIES); + disableFeature(factory, EXTERNAL_PARAMETER_ENTITIES); + disableFeature(factory, LOAD_EXTERNAL_DTD); + disableXIncludeAware(factory); + factory.setExpandEntityReferences(false); + factory.setValidating(false); + return newDocumentBuilder(factory); + } + + public static DocumentBuilder createDocumentBuilderV2() throws IOException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + enableFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING); + enableFeature(factory, DISABLE_DOCTYPE_DECLARATION); + disableXIncludeAware(factory); + return newDocumentBuilder(factory); + } + + private static void disableXIncludeAware(DocumentBuilderFactory factory) throws IOException { + try { + factory.setXIncludeAware(false); + } catch (UnsupportedOperationException e) { + throw new IOException( + "Failed to disable XInclude on DocumentBuilderFactory " + + factory.getClass().getName(), + e); + } + } + + private static DocumentBuilder newDocumentBuilder(DocumentBuilderFactory factory) throws IOException { + try { + DocumentBuilder builder = factory.newDocumentBuilder(); + // The default error handler pollutes `System.err` + builder.setErrorHandler(new ThrowingErrorHandler()); + return builder; + } catch (ParserConfigurationException e) { + throw new IOException( + "Failed to create DocumentBuilder using DocumentBuilderFactory " + + factory.getClass().getName(), + e); + } + } + + public static Stream childStream(Node parent) { + NodeList list = parent.getChildNodes(); + return IntStream.range(0, list.getLength()) + .mapToObj(list::item) + .filter(node -> node.getNodeType() == Node.ELEMENT_NODE) + .map(Element.class::cast); + } + + public static void forEachChild(Node parent, Consumer consumer) { + childStream(parent).forEach(consumer); + } + + private static void enableFeature(DocumentBuilderFactory factory, String feature) throws IOException { + setFeature(factory, feature, true); + } + + private static void disableFeature(DocumentBuilderFactory factory, String feature) throws IOException { + setFeature(factory, feature, false); + } + + private static void setFeature(DocumentBuilderFactory factory, String feature, boolean value) throws IOException { + try { + factory.setFeature(feature, value); + } catch (ParserConfigurationException e) { + throw new IOException( + "Failed to enable '" + feature + "' feature on DocumentBuilderFactory " + + factory.getClass().getName(), + e); + } + } + + private XmlUtils() {} + + private static class ThrowingErrorHandler implements ErrorHandler { + @Override + public void warning(SAXParseException exception) {} + + @Override + public void error(SAXParseException exception) throws SAXException { + throwOnParseException(exception); + } + + @Override + public void fatalError(SAXParseException exception) throws SAXException { + throwOnParseException(exception); + } + + private void throwOnParseException(SAXParseException exception) throws SAXException { + IOException ioException = new IOException( + String.format( + "Invalid configuration file content at line %d, column %d: %s", + exception.getLineNumber(), exception.getColumnNumber(), exception.getMessage()), + exception); + throw new SAXException(ioException); + } + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/AbstractJacksonConfigurationMapper.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/AbstractJacksonConfigurationMapper.java index 6540fd2e..d9708f3a 100644 --- a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/AbstractJacksonConfigurationMapper.java +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/AbstractJacksonConfigurationMapper.java @@ -16,6 +16,8 @@ */ package org.apache.logging.converter.config.internal.v2; +import static org.apache.logging.converter.config.internal.ComponentUtils.newNodeBuilder; + import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -28,7 +30,7 @@ import java.util.Collection; import java.util.HashSet; import java.util.Iterator; -import org.apache.logging.converter.config.internal.ConfigurationNodeImpl; +import org.apache.logging.converter.config.internal.ComponentUtils.ConfigurationNodeBuilder; import org.apache.logging.converter.config.spi.ConfigurationMapper; import org.apache.logging.converter.config.spi.ConfigurationNode; import org.jspecify.annotations.Nullable; @@ -74,7 +76,7 @@ public void writeConfiguration(OutputStream outputStream, ConfigurationNode conf } private static ConfigurationNode parseObjectNode(ObjectNode objectNode, String fieldName) { - ConfigurationNodeImpl.NodeBuilder builder = ConfigurationNodeImpl.newNodeBuilder(); + ConfigurationNodeBuilder builder = newNodeBuilder(); objectNode.fields().forEachRemaining(entry -> { String childFieldName = entry.getKey(); JsonNode childNode = entry.getValue(); @@ -91,8 +93,7 @@ private static ConfigurationNode parseObjectNode(ObjectNode objectNode, String f return builder.setPluginName(getPluginName(objectNode, fieldName)).build(); } - private static void processArrayNode( - ArrayNode arrayNode, String fieldName, ConfigurationNodeImpl.NodeBuilder builder) { + private static void processArrayNode(ArrayNode arrayNode, String fieldName, ConfigurationNodeBuilder builder) { arrayNode.elements().forEachRemaining(childNode -> { if (childNode.isObject()) { builder.addChild(parseObjectNode((ObjectNode) childNode, fieldName)); diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/PropertiesV2ConfigurationParser.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/PropertiesV2ConfigurationParser.java index e6a22be1..c46595c2 100644 --- a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/PropertiesV2ConfigurationParser.java +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/PropertiesV2ConfigurationParser.java @@ -16,6 +16,7 @@ */ package org.apache.logging.converter.config.internal.v2; +import static org.apache.logging.converter.config.internal.ComponentUtils.newNodeBuilder; import static org.apache.logging.log4j.util.PropertiesUtil.extractSubset; import static org.apache.logging.log4j.util.PropertiesUtil.partitionOnCommonPrefixes; @@ -29,7 +30,7 @@ import java.util.TreeMap; import java.util.function.Supplier; import org.apache.logging.converter.config.ConfigurationConverterException; -import org.apache.logging.converter.config.internal.ConfigurationNodeImpl; +import org.apache.logging.converter.config.internal.ComponentUtils.ConfigurationNodeBuilder; import org.apache.logging.converter.config.spi.ConfigurationNode; import org.apache.logging.converter.config.spi.ConfigurationParser; import org.apache.logging.log4j.util.Strings; @@ -73,8 +74,7 @@ public String getInputFormat() { public ConfigurationNode parse(InputStream inputStream) throws IOException { Properties rootProperties = new Properties(); rootProperties.load(inputStream); - ConfigurationNodeImpl.NodeBuilder builder = - ConfigurationNodeImpl.newNodeBuilder().setPluginName(CONFIGURATION_PLUGIN_NAME); + ConfigurationNodeBuilder builder = newNodeBuilder().setPluginName(CONFIGURATION_PLUGIN_NAME); for (final String key : rootProperties.stringPropertyNames()) { if (!key.contains(".")) { @@ -107,8 +107,7 @@ public ConfigurationNode parse(InputStream inputStream) throws IOException { builder.addChild(processAppenders(appenders)); } - ConfigurationNodeImpl.NodeBuilder loggersBuilder = - ConfigurationNodeImpl.newNodeBuilder().setPluginName(LOGGERS_PLUGIN_NAME); + ConfigurationNodeBuilder loggersBuilder = newNodeBuilder().setPluginName(LOGGERS_PLUGIN_NAME); // 1. Start with the root logger Properties rootLoggerProperties = extractSubset(rootProperties, "rootLogger"); String rootLoggerProperty = rootProperties.getProperty("rootLogger"); @@ -149,10 +148,9 @@ private static Map extractSubsetAndPartition( } private static ConfigurationNode processPropertyPlaceholders(final Properties propertyPlaceholders) { - ConfigurationNodeImpl.NodeBuilder builder = - ConfigurationNodeImpl.newNodeBuilder().setPluginName(PROPERTIES_PLUGIN_NAME); + ConfigurationNodeBuilder builder = newNodeBuilder().setPluginName(PROPERTIES_PLUGIN_NAME); for (final String key : propertyPlaceholders.stringPropertyNames()) { - builder.addChild(ConfigurationNodeImpl.newNodeBuilder() + builder.addChild(newNodeBuilder() .setPluginName(PROPERTY_PLUGIN_NAME) .addAttribute(NAME_ATTRIBUTE, key) .addAttribute(VALUE_ATTRIBUTE, propertyPlaceholders.getProperty(key)) @@ -162,8 +160,7 @@ private static ConfigurationNode processPropertyPlaceholders(final Properties pr } private ConfigurationNode processScripts(Map scripts) { - ConfigurationNodeImpl.NodeBuilder builder = - ConfigurationNodeImpl.newNodeBuilder().setPluginName(SCRIPTS_PLUGIN_NAME); + ConfigurationNodeBuilder builder = newNodeBuilder().setPluginName(SCRIPTS_PLUGIN_NAME); for (final Map.Entry entry : scripts.entrySet()) { String scriptPrefix = "script." + entry.getKey(); Properties scriptProperties = entry.getValue(); @@ -173,11 +170,10 @@ private ConfigurationNode processScripts(Map scripts) { } private ConfigurationNode processCustomLevels(Properties customLevels) { - ConfigurationNodeImpl.NodeBuilder builder = - ConfigurationNodeImpl.newNodeBuilder().setPluginName(CUSTOM_LEVELS_PLUGIN_NAME); + ConfigurationNodeBuilder builder = newNodeBuilder().setPluginName(CUSTOM_LEVELS_PLUGIN_NAME); for (final String key : customLevels.stringPropertyNames()) { String value = validateInteger("customLevel." + key, customLevels.getProperty(key)); - builder.addChild(ConfigurationNodeImpl.newNodeBuilder() + builder.addChild(newNodeBuilder() .setPluginName(CUSTOM_LEVEL_PLUGIN_NAME) .addAttribute(NAME_ATTRIBUTE, key) .addAttribute(INT_LEVEL_ATTRIBUTE, value) @@ -199,8 +195,7 @@ private static ConfigurationNode processFilters(String prefix, Map filterEntry : filters.entrySet()) { builder.addChild(processFilter(prefix, filterEntry)); } @@ -215,8 +210,7 @@ private static ConfigurationNode processFilter(String prefix, Map.Entry appenders) { - ConfigurationNodeImpl.NodeBuilder builder = - ConfigurationNodeImpl.newNodeBuilder().setPluginName(APPENDERS_PLUGIN_NAME); + ConfigurationNodeBuilder builder = newNodeBuilder().setPluginName(APPENDERS_PLUGIN_NAME); for (Map.Entry entry : appenders.entrySet()) { builder.addChild(processAppender(entry.getKey(), entry.getValue())); } @@ -225,7 +219,7 @@ private static ConfigurationNode processAppenders(Map append private static ConfigurationNode processAppender(String key, Properties properties) { String appenderPrefix = "appender." + key; - ConfigurationNodeImpl.NodeBuilder builder = ConfigurationNodeImpl.newNodeBuilder() + ConfigurationNodeBuilder builder = newNodeBuilder() .setPluginName(getRequiredAttribute( properties, TYPE_ATTRIBUTE, () -> "No type attribute provided for Appender " + appenderPrefix)) .addAttribute( @@ -242,7 +236,7 @@ private static ConfigurationNode processAppender(String key, Properties properti } private static ConfigurationNode processLogger(String key, Properties properties) { - ConfigurationNodeImpl.NodeBuilder builder = ConfigurationNodeImpl.newNodeBuilder() + ConfigurationNodeBuilder builder = newNodeBuilder() .addAttribute(LEVEL_AND_REFS_ATTRIBUTE, remove(properties, "")) .addAttribute( NAME_ATTRIBUTE, @@ -267,7 +261,7 @@ private static ConfigurationNode processLogger(String key, Properties properties } private static void addAppenderRefsToComponent( - String prefix, Properties properties, ConfigurationNodeImpl.NodeBuilder builder) { + String prefix, Properties properties, ConfigurationNodeBuilder builder) { Map appenderRefs = extractSubsetAndPartition(properties, "appenderRef"); for (final Map.Entry entry : appenderRefs.entrySet()) { builder.addChild(processAppenderRef(prefix + ".appenderRef." + entry.getKey(), entry.getValue())); @@ -275,7 +269,7 @@ private static void addAppenderRefsToComponent( } private static ConfigurationNode processAppenderRef(String prefix, Properties properties) { - ConfigurationNodeImpl.NodeBuilder builder = ConfigurationNodeImpl.newNodeBuilder() + ConfigurationNodeBuilder builder = newNodeBuilder() .setPluginName(APPENDER_REF_PLUGIN_NAME) .addAttribute( REF_ATTRIBUTE, @@ -295,8 +289,7 @@ private static ConfigurationNode processAppenderRef(String prefix, Properties pr return builder.build(); } - private static void addFiltersToComponent( - String prefix, Properties properties, ConfigurationNodeImpl.NodeBuilder builder) { + private static void addFiltersToComponent(String prefix, Properties properties, ConfigurationNodeBuilder builder) { Map filters = extractSubsetAndPartition(properties, "filter"); if (!filters.isEmpty()) { builder.addChild(processFilters(prefix, filters)); @@ -304,8 +297,8 @@ private static void addFiltersToComponent( } private static ConfigurationNode processRootLogger(Properties properties) { - ConfigurationNodeImpl.NodeBuilder builder = - ConfigurationNodeImpl.newNodeBuilder().addAttribute(LEVEL_AND_REFS_ATTRIBUTE, remove(properties, "")); + ConfigurationNodeBuilder builder = + newNodeBuilder().addAttribute(LEVEL_AND_REFS_ATTRIBUTE, remove(properties, "")); String type = remove(properties, TYPE_ATTRIBUTE); if (ASYNC_ROOT_PLUGIN_NAME.equalsIgnoreCase(type)) { @@ -340,7 +333,7 @@ private static ConfigurationNode processRootLogger(Properties properties) { */ private static ConfigurationNode processGenericComponent( String prefix, String componentCategory, Properties properties) { - ConfigurationNodeImpl.NodeBuilder builder = ConfigurationNodeImpl.newNodeBuilder(); + ConfigurationNodeBuilder builder = newNodeBuilder(); builder.setPluginName(getRequiredAttribute( properties, @@ -351,7 +344,7 @@ private static ConfigurationNode processGenericComponent( } private static void processRemainingProperties( - String prefix, Properties properties, ConfigurationNodeImpl.NodeBuilder builder) { + String prefix, Properties properties, ConfigurationNodeBuilder builder) { while (!properties.isEmpty()) { String propertyName = properties.stringPropertyNames().iterator().next(); int index = propertyName.indexOf('.'); diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/XmlConfigurationMapper.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/XmlConfigurationMapper.java index 5956a035..bef005c6 100644 --- a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/XmlConfigurationMapper.java +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/XmlConfigurationMapper.java @@ -25,13 +25,13 @@ import java.util.Map; import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; import org.apache.logging.converter.config.ConfigurationConverterException; -import org.apache.logging.converter.config.internal.ConfigurationNodeImpl; +import org.apache.logging.converter.config.internal.ComponentUtils; +import org.apache.logging.converter.config.internal.ComponentUtils.ConfigurationNodeBuilder; +import org.apache.logging.converter.config.internal.XmlUtils; import org.apache.logging.converter.config.spi.ConfigurationMapper; import org.apache.logging.converter.config.spi.ConfigurationNode; import org.jspecify.annotations.Nullable; @@ -41,14 +41,11 @@ import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; -import org.xml.sax.ErrorHandler; import org.xml.sax.SAXException; -import org.xml.sax.SAXParseException; @ServiceProvider(value = ConfigurationMapper.class, resolution = Resolution.MANDATORY) public class XmlConfigurationMapper implements ConfigurationMapper { - private static final String DISABLE_DOCTYPE_DECLARATION = "http://apache.org/xml/features/disallow-doctype-decl"; private static final String LOG4J_NAMESPACE_URI = "https://logging.apache.org/xml/ns"; private static final String LOG4J_SCHEMA_LOCATION = LOG4J_NAMESPACE_URI + " https://logging.apache.org/xml/ns/log4j-config-2.xsd"; @@ -56,9 +53,7 @@ public class XmlConfigurationMapper implements ConfigurationMapper { @Override public ConfigurationNode parse(InputStream inputStream) throws IOException { - DocumentBuilder documentBuilder = createDocumentBuilder(); - // The default error handler pollutes `System.err` - documentBuilder.setErrorHandler(new ThrowingErrorHandler()); + DocumentBuilder documentBuilder = XmlUtils.createDocumentBuilderV2(); try { Document document = documentBuilder.parse(inputStream); Element configurationElement = document.getDocumentElement(); @@ -113,29 +108,6 @@ public String getFormat() { return LOG4J_V2_XML_FORMAT; } - private static DocumentBuilder createDocumentBuilder() throws IOException { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setNamespaceAware(true); - enableFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING); - enableFeature(factory, DISABLE_DOCTYPE_DECLARATION); - try { - factory.setXIncludeAware(false); - } catch (UnsupportedOperationException e) { - throw new IOException( - "Failed to disable XInclude on DocumentBuilderFactory " - + factory.getClass().getName(), - e); - } - try { - return factory.newDocumentBuilder(); - } catch (ParserConfigurationException e) { - throw new IOException( - "Failed to create DocumentBuilder using DocumentBuilderFactory " - + factory.getClass().getName(), - e); - } - } - private static XMLStreamWriter createStreamWriter(OutputStream outputStream) throws IOException { XMLOutputFactory outputFactory = XMLOutputFactory.newFactory(); try { @@ -149,22 +121,11 @@ private static XMLStreamWriter createStreamWriter(OutputStream outputStream) thr } } - private static void enableFeature(DocumentBuilderFactory factory, String feature) throws IOException { - try { - factory.setFeature(feature, true); - } catch (ParserConfigurationException e) { - throw new IOException( - "Failed to enable '" + feature + "' feature on DocumentBuilderFactory " - + factory.getClass().getName(), - e); - } - } - /** * Transforms an XML element into a Log4j configuration node. */ private static ConfigurationNode parseComplexElement(Element element) { - ConfigurationNodeImpl.NodeBuilder builder = ConfigurationNodeImpl.newNodeBuilder(); + ConfigurationNodeBuilder builder = ComponentUtils.newNodeBuilder(); // Handle child elements NodeList childNodes = element.getChildNodes(); for (int i = 0; i < childNodes.getLength(); i++) { @@ -182,7 +143,7 @@ private static ConfigurationNode parseComplexElement(Element element) { * @param childNode An XML node. * @param builder A {@link ConfigurationNode} builder. */ - private static void processChildElement(org.w3c.dom.Node childNode, ConfigurationNodeImpl.NodeBuilder builder) { + private static void processChildElement(org.w3c.dom.Node childNode, ConfigurationNodeBuilder builder) { if (isLog4jNamespace(childNode) && childNode instanceof Element) { Element childElement = (Element) childNode; if (isComplexElement(childElement)) { @@ -225,7 +186,7 @@ private static String getSimpleElementValue(org.w3c.dom.Node element) { * @param nodeMap A collection of XML attributes. * @param builder A {@link ConfigurationNode} builder. */ - private static void processAttributes(NamedNodeMap nodeMap, ConfigurationNodeImpl.NodeBuilder builder) { + private static void processAttributes(NamedNodeMap nodeMap, ConfigurationNodeBuilder builder) { for (int i = 0; i < nodeMap.getLength(); i++) { org.w3c.dom.Node item = nodeMap.item(i); if (isLog4jNamespace(item) && item instanceof Attr) { @@ -265,28 +226,4 @@ private static boolean isLog4jNamespace(Node node) { @Nullable String namespaceUri = node.getNamespaceURI(); return namespaceUri == null || namespaceUri.equals(LOG4J_NAMESPACE_URI); } - - private static class ThrowingErrorHandler implements ErrorHandler { - @Override - public void warning(SAXParseException exception) {} - - @Override - public void error(SAXParseException exception) throws SAXException { - throwOnParseException(exception); - } - - @Override - public void fatalError(SAXParseException exception) throws SAXException { - throwOnParseException(exception); - } - - private void throwOnParseException(SAXParseException exception) throws SAXException { - IOException ioException = new IOException( - String.format( - "Invalid configuration file content at line %d, column %d: %s", - exception.getLineNumber(), exception.getColumnNumber(), exception.getMessage()), - exception); - throw new SAXException(ioException); - } - } } diff --git a/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/AbstractConfigurationMapperTest.java b/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/AbstractConfigurationMapperTest.java index d7edda06..3317ef22 100644 --- a/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/AbstractConfigurationMapperTest.java +++ b/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/AbstractConfigurationMapperTest.java @@ -16,7 +16,7 @@ */ package org.apache.logging.converter.config.internal; -import static org.apache.logging.converter.config.internal.ConfigurationNodeImpl.newNodeBuilder; +import static org.apache.logging.converter.config.internal.ComponentUtils.newNodeBuilder; import java.util.ArrayList; import java.util.Comparator; diff --git a/log4j-converter-config/src/test/resources/log4j2.component.properties b/log4j-converter-config/src/test/resources/log4j2.component.properties new file mode 100644 index 00000000..8b7afc58 --- /dev/null +++ b/log4j-converter-config/src/test/resources/log4j2.component.properties @@ -0,0 +1,19 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to you under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +## +# +log4j.provider = org.apache.logging.log4j.simple.internal.SimpleProvider diff --git a/log4j-converter-config/src/test/resources/log4j2.simplelog.properties b/log4j-converter-config/src/test/resources/log4j2.simplelog.properties new file mode 100644 index 00000000..fbc9b6d9 --- /dev/null +++ b/log4j-converter-config/src/test/resources/log4j2.simplelog.properties @@ -0,0 +1,19 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to you under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +## +# +log4j2.simplelog.level = INFO From 8ad35b907cf8f8140c62a6a0aa78b5d427709369 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Sat, 23 Nov 2024 12:38:33 +0100 Subject: [PATCH 4/5] Add support for Log4j 1 configuration formats This adds support for Log4j 1 configuration formats. Since Log4j Core 2 is a complete rewrite for Log4j 1, the converter: - Needs to know exactly what configuration parameters a Log4j 1 component supports to create an equivalent Log4j Core 2 configuration. - Introduces a pluggable `spi/v1/Log4j1ComponentParser` interface. For each supported Log4j 1 component, an implementation of this interface must be provided and registered with `ServiceLoader`. The following Log4j 1 components are currently supported: - Appenders: `ConsoleAppender`, `DailyRollingFileAppender`, `FileAppender` and `RollingFileAppender`. - Filters: `DenyAllFilter`, `LevelMatchFilter`, `LevelRangeFilter` and `StringMatchFilter`. - Layouts: `HTMLLayout`, `PatternLayout`, `SimpleLayout`, `TTCCLayout`. Part of apache/logging-log4j2#3220 --- .../config/internal/ComponentUtils.java | 28 +- .../config/internal/PropertiesUtils.java | 98 +++++++ .../config/internal/StringUtils.java | 73 +++++ .../converter/config/internal/XmlUtils.java | 36 +++ .../internal/v1/AbstractComponentParser.java | 255 ++++++++++++++++++ .../v1/AbstractV1ConfigurationParser.java | 47 ++++ .../config/internal/v1/LoggerConfig.java | 94 +++++++ .../v1/PropertiesV1ConfigurationParser.java | 165 ++++++++++++ .../internal/v1/XmlV1ConfigurationParser.java | 154 +++++++++++ .../v1/appender/AbstractAppenderParser.java | 153 +++++++++++ .../appender/AbstractFileAppenderParser.java | 108 ++++++++ .../v1/appender/AsyncAppenderParser.java | 132 +++++++++ .../v1/appender/ConsoleAppenderParser.java | 112 ++++++++ .../DailyRollingFileAppenderParser.java | 92 +++++++ .../v1/appender/FileAppenderParser.java | 52 ++++ .../appender/RollingFileAppenderParser.java | 133 +++++++++ .../internal/v1/appender/package-info.java | 20 ++ .../v1/filter/AbstractFilterParser.java | 41 +++ .../v1/filter/DenyAllFilterParser.java | 68 +++++ .../v1/filter/LevelMatchFilterParser.java | 91 +++++++ .../v1/filter/LevelRangeFilterParser.java | 99 +++++++ .../v1/filter/StringMatchFilterParser.java | 94 +++++++ .../internal/v1/filter/package-info.java | 20 ++ .../layout/EnhancedPatternLayoutParser.java | 54 ++++ .../internal/v1/layout/HtmlLayoutParser.java | 92 +++++++ .../v1/layout/PatternLayoutParser.java | 82 ++++++ .../v1/layout/SimpleLayoutParser.java | 70 +++++ .../internal/v1/layout/TTCCLayoutParser.java | 164 +++++++++++ .../internal/v1/layout/package-info.java | 20 ++ .../config/internal/v1/package-info.java | 20 ++ .../AbstractJacksonConfigurationMapper.java | 2 +- .../v2/PropertiesV2ConfigurationParser.java | 36 +-- .../internal/v2/XmlConfigurationMapper.java | 2 +- .../config/spi/v1/Log4j1ComponentParser.java | 47 ++++ .../config/spi/v1/Log4j1ParserContext.java | 25 ++ .../config/spi/v1/PropertiesSubset.java | 54 ++++ .../config/ConfigurationConverterTest.java | 9 +- .../AbstractConfigurationMapperTest.java | 66 ++--- .../v1/AbstractV1ConfigurationParserTest.java | 229 ++++++++++++++++ .../PropertiesV1ConfigurationParserTest.java | 38 +++ .../v1/XmlV1ConfigurationParserTest.java | 38 +++ .../resources/v1/log4j-lowercase.properties | 118 ++++++++ .../src/test/resources/v1/log4j-lowercase.xml | 139 ++++++++++ .../resources/v1/log4j-uppercase.properties | 118 ++++++++ .../src/test/resources/v1/log4j-uppercase.xml | 139 ++++++++++ .../src/test/resources/v1/log4j.dtd | 237 ++++++++++++++++ 46 files changed, 3900 insertions(+), 64 deletions(-) create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/PropertiesUtils.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/StringUtils.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/AbstractComponentParser.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/AbstractV1ConfigurationParser.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/LoggerConfig.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/PropertiesV1ConfigurationParser.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/XmlV1ConfigurationParser.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/AbstractAppenderParser.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/AbstractFileAppenderParser.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/AsyncAppenderParser.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/ConsoleAppenderParser.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/DailyRollingFileAppenderParser.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/FileAppenderParser.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/RollingFileAppenderParser.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/package-info.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/filter/AbstractFilterParser.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/filter/DenyAllFilterParser.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/filter/LevelMatchFilterParser.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/filter/LevelRangeFilterParser.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/filter/StringMatchFilterParser.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/filter/package-info.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/layout/EnhancedPatternLayoutParser.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/layout/HtmlLayoutParser.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/layout/PatternLayoutParser.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/layout/SimpleLayoutParser.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/layout/TTCCLayoutParser.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/layout/package-info.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/package-info.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/v1/Log4j1ComponentParser.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/v1/Log4j1ParserContext.java create mode 100644 log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/v1/PropertiesSubset.java create mode 100644 log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v1/AbstractV1ConfigurationParserTest.java create mode 100644 log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v1/PropertiesV1ConfigurationParserTest.java create mode 100644 log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v1/XmlV1ConfigurationParserTest.java create mode 100644 log4j-converter-config/src/test/resources/v1/log4j-lowercase.properties create mode 100644 log4j-converter-config/src/test/resources/v1/log4j-lowercase.xml create mode 100644 log4j-converter-config/src/test/resources/v1/log4j-uppercase.properties create mode 100644 log4j-converter-config/src/test/resources/v1/log4j-uppercase.xml create mode 100644 log4j-converter-config/src/test/resources/v1/log4j.dtd diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/ComponentUtils.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/ComponentUtils.java index 6940648c..ade2f924 100644 --- a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/ComponentUtils.java +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/ComponentUtils.java @@ -33,17 +33,24 @@ public static ConfigurationNodeBuilder newNodeBuilder() { return new ConfigurationNodeBuilder(); } - public static ConfigurationNode createThresholdFilter(String level) { + public static ConfigurationNode newAppenderRef(String ref) { + return newNodeBuilder() + .setPluginName("AppenderRef") + .addAttribute("ref", ref) + .get(); + } + + public static ConfigurationNode newThresholdFilter(String level) { return newNodeBuilder() .setPluginName("ThresholdFilter") .addAttribute("level", level) - .build(); + .get(); } - public static ConfigurationNode createCompositeFilter(Iterable filters) { + public static ConfigurationNode newCompositeFilter(Iterable filters) { ConfigurationNodeBuilder builder = newNodeBuilder().setPluginName("Filters"); filters.forEach(builder::addChild); - return builder.build(); + return builder.get(); } private ComponentUtils() {} @@ -73,22 +80,23 @@ public ConfigurationNodeBuilder addAttribute(String key, boolean value) { return this; } + public ConfigurationNodeBuilder addAttribute(String key, int value) { + attributes.put(key, String.valueOf(value)); + return this; + } + public ConfigurationNodeBuilder addChild(ConfigurationNode child) { children.add(child); return this; } - public ConfigurationNode build() { + @Override + public ConfigurationNode get() { if (pluginName == null) { throw new ConfigurationConverterException("No plugin name specified"); } return new ConfigurationNodeImpl(pluginName, attributes, children); } - - @Override - public ConfigurationNode get() { - return build(); - } } private static final class ConfigurationNodeImpl implements ConfigurationNode { diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/PropertiesUtils.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/PropertiesUtils.java new file mode 100644 index 00000000..d2e3fdf1 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/PropertiesUtils.java @@ -0,0 +1,98 @@ +/* + * 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.converter.config.internal; + +import java.util.Map; +import java.util.Properties; +import java.util.stream.Stream; +import org.apache.logging.converter.config.ConfigurationConverterException; +import org.apache.logging.converter.config.spi.v1.PropertiesSubset; +import org.jspecify.annotations.Nullable; + +public final class PropertiesUtils { + + public static @Nullable String getAndRemove(Properties properties, String key) { + return (String) properties.remove(key); + } + + public static String getLastComponent(String name) { + int idx = name.lastIndexOf('.'); + return idx == -1 ? name : name.substring(idx + 1); + } + + public static Properties extractSubset(Properties properties, String prefix) { + Properties subset = org.apache.logging.log4j.util.PropertiesUtil.extractSubset(properties, prefix); + String value = getAndRemove(properties, prefix); + if (value != null) { + subset.setProperty("", value); + } + return subset; + } + + public static @Nullable String extractProperty(PropertiesSubset subset, String key) { + return (String) subset.getProperties().remove(key); + } + + public static PropertiesSubset extractSubset(PropertiesSubset parentSubset, String childPrefix) { + Properties parentProperties = parentSubset.getProperties(); + Properties properties = + org.apache.logging.log4j.util.PropertiesUtil.extractSubset(parentProperties, childPrefix); + String value = getAndRemove(parentProperties, childPrefix); + if (value != null) { + properties.setProperty("", value); + } + return PropertiesSubset.of(addPrefixes(parentSubset.getPrefix(), childPrefix), properties); + } + + public static Map partitionOnCommonPrefixes(Properties properties) { + return org.apache.logging.log4j.util.PropertiesUtil.partitionOnCommonPrefixes(properties, true); + } + + public static Stream partitionOnCommonPrefixes(PropertiesSubset parentSubset) { + String parentPrefix = parentSubset.getPrefix(); + String effectivePrefix = parentPrefix.isEmpty() ? parentPrefix : parentPrefix + "."; + return org.apache.logging.log4j.util.PropertiesUtil.partitionOnCommonPrefixes( + parentSubset.getProperties(), true) + .entrySet() + .stream() + .map(entry -> PropertiesSubset.of(effectivePrefix + entry.getKey(), entry.getValue())); + } + + private static String addPrefixes(String left, String right) { + return left.isEmpty() ? right : right.isEmpty() ? left : left + "." + right; + } + + public static void throwIfNotEmpty(PropertiesSubset subset) { + Properties properties = subset.getProperties(); + if (!properties.isEmpty()) { + String prefix = subset.getPrefix(); + if (properties.size() == 1) { + throw new ConfigurationConverterException("Unknown configuration property '" + + addPrefixes( + prefix, + properties.stringPropertyNames().iterator().next()) + "'."); + } + StringBuilder messageBuilder = new StringBuilder("Unknown configuration properties:"); + properties.stringPropertyNames().stream() + .map(k -> addPrefixes(prefix, k)) + .forEach(k -> messageBuilder.append("\n\t").append(k)); + throw new ConfigurationConverterException(messageBuilder.toString()); + } + } + + private PropertiesUtils() {} +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/StringUtils.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/StringUtils.java new file mode 100644 index 00000000..c0ebeb00 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/StringUtils.java @@ -0,0 +1,73 @@ +/* + * 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.converter.config.internal; + +import java.util.regex.Pattern; +import org.apache.logging.converter.config.ConfigurationConverterException; +import org.apache.logging.log4j.util.Strings; + +public final class StringUtils { + + private static final Pattern SUBSTITUTION_PATTERN = Pattern.compile("\\$\\{([^}]+)\\}"); + + public static final String FALSE = "false"; + public static final String TRUE = "true"; + + public static String capitalize(String value) { + if (Strings.isEmpty(value) || Character.isUpperCase(value.charAt(0))) { + return value; + } + final char[] chars = value.toCharArray(); + chars[0] = Character.toUpperCase(chars[0]); + return new String(chars); + } + + public static String decapitalize(String value) { + if (Strings.isEmpty(value) || Character.isLowerCase(value.charAt(0))) { + return value; + } + final char[] chars = value.toCharArray(); + chars[0] = Character.toLowerCase(chars[0]); + return new String(chars); + } + + public static boolean parseBoolean(String value) { + return Boolean.parseBoolean(value.trim()); + } + + public static int parseInteger(String value) { + try { + return Integer.parseInt(value.trim()); + } catch (NumberFormatException e) { + throw new ConfigurationConverterException("Invalid integer value: " + value, e); + } + } + + public static long parseLong(String value) { + try { + return Long.parseLong(value.trim()); + } catch (NumberFormatException e) { + throw new ConfigurationConverterException("Invalid long value: " + value, e); + } + } + + public static String convertPropertySubstitution(String value) { + return SUBSTITUTION_PATTERN.matcher(value).replaceAll("${sys:$1}"); + } + + private StringUtils() {} +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/XmlUtils.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/XmlUtils.java index 6b4b85d8..11653d55 100644 --- a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/XmlUtils.java +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/XmlUtils.java @@ -24,6 +24,7 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; +import org.apache.logging.converter.config.ConfigurationConverterException; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; @@ -60,6 +61,41 @@ public static DocumentBuilder createDocumentBuilderV2() throws IOException { return newDocumentBuilder(factory); } + /** + * Finds an XPath expression that helps to identify a node in an XML document + * + * @param node An XML node. + * @return An XPath expression. + */ + public static String getXPathExpression(Element node) { + String tagName = node.getTagName(); + // Position of the node among siblings + int position = 1; + Node sibling = node.getPreviousSibling(); + while (sibling != null) { + if (sibling instanceof Element && tagName.equals(((Element) sibling).getTagName())) { + position++; + } + sibling = sibling.getPreviousSibling(); + } + Node parent = node.getParentNode(); + String parentExpression = parent instanceof Element ? getXPathExpression((Element) parent) : ""; + return parentExpression + "/" + tagName + "[" + position + "]"; + } + + public static void throwUnknownElement(Element node) { + throw new ConfigurationConverterException("Unknown configuration element '" + getXPathExpression(node) + "'."); + } + + public static String requireNonEmpty(Element node, String attributeName) { + String value = node.getAttribute(attributeName); + if (value.isEmpty()) { + throw new ConfigurationConverterException("Missing required attribute '" + attributeName + + "' on configuration element '" + getXPathExpression(node) + "'."); + } + return value; + } + private static void disableXIncludeAware(DocumentBuilderFactory factory) throws IOException { try { factory.setXIncludeAware(false); diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/AbstractComponentParser.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/AbstractComponentParser.java new file mode 100644 index 00000000..2eb6ecc3 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/AbstractComponentParser.java @@ -0,0 +1,255 @@ +/* + * 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.converter.config.internal.v1; + +import static org.apache.logging.converter.config.internal.PropertiesUtils.extractProperty; +import static org.apache.logging.converter.config.internal.XmlUtils.requireNonEmpty; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Supplier; +import org.apache.logging.converter.config.ConfigurationConverterException; +import org.apache.logging.converter.config.internal.PropertiesUtils; +import org.apache.logging.converter.config.internal.StringUtils; +import org.apache.logging.converter.config.internal.XmlUtils; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.apache.logging.converter.config.spi.v1.Log4j1ComponentParser; +import org.apache.logging.converter.config.spi.v1.Log4j1ParserContext; +import org.apache.logging.converter.config.spi.v1.PropertiesSubset; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +/** + * Base class for Log4j 1 component parsers. + */ +public abstract class AbstractComponentParser> implements Log4j1ComponentParser { + + // XML tags + public static final String PARAM_TAG = "param"; + + // XML attributes + public static final String NAME_ATTR = "name"; + public static final String REF_ATTR = "ref"; + public static final String VALUE_ATTR = "value"; + + // V1 parameter names + protected static final String THRESHOLD_PARAM = "Threshold"; + + /** + * Creates a builder for the configuration node. + * + * @param element An XML element. + */ + protected abstract T createBuilder(Element element); + + /** + * Creates a builder for the configuration node. + * + * @param properties A subset of properties. + */ + protected abstract T createBuilder(PropertiesSubset properties); + + /** + * A map between Log4j 1 parameter names and setter methods on the builder. + */ + protected abstract Map getAttributeMap(); + + @Override + public final ConfigurationNode parseXml(Element element, Log4j1ParserContext context) + throws ConfigurationConverterException { + T builder = createBuilder(element); + handleChildrenElements(element, context, getAttributeMap(), builder); + return builder.get(); + } + + @Override + public final ConfigurationNode parseProperties(PropertiesSubset properties, Log4j1ParserContext context) + throws ConfigurationConverterException { + T builder = createBuilder(properties); + handleChildrenProperties(properties, context, getAttributeMap(), builder); + return builder.get(); + } + + /** + * Handles all child elements, except {@code }. + * + * @param childElement An XML element. + * @param context The parser context. + * @param componentBuilder A builder for the configuration node. + */ + protected void handleUnknownElement(Element childElement, Log4j1ParserContext context, T componentBuilder) { + XmlUtils.throwUnknownElement(childElement); + } + + /** + * Handles subsets of properties that are not known attributes. + * + * @param properties A subset of properties to handle. + * @param context The parser context. + * @param componentBuilder A builder for the configuration node. + */ + protected void handleUnknownProperties( + PropertiesSubset properties, Log4j1ParserContext context, T componentBuilder) { + PropertiesUtils.throwIfNotEmpty(properties); + } + + /** + * Handles configuration attributes and nested elements. + * + * @param element An XML element. + * @param context A reference to the parser context. + * @param attributeMap A map from attribute names to setters of the component builder. + * @param componentBuilder A component builder. + * @throws ConfigurationConverterException If a parsing error occurs. + */ + private void handleChildrenElements( + Node element, + Log4j1ParserContext context, + Map attributeMap, + T componentBuilder) + throws ConfigurationConverterException { + // Counts children by tag name for error handling purposes + XmlUtils.forEachChild(element, (Consumer) childElement -> { + String nodeName = childElement.getNodeName(); + if (nodeName.equals(PARAM_TAG)) { + // Handle attributes + String name = childElement.getAttribute(NAME_ATTR); + MethodHandle attributeSetter = attributeMap.get(name); + if (attributeSetter == null) { + attributeSetter = attributeMap.get(StringUtils.capitalize(name)); + } + if (attributeSetter != null) { + String value = childElement.getAttribute(VALUE_ATTR); + if (value.isEmpty()) { + throw new ConfigurationConverterException("No value specified for attribute " + name); + } + invokeAttributeSetter(componentBuilder, attributeSetter, name, value); + } else { + throw new ConfigurationConverterException("Unsupported configuration attribute " + name + + " at path " + XmlUtils.getXPathExpression(childElement) + "."); + } + } else { + handleUnknownElement(childElement, context, componentBuilder); + } + }); + } + + /** + * Handles configuration attributes and nested elements. + * + * @param properties A subset of properties. + * @param context A reference to the parser context. + * @param attributeMap A map from attribute names to setters of the component builder. + * @param componentBuilder A component builder. + * @throws ConfigurationConverterException If a parsing error occurs. + */ + private void handleChildrenProperties( + PropertiesSubset properties, + Log4j1ParserContext context, + Map attributeMap, + T componentBuilder) { + // Handle attributes + attributeMap.forEach((attributeName, attributeSetter) -> { + String value = extractProperty(properties, attributeName); + if (value == null) { + value = extractProperty(properties, StringUtils.decapitalize(attributeName)); + } + if (value != null) { + invokeAttributeSetter(componentBuilder, attributeSetter, attributeName, value); + } + }); + // Handle nested components + PropertiesUtils.partitionOnCommonPrefixes(properties) + .forEach(childProperties -> handleUnknownProperties(childProperties, context, componentBuilder)); + } + + private static void invokeAttributeSetter( + Supplier builder, MethodHandle setter, String name, String value) { + try { + setter.bindTo(builder).invokeExact(value); + } catch (Throwable e) { + throw new ConfigurationConverterException("Failed to set attribute " + name, e); + } + } + + protected static AttributeMapBuilder attributeMapBuilder( + Class> componentBuilderClass) { + return new AttributeMapBuilder(componentBuilderClass); + } + + protected static ConfigurationNode parseConfigurationElement(Log4j1ParserContext context, Element element) { + return context.getParserForClass(requireNonEmpty(element, "class")).parseXml(element, context); + } + + protected static ConfigurationNode parseConfigurationElement(Log4j1ParserContext context, PropertiesSubset subset) { + String className = extractProperty(subset, ""); + if (className == null) { + throw new ConfigurationConverterException("The required property '" + subset.getPrefix() + "' is missing."); + } + return context.getParserForClass(className).parseProperties(subset, context); + } + + protected static final class AttributeMapBuilder implements Supplier> { + + private static final MethodType ATTRIBUTE_SETTER = MethodType.methodType(void.class, String.class); + private static final MethodHandles.Lookup lookup = MethodHandles.publicLookup(); + private final Map map; + private final Class> componentBuilderClass; + + private AttributeMapBuilder(Class> componentBuilderClass) { + this.map = new HashMap<>(); + this.componentBuilderClass = componentBuilderClass; + } + + public AttributeMapBuilder add(String name) { + String setterName = getSetterName(name); + try { + map.put(name, lookup.findVirtual(componentBuilderClass, setterName, ATTRIBUTE_SETTER)); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new ExceptionInInitializerError(e); + } + return this; + } + + public AttributeMapBuilder addAll(Map attributes) { + map.putAll(attributes); + return this; + } + + private static String getSetterName(CharSequence value) { + StringBuilder builder = new StringBuilder("set"); + int len = value.length(); + if (len > 0) { + builder.append(Character.toUpperCase(value.charAt(0))); + } + if (len > 1) { + builder.append(value, 1, len); + } + return builder.toString(); + } + + @Override + public Map get() { + return Collections.unmodifiableMap(map); + } + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/AbstractV1ConfigurationParser.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/AbstractV1ConfigurationParser.java new file mode 100644 index 00000000..9121da6f --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/AbstractV1ConfigurationParser.java @@ -0,0 +1,47 @@ +/* + * 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.converter.config.internal.v1; + +import aQute.bnd.annotation.Cardinality; +import aQute.bnd.annotation.spi.ServiceConsumer; +import java.util.HashMap; +import java.util.Map; +import java.util.ServiceLoader; +import org.apache.logging.converter.config.ConfigurationConverterException; +import org.apache.logging.converter.config.spi.ConfigurationParser; +import org.apache.logging.converter.config.spi.v1.Log4j1ComponentParser; +import org.apache.logging.converter.config.spi.v1.Log4j1ParserContext; + +@ServiceConsumer(value = Log4j1ComponentParser.class, cardinality = Cardinality.MULTIPLE) +public abstract class AbstractV1ConfigurationParser implements ConfigurationParser, Log4j1ParserContext { + + private static final Map componentParsers = new HashMap<>(); + + protected AbstractV1ConfigurationParser() { + ServiceLoader.load(Log4j1ComponentParser.class) + .forEach(parser -> componentParsers.put(parser.getClassName(), parser)); + } + + @Override + public Log4j1ComponentParser getParserForClass(String className) { + Log4j1ComponentParser parser = componentParsers.get(className); + if (parser == null) { + throw new ConfigurationConverterException("Unsupported Log4j 1 component class: " + className); + } + return parser; + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/LoggerConfig.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/LoggerConfig.java new file mode 100644 index 00000000..d20dbe58 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/LoggerConfig.java @@ -0,0 +1,94 @@ +/* + * 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.converter.config.internal.v1; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import org.apache.logging.converter.config.internal.ComponentUtils; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.jspecify.annotations.Nullable; + +class LoggerConfig { + + private static final String INHERITED = "INHERITED"; + private static final String NULL = "NULL"; + + private final String name; + private @Nullable String level; + private final Collection appenderRefs = new ArrayList<>(); + private @Nullable Boolean additivity; + + LoggerConfig(String name) { + this.name = name; + } + + void setLevel(String level) { + this.level = INHERITED.equalsIgnoreCase(level) || NULL.equalsIgnoreCase(level) ? null : level; + } + + void addAppenderRef(String appenderRef) { + appenderRefs.add(appenderRef); + } + + void setLevelAndRefs(String levelAndRefs) { + String[] values = + Arrays.stream(levelAndRefs.split(",", -1)).map(String::trim).toArray(String[]::new); + if (values.length > 0) { + setLevel(values[0]); + } + appenderRefs.clear(); + for (int i = 1; i < values.length; i++) { + addAppenderRef(values[i]); + } + } + + void setAdditivity(String additivity) { + this.additivity = Boolean.parseBoolean(additivity); + } + + ConfigurationNode buildLogger() { + ComponentUtils.ConfigurationNodeBuilder builder = ComponentUtils.newNodeBuilder() + .setPluginName("Logger") + .addAttribute("name", name) + .addAttribute("level", level); + if (additivity != null) { + builder.addAttribute("additivity", additivity); + } + appenderRefs.forEach(ref -> builder.addChild(ComponentUtils.newAppenderRef(ref))); + return builder.get(); + } + + ConfigurationNode buildRoot() { + ComponentUtils.ConfigurationNodeBuilder builder = + ComponentUtils.newNodeBuilder().setPluginName("Root"); + fillRemainingParameters(builder); + return builder.get(); + } + + private void fillRemainingParameters(ComponentUtils.ConfigurationNodeBuilder builder) { + builder.addAttribute("level", level); + if (additivity != null) { + builder.addAttribute("additivity", additivity); + } + appenderRefs.forEach(ref -> { + ComponentUtils.ConfigurationNodeBuilder builder1 = + ComponentUtils.newNodeBuilder().setPluginName("AppenderRef").addAttribute("ref", ref); + builder.addChild(builder1.get()); + }); + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/PropertiesV1ConfigurationParser.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/PropertiesV1ConfigurationParser.java new file mode 100644 index 00000000..1c174c98 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/PropertiesV1ConfigurationParser.java @@ -0,0 +1,165 @@ +/* + * 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.converter.config.internal.v1; + +import static org.apache.logging.converter.config.internal.PropertiesUtils.extractProperty; +import static org.apache.logging.converter.config.internal.PropertiesUtils.extractSubset; +import static org.apache.logging.converter.config.internal.PropertiesUtils.partitionOnCommonPrefixes; + +import aQute.bnd.annotation.Resolution; +import aQute.bnd.annotation.spi.ServiceProvider; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import org.apache.logging.converter.config.ConfigurationConverterException; +import org.apache.logging.converter.config.internal.ComponentUtils; +import org.apache.logging.converter.config.internal.ComponentUtils.ConfigurationNodeBuilder; +import org.apache.logging.converter.config.internal.PropertiesUtils; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.apache.logging.converter.config.spi.ConfigurationParser; +import org.apache.logging.converter.config.spi.v1.PropertiesSubset; + +@ServiceProvider(value = ConfigurationParser.class, resolution = Resolution.MANDATORY) +public class PropertiesV1ConfigurationParser extends AbstractV1ConfigurationParser { + + private static final String ADDITIVITY_PREFIX = "log4j.additivity"; + private static final String APPENDER_PREFIX = "log4j.appender"; + private static final String CATEGORY_PREFIX = "log4j.category"; + private static final String LOGGER_PREFIX = "log4j.logger"; + + private static final String ROOT_CATEGORY_KEY = "log4j.rootCategory"; + private static final String ROOT_LOGGER_KEY = "log4j.rootLogger"; + private static final String THRESHOLD_KEY = "log4j.threshold"; + + private static final String LOG4J_V1_PROPERTIES_FORMAT = "v1:properties"; + + @Override + public String getInputFormat() { + return LOG4J_V1_PROPERTIES_FORMAT; + } + + @Override + public ConfigurationNode parse(InputStream inputStream) throws IOException { + Properties properties = new Properties(); + properties.load(inputStream); + return parse(PropertiesSubset.of("", properties)); + } + + private ConfigurationNode parse(PropertiesSubset properties) { + ConfigurationNodeBuilder builder = ComponentUtils.newNodeBuilder().setPluginName("Configuration"); + + String level = extractProperty(properties, THRESHOLD_KEY); + + PropertiesSubset appendersProperties = extractSubset(properties, APPENDER_PREFIX); + ConfigurationNode appenders = parseAppenders(appendersProperties); + + ConfigurationNodeBuilder loggersNodeBuilder = + ComponentUtils.newNodeBuilder().setPluginName("Loggers"); + loggersNodeBuilder.addChild(parseRootLogger(properties)); + parseLoggers(properties).forEach(loggersNodeBuilder::addChild); + ConfigurationNode loggers = loggersNodeBuilder.get(); + + // Whatever is left, are user properties + builder.addChild(parseProperties(properties)); + builder.addChild(appenders); + if (level != null) { + builder.addChild(ComponentUtils.newThresholdFilter(level)); + } + builder.addChild(loggers); + return builder.get(); + } + + private ConfigurationNode parseAppenders(PropertiesSubset appendersProperties) { + ConfigurationNodeBuilder appendersBuilder = + ComponentUtils.newNodeBuilder().setPluginName("Appenders"); + partitionOnCommonPrefixes(appendersProperties) + .forEach(appenderProperties -> appendersBuilder.addChild( + AbstractComponentParser.parseConfigurationElement(this, appenderProperties))); + return appendersBuilder.get(); + } + + private ConfigurationNode parseRootLogger(PropertiesSubset globalProperties) { + PropertiesSubset rootProperties = extractSubset(globalProperties, ROOT_LOGGER_KEY); + String levelAndRefs = extractProperty(rootProperties, ""); + PropertiesUtils.throwIfNotEmpty(rootProperties); + // Check rootCategory + rootProperties = extractSubset(rootProperties, ROOT_CATEGORY_KEY); + if (levelAndRefs == null) { + levelAndRefs = extractProperty(globalProperties, ""); + } + PropertiesUtils.throwIfNotEmpty(rootProperties); + if (levelAndRefs == null) { + throw new ConfigurationConverterException("No root logger configuration found!"); + } + LoggerConfig loggerConfig = new LoggerConfig(""); + loggerConfig.setLevelAndRefs(levelAndRefs); + return loggerConfig.buildRoot(); + } + + private List parseLoggers(PropertiesSubset globalProperties) { + List loggers = new ArrayList<>(); + Map loggerConfigs = new HashMap<>(); + // Handle `log4j.logger` + extractSubset(globalProperties, LOGGER_PREFIX) + .getProperties() + .forEach((key, levelAndRefs) -> loggerConfigs.compute((String) key, (name, oldConfig) -> { + LoggerConfig config = new LoggerConfig(name); + config.setLevelAndRefs((String) levelAndRefs); + return config; + })); + // Handler `log4j.catetory` + extractSubset(globalProperties, CATEGORY_PREFIX) + .getProperties() + .forEach((key, levelAndRefs) -> loggerConfigs.compute((String) key, (name, oldConfig) -> { + if (oldConfig != null) { + throw new ConfigurationConverterException(String.format( + "Configuration file contains both a '%s.%s' and '%s.%s' key.", + LOGGER_PREFIX, key, CATEGORY_PREFIX, key)); + } + LoggerConfig config = new LoggerConfig(name); + config.setLevelAndRefs((String) levelAndRefs); + return config; + })); + // Handle `log4j.additivity` + extractSubset(globalProperties, ADDITIVITY_PREFIX) + .getProperties() + .forEach((key, additivity) -> loggerConfigs.compute((String) key, (name, oldConfig) -> { + LoggerConfig config = oldConfig != null ? oldConfig : new LoggerConfig(name); + config.setAdditivity((String) additivity); + return config; + })); + loggerConfigs.values().stream().map(LoggerConfig::buildLogger).forEach(loggers::add); + return loggers; + } + + private ConfigurationNode parseProperties(PropertiesSubset globalProperties) { + ConfigurationNodeBuilder propertiesBuilder = + ComponentUtils.newNodeBuilder().setPluginName("Properties"); + globalProperties.getProperties().forEach((name, value) -> { + ConfigurationNodeBuilder builder = ComponentUtils.newNodeBuilder() + .setPluginName("Property") + .addAttribute("name", (String) name) + .addAttribute("value", (String) value); + propertiesBuilder.addChild(builder.get()); + }); + return propertiesBuilder.get(); + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/XmlV1ConfigurationParser.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/XmlV1ConfigurationParser.java new file mode 100644 index 00000000..84250143 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/XmlV1ConfigurationParser.java @@ -0,0 +1,154 @@ +/* + * 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.converter.config.internal.v1; + +import static org.apache.logging.converter.config.internal.XmlUtils.throwUnknownElement; +import static org.apache.logging.converter.config.internal.v1.AbstractComponentParser.NAME_ATTR; +import static org.apache.logging.converter.config.internal.v1.AbstractComponentParser.VALUE_ATTR; + +import aQute.bnd.annotation.Resolution; +import aQute.bnd.annotation.spi.ServiceProvider; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Locale; +import javax.xml.parsers.DocumentBuilder; +import org.apache.logging.converter.config.internal.ComponentUtils; +import org.apache.logging.converter.config.internal.ComponentUtils.ConfigurationNodeBuilder; +import org.apache.logging.converter.config.internal.XmlUtils; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.apache.logging.converter.config.spi.ConfigurationParser; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +@ServiceProvider(value = ConfigurationParser.class, resolution = Resolution.MANDATORY) +public class XmlV1ConfigurationParser extends AbstractV1ConfigurationParser { + + private static final String APPENDER_TAG = "appender"; + private static final String APPENDER_REF_TAG = "appender-ref"; + private static final String CONFIGURATION_TAG = "log4j:configuration"; + private static final String OLD_CONFIGURATION_TAG = "configuration"; + private static final String LEVEL_TAG = "level"; + private static final String OLD_LEVEL_TAG = "priority"; + private static final String LOGGER_TAG = "logger"; + private static final String OLD_LOGGER_TAG = "category"; + private static final String ROOT_TAG = "root"; + + private static final String ADDITIVITY_ATTR = "additivity"; + private static final String REF_ATTR = "ref"; + private static final String THRESHOLD_ATTR = "threshold"; + + private static final String LOG4J_V1_XML_FORMAT = "v1:xml"; + + @Override + public String getInputFormat() { + return LOG4J_V1_XML_FORMAT; + } + + @Override + public ConfigurationNode parse(InputStream inputStream) throws IOException { + DocumentBuilder documentBuilder = XmlUtils.createDocumentBuilderV1(); + try { + Document document = documentBuilder.parse(inputStream); + return parse(document.getDocumentElement()); + } catch (SAXException e) { + Throwable cause = e.getCause(); + if (cause instanceof IOException) { + throw (IOException) cause; + } + throw new IOException("Unable to parse configuration file.", e); + } + } + + private ConfigurationNode parse(Element configurationElement) { + ConfigurationNodeBuilder builder = ComponentUtils.newNodeBuilder().setPluginName("Configuration"); + switch (configurationElement.getTagName()) { + case CONFIGURATION_TAG: + case OLD_CONFIGURATION_TAG: + break; + default: + throwUnknownElement(configurationElement); + } + + String level = configurationElement.getAttribute(THRESHOLD_ATTR); + + ConfigurationNodeBuilder appendersBuilder = + ComponentUtils.newNodeBuilder().setPluginName("Appenders"); + + LoggerConfig rootLogger = new LoggerConfig(""); + rootLogger.setLevel("DEBUG"); + Collection loggerConfigs = new ArrayList<>(); + XmlUtils.childStream(configurationElement).forEach(childElement -> { + switch (childElement.getTagName()) { + case APPENDER_TAG: + appendersBuilder.addChild(parseAppender(childElement)); + break; + case ROOT_TAG: + parseLoggerChildren(rootLogger, childElement); + break; + case LOGGER_TAG: + case OLD_LOGGER_TAG: + LoggerConfig loggerConfig = new LoggerConfig(childElement.getAttribute(NAME_ATTR)); + loggerConfigs.add(parseLoggerChildren(loggerConfig, childElement)); + break; + default: + throwUnknownElement(childElement); + } + }); + builder.addChild(appendersBuilder.get()); + + if (!level.isEmpty()) { + builder.addChild(ComponentUtils.newThresholdFilter(level.toUpperCase(Locale.ROOT))); + } + + ConfigurationNodeBuilder loggersNodeBuilder = + ComponentUtils.newNodeBuilder().setPluginName("Loggers"); + loggersNodeBuilder.addChild(rootLogger.buildRoot()); + loggerConfigs.stream().map(LoggerConfig::buildLogger).forEach(loggersNodeBuilder::addChild); + builder.addChild(loggersNodeBuilder.get()); + + return builder.get(); + } + + private ConfigurationNode parseAppender(Element appenderElement) { + return AbstractComponentParser.parseConfigurationElement(this, appenderElement); + } + + private LoggerConfig parseLoggerChildren(LoggerConfig loggerConfig, Element element) { + String additivity = element.getAttribute(ADDITIVITY_ATTR); + if (!additivity.isEmpty()) { + loggerConfig.setAdditivity(additivity); + } + XmlUtils.forEachChild(element, childElement -> { + String nodeName = childElement.getTagName(); + switch (nodeName) { + case APPENDER_REF_TAG: + loggerConfig.addAppenderRef(childElement.getAttribute(REF_ATTR)); + break; + case LEVEL_TAG: + case OLD_LEVEL_TAG: + loggerConfig.setLevel(childElement.getAttribute(VALUE_ATTR)); + break; + default: + throwUnknownElement(childElement); + } + }); + return loggerConfig; + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/AbstractAppenderParser.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/AbstractAppenderParser.java new file mode 100644 index 00000000..28fb08c6 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/AbstractAppenderParser.java @@ -0,0 +1,153 @@ +/* + * 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.converter.config.internal.v1.appender; + +import static org.apache.logging.converter.config.internal.ComponentUtils.newCompositeFilter; +import static org.apache.logging.converter.config.internal.ComponentUtils.newThresholdFilter; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; +import org.apache.logging.converter.config.ConfigurationConverterException; +import org.apache.logging.converter.config.internal.ComponentUtils.ConfigurationNodeBuilder; +import org.apache.logging.converter.config.internal.PropertiesUtils; +import org.apache.logging.converter.config.internal.v1.AbstractComponentParser; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.apache.logging.converter.config.spi.v1.Log4j1ParserContext; +import org.apache.logging.converter.config.spi.v1.PropertiesSubset; +import org.jspecify.annotations.Nullable; +import org.w3c.dom.Element; + +/** + * Common base for all appender parsers. + */ +public abstract class AbstractAppenderParser + extends AbstractComponentParser { + + // XML tags + private static final String FILTER_TAG = "filter"; + private static final String LAYOUT_TAG = "layout"; + + // parameters + protected static final String APPEND_PARAM = "Append"; + protected static final String BUFFERED_IO_PARAM = "BufferedIO"; + protected static final String BUFFER_SIZE_PARAM = "BufferSize"; + protected static final String IMMEDIATE_FLUSH_PARAM = "ImmediateFlush"; + + protected static String getAppenderName(PropertiesSubset properties) { + return PropertiesUtils.getLastComponent(properties.getPrefix()); + } + + protected static String getAppenderName(Element appenderElement) { + String name = appenderElement.getAttribute(NAME_ATTR); + if (name.isEmpty()) { + throw new ConfigurationConverterException("No name specified for appender " + appenderElement.getTagName()); + } + return name; + } + + @Override + protected final T createBuilder(Element element) { + return createBuilder(getAppenderName(element)); + } + + @Override + protected final T createBuilder(PropertiesSubset properties) { + return createBuilder(getAppenderName(properties)); + } + + /** + * Creates a configuration node builder. + * + * @param name The name of the appender. + */ + protected abstract T createBuilder(String name); + + @Override + protected void handleUnknownElement(Element childElement, Log4j1ParserContext context, T componentBuilder) + throws ConfigurationConverterException { + String nodeName = childElement.getTagName(); + if (nodeName.equals(LAYOUT_TAG)) { + componentBuilder.setLayout(parseConfigurationElement(context, childElement)); + } else if (nodeName.equals(FILTER_TAG)) { + componentBuilder.addFilter(parseConfigurationElement(context, childElement)); + } else { + handleUnknownElement(childElement, context, componentBuilder); + } + } + + @Override + protected void handleUnknownProperties(PropertiesSubset properties, Log4j1ParserContext context, T componentBuilder) + throws ConfigurationConverterException { + String key = PropertiesUtils.getLastComponent(properties.getPrefix()); + if (key.equals(LAYOUT_TAG)) { + componentBuilder.setLayout(parseConfigurationElement(context, properties)); + } else if (key.equals(FILTER_TAG)) { + PropertiesUtils.partitionOnCommonPrefixes(properties) + .forEach(filterProperties -> + componentBuilder.addFilter(parseConfigurationElement(context, filterProperties))); + } else { + super.handleUnknownProperties(properties, context, componentBuilder); + } + } + + public abstract static class AppenderBuilder implements Supplier { + + private final String name; + private final boolean requiresLayout; + + private @Nullable String threshold = null; + private final List filters = new ArrayList<>(); + private @Nullable ConfigurationNode layout; + + protected AppenderBuilder(String name, boolean requiresLayout) { + this.name = name; + this.requiresLayout = requiresLayout; + } + + public void setThreshold(String level) { + this.threshold = level; + } + + public void addFilter(ConfigurationNode filter) { + filters.add(filter); + } + + public void setLayout(ConfigurationNode layout) { + this.layout = layout; + } + + protected String getName() { + return name; + } + + protected ConfigurationNodeBuilder addStandardChildren(ConfigurationNodeBuilder builder) { + if (threshold != null) { + filters.add(0, newThresholdFilter(threshold)); + } + if (!filters.isEmpty()) { + builder.addChild(filters.size() > 1 ? newCompositeFilter(filters) : filters.get(0)); + } + if (layout != null) { + builder.addChild(layout); + } else if (requiresLayout) { + throw new ConfigurationConverterException("No layout provided for appender " + name); + } + return builder; + } + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/AbstractFileAppenderParser.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/AbstractFileAppenderParser.java new file mode 100644 index 00000000..abea4950 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/AbstractFileAppenderParser.java @@ -0,0 +1,108 @@ +/* + * 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.converter.config.internal.v1.appender; + +import static org.apache.logging.converter.config.internal.StringUtils.decapitalize; + +import java.lang.invoke.MethodHandle; +import java.util.Map; +import java.util.Objects; +import org.apache.logging.converter.config.ConfigurationConverterException; +import org.apache.logging.converter.config.internal.ComponentUtils; +import org.apache.logging.converter.config.internal.ComponentUtils.ConfigurationNodeBuilder; +import org.apache.logging.converter.config.internal.StringUtils; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.jspecify.annotations.Nullable; + +/** + * Common base for all file appender parsers. + */ +public abstract class AbstractFileAppenderParser + extends AbstractAppenderParser { + + private static final String FILE_PARAM = "File"; + + protected static final Map FILE_ATTRIBUTE_MAP = attributeMapBuilder( + AbstractFileAppenderParser.AbstractFileAppenderBuilder.class) + .add(APPEND_PARAM) + .add(BUFFERED_IO_PARAM) + .add(BUFFER_SIZE_PARAM) + .add(FILE_PARAM) + .add(IMMEDIATE_FLUSH_PARAM) + .add(THRESHOLD_PARAM) + .get(); + + public abstract static class AbstractFileAppenderBuilder extends AbstractAppenderParser.AppenderBuilder { + + private String append = StringUtils.TRUE; + private boolean bufferedIO = false; + private String bufferSize = "8192"; + private @Nullable String file; + private boolean immediateFlush = true; + + protected AbstractFileAppenderBuilder(String name) { + super(name, true); + } + + public void setAppend(String append) { + this.append = append; + } + + public void setBufferedIO(String bufferedIO) { + this.bufferedIO = StringUtils.parseBoolean(bufferedIO); + } + + public void setBufferSize(String bufferSize) { + this.bufferSize = bufferSize; + } + + protected String getRequiredFile() { + return Objects.requireNonNull(file); + } + + public void setFile(String file) { + this.file = file; + } + + public void setImmediateFlush(String immediateFlush) { + this.immediateFlush = StringUtils.parseBoolean(immediateFlush); + } + + protected void addFileAttributes(ConfigurationNodeBuilder builder) { + if (bufferedIO) { + immediateFlush = false; + } + if (file == null) { + throw new ConfigurationConverterException("No file specified for appender " + getName()); + } + builder.addAttribute("name", getName()) + .addAttribute(decapitalize(APPEND_PARAM), append) + .addAttribute("bufferedIo", bufferedIO) + .addAttribute(decapitalize(BUFFER_SIZE_PARAM), bufferSize) + .addAttribute(decapitalize(IMMEDIATE_FLUSH_PARAM), immediateFlush) + .addAttribute("fileName", file); + } + + @Override + public ConfigurationNode get() { + ConfigurationNodeBuilder builder = ComponentUtils.newNodeBuilder().setPluginName("File"); + addFileAttributes(builder); + addStandardChildren(builder); + return builder.get(); + } + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/AsyncAppenderParser.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/AsyncAppenderParser.java new file mode 100644 index 00000000..f7a614cf --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/AsyncAppenderParser.java @@ -0,0 +1,132 @@ +/* + * 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.converter.config.internal.v1.appender; + +import static org.apache.logging.converter.config.internal.StringUtils.decapitalize; + +import aQute.bnd.annotation.spi.ServiceProvider; +import java.lang.invoke.MethodHandle; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; +import org.apache.logging.converter.config.ConfigurationConverterException; +import org.apache.logging.converter.config.internal.ComponentUtils; +import org.apache.logging.converter.config.internal.ComponentUtils.ConfigurationNodeBuilder; +import org.apache.logging.converter.config.internal.StringUtils; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.apache.logging.converter.config.spi.v1.Log4j1ComponentParser; +import org.apache.logging.converter.config.spi.v1.Log4j1ParserContext; +import org.w3c.dom.Element; + +/** + * Parses a + * AsyncAppender + * configuration. + */ +@ServiceProvider(Log4j1ComponentParser.class) +public class AsyncAppenderParser extends AbstractAppenderParser { + + // XML tags + private static final String APPENDER_REF_TAG = "appender-ref"; + + // Parameters + private static final String BLOCKING_PARAM = "Blocking"; + private static final String INCLUDE_LOCATION_PARAM = "IncludeLocation"; + + private static final Map ATTRIBUTE_MAP = attributeMapBuilder( + AsyncAppenderParser.AsyncAppenderBuilder.class) + .add(BLOCKING_PARAM) + .add(BUFFER_SIZE_PARAM) + .add(INCLUDE_LOCATION_PARAM) + .add(THRESHOLD_PARAM) + .get(); + + @Override + public String getClassName() { + return "org.apache.log4j.AsyncAppender"; + } + + @Override + protected AsyncAppenderBuilder createBuilder(String appenderName) { + return new AsyncAppenderBuilder(appenderName); + } + + @Override + protected Map getAttributeMap() { + return ATTRIBUTE_MAP; + } + + @Override + protected void handleUnknownElement( + Element childElement, Log4j1ParserContext context, AsyncAppenderBuilder componentBuilder) + throws ConfigurationConverterException { + if (childElement.getTagName().equals(APPENDER_REF_TAG)) { + componentBuilder.addAppenderRef(childElement.getAttribute(REF_ATTR)); + } else { + super.handleUnknownElement(childElement, context, componentBuilder); + } + } + + public static final class AsyncAppenderBuilder extends AbstractAppenderParser.AppenderBuilder { + + private final Collection appenderRefs = new ArrayList<>(); + private String blocking = StringUtils.FALSE; + private String bufferSize = "1024"; + private String includeLocation = StringUtils.FALSE; + + private AsyncAppenderBuilder(String name) { + super(name, false); + } + + public void addAppenderRef(String appenderRef) { + appenderRefs.add(appenderRef); + } + + public void setAppenderRefs(String appenderRefs) { + this.appenderRefs.clear(); + String[] array = appenderRefs.split(",", -1); + for (String appenderRef : array) { + this.appenderRefs.add(appenderRef.trim()); + } + } + + public void setBlocking(String blocking) { + this.blocking = blocking; + } + + public void setBufferSize(String bufferSize) { + this.bufferSize = bufferSize; + } + + public void setIncludeLocation(String includeLocation) { + this.includeLocation = includeLocation; + } + + @Override + public ConfigurationNode get() { + ConfigurationNodeBuilder builder = ComponentUtils.newNodeBuilder() + .setPluginName("Async") + .addAttribute("name", getName()) + .addAttribute(decapitalize(BLOCKING_PARAM), blocking) + .addAttribute(decapitalize(BUFFER_SIZE_PARAM), bufferSize) + .addAttribute(decapitalize(INCLUDE_LOCATION_PARAM), includeLocation); + addStandardChildren(builder); + appenderRefs.forEach(ref -> builder.addChild(ComponentUtils.newAppenderRef(ref))); + return builder.get(); + } + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/ConsoleAppenderParser.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/ConsoleAppenderParser.java new file mode 100644 index 00000000..60541793 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/ConsoleAppenderParser.java @@ -0,0 +1,112 @@ +/* + * 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.converter.config.internal.v1.appender; + +import static org.apache.logging.converter.config.internal.StringUtils.decapitalize; + +import aQute.bnd.annotation.spi.ServiceProvider; +import java.lang.invoke.MethodHandle; +import java.util.Map; +import org.apache.logging.converter.config.ConfigurationConverterException; +import org.apache.logging.converter.config.internal.ComponentUtils; +import org.apache.logging.converter.config.internal.ComponentUtils.ConfigurationNodeBuilder; +import org.apache.logging.converter.config.internal.StringUtils; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.apache.logging.converter.config.spi.v1.Log4j1ComponentParser; + +/** + * Parses a + * ConsoleAppender + * configuration. + */ +@ServiceProvider(Log4j1ComponentParser.class) +public class ConsoleAppenderParser extends AbstractAppenderParser { + + private static final String V1_SYSTEM_OUT = "System.out"; + private static final String V1_SYSTEM_ERR = "System.err"; + private static final String V2_SYSTEM_OUT = "SYSTEM_OUT"; + private static final String V2_SYSTEM_ERR = "SYSTEM_ERR"; + + // V1 configuration parameters + private static final String TARGET_PARAM = "Target"; + private static final String FOLLOW_PARAM = "Follow"; + + private static final Map ATTRIBUTE_MAP = attributeMapBuilder(ConsoleAppenderBuilder.class) + .add(FOLLOW_PARAM) + .add(IMMEDIATE_FLUSH_PARAM) + .add(TARGET_PARAM) + .add(THRESHOLD_PARAM) + .get(); + + @Override + public String getClassName() { + return "org.apache.log4j.ConsoleAppender"; + } + + @Override + protected ConsoleAppenderBuilder createBuilder(String name) { + return new ConsoleAppenderBuilder(name); + } + + @Override + protected Map getAttributeMap() { + return ATTRIBUTE_MAP; + } + + public static final class ConsoleAppenderBuilder extends AbstractAppenderParser.AppenderBuilder { + + private String follow = StringUtils.FALSE; + private String immediateFlush = StringUtils.TRUE; + private String v1Target = V1_SYSTEM_OUT; + + private ConsoleAppenderBuilder(String name) { + super(name, true); + } + + public void setFollow(String follow) { + this.follow = follow; + } + + public void setImmediateFlush(String immediateFlush) { + this.immediateFlush = immediateFlush; + } + + public void setTarget(String v1Target) { + switch (v1Target) { + case V1_SYSTEM_OUT: + case V1_SYSTEM_ERR: + this.v1Target = v1Target; + break; + default: + throw new ConfigurationConverterException( + "Invalid target attribute `" + v1Target + "` for ConsoleAppender " + getName()); + } + } + + @Override + public ConfigurationNode get() { + ConfigurationNodeBuilder builder = ComponentUtils.newNodeBuilder() + .setPluginName("Console") + .addAttribute("name", getName()) + .addAttribute(decapitalize(FOLLOW_PARAM), follow) + .addAttribute(decapitalize(IMMEDIATE_FLUSH_PARAM), immediateFlush) + .addAttribute( + decapitalize(TARGET_PARAM), V1_SYSTEM_OUT.equals(v1Target) ? V2_SYSTEM_OUT : V2_SYSTEM_ERR); + return addStandardChildren(builder).get(); + } + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/DailyRollingFileAppenderParser.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/DailyRollingFileAppenderParser.java new file mode 100644 index 00000000..5fb53404 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/DailyRollingFileAppenderParser.java @@ -0,0 +1,92 @@ +/* + * 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.converter.config.internal.v1.appender; + +import aQute.bnd.annotation.spi.ServiceProvider; +import java.lang.invoke.MethodHandle; +import java.util.Map; +import org.apache.logging.converter.config.internal.ComponentUtils; +import org.apache.logging.converter.config.internal.ComponentUtils.ConfigurationNodeBuilder; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.apache.logging.converter.config.spi.v1.Log4j1ComponentParser; + +/** + * Parses a + * DailyRollingFileAppender + * configuration. + */ +@ServiceProvider(Log4j1ComponentParser.class) +public class DailyRollingFileAppenderParser + extends AbstractFileAppenderParser { + + private static final String DEFAULT_DATE_PATTERN = ".yyyy-MM-dd"; + private static final String DATE_PATTERN_PARAM = "DatePattern"; + + private static final Map ATTRIBUTE_MAP = attributeMapBuilder( + DailyRollingFileAppenderBuilder.class) + .add(DATE_PATTERN_PARAM) + .addAll(FILE_ATTRIBUTE_MAP) + .get(); + + @Override + public String getClassName() { + return "org.apache.log4j.DailyRollingFileAppender"; + } + + @Override + protected DailyRollingFileAppenderBuilder createBuilder(String name) { + return new DailyRollingFileAppenderBuilder(name); + } + + @Override + protected Map getAttributeMap() { + return ATTRIBUTE_MAP; + } + + public static final class DailyRollingFileAppenderBuilder + extends AbstractFileAppenderParser.AbstractFileAppenderBuilder { + + private String datePattern = DEFAULT_DATE_PATTERN; + + private DailyRollingFileAppenderBuilder(String name) { + super(name); + } + + public void setDatePattern(String datePattern) { + this.datePattern = datePattern; + } + + @Override + public ConfigurationNode get() { + ConfigurationNodeBuilder builder = ComponentUtils.newNodeBuilder().setPluginName("RollingFile"); + addFileAttributes(builder); + addStandardChildren(builder); + + String filePattern = getRequiredFile() + "%d{" + datePattern + "}"; + return builder.addAttribute("filePattern", filePattern) + .addChild(createTriggeringPolicy()) + .get(); + } + + private ConfigurationNode createTriggeringPolicy() { + return ComponentUtils.newNodeBuilder() + .setPluginName("TimeBasedTriggeringPolicy") + .addAttribute("modulate", true) + .get(); + } + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/FileAppenderParser.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/FileAppenderParser.java new file mode 100644 index 00000000..961ad67b --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/FileAppenderParser.java @@ -0,0 +1,52 @@ +/* + * 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.converter.config.internal.v1.appender; + +import aQute.bnd.annotation.spi.ServiceProvider; +import java.lang.invoke.MethodHandle; +import java.util.Map; +import org.apache.logging.converter.config.spi.v1.Log4j1ComponentParser; + +/** + * Parses a + * FileAppender + * configuration. + */ +@ServiceProvider(Log4j1ComponentParser.class) +public class FileAppenderParser extends AbstractFileAppenderParser { + @Override + public String getClassName() { + return "org.apache.log4j.FileAppender"; + } + + @Override + protected FileAppenderBuilder createBuilder(String name) { + return new FileAppenderBuilder(name); + } + + @Override + protected Map getAttributeMap() { + return FILE_ATTRIBUTE_MAP; + } + + public static final class FileAppenderBuilder extends AbstractFileAppenderParser.AbstractFileAppenderBuilder { + + private FileAppenderBuilder(String name) { + super(name); + } + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/RollingFileAppenderParser.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/RollingFileAppenderParser.java new file mode 100644 index 00000000..6a69e95c --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/RollingFileAppenderParser.java @@ -0,0 +1,133 @@ +/* + * 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.converter.config.internal.v1.appender; + +import aQute.bnd.annotation.spi.ServiceProvider; +import java.lang.invoke.MethodHandle; +import java.util.Formatter; +import java.util.Locale; +import java.util.Map; +import org.apache.logging.converter.config.internal.ComponentUtils; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.apache.logging.converter.config.spi.v1.Log4j1ComponentParser; + +/** + * Parses a + * RollingFileAppender + * configuration. + */ +@ServiceProvider(Log4j1ComponentParser.class) +public class RollingFileAppenderParser + extends AbstractFileAppenderParser { + + private static final String MAX_BACKUP_INDEX_PARAM = "MaxBackupIndex"; + private static final String MAX_FILE_SIZE_PARAM = "MaxFileSize"; + + private static final String DEFAULT_MAX_BACKUP_INDEX = "1"; + private static final long KB = 1024; + private static final long MB = KB * KB; + private static final long GB = KB * MB; + private static final long DEFAULT_MAX_FILE_SIZE = 10 * MB; + private static final String SIZE_FORMAT = "%.2f %s"; + + private static final Map ATTRIBUTE_MAP = attributeMapBuilder( + RollingFileAppenderParser.RollingFileAppenderBuilder.class) + .add(MAX_BACKUP_INDEX_PARAM) + .add(MAX_FILE_SIZE_PARAM) + .addAll(FILE_ATTRIBUTE_MAP) + .get(); + + @Override + public String getClassName() { + return "org.apache.log4j.RollingFileAppender"; + } + + @Override + protected RollingFileAppenderBuilder createBuilder(String name) { + return new RollingFileAppenderBuilder(name); + } + + @Override + protected Map getAttributeMap() { + return ATTRIBUTE_MAP; + } + + public static final class RollingFileAppenderBuilder + extends AbstractFileAppenderParser.AbstractFileAppenderBuilder { + + private String maxBackupIndex = DEFAULT_MAX_BACKUP_INDEX; + private String maxFileSize = Long.toString(DEFAULT_MAX_FILE_SIZE); + + private RollingFileAppenderBuilder(String name) { + super(name); + } + + public void setMaxBackupIndex(String maxBackupIndex) { + this.maxBackupIndex = maxBackupIndex; + } + + public void setMaxFileSize(String maxFileSize) { + this.maxFileSize = maxFileSize; + } + + @Override + public ConfigurationNode get() { + ComponentUtils.ConfigurationNodeBuilder builder = + ComponentUtils.newNodeBuilder().setPluginName("RollingFile"); + addFileAttributes(builder); + addStandardChildren(builder); + + String filePattern = getRequiredFile() + ".%i"; + return builder.addAttribute("filePattern", filePattern) + .addChild(createTriggeringPolicy()) + .addChild(createRolloverStrategy()) + .get(); + } + + private ConfigurationNode createTriggeringPolicy() { + StringBuilder sizeBuilder = new StringBuilder(); + try { + long maxFileSize = Long.parseLong(this.maxFileSize); + Formatter sizeFormatter = new Formatter(sizeBuilder, Locale.ROOT); + if (maxFileSize > GB) { + sizeFormatter.format(SIZE_FORMAT, ((float) maxFileSize) / GB, "GB"); + } else if (maxFileSize > MB) { + sizeFormatter.format(SIZE_FORMAT, ((float) maxFileSize) / MB, "MB"); + } else if (maxFileSize > KB) { + sizeFormatter.format(SIZE_FORMAT, ((float) maxFileSize) / KB, "KB"); + } else { + sizeBuilder.append(maxFileSize); + } + } catch (NumberFormatException e) { + // The value contains property expansions + sizeBuilder.append(this.maxFileSize); + } + return ComponentUtils.newNodeBuilder() + .setPluginName("SizeBasedTriggeringPolicy") + .addAttribute("size", sizeBuilder.toString()) + .get(); + } + + private ConfigurationNode createRolloverStrategy() { + return ComponentUtils.newNodeBuilder() + .setPluginName("DefaultRolloverStrategy") + .addAttribute("max", maxBackupIndex) + .addAttribute("fileIndex", "min") + .get(); + } + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/package-info.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/package-info.java new file mode 100644 index 00000000..1731f8ff --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/appender/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ +@NullMarked +package org.apache.logging.converter.config.internal.v1.appender; + +import org.jspecify.annotations.NullMarked; diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/filter/AbstractFilterParser.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/filter/AbstractFilterParser.java new file mode 100644 index 00000000..1110708b --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/filter/AbstractFilterParser.java @@ -0,0 +1,41 @@ +/* + * 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.converter.config.internal.v1.filter; + +import java.util.function.Supplier; +import org.apache.logging.converter.config.internal.ComponentUtils.ConfigurationNodeBuilder; +import org.apache.logging.converter.config.internal.v1.AbstractComponentParser; +import org.apache.logging.converter.config.spi.ConfigurationNode; + +/** + * Common base for all filter parsers. + */ +public abstract class AbstractFilterParser> extends AbstractComponentParser { + + static final String ACCEPT_ON_MATCH_PARAM = "AcceptOnMatch"; + + private static final String ACCEPT = "ACCEPT"; + private static final String DENY = "DENY"; + private static final String NEUTRAL = "NEUTRAL"; + + private static final String ON_MATCH = "onMatch"; + private static final String ON_MISMATCH = "onMismatch"; + + static void addOnMatchAndMismatch(ConfigurationNodeBuilder builder, boolean acceptOnMatch) { + builder.addAttribute(ON_MATCH, acceptOnMatch ? ACCEPT : DENY).addAttribute(ON_MISMATCH, NEUTRAL); + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/filter/DenyAllFilterParser.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/filter/DenyAllFilterParser.java new file mode 100644 index 00000000..d8c2b757 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/filter/DenyAllFilterParser.java @@ -0,0 +1,68 @@ +/* + * 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.converter.config.internal.v1.filter; + +import aQute.bnd.annotation.spi.ServiceProvider; +import java.lang.invoke.MethodHandle; +import java.util.Collections; +import java.util.Map; +import java.util.function.Supplier; +import org.apache.logging.converter.config.internal.ComponentUtils; +import org.apache.logging.converter.config.internal.v1.AbstractComponentParser; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.apache.logging.converter.config.spi.v1.Log4j1ComponentParser; +import org.apache.logging.converter.config.spi.v1.PropertiesSubset; +import org.w3c.dom.Element; + +/** + * Parses a + * DenyAllFilter + * configuration. + */ +@ServiceProvider(Log4j1ComponentParser.class) +public class DenyAllFilterParser extends AbstractComponentParser { + + @Override + public String getClassName() { + return "org.apache.log4j.varia.DenyAllFilter"; + } + + @Override + protected DenyAllFilterBuilder createBuilder(Element element) { + return new DenyAllFilterBuilder(); + } + + @Override + protected DenyAllFilterBuilder createBuilder(PropertiesSubset properties) { + return new DenyAllFilterBuilder(); + } + + @Override + protected Map getAttributeMap() { + return Collections.emptyMap(); + } + + public static final class DenyAllFilterBuilder implements Supplier { + + @Override + public ConfigurationNode get() { + return ComponentUtils.newNodeBuilder() + .setPluginName("DenyAllFilter") + .get(); + } + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/filter/LevelMatchFilterParser.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/filter/LevelMatchFilterParser.java new file mode 100644 index 00000000..acf54bcf --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/filter/LevelMatchFilterParser.java @@ -0,0 +1,91 @@ +/* + * 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.converter.config.internal.v1.filter; + +import static org.apache.logging.converter.config.internal.StringUtils.parseBoolean; + +import aQute.bnd.annotation.spi.ServiceProvider; +import java.lang.invoke.MethodHandle; +import java.util.Map; +import java.util.function.Supplier; +import org.apache.logging.converter.config.internal.ComponentUtils; +import org.apache.logging.converter.config.internal.ComponentUtils.ConfigurationNodeBuilder; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.apache.logging.converter.config.spi.v1.Log4j1ComponentParser; +import org.apache.logging.converter.config.spi.v1.PropertiesSubset; +import org.w3c.dom.Element; + +/** + * Parses a + * LevelMatchFilter + * configuration. + */ +@ServiceProvider(Log4j1ComponentParser.class) +public class LevelMatchFilterParser extends AbstractFilterParser { + + private static final String ACCEPT_ON_MATCH = "AcceptOnMatch"; + private static final String LEVEL_TO_MATCH = "LevelToMatch"; + + private static final Map ATTRIBUTE_MAP = attributeMapBuilder( + LevelMatchFilterParser.LevelMatchFilterBuilder.class) + .add(ACCEPT_ON_MATCH) + .add(LEVEL_TO_MATCH) + .get(); + + @Override + public String getClassName() { + return "org.apache.log4j.varia.LevelMatchFilter"; + } + + @Override + protected LevelMatchFilterBuilder createBuilder(Element element) { + return new LevelMatchFilterBuilder(); + } + + @Override + protected LevelMatchFilterBuilder createBuilder(PropertiesSubset properties) { + return new LevelMatchFilterBuilder(); + } + + @Override + protected Map getAttributeMap() { + return ATTRIBUTE_MAP; + } + + public static final class LevelMatchFilterBuilder implements Supplier { + + private boolean acceptOnMatch = false; + private String level = "ERROR"; + + public void setAcceptOnMatch(String acceptOnMatch) { + this.acceptOnMatch = parseBoolean(acceptOnMatch); + } + + public void setLevelToMatch(String level) { + this.level = level; + } + + @Override + public ConfigurationNode get() { + ConfigurationNodeBuilder builder = ComponentUtils.newNodeBuilder() + .setPluginName("LevelMatchFilter") + .addAttribute("level", level); + addOnMatchAndMismatch(builder, acceptOnMatch); + return builder.get(); + } + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/filter/LevelRangeFilterParser.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/filter/LevelRangeFilterParser.java new file mode 100644 index 00000000..d668738c --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/filter/LevelRangeFilterParser.java @@ -0,0 +1,99 @@ +/* + * 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.converter.config.internal.v1.filter; + +import static org.apache.logging.converter.config.internal.StringUtils.parseBoolean; + +import aQute.bnd.annotation.spi.ServiceProvider; +import java.lang.invoke.MethodHandle; +import java.util.Map; +import java.util.function.Supplier; +import org.apache.logging.converter.config.internal.ComponentUtils; +import org.apache.logging.converter.config.internal.ComponentUtils.ConfigurationNodeBuilder; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.apache.logging.converter.config.spi.v1.Log4j1ComponentParser; +import org.apache.logging.converter.config.spi.v1.PropertiesSubset; +import org.w3c.dom.Element; + +/** + * Parses a + * LevelRangeFilter + * configuration. + */ +@ServiceProvider(Log4j1ComponentParser.class) +public class LevelRangeFilterParser extends AbstractFilterParser { + + private static final String LEVEL_MAX_PARAM = "LevelMax"; + private static final String LEVEL_MIN_PARAM = "LevelMin"; + + private static final Map ATTRIBUTE_MAP = attributeMapBuilder(LevelRangeFilterBuilder.class) + .add(ACCEPT_ON_MATCH_PARAM) + .add(LEVEL_MAX_PARAM) + .add(LEVEL_MIN_PARAM) + .get(); + + @Override + public String getClassName() { + return "org.apache.log4j.varia.LevelRangeFilter"; + } + + @Override + protected LevelRangeFilterBuilder createBuilder(Element element) { + return new LevelRangeFilterBuilder(); + } + + @Override + protected LevelRangeFilterBuilder createBuilder(PropertiesSubset properties) { + return new LevelRangeFilterBuilder(); + } + + @Override + protected Map getAttributeMap() { + return ATTRIBUTE_MAP; + } + + public static final class LevelRangeFilterBuilder implements Supplier { + + private boolean acceptOnMatch = false; + private String levelMax = "OFF"; + private String levelMin = "ALL"; + + public void setAcceptOnMatch(String acceptOnMatch) { + this.acceptOnMatch = parseBoolean(acceptOnMatch); + } + + public void setLevelMax(String level) { + this.levelMax = level; + } + + public void setLevelMin(String levelMin) { + this.levelMin = levelMin; + } + + @Override + public ConfigurationNode get() { + // log4j1 order: ALL < TRACE < DEBUG < ... < FATAL < OFF + // log4j2 order: ALL > TRACE > DEBUG > ... > FATAL > OFF + ConfigurationNodeBuilder builder = ComponentUtils.newNodeBuilder() + .setPluginName("LevelRangeFilter") + .addAttribute("minLevel", levelMax) + .addAttribute("maxLevel", levelMin); + addOnMatchAndMismatch(builder, acceptOnMatch); + return builder.get(); + } + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/filter/StringMatchFilterParser.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/filter/StringMatchFilterParser.java new file mode 100644 index 00000000..0098881e --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/filter/StringMatchFilterParser.java @@ -0,0 +1,94 @@ +/* + * 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.converter.config.internal.v1.filter; + +import static org.apache.logging.converter.config.internal.StringUtils.parseBoolean; + +import aQute.bnd.annotation.spi.ServiceProvider; +import java.lang.invoke.MethodHandle; +import java.util.Map; +import java.util.function.Supplier; +import org.apache.logging.converter.config.ConfigurationConverterException; +import org.apache.logging.converter.config.internal.ComponentUtils; +import org.apache.logging.converter.config.internal.ComponentUtils.ConfigurationNodeBuilder; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.apache.logging.converter.config.spi.v1.Log4j1ComponentParser; +import org.apache.logging.converter.config.spi.v1.PropertiesSubset; +import org.jspecify.annotations.Nullable; +import org.w3c.dom.Element; + +/** + * Parses a + * StringMatchFilter + * configuration. + */ +@ServiceProvider(Log4j1ComponentParser.class) +public class StringMatchFilterParser extends AbstractFilterParser { + + private static final String STRING_TO_MATCH = "StringToMatch"; + + private static final Map ATTRIBUTE_MAP = attributeMapBuilder(StringMatchFilterBuilder.class) + .add(ACCEPT_ON_MATCH_PARAM) + .add(STRING_TO_MATCH) + .get(); + + @Override + public String getClassName() { + return "org.apache.log4j.varia.StringMatchFilter"; + } + + @Override + protected StringMatchFilterBuilder createBuilder(Element element) { + return new StringMatchFilterBuilder(); + } + + @Override + protected StringMatchFilterBuilder createBuilder(PropertiesSubset properties) { + return new StringMatchFilterBuilder(); + } + + @Override + protected Map getAttributeMap() { + return ATTRIBUTE_MAP; + } + + public static final class StringMatchFilterBuilder implements Supplier { + + private boolean acceptOnMatch = false; + private @Nullable String stringToMatch = null; + + public void setAcceptOnMatch(String acceptOnMatch) { + this.acceptOnMatch = parseBoolean(acceptOnMatch); + } + + public void setStringToMatch(String stringToMatch) { + this.stringToMatch = stringToMatch; + } + + @Override + public ConfigurationNode get() { + if (stringToMatch == null) { + throw new ConfigurationConverterException("Missing required '" + STRING_TO_MATCH + "' attribute"); + } + ConfigurationNodeBuilder builder = ComponentUtils.newNodeBuilder() + .setPluginName("StringMatchFilter") + .addAttribute("text", stringToMatch); + addOnMatchAndMismatch(builder, acceptOnMatch); + return builder.get(); + } + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/filter/package-info.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/filter/package-info.java new file mode 100644 index 00000000..2110ae4e --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/filter/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ +@NullMarked +package org.apache.logging.converter.config.internal.v1.filter; + +import org.jspecify.annotations.NullMarked; diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/layout/EnhancedPatternLayoutParser.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/layout/EnhancedPatternLayoutParser.java new file mode 100644 index 00000000..7603a53f --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/layout/EnhancedPatternLayoutParser.java @@ -0,0 +1,54 @@ +/* + * 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.converter.config.internal.v1.layout; + +import aQute.bnd.annotation.spi.ServiceProvider; +import java.lang.invoke.MethodHandle; +import java.util.Map; +import org.apache.logging.converter.config.internal.v1.AbstractComponentParser; +import org.apache.logging.converter.config.spi.v1.Log4j1ComponentParser; +import org.apache.logging.converter.config.spi.v1.PropertiesSubset; +import org.w3c.dom.Element; + +/** + * Parses a + * EnhancedPatternLayout + * configuration. + */ +@ServiceProvider(Log4j1ComponentParser.class) +public class EnhancedPatternLayoutParser extends AbstractComponentParser { + + @Override + public String getClassName() { + return "org.apache.log4j.EnhancedPatternLayout"; + } + + @Override + protected PatternLayoutParser.PatternLayoutBuilder createBuilder(Element element) { + return new PatternLayoutParser.PatternLayoutBuilder(); + } + + @Override + protected PatternLayoutParser.PatternLayoutBuilder createBuilder(PropertiesSubset properties) { + return new PatternLayoutParser.PatternLayoutBuilder(); + } + + @Override + protected Map getAttributeMap() { + return PatternLayoutParser.ATTRIBUTE_MAP; + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/layout/HtmlLayoutParser.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/layout/HtmlLayoutParser.java new file mode 100644 index 00000000..83fed369 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/layout/HtmlLayoutParser.java @@ -0,0 +1,92 @@ +/* + * 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.converter.config.internal.v1.layout; + +import aQute.bnd.annotation.spi.ServiceProvider; +import java.lang.invoke.MethodHandle; +import java.util.Map; +import java.util.function.Supplier; +import org.apache.logging.converter.config.internal.ComponentUtils; +import org.apache.logging.converter.config.internal.StringUtils; +import org.apache.logging.converter.config.internal.v1.AbstractComponentParser; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.apache.logging.converter.config.spi.v1.Log4j1ComponentParser; +import org.apache.logging.converter.config.spi.v1.PropertiesSubset; +import org.w3c.dom.Element; + +/** + * Parses a + * HTMLLayout + * configuration. + */ +@ServiceProvider(Log4j1ComponentParser.class) +public class HtmlLayoutParser extends AbstractComponentParser { + + private static final String LOCATION_INFO_PARAM = "LocationInfo"; + private static final String TITLE_PARAM = "Title"; + private static final String TITLE_DEFAULT = "Log4J Log Messages"; + + private static final Map ATTRIBUTE_MAP = attributeMapBuilder(HtmlLayoutBuilder.class) + .add(LOCATION_INFO_PARAM) + .add(TITLE_PARAM) + .get(); + + @Override + public String getClassName() { + return "org.apache.log4j.HTMLLayout"; + } + + @Override + protected HtmlLayoutBuilder createBuilder(Element element) { + return new HtmlLayoutBuilder(); + } + + @Override + protected HtmlLayoutBuilder createBuilder(PropertiesSubset properties) { + return new HtmlLayoutBuilder(); + } + + @Override + protected Map getAttributeMap() { + return ATTRIBUTE_MAP; + } + + public static final class HtmlLayoutBuilder implements Supplier { + + private String title = TITLE_DEFAULT; + private String locationInfo = StringUtils.FALSE; + + private HtmlLayoutBuilder() {} + + public void setTitle(String title) { + this.title = title; + } + + public void setLocationInfo(String locationInfo) { + this.locationInfo = locationInfo; + } + + @Override + public ConfigurationNode get() { + ComponentUtils.ConfigurationNodeBuilder builder = + ComponentUtils.newNodeBuilder().setPluginName("HtmlLayout"); + builder.addAttribute("title", title); + builder.addAttribute("locationInfo", locationInfo); + return builder.get(); + } + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/layout/PatternLayoutParser.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/layout/PatternLayoutParser.java new file mode 100644 index 00000000..6c02d663 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/layout/PatternLayoutParser.java @@ -0,0 +1,82 @@ +/* + * 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.converter.config.internal.v1.layout; + +import aQute.bnd.annotation.spi.ServiceProvider; +import java.lang.invoke.MethodHandle; +import java.util.Map; +import java.util.function.Supplier; +import org.apache.logging.converter.config.internal.ComponentUtils; +import org.apache.logging.converter.config.internal.v1.AbstractComponentParser; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.apache.logging.converter.config.spi.v1.Log4j1ComponentParser; +import org.apache.logging.converter.config.spi.v1.PropertiesSubset; +import org.w3c.dom.Element; + +/** + * Parses a + * PatternLayout + * configuration. + */ +@ServiceProvider(Log4j1ComponentParser.class) +public class PatternLayoutParser extends AbstractComponentParser { + + private static final String CONVERSION_PATTERN_PARAM = "ConversionPattern"; + + static final Map ATTRIBUTE_MAP = attributeMapBuilder(PatternLayoutBuilder.class) + .add(CONVERSION_PATTERN_PARAM) + .get(); + + @Override + public String getClassName() { + return "org.apache.log4j.PatternLayout"; + } + + @Override + protected PatternLayoutBuilder createBuilder(Element element) { + return new PatternLayoutBuilder(); + } + + @Override + protected PatternLayoutBuilder createBuilder(PropertiesSubset properties) { + return new PatternLayoutBuilder(); + } + + @Override + protected Map getAttributeMap() { + return ATTRIBUTE_MAP; + } + + public static final class PatternLayoutBuilder implements Supplier { + + private String conversionPattern = "%m%n"; + + PatternLayoutBuilder() {} + + public void setConversionPattern(String conversionPattern) { + this.conversionPattern = conversionPattern; + } + + @Override + public ConfigurationNode get() { + return ComponentUtils.newNodeBuilder() + .setPluginName("PatternLayout") + .addAttribute("pattern", conversionPattern) + .get(); + } + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/layout/SimpleLayoutParser.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/layout/SimpleLayoutParser.java new file mode 100644 index 00000000..2795d6f4 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/layout/SimpleLayoutParser.java @@ -0,0 +1,70 @@ +/* + * 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.converter.config.internal.v1.layout; + +import aQute.bnd.annotation.spi.ServiceProvider; +import java.lang.invoke.MethodHandle; +import java.util.Collections; +import java.util.Map; +import java.util.function.Supplier; +import org.apache.logging.converter.config.internal.ComponentUtils; +import org.apache.logging.converter.config.internal.v1.AbstractComponentParser; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.apache.logging.converter.config.spi.v1.Log4j1ComponentParser; +import org.apache.logging.converter.config.spi.v1.PropertiesSubset; +import org.w3c.dom.Element; + +/** + * Parses a + * SimpleLayout + * configuration. + */ +@ServiceProvider(Log4j1ComponentParser.class) +public class SimpleLayoutParser extends AbstractComponentParser { + + @Override + public String getClassName() { + return "org.apache.log4j.SimpleLayout"; + } + + @Override + protected SimpleLayoutBuilder createBuilder(Element element) { + return new SimpleLayoutBuilder(); + } + + @Override + protected SimpleLayoutBuilder createBuilder(PropertiesSubset properties) { + return new SimpleLayoutBuilder(); + } + + @Override + protected Map getAttributeMap() { + return Collections.emptyMap(); + } + + public static class SimpleLayoutBuilder implements Supplier { + + @Override + public ConfigurationNode get() { + return ComponentUtils.newNodeBuilder() + .setPluginName("PatternLayout") + .addAttribute("pattern", "%p - %m%n") + .addAttribute("alwaysWriteExceptions", "false") + .get(); + } + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/layout/TTCCLayoutParser.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/layout/TTCCLayoutParser.java new file mode 100644 index 00000000..f37d93c6 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/layout/TTCCLayoutParser.java @@ -0,0 +1,164 @@ +/* + * 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.converter.config.internal.v1.layout; + +import aQute.bnd.annotation.spi.ServiceProvider; +import java.lang.invoke.MethodHandle; +import java.util.Locale; +import java.util.Map; +import java.util.function.Supplier; +import org.apache.logging.converter.config.internal.ComponentUtils; +import org.apache.logging.converter.config.internal.ComponentUtils.ConfigurationNodeBuilder; +import org.apache.logging.converter.config.internal.StringUtils; +import org.apache.logging.converter.config.internal.v1.AbstractComponentParser; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.apache.logging.converter.config.spi.v1.Log4j1ComponentParser; +import org.apache.logging.converter.config.spi.v1.PropertiesSubset; +import org.jspecify.annotations.Nullable; +import org.w3c.dom.Element; + +/** + * Parses a + * TTCCLayout + * configuration. + */ +@ServiceProvider(Log4j1ComponentParser.class) +public class TTCCLayoutParser extends AbstractComponentParser { + + private static final String CATEGORY_PREFIXING_PARAM = "CategoryPrefixing"; + private static final String CONTEXT_PRINTING_PARAM = "ContextPrinting"; + private static final String DATE_FORMAT_PARAM = "DateFormat"; + private static final String THREAD_PRINTING_PARAM = "ThreadPrinting"; + private static final String TIMEZONE_FORMAT_PARAM = "TimeZone"; + + private static final String ABSOLUTE = "ABSOLUTE"; + private static final String ABSOLUTE_FORMAT = "HH:mm:ss,SSS"; + private static final String DATE = "DATE"; + private static final String DATE_FORMAT = "dd MMM yyyy HH:mm:ss,SSS"; + private static final String ISO8601 = "ISO8601"; + private static final String ISO8601_FORMAT = "yyyy-MM-dd HH:mm:ss,SSS"; + private static final String NULL = "NULL"; + private static final String RELATIVE = "RELATIVE"; + + private static final Map ATTRIBUTE_MAP = attributeMapBuilder(TTCCLayoutBuilder.class) + .add(CATEGORY_PREFIXING_PARAM) + .add(CONTEXT_PRINTING_PARAM) + .add(DATE_FORMAT_PARAM) + .add(THREAD_PRINTING_PARAM) + .add(TIMEZONE_FORMAT_PARAM) + .get(); + + @Override + public String getClassName() { + return "org.apache.log4j.TTCCLayout"; + } + + @Override + protected TTCCLayoutBuilder createBuilder(Element element) { + return new TTCCLayoutBuilder(); + } + + @Override + protected TTCCLayoutBuilder createBuilder(PropertiesSubset properties) { + return new TTCCLayoutBuilder(); + } + + @Override + protected Map getAttributeMap() { + return ATTRIBUTE_MAP; + } + + public static final class TTCCLayoutBuilder implements Supplier { + + private boolean threadPrinting = true; + private boolean categoryPrefixing = true; + private boolean contextPrinting = true; + + private @Nullable String dateFormat = RELATIVE; + private @Nullable String timeZone = null; + + private TTCCLayoutBuilder() {} + + public void setThreadPrinting(String threadPrinting) { + this.threadPrinting = StringUtils.parseBoolean(threadPrinting); + } + + public void setCategoryPrefixing(String categoryPrefixing) { + this.categoryPrefixing = StringUtils.parseBoolean(categoryPrefixing); + } + + public void setContextPrinting(String contextPrinting) { + this.contextPrinting = StringUtils.parseBoolean(contextPrinting); + } + + public void setDateFormat(String dateFormat) { + switch (dateFormat.toUpperCase(Locale.ROOT)) { + case ABSOLUTE: + this.dateFormat = ABSOLUTE_FORMAT; + break; + case DATE: + this.dateFormat = DATE_FORMAT; + break; + case ISO8601: + this.dateFormat = ISO8601_FORMAT; + break; + case RELATIVE: + this.dateFormat = RELATIVE; + break; + case NULL: + this.dateFormat = null; + break; + default: + } + } + + public void setTimeZone(String timeZone) { + this.timeZone = timeZone; + } + + @Override + public ConfigurationNode get() { + ConfigurationNodeBuilder nodeBuilder = + ComponentUtils.newNodeBuilder().setPluginName("PatternLayout"); + StringBuilder patternBuilder = new StringBuilder(); + if (dateFormat != null) { + if (RELATIVE.equalsIgnoreCase(dateFormat)) { + patternBuilder.append("%r "); + } else { + patternBuilder.append("%d{").append(dateFormat).append("}"); + if (timeZone != null) { + patternBuilder.append("{").append(timeZone).append("}"); + } + patternBuilder.append(" "); + } + } + if (threadPrinting) { + patternBuilder.append("[%t] "); + } + patternBuilder.append("%p "); + if (categoryPrefixing) { + patternBuilder.append("%c "); + } + if (contextPrinting) { + patternBuilder.append("%notEmpty{%NDC }"); + } + patternBuilder.append("- %m%n"); + nodeBuilder.addAttribute("pattern", patternBuilder.toString()); + return nodeBuilder.get(); + } + } +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/layout/package-info.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/layout/package-info.java new file mode 100644 index 00000000..dcb8e4f5 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/layout/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ +@NullMarked +package org.apache.logging.converter.config.internal.v1.layout; + +import org.jspecify.annotations.NullMarked; diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/package-info.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/package-info.java new file mode 100644 index 00000000..2aa1cd36 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v1/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ +@NullMarked +package org.apache.logging.converter.config.internal.v1; + +import org.jspecify.annotations.NullMarked; diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/AbstractJacksonConfigurationMapper.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/AbstractJacksonConfigurationMapper.java index d9708f3a..dd4b4885 100644 --- a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/AbstractJacksonConfigurationMapper.java +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/AbstractJacksonConfigurationMapper.java @@ -90,7 +90,7 @@ private static ConfigurationNode parseObjectNode(ObjectNode objectNode, String f builder.addAttribute(childFieldName, childNode.asText()); } }); - return builder.setPluginName(getPluginName(objectNode, fieldName)).build(); + return builder.setPluginName(getPluginName(objectNode, fieldName)).get(); } private static void processArrayNode(ArrayNode arrayNode, String fieldName, ConfigurationNodeBuilder builder) { diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/PropertiesV2ConfigurationParser.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/PropertiesV2ConfigurationParser.java index c46595c2..e1ef6724 100644 --- a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/PropertiesV2ConfigurationParser.java +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/PropertiesV2ConfigurationParser.java @@ -123,9 +123,9 @@ public ConfigurationNode parse(InputStream inputStream) throws IOException { } } // Add the `Loggers` plugin - builder.addChild(loggersBuilder.build()); + builder.addChild(loggersBuilder.get()); - return builder.build(); + return builder.get(); } private static Map extractSubsetAndPartition(Properties rootProperties, String prefix) { @@ -150,13 +150,13 @@ private static Map extractSubsetAndPartition( private static ConfigurationNode processPropertyPlaceholders(final Properties propertyPlaceholders) { ConfigurationNodeBuilder builder = newNodeBuilder().setPluginName(PROPERTIES_PLUGIN_NAME); for (final String key : propertyPlaceholders.stringPropertyNames()) { - builder.addChild(newNodeBuilder() + ConfigurationNodeBuilder builder1 = newNodeBuilder() .setPluginName(PROPERTY_PLUGIN_NAME) .addAttribute(NAME_ATTRIBUTE, key) - .addAttribute(VALUE_ATTRIBUTE, propertyPlaceholders.getProperty(key)) - .build()); + .addAttribute(VALUE_ATTRIBUTE, propertyPlaceholders.getProperty(key)); + builder.addChild(builder1.get()); } - return builder.build(); + return builder.get(); } private ConfigurationNode processScripts(Map scripts) { @@ -166,20 +166,20 @@ private ConfigurationNode processScripts(Map scripts) { Properties scriptProperties = entry.getValue(); builder.addChild(processGenericComponent(scriptPrefix, "Script", scriptProperties)); } - return builder.build(); + return builder.get(); } private ConfigurationNode processCustomLevels(Properties customLevels) { ConfigurationNodeBuilder builder = newNodeBuilder().setPluginName(CUSTOM_LEVELS_PLUGIN_NAME); for (final String key : customLevels.stringPropertyNames()) { String value = validateInteger("customLevel." + key, customLevels.getProperty(key)); - builder.addChild(newNodeBuilder() + ConfigurationNodeBuilder builder1 = newNodeBuilder() .setPluginName(CUSTOM_LEVEL_PLUGIN_NAME) .addAttribute(NAME_ATTRIBUTE, key) - .addAttribute(INT_LEVEL_ATTRIBUTE, value) - .build()); + .addAttribute(INT_LEVEL_ATTRIBUTE, value); + builder.addChild(builder1.get()); } - return builder.build(); + return builder.get(); } private static String validateInteger(String key, String value) { @@ -199,7 +199,7 @@ private static ConfigurationNode processFilters(String prefix, Map filterEntry : filters.entrySet()) { builder.addChild(processFilter(prefix, filterEntry)); } - return builder.build(); + return builder.get(); } private static ConfigurationNode processFilter(String prefix, Map.Entry filterEntry) { @@ -214,7 +214,7 @@ private static ConfigurationNode processAppenders(Map append for (Map.Entry entry : appenders.entrySet()) { builder.addChild(processAppender(entry.getKey(), entry.getValue())); } - return builder.build(); + return builder.get(); } private static ConfigurationNode processAppender(String key, Properties properties) { @@ -232,7 +232,7 @@ private static ConfigurationNode processAppender(String key, Properties properti addFiltersToComponent(appenderPrefix, properties, builder); processRemainingProperties(appenderPrefix, properties, builder); - return builder.build(); + return builder.get(); } private static ConfigurationNode processLogger(String key, Properties properties) { @@ -257,7 +257,7 @@ private static ConfigurationNode processLogger(String key, Properties properties addFiltersToComponent(prefix, properties, builder); processRemainingProperties(prefix, properties, builder); - return builder.build(); + return builder.get(); } private static void addAppenderRefsToComponent( @@ -286,7 +286,7 @@ private static ConfigurationNode processAppenderRef(String prefix, Properties pr addFiltersToComponent(prefix, properties, builder); processRemainingProperties(prefix, properties, builder); - return builder.build(); + return builder.get(); } private static void addFiltersToComponent(String prefix, Properties properties, ConfigurationNodeBuilder builder) { @@ -314,7 +314,7 @@ private static ConfigurationNode processRootLogger(Properties properties) { addFiltersToComponent(prefix, properties, builder); processRemainingProperties(prefix, properties, builder); - return builder.build(); + return builder.get(); } private static @Nullable String remove(final Properties properties, final String key) { @@ -340,7 +340,7 @@ private static ConfigurationNode processGenericComponent( TYPE_ATTRIBUTE, () -> "No type attribute provided for " + componentCategory + " " + prefix)); processRemainingProperties(prefix, properties, builder); - return builder.build(); + return builder.get(); } private static void processRemainingProperties( diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/XmlConfigurationMapper.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/XmlConfigurationMapper.java index bef005c6..8f87c860 100644 --- a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/XmlConfigurationMapper.java +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/internal/v2/XmlConfigurationMapper.java @@ -134,7 +134,7 @@ private static ConfigurationNode parseComplexElement(Element element) { // Handle child attributes NamedNodeMap nodeMap = element.getAttributes(); processAttributes(nodeMap, builder); - return builder.setPluginName(element.getTagName()).build(); + return builder.setPluginName(element.getTagName()).get(); } /** diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/v1/Log4j1ComponentParser.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/v1/Log4j1ComponentParser.java new file mode 100644 index 00000000..a3c544c0 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/v1/Log4j1ComponentParser.java @@ -0,0 +1,47 @@ +/* + * 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.converter.config.spi.v1; + +import org.apache.logging.converter.config.ConfigurationConverterException; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.w3c.dom.Element; + +public interface Log4j1ComponentParser { + + String getClassName(); + + /** + * Parses an XML element of a Log4j 1 configuration file. + * + * @param element An XML element. + * @param context A Log4j 1 parser context. + * @return A Log4j Core 2 configuration node. + * @throws ConfigurationConverterException If a parsing exception occurs. + */ + ConfigurationNode parseXml(Element element, Log4j1ParserContext context) throws ConfigurationConverterException; + + /** + * Parser a subset of configuration properties. + * + * @param properties A subset of configuration properties. + * @param context A Log4j 1 parser context. + * @return A Log4j Core 2 configuration node. + * @throws ConfigurationConverterException If a parsing exception occurs. + */ + ConfigurationNode parseProperties(PropertiesSubset properties, Log4j1ParserContext context) + throws ConfigurationConverterException; +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/v1/Log4j1ParserContext.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/v1/Log4j1ParserContext.java new file mode 100644 index 00000000..64e42d23 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/v1/Log4j1ParserContext.java @@ -0,0 +1,25 @@ +/* + * 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.converter.config.spi.v1; + +/** + * Provides access to other {@link Log4j1ComponentParser} implementations. + */ +public interface Log4j1ParserContext { + + Log4j1ComponentParser getParserForClass(String className); +} diff --git a/log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/v1/PropertiesSubset.java b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/v1/PropertiesSubset.java new file mode 100644 index 00000000..64d823c1 --- /dev/null +++ b/log4j-converter-config/src/main/java/org/apache/logging/converter/config/spi/v1/PropertiesSubset.java @@ -0,0 +1,54 @@ +/* + * 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.converter.config.spi.v1; + +import java.util.AbstractMap; +import java.util.Map; +import java.util.Properties; + +/** + * Represents a subset of configuration properties. + */ +public final class PropertiesSubset { + + private final Map.Entry entry; + + public static PropertiesSubset of(final String prefix, final Properties properties) { + return new PropertiesSubset(prefix, properties); + } + + private PropertiesSubset(final String prefix, final Properties properties) { + this.entry = new AbstractMap.SimpleImmutableEntry<>(prefix, properties); + } + + /** + * The common prefix of this subset in the global configuration properties. + *

+ * Used for debugging messages. + *

+ */ + public String getPrefix() { + return entry.getKey(); + } + + /** + * A subset of the global configuration properties with {@link #getPrefix()} removed. + */ + public Properties getProperties() { + return entry.getValue(); + } +} diff --git a/log4j-converter-config/src/test/java/org/apache/logging/converter/config/ConfigurationConverterTest.java b/log4j-converter-config/src/test/java/org/apache/logging/converter/config/ConfigurationConverterTest.java index d86407bc..382b5718 100644 --- a/log4j-converter-config/src/test/java/org/apache/logging/converter/config/ConfigurationConverterTest.java +++ b/log4j-converter-config/src/test/java/org/apache/logging/converter/config/ConfigurationConverterTest.java @@ -67,7 +67,14 @@ void conversionToXml(String inputResource, String inputFormat) throws IOExceptio @Test void supportedFormats() { assertThat(converter.getSupportedInputFormats()) - .containsExactlyInAnyOrder("v2:json", "v2:properties", DEFAULT_FORMAT, "v2:yaml", "v3:properties"); + .containsExactlyInAnyOrder( + "v1:properties", + "v1:xml", + "v2:json", + "v2:properties", + DEFAULT_FORMAT, + "v2:yaml", + "v3:properties"); assertThat(converter.getSupportedOutputFormats()) .containsExactlyInAnyOrder("v2:json", DEFAULT_FORMAT, "v2:yaml", "v3:properties"); } diff --git a/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/AbstractConfigurationMapperTest.java b/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/AbstractConfigurationMapperTest.java index 3317ef22..73d81b9e 100644 --- a/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/AbstractConfigurationMapperTest.java +++ b/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/AbstractConfigurationMapperTest.java @@ -40,16 +40,16 @@ public abstract class AbstractConfigurationMapperTest { .setPluginName("Property") .addAttribute("name", "pattern") .addAttribute("value", "%d [%t] %-5p %c - %m%n%ex") - .build()) - .build()) + .get()) + .get()) .addChild(newNodeBuilder() .setPluginName("CustomLevels") .addChild(newNodeBuilder() .setPluginName("CustomLevel") .addAttribute("name", "CONFIG") .addAttribute("intLevel", "450") - .build()) - .build()) + .get()) + .get()) .addChild(newNodeBuilder() .setPluginName("Appenders") .addChild(newNodeBuilder() @@ -58,8 +58,8 @@ public abstract class AbstractConfigurationMapperTest { .addAttribute("fileName", "main.log") .addChild(newNodeBuilder() .setPluginName("JsonTemplateLayout") - .build()) - .build()) + .get()) + .get()) .addChild(newNodeBuilder() .setPluginName("File") .addAttribute("name", "AUDIT") @@ -67,12 +67,12 @@ public abstract class AbstractConfigurationMapperTest { .addChild(newNodeBuilder() .setPluginName("MarkerFilter") .addAttribute("marker", "AUDIT") - .build()) + .get()) .addChild(newNodeBuilder() .setPluginName("PatternLayout") .addAttribute("pattern", "${pattern}") - .build()) - .build()) + .get()) + .get()) .addChild(newNodeBuilder() .setPluginName("Console") .addAttribute("name", "CONSOLE") @@ -83,24 +83,24 @@ public abstract class AbstractConfigurationMapperTest { .addAttribute("level", "WARN") .addAttribute("onMatch", "ACCEPT") .addAttribute("onMismatch", "NEUTRAL") - .build()) + .get()) .addChild(newNodeBuilder() .setPluginName("BurstFilter") - .build()) - .build()) + .get()) + .get()) .addChild(newNodeBuilder() .setPluginName("JsonTemplateLayout") - .build()) - .build()) + .get()) + .get()) .addChild(newNodeBuilder() .setPluginName("File") .addAttribute("name", "DEBUG_LOG") .addAttribute("fileName", "debug.log") .addChild(newNodeBuilder() .setPluginName("JsonTemplateLayout") - .build()) - .build()) - .build()) + .get()) + .get()) + .get()) .addChild(newNodeBuilder() .setPluginName("Loggers") .addChild(newNodeBuilder() @@ -112,8 +112,8 @@ public abstract class AbstractConfigurationMapperTest { .addChild(newNodeBuilder() .setPluginName("MarkerFilter") .addAttribute("marker", "PRIVATE") - .build()) - .build()) + .get()) + .get()) .addChild(newNodeBuilder() .setPluginName("AppenderRef") .addAttribute("ref", "CONSOLE") @@ -124,16 +124,16 @@ public abstract class AbstractConfigurationMapperTest { .addAttribute("level", "WARN") .addAttribute("onMatch", "ACCEPT") .addAttribute("onMismatch", "NEUTRAL") - .build()) + .get()) .addChild(newNodeBuilder() .setPluginName("BurstFilter") - .build()) - .build()) - .build()) + .get()) + .get()) + .get()) .addChild(newNodeBuilder() .setPluginName("BurstFilter") - .build()) - .build()) + .get()) + .get()) .addChild(newNodeBuilder() .setPluginName("Logger") .addAttribute("name", "org.apache.logging") @@ -142,11 +142,11 @@ public abstract class AbstractConfigurationMapperTest { .addChild(newNodeBuilder() .setPluginName("AppenderRef") .addAttribute("ref", "AUDIT") - .build()) + .get()) .addChild(newNodeBuilder() .setPluginName("AppenderRef") .addAttribute("ref", "DEBUG_LOG") - .build()) + .get()) .addChild(newNodeBuilder() .setPluginName("Filters") .addChild(newNodeBuilder() @@ -154,15 +154,15 @@ public abstract class AbstractConfigurationMapperTest { .addAttribute("level", "DEBUG") .addAttribute("onMatch", "ACCEPT") .addAttribute("onMismatch", "NEUTRAL") - .build()) + .get()) .addChild(newNodeBuilder() .setPluginName("BurstFilter") .addAttribute("level", "TRACE") - .build()) - .build()) - .build()) - .build()) - .build(); + .get()) + .get()) + .get()) + .get()) + .get(); public static ConfigurationNodeAssert assertThat(ConfigurationNode node) { return new ConfigurationNodeAssert(node, false); diff --git a/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v1/AbstractV1ConfigurationParserTest.java b/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v1/AbstractV1ConfigurationParserTest.java new file mode 100644 index 00000000..071f8d86 --- /dev/null +++ b/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v1/AbstractV1ConfigurationParserTest.java @@ -0,0 +1,229 @@ +/* + * 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.converter.config.internal.v1; + +import static org.apache.logging.converter.config.internal.ComponentUtils.newNodeBuilder; +import static org.apache.logging.converter.config.internal.ComponentUtils.newThresholdFilter; + +import java.util.Arrays; +import org.apache.logging.converter.config.internal.AbstractConfigurationMapperTest; +import org.apache.logging.converter.config.internal.ComponentUtils; +import org.apache.logging.converter.config.internal.ComponentUtils.ConfigurationNodeBuilder; +import org.apache.logging.converter.config.spi.ConfigurationNode; + +public class AbstractV1ConfigurationParserTest extends AbstractConfigurationMapperTest { + + static ConfigurationNode EXAMPLE_V1_CONFIGURATION = newNodeBuilder() + .setPluginName("Configuration") + .addChild(newNodeBuilder() + .setPluginName("Properties") + .addChild(newNodeBuilder() + .setPluginName("Property") + .addAttribute("name", "foo") + .addAttribute("value", "bar") + .get()) + .addChild(newNodeBuilder() + .setPluginName("Property") + .addAttribute("name", "log4j.foo") + .addAttribute("value", "baz") + .get()) + .get()) + .addChild(newNodeBuilder() + .setPluginName("Appenders") + .addChild(newNodeBuilder() + .setPluginName("Async") + .addAttribute("name", "ASYNC") + .addAttribute("blocking", true) + .addAttribute("bufferSize", 512) + .addAttribute("includeLocation", true) + .addChild(newAppenderRef("CONSOLE")) + .addChild(newAppenderRef("DAILY_ROLLING")) + .addChild(newAppenderRef("FILE")) + .addChild(newAppenderRef("ROLLING")) + .get()) + .addChild(newNodeBuilder() + .setPluginName("Console") + .addAttribute("name", "CONSOLE") + .addAttribute("follow", "true") + .addAttribute("immediateFlush", "false") + .addAttribute("target", "SYSTEM_ERR") + .addChild(newThresholdFilter("WARN")) + .addChild(newSimpleLayout()) + .get()) + .addChild(newNodeBuilder() + .setPluginName("RollingFile") + .addAttribute("name", "DAILY_ROLLING") + .addAttribute("append", false) + .addAttribute("bufferSize", 1024) + .addAttribute("bufferedIo", true) + .addAttribute("fileName", "file.log") + .addAttribute("filePattern", "file.log%d{.yyyy_MM_dd}") + .addAttribute("immediateFlush", false) + .addChild(newThresholdFilter("WARN")) + .addChild(newSimpleLayout()) + .addChild(newNodeBuilder() + .setPluginName("TimeBasedTriggeringPolicy") + .addAttribute("modulate", true) + .get()) + .get()) + .addChild(newNodeBuilder() + .setPluginName("File") + .addAttribute("name", "FILE") + .addAttribute("append", false) + .addAttribute("bufferSize", 1024) + .addAttribute("bufferedIo", true) + .addAttribute("fileName", "file.log") + .addAttribute("immediateFlush", false) + .addChild(newThresholdFilter("WARN")) + .addChild(newSimpleLayout()) + .get()) + .addChild(newNodeBuilder() + .setPluginName("RollingFile") + .addAttribute("name", "ROLLING") + .addAttribute("append", false) + .addAttribute("bufferSize", 1024) + .addAttribute("bufferedIo", true) + .addAttribute("fileName", "file.log") + .addAttribute("filePattern", "file.log.%i") + .addAttribute("immediateFlush", false) + .addChild(newThresholdFilter("WARN")) + .addChild(newSimpleLayout()) + .addChild(newNodeBuilder() + .setPluginName("SizeBasedTriggeringPolicy") + .addAttribute("size", "10.00 GB") + .get()) + .addChild(newNodeBuilder() + .setPluginName("DefaultRolloverStrategy") + .addAttribute("fileIndex", "min") + .addAttribute("max", "30") + .get()) + .get()) + .addChild(newConsoleAppenderBuilder("FILTERS") + .addChild(newCompositeFilter( + newNodeBuilder() + .setPluginName("DenyAllFilter") + .get(), + newNodeBuilder() + .setPluginName("LevelMatchFilter") + .addAttribute("onMatch", "ACCEPT") + .addAttribute("onMismatch", "NEUTRAL") + .addAttribute("level", "WARN") + .get(), + newNodeBuilder() + .setPluginName("LevelRangeFilter") + .addAttribute("onMatch", "ACCEPT") + .addAttribute("onMismatch", "NEUTRAL") + .addAttribute("minLevel", "INFO") + .addAttribute("maxLevel", "DEBUG") + .get(), + newNodeBuilder() + .setPluginName("StringMatchFilter") + .addAttribute("onMatch", "ACCEPT") + .addAttribute("onMismatch", "NEUTRAL") + .addAttribute("text", "Hello") + .get())) + .addChild(newSimpleLayout()) + .get()) + .addChild(newConsoleAppenderBuilder("HTML") + .addChild(newNodeBuilder() + .setPluginName("HtmlLayout") + .addAttribute("locationInfo", true) + .addAttribute("title", "Example HTML Layout") + .get()) + .get()) + .addChild(newConsoleAppenderBuilder("PATTERN") + .addChild(newNodeBuilder() + .setPluginName("PatternLayout") + .addAttribute("pattern", "%d [%t] %-5p %c - %m%n%ex") + .get()) + .get()) + .addChild(newConsoleAppenderBuilder("EPATTERN") + .addChild(newNodeBuilder() + .setPluginName("PatternLayout") + .addAttribute("pattern", "%d [%t] %-5p %c - %m%n%ex") + .get()) + .get()) + .addChild(newConsoleAppenderBuilder("SIMPLE") + .addAttribute("name", "SIMPLE") + .addChild(newSimpleLayout()) + .get()) + .addChild(newConsoleAppenderBuilder("TTCC") + .addChild(newNodeBuilder() + .setPluginName("PatternLayout") + .addAttribute( + "pattern", + "%d{yyyy-MM-dd HH:mm:ss,SSS}{UTC} [%t] %p %c %notEmpty{%NDC }- %m%n") + .get()) + .get()) + .get()) + .addChild(newThresholdFilter("INFO")) + .addChild(newNodeBuilder() + .setPluginName("Loggers") + .addChild(newNodeBuilder() + .setPluginName("Root") + .addAttribute("level", "INFO") + .addChild(newAppenderRef("CONSOLE")) + .get()) + .addChild(newNodeBuilder() + .setPluginName("Logger") + .addAttribute("additivity", "false") + .addAttribute("level", "DEBUG") + .addAttribute("name", "org.apache.logging") + .addChild(newAppenderRef("CONSOLE")) + .addChild(newAppenderRef("DAILY_ROLLING")) + .addChild(newAppenderRef("FILE")) + .addChild(newAppenderRef("ROLLING")) + .get()) + .get()) + .get(); + + static ConfigurationNode filterOutPlugin(ConfigurationNode node, String pluginName) { + ConfigurationNodeBuilder builder = newNodeBuilder().setPluginName(node.getPluginName()); + node.getAttributes().forEach(builder::addAttribute); + for (ConfigurationNode child : node.getChildren()) { + if (!pluginName.equals(child.getPluginName())) { + builder.addChild(filterOutPlugin(child, pluginName)); + } + } + return builder.get(); + } + + private static ConfigurationNode newSimpleLayout() { + return newNodeBuilder() + .setPluginName("PatternLayout") + .addAttribute("alwaysWriteExceptions", false) + .addAttribute("pattern", "%p - %m%n") + .get(); + } + + private static ConfigurationNode newAppenderRef(String ref) { + return ComponentUtils.newAppenderRef(ref); + } + + private static ConfigurationNode newCompositeFilter(ConfigurationNode... filters) { + return ComponentUtils.newCompositeFilter(Arrays.asList(filters)); + } + + private static ConfigurationNodeBuilder newConsoleAppenderBuilder(String name) { + return newNodeBuilder() + .setPluginName("Console") + .addAttribute("name", name) + .addAttribute("follow", false) + .addAttribute("immediateFlush", true) + .addAttribute("target", "SYSTEM_OUT"); + } +} diff --git a/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v1/PropertiesV1ConfigurationParserTest.java b/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v1/PropertiesV1ConfigurationParserTest.java new file mode 100644 index 00000000..83bccaa9 --- /dev/null +++ b/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v1/PropertiesV1ConfigurationParserTest.java @@ -0,0 +1,38 @@ +/* + * 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.converter.config.internal.v1; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.apache.logging.converter.config.spi.ConfigurationParser; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PropertiesV1ConfigurationParserTest extends AbstractV1ConfigurationParserTest { + + @ParameterizedTest + @ValueSource(strings = {"/v1/log4j-uppercase.properties", "/v1/log4j-lowercase.properties"}) + void convertFromProperties(String resource) throws IOException { + ConfigurationParser parser = new PropertiesV1ConfigurationParser(); + try (InputStream inputStream = getClass().getResourceAsStream(resource)) { + ConfigurationNode actual = parser.parse(Objects.requireNonNull(inputStream)); + assertThat(actual).ignoringOrder().isEqualTo(filterOutPlugin(EXAMPLE_V1_CONFIGURATION, "Async")); + } + } +} diff --git a/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v1/XmlV1ConfigurationParserTest.java b/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v1/XmlV1ConfigurationParserTest.java new file mode 100644 index 00000000..82bbfceb --- /dev/null +++ b/log4j-converter-config/src/test/java/org/apache/logging/converter/config/internal/v1/XmlV1ConfigurationParserTest.java @@ -0,0 +1,38 @@ +/* + * 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.converter.config.internal.v1; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; +import org.apache.logging.converter.config.spi.ConfigurationNode; +import org.apache.logging.converter.config.spi.ConfigurationParser; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class XmlV1ConfigurationParserTest extends AbstractV1ConfigurationParserTest { + + @ParameterizedTest + @ValueSource(strings = {"/v1/log4j-uppercase.xml", "/v1/log4j-lowercase.xml"}) + void convertFromXml(String resource) throws IOException { + ConfigurationParser parser = new XmlV1ConfigurationParser(); + try (InputStream inputStream = getClass().getResourceAsStream(resource)) { + ConfigurationNode actual = parser.parse(Objects.requireNonNull(inputStream)); + assertThat(actual).isEqualTo(filterOutPlugin(EXAMPLE_V1_CONFIGURATION, "Properties")); + } + } +} diff --git a/log4j-converter-config/src/test/resources/v1/log4j-lowercase.properties b/log4j-converter-config/src/test/resources/v1/log4j-lowercase.properties new file mode 100644 index 00000000..6448ef0e --- /dev/null +++ b/log4j-converter-config/src/test/resources/v1/log4j-lowercase.properties @@ -0,0 +1,118 @@ +# +# 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. +# +## +## +# ThresholdFilter +log4j.threshold = INFO + +## +# Properties +foo = bar +log4j.foo = baz + +## +# Appenders +log4j.appender.CONSOLE = org.apache.log4j.ConsoleAppender +log4j.appender.CONSOLE.follow = true +log4j.appender.CONSOLE.immediateFlush = false +log4j.appender.CONSOLE.target = System.err +log4j.appender.CONSOLE.threshold = WARN +log4j.appender.CONSOLE.layout = org.apache.log4j.SimpleLayout + +log4j.appender.DAILY_ROLLING = org.apache.log4j.DailyRollingFileAppender +log4j.appender.DAILY_ROLLING.append = false +log4j.appender.DAILY_ROLLING.bufferedIO = true +log4j.appender.DAILY_ROLLING.bufferSize = 1024 +log4j.appender.DAILY_ROLLING.datePattern = .yyyy_MM_dd +log4j.appender.DAILY_ROLLING.file = file.log +log4j.appender.DAILY_ROLLING.immediateFlush = false +log4j.appender.DAILY_ROLLING.threshold = WARN +log4j.appender.DAILY_ROLLING.layout = org.apache.log4j.SimpleLayout + +log4j.appender.FILE = org.apache.log4j.FileAppender +log4j.appender.FILE.append = false +log4j.appender.FILE.bufferedIO = true +log4j.appender.FILE.bufferSize = 1024 +log4j.appender.FILE.file = file.log +log4j.appender.FILE.immediateFlush = false +log4j.appender.FILE.threshold = WARN +log4j.appender.FILE.layout = org.apache.log4j.SimpleLayout + +log4j.appender.ROLLING = org.apache.log4j.RollingFileAppender +log4j.appender.ROLLING.append = false +log4j.appender.ROLLING.bufferedIO = true +log4j.appender.ROLLING.bufferSize = 1024 +log4j.appender.ROLLING.file = file.log +log4j.appender.ROLLING.immediateFlush = false +log4j.appender.ROLLING.maxBackupIndex = 30 +# Exactly 10 GiB +log4j.appender.ROLLING.maxFileSize = 10737418240 +log4j.appender.ROLLING.threshold = WARN +log4j.appender.ROLLING.layout = org.apache.log4j.SimpleLayout + +## +# Filters +log4j.appender.FILTERS = org.apache.log4j.ConsoleAppender +log4j.appender.FILTERS.layout = org.apache.log4j.SimpleLayout + +log4j.appender.FILTERS.filter.f1 = org.apache.log4j.varia.DenyAllFilter + +log4j.appender.FILTERS.filter.f2 = org.apache.log4j.varia.LevelMatchFilter +log4j.appender.FILTERS.filter.f2.acceptOnMatch = true +log4j.appender.FILTERS.filter.f2.levelToMatch = WARN + +log4j.appender.FILTERS.filter.f3 = org.apache.log4j.varia.LevelRangeFilter +log4j.appender.FILTERS.filter.f3.acceptOnMatch = true +log4j.appender.FILTERS.filter.f3.levelMin = DEBUG +log4j.appender.FILTERS.filter.f3.levelMax = INFO + +log4j.appender.FILTERS.filter.f4 = org.apache.log4j.varia.StringMatchFilter +log4j.appender.FILTERS.filter.f4.acceptOnMatch = true +log4j.appender.FILTERS.filter.f4.stringToMatch = Hello + +## +# Layouts +log4j.appender.HTML = org.apache.log4j.ConsoleAppender +log4j.appender.HTML.layout = org.apache.log4j.HTMLLayout +log4j.appender.HTML.layout.locationInfo = true +log4j.appender.HTML.layout.title = Example HTML Layout + +log4j.appender.PATTERN = org.apache.log4j.ConsoleAppender +log4j.appender.PATTERN.layout = org.apache.log4j.PatternLayout +log4j.appender.PATTERN.layout.conversionPattern = %d [%t] %-5p %c - %m%n%ex + +log4j.appender.EPATTERN = org.apache.log4j.ConsoleAppender +log4j.appender.EPATTERN.layout = org.apache.log4j.EnhancedPatternLayout +log4j.appender.EPATTERN.layout.conversionPattern = %d [%t] %-5p %c - %m%n%ex + +log4j.appender.SIMPLE = org.apache.log4j.ConsoleAppender +log4j.appender.SIMPLE.layout = org.apache.log4j.SimpleLayout + +log4j.appender.TTCC = org.apache.log4j.ConsoleAppender +log4j.appender.TTCC.layout = org.apache.log4j.TTCCLayout +log4j.appender.TTCC.layout.categoryPrefixing = true +log4j.appender.TTCC.layout.contextPrinting = true +log4j.appender.TTCC.layout.dateFormat = ISO8601 +log4j.appender.TTCC.layout.threadPrinting = true +log4j.appender.TTCC.layout.timeZone = UTC + +## +# Loggers +log4j.rootLogger = INFO, CONSOLE + +log4j.logger.org.apache.logging = DEBUG, CONSOLE, DAILY_ROLLING, FILE, ROLLING +log4j.additivity.org.apache.logging = false diff --git a/log4j-converter-config/src/test/resources/v1/log4j-lowercase.xml b/log4j-converter-config/src/test/resources/v1/log4j-lowercase.xml new file mode 100644 index 00000000..385cee4f --- /dev/null +++ b/log4j-converter-config/src/test/resources/v1/log4j-lowercase.xml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/log4j-converter-config/src/test/resources/v1/log4j-uppercase.properties b/log4j-converter-config/src/test/resources/v1/log4j-uppercase.properties new file mode 100644 index 00000000..66bc9e01 --- /dev/null +++ b/log4j-converter-config/src/test/resources/v1/log4j-uppercase.properties @@ -0,0 +1,118 @@ +# +# 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. +# +## +## +# ThresholdFilter +log4j.threshold = INFO + +## +# Properties +foo = bar +log4j.foo = baz + +## +# Appenders +log4j.appender.CONSOLE = org.apache.log4j.ConsoleAppender +log4j.appender.CONSOLE.Follow = true +log4j.appender.CONSOLE.ImmediateFlush = false +log4j.appender.CONSOLE.Target = System.err +log4j.appender.CONSOLE.Threshold = WARN +log4j.appender.CONSOLE.layout = org.apache.log4j.SimpleLayout + +log4j.appender.DAILY_ROLLING = org.apache.log4j.DailyRollingFileAppender +log4j.appender.DAILY_ROLLING.Append = false +log4j.appender.DAILY_ROLLING.BufferedIO = true +log4j.appender.DAILY_ROLLING.BufferSize = 1024 +log4j.appender.DAILY_ROLLING.DatePattern = .yyyy_MM_dd +log4j.appender.DAILY_ROLLING.File = file.log +log4j.appender.DAILY_ROLLING.ImmediateFlush = false +log4j.appender.DAILY_ROLLING.Threshold = WARN +log4j.appender.DAILY_ROLLING.layout = org.apache.log4j.SimpleLayout + +log4j.appender.FILE = org.apache.log4j.FileAppender +log4j.appender.FILE.Append = false +log4j.appender.FILE.BufferedIO = true +log4j.appender.FILE.BufferSize = 1024 +log4j.appender.FILE.File = file.log +log4j.appender.FILE.ImmediateFlush = false +log4j.appender.FILE.Threshold = WARN +log4j.appender.FILE.layout = org.apache.log4j.SimpleLayout + +log4j.appender.ROLLING = org.apache.log4j.RollingFileAppender +log4j.appender.ROLLING.Append = false +log4j.appender.ROLLING.BufferedIO = true +log4j.appender.ROLLING.BufferSize = 1024 +log4j.appender.ROLLING.File = file.log +log4j.appender.ROLLING.ImmediateFlush = false +log4j.appender.ROLLING.MaxBackupIndex = 30 +# Exactly 10 GiB +log4j.appender.ROLLING.MaxFileSize = 10737418240 +log4j.appender.ROLLING.Threshold = WARN +log4j.appender.ROLLING.layout = org.apache.log4j.SimpleLayout + +## +# Filters +log4j.appender.FILTERS = org.apache.log4j.ConsoleAppender +log4j.appender.FILTERS.layout = org.apache.log4j.SimpleLayout + +log4j.appender.FILTERS.filter.f1 = org.apache.log4j.varia.DenyAllFilter + +log4j.appender.FILTERS.filter.f2 = org.apache.log4j.varia.LevelMatchFilter +log4j.appender.FILTERS.filter.f2.AcceptOnMatch = true +log4j.appender.FILTERS.filter.f2.LevelToMatch = WARN + +log4j.appender.FILTERS.filter.f3 = org.apache.log4j.varia.LevelRangeFilter +log4j.appender.FILTERS.filter.f3.AcceptOnMatch = true +log4j.appender.FILTERS.filter.f3.LevelMin = DEBUG +log4j.appender.FILTERS.filter.f3.LevelMax = INFO + +log4j.appender.FILTERS.filter.f4 = org.apache.log4j.varia.StringMatchFilter +log4j.appender.FILTERS.filter.f4.AcceptOnMatch = true +log4j.appender.FILTERS.filter.f4.StringToMatch = Hello + +## +# Layouts +log4j.appender.HTML = org.apache.log4j.ConsoleAppender +log4j.appender.HTML.layout = org.apache.log4j.HTMLLayout +log4j.appender.HTML.layout.LocationInfo = true +log4j.appender.HTML.layout.Title = Example HTML Layout + +log4j.appender.PATTERN = org.apache.log4j.ConsoleAppender +log4j.appender.PATTERN.layout = org.apache.log4j.PatternLayout +log4j.appender.PATTERN.layout.ConversionPattern = %d [%t] %-5p %c - %m%n%ex + +log4j.appender.EPATTERN = org.apache.log4j.ConsoleAppender +log4j.appender.EPATTERN.layout = org.apache.log4j.EnhancedPatternLayout +log4j.appender.EPATTERN.layout.ConversionPattern = %d [%t] %-5p %c - %m%n%ex + +log4j.appender.SIMPLE = org.apache.log4j.ConsoleAppender +log4j.appender.SIMPLE.layout = org.apache.log4j.SimpleLayout + +log4j.appender.TTCC = org.apache.log4j.ConsoleAppender +log4j.appender.TTCC.layout = org.apache.log4j.TTCCLayout +log4j.appender.TTCC.layout.CategoryPrefixing = true +log4j.appender.TTCC.layout.ContextPrinting = true +log4j.appender.TTCC.layout.DateFormat = ISO8601 +log4j.appender.TTCC.layout.ThreadPrinting = true +log4j.appender.TTCC.layout.TimeZone = UTC + +## +# Loggers +log4j.rootLogger = INFO, CONSOLE + +log4j.logger.org.apache.logging = DEBUG, CONSOLE, DAILY_ROLLING, FILE, ROLLING +log4j.additivity.org.apache.logging = false diff --git a/log4j-converter-config/src/test/resources/v1/log4j-uppercase.xml b/log4j-converter-config/src/test/resources/v1/log4j-uppercase.xml new file mode 100644 index 00000000..9b3447f5 --- /dev/null +++ b/log4j-converter-config/src/test/resources/v1/log4j-uppercase.xml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/log4j-converter-config/src/test/resources/v1/log4j.dtd b/log4j-converter-config/src/test/resources/v1/log4j.dtd new file mode 100644 index 00000000..f37f04a4 --- /dev/null +++ b/log4j-converter-config/src/test/resources/v1/log4j.dtd @@ -0,0 +1,237 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 84a4adcbc8fc44055ab2466ca071192e7fd5c239 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Wed, 27 Nov 2024 15:24:16 +0100 Subject: [PATCH 5/5] Update documentation --- .../modules/ROOT/pages/log4j-converter-config.adoc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/site/antora/modules/ROOT/pages/log4j-converter-config.adoc b/src/site/antora/modules/ROOT/pages/log4j-converter-config.adoc index f502670f..48a16fd4 100644 --- a/src/site/antora/modules/ROOT/pages/log4j-converter-config.adoc +++ b/src/site/antora/modules/ROOT/pages/log4j-converter-config.adoc @@ -78,6 +78,16 @@ The library provides an out-of-the-box support for the following configuration f |=== | Format name | Format id | Parsing support | Writing support +| {logging-services-url}/log4j/1.x/apidocs/org/apache/log4j/PropertyConfigurator.html[Log4j 1 Properties] +| v1:properties +| yes +| no + +| {logging-services-url}/log4j/1.x/apidocs/org/apache/log4j/xml/DOMConfigurator.html[Log4j 1 XML] +| v1:xml +| yes +| no + | {logging-services-url}/log4j/2.x/manual/configuration.html#xml[Log4j Core 2 XML] | v2:xml | yes