blob: 112ab4247fb29e0e121d3225928f15321e73e965 [file] [log] [blame]
/*
* 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.log4j.config;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.TreeMap;
import org.apache.logging.log4j.Level;
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.AppenderComponentBuilder;
import org.apache.logging.log4j.core.config.builder.api.ComponentBuilder;
import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilder;
import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory;
import org.apache.logging.log4j.core.config.builder.api.LayoutComponentBuilder;
import org.apache.logging.log4j.core.config.builder.api.LoggerComponentBuilder;
import org.apache.logging.log4j.core.config.builder.api.RootLoggerComponentBuilder;
import org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration;
import org.apache.logging.log4j.core.lookup.StrSubstitutor;
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.
*
* <p>
* From the Log4j 1.2 Javadocs:
* </p>
* <p>
* 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.
* </p>
*/
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 final Properties properties = new Properties();
private StrSubstitutor strSubstitutorProperties;
private StrSubstitutor strSubstitutorSystem;
private final ConfigurationBuilder<BuiltConfiguration> 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 {@literal null}
* @throws IOException
* if unable to read the input
* @throws ConfigurationException
* if the input does not contain a valid configuration
*/
public ConfigurationBuilder<BuiltConfiguration> buildConfigurationBuilder(final InputStream input)
throws IOException {
try {
properties.load(input);
strSubstitutorProperties = new StrSubstitutor(properties);
strSubstitutorSystem = new StrSubstitutor(System.getProperties());
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.valueOf(debugValue)) {
builder.setStatusLevel(Level.DEBUG);
}
// Root
buildRootLogger(getLog4jValue(ROOTCATEGORY));
buildRootLogger(getLog4jValue(ROOTLOGGER));
// Appenders
final Map<String, String> appenderNameToClassName = buildClassToPropertyPrefixMap();
for (final Map.Entry<String, String> 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<Object, Object> entry : new TreeMap<>(properties).entrySet()) {
final String key = entry.getKey().toString();
if (!key.startsWith("log4j.") && !key.equals(ROOTCATEGORY) && !key.equals(ROOTLOGGER)) {
builder.addProperty(key, Objects.toString(entry.getValue(), Strings.EMPTY));
}
}
}
private void warn(final String string) {
System.err.println(string);
}
private Map<String, String> buildClassToPropertyPrefixMap() {
final String prefix = "log4j.appender.";
final int preLength = prefix.length();
final Map<String, String> map = new HashMap<>();
for (final Map.Entry<Object, Object> entry : properties.entrySet()) {
final Object keyObj = entry.getKey();
if (keyObj != null) {
final String key = keyObj.toString();
if (key.startsWith(prefix)) {
if (key.indexOf('.', preLength) < 0) {
final String name = key.substring(preLength);
final Object value = entry.getValue();
if (value != null) {
map.put(name, value.toString());
}
}
}
}
}
return map;
}
private void buildAppender(final String appenderName, final String appenderClass) {
switch (appenderClass) {
case "org.apache.log4j.ConsoleAppender":
buildConsoleAppender(appenderName);
break;
case "org.apache.log4j.FileAppender":
buildFileAppender(appenderName);
break;
case "org.apache.log4j.DailyRollingFileAppender":
buildDailyRollingFileAppender(appenderName);
break;
case "org.apache.log4j.RollingFileAppender":
buildRollingFileAppender(appenderName);
break;
case "org.apache.log4j.varia.NullAppender":
buildNullAppender(appenderName);
break;
default:
reportWarning("Unknown appender class: " + appenderClass + "; ignoring appender: " + appenderName);
}
}
private void buildConsoleAppender(final String appenderName) {
final AppenderComponentBuilder appenderBuilder = builder.newAppender(appenderName, ConsoleAppender.PLUGIN_NAME);
final String targetValue = getLog4jAppenderValue(appenderName, "Target", "System.out");
if (targetValue != null) {
final ConsoleAppender.Target target;
switch (targetValue) {
case "System.out":
target = ConsoleAppender.Target.SYSTEM_OUT;
break;
case "System.err":
target = ConsoleAppender.Target.SYSTEM_ERR;
break;
default:
reportWarning("Unknown value for console Target: " + targetValue);
target = null;
}
if (target != null) {
appenderBuilder.addAttribute("target", target);
}
}
buildAttribute(appenderName, appenderBuilder, "Follow", "follow");
if (FALSE.equalsIgnoreCase(getLog4jAppenderValue(appenderName, "ImmediateFlush"))) {
reportWarning("ImmediateFlush=false is not supported on Console appender");
}
buildAppenderLayout(appenderName, appenderBuilder);
builder.add(appenderBuilder);
}
private void buildFileAppender(final String appenderName) {
final AppenderComponentBuilder appenderBuilder = builder.newAppender(appenderName, FileAppender.PLUGIN_NAME);
buildFileAppender(appenderName, appenderBuilder);
builder.add(appenderBuilder);
}
private void buildFileAppender(final String appenderName, final AppenderComponentBuilder appenderBuilder) {
buildMandatoryAttribute(appenderName, appenderBuilder, "File", "fileName");
buildAttribute(appenderName, appenderBuilder, "Append", "append");
buildAttribute(appenderName, appenderBuilder, "BufferedIO", "bufferedIo");
buildAttribute(appenderName, appenderBuilder, "BufferSize", "bufferSize");
buildAttribute(appenderName, appenderBuilder, "ImmediateFlush", "immediateFlush");
buildAppenderLayout(appenderName, appenderBuilder);
}
private void buildDailyRollingFileAppender(final String appenderName) {
final AppenderComponentBuilder appenderBuilder = builder.newAppender(appenderName,
RollingFileAppender.PLUGIN_NAME);
buildFileAppender(appenderName, appenderBuilder);
final String fileName = getLog4jAppenderValue(appenderName, "File");
final String datePattern = getLog4jAppenderValue(appenderName, "DatePattern", fileName + "'.'yyyy-MM-dd");
appenderBuilder.addAttribute("filePattern", fileName + "%d{" + datePattern + "}");
final ComponentBuilder<?> triggeringPolicy = builder.newComponent("Policies")
.addComponent(builder.newComponent("TimeBasedTriggeringPolicy").addAttribute("modulate", true));
appenderBuilder.addComponent(triggeringPolicy);
appenderBuilder
.addComponent(builder.newComponent("DefaultRolloverStrategy").addAttribute("max", Integer.MAX_VALUE));
builder.add(appenderBuilder);
}
private void buildRollingFileAppender(final String appenderName) {
final AppenderComponentBuilder appenderBuilder = builder.newAppender(appenderName,
RollingFileAppender.PLUGIN_NAME);
buildFileAppender(appenderName, appenderBuilder);
final String fileName = getLog4jAppenderValue(appenderName, "File");
appenderBuilder.addAttribute("filePattern", fileName + ".%i");
final String maxFileSizeString = getLog4jAppenderValue(appenderName, "MaxFileSize", "10485760");
final String maxBackupIndexString = getLog4jAppenderValue(appenderName, "MaxBackupIndex", "1");
final ComponentBuilder<?> triggeringPolicy = builder.newComponent("Policies").addComponent(
builder.newComponent("SizeBasedTriggeringPolicy").addAttribute("size", maxFileSizeString));
appenderBuilder.addComponent(triggeringPolicy);
appenderBuilder.addComponent(
builder.newComponent("DefaultRolloverStrategy").addAttribute("max", maxBackupIndexString));
builder.add(appenderBuilder);
}
private void buildAttribute(final String componentName, final ComponentBuilder componentBuilder,
final String sourceAttributeName, final String targetAttributeName) {
final String attributeValue = getLog4jAppenderValue(componentName, sourceAttributeName);
if (attributeValue != null) {
componentBuilder.addAttribute(targetAttributeName, attributeValue);
}
}
private void buildAttributeWithDefault(final String componentName, final ComponentBuilder componentBuilder,
final String sourceAttributeName, final String targetAttributeName, final String defaultValue) {
final String attributeValue = getLog4jAppenderValue(componentName, sourceAttributeName, defaultValue);
componentBuilder.addAttribute(targetAttributeName, attributeValue);
}
private void buildMandatoryAttribute(final String componentName, final ComponentBuilder componentBuilder,
final String sourceAttributeName, final String targetAttributeName) {
final String attributeValue = getLog4jAppenderValue(componentName, sourceAttributeName);
if (attributeValue != null) {
componentBuilder.addAttribute(targetAttributeName, attributeValue);
} else {
reportWarning("Missing " + sourceAttributeName + " for " + componentName);
}
}
private void buildNullAppender(final String appenderName) {
final AppenderComponentBuilder appenderBuilder = builder.newAppender(appenderName, NullAppender.PLUGIN_NAME);
builder.add(appenderBuilder);
}
private void buildAppenderLayout(final String name, final AppenderComponentBuilder appenderBuilder) {
final String layoutClass = getLog4jAppenderValue(name, "layout", null);
if (layoutClass != null) {
switch (layoutClass) {
case "org.apache.log4j.PatternLayout":
case "org.apache.log4j.EnhancedPatternLayout": {
final String pattern = getLog4jAppenderValue(name, "layout.ConversionPattern", null)
// Log4j 2's %x (NDC) is not compatible with Log4j 1's
// %x
// Log4j 1: "foo bar baz"
// Log4j 2: "[foo, bar, baz]"
// Use %ndc to get the Log4j 1 format
.replace("%x", "%ndc")
// Log4j 2's %X (MDC) is not compatible with Log4j 1's
// %X
// Log4j 1: "{{foo,bar}{hoo,boo}}"
// Log4j 2: "{foo=bar,hoo=boo}"
// Use %properties to get the Log4j 1 format
.replace("%X", "%properties");
appenderBuilder.add(newPatternLayout(pattern));
break;
}
case "org.apache.log4j.SimpleLayout": {
appenderBuilder.add(newPatternLayout("%level - %m%n"));
break;
}
case "org.apache.log4j.TTCCLayout": {
String pattern = "%r ";
if (Boolean.parseBoolean(getLog4jAppenderValue(name, "layout.ThreadPrinting", TRUE))) {
pattern += "[%t] ";
}
pattern += "%p ";
if (Boolean.parseBoolean(getLog4jAppenderValue(name, "layout.CategoryPrefixing", TRUE))) {
pattern += "%c ";
}
if (Boolean.parseBoolean(getLog4jAppenderValue(name, "layout.ContextPrinting", TRUE))) {
pattern += "%notEmpty{%ndc }";
}
pattern += "- %m%n";
appenderBuilder.add(newPatternLayout(pattern));
break;
}
case "org.apache.log4j.HTMLLayout": {
final LayoutComponentBuilder htmlLayout = builder.newLayout("HtmlLayout");
htmlLayout.addAttribute("title", getLog4jAppenderValue(name, "layout.Title", "Log4J Log Messages"));
htmlLayout.addAttribute("locationInfo",
Boolean.parseBoolean(getLog4jAppenderValue(name, "layout.LocationInfo", FALSE)));
appenderBuilder.add(htmlLayout);
break;
}
case "org.apache.log4j.xml.XMLLayout": {
final LayoutComponentBuilder xmlLayout = builder.newLayout("Log4j1XmlLayout");
xmlLayout.addAttribute("locationInfo",
Boolean.parseBoolean(getLog4jAppenderValue(name, "layout.LocationInfo", FALSE)));
xmlLayout.addAttribute("properties",
Boolean.parseBoolean(getLog4jAppenderValue(name, "layout.Properties", FALSE)));
appenderBuilder.add(xmlLayout);
break;
}
default:
reportWarning("Unknown layout class: " + layoutClass);
}
}
}
private LayoutComponentBuilder newPatternLayout(final String pattern) {
final LayoutComponentBuilder layoutBuilder = builder.newLayout("PatternLayout");
if (pattern != null) {
layoutBuilder.addAttribute("pattern", pattern);
}
return layoutBuilder;
}
private void buildRootLogger(final String rootLoggerValue) {
if (rootLoggerValue == null) {
return;
}
final String[] rootLoggerParts = rootLoggerValue.split(COMMA_DELIMITED_RE);
final String rootLoggerLevel = getLevelString(rootLoggerParts, Level.ERROR.name());
final RootLoggerComponentBuilder loggerBuilder = builder.newRootLogger(rootLoggerLevel);
//
final String[] sortedAppenderNames = Arrays.copyOfRange(rootLoggerParts, 1, rootLoggerParts.length);
Arrays.sort(sortedAppenderNames);
for (final String appender : sortedAppenderNames) {
loggerBuilder.add(builder.newAppenderRef(appender));
}
builder.add(loggerBuilder);
}
private String getLevelString(final String[] loggerParts, final String defaultLevel) {
return loggerParts.length > 0 ? loggerParts[0] : defaultLevel;
}
private void buildLoggers(final String prefix) {
final int preLength = prefix.length();
for (final Map.Entry<Object, Object> entry : properties.entrySet()) {
final Object keyObj = entry.getKey();
if (keyObj != null) {
final String key = keyObj.toString();
if (key.startsWith(prefix)) {
final String name = key.substring(preLength);
final Object value = entry.getValue();
if (value != null) {
// a Level may be followed by a list of Appender refs.
final String valueStr = value.toString();
final String[] split = valueStr.split(COMMA_DELIMITED_RE);
final String level = getLevelString(split, null);
if (level == null) {
warn("Level is missing for entry " + entry);
} else {
final LoggerComponentBuilder newLogger = builder.newLogger(name, level);
if (split.length > 1) {
// Add Appenders to this logger
final String[] sortedAppenderNames = Arrays.copyOfRange(split, 1, split.length);
Arrays.sort(sortedAppenderNames);
for (final String appenderName : sortedAppenderNames) {
newLogger.add(builder.newAppenderRef(appenderName));
}
}
builder.add(newLogger);
}
}
}
}
}
}
private String getLog4jAppenderValue(final String appenderName, final String attributeName) {
return getProperty("log4j.appender." + appenderName + "." + attributeName);
}
private String getProperty(final String key) {
final String value = properties.getProperty(key);
final String sysValue = strSubstitutorSystem.replace(value);
return strSubstitutorProperties.replace(sysValue);
}
private String getProperty(final String key, final String defaultValue) {
final String value = getProperty(key);
return value == null ? defaultValue : value;
}
private String getLog4jAppenderValue(final String appenderName, final String attributeName,
final String defaultValue) {
return getProperty("log4j.appender." + appenderName + "." + attributeName, defaultValue);
}
private String getLog4jValue(final String key) {
return getProperty("log4j." + key);
}
private void reportWarning(final String msg) {
StatusLogger.getLogger().warn("Log4j 1 configuration parser: " + msg);
}
}