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 org.apache.logging.log4j.Logger;
020 import org.apache.logging.log4j.core.config.Configuration;
021 import org.apache.logging.log4j.core.config.plugins.util.PluginManager;
022 import org.apache.logging.log4j.core.config.plugins.util.PluginType;
023 import org.apache.logging.log4j.status.StatusLogger;
024 import org.apache.logging.log4j.util.Strings;
025
026 import java.lang.reflect.Method;
027 import java.lang.reflect.Modifier;
028 import java.util.ArrayList;
029 import java.util.Iterator;
030 import java.util.LinkedHashMap;
031 import java.util.List;
032 import 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 */
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(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 }