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 '-': 400 formattingInfo = new FormattingInfo(true, formattingInfo.getMinLength(), 401 formattingInfo.getMaxLength(), formattingInfo.isLeftTruncate()); 402 break; 403 404 case '.': 405 state = ParserState.DOT_STATE; 406 break; 407 408 default: 409 410 if (c >= '0' && c <= '9') { 411 formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), c - '0', 412 formattingInfo.getMaxLength(), formattingInfo.isLeftTruncate()); 413 state = ParserState.MIN_STATE; 414 } else { 415 i = finalizeConverter(c, pattern, i, currentLiteral, formattingInfo, converterRules, 416 patternConverters, formattingInfos, disableAnsi, noConsoleNoAnsi, convertBackslashes); 417 418 // Next pattern is assumed to be a literal. 419 state = ParserState.LITERAL_STATE; 420 formattingInfo = FormattingInfo.getDefault(); 421 currentLiteral.setLength(0); 422 } 423 } // switch 424 425 break; 426 427 case MIN_STATE: 428 currentLiteral.append(c); 429 430 if (c >= '0' && c <= '9') { 431 // Multiply the existing value and add the value of the number just encountered. 432 formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength() 433 * DECIMAL + c - '0', formattingInfo.getMaxLength(), formattingInfo.isLeftTruncate()); 434 } else if (c == '.') { 435 state = ParserState.DOT_STATE; 436 } else { 437 i = finalizeConverter(c, pattern, i, currentLiteral, formattingInfo, converterRules, 438 patternConverters, formattingInfos, disableAnsi, noConsoleNoAnsi, convertBackslashes); 439 state = ParserState.LITERAL_STATE; 440 formattingInfo = FormattingInfo.getDefault(); 441 currentLiteral.setLength(0); 442 } 443 444 break; 445 446 case DOT_STATE: 447 currentLiteral.append(c); 448 switch (c) { 449 case '-': 450 formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength(), 451 formattingInfo.getMaxLength(),false); 452 break; 453 454 default: 455 456 if (c >= '0' && c <= '9') { 457 formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength(), 458 c - '0', formattingInfo.isLeftTruncate()); 459 state = ParserState.MAX_STATE; 460 } else { 461 LOGGER.error("Error occurred in position " + i + ".\n Was expecting digit, instead got char \"" + c 462 + "\"."); 463 464 state = ParserState.LITERAL_STATE; 465 } 466 } 467 468 break; 469 470 case MAX_STATE: 471 currentLiteral.append(c); 472 473 if (c >= '0' && c <= '9') { 474 // Multiply the existing value and add the value of the number just encountered. 475 formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength(), 476 formattingInfo.getMaxLength() * DECIMAL + c - '0', formattingInfo.isLeftTruncate()); 477 } else { 478 i = finalizeConverter(c, pattern, i, currentLiteral, formattingInfo, converterRules, 479 patternConverters, formattingInfos, disableAnsi, noConsoleNoAnsi, convertBackslashes); 480 state = ParserState.LITERAL_STATE; 481 formattingInfo = FormattingInfo.getDefault(); 482 currentLiteral.setLength(0); 483 } 484 485 break; 486 } // switch 487 } 488 489 // while 490 if (currentLiteral.length() != 0) { 491 patternConverters.add(new LiteralPatternConverter(config, currentLiteral.toString(), convertBackslashes)); 492 formattingInfos.add(FormattingInfo.getDefault()); 493 } 494 } 495 496 /** 497 * Creates a new PatternConverter. 498 * 499 * @param converterId 500 * converterId. 501 * @param currentLiteral 502 * literal to be used if converter is unrecognized or following converter if converterId contains extra 503 * characters. 504 * @param rules 505 * map of stock pattern converters keyed by format specifier. 506 * @param options 507 * converter options. 508 * @param disableAnsi 509 * do not output ANSI escape codes 510 * @param noConsoleNoAnsi 511 * do not do not output ANSI escape codes if {@link System#console()} 512 * @return converter or null. 513 */ 514 private PatternConverter createConverter(final String converterId, final StringBuilder currentLiteral, 515 final Map<String, Class<PatternConverter>> rules, final List<String> options, final boolean disableAnsi, 516 final boolean noConsoleNoAnsi) { 517 String converterName = converterId; 518 Class<PatternConverter> converterClass = null; 519 520 if (rules == null) { 521 LOGGER.error("Null rules for [" + converterId + ']'); 522 return null; 523 } 524 for (int i = converterId.length(); i > 0 && converterClass == null; i--) { 525 converterName = converterName.substring(0, i); 526 converterClass = rules.get(converterName); 527 } 528 529 if (converterClass == null) { 530 LOGGER.error("Unrecognized format specifier [" + converterId + ']'); 531 return null; 532 } 533 534 if (AnsiConverter.class.isAssignableFrom(converterClass)) { 535 options.add(DISABLE_ANSI + '=' + disableAnsi); 536 options.add(NO_CONSOLE_NO_ANSI + '=' + noConsoleNoAnsi); 537 } 538 // Work around the regression bug in Class.getDeclaredMethods() in Oracle Java in version > 1.6.0_17: 539 // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6815786 540 final Method[] methods = converterClass.getDeclaredMethods(); 541 Method newInstanceMethod = null; 542 for (final Method method : methods) { 543 if (Modifier.isStatic(method.getModifiers()) && method.getDeclaringClass().equals(converterClass) 544 && method.getName().equals("newInstance")) { 545 if (newInstanceMethod == null) { 546 newInstanceMethod = method; 547 } else if (method.getReturnType().equals(newInstanceMethod.getReturnType())) { 548 LOGGER.error("Class " + converterClass + " cannot contain multiple static newInstance methods"); 549 return null; 550 } 551 } 552 } 553 if (newInstanceMethod == null) { 554 LOGGER.error("Class " + converterClass + " does not contain a static newInstance method"); 555 return null; 556 } 557 558 final Class<?>[] parmTypes = newInstanceMethod.getParameterTypes(); 559 final Object[] parms = parmTypes.length > 0 ? new Object[parmTypes.length] : null; 560 561 if (parms != null) { 562 int i = 0; 563 boolean errors = false; 564 for (final Class<?> clazz : parmTypes) { 565 if (clazz.isArray() && clazz.getName().equals("[Ljava.lang.String;")) { 566 final String[] optionsArray = options.toArray(new String[options.size()]); 567 parms[i] = optionsArray; 568 } else if (clazz.isAssignableFrom(Configuration.class)) { 569 parms[i] = config; 570 } else { 571 LOGGER.error("Unknown parameter type " + clazz.getName() + " for static newInstance method of " 572 + converterClass.getName()); 573 errors = true; 574 } 575 ++i; 576 } 577 if (errors) { 578 return null; 579 } 580 } 581 582 try { 583 final Object newObj = newInstanceMethod.invoke(null, parms); 584 585 if (newObj instanceof PatternConverter) { 586 currentLiteral.delete(0, currentLiteral.length() - (converterId.length() - converterName.length())); 587 588 return (PatternConverter) newObj; 589 } 590 LOGGER.warn("Class {} does not extend PatternConverter.", converterClass.getName()); 591 } catch (final Exception ex) { 592 LOGGER.error("Error creating converter for " + converterId, ex); 593 } 594 595 return null; 596 } 597 598 /** 599 * Processes a format specifier sequence. 600 * 601 * @param c 602 * initial character of format specifier. 603 * @param pattern 604 * conversion pattern 605 * @param start 606 * current position in conversion pattern. 607 * @param currentLiteral 608 * current literal. 609 * @param formattingInfo 610 * current field specifier. 611 * @param rules 612 * map of stock pattern converters keyed by format specifier. 613 * @param patternConverters 614 * list to receive parsed pattern converter. 615 * @param formattingInfos 616 * list to receive corresponding field specifier. 617 * @param disableAnsi 618 * do not output ANSI escape codes 619 * @param noConsoleNoAnsi 620 * do not do not output ANSI escape codes if {@link System#console()} 621 * @param convertBackslashes if {@code true}, backslash characters are treated as escape characters and character 622 * sequences like "\" followed by "t" (backslash+t) are converted to special characters like '\t' (tab). 623 * @return position after format specifier sequence. 624 */ 625 private int finalizeConverter(final char c, final String pattern, final int start, 626 final StringBuilder currentLiteral, final FormattingInfo formattingInfo, 627 final Map<String, Class<PatternConverter>> rules, final List<PatternConverter> patternConverters, 628 final List<FormattingInfo> formattingInfos, final boolean disableAnsi, final boolean noConsoleNoAnsi, 629 final boolean convertBackslashes) { 630 int i = start; 631 final StringBuilder convBuf = new StringBuilder(); 632 i = extractConverter(c, pattern, i, convBuf, currentLiteral); 633 634 final String converterId = convBuf.toString(); 635 636 final List<String> options = new ArrayList<>(); 637 i = extractOptions(pattern, i, options); 638 639 final PatternConverter pc = createConverter(converterId, currentLiteral, rules, options, disableAnsi, 640 noConsoleNoAnsi); 641 642 if (pc == null) { 643 StringBuilder msg; 644 645 if (Strings.isEmpty(converterId)) { 646 msg = new StringBuilder("Empty conversion specifier starting at position "); 647 } else { 648 msg = new StringBuilder("Unrecognized conversion specifier ["); 649 msg.append(converterId); 650 msg.append("] starting at position "); 651 } 652 653 msg.append(Integer.toString(i)); 654 msg.append(" in conversion pattern."); 655 656 LOGGER.error(msg.toString()); 657 658 patternConverters.add(new LiteralPatternConverter(config, currentLiteral.toString(), convertBackslashes)); 659 formattingInfos.add(FormattingInfo.getDefault()); 660 } else { 661 patternConverters.add(pc); 662 formattingInfos.add(formattingInfo); 663 664 if (currentLiteral.length() > 0) { 665 patternConverters 666 .add(new LiteralPatternConverter(config, currentLiteral.toString(), convertBackslashes)); 667 formattingInfos.add(FormattingInfo.getDefault()); 668 } 669 } 670 671 currentLiteral.setLength(0); 672 673 return i; 674 } 675}