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}