View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache license, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the license for the specific language governing permissions and
15   * limitations under the license.
16   */
17  package org.apache.logging.log4j.core.pattern;
18  
19  import org.apache.logging.log4j.Logger;
20  import org.apache.logging.log4j.core.config.Configuration;
21  import org.apache.logging.log4j.core.config.plugins.PluginManager;
22  import org.apache.logging.log4j.core.config.plugins.PluginType;
23  import org.apache.logging.log4j.status.StatusLogger;
24  
25  import java.lang.reflect.Method;
26  import java.lang.reflect.Modifier;
27  import java.util.ArrayList;
28  import java.util.HashMap;
29  import java.util.Iterator;
30  import java.util.List;
31  import java.util.Map;
32  
33  /**
34   * Most of the work of the {@link org.apache.logging.log4j.core.layout.PatternLayout} class
35   * is delegated to the PatternParser class.
36   * <p>It is this class that parses conversion patterns and creates
37   * a chained list of {@link PatternConverter PatternConverters}.
38   */
39  public final class PatternParser {
40      /**
41       * Escape character for format specifier.
42       */
43      private static final char ESCAPE_CHAR = '%';
44  
45      /**
46       * The states the parser can be in while parsing the pattern.
47       */
48      private enum ParserState {
49          /**
50           * Literal state.
51           */
52          LITERAL_STATE,
53  
54          /**
55           * In converter name state.
56           */
57          CONVERTER_STATE,
58  
59          /**
60           * Dot state.
61           */
62          DOT_STATE,
63  
64          /**
65           * Min state.
66           */
67          MIN_STATE,
68  
69          /**
70           * Max state.
71           */
72          MAX_STATE;
73      }
74  
75      private static final Logger LOGGER = StatusLogger.getLogger();
76  
77      private static final int BUF_SIZE = 32;
78  
79      private static final int DECIMAL = 10;
80  
81      private final Configuration config;
82  
83      private final Map<String, Class<PatternConverter>> converterRules;
84  
85  
86      /**
87       * Constructor.
88       * @param converterKey The type of converters that will be used.
89       */
90      public PatternParser(final String converterKey) {
91          this(null, converterKey, null, null);
92      }
93  
94      /**
95       * Constructor.
96       * @param config The current Configuration.
97       * @param converterKey The key to lookup the converters.
98       * @param expected The expected base Class of each Converter.
99       */
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 }