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