View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache license, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the license for the specific language governing permissions and
15   * limitations under the license.
16   */
17  package org.apache.log4j.config;
18  
19  import java.io.IOException;
20  import java.io.InputStream;
21  import java.util.Arrays;
22  import java.util.HashMap;
23  import java.util.Map;
24  import java.util.Objects;
25  import java.util.Properties;
26  import java.util.TreeMap;
27  
28  import org.apache.logging.log4j.Level;
29  import org.apache.logging.log4j.core.appender.ConsoleAppender;
30  import org.apache.logging.log4j.core.appender.FileAppender;
31  import org.apache.logging.log4j.core.appender.NullAppender;
32  import org.apache.logging.log4j.core.appender.RollingFileAppender;
33  import org.apache.logging.log4j.core.config.ConfigurationException;
34  import org.apache.logging.log4j.core.config.builder.api.AppenderComponentBuilder;
35  import org.apache.logging.log4j.core.config.builder.api.ComponentBuilder;
36  import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilder;
37  import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory;
38  import org.apache.logging.log4j.core.config.builder.api.LayoutComponentBuilder;
39  import org.apache.logging.log4j.core.config.builder.api.LoggerComponentBuilder;
40  import org.apache.logging.log4j.core.config.builder.api.RootLoggerComponentBuilder;
41  import org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration;
42  import org.apache.logging.log4j.core.lookup.ConfigurationStrSubstitutor;
43  import org.apache.logging.log4j.core.lookup.StrSubstitutor;
44  import org.apache.logging.log4j.status.StatusLogger;
45  import org.apache.logging.log4j.util.Strings;
46  
47  /**
48   * Experimental parser for Log4j 1.2 properties configuration files.
49   *
50   * This class is not thread-safe.
51   * 
52   * <p>
53   * From the Log4j 1.2 Javadocs:
54   * </p>
55   * <p>
56   * All option values admit variable substitution. The syntax of variable substitution is similar to that of Unix shells. The string between
57   * an opening "${" and closing "}" is interpreted as a key. The value of the substituted variable can be defined as a system property or in
58   * the configuration file itself. The value of the key is first searched in the system properties, and if not found there, it is then
59   * searched in the configuration file being parsed. The corresponding value replaces the ${variableName} sequence. For example, if java.home
60   * system property is set to /home/xyz, then every occurrence of the sequence ${java.home} will be interpreted as /home/xyz.
61   * </p>
62   */
63  public class Log4j1ConfigurationParser {
64  
65      private static final String COMMA_DELIMITED_RE = "\\s*,\\s*";
66      private static final String ROOTLOGGER = "rootLogger";
67      private static final String ROOTCATEGORY = "rootCategory";
68      private static final String TRUE = "true";
69      private static final String FALSE = "false";
70  
71      private final Properties properties = new Properties();
72      private StrSubstitutor strSubstitutorProperties;
73      private StrSubstitutor strSubstitutorSystem;
74  
75      private final ConfigurationBuilder<BuiltConfiguration> builder = ConfigurationBuilderFactory
76              .newConfigurationBuilder();
77  
78      /**
79       * Parses a Log4j 1.2 properties configuration file in ISO 8859-1 encoding into a ConfigurationBuilder.
80       *
81       * @param input
82       *            InputStream to read from is assumed to be ISO 8859-1, and will not be closed.
83       * @return the populated ConfigurationBuilder, never {@literal null}
84       * @throws IOException
85       *             if unable to read the input
86       * @throws ConfigurationException
87       *             if the input does not contain a valid configuration
88       */
89      public ConfigurationBuilder<BuiltConfiguration> buildConfigurationBuilder(final InputStream input)
90              throws IOException {
91          try {
92              properties.load(input);
93              strSubstitutorProperties = new ConfigurationStrSubstitutor(properties);
94              strSubstitutorSystem = new ConfigurationStrSubstitutor(System.getProperties());
95              final String rootCategoryValue = getLog4jValue(ROOTCATEGORY);
96              final String rootLoggerValue = getLog4jValue(ROOTLOGGER);
97              if (rootCategoryValue == null && rootLoggerValue == null) {
98                  // This is not a Log4j 1 properties configuration file.
99                  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.valueOf(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 }