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.logging.log4j.core.pattern; 018 019import java.lang.reflect.Method; 020import java.lang.reflect.Modifier; 021import java.util.ArrayList; 022import java.util.Iterator; 023import java.util.LinkedHashMap; 024import java.util.List; 025import java.util.Map; 026import java.util.Objects; 027 028import org.apache.logging.log4j.Logger; 029import org.apache.logging.log4j.core.config.Configuration; 030import org.apache.logging.log4j.core.config.plugins.util.PluginManager; 031import org.apache.logging.log4j.core.config.plugins.util.PluginType; 032import org.apache.logging.log4j.core.util.SystemNanoClock; 033import org.apache.logging.log4j.status.StatusLogger; 034import org.apache.logging.log4j.util.Strings; 035 036/** 037 * Most of the work of the {@link org.apache.logging.log4j.core.layout.PatternLayout} class is delegated to the 038 * PatternParser class. 039 * <p> 040 * It is this class that parses conversion patterns and creates a chained list of {@link PatternConverter 041 * PatternConverters}. 042 */ 043public final class PatternParser { 044 static final String DISABLE_ANSI = "disableAnsi"; 045 static final String NO_CONSOLE_NO_ANSI = "noConsoleNoAnsi"; 046 047 /** 048 * Escape character for format specifier. 049 */ 050 private static final char ESCAPE_CHAR = '%'; 051 052 /** 053 * The states the parser can be in while parsing the pattern. 054 */ 055 private enum ParserState { 056 /** 057 * Literal state. 058 */ 059 LITERAL_STATE, 060 061 /** 062 * In converter name state. 063 */ 064 CONVERTER_STATE, 065 066 /** 067 * Dot state. 068 */ 069 DOT_STATE, 070 071 /** 072 * Min state. 073 */ 074 MIN_STATE, 075 076 /** 077 * Max state. 078 */ 079 MAX_STATE; 080 } 081 082 private static final Logger LOGGER = StatusLogger.getLogger(); 083 084 private static final int BUF_SIZE = 32; 085 086 private static final int DECIMAL = 10; 087 088 private final Configuration config; 089 090 private final Map<String, Class<PatternConverter>> converterRules; 091 092 /** 093 * Constructor. 094 * 095 * @param converterKey 096 * The type of converters that will be used. 097 */ 098 public PatternParser(final String converterKey) { 099 this(null, converterKey, null, null); 100 } 101 102 /** 103 * Constructor. 104 * 105 * @param config 106 * The current Configuration. 107 * @param converterKey 108 * The key to lookup the converters. 109 * @param expected 110 * The expected base Class of each Converter. 111 */ 112 public PatternParser(final Configuration config, final String converterKey, final Class<?> expected) { 113 this(config, converterKey, expected, null); 114 } 115 116 /** 117 * Constructor. 118 * 119 * @param config 120 * The current Configuration. 121 * @param converterKey 122 * The key to lookup the converters. 123 * @param expectedClass 124 * The expected base Class of each Converter. 125 * @param filterClass 126 * Filter the returned plugins after calling the plugin manager. 127 */ 128 public PatternParser(final Configuration config, final String converterKey, final Class<?> expectedClass, 129 final Class<?> filterClass) { 130 this.config = config; 131 final PluginManager manager = new PluginManager(converterKey); 132 manager.collectPlugins(config == null ? null : config.getPluginPackages()); 133 final Map<String, PluginType<?>> plugins = manager.getPlugins(); 134 final Map<String, Class<PatternConverter>> converters = new LinkedHashMap<>(); 135 136 for (final PluginType<?> type : plugins.values()) { 137 try { 138 @SuppressWarnings("unchecked") 139 final Class<PatternConverter> clazz = (Class<PatternConverter>) type.getPluginClass(); 140 if (filterClass != null && !filterClass.isAssignableFrom(clazz)) { 141 continue; 142 } 143 final ConverterKeys keys = clazz.getAnnotation(ConverterKeys.class); 144 if (keys != null) { 145 for (final String key : keys.value()) { 146 if (converters.containsKey(key)) { 147 LOGGER.warn("Converter key '{}' is already mapped to '{}'. " + 148 "Sorry, Dave, I can't let you do that! Ignoring plugin [{}].", 149 key, converters.get(key), clazz); 150 } else { 151 converters.put(key, clazz); 152 } 153 } 154 } 155 } catch (final Exception ex) { 156 LOGGER.error("Error processing plugin " + type.getElementName(), ex); 157 } 158 } 159 converterRules = converters; 160 } 161 162 public List<PatternFormatter> parse(final String pattern) { 163 return parse(pattern, false, false, false); 164 } 165 166 public List<PatternFormatter> parse(final String pattern, final boolean alwaysWriteExceptions, 167 final boolean noConsoleNoAnsi) { 168 return parse(pattern, alwaysWriteExceptions, false, noConsoleNoAnsi); 169 } 170 171 public List<PatternFormatter> parse(final String pattern, final boolean alwaysWriteExceptions, 172 final boolean disableAnsi, final boolean noConsoleNoAnsi) { 173 final List<PatternFormatter> list = new ArrayList<>(); 174 final List<PatternConverter> converters = new ArrayList<>(); 175 final List<FormattingInfo> fields = new ArrayList<>(); 176 177 parse(pattern, converters, fields, disableAnsi, noConsoleNoAnsi, true); 178 179 final Iterator<FormattingInfo> fieldIter = fields.iterator(); 180 boolean handlesThrowable = false; 181 182 for (final PatternConverter converter : converters) { 183 if (converter instanceof NanoTimePatternConverter) { 184 // LOG4J2-1074 Switch to actual clock if nanosecond timestamps are required in config. 185 // LOG4J2-1248 set config nanoclock 186 if (config != null) { 187 config.setNanoClock(new SystemNanoClock()); 188 } 189 } 190 LogEventPatternConverter pc; 191 if (converter instanceof LogEventPatternConverter) { 192 pc = (LogEventPatternConverter) converter; 193 handlesThrowable |= pc.handlesThrowable(); 194 } else { 195 pc = new LiteralPatternConverter(config, Strings.EMPTY, true); 196 } 197 198 FormattingInfo field; 199 if (fieldIter.hasNext()) { 200 field = fieldIter.next(); 201 } else { 202 field = FormattingInfo.getDefault(); 203 } 204 list.add(new PatternFormatter(pc, field)); 205 } 206 if (alwaysWriteExceptions && !handlesThrowable) { 207 final LogEventPatternConverter pc = ExtendedThrowablePatternConverter.newInstance(config, null); 208 list.add(new PatternFormatter(pc, FormattingInfo.getDefault())); 209 } 210 return list; 211 } 212 213 /** 214 * Extracts the converter identifier found at the given start position. 215 * <p> 216 * After this function returns, the variable i will point to the first char after the end of the converter 217 * identifier. 218 * </p> 219 * <p> 220 * If i points to a char which is not a character acceptable at the start of a unicode identifier, the value null is 221 * returned. 222 * </p> 223 * 224 * @param lastChar 225 * last processed character. 226 * @param pattern 227 * format string. 228 * @param start 229 * current index into pattern format. 230 * @param convBuf 231 * buffer to receive conversion specifier. 232 * @param currentLiteral 233 * literal to be output in case format specifier in unrecognized. 234 * @return position in pattern after converter. 235 */ 236 private static int extractConverter(final char lastChar, final String pattern, final int start, 237 final StringBuilder convBuf, final StringBuilder currentLiteral) { 238 int i = start; 239 convBuf.setLength(0); 240 241 // When this method is called, lastChar points to the first character of the 242 // conversion word. For example: 243 // For "%hello" lastChar = 'h' 244 // For "%-5hello" lastChar = 'h' 245 // System.out.println("lastchar is "+lastChar); 246 if (!Character.isUnicodeIdentifierStart(lastChar)) { 247 return i; 248 } 249 250 convBuf.append(lastChar); 251 252 while (i < pattern.length() && Character.isUnicodeIdentifierPart(pattern.charAt(i))) { 253 convBuf.append(pattern.charAt(i)); 254 currentLiteral.append(pattern.charAt(i)); 255 i++; 256 } 257 258 return i; 259 } 260 261 /** 262 * Extract options. 263 * 264 * @param pattern 265 * conversion pattern. 266 * @param start 267 * start of options. 268 * @param options 269 * array to receive extracted options 270 * @return position in pattern after options. 271 */ 272 private static int extractOptions(final String pattern, final int start, final List<String> options) { 273 int i = start; 274 while (i < pattern.length() && pattern.charAt(i) == '{') { 275 i++; // skip opening "{" 276 final int begin = i; // position of first real char 277 int depth = 1; // already inside one level 278 while (depth > 0 && i < pattern.length()) { 279 final char c = pattern.charAt(i); 280 if (c == '{') { 281 depth++; 282 } else if (c == '}') { 283 depth--; 284 // TODO(?) maybe escaping of { and } with \ or % 285 } 286 i++; 287 } // while 288 289 if (depth > 0) { // option not closed, continue with pattern after closing bracket 290 i = pattern.lastIndexOf('}'); 291 if (i == -1 || i < start) { 292 // if no closing bracket could be found or there is no closing bracket behind the starting 293 // character of our parsing process continue parsing after the first opening bracket 294 return begin; 295 } 296 return i + 1; 297 } 298 299 options.add(pattern.substring(begin, i - 1)); 300 } // while 301 302 return i; 303 } 304 305 /** 306 * Parse a format specifier. 307 * 308 * @param pattern 309 * pattern to parse. 310 * @param patternConverters 311 * list to receive pattern converters. 312 * @param formattingInfos 313 * list to receive field specifiers corresponding to pattern converters. 314 * @param noConsoleNoAnsi 315 * do not do not output ANSI escape codes if {@link System#console()} 316 * @param convertBackslashes if {@code true}, backslash characters are treated as escape characters and character 317 * sequences like "\" followed by "t" (backslash+t) are converted to special characters like '\t' (tab). 318 */ 319 public void parse(final String pattern, final List<PatternConverter> patternConverters, 320 final List<FormattingInfo> formattingInfos, final boolean noConsoleNoAnsi, 321 final boolean convertBackslashes) { 322 parse(pattern, patternConverters, formattingInfos, false, noConsoleNoAnsi, convertBackslashes); 323 } 324 325 /** 326 * Parse a format specifier. 327 * 328 * @param pattern 329 * pattern to parse. 330 * @param patternConverters 331 * list to receive pattern converters. 332 * @param formattingInfos 333 * list to receive field specifiers corresponding to pattern converters. 334 * @param disableAnsi 335 * do not output ANSI escape codes 336 * @param noConsoleNoAnsi 337 * do not do not output ANSI escape codes if {@link System#console()} 338 * @param convertBackslashes if {@code true}, backslash characters are treated as escape characters and character 339 * sequences like "\" followed by "t" (backslash+t) are converted to special characters like '\t' (tab). 340 */ 341 public void parse(final String pattern, final List<PatternConverter> patternConverters, 342 final List<FormattingInfo> formattingInfos, final boolean disableAnsi, 343 final boolean noConsoleNoAnsi, final boolean convertBackslashes) { 344 Objects.requireNonNull(pattern, "pattern"); 345 346 final StringBuilder currentLiteral = new StringBuilder(BUF_SIZE); 347 348 final int patternLength = pattern.length(); 349 ParserState state = ParserState.LITERAL_STATE; 350 char c; 351 int i = 0; 352 FormattingInfo formattingInfo = FormattingInfo.getDefault(); 353 354 while (i < patternLength) { 355 c = pattern.charAt(i++); 356 357 switch (state) { 358 case LITERAL_STATE: 359 360 // In literal state, the last char is always a literal. 361 if (i == patternLength) { 362 currentLiteral.append(c); 363 364 continue; 365 } 366 367 if (c == ESCAPE_CHAR) { 368 // peek at the next char. 369 switch (pattern.charAt(i)) { 370 case ESCAPE_CHAR: 371 currentLiteral.append(c); 372 i++; // move pointer 373 374 break; 375 376 default: 377 378 if (currentLiteral.length() != 0) { 379 patternConverters.add(new LiteralPatternConverter(config, currentLiteral.toString(), 380 convertBackslashes)); 381 formattingInfos.add(FormattingInfo.getDefault()); 382 } 383 384 currentLiteral.setLength(0); 385 currentLiteral.append(c); // append % 386 state = ParserState.CONVERTER_STATE; 387 formattingInfo = FormattingInfo.getDefault(); 388 } 389 } else { 390 currentLiteral.append(c); 391 } 392 393 break; 394 395 case CONVERTER_STATE: 396 currentLiteral.append(c); 397 398 switch (c) { 399 case '0': 400 // a '0' directly after the % sign indicates zero-padding 401 formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength(), 402 formattingInfo.getMaxLength(), formattingInfo.isLeftTruncate(), true); 403 break; 404 405 case '-': 406 formattingInfo = new FormattingInfo(true, formattingInfo.getMinLength(), 407 formattingInfo.getMaxLength(), formattingInfo.isLeftTruncate(), formattingInfo.isZeroPad()); 408 break; 409 410 case '.': 411 state = ParserState.DOT_STATE; 412 break; 413 414 default: 415 416 if (c >= '0' && c <= '9') { 417 formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), c - '0', 418 formattingInfo.getMaxLength(), formattingInfo.isLeftTruncate(), formattingInfo.isZeroPad()); 419 state = ParserState.MIN_STATE; 420 } else { 421 i = finalizeConverter(c, pattern, i, currentLiteral, formattingInfo, converterRules, 422 patternConverters, formattingInfos, disableAnsi, noConsoleNoAnsi, convertBackslashes); 423 424 // Next pattern is assumed to be a literal. 425 state = ParserState.LITERAL_STATE; 426 formattingInfo = FormattingInfo.getDefault(); 427 currentLiteral.setLength(0); 428 } 429 } // switch 430 431 break; 432 433 case MIN_STATE: 434 currentLiteral.append(c); 435 436 if (c >= '0' && c <= '9') { 437 // Multiply the existing value and add the value of the number just encountered. 438 formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength() 439 * DECIMAL + c - '0', formattingInfo.getMaxLength(), formattingInfo.isLeftTruncate(), formattingInfo.isZeroPad()); 440 } else if (c == '.') { 441 state = ParserState.DOT_STATE; 442 } else { 443 i = finalizeConverter(c, pattern, i, currentLiteral, formattingInfo, converterRules, 444 patternConverters, formattingInfos, disableAnsi, noConsoleNoAnsi, convertBackslashes); 445 state = ParserState.LITERAL_STATE; 446 formattingInfo = FormattingInfo.getDefault(); 447 currentLiteral.setLength(0); 448 } 449 450 break; 451 452 case DOT_STATE: 453 currentLiteral.append(c); 454 switch (c) { 455 case '-': 456 formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength(), 457 formattingInfo.getMaxLength(),false, formattingInfo.isZeroPad()); 458 break; 459 460 default: 461 462 if (c >= '0' && c <= '9') { 463 formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength(), 464 c - '0', formattingInfo.isLeftTruncate(), formattingInfo.isZeroPad()); 465 state = ParserState.MAX_STATE; 466 } else { 467 LOGGER.error("Error occurred in position " + i + ".\n Was expecting digit, instead got char \"" + c 468 + "\"."); 469 470 state = ParserState.LITERAL_STATE; 471 } 472 } 473 474 break; 475 476 case MAX_STATE: 477 currentLiteral.append(c); 478 479 if (c >= '0' && c <= '9') { 480 // Multiply the existing value and add the value of the number just encountered. 481 formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength(), 482 formattingInfo.getMaxLength() * DECIMAL + c - '0', formattingInfo.isLeftTruncate(), formattingInfo.isZeroPad()); 483 } else { 484 i = finalizeConverter(c, pattern, i, currentLiteral, formattingInfo, converterRules, 485 patternConverters, formattingInfos, disableAnsi, noConsoleNoAnsi, convertBackslashes); 486 state = ParserState.LITERAL_STATE; 487 formattingInfo = FormattingInfo.getDefault(); 488 currentLiteral.setLength(0); 489 } 490 491 break; 492 } // switch 493 } 494 495 // while 496 if (currentLiteral.length() != 0) { 497 patternConverters.add(new LiteralPatternConverter(config, currentLiteral.toString(), convertBackslashes)); 498 formattingInfos.add(FormattingInfo.getDefault()); 499 } 500 } 501 502 /** 503 * Creates a new PatternConverter. 504 * 505 * @param converterId 506 * converterId. 507 * @param currentLiteral 508 * literal to be used if converter is unrecognized or following converter if converterId contains extra 509 * characters. 510 * @param rules 511 * map of stock pattern converters keyed by format specifier. 512 * @param options 513 * converter options. 514 * @param disableAnsi 515 * do not output ANSI escape codes 516 * @param noConsoleNoAnsi 517 * do not do not output ANSI escape codes if {@link System#console()} 518 * @return converter or null. 519 */ 520 private PatternConverter createConverter(final String converterId, final StringBuilder currentLiteral, 521 final Map<String, Class<PatternConverter>> rules, final List<String> options, final boolean disableAnsi, 522 final boolean noConsoleNoAnsi) { 523 String converterName = converterId; 524 Class<PatternConverter> converterClass = null; 525 526 if (rules == null) { 527 LOGGER.error("Null rules for [" + converterId + ']'); 528 return null; 529 } 530 for (int i = converterId.length(); i > 0 && converterClass == null; i--) { 531 converterName = converterName.substring(0, i); 532 converterClass = rules.get(converterName); 533 } 534 535 if (converterClass == null) { 536 LOGGER.error("Unrecognized format specifier [" + converterId + ']'); 537 return null; 538 } 539 540 if (AnsiConverter.class.isAssignableFrom(converterClass)) { 541 options.add(DISABLE_ANSI + '=' + disableAnsi); 542 options.add(NO_CONSOLE_NO_ANSI + '=' + noConsoleNoAnsi); 543 } 544 // Work around the regression bug in Class.getDeclaredMethods() in Oracle Java in version > 1.6.0_17: 545 // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6815786 546 final Method[] methods = converterClass.getDeclaredMethods(); 547 Method newInstanceMethod = null; 548 for (final Method method : methods) { 549 if (Modifier.isStatic(method.getModifiers()) 550 && method.getDeclaringClass().equals(converterClass) 551 && method.getName().equals("newInstance") 552 && areValidNewInstanceParameters(method.getParameterTypes())) { 553 if (newInstanceMethod == null) { 554 newInstanceMethod = method; 555 } else if (method.getReturnType().equals(newInstanceMethod.getReturnType())) { 556 LOGGER.error("Class " + converterClass + " cannot contain multiple static newInstance methods"); 557 return null; 558 } 559 } 560 } 561 if (newInstanceMethod == null) { 562 LOGGER.error("Class " + converterClass + " does not contain a static newInstance method"); 563 return null; 564 } 565 566 final Class<?>[] parmTypes = newInstanceMethod.getParameterTypes(); 567 final Object[] parms = parmTypes.length > 0 ? new Object[parmTypes.length] : null; 568 569 if (parms != null) { 570 int i = 0; 571 boolean errors = false; 572 for (final Class<?> clazz : parmTypes) { 573 if (clazz.isArray() && clazz.getName().equals("[Ljava.lang.String;")) { 574 final String[] optionsArray = options.toArray(new String[options.size()]); 575 parms[i] = optionsArray; 576 } else if (clazz.isAssignableFrom(Configuration.class)) { 577 parms[i] = config; 578 } else { 579 LOGGER.error("Unknown parameter type " + clazz.getName() + " for static newInstance method of " 580 + converterClass.getName()); 581 errors = true; 582 } 583 ++i; 584 } 585 if (errors) { 586 return null; 587 } 588 } 589 590 try { 591 final Object newObj = newInstanceMethod.invoke(null, parms); 592 593 if (newObj instanceof PatternConverter) { 594 currentLiteral.delete(0, currentLiteral.length() - (converterId.length() - converterName.length())); 595 596 return (PatternConverter) newObj; 597 } 598 LOGGER.warn("Class {} does not extend PatternConverter.", converterClass.getName()); 599 } catch (final Exception ex) { 600 LOGGER.error("Error creating converter for " + converterId, ex); 601 } 602 603 return null; 604 } 605 606 /** LOG4J2-2564: Returns true if all method parameters are valid for injection. */ 607 private static boolean areValidNewInstanceParameters(Class<?>[] parameterTypes) { 608 for (Class<?> clazz : parameterTypes) { 609 if (!clazz.isAssignableFrom(Configuration.class) 610 && !(clazz.isArray() && "[Ljava.lang.String;".equals(clazz.getName()))) { 611 return false; 612 } 613 } 614 return true; 615 } 616 617 /** 618 * Processes a format specifier sequence. 619 * 620 * @param c 621 * initial character of format specifier. 622 * @param pattern 623 * conversion pattern 624 * @param start 625 * current position in conversion pattern. 626 * @param currentLiteral 627 * current literal. 628 * @param formattingInfo 629 * current field specifier. 630 * @param rules 631 * map of stock pattern converters keyed by format specifier. 632 * @param patternConverters 633 * list to receive parsed pattern converter. 634 * @param formattingInfos 635 * list to receive corresponding field specifier. 636 * @param disableAnsi 637 * do not output ANSI escape codes 638 * @param noConsoleNoAnsi 639 * do not do not output ANSI escape codes if {@link System#console()} 640 * @param convertBackslashes if {@code true}, backslash characters are treated as escape characters and character 641 * sequences like "\" followed by "t" (backslash+t) are converted to special characters like '\t' (tab). 642 * @return position after format specifier sequence. 643 */ 644 private int finalizeConverter(final char c, final String pattern, final int start, 645 final StringBuilder currentLiteral, final FormattingInfo formattingInfo, 646 final Map<String, Class<PatternConverter>> rules, final List<PatternConverter> patternConverters, 647 final List<FormattingInfo> formattingInfos, final boolean disableAnsi, final boolean noConsoleNoAnsi, 648 final boolean convertBackslashes) { 649 int i = start; 650 final StringBuilder convBuf = new StringBuilder(); 651 i = extractConverter(c, pattern, i, convBuf, currentLiteral); 652 653 final String converterId = convBuf.toString(); 654 655 final List<String> options = new ArrayList<>(); 656 i = extractOptions(pattern, i, options); 657 658 final PatternConverter pc = createConverter(converterId, currentLiteral, rules, options, disableAnsi, 659 noConsoleNoAnsi); 660 661 if (pc == null) { 662 StringBuilder msg; 663 664 if (Strings.isEmpty(converterId)) { 665 msg = new StringBuilder("Empty conversion specifier starting at position "); 666 } else { 667 msg = new StringBuilder("Unrecognized conversion specifier ["); 668 msg.append(converterId); 669 msg.append("] starting at position "); 670 } 671 672 msg.append(Integer.toString(i)); 673 msg.append(" in conversion pattern."); 674 675 LOGGER.error(msg.toString()); 676 677 patternConverters.add(new LiteralPatternConverter(config, currentLiteral.toString(), convertBackslashes)); 678 formattingInfos.add(FormattingInfo.getDefault()); 679 } else { 680 patternConverters.add(pc); 681 formattingInfos.add(formattingInfo); 682 683 if (currentLiteral.length() > 0) { 684 patternConverters 685 .add(new LiteralPatternConverter(config, currentLiteral.toString(), convertBackslashes)); 686 formattingInfos.add(FormattingInfo.getDefault()); 687 } 688 } 689 690 currentLiteral.setLength(0); 691 692 return i; 693 } 694}