+ */
+@Command(name = "convert")
+public final class Log4j1ConfigurationConverter implements Runnable {
+
+ @Option(
+ names = {"--help", "-h"},
+ usageHelp = true,
+ description = "Usage help.")
+ private boolean help;
+
+ @Option(
+ names = {"--failfast", "-f"},
+ description = "Fails on the first failure in recurse mode.")
+ private boolean failFast;
+
+ @Option(
+ names = {"--in", "-i"},
+ description = "Specifies the input file.")
+ private Path pathIn;
+
+ @Option(
+ names = {"--out", "-o"},
+ description = "Specifies the output file.")
+ private Path pathOut;
+
+ @Option(
+ names = {"--recurse", "-r"},
+ description = "Recurses into this folder looking for the input file")
+ private Path recurseIntoPath;
+
+ @Option(
+ names = {"--verbose", "-v"},
+ description = "Be verbose.")
+ private boolean verbose;
+
+ private Log4j1ConfigurationConverter() {}
+
+ public boolean isHelp() {
+ return help;
+ }
+
+ public Path getPathIn() {
+ return pathIn;
+ }
+
+ public Path getPathOut() {
+ return pathOut;
+ }
+
+ public Path getRecurseIntoPath() {
+ return recurseIntoPath;
+ }
+
+ public boolean isFailFast() {
+ return failFast;
+ }
+
+ public boolean isVerbose() {
+ return verbose;
+ }
+
+ public void setHelp(boolean help) {
+ this.help = help;
+ }
+
+ public void setFailFast(final boolean failFast) {
+ this.failFast = failFast;
+ }
+
+ public void setPathIn(final Path pathIn) {
+ this.pathIn = pathIn;
+ }
+
+ public void setPathOut(final Path pathOut) {
+ this.pathOut = pathOut;
+ }
+
+ public void setRecurseIntoPath(final Path recurseIntoPath) {
+ this.recurseIntoPath = recurseIntoPath;
+ }
+
+ public void setVerbose(final boolean verbose) {
+ this.verbose = verbose;
+ }
+
+ private static final String FILE_EXT_XML = ".xml";
+
+ public static void main(final String[] args) {
+ final Log4j1ConfigurationConverter command = new Log4j1ConfigurationConverter();
+ new CommandLine(command).execute(args);
+ }
+
+ private void convert(final InputStream input, final OutputStream output) throws IOException {
+ final ConfigurationBuilder builder =
+ new Log4j1ConfigurationParser().buildConfigurationBuilder(input);
+ builder.writeXmlConfiguration(output);
+ }
+
+ private InputStream getInputStream() throws IOException {
+ final Path pathIn = getPathIn();
+ return pathIn == null ? System.in : new InputStreamWrapper(Files.newInputStream(pathIn), pathIn.toString());
+ }
+
+ private OutputStream getOutputStream() throws IOException {
+ final Path pathOut = getPathOut();
+ return pathOut == null ? System.out : Files.newOutputStream(pathOut);
+ }
+
+ @Override
+ public void run() {
+ final Path recurseIntoPath = getRecurseIntoPath();
+ final Path pathIn = getPathIn();
+ if (recurseIntoPath != null) {
+ final AtomicInteger countOKs = new AtomicInteger();
+ final AtomicInteger countFails = new AtomicInteger();
+ try {
+ Files.walkFileTree(recurseIntoPath, new SimpleFileVisitor() {
+ @Override
+ public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs)
+ throws IOException {
+ if (pathIn == null || pathIn.equals(file.getFileName())) {
+ verbose("Reading %s", file);
+ String newFile = String.valueOf(file.getFileName());
+ final int lastIndex = newFile.lastIndexOf(".");
+ newFile = lastIndex < 0
+ ? newFile + FILE_EXT_XML
+ : newFile.substring(0, lastIndex) + FILE_EXT_XML;
+ final Path resolvedPath = file.resolveSibling(newFile);
+ try (final InputStream input =
+ new InputStreamWrapper(Files.newInputStream(file), file.toString());
+ final OutputStream output = Files.newOutputStream(resolvedPath)) {
+ try {
+ final ByteArrayOutputStream tmpOutput = new ByteArrayOutputStream();
+ convert(input, tmpOutput);
+ tmpOutput.close();
+ DefaultConfigurationBuilder.formatXml(
+ new StreamSource(new ByteArrayInputStream(tmpOutput.toByteArray())),
+ new StreamResult(output));
+ countOKs.incrementAndGet();
+ } catch (ConfigurationException | IOException e) {
+ countFails.incrementAndGet();
+ if (isFailFast()) {
+ throw e;
+ }
+ e.printStackTrace();
+ } catch (TransformerException e) {
+ countFails.incrementAndGet();
+ if (isFailFast()) {
+ throw new IOException(e);
+ }
+ e.printStackTrace();
+ }
+ verbose("Wrote %s", resolvedPath);
+ }
+ }
+ return FileVisitResult.CONTINUE;
+ }
+ });
+ } catch (final IOException e) {
+ throw new ConfigurationException(e);
+ } finally {
+ verbose(
+ "OK = %,d, Failures = %,d, Total = %,d",
+ countOKs.get(), countFails.get(), countOKs.get() + countFails.get());
+ }
+ } else {
+ verbose("Reading %s", pathIn);
+ try (final InputStream input = getInputStream();
+ final OutputStream output = getOutputStream()) {
+ convert(input, output);
+ } catch (final IOException e) {
+ throw new ConfigurationException(e);
+ }
+ verbose("Wrote %s", getPathOut());
+ }
+ }
+
+ private void verbose(final String template, final Object... args) {
+ if (isVerbose()) {
+ System.err.println(String.format(template, args));
+ }
+ }
+}
diff --git a/log4j-config-converter/src/main/java/org/apache/logging/log4j/config/converter/Log4j1ConfigurationParser.java b/log4j-config-converter/src/main/java/org/apache/logging/log4j/config/converter/Log4j1ConfigurationParser.java
new file mode 100644
index 00000000..e7ce75a2
--- /dev/null
+++ b/log4j-config-converter/src/main/java/org/apache/logging/log4j/config/converter/Log4j1ConfigurationParser.java
@@ -0,0 +1,443 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.config.converter;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.*;
+import org.apache.log4j.config.PropertiesConfiguration;
+import org.apache.log4j.helpers.OptionConverter;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.core.Filter.Result;
+import org.apache.logging.log4j.core.appender.ConsoleAppender;
+import org.apache.logging.log4j.core.appender.FileAppender;
+import org.apache.logging.log4j.core.appender.NullAppender;
+import org.apache.logging.log4j.core.appender.RollingFileAppender;
+import org.apache.logging.log4j.core.config.ConfigurationException;
+import org.apache.logging.log4j.core.config.builder.api.*;
+import org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.Strings;
+
+/**
+ * Experimental parser for Log4j 1.2 properties configuration files.
+ *
+ * This class is not thread-safe.
+ *
+ *
+ * From the Log4j 1.2 Javadocs:
+ *
+ *
+ * All option values admit variable substitution. The syntax of variable substitution is similar to that of Unix shells. The string between
+ * an opening "${" and closing "}" is interpreted as a key. The value of the substituted variable can be defined as a system property or in
+ * the configuration file itself. The value of the key is first searched in the system properties, and if not found there, it is then
+ * searched in the configuration file being parsed. The corresponding value replaces the ${variableName} sequence. For example, if java.home
+ * system property is set to /home/xyz, then every occurrence of the sequence ${java.home} will be interpreted as /home/xyz.
+ *
+ */
+public class Log4j1ConfigurationParser {
+
+ private static final String COMMA_DELIMITED_RE = "\\s*,\\s*";
+ private static final String ROOTLOGGER = "rootLogger";
+ private static final String ROOTCATEGORY = "rootCategory";
+ private static final String TRUE = "true";
+ private static final String FALSE = "false";
+ private static final String RELATIVE = "RELATIVE";
+ private static final String NULL = "NULL";
+
+ private final Properties properties = new Properties();
+
+ private final ConfigurationBuilder builder =
+ ConfigurationBuilderFactory.newConfigurationBuilder();
+
+ /**
+ * Parses a Log4j 1.2 properties configuration file in ISO 8859-1 encoding into a ConfigurationBuilder.
+ *
+ * @param input
+ * InputStream to read from is assumed to be ISO 8859-1, and will not be closed.
+ * @return the populated ConfigurationBuilder, never {@code null}
+ * @throws IOException
+ * if unable to read the input
+ * @throws ConfigurationException
+ * if the input does not contain a valid configuration
+ */
+ ConfigurationBuilder buildConfigurationBuilder(final InputStream input) throws IOException {
+ try {
+ properties.load(input);
+ final String rootCategoryValue = getLog4jValue(ROOTCATEGORY);
+ final String rootLoggerValue = getLog4jValue(ROOTLOGGER);
+ if (rootCategoryValue == null && rootLoggerValue == null) {
+ // This is not a Log4j 1 properties configuration file.
+ warn("Missing " + ROOTCATEGORY + " or " + ROOTLOGGER + " in " + input);
+ // throw new ConfigurationException(
+ // "Missing " + ROOTCATEGORY + " or " + ROOTLOGGER + " in " + input);
+ }
+ builder.setConfigurationName("Log4j1");
+ // DEBUG
+ final String debugValue = getLog4jValue("debug");
+ if (Boolean.parseBoolean(debugValue)) {
+ builder.setStatusLevel(Level.DEBUG);
+ }
+ // global threshold
+ final String threshold = OptionConverter.findAndSubst(PropertiesConfiguration.THRESHOLD_KEY, properties);
+ if (threshold != null) {
+ final Level level = OptionConverter.convertLevel(threshold.trim(), Level.ALL);
+ builder.add(builder.newFilter("ThresholdFilter", Result.NEUTRAL, Result.DENY)
+ .addAttribute("level", level));
+ }
+ // Root
+ buildRootLogger(getLog4jValue(ROOTCATEGORY));
+ buildRootLogger(getLog4jValue(ROOTLOGGER));
+ // Appenders
+ final Map appenderNameToClassName = buildClassToPropertyPrefixMap();
+ for (final Map.Entry entry : appenderNameToClassName.entrySet()) {
+ final String appenderName = entry.getKey();
+ final String appenderClass = entry.getValue();
+ buildAppender(appenderName, appenderClass);
+ }
+ // Loggers
+ buildLoggers("log4j.category.");
+ buildLoggers("log4j.logger.");
+ buildProperties();
+ return builder;
+ } catch (final IllegalArgumentException e) {
+ throw new ConfigurationException(e);
+ }
+ }
+
+ private void buildProperties() {
+ for (final Map.Entry