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 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 for (final PatternConverter converter : converters) { 177 if (converter instanceof NanoTimePatternConverter) { 178 // LOG4J2-1074 Switch to actual clock if nanosecond timestamps are required in config. 179 // LOG4J2-1248 set config nanoclock 180 if (config != null) { 181 config.setNanoClock(new SystemNanoClock()); 182 } 183 } 184 LogEventPatternConverter pc; 185 if (converter instanceof LogEventPatternConverter) { 186 pc = (LogEventPatternConverter) converter; 187 handlesThrowable |= pc.handlesThrowable(); 188 } else { 189 pc = new LiteralPatternConverter(config, Strings.EMPTY, true); 190 } 191 192 FormattingInfo field; 193 if (fieldIter.hasNext()) { 194 field = fieldIter.next(); 195 } else { 196 field = FormattingInfo.getDefault(); 197 } 198 list.add(new PatternFormatter(pc, field)); 199 } 200 if (alwaysWriteExceptions && !handlesThrowable) { 201 final LogEventPatternConverter pc = ExtendedThrowablePatternConverter.newInstance(null); 202 list.add(new PatternFormatter(pc, FormattingInfo.getDefault())); 203 } 204 return list; 205 } 206 207 /** 208 * Extracts the converter identifier found at the given start position. 209 * <p> 210 * After this function returns, the variable i will point to the first char after the end of the converter 211 * identifier. 212 * </p> 213 * <p> 214 * If i points to a char which is not a character acceptable at the start of a unicode identifier, the value null is 215 * returned. 216 * </p> 217 * 218 * @param lastChar 219 * last processed character. 220 * @param pattern 221 * format string. 222 * @param start 223 * current index into pattern format. 224 * @param convBuf 225 * buffer to receive conversion specifier. 226 * @param currentLiteral 227 * literal to be output in case format specifier in unrecognized. 228 * @return position in pattern after converter. 229 */ 230 private static int extractConverter(final char lastChar, final String pattern, final int start, 231 final StringBuilder convBuf, final StringBuilder currentLiteral) { 232 int i = start; 233 convBuf.setLength(0); 234 235 // When this method is called, lastChar points to the first character of the 236 // conversion word. For example: 237 // For "%hello" lastChar = 'h' 238 // For "%-5hello" lastChar = 'h' 239 // System.out.println("lastchar is "+lastChar); 240 if (!Character.isUnicodeIdentifierStart(lastChar)) { 241 return i; 242 } 243 244 convBuf.append(lastChar); 245 246 while (i < pattern.length() && Character.isUnicodeIdentifierPart(pattern.charAt(i))) { 247 convBuf.append(pattern.charAt(i)); 248 currentLiteral.append(pattern.charAt(i)); 249 i++; 250 } 251 252 return i; 253 } 254 255 /** 256 * Extract options. 257 * 258 * @param pattern 259 * conversion pattern. 260 * @param start 261 * start of options. 262 * @param options 263 * array to receive extracted options 264 * @return position in pattern after options. 265 */ 266 private static int extractOptions(final String pattern, final int start, final List<String> options) { 267 int i = start; 268 while (i < pattern.length() && pattern.charAt(i) == '{') { 269 final int begin = i++; 270 int end; 271 int depth = 0; 272 do { 273 end = pattern.indexOf('}', i); 274 if (end == -1) { 275 break; 276 } 277 final int next = pattern.indexOf("{", i); 278 if (next != -1 && next < end) { 279 i = end + 1; 280 ++depth; 281 } else if (depth > 0) { 282 --depth; 283 } 284 } while (depth > 0); 285 286 if (end == -1) { 287 break; 288 } 289 290 final String r = pattern.substring(begin + 1, end); 291 options.add(r); 292 i = end + 1; 293 } 294 295 return i; 296 } 297 298 /** 299 * Parse a format specifier. 300 * 301 * @param pattern 302 * pattern to parse. 303 * @param patternConverters 304 * list to receive pattern converters. 305 * @param formattingInfos 306 * list to receive field specifiers corresponding to pattern converters. 307 * @param noConsoleNoAnsi 308 * TODO 309 * @param convertBackslashes if {@code true}, backslash characters are treated as escape characters and character 310 * sequences like "\" followed by "t" (backslash+t) are converted to special characters like '\t' (tab). 311 */ 312 public void parse(final String pattern, final List<PatternConverter> patternConverters, 313 final List<FormattingInfo> formattingInfos, final boolean noConsoleNoAnsi, 314 final boolean convertBackslashes) { 315 Objects.requireNonNull(pattern, "pattern"); 316 317 final StringBuilder currentLiteral = new StringBuilder(BUF_SIZE); 318 319 final int patternLength = pattern.length(); 320 ParserState state = ParserState.LITERAL_STATE; 321 char c; 322 int i = 0; 323 FormattingInfo formattingInfo = FormattingInfo.getDefault(); 324 325 while (i < patternLength) { 326 c = pattern.charAt(i++); 327 328 switch (state) { 329 case LITERAL_STATE: 330 331 // In literal state, the last char is always a literal. 332 if (i == patternLength) { 333 currentLiteral.append(c); 334 335 continue; 336 } 337 338 if (c == ESCAPE_CHAR) { 339 // peek at the next char. 340 switch (pattern.charAt(i)) { 341 case ESCAPE_CHAR: 342 currentLiteral.append(c); 343 i++; // move pointer 344 345 break; 346 347 default: 348 349 if (currentLiteral.length() != 0) { 350 patternConverters.add(new LiteralPatternConverter(config, currentLiteral.toString(), 351 convertBackslashes)); 352 formattingInfos.add(FormattingInfo.getDefault()); 353 } 354 355 currentLiteral.setLength(0); 356 currentLiteral.append(c); // append % 357 state = ParserState.CONVERTER_STATE; 358 formattingInfo = FormattingInfo.getDefault(); 359 } 360 } else { 361 currentLiteral.append(c); 362 } 363 364 break; 365 366 case CONVERTER_STATE: 367 currentLiteral.append(c); 368 369 switch (c) { 370 case '-': 371 formattingInfo = new FormattingInfo(true, formattingInfo.getMinLength(), 372 formattingInfo.getMaxLength(), formattingInfo.isLeftTruncate()); 373 break; 374 375 case '.': 376 state = ParserState.DOT_STATE; 377 break; 378 379 default: 380 381 if (c >= '0' && c <= '9') { 382 formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), c - '0', 383 formattingInfo.getMaxLength(), formattingInfo.isLeftTruncate()); 384 state = ParserState.MIN_STATE; 385 } else { 386 i = finalizeConverter(c, pattern, i, currentLiteral, formattingInfo, converterRules, 387 patternConverters, formattingInfos, noConsoleNoAnsi, convertBackslashes); 388 389 // Next pattern is assumed to be a literal. 390 state = ParserState.LITERAL_STATE; 391 formattingInfo = FormattingInfo.getDefault(); 392 currentLiteral.setLength(0); 393 } 394 } // switch 395 396 break; 397 398 case MIN_STATE: 399 currentLiteral.append(c); 400 401 if (c >= '0' && c <= '9') { 402 // Multiply the existing value and add the value of the number just encountered. 403 formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength() 404 * DECIMAL + c - '0', formattingInfo.getMaxLength(), formattingInfo.isLeftTruncate()); 405 } else if (c == '.') { 406 state = ParserState.DOT_STATE; 407 } else { 408 i = finalizeConverter(c, pattern, i, currentLiteral, formattingInfo, converterRules, 409 patternConverters, formattingInfos, noConsoleNoAnsi, convertBackslashes); 410 state = ParserState.LITERAL_STATE; 411 formattingInfo = FormattingInfo.getDefault(); 412 currentLiteral.setLength(0); 413 } 414 415 break; 416 417 case DOT_STATE: 418 currentLiteral.append(c); 419 switch (c) { 420 case '-': 421 formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength(), 422 formattingInfo.getMaxLength(),false); 423 break; 424 425 default: 426 427 if (c >= '0' && c <= '9') { 428 formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength(), 429 c - '0', formattingInfo.isLeftTruncate()); 430 state = ParserState.MAX_STATE; 431 } else { 432 LOGGER.error("Error occurred in position " + i + ".\n Was expecting digit, instead got char \"" + c 433 + "\"."); 434 435 state = ParserState.LITERAL_STATE; 436 } 437 } 438 439 break; 440 441 case MAX_STATE: 442 currentLiteral.append(c); 443 444 if (c >= '0' && c <= '9') { 445 // Multiply the existing value and add the value of the number just encountered. 446 formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength(), 447 formattingInfo.getMaxLength() * DECIMAL + c - '0', formattingInfo.isLeftTruncate()); 448 } else { 449 i = finalizeConverter(c, pattern, i, currentLiteral, formattingInfo, converterRules, 450 patternConverters, formattingInfos, noConsoleNoAnsi, convertBackslashes); 451 state = ParserState.LITERAL_STATE; 452 formattingInfo = FormattingInfo.getDefault(); 453 currentLiteral.setLength(0); 454 } 455 456 break; 457 } // switch 458 } 459 460 // while 461 if (currentLiteral.length() != 0) { 462 patternConverters.add(new LiteralPatternConverter(config, currentLiteral.toString(), convertBackslashes)); 463 formattingInfos.add(FormattingInfo.getDefault()); 464 } 465 } 466 467 /** 468 * Creates a new PatternConverter. 469 * 470 * @param converterId 471 * converterId. 472 * @param currentLiteral 473 * literal to be used if converter is unrecognized or following converter if converterId contains extra 474 * characters. 475 * @param rules 476 * map of stock pattern converters keyed by format specifier. 477 * @param options 478 * converter options. 479 * @param noConsoleNoAnsi TODO 480 * @return converter or null. 481 */ 482 private PatternConverter createConverter(final String converterId, final StringBuilder currentLiteral, 483 final Map<String, Class<PatternConverter>> rules, final List<String> options, final boolean noConsoleNoAnsi) { 484 String converterName = converterId; 485 Class<PatternConverter> converterClass = null; 486 487 if (rules == null) { 488 LOGGER.error("Null rules for [" + converterId + ']'); 489 return null; 490 } 491 for (int i = converterId.length(); i > 0 && converterClass == null; i--) { 492 converterName = converterName.substring(0, i); 493 converterClass = rules.get(converterName); 494 } 495 496 if (converterClass == null) { 497 LOGGER.error("Unrecognized format specifier [" + converterId + ']'); 498 return null; 499 } 500 501 if (AnsiConverter.class.isAssignableFrom(converterClass)) { 502 options.add(NO_CONSOLE_NO_ANSI + '=' + noConsoleNoAnsi); 503 } 504 // Work around the regression bug in Class.getDeclaredMethods() in Oracle Java in version > 1.6.0_17: 505 // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6815786 506 final Method[] methods = converterClass.getDeclaredMethods(); 507 Method newInstanceMethod = null; 508 for (final Method method : methods) { 509 if (Modifier.isStatic(method.getModifiers()) && method.getDeclaringClass().equals(converterClass) 510 && method.getName().equals("newInstance")) { 511 if (newInstanceMethod == null) { 512 newInstanceMethod = method; 513 } else if (method.getReturnType().equals(newInstanceMethod.getReturnType())) { 514 LOGGER.error("Class " + converterClass + " cannot contain multiple static newInstance methods"); 515 return null; 516 } 517 } 518 } 519 if (newInstanceMethod == null) { 520 LOGGER.error("Class " + converterClass + " does not contain a static newInstance method"); 521 return null; 522 } 523 524 final Class<?>[] parmTypes = newInstanceMethod.getParameterTypes(); 525 final Object[] parms = parmTypes.length > 0 ? new Object[parmTypes.length] : null; 526 527 if (parms != null) { 528 int i = 0; 529 boolean errors = false; 530 for (final Class<?> clazz : parmTypes) { 531 if (clazz.isArray() && clazz.getName().equals("[Ljava.lang.String;")) { 532 final String[] optionsArray = options.toArray(new String[options.size()]); 533 parms[i] = optionsArray; 534 } else if (clazz.isAssignableFrom(Configuration.class)) { 535 parms[i] = config; 536 } else { 537 LOGGER.error("Unknown parameter type " + clazz.getName() + " for static newInstance method of " 538 + converterClass.getName()); 539 errors = true; 540 } 541 ++i; 542 } 543 if (errors) { 544 return null; 545 } 546 } 547 548 try { 549 final Object newObj = newInstanceMethod.invoke(null, parms); 550 551 if (newObj instanceof PatternConverter) { 552 currentLiteral.delete(0, currentLiteral.length() - (converterId.length() - converterName.length())); 553 554 return (PatternConverter) newObj; 555 } 556 LOGGER.warn("Class {} does not extend PatternConverter.", converterClass.getName()); 557 } catch (final Exception ex) { 558 LOGGER.error("Error creating converter for " + converterId, ex); 559 } 560 561 return null; 562 } 563 564 /** 565 * Processes a format specifier sequence. 566 * 567 * @param c 568 * initial character of format specifier. 569 * @param pattern 570 * conversion pattern 571 * @param start 572 * current position in conversion pattern. 573 * @param currentLiteral 574 * current literal. 575 * @param formattingInfo 576 * current field specifier. 577 * @param rules 578 * map of stock pattern converters keyed by format specifier. 579 * @param patternConverters 580 * list to receive parsed pattern converter. 581 * @param formattingInfos 582 * list to receive corresponding field specifier. 583 * @param noConsoleNoAnsi 584 * TODO 585 * @param convertBackslashes if {@code true}, backslash characters are treated as escape characters and character 586 * sequences like "\" followed by "t" (backslash+t) are converted to special characters like '\t' (tab). 587 * @return position after format specifier sequence. 588 */ 589 private int finalizeConverter(final char c, final String pattern, final int start, 590 final StringBuilder currentLiteral, final FormattingInfo formattingInfo, 591 final Map<String, Class<PatternConverter>> rules, final List<PatternConverter> patternConverters, 592 final List<FormattingInfo> formattingInfos, final boolean noConsoleNoAnsi, final boolean convertBackslashes) { 593 int i = start; 594 final StringBuilder convBuf = new StringBuilder(); 595 i = extractConverter(c, pattern, i, convBuf, currentLiteral); 596 597 final String converterId = convBuf.toString(); 598 599 final List<String> options = new ArrayList<>(); 600 i = extractOptions(pattern, i, options); 601 602 final PatternConverter pc = createConverter(converterId, currentLiteral, rules, options, noConsoleNoAnsi); 603 604 if (pc == null) { 605 StringBuilder msg; 606 607 if (Strings.isEmpty(converterId)) { 608 msg = new StringBuilder("Empty conversion specifier starting at position "); 609 } else { 610 msg = new StringBuilder("Unrecognized conversion specifier ["); 611 msg.append(converterId); 612 msg.append("] starting at position "); 613 } 614 615 msg.append(Integer.toString(i)); 616 msg.append(" in conversion pattern."); 617 618 LOGGER.error(msg.toString()); 619 620 patternConverters.add(new LiteralPatternConverter(config, currentLiteral.toString(), convertBackslashes)); 621 formattingInfos.add(FormattingInfo.getDefault()); 622 } else { 623 patternConverters.add(pc); 624 formattingInfos.add(formattingInfo); 625 626 if (currentLiteral.length() > 0) { 627 patternConverters 628 .add(new LiteralPatternConverter(config, currentLiteral.toString(), convertBackslashes)); 629 formattingInfos.add(FormattingInfo.getDefault()); 630 } 631 } 632 633 currentLiteral.setLength(0); 634 635 return i; 636 } 637}