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