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.StrSubstitutor;
43  import org.apache.logging.log4j.status.StatusLogger;
44  import org.apache.logging.log4j.util.Strings;
45  
46  /**
47   * Experimental parser for Log4j 1.2 properties configuration files.
48   *
49   * This class is not thread-safe.
50   * 
51   * <p>
52   * From the Log4j 1.2 Javadocs:
53   * </p>
54   * <p>
55   * All option values admit variable substitution. The syntax of variable substitution is similar to that of Unix shells. The string between
56   * an opening "${" and closing "}" is interpreted as a key. The value of the substituted variable can be defined as a system property or in
57   * the configuration file itself. The value of the key is first searched in the system properties, and if not found there, it is then
58   * searched in the configuration file being parsed. The corresponding value replaces the ${variableName} sequence. For example, if java.home
59   * system property is set to /home/xyz, then every occurrence of the sequence ${java.home} will be interpreted as /home/xyz.
60   * </p>
61   */
62  public class Log4j1ConfigurationParser {
63  
64      private static final String COMMA_DELIMITED_RE = "\\s*,\\s*";
65      private static final String ROOTLOGGER = "rootLogger";
66      private static final String ROOTCATEGORY = "rootCategory";
67      private static final String TRUE = "true";
68      private static final String FALSE = "false";
69  
70      private final Properties properties = new Properties();
71      private StrSubstitutor strSubstitutorProperties;
72      private StrSubstitutor strSubstitutorSystem;
73  
74      private final ConfigurationBuilder<BuiltConfiguration> builder = ConfigurationBuilderFactory
75              .newConfigurationBuilder();
76  
77      /**
78       * Parses a Log4j 1.2 properties configuration file in ISO 8859-1 encoding into a ConfigurationBuilder.
79       *
80       * @param input
81       *            InputStream to read from is assumed to be ISO 8859-1, and will not be closed.
82       * @return the populated ConfigurationBuilder, never {@literal null}
83       * @throws IOException
84       *             if unable to read the input
85       * @throws ConfigurationException
86       *             if the input does not contain a valid configuration
87       */
88      public ConfigurationBuilder<BuiltConfiguration> buildConfigurationBuilder(final InputStream input)
89              throws IOException {
90          try {
91              properties.load(input);
92              strSubstitutorProperties = new StrSubstitutor(properties);
93              strSubstitutorSystem = new StrSubstitutor(System.getProperties());
94              final String rootCategoryValue = getLog4jValue(ROOTCATEGORY);
95              final String rootLoggerValue = getLog4jValue(ROOTLOGGER);
96              if (rootCategoryValue == null && rootLoggerValue == null) {
97                  // This is not a Log4j 1 properties configuration file.
98                  warn("Missing " + ROOTCATEGORY + " or " + ROOTLOGGER + " in " + input);
99                  // 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 }