+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.log4j.pattern;
+
+import org.apache.log4j.helpers.Loader;
+import org.apache.log4j.helpers.LogLog;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+// Contributors: Nelson Minar <(nelson@monkey.org>
+// Igor E. Poteryaev <jah@mail.ru>
+// Reinhard Deschler <reinhard.deschler@web.de>
+
+/**
+ * Most of the work of the {@link org.apache.log4j.EnhancedPatternLayout} class
+ * is delegated to the PatternParser class.
+ * <p>It is this class that parses conversion patterns and creates
+ * a chained list of {@link PatternConverter PatternConverters}.
+ *
+ * @author James P. Cakalic
+ * @author Ceki Gülcü
+ * @author Anders Kristensen
+ * @author Paul Smith
+ * @author Curt Arnold
+ *
+*/
+public final class PatternParser {
+ /**
+ * Escape character for format specifier.
+ */
+ private static final char ESCAPE_CHAR = '%';
+
+ /**
+ * Literal state.
+ */
+ private static final int LITERAL_STATE = 0;
+
+ /**
+ * In converter name state.
+ */
+ private static final int CONVERTER_STATE = 1;
+
+ /**
+ * Dot state.
+ */
+ private static final int DOT_STATE = 3;
+
+ /**
+ * Min state.
+ */
+ private static final int MIN_STATE = 4;
+
+ /**
+ * Max state.
+ */
+ private static final int MAX_STATE = 5;
+
+ /**
+ * Standard format specifiers for EnhancedPatternLayout.
+ */
+ private static final Map PATTERN_LAYOUT_RULES;
+
+ /**
+ * Standard format specifiers for rolling file appenders.
+ */
+ private static final Map FILENAME_PATTERN_RULES;
+
+ static {
+ // We set the global rules in the static initializer of PatternParser class
+ Map rules = new HashMap(17);
+ rules.put("c", LoggerPatternConverter.class);
+ rules.put("logger", LoggerPatternConverter.class);
+
+ rules.put("C", ClassNamePatternConverter.class);
+ rules.put("class", ClassNamePatternConverter.class);
+
+ rules.put("d", DatePatternConverter.class);
+ rules.put("date", DatePatternConverter.class);
+
+ rules.put("F", FileLocationPatternConverter.class);
+ rules.put("file", FileLocationPatternConverter.class);
+
+ rules.put("l", FullLocationPatternConverter.class);
+
+ rules.put("L", LineLocationPatternConverter.class);
+ rules.put("line", LineLocationPatternConverter.class);
+
+ rules.put("m", MessagePatternConverter.class);
+ rules.put("message", MessagePatternConverter.class);
+
+ rules.put("n", LineSeparatorPatternConverter.class);
+
+ rules.put("M", MethodLocationPatternConverter.class);
+ rules.put("method", MethodLocationPatternConverter.class);
+
+ rules.put("p", LevelPatternConverter.class);
+ rules.put("level", LevelPatternConverter.class);
+
+ rules.put("r", RelativeTimePatternConverter.class);
+ rules.put("relative", RelativeTimePatternConverter.class);
+
+ rules.put("t", ThreadPatternConverter.class);
+ rules.put("thread", ThreadPatternConverter.class);
+
+ rules.put("x", NDCPatternConverter.class);
+ rules.put("ndc", NDCPatternConverter.class);
+
+ rules.put("X", PropertiesPatternConverter.class);
+ rules.put("properties", PropertiesPatternConverter.class);
+
+ rules.put("sn", SequenceNumberPatternConverter.class);
+ rules.put("sequenceNumber", SequenceNumberPatternConverter.class);
+
+ rules.put("throwable", ThrowableInformationPatternConverter.class);
+ PATTERN_LAYOUT_RULES = new ReadOnlyMap(rules);
+
+ Map fnameRules = new HashMap(4);
+ fnameRules.put("d", FileDatePatternConverter.class);
+ fnameRules.put("date", FileDatePatternConverter.class);
+ fnameRules.put("i", IntegerPatternConverter.class);
+ fnameRules.put("index", IntegerPatternConverter.class);
+
+ FILENAME_PATTERN_RULES = new ReadOnlyMap(fnameRules);
+ }
+
+ /**
+ * Private constructor.
+ */
+ private PatternParser() {
+ }
+
+ /**
+ * Get standard format specifiers for EnhancedPatternLayout.
+ * @return read-only map of format converter classes keyed by format specifier strings.
+ */
+ public static Map getPatternLayoutRules() {
+ return PATTERN_LAYOUT_RULES;
+ }
+
+ /**
+ * Get standard format specifiers for rolling file appender file specification.
+ * @return read-only map of format converter classes keyed by format specifier strings.
+ */
+ public static Map getFileNamePatternRules() {
+ return FILENAME_PATTERN_RULES;
+ }
+
+ /** Extract the converter identifier found at position i.
+ *
+ * After this function returns, the variable i will point to the
+ * first char after the end of the converter identifier.
+ *
+ * If i points to a char which is not a character acceptable at the
+ * start of a unicode identifier, the value null is returned.
+ *
+ * @param lastChar last processed character.
+ * @param pattern format string.
+ * @param i current index into pattern format.
+ * @param convBuf buffer to receive conversion specifier.
+ * @param currentLiteral literal to be output in case format specifier in unrecognized.
+ * @return position in pattern after converter.
+ */
+ private static int extractConverter(
+ char lastChar, final String pattern, int i, final StringBuffer convBuf,
+ final StringBuffer currentLiteral) {
+ convBuf.setLength(0);
+
+ // When this method is called, lastChar points to the first character of the
+ // conversion word. For example:
+ // For "%hello" lastChar = 'h'
+ // For "%-5hello" lastChar = 'h'
+ //System.out.println("lastchar is "+lastChar);
+ if (!Character.isUnicodeIdentifierStart(lastChar)) {
+ return i;
+ }
+
+ convBuf.append(lastChar);
+
+ while (
+ (i < pattern.length())
+ && Character.isUnicodeIdentifierPart(pattern.charAt(i))) {
+ convBuf.append(pattern.charAt(i));
+ currentLiteral.append(pattern.charAt(i));
+
+ //System.out.println("conv buffer is now ["+convBuf+"].");
+ i++;
+ }
+
+ return i;
+ }
+
+ /**
+ * Extract options.
+ * @param pattern conversion pattern.
+ * @param i start of options.
+ * @param options array to receive extracted options
+ * @return position in pattern after options.
+ */
+ private static int extractOptions(String pattern, int i, List options) {
+ while ((i < pattern.length()) && (pattern.charAt(i) == '{')) {
+ int end = pattern.indexOf('}', i);
+
+ if (end == -1) {
+ break;
+ }
+
+ String r = pattern.substring(i + 1, end);
+ options.add(r);
+ i = end + 1;
+ }
+
+ return i;
+ }
+
+ /**
+ * Parse a format specifier.
+ * @param pattern pattern to parse.
+ * @param patternConverters list to receive pattern converters.
+ * @param formattingInfos list to receive field specifiers corresponding to pattern converters.
+ * @param converterRegistry map of user-supported pattern converters keyed by format specifier, may be null.
+ * @param rules map of stock pattern converters keyed by format specifier.
+ */
+ public static void parse(
+ final String pattern, final List patternConverters,
+ final List formattingInfos, final Map converterRegistry, final Map rules) {
+ if (pattern == null) {
+ throw new NullPointerException("pattern");
+ }
+
+ StringBuffer currentLiteral = new StringBuffer(32);
+
+ int patternLength = pattern.length();
+ int state = LITERAL_STATE;
+ char c;
+ int i = 0;
+ FormattingInfo formattingInfo = FormattingInfo.getDefault();
+
+ while (i < patternLength) {
+ c = pattern.charAt(i++);
+
+ switch (state) {
+ case LITERAL_STATE:
+
+ // In literal state, the last char is always a literal.
+ if (i == patternLength) {
+ currentLiteral.append(c);
+
+ continue;
+ }
+
+ if (c == ESCAPE_CHAR) {
+ // peek at the next char.
+ switch (pattern.charAt(i)) {
+ case ESCAPE_CHAR:
+ currentLiteral.append(c);
+ i++; // move pointer
+
+ break;
+
+ default:
+
+ if (currentLiteral.length() != 0) {
+ patternConverters.add(
+ new LiteralPatternConverter(currentLiteral.toString()));
+ formattingInfos.add(FormattingInfo.getDefault());
+ }
+
+ currentLiteral.setLength(0);
+ currentLiteral.append(c); // append %
+ state = CONVERTER_STATE;
+ formattingInfo = FormattingInfo.getDefault();
+ }
+ } else {
+ currentLiteral.append(c);
+ }
+
+ break;
+
+ case CONVERTER_STATE:
+ currentLiteral.append(c);
+
+ switch (c) {
+ case '-':
+ formattingInfo =
+ new FormattingInfo(
+ true,
+ formattingInfo.isRightTruncated(),
+ formattingInfo.getMinLength(),
+ formattingInfo.getMaxLength());
+ break;
+
+ case '!':
+ formattingInfo =
+ new FormattingInfo(
+ formattingInfo.isLeftAligned(),
+ true,
+ formattingInfo.getMinLength(),
+ formattingInfo.getMaxLength());
+ break;
+
+
+ case '.':
+ state = DOT_STATE;
+
+ break;
+
+ default:
+
+ if ((c >= '0') && (c <= '9')) {
+ formattingInfo =
+ new FormattingInfo(
+ formattingInfo.isLeftAligned(),
+ formattingInfo.isRightTruncated(),
+ c - '0',
+ formattingInfo.getMaxLength());
+ state = MIN_STATE;
+ } else {
+ i = finalizeConverter(
+ c, pattern, i, currentLiteral, formattingInfo,
+ converterRegistry, rules, patternConverters, formattingInfos);
+
+ // Next pattern is assumed to be a literal.
+ state = LITERAL_STATE;
+ formattingInfo = FormattingInfo.getDefault();
+ currentLiteral.setLength(0);
+ }
+ } // switch
+
+ break;
+
+ case MIN_STATE:
+ currentLiteral.append(c);
+
+ if ((c >= '0') && (c <= '9')) {
+ formattingInfo =
+ new FormattingInfo(
+ formattingInfo.isLeftAligned(),
+ formattingInfo.isRightTruncated(),
+ (formattingInfo.getMinLength() * 10) + (c - '0'),
+ formattingInfo.getMaxLength());
+ } else if (c == '.') {
+ state = DOT_STATE;
+ } else {
+ i = finalizeConverter(
+ c, pattern, i, currentLiteral, formattingInfo,
+ converterRegistry, rules, patternConverters, formattingInfos);
+ state = LITERAL_STATE;
+ formattingInfo = FormattingInfo.getDefault();
+ currentLiteral.setLength(0);
+ }
+
+ break;
+
+ case DOT_STATE:
+ currentLiteral.append(c);
+
+ if ((c >= '0') && (c <= '9')) {
+ formattingInfo =
+ new FormattingInfo(
+ formattingInfo.isLeftAligned(),
+ formattingInfo.isRightTruncated(),
+ formattingInfo.getMinLength(),
+ c - '0');
+ state = MAX_STATE;
+ } else {
+ LogLog.error(
+ "Error occured in position " + i
+ + ".\n Was expecting digit, instead got char \"" + c + "\".");
+
+ state = LITERAL_STATE;
+ }
+
+ break;
+
+ case MAX_STATE:
+ currentLiteral.append(c);
+
+ if ((c >= '0') && (c <= '9')) {
+ formattingInfo =
+ new FormattingInfo(
+ formattingInfo.isLeftAligned(),
+ formattingInfo.isRightTruncated(),
+ formattingInfo.getMinLength(),
+ (formattingInfo.getMaxLength() * 10) + (c - '0'));
+ } else {
+ i = finalizeConverter(
+ c, pattern, i, currentLiteral, formattingInfo,
+ converterRegistry, rules, patternConverters, formattingInfos);
+ state = LITERAL_STATE;
+ formattingInfo = FormattingInfo.getDefault();
+ currentLiteral.setLength(0);
+ }
+
+ break;
+ } // switch
+ }
+
+ // while
+ if (currentLiteral.length() != 0) {
+ patternConverters.add(
+ new LiteralPatternConverter(currentLiteral.toString()));
+ formattingInfos.add(FormattingInfo.getDefault());
+ }
+ }
+
+ /**
+ * Creates a new PatternConverter.
+ *
+ *
+ * @param converterId converterId.
+ * @param currentLiteral literal to be used if converter is unrecognized or following converter
+ * if converterId contains extra characters.
+ * @param converterRegistry map of user-supported pattern converters keyed by format specifier, may be null.
+ * @param rules map of stock pattern converters keyed by format specifier.
+ * @param options converter options.
+ * @return converter or null.
+ */
+ private static PatternConverter createConverter(
+ final String converterId, final StringBuffer currentLiteral,
+ final Map converterRegistry, final Map rules, final List options) {
+ String converterName = converterId;
+ Object converterObj = null;
+
+ for (int i = converterId.length(); (i > 0) && (converterObj == null);
+ i--) {
+ converterName = converterName.substring(0, i);
+
+ if (converterRegistry != null) {
+ converterObj = converterRegistry.get(converterName);
+ }
+
+ if ((converterObj == null) && (rules != null)) {
+ converterObj = rules.get(converterName);
+ }
+ }
+
+ if (converterObj == null) {
+ LogLog.error("Unrecognized format specifier [" + converterId + "]");
+
+ return null;
+ }
+
+ Class converterClass = null;
+
+ if (converterObj instanceof Class) {
+ converterClass = (Class) converterObj;
+ } else {
+ if (converterObj instanceof String) {
+ try {
+ converterClass = Loader.loadClass((String) converterObj);
+ } catch (ClassNotFoundException ex) {
+ LogLog.warn(
+ "Class for conversion pattern %" + converterName + " not found",
+ ex);
+
+ return null;
+ }
+ } else {
+ LogLog.warn(
+ "Bad map entry for conversion pattern %" + converterName + ".");
+
+ return null;
+ }
+ }
+
+ try {
+ Method factory =
+ converterClass.getMethod(
+ "newInstance",
+ new Class[] {
+ Class.forName("[Ljava.lang.String;")
+ });
+ String[] optionsArray = new String[options.size()];
+ optionsArray = (String[]) options.toArray(optionsArray);
+
+ Object newObj =
+ factory.invoke(null, new Object[] { optionsArray });
+
+ if (newObj instanceof PatternConverter) {
+ currentLiteral.delete(
+ 0,
+ currentLiteral.length()
+ - (converterId.length() - converterName.length()));
+
+ return (PatternConverter) newObj;
+ } else {
+ LogLog.warn(
+ "Class " + converterClass.getName()
+ + " does not extend PatternConverter.");
+ }
+ } catch (Exception ex) {
+ LogLog.error("Error creating converter for " + converterId, ex);
+
+ try {
+ //
+ // try default constructor
+ PatternConverter pc = (PatternConverter) converterClass.newInstance();
+ currentLiteral.delete(
+ 0,
+ currentLiteral.length()
+ - (converterId.length() - converterName.length()));
+
+ return pc;
+ } catch (Exception ex2) {
+ LogLog.error("Error creating converter for " + converterId, ex2);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Processes a format specifier sequence.
+ *
+ * @param c initial character of format specifier.
+ * @param pattern conversion pattern
+ * @param i current position in conversion pattern.
+ * @param currentLiteral current literal.
+ * @param formattingInfo current field specifier.
+ * @param converterRegistry map of user-provided pattern converters keyed by format specifier, may be null.
+ * @param rules map of stock pattern converters keyed by format specifier.
+ * @param patternConverters list to receive parsed pattern converter.
+ * @param formattingInfos list to receive corresponding field specifier.
+ * @return position after format specifier sequence.
+ */
+ private static int finalizeConverter(
+ char c, String pattern, int i,
+ final StringBuffer currentLiteral, final FormattingInfo formattingInfo,
+ final Map converterRegistry, final Map rules, final List patternConverters,
+ final List formattingInfos) {
+ StringBuffer convBuf = new StringBuffer();
+ i = extractConverter(c, pattern, i, convBuf, currentLiteral);
+
+ String converterId = convBuf.toString();
+
+ List options = new ArrayList();
+ i = extractOptions(pattern, i, options);
+
+ PatternConverter pc =
+ createConverter(
+ converterId, currentLiteral, converterRegistry, rules, options);
+
+ if (pc == null) {
+ StringBuffer msg;
+
+ if ((converterId == null) || (converterId.length() == 0)) {
+ msg =
+ new StringBuffer("Empty conversion specifier starting at position ");
+ } else {
+ msg = new StringBuffer("Unrecognized conversion specifier [");
+ msg.append(converterId);
+ msg.append("] starting at position ");
+ }
+
+ msg.append(Integer.toString(i));
+ msg.append(" in conversion pattern.");
+
+ LogLog.error(msg.toString());
+
+ patternConverters.add(
+ new LiteralPatternConverter(currentLiteral.toString()));
+ formattingInfos.add(FormattingInfo.getDefault());
+ } else {
+ patternConverters.add(pc);
+ formattingInfos.add(formattingInfo);
+
+ if (currentLiteral.length() > 0) {
+ patternConverters.add(
+ new LiteralPatternConverter(currentLiteral.toString()));
+ formattingInfos.add(FormattingInfo.getDefault());
+ }
+ }
+
+ currentLiteral.setLength(0);
+
+ return i;
+ }
+
+ /**
+ * The class wraps another Map but throws exceptions on any attempt to modify the map.
+ */
+ private static class ReadOnlyMap implements Map {
+ /**
+ * Wrapped map.
+ */
+ private final Map map;
+
+ /**
+ * Constructor
+ * @param src source map.
+ */
+ public ReadOnlyMap(Map src) {
+ map = src;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void clear() {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean containsKey(Object key) {
+ return map.containsKey(key);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean containsValue(Object value) {
+ return map.containsValue(value);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Set entrySet() {
+ return map.entrySet();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Object get(Object key) {
+ return map.get(key);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean isEmpty() {
+ return map.isEmpty();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Set keySet() {
+ return map.keySet();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Object put(Object key, Object value) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void putAll(Map t) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Object remove(Object key) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public int size() {
+ return map.size();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Collection values() {
+ return map.values();
+ }
+ }
+}