/*
 * 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.spi;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Locale;

import org.apache.log4j.rule.InFixToPostFix;


/**
 * A singleton helper utility which accepts a field name
 * and a LoggingEvent and returns the value of that field.
 *
 * This class defines a grammar used in creation of an expression-based Rule.
 *
 * The only available method is
 * Object getField(String fieldName, LoggingEvent event).
 *
 * Here is a description of the mapping of field names in the grammar
 * to fields on the logging event.  While the getField method returns an Object,
 * the individual types returned per field are described here:
 *
 * Field Name    Field value (String representation        Return type
 * LOGGER        category name (logger)                    String
 * LEVEL         level                                     Level
 * CLASS         locationInformation's class name          String
 * FILE          locationInformation's file name           String
 * LINE          locationInformation's line number         String
 * METHOD        locationInformation's method name         String
 * MSG           message                                   Object
 * NDC           NDC                                       String
 * EXCEPTION     throwable string representation           ThrowableInformation
 * TIMESTAMP     timestamp                                 Long
 * THREAD        thread                                    String
 * PROP.keyName  entry in the Property hashtable           String
 *               mapped to the key [keyName]

 * NOTE:  the values for the 'keyName' portion of the MDC and PROP mappings must
 * be an exact match to the key in the hashTable (case sensitive).
 *
 * If the passed-in field is null or doesn't match an entry
 * in the above-described mapping, an exception is thrown.
 *
 * @author Scott Deboy (sdeboy@apache.org)
 * @author Paul Smith (psmith@apache.org)
 *
 */
public final class LoggingEventFieldResolver {
    /**
     * Keyword list.
     */
  public static final List KEYWORD_LIST = new ArrayList();
    /**
     * LOGGER string literal.
     */
  public static final String LOGGER_FIELD = "LOGGER";
    /**
     * LEVEL string literal.
     */
  public static final String LEVEL_FIELD = "LEVEL";
    /**
     * CLASS string literal.
     */
  public static final String CLASS_FIELD = "CLASS";
    /**
     * FILE string literal.
     */
  public static final String FILE_FIELD = "FILE";
    /**
     * LINE string literal.
     */
  public static final String LINE_FIELD = "LINE";
    /**
     * METHOD string literal.
     */
  public static final String METHOD_FIELD = "METHOD";
    /**
     * MSG string literal.
     */
  public static final String MSG_FIELD = "MSG";
    /**
     * NDC string literal.
     */
  public static final String NDC_FIELD = "NDC";
    /**
     * EXCEPTION string literal.
     */
  public static final String EXCEPTION_FIELD = "EXCEPTION";
    /**
     * TIMESTAMP string literal.
     */
  public static final String TIMESTAMP_FIELD = "TIMESTAMP";
    /**
     * THREAD string literal.
     */
  public static final String THREAD_FIELD = "THREAD";
    /**
     * PROP. string literal.
     */
  public static final String PROP_FIELD = "PROP.";
    /**
     * empty string literal.
     */
  public static final String EMPTY_STRING = "";
    /**
     * LOGGER string literal.
     */
  private static final LoggingEventFieldResolver RESOLVER =
    new LoggingEventFieldResolver();

    /**
     * Create new instance.
     */
  private LoggingEventFieldResolver() {
    super();
    KEYWORD_LIST.add(LOGGER_FIELD);
    KEYWORD_LIST.add(LEVEL_FIELD);
    KEYWORD_LIST.add(CLASS_FIELD);
    KEYWORD_LIST.add(FILE_FIELD);
    KEYWORD_LIST.add(LINE_FIELD);
    KEYWORD_LIST.add(METHOD_FIELD);
    KEYWORD_LIST.add(MSG_FIELD);
    KEYWORD_LIST.add(NDC_FIELD);
    KEYWORD_LIST.add(EXCEPTION_FIELD);
    KEYWORD_LIST.add(TIMESTAMP_FIELD);
    KEYWORD_LIST.add(THREAD_FIELD);
    KEYWORD_LIST.add(PROP_FIELD);
  }

    /**
     * Apply fields.
     * @param replaceText replacement text.
     * @param event logging event.
     * @return evaluted expression
     */
  public String applyFields(final String replaceText,
                            final LoggingEvent event) {
      if (replaceText == null) {
        return null;
      }
      InFixToPostFix.CustomTokenizer tokenizer = new InFixToPostFix.CustomTokenizer(replaceText);
      StringBuffer result = new StringBuffer();
      boolean found = false;

      while (tokenizer.hasMoreTokens()) {
          String token = tokenizer.nextToken();
          if (isField(token) || token.toUpperCase(Locale.US).startsWith(PROP_FIELD)) {
              result.append(getValue(token, event).toString());
              found = true;
          } else {
              result.append(token);
          }
      }
      if (found) {
        return result.toString();
      }
      return null;
  }

    /**
     * Get singleton instance.
     * @return singleton instance
     */
  public static LoggingEventFieldResolver getInstance() {
    return RESOLVER;
  }

    /**
     * Determines if specified string is a recognized field.
     * @param fieldName field name
     * @return true if recognized field.
     */
  public boolean isField(final String fieldName) {
    if (fieldName != null) {
        return (KEYWORD_LIST.contains(
                fieldName.toUpperCase(Locale.US))
                || fieldName.toUpperCase().startsWith(PROP_FIELD));
    }
    return false;
  }

    /**
     * Get value of field.
     * @param fieldName field
     * @param event event
     * @return value of field
     */
  public Object getValue(final String fieldName,
                         final LoggingEvent event) {
    String upperField = fieldName.toUpperCase(Locale.US);
    if (LOGGER_FIELD.equals(upperField)) {
      return event.getLoggerName();
    } else if (LEVEL_FIELD.equals(upperField)) {
      return event.getLevel();
    } else if (MSG_FIELD.equals(upperField)) {
      return event.getMessage();
    } else if (NDC_FIELD.equals(upperField)) {
      String ndcValue = event.getNDC();
      return ((ndcValue == null) ? EMPTY_STRING : ndcValue);
    } else if (EXCEPTION_FIELD.equals(upperField)) {
        String[] throwableRep = event.getThrowableStrRep();
        if (throwableRep == null) {
            return EMPTY_STRING;
        } else {
            return getExceptionMessage(throwableRep);
        }
    } else if (TIMESTAMP_FIELD.equals(upperField)) {
      return new Long(event.timeStamp);
    } else if (THREAD_FIELD.equals(upperField)) {
      return event.getThreadName();
    } else if (upperField.startsWith(PROP_FIELD)) {
      //note: need to use actual fieldname since case matters
      Object propValue = event.getMDC(fieldName.substring(5));
      if (propValue == null) {
          //case-specific match didn't work, try case insensitive match
          String lowerPropKey = fieldName.substring(5).toLowerCase();
          Set entrySet = event.getProperties().entrySet();
          for (Iterator iter = entrySet.iterator();iter.hasNext();) {
              Map.Entry thisEntry = (Map.Entry) iter.next();
              if (thisEntry.getKey().toString().equalsIgnoreCase(lowerPropKey)) {
                  propValue = thisEntry.getValue();
              }
          }
      }
      return ((propValue == null) ? EMPTY_STRING : propValue.toString());
    } else {
        LocationInfo info = event.getLocationInformation();
        if (CLASS_FIELD.equals(upperField)) {
            return ((info == null) ? EMPTY_STRING : info.getClassName());
        } else if (FILE_FIELD.equals(upperField)) {
            return ((info == null) ? EMPTY_STRING : info.getFileName());
        } else if (LINE_FIELD.equals(upperField)) {
            return ((info == null) ? EMPTY_STRING : info.getLineNumber());
        } else if (METHOD_FIELD.equals(upperField)) {
            return ((info == null) ? EMPTY_STRING : info.getMethodName());
        }
    }

    //there wasn't a match, so throw a runtime exception
    throw new IllegalArgumentException("Unsupported field name: " + fieldName);
  }

    /**
     * Get message from throwable representation.
     * @param exception exception
     * @return message
     */
    private static String getExceptionMessage(final String[] exception) {
        StringBuffer buff = new StringBuffer();
        for (int i = 0; i < exception.length; i++) {
            buff.append(exception[i]);
        }
        return buff.toString();
    }
}
