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