/*
 * 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 java.util.logging;

import java.security.AccessController;
import java.security.PrivilegedAction;
import java.text.MessageFormat;
import java.util.Date;
import java.util.ResourceBundle;

/**
 * Formatter to convert a {@link LogRecord} into an XML string. The DTD
 * specified in Appendix A to the Java Logging APIs specification is used.
 * {@code XMLFormatter} uses the output handler's encoding if it is specified,
 * otherwise the default platform encoding is used instead. UTF-8 is the
 * recommended encoding.
 */
public class XMLFormatter extends Formatter {

    private static final String lineSeperator = LogManager
            .getSystemLineSeparator();

    private static final String indent = "    ";

    /**
     * Constructs a new {@code XMLFormatter}.
     */
    public XMLFormatter() {
        super();
    }

    /**
     * Converts a {@code LogRecord} into an XML string.
     *
     * @param r
     *            the log record to be formatted.
     * @return the log record formatted as an XML string.
     */
    @SuppressWarnings("nls")
    @Override
    public String format(LogRecord r) {
        // call a method of LogRecord to ensure not null
        long time = r.getMillis();
        // format to date
        String date = MessageFormat.format("{0, date} {0, time}",
                new Object[] { new Date(time) });

        StringBuilder sb = new StringBuilder();
        sb.append(("<record>")).append(lineSeperator);
        sb.append(indent).append(("<date>")).append(date).append(("</date>"))
                .append(lineSeperator);
        sb.append(indent).append(("<millis>")).append(time).append(
                ("</millis>")).append(lineSeperator);
        sb.append(indent).append(("<sequence>")).append(r.getSequenceNumber())
                .append(("</sequence>")).append(lineSeperator);
        if (null != r.getLoggerName()) {
            sb.append(indent).append(("<logger>")).append(r.getLoggerName())
                    .append(("</logger>")).append(lineSeperator);
        }
        sb.append(indent).append(("<level>")).append(r.getLevel().getName())
                .append(("</level>")).append(lineSeperator);
        if (null != r.getSourceClassName()) {
            sb.append(indent).append(("<class>"))
                    .append(r.getSourceClassName()).append(("</class>"))
                    .append(lineSeperator);
        }
        if (null != r.getSourceMethodName()) {
            sb.append(indent).append(("<method>")).append(
                    r.getSourceMethodName()).append(("</method>")).append(
                    lineSeperator);
        }
        sb.append(indent).append(("<thread>")).append(r.getThreadID()).append(
                ("</thread>")).append(lineSeperator);
        formatMessages(r, sb);
        Object[] params;
        if ((params = r.getParameters()) != null) {
            for (Object element : params) {
                sb.append(indent).append(("<param>")).append(element).append(
                        ("</param>")).append(lineSeperator);
            }
        }
        formatThrowable(r, sb);
        sb.append(("</record>")).append(lineSeperator);
        return sb.toString();
    }

    @SuppressWarnings("nls")
    private void formatMessages(LogRecord r, StringBuilder sb) {
        // get localized message if has, but don't call Formatter.formatMessage
        // to parse pattern string
        ResourceBundle rb = r.getResourceBundle();
        String pattern = r.getMessage();
        if (null != rb && null != pattern) {
            String message;
            try {
                message = rb.getString(pattern);
            } catch (Exception e) {
                message = null;
            }

            if (message == null) {
                message = pattern;
                sb.append(indent).append(("<message>")).append(message).append(
                        ("</message>")).append(lineSeperator);
            } else {
                sb.append(indent).append(("<message>")).append(message).append(
                        ("</message>")).append(lineSeperator);
                sb.append(indent).append(("<key>")).append(pattern).append(
                        ("</key>")).append(lineSeperator);
                sb.append(indent).append(("<catalog>")).append(
                        r.getResourceBundleName()).append(("</catalog>"))
                        .append(lineSeperator);
            }
        } else if (null != pattern) {
            sb.append(indent).append(("<message>")).append(pattern).append(
                    ("</message>")).append(lineSeperator);
        } else {
            sb.append(indent).append(("<message/>"));
        }
    }

    @SuppressWarnings("nls")
    private void formatThrowable(LogRecord r, StringBuilder sb) {
        Throwable t;
        if ((t = r.getThrown()) != null) {
            sb.append(indent).append("<exception>").append(lineSeperator);
            sb.append(indent).append(indent).append("<message>").append(
                    t.toString()).append("</message>").append(lineSeperator);
            // format throwable's stack trace
            StackTraceElement[] elements = t.getStackTrace();
            for (StackTraceElement e : elements) {
                sb.append(indent).append(indent).append("<frame>").append(
                        lineSeperator);
                sb.append(indent).append(indent).append(indent).append(
                        "<class>").append(e.getClassName()).append("</class>")
                        .append(lineSeperator);
                sb.append(indent).append(indent).append(indent).append(
                        "<method>").append(e.getMethodName()).append(
                        "</method>").append(lineSeperator);
                sb.append(indent).append(indent).append(indent)
                        .append("<line>").append(e.getLineNumber()).append(
                                "</line>").append(lineSeperator);
                sb.append(indent).append(indent).append("</frame>").append(
                        lineSeperator);
            }
            sb.append(indent).append("</exception>").append(lineSeperator);
        }
    }

    /**
     * Returns the header string for a set of log records formatted as XML
     * strings, using the output handler's encoding if it is defined, otherwise
     * using the default platform encoding.
     *
     * @param h
     *            the output handler, may be {@code null}.
     * @return the header string for log records formatted as XML strings.
     */
    @SuppressWarnings("nls")
    @Override
    public String getHead(Handler h) {
        String encoding = null;
        if (null != h) {
            encoding = h.getEncoding();
        }
        if (null == encoding) {
            encoding = getSystemProperty("file.encoding");
        }
        StringBuilder sb = new StringBuilder();
        sb.append("<?xml version=\"1.0\" encoding=\"").append(encoding).append(
                "\" standalone=\"no\"?>").append(lineSeperator);
        sb.append("<!DOCTYPE log SYSTEM \"logger.dtd\">").append(lineSeperator);
        sb.append(("<log>"));
        return sb.toString();
    }

    /**
     * Returns the tail string for a set of log records formatted as XML
     * strings.
     *
     * @param h
     *            the output handler, may be {@code null}.
     * @return the tail string for log records formatted as XML strings.
     */
    @Override
    public String getTail(Handler h) {
        return "</log>";
    }

    // use privilege code to get system property
    private static String getSystemProperty(final String key) {
        return AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                return System.getProperty(key);
            }
        });
    }
}
