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}