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