001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache license, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the license for the specific language governing permissions and
015 * limitations under the license.
016 */
017package org.apache.log4j.config;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.util.Arrays;
022import java.util.HashMap;
023import java.util.Map;
024import java.util.Objects;
025import java.util.Properties;
026import java.util.TreeMap;
027
028import org.apache.logging.log4j.Level;
029import org.apache.logging.log4j.core.appender.ConsoleAppender;
030import org.apache.logging.log4j.core.appender.FileAppender;
031import org.apache.logging.log4j.core.appender.NullAppender;
032import org.apache.logging.log4j.core.appender.RollingFileAppender;
033import org.apache.logging.log4j.core.config.ConfigurationException;
034import org.apache.logging.log4j.core.config.builder.api.AppenderComponentBuilder;
035import org.apache.logging.log4j.core.config.builder.api.ComponentBuilder;
036import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilder;
037import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory;
038import org.apache.logging.log4j.core.config.builder.api.LayoutComponentBuilder;
039import org.apache.logging.log4j.core.config.builder.api.LoggerComponentBuilder;
040import org.apache.logging.log4j.core.config.builder.api.RootLoggerComponentBuilder;
041import org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration;
042import org.apache.logging.log4j.core.lookup.ConfigurationStrSubstitutor;
043import org.apache.logging.log4j.core.lookup.StrSubstitutor;
044import org.apache.logging.log4j.status.StatusLogger;
045import org.apache.logging.log4j.util.Strings;
046
047/**
048 * Experimental parser for Log4j 1.2 properties configuration files.
049 *
050 * This class is not thread-safe.
051 *
052 * <p>
053 * From the Log4j 1.2 Javadocs:
054 * </p>
055 * <p>
056 * All option values admit variable substitution. The syntax of variable substitution is similar to that of Unix shells. The string between
057 * an opening "${" and closing "}" is interpreted as a key. The value of the substituted variable can be defined as a system property or in
058 * the configuration file itself. The value of the key is first searched in the system properties, and if not found there, it is then
059 * searched in the configuration file being parsed. The corresponding value replaces the ${variableName} sequence. For example, if java.home
060 * system property is set to /home/xyz, then every occurrence of the sequence ${java.home} will be interpreted as /home/xyz.
061 * </p>
062 */
063public class Log4j1ConfigurationParser {
064
065    private static final String COMMA_DELIMITED_RE = "\\s*,\\s*";
066    private static final String ROOTLOGGER = "rootLogger";
067    private static final String ROOTCATEGORY = "rootCategory";
068    private static final String TRUE = "true";
069    private static final String FALSE = "false";
070
071    private final Properties properties = new Properties();
072    private StrSubstitutor strSubstitutorProperties;
073    private StrSubstitutor strSubstitutorSystem;
074
075    private final ConfigurationBuilder<BuiltConfiguration> builder = ConfigurationBuilderFactory
076            .newConfigurationBuilder();
077
078    /**
079     * Parses a Log4j 1.2 properties configuration file in ISO 8859-1 encoding into a ConfigurationBuilder.
080     *
081     * @param input
082     *            InputStream to read from is assumed to be ISO 8859-1, and will not be closed.
083     * @return the populated ConfigurationBuilder, never {@literal null}
084     * @throws IOException
085     *             if unable to read the input
086     * @throws ConfigurationException
087     *             if the input does not contain a valid configuration
088     */
089    public ConfigurationBuilder<BuiltConfiguration> buildConfigurationBuilder(final InputStream input)
090            throws IOException {
091        try {
092            properties.load(input);
093            strSubstitutorProperties = new ConfigurationStrSubstitutor(properties);
094            strSubstitutorSystem = new ConfigurationStrSubstitutor(System.getProperties());
095            final String rootCategoryValue = getLog4jValue(ROOTCATEGORY);
096            final String rootLoggerValue = getLog4jValue(ROOTLOGGER);
097            if (rootCategoryValue == null && rootLoggerValue == null) {
098                // This is not a Log4j 1 properties configuration file.
099                warn("Missing " + ROOTCATEGORY + " or " + ROOTLOGGER + " in " + input);
100                // throw new ConfigurationException(
101                // "Missing " + ROOTCATEGORY + " or " + ROOTLOGGER + " in " + input);
102            }
103            builder.setConfigurationName("Log4j1");
104            // DEBUG
105            final String debugValue = getLog4jValue("debug");
106            if (Boolean.parseBoolean(debugValue)) {
107                builder.setStatusLevel(Level.DEBUG);
108            }
109            // Root
110            buildRootLogger(getLog4jValue(ROOTCATEGORY));
111            buildRootLogger(getLog4jValue(ROOTLOGGER));
112            // Appenders
113            final Map<String, String> appenderNameToClassName = buildClassToPropertyPrefixMap();
114            for (final Map.Entry<String, String> entry : appenderNameToClassName.entrySet()) {
115                final String appenderName = entry.getKey();
116                final String appenderClass = entry.getValue();
117                buildAppender(appenderName, appenderClass);
118            }
119            // Loggers
120            buildLoggers("log4j.category.");
121            buildLoggers("log4j.logger.");
122            buildProperties();
123            return builder;
124        } catch (final IllegalArgumentException e) {
125            throw new ConfigurationException(e);
126        }
127    }
128
129    private void buildProperties() {
130        for (final Map.Entry<Object, Object> entry : new TreeMap<>(properties).entrySet()) {
131            final String key = entry.getKey().toString();
132            if (!key.startsWith("log4j.") && !key.equals(ROOTCATEGORY) && !key.equals(ROOTLOGGER)) {
133                builder.addProperty(key, Objects.toString(entry.getValue(), Strings.EMPTY));
134            }
135        }
136    }
137
138    private void warn(final String string) {
139        System.err.println(string);
140    }
141
142    private Map<String, String> buildClassToPropertyPrefixMap() {
143        final String prefix = "log4j.appender.";
144        final int preLength = prefix.length();
145        final Map<String, String> map = new HashMap<>();
146        for (final Map.Entry<Object, Object> entry : properties.entrySet()) {
147            final Object keyObj = entry.getKey();
148            if (keyObj != null) {
149                final String key = keyObj.toString();
150                if (key.startsWith(prefix)) {
151                    if (key.indexOf('.', preLength) < 0) {
152                        final String name = key.substring(preLength);
153                        final Object value = entry.getValue();
154                        if (value != null) {
155                            map.put(name, value.toString());
156                        }
157                    }
158                }
159            }
160        }
161        return map;
162    }
163
164    private void buildAppender(final String appenderName, final String appenderClass) {
165        switch (appenderClass) {
166        case "org.apache.log4j.ConsoleAppender":
167            buildConsoleAppender(appenderName);
168            break;
169        case "org.apache.log4j.FileAppender":
170            buildFileAppender(appenderName);
171            break;
172        case "org.apache.log4j.DailyRollingFileAppender":
173            buildDailyRollingFileAppender(appenderName);
174            break;
175        case "org.apache.log4j.RollingFileAppender":
176            buildRollingFileAppender(appenderName);
177            break;
178        case "org.apache.log4j.varia.NullAppender":
179            buildNullAppender(appenderName);
180            break;
181        default:
182            reportWarning("Unknown appender class: " + appenderClass + "; ignoring appender: " + appenderName);
183        }
184    }
185
186    private void buildConsoleAppender(final String appenderName) {
187        final AppenderComponentBuilder appenderBuilder = builder.newAppender(appenderName, ConsoleAppender.PLUGIN_NAME);
188        final String targetValue = getLog4jAppenderValue(appenderName, "Target", "System.out");
189        if (targetValue != null) {
190            final ConsoleAppender.Target target;
191            switch (targetValue) {
192            case "System.out":
193                target = ConsoleAppender.Target.SYSTEM_OUT;
194                break;
195            case "System.err":
196                target = ConsoleAppender.Target.SYSTEM_ERR;
197                break;
198            default:
199                reportWarning("Unknown value for console Target: " + targetValue);
200                target = null;
201            }
202            if (target != null) {
203                appenderBuilder.addAttribute("target", target);
204            }
205        }
206        buildAttribute(appenderName, appenderBuilder, "Follow", "follow");
207        if (FALSE.equalsIgnoreCase(getLog4jAppenderValue(appenderName, "ImmediateFlush"))) {
208            reportWarning("ImmediateFlush=false is not supported on Console appender");
209        }
210        buildAppenderLayout(appenderName, appenderBuilder);
211        builder.add(appenderBuilder);
212    }
213
214    private void buildFileAppender(final String appenderName) {
215        final AppenderComponentBuilder appenderBuilder = builder.newAppender(appenderName, FileAppender.PLUGIN_NAME);
216        buildFileAppender(appenderName, appenderBuilder);
217        builder.add(appenderBuilder);
218    }
219
220    private void buildFileAppender(final String appenderName, final AppenderComponentBuilder appenderBuilder) {
221        buildMandatoryAttribute(appenderName, appenderBuilder, "File", "fileName");
222        buildAttribute(appenderName, appenderBuilder, "Append", "append");
223        buildAttribute(appenderName, appenderBuilder, "BufferedIO", "bufferedIo");
224        buildAttribute(appenderName, appenderBuilder, "BufferSize", "bufferSize");
225        buildAttribute(appenderName, appenderBuilder, "ImmediateFlush", "immediateFlush");
226        buildAppenderLayout(appenderName, appenderBuilder);
227    }
228
229    private void buildDailyRollingFileAppender(final String appenderName) {
230        final AppenderComponentBuilder appenderBuilder = builder.newAppender(appenderName,
231                RollingFileAppender.PLUGIN_NAME);
232        buildFileAppender(appenderName, appenderBuilder);
233        final String fileName = getLog4jAppenderValue(appenderName, "File");
234        final String datePattern = getLog4jAppenderValue(appenderName, "DatePattern", fileName + "'.'yyyy-MM-dd");
235        appenderBuilder.addAttribute("filePattern", fileName + "%d{" + datePattern + "}");
236        final ComponentBuilder<?> triggeringPolicy = builder.newComponent("Policies")
237                .addComponent(builder.newComponent("TimeBasedTriggeringPolicy").addAttribute("modulate", true));
238        appenderBuilder.addComponent(triggeringPolicy);
239        appenderBuilder
240                .addComponent(builder.newComponent("DefaultRolloverStrategy").addAttribute("max", Integer.MAX_VALUE));
241        builder.add(appenderBuilder);
242    }
243
244    private void buildRollingFileAppender(final String appenderName) {
245        final AppenderComponentBuilder appenderBuilder = builder.newAppender(appenderName,
246                RollingFileAppender.PLUGIN_NAME);
247        buildFileAppender(appenderName, appenderBuilder);
248        final String fileName = getLog4jAppenderValue(appenderName, "File");
249        appenderBuilder.addAttribute("filePattern", fileName + ".%i");
250        final String maxFileSizeString = getLog4jAppenderValue(appenderName, "MaxFileSize", "10485760");
251        final String maxBackupIndexString = getLog4jAppenderValue(appenderName, "MaxBackupIndex", "1");
252        final ComponentBuilder<?> triggeringPolicy = builder.newComponent("Policies").addComponent(
253                builder.newComponent("SizeBasedTriggeringPolicy").addAttribute("size", maxFileSizeString));
254        appenderBuilder.addComponent(triggeringPolicy);
255        appenderBuilder.addComponent(
256                builder.newComponent("DefaultRolloverStrategy").addAttribute("max", maxBackupIndexString));
257        builder.add(appenderBuilder);
258    }
259
260    private void buildAttribute(final String componentName, final ComponentBuilder componentBuilder,
261            final String sourceAttributeName, final String targetAttributeName) {
262        final String attributeValue = getLog4jAppenderValue(componentName, sourceAttributeName);
263        if (attributeValue != null) {
264            componentBuilder.addAttribute(targetAttributeName, attributeValue);
265        }
266    }
267
268    private void buildAttributeWithDefault(final String componentName, final ComponentBuilder componentBuilder,
269            final String sourceAttributeName, final String targetAttributeName, final String defaultValue) {
270        final String attributeValue = getLog4jAppenderValue(componentName, sourceAttributeName, defaultValue);
271        componentBuilder.addAttribute(targetAttributeName, attributeValue);
272    }
273
274    private void buildMandatoryAttribute(final String componentName, final ComponentBuilder componentBuilder,
275            final String sourceAttributeName, final String targetAttributeName) {
276        final String attributeValue = getLog4jAppenderValue(componentName, sourceAttributeName);
277        if (attributeValue != null) {
278            componentBuilder.addAttribute(targetAttributeName, attributeValue);
279        } else {
280            reportWarning("Missing " + sourceAttributeName + " for " + componentName);
281        }
282    }
283
284    private void buildNullAppender(final String appenderName) {
285        final AppenderComponentBuilder appenderBuilder = builder.newAppender(appenderName, NullAppender.PLUGIN_NAME);
286        builder.add(appenderBuilder);
287    }
288
289    private void buildAppenderLayout(final String name, final AppenderComponentBuilder appenderBuilder) {
290        final String layoutClass = getLog4jAppenderValue(name, "layout", null);
291        if (layoutClass != null) {
292            switch (layoutClass) {
293            case "org.apache.log4j.PatternLayout":
294            case "org.apache.log4j.EnhancedPatternLayout": {
295                final String pattern = getLog4jAppenderValue(name, "layout.ConversionPattern", null)
296
297                        // Log4j 2's %x (NDC) is not compatible with Log4j 1's
298                        // %x
299                        // Log4j 1: "foo bar baz"
300                        // Log4j 2: "[foo, bar, baz]"
301                        // Use %ndc to get the Log4j 1 format
302                        .replace("%x", "%ndc")
303
304                        // Log4j 2's %X (MDC) is not compatible with Log4j 1's
305                        // %X
306                        // Log4j 1: "{{foo,bar}{hoo,boo}}"
307                        // Log4j 2: "{foo=bar,hoo=boo}"
308                        // Use %properties to get the Log4j 1 format
309                        .replace("%X", "%properties");
310
311                appenderBuilder.add(newPatternLayout(pattern));
312                break;
313            }
314            case "org.apache.log4j.SimpleLayout": {
315                appenderBuilder.add(newPatternLayout("%level - %m%n"));
316                break;
317            }
318            case "org.apache.log4j.TTCCLayout": {
319                String pattern = "%r ";
320                if (Boolean.parseBoolean(getLog4jAppenderValue(name, "layout.ThreadPrinting", TRUE))) {
321                    pattern += "[%t] ";
322                }
323                pattern += "%p ";
324                if (Boolean.parseBoolean(getLog4jAppenderValue(name, "layout.CategoryPrefixing", TRUE))) {
325                    pattern += "%c ";
326                }
327                if (Boolean.parseBoolean(getLog4jAppenderValue(name, "layout.ContextPrinting", TRUE))) {
328                    pattern += "%notEmpty{%ndc }";
329                }
330                pattern += "- %m%n";
331                appenderBuilder.add(newPatternLayout(pattern));
332                break;
333            }
334            case "org.apache.log4j.HTMLLayout": {
335                final LayoutComponentBuilder htmlLayout = builder.newLayout("HtmlLayout");
336                htmlLayout.addAttribute("title", getLog4jAppenderValue(name, "layout.Title", "Log4J Log Messages"));
337                htmlLayout.addAttribute("locationInfo",
338                        Boolean.parseBoolean(getLog4jAppenderValue(name, "layout.LocationInfo", FALSE)));
339                appenderBuilder.add(htmlLayout);
340                break;
341            }
342            case "org.apache.log4j.xml.XMLLayout": {
343                final LayoutComponentBuilder xmlLayout = builder.newLayout("Log4j1XmlLayout");
344                xmlLayout.addAttribute("locationInfo",
345                        Boolean.parseBoolean(getLog4jAppenderValue(name, "layout.LocationInfo", FALSE)));
346                xmlLayout.addAttribute("properties",
347                        Boolean.parseBoolean(getLog4jAppenderValue(name, "layout.Properties", FALSE)));
348                appenderBuilder.add(xmlLayout);
349                break;
350            }
351            default:
352                reportWarning("Unknown layout class: " + layoutClass);
353            }
354        }
355    }
356
357    private LayoutComponentBuilder newPatternLayout(final String pattern) {
358        final LayoutComponentBuilder layoutBuilder = builder.newLayout("PatternLayout");
359        if (pattern != null) {
360            layoutBuilder.addAttribute("pattern", pattern);
361        }
362        return layoutBuilder;
363    }
364
365    private void buildRootLogger(final String rootLoggerValue) {
366        if (rootLoggerValue == null) {
367            return;
368        }
369        final String[] rootLoggerParts = rootLoggerValue.split(COMMA_DELIMITED_RE);
370        final String rootLoggerLevel = getLevelString(rootLoggerParts, Level.ERROR.name());
371        final RootLoggerComponentBuilder loggerBuilder = builder.newRootLogger(rootLoggerLevel);
372        //
373        final String[] sortedAppenderNames = Arrays.copyOfRange(rootLoggerParts, 1, rootLoggerParts.length);
374        Arrays.sort(sortedAppenderNames);
375        for (final String appender : sortedAppenderNames) {
376            loggerBuilder.add(builder.newAppenderRef(appender));
377        }
378        builder.add(loggerBuilder);
379    }
380
381    private String getLevelString(final String[] loggerParts, final String defaultLevel) {
382        return loggerParts.length > 0 ? loggerParts[0] : defaultLevel;
383    }
384
385    private void buildLoggers(final String prefix) {
386        final int preLength = prefix.length();
387        for (final Map.Entry<Object, Object> entry : properties.entrySet()) {
388            final Object keyObj = entry.getKey();
389            if (keyObj != null) {
390                final String key = keyObj.toString();
391                if (key.startsWith(prefix)) {
392                    final String name = key.substring(preLength);
393                    final Object value = entry.getValue();
394                    if (value != null) {
395                        // a Level may be followed by a list of Appender refs.
396                        final String valueStr = value.toString();
397                        final String[] split = valueStr.split(COMMA_DELIMITED_RE);
398                        final String level = getLevelString(split, null);
399                        if (level == null) {
400                            warn("Level is missing for entry " + entry);
401                        } else {
402                            final LoggerComponentBuilder newLogger = builder.newLogger(name, level);
403                            if (split.length > 1) {
404                                // Add Appenders to this logger
405                                final String[] sortedAppenderNames = Arrays.copyOfRange(split, 1, split.length);
406                                Arrays.sort(sortedAppenderNames);
407                                for (final String appenderName : sortedAppenderNames) {
408                                    newLogger.add(builder.newAppenderRef(appenderName));
409                                }
410                            }
411                            builder.add(newLogger);
412                        }
413                    }
414                }
415            }
416        }
417    }
418
419    private String getLog4jAppenderValue(final String appenderName, final String attributeName) {
420        return getProperty("log4j.appender." + appenderName + "." + attributeName);
421    }
422
423    private String getProperty(final String key) {
424        final String value = properties.getProperty(key);
425        final String sysValue = strSubstitutorSystem.replace(value);
426        return strSubstitutorProperties.replace(sysValue);
427    }
428
429    private String getProperty(final String key, final String defaultValue) {
430        final String value = getProperty(key);
431        return value == null ? defaultValue : value;
432    }
433
434    private String getLog4jAppenderValue(final String appenderName, final String attributeName,
435            final String defaultValue) {
436        return getProperty("log4j.appender." + appenderName + "." + attributeName, defaultValue);
437    }
438
439    private String getLog4jValue(final String key) {
440        return getProperty("log4j." + key);
441    }
442
443    private void reportWarning(final String msg) {
444        StatusLogger.getLogger().warn("Log4j 1 configuration parser: " + msg);
445    }
446
447}