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
018package org.apache.log4j.pattern;
019
020import org.apache.log4j.helpers.Loader;
021import org.apache.log4j.helpers.LogLog;
022
023import java.lang.reflect.Method;
024import java.util.ArrayList;
025import java.util.Collection;
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029import java.util.Set;
030
031// Contributors:   Nelson Minar <(nelson@monkey.org>
032//                 Igor E. Poteryaev <jah@mail.ru>
033//                 Reinhard Deschler <reinhard.deschler@web.de>
034
035/**
036 * Most of the work of the {@link org.apache.log4j.EnhancedPatternLayout} class
037 * is delegated to the PatternParser class.
038 * <p>It is this class that parses conversion patterns and creates
039 * a chained list of {@link PatternConverter PatternConverters}.
040 *
041 * @author James P. Cakalic
042 * @author Ceki G&uuml;lc&uuml;
043 * @author Anders Kristensen
044 * @author Paul Smith
045 * @author Curt Arnold
046 *
047*/
048public final class PatternParser {
049  /**
050   * Escape character for format specifier.
051   */
052  private static final char ESCAPE_CHAR = '%';
053
054  /**
055   * Literal state.
056   */
057  private static final int LITERAL_STATE = 0;
058
059  /**
060   * In converter name state.
061   */
062  private static final int CONVERTER_STATE = 1;
063
064  /**
065   * Dot state.
066   */
067  private static final int DOT_STATE = 3;
068
069  /**
070   * Min state.
071   */
072  private static final int MIN_STATE = 4;
073
074  /**
075   * Max state.
076   */
077  private static final int MAX_STATE = 5;
078
079  /**
080   * Standard format specifiers for EnhancedPatternLayout.
081   */
082  private static final Map PATTERN_LAYOUT_RULES;
083
084  /**
085   * Standard format specifiers for rolling file appenders.
086   */
087  private static final Map FILENAME_PATTERN_RULES;
088
089  static {
090    // We set the global rules in the static initializer of PatternParser class
091    Map rules = new HashMap(17);
092    rules.put("c", LoggerPatternConverter.class);
093    rules.put("logger", LoggerPatternConverter.class);
094
095    rules.put("C", ClassNamePatternConverter.class);
096    rules.put("class", ClassNamePatternConverter.class);
097
098    rules.put("d", DatePatternConverter.class);
099    rules.put("date", DatePatternConverter.class);
100
101    rules.put("F", FileLocationPatternConverter.class);
102    rules.put("file", FileLocationPatternConverter.class);
103
104    rules.put("l", FullLocationPatternConverter.class);
105
106    rules.put("L", LineLocationPatternConverter.class);
107    rules.put("line", LineLocationPatternConverter.class);
108
109    rules.put("m", MessagePatternConverter.class);
110    rules.put("message", MessagePatternConverter.class);
111
112    rules.put("n", LineSeparatorPatternConverter.class);
113
114    rules.put("M", MethodLocationPatternConverter.class);
115    rules.put("method", MethodLocationPatternConverter.class);
116
117    rules.put("p", LevelPatternConverter.class);
118    rules.put("level", LevelPatternConverter.class);
119
120    rules.put("r", RelativeTimePatternConverter.class);
121    rules.put("relative", RelativeTimePatternConverter.class);
122
123    rules.put("t", ThreadPatternConverter.class);
124    rules.put("thread", ThreadPatternConverter.class);
125
126    rules.put("x", NDCPatternConverter.class);
127    rules.put("ndc", NDCPatternConverter.class);
128
129    rules.put("X", PropertiesPatternConverter.class);
130    rules.put("properties", PropertiesPatternConverter.class);
131
132    rules.put("sn", SequenceNumberPatternConverter.class);
133    rules.put("sequenceNumber", SequenceNumberPatternConverter.class);
134
135    rules.put("throwable", ThrowableInformationPatternConverter.class);
136    PATTERN_LAYOUT_RULES = new ReadOnlyMap(rules);
137
138    Map fnameRules = new HashMap(4);
139    fnameRules.put("d", FileDatePatternConverter.class);
140    fnameRules.put("date", FileDatePatternConverter.class);
141    fnameRules.put("i", IntegerPatternConverter.class);
142    fnameRules.put("index", IntegerPatternConverter.class);
143
144    FILENAME_PATTERN_RULES = new ReadOnlyMap(fnameRules);
145  }
146
147  /**
148   * Private constructor.
149   */
150  private PatternParser() {
151  }
152
153  /**
154   * Get standard format specifiers for EnhancedPatternLayout.
155   * @return read-only map of format converter classes keyed by format specifier strings.
156   */
157  public static Map getPatternLayoutRules() {
158    return PATTERN_LAYOUT_RULES;
159  }
160
161  /**
162   * Get standard format specifiers for rolling file appender file specification.
163   * @return read-only map of format converter classes keyed by format specifier strings.
164   */
165  public static Map getFileNamePatternRules() {
166    return FILENAME_PATTERN_RULES;
167  }
168
169  /** Extract the converter identifier found at position i.
170   *
171   * After this function returns, the variable i will point to the
172   * first char after the end of the converter identifier.
173   *
174   * If i points to a char which is not a character acceptable at the
175   * start of a unicode identifier, the value null is returned.
176   *
177   * @param lastChar last processed character.
178   * @param pattern format string.
179   * @param i current index into pattern format.
180   * @param convBuf buffer to receive conversion specifier.
181   * @param currentLiteral literal to be output in case format specifier in unrecognized.
182   * @return position in pattern after converter.
183   */
184  private static int extractConverter(
185    char lastChar, final String pattern, int i, final StringBuffer convBuf,
186    final StringBuffer currentLiteral) {
187    convBuf.setLength(0);
188
189    // When this method is called, lastChar points to the first character of the
190    // conversion word. For example:
191    // For "%hello"     lastChar = 'h'
192    // For "%-5hello"   lastChar = 'h'
193    //System.out.println("lastchar is "+lastChar);
194    if (!Character.isUnicodeIdentifierStart(lastChar)) {
195      return i;
196    }
197
198    convBuf.append(lastChar);
199
200    while (
201      (i < pattern.length())
202        && Character.isUnicodeIdentifierPart(pattern.charAt(i))) {
203      convBuf.append(pattern.charAt(i));
204      currentLiteral.append(pattern.charAt(i));
205
206      //System.out.println("conv buffer is now ["+convBuf+"].");
207      i++;
208    }
209
210    return i;
211  }
212
213  /**
214   * Extract options.
215   * @param pattern conversion pattern.
216   * @param i start of options.
217   * @param options array to receive extracted options
218   * @return position in pattern after options.
219   */
220  private static int extractOptions(String pattern, int i, List options) {
221    while ((i < pattern.length()) && (pattern.charAt(i) == '{')) {
222      int end = pattern.indexOf('}', i);
223
224      if (end == -1) {
225        break;
226      }
227
228      String r = pattern.substring(i + 1, end);
229      options.add(r);
230      i = end + 1;
231    }
232
233    return i;
234  }
235
236  /**
237   * Parse a format specifier.
238   * @param pattern pattern to parse.
239   * @param patternConverters list to receive pattern converters.
240   * @param formattingInfos list to receive field specifiers corresponding to pattern converters.
241   * @param converterRegistry map of user-supported pattern converters keyed by format specifier, may be null.
242   * @param rules map of stock pattern converters keyed by format specifier.
243   */
244  public static void parse(
245    final String pattern, final List patternConverters,
246    final List formattingInfos, final Map converterRegistry, final Map rules) {
247    if (pattern == null) {
248      throw new NullPointerException("pattern");
249    }
250
251    StringBuffer currentLiteral = new StringBuffer(32);
252
253    int patternLength = pattern.length();
254    int state = LITERAL_STATE;
255    char c;
256    int i = 0;
257    FormattingInfo formattingInfo = FormattingInfo.getDefault();
258
259    while (i < patternLength) {
260      c = pattern.charAt(i++);
261
262      switch (state) {
263      case LITERAL_STATE:
264
265        // In literal state, the last char is always a literal.
266        if (i == patternLength) {
267          currentLiteral.append(c);
268
269          continue;
270        }
271
272        if (c == ESCAPE_CHAR) {
273          // peek at the next char.
274          switch (pattern.charAt(i)) {
275          case ESCAPE_CHAR:
276            currentLiteral.append(c);
277            i++; // move pointer
278
279            break;
280
281          default:
282
283            if (currentLiteral.length() != 0) {
284              patternConverters.add(
285                new LiteralPatternConverter(currentLiteral.toString()));
286              formattingInfos.add(FormattingInfo.getDefault());
287            }
288
289            currentLiteral.setLength(0);
290            currentLiteral.append(c); // append %
291            state = CONVERTER_STATE;
292            formattingInfo = FormattingInfo.getDefault();
293          }
294        } else {
295          currentLiteral.append(c);
296        }
297
298        break;
299
300      case CONVERTER_STATE:
301        currentLiteral.append(c);
302
303        switch (c) {
304        case '-':
305          formattingInfo =
306            new FormattingInfo(
307              true, formattingInfo.getMinLength(),
308              formattingInfo.getMaxLength());
309
310          break;
311
312        case '.':
313          state = DOT_STATE;
314
315          break;
316
317        default:
318
319          if ((c >= '0') && (c <= '9')) {
320            formattingInfo =
321              new FormattingInfo(
322                formattingInfo.isLeftAligned(), c - '0',
323                formattingInfo.getMaxLength());
324            state = MIN_STATE;
325          } else {
326            i = finalizeConverter(
327                c, pattern, i, currentLiteral, formattingInfo,
328                converterRegistry, rules, patternConverters, formattingInfos);
329
330            // Next pattern is assumed to be a literal.
331            state = LITERAL_STATE;
332            formattingInfo = FormattingInfo.getDefault();
333            currentLiteral.setLength(0);
334          }
335        } // switch
336
337        break;
338
339      case MIN_STATE:
340        currentLiteral.append(c);
341
342        if ((c >= '0') && (c <= '9')) {
343          formattingInfo =
344            new FormattingInfo(
345              formattingInfo.isLeftAligned(),
346              (formattingInfo.getMinLength() * 10) + (c - '0'),
347              formattingInfo.getMaxLength());
348        } else if (c == '.') {
349          state = DOT_STATE;
350        } else {
351          i = finalizeConverter(
352              c, pattern, i, currentLiteral, formattingInfo,
353              converterRegistry, rules, patternConverters, formattingInfos);
354          state = LITERAL_STATE;
355          formattingInfo = FormattingInfo.getDefault();
356          currentLiteral.setLength(0);
357        }
358
359        break;
360
361      case DOT_STATE:
362        currentLiteral.append(c);
363
364        if ((c >= '0') && (c <= '9')) {
365          formattingInfo =
366            new FormattingInfo(
367              formattingInfo.isLeftAligned(), formattingInfo.getMinLength(),
368              c - '0');
369          state = MAX_STATE;
370        } else {
371            LogLog.error(
372              "Error occured in position " + i
373              + ".\n Was expecting digit, instead got char \"" + c + "\".");
374
375          state = LITERAL_STATE;
376        }
377
378        break;
379
380      case MAX_STATE:
381        currentLiteral.append(c);
382
383        if ((c >= '0') && (c <= '9')) {
384          formattingInfo =
385            new FormattingInfo(
386              formattingInfo.isLeftAligned(), formattingInfo.getMinLength(),
387              (formattingInfo.getMaxLength() * 10) + (c - '0'));
388        } else {
389          i = finalizeConverter(
390              c, pattern, i, currentLiteral, formattingInfo,
391              converterRegistry, rules, patternConverters, formattingInfos);
392          state = LITERAL_STATE;
393          formattingInfo = FormattingInfo.getDefault();
394          currentLiteral.setLength(0);
395        }
396
397        break;
398      } // switch
399    }
400
401    // while
402    if (currentLiteral.length() != 0) {
403      patternConverters.add(
404        new LiteralPatternConverter(currentLiteral.toString()));
405      formattingInfos.add(FormattingInfo.getDefault());
406    }
407  }
408
409  /**
410   * Creates a new PatternConverter.
411   *
412   *
413   * @param converterId converterId.
414   * @param currentLiteral literal to be used if converter is unrecognized or following converter
415   *    if converterId contains extra characters.
416   * @param converterRegistry map of user-supported pattern converters keyed by format specifier, may be null.
417   * @param rules map of stock pattern converters keyed by format specifier.
418   * @param options converter options.
419   * @return  converter or null.
420   */
421  private static PatternConverter createConverter(
422    final String converterId, final StringBuffer currentLiteral,
423    final Map converterRegistry, final Map rules, final List options) {
424    String converterName = converterId;
425    Object converterObj = null;
426
427    for (int i = converterId.length(); (i > 0) && (converterObj == null);
428        i--) {
429      converterName = converterName.substring(0, i);
430
431      if (converterRegistry != null) {
432        converterObj = converterRegistry.get(converterName);
433      }
434
435      if ((converterObj == null) && (rules != null)) {
436        converterObj = rules.get(converterName);
437      }
438    }
439
440    if (converterObj == null) {
441        LogLog.error("Unrecognized format specifier [" + converterId + "]");
442
443      return null;
444    }
445
446    Class converterClass = null;
447
448    if (converterObj instanceof Class) {
449      converterClass = (Class) converterObj;
450    } else {
451      if (converterObj instanceof String) {
452        try {
453          converterClass = Loader.loadClass((String) converterObj);
454        } catch (ClassNotFoundException ex) {
455            LogLog.warn(
456              "Class for conversion pattern %" + converterName + " not found",
457              ex);
458
459          return null;
460        }
461      } else {
462          LogLog.warn(
463            "Bad map entry for conversion pattern %" +  converterName + ".");
464
465        return null;
466      }
467    }
468
469    try {
470      Method factory =
471        converterClass.getMethod(
472          "newInstance",
473          new Class[] {
474            Class.forName("[Ljava.lang.String;")
475          });
476      String[] optionsArray = new String[options.size()];
477      optionsArray = (String[]) options.toArray(optionsArray);
478
479      Object newObj =
480        factory.invoke(null, new Object[] { optionsArray });
481
482      if (newObj instanceof PatternConverter) {
483        currentLiteral.delete(
484          0,
485          currentLiteral.length()
486          - (converterId.length() - converterName.length()));
487
488        return (PatternConverter) newObj;
489      } else {
490          LogLog.warn(
491            "Class " + converterClass.getName()
492            + " does not extend PatternConverter.");
493      }
494    } catch (Exception ex) {
495        LogLog.error("Error creating converter for " + converterId, ex);
496
497      try {
498        //
499        //  try default constructor
500        PatternConverter pc = (PatternConverter) converterClass.newInstance();
501        currentLiteral.delete(
502          0,
503          currentLiteral.length()
504          - (converterId.length() - converterName.length()));
505
506        return pc;
507      } catch (Exception ex2) {
508          LogLog.error("Error creating converter for " + converterId, ex2);
509      }
510    }
511
512    return null;
513  }
514
515  /**
516   * Processes a format specifier sequence.
517   *
518   * @param c initial character of format specifier.
519   * @param pattern conversion pattern
520   * @param i current position in conversion pattern.
521   * @param currentLiteral current literal.
522   * @param formattingInfo current field specifier.
523   * @param converterRegistry map of user-provided pattern converters keyed by format specifier, may be null.
524   * @param rules map of stock pattern converters keyed by format specifier.
525   * @param patternConverters list to receive parsed pattern converter.
526   * @param formattingInfos list to receive corresponding field specifier.
527   * @return position after format specifier sequence.
528   */
529  private static int finalizeConverter(
530    char c, String pattern, int i,
531    final StringBuffer currentLiteral, final FormattingInfo formattingInfo,
532    final Map converterRegistry, final Map rules, final List patternConverters,
533    final List formattingInfos) {
534    StringBuffer convBuf = new StringBuffer();
535    i = extractConverter(c, pattern, i, convBuf, currentLiteral);
536
537    String converterId = convBuf.toString();
538
539    List options = new ArrayList();
540    i = extractOptions(pattern, i, options);
541
542    PatternConverter pc =
543      createConverter(
544        converterId, currentLiteral, converterRegistry, rules, options);
545
546    if (pc == null) {
547      StringBuffer msg;
548
549      if ((converterId == null) || (converterId.length() == 0)) {
550        msg =
551          new StringBuffer("Empty conversion specifier starting at position ");
552      } else {
553        msg = new StringBuffer("Unrecognized conversion specifier [");
554        msg.append(converterId);
555        msg.append("] starting at position ");
556      }
557
558      msg.append(Integer.toString(i));
559      msg.append(" in conversion pattern.");
560
561        LogLog.error(msg.toString());
562
563      patternConverters.add(
564        new LiteralPatternConverter(currentLiteral.toString()));
565      formattingInfos.add(FormattingInfo.getDefault());
566    } else {
567      patternConverters.add(pc);
568      formattingInfos.add(formattingInfo);
569
570      if (currentLiteral.length() > 0) {
571        patternConverters.add(
572          new LiteralPatternConverter(currentLiteral.toString()));
573        formattingInfos.add(FormattingInfo.getDefault());
574      }
575    }
576
577    currentLiteral.setLength(0);
578
579    return i;
580  }
581
582  /**
583   * The class wraps another Map but throws exceptions on any attempt to modify the map.
584   */
585  private static class ReadOnlyMap implements Map {
586    /**
587     * Wrapped map.
588     */
589    private final Map map;
590
591    /**
592     * Constructor
593     * @param src source map.
594     */
595    public ReadOnlyMap(Map src) {
596      map = src;
597    }
598
599    /**
600     * {@inheritDoc}
601     */
602    public void clear() {
603      throw new UnsupportedOperationException();
604    }
605
606    /**
607     * {@inheritDoc}
608     */
609    public boolean containsKey(Object key) {
610      return map.containsKey(key);
611    }
612
613    /**
614     * {@inheritDoc}
615     */
616    public boolean containsValue(Object value) {
617      return map.containsValue(value);
618    }
619
620    /**
621     * {@inheritDoc}
622     */
623    public Set entrySet() {
624      return map.entrySet();
625    }
626
627    /**
628     * {@inheritDoc}
629     */
630    public Object get(Object key) {
631      return map.get(key);
632    }
633
634    /**
635     * {@inheritDoc}
636     */
637    public boolean isEmpty() {
638      return map.isEmpty();
639    }
640
641    /**
642     * {@inheritDoc}
643     */
644    public Set keySet() {
645      return map.keySet();
646    }
647
648    /**
649     * {@inheritDoc}
650     */
651    public Object put(Object key, Object value) {
652      throw new UnsupportedOperationException();
653    }
654
655    /**
656     * {@inheritDoc}
657     */
658    public void putAll(Map t) {
659      throw new UnsupportedOperationException();
660    }
661
662    /**
663     * {@inheritDoc}
664     */
665    public Object remove(Object key) {
666      throw new UnsupportedOperationException();
667    }
668
669    /**
670     * {@inheritDoc}
671     */
672    public int size() {
673      return map.size();
674    }
675
676    /**
677     * {@inheritDoc}
678     */
679    public Collection values() {
680      return map.values();
681    }
682  }
683}