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