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