/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License. You can obtain
 * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
 * or glassfish/bootstrap/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
 * Sun designates this particular file as subject to the "Classpath" exception
 * as provided by Sun in the GPL Version 2 section of the License file that
 * accompanied this code.  If applicable, add the following below the License
 * Header, with the fields enclosed by brackets [] replaced by your own
 * identifying information: "Portions Copyrighted [year]
 * [name of copyright owner]"
 *
 * Contributor(s):
 *
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */
package com.sun.enterprise.server.logging;

import java.io.PrintWriter;
import java.io.StringWriter;

import java.text.FieldPosition;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.StringTokenizer;
import java.util.logging.ErrorManager;
import java.util.logging.Formatter;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.LogRecord;
import java.util.logging.Logger;


/**
 * CustomFormat provides the flexibility to choose the log record format
 * in the log
 * The specified format is
 * "time-stamp,record-level,product-id,logger-name,thread-info,name-value-pairs,
 * raw-message"
 */
public class CustomLogFormatter extends Formatter {
    private static String PRODUCTID_CONTEXTID = null;

    private static final String PRODUCT_VERSION = com.sun.appserv.server.util.Version.getAbbreviatedVersion();
    private static final int FINE_LEVEL_INT_VALUE = Level.FINE.intValue();
    private static boolean LOG_SOURCE_IN_KEY_VALUE = false;
    private static boolean RECORD_NUMBER_IN_KEY_VALUE = false;

    static {
        String logSource = System.getProperty(
                "com.sun.aas.logging.keyvalue.logsource");

        if ((logSource != null) && (logSource.equals("true"))) {
            LOG_SOURCE_IN_KEY_VALUE = true;
        }

        String recordCount = System.getProperty(
                "com.sun.aas.logging.keyvalue.recordnumber");

        if ((recordCount != null) && (recordCount.equals("true"))) {
            RECORD_NUMBER_IN_KEY_VALUE = true;
        }
    }

    private static final String LINE_SEPARATOR = (String) java.security.AccessController.doPrivileged(new sun.security.action.GetPropertyAction(
                "line.separator"));
    private static final String RECORD_BEGIN_MARKER = "[#|";
    private static final String RECORD_END_MARKER = "|#]" + LINE_SEPARATOR +
        LINE_SEPARATOR;
    private static final char FIELD_SEPARATOR = '|';
    private static final char NVPAIR_SEPARATOR = ';';
    private static final char NV_SEPARATOR = '=';
    private static final String RFC_3339_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
    private static final SimpleDateFormat dateFormatter = new SimpleDateFormat(RFC_3339_DATE_FORMAT);
    private static HashMap<String, Integer> formatMap = new HashMap<String, Integer>();

    static {
        formatMap.put("time-stamp", 0);
        formatMap.put("record-level", 1);
        formatMap.put("product-id", 2);
        formatMap.put("logger-name", 3);
        formatMap.put("thread-info", 4);
        formatMap.put("name-value-pairs", 5);
        formatMap.put("raw-message", 6);
    }

    private static ArrayList<Integer> formatList = new ArrayList<Integer>(6);

    static {
        formatList.add(0);
        formatList.add(1);
        formatList.add(2);
        formatList.add(3);
        formatList.add(4);
        formatList.add(5);
        formatList.add(6);
    }

    // loggerResourceBundleTable caches references to all the ResourceBundle
    // and can be searched using the LoggerName as the key 
    private HashMap loggerResourceBundleTable;
    private LogManager logManager;

    // A Dummy Container Date Object is used to format the date
    private Date date = new Date();
    private long recordNumber = 0;

    public CustomLogFormatter() {
        super();
        loggerResourceBundleTable = new HashMap();
        logManager = LogManager.getLogManager();

        // parseFormat();
    }

    static void parseFormat(String format) {
        if ("DEFAULT".equals(format)) {
            return;
        }

        StringTokenizer st = new StringTokenizer(format, ",");
        formatList.clear();

        while (st.hasMoreTokens()) {
            formatList.add(formatMap.get(st.nextToken()));
        }
    }

    public String format(LogRecord record) {
        return customLogFormat(record);
    }

    public String formatMessage(LogRecord record) {
        return customLogFormat(record);
    }

    /**
     * Sun One AppServer SE/EE can override to specify their product version
     */
    protected String getProductId() {
        return PRODUCT_VERSION;
    }

    /**
     * Sun One Appserver SE/EE? can override to specify their product specific
     * key value pairs.
     */
    protected void getNameValuePairs(StringBuilder buf, LogRecord record) {
        Object[] parameters = record.getParameters();

        if ((parameters == null) || (parameters.length == 0)) {
            return;
        }

        try {
            for (Object obj : parameters) {
                if (obj == null) {
                    continue;
                }

                if (obj instanceof Map) {
                    Map map = (Map) obj;

                    for (Object key : map.keySet()) {
                        buf.append(key.toString()).append(NV_SEPARATOR)
                           .append(map.get(key).toString())
                           .append(NVPAIR_SEPARATOR);
                    }
                } else if (obj instanceof java.util.Collection) {
                    for (Object entry : ((Collection) obj)) {
                        buf.append(entry.toString()).append(NVPAIR_SEPARATOR);
                    }
                } else {
                    buf.append(obj.toString()).append(NVPAIR_SEPARATOR);
                }
            }
        } catch (Exception e) {
            new ErrorManager().error("Error in extracting Name Value Pairs", e,
                ErrorManager.FORMAT_FAILURE);
        }
    }

    /**
     *  Note: This method is not synchronized, we are assuming that the
     *  synchronization will happen at the Log Handler.publish( ) method.
     */
    private String customLogFormat(LogRecord record) {
        try {
            String logMessage = record.getMessage();
            int msgLength = 150; // typical length of log record with out msg

            if (logMessage != null) {
                msgLength += logMessage.length();
            }

            StringBuilder recordBuffer = new StringBuilder(msgLength);
            recordBuffer.append(RECORD_BEGIN_MARKER);

            for (Integer i : formatList) {
                switch (i) {
                case 0:
                    date.setTime(record.getMillis());
                    recordBuffer.append(dateFormatter.format(date));
                    recordBuffer.append(FIELD_SEPARATOR);

                    break;

                case 1:
                    recordBuffer.append(record.getLevel())
                                .append(FIELD_SEPARATOR);

                    break;

                case 2:
                    recordBuffer.append(getProductId()).append(FIELD_SEPARATOR);

                    break;

                case 3:
                    recordBuffer.append(record.getLoggerName())
                                .append(FIELD_SEPARATOR);

                    break;

                case 4:
                    recordBuffer.append("_ThreadID").append(NV_SEPARATOR);
                    recordBuffer.append(record.getThreadID())
                                .append(NVPAIR_SEPARATOR);

                    recordBuffer.append("_ThreadName").append(NV_SEPARATOR);
                    recordBuffer.append(Thread.currentThread().getName());
                    recordBuffer.append(NVPAIR_SEPARATOR);

                    // See 6316018. ClassName and MethodName information should be
                    // included for FINER and FINEST log levels.
                    Level level = record.getLevel();

                    if (LOG_SOURCE_IN_KEY_VALUE ||
                            (level.intValue() <= Level.FINE.intValue())) {
                        recordBuffer.append("ClassName").append(NV_SEPARATOR);
                        recordBuffer.append(record.getSourceClassName());
                        recordBuffer.append(NVPAIR_SEPARATOR);
                        recordBuffer.append("MethodName").append(NV_SEPARATOR);
                        recordBuffer.append(record.getSourceMethodName());
                        recordBuffer.append(NVPAIR_SEPARATOR);
                    }

                    if (RECORD_NUMBER_IN_KEY_VALUE) {
                        recordBuffer.append("RecordNumber").append(NV_SEPARATOR);
                        recordBuffer.append(recordNumber++)
                                    .append(NVPAIR_SEPARATOR);
                    }

                    break;

                case 5:
                    getNameValuePairs(recordBuffer, record);
                    recordBuffer.append(FIELD_SEPARATOR);

                    break;

                case 6:

                    if (logMessage == null) {
                        logMessage = "The log message is null.";
                    }

                    if (logMessage.indexOf("{0}") >= 0) {
                        // If we find {0} or {1} etc., in the message, then it's most
                        // likely finer level messages for Method Entry, Exit etc.,
                        logMessage = java.text.MessageFormat.format(logMessage,
                                record.getParameters());
                    } else {
                        ResourceBundle rb = getResourceBundle(record.getLoggerName());

                        if (rb != null) {
                            try {
                                logMessage = MessageFormat.format(rb.getString(
                                            logMessage), record.getParameters());
                            } catch (java.util.MissingResourceException e) {
                                // If we don't find an entry, then we are covered 
                                // because the logMessage is intialized already
                            }
                        }
                    }

                    recordBuffer.append(logMessage);

                    if (record.getThrown() != null) {
                        recordBuffer.append(LINE_SEPARATOR);

                        StringWriter sw = new StringWriter();
                        PrintWriter pw = new PrintWriter(sw);
                        record.getThrown().printStackTrace(pw);
                        pw.close();
                        recordBuffer.append(sw.toString());
                    }

                    break;
                }
            }

            recordBuffer.append(RECORD_END_MARKER);

            return recordBuffer.toString();
        } catch (Exception ex) {
            new ErrorManager().error("Error in formatting Logrecord", ex,
                ErrorManager.FORMAT_FAILURE);

            // We've already notified the exception, the following
            // return is to keep javac happy
            return new String("");
        }
    }

    private synchronized ResourceBundle getResourceBundle(String loggerName) {
        if (loggerName == null) {
            return null;
        }

        ResourceBundle rb = (ResourceBundle) loggerResourceBundleTable.get(loggerName);

        if (rb == null) {
            rb = logManager.getLogger(loggerName).getResourceBundle();
            loggerResourceBundleTable.put(loggerName, rb);
        }

        return rb;
    }
}
