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