/*
 * Copyright (c) 2001, 2013, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package sun.rmi.runtime;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.OutputStream;
import java.rmi.server.LogStream;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Formatter;
import java.util.logging.SimpleFormatter;
import java.util.logging.StreamHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.LogManager;
import java.util.logging.LogRecord;
import java.util.logging.StreamHandler;
import java.util.Map;
import java.util.HashMap;

/**
 * Utility which provides an abstract "logger" like RMI internal API
 * which can be directed to use one of two types of logging
 * infrastructure: the java.util.logging API or the
 * java.rmi.server.LogStream API.  The default behavior is to use the
 * java.util.logging API.  The LogStream API may be used instead by
 * setting the system property sun.rmi.log.useOld to true.
 *
 * For backwards compatibility, supports the RMI system logging
 * properties which pre-1.4 comprised the only way to configure RMI
 * logging.  If the java.util.logging API is used and RMI system log
 * properties are set, the system properties override initial RMI
 * logger values as appropriate. If the java.util.logging API is
 * turned off, pre-1.4 logging behavior is used.
 *
 * @author Laird Dornin
 * @since 1.4
 */
@SuppressWarnings("deprecation")
public abstract class Log {

    /** Logger re-definition of old RMI log values */
    public static final Level BRIEF = Level.FINE;
    public static final Level VERBOSE = Level.FINER;

    /* selects log implementation */
    private static final LogFactory logFactory;
    static {
        boolean useOld =
            Boolean.valueOf(java.security.AccessController.
                doPrivileged(new sun.security.action.GetPropertyAction(
                    "sun.rmi.log.useOld"))).booleanValue();

        /* set factory to select the logging facility to use */
        logFactory = (useOld ? (LogFactory) new LogStreamLogFactory() :
                      (LogFactory) new LoggerLogFactory());
    }

    /** "logger like" API to be used by RMI implementation */
    public abstract boolean isLoggable(Level level);
    public abstract void log(Level level, String message);
    public abstract void log(Level level, String message, Throwable thrown);

    /** get and set the RMI server call output stream */
    public abstract void setOutputStream(OutputStream stream);
    public abstract PrintStream getPrintStream();

    /** factory interface enables Logger and LogStream implementations */
    private static interface LogFactory {
        Log createLog(String loggerName, String oldLogName, Level level);
    }

    /* access log objects */

    /**
     * Access log for a tri-state system property.
     *
     * Need to first convert override value to a log level, taking
     * care to interpret a range of values between BRIEF, VERBOSE and
     * SILENT.
     *
     * An override < 0 is interpreted to mean that the logging
     * configuration should not be overridden. The level passed to the
     * factories createLog method will be null in this case.
     *
     * Note that if oldLogName is null and old logging is on, the
     * returned LogStreamLog will ignore the override parameter - the
     * log will never log messages.  This permits new logs that only
     * write to Loggers to do nothing when old logging is active.
     *
     * Do not call getLog multiple times on the same logger name.
     * Since this is an internal API, no checks are made to ensure
     * that multiple logs do not exist for the same logger.
     */
    public static Log getLog(String loggerName, String oldLogName,
                             int override)
    {
        Level level;

        if (override < 0) {
            level = null;
        } else if (override == LogStream.SILENT) {
            level = Level.OFF;
        } else if ((override > LogStream.SILENT) &&
                   (override <= LogStream.BRIEF)) {
            level = BRIEF;
        } else if ((override > LogStream.BRIEF) &&
                   (override <= LogStream.VERBOSE))
        {
            level = VERBOSE;
        } else {
            level = Level.FINEST;
        }
        return logFactory.createLog(loggerName, oldLogName, level);
    }

    /**
     * Access logs associated with boolean properties
     *
     * Do not call getLog multiple times on the same logger name.
     * Since this is an internal API, no checks are made to ensure
     * that multiple logs do not exist for the same logger.
     */
    public static Log getLog(String loggerName, String oldLogName,
                             boolean override)
    {
        Level level = (override ? VERBOSE : null);
        return logFactory.createLog(loggerName, oldLogName, level);
    }

    /**
     * Factory to create Log objects which deliver log messages to the
     * java.util.logging API.
     */
    private static class LoggerLogFactory implements LogFactory {
        LoggerLogFactory() {}

        /*
         * Accessor to obtain an arbitrary RMI logger with name
         * loggerName.  If the level of the logger is greater than the
         * level for the system property with name, the logger level
         * will be set to the value of system property.
         */
        public Log createLog(final String loggerName, String oldLogName,
                             final Level level)
        {
            Logger logger = Logger.getLogger(loggerName);
            return new LoggerLog(logger, level);
        }
    }

    /**
     * Class specialized to log messages to the java.util.logging API
     */
    private static class LoggerLog extends Log {

        /* alternate console handler for RMI loggers */
        private static final Handler alternateConsole =
                java.security.AccessController.doPrivileged(
                new java.security.PrivilegedAction<Handler>() {
                    public Handler run() {
                            InternalStreamHandler alternate =
                                new InternalStreamHandler(System.err);
                            alternate.setLevel(Level.ALL);
                            return alternate;
                        }
                });

        /** handler to which messages are copied */
        private InternalStreamHandler copyHandler = null;

        /* logger to which log messages are written */
        private final Logger logger;

        /* used as return value of RemoteServer.getLog */
        private LoggerPrintStream loggerSandwich;

        /** creates a Log which will delegate to the given logger */
        private LoggerLog(final Logger logger, final Level level) {
            this.logger = logger;

            if (level != null){
                java.security.AccessController.doPrivileged(
                    new java.security.PrivilegedAction<Void>() {
                        public Void run() {
                            if (!logger.isLoggable(level)) {
                                logger.setLevel(level);
                            }
                            logger.addHandler(alternateConsole);
                            return null;
                        }
                    }
                );
            }
        }

        public boolean isLoggable(Level level) {
            return logger.isLoggable(level);
        }

        public void log(Level level, String message) {
            if (isLoggable(level)) {
                String[] source = getSource();
                logger.logp(level, source[0], source[1],
                           Thread.currentThread().getName() + ": " + message);
            }
        }

        public void log(Level level, String message, Throwable thrown) {
            if (isLoggable(level)) {
                String[] source = getSource();
                logger.logp(level, source[0], source[1],
                    Thread.currentThread().getName() + ": " +
                           message, thrown);
            }
        }

        /**
         * Set the output stream associated with the RMI server call
         * logger.
         *
         * Calling code needs LoggingPermission "control".
         */
        public synchronized void setOutputStream(OutputStream out) {
            if (out != null) {
                if (!logger.isLoggable(VERBOSE)) {
                    logger.setLevel(VERBOSE);
                }
                copyHandler = new InternalStreamHandler(out);
                copyHandler.setLevel(Log.VERBOSE);
                logger.addHandler(copyHandler);
            } else {
                /* ensure that messages are not logged */
                if (copyHandler != null) {
                    logger.removeHandler(copyHandler);
                }
                copyHandler = null;
            }
        }

        public synchronized PrintStream getPrintStream() {
            if (loggerSandwich == null) {
                loggerSandwich = new LoggerPrintStream(logger);
            }
            return loggerSandwich;
        }
    }

    /**
     * Subclass of StreamHandler for redirecting log output.  flush
     * must be called in the publish and close methods.
     */
    private static class InternalStreamHandler extends StreamHandler {
        InternalStreamHandler(OutputStream out) {
            super(out, new SimpleFormatter());
        }

        public void publish(LogRecord record) {
            super.publish(record);
            flush();
        }

        public void close() {
            flush();
        }
    }

    /**
     * PrintStream which forwards log messages to the logger.  Class
     * is needed to maintain backwards compatibility with
     * RemoteServer.{set|get}Log().
     */
    private static class LoggerPrintStream extends PrintStream {

        /** logger where output of this log is sent */
        private final Logger logger;

        /** record the last character written to this stream */
        private int last = -1;

        /** stream used for buffering lines */
        private final ByteArrayOutputStream bufOut;

        private LoggerPrintStream(Logger logger)
        {
            super(new ByteArrayOutputStream());
            bufOut = (ByteArrayOutputStream) super.out;
            this.logger = logger;
        }

        public void write(int b) {
            if ((last == '\r') && (b == '\n')) {
                last = -1;
                return;
            } else if ((b == '\n') || (b == '\r')) {
                try {
                    /* write the converted bytes of the log message */
                    String message =
                        Thread.currentThread().getName() + ": " +
                        bufOut.toString();
                    logger.logp(Level.INFO, "LogStream", "print", message);
                } finally {
                    bufOut.reset();
                }
            } else {
                super.write(b);
            }
            last = b;
        }

        public void write(byte b[], int off, int len) {
            if (len < 0) {
                throw new ArrayIndexOutOfBoundsException(len);
            }
            for (int i = 0; i < len; i++) {
                write(b[off + i]);
            }
        }

        public String toString() {
            return "RMI";
        }
    }

    /**
     * Factory to create Log objects which deliver log messages to the
     * java.rmi.server.LogStream API
     */
    private static class LogStreamLogFactory implements LogFactory {
        LogStreamLogFactory() {}

        /* create a new LogStreamLog for the specified log */
        public Log createLog(String loggerName, String oldLogName,
                             Level level)
        {
            LogStream stream = null;
            if (oldLogName != null) {
                stream = LogStream.log(oldLogName);
            }
            return new LogStreamLog(stream, level);
        }
    }

    /**
     * Class specialized to log messages to the
     * java.rmi.server.LogStream API
     */
    private static class LogStreamLog extends Log {
        /** Log stream to which log messages are written */
        private final LogStream stream;

        /** the level of the log as set by associated property */
        private int levelValue = Level.OFF.intValue();

        private LogStreamLog(LogStream stream, Level level) {
            if ((stream != null) && (level != null)) {
                /* if the stream or level is null, don't log any
                 * messages
                 */
                levelValue = level.intValue();
            }
            this.stream = stream;
        }

        public synchronized boolean isLoggable(Level level) {
            return (level.intValue() >= levelValue);
        }

        public void log(Level messageLevel, String message) {
            if (isLoggable(messageLevel)) {
                String[] source = getSource();
                stream.println(unqualifiedName(source[0]) +
                               "." + source[1] + ": " + message);
            }
        }

        public void log(Level level, String message, Throwable thrown) {
            if (isLoggable(level)) {
                /*
                 * keep output contiguous and maintain the contract of
                 * RemoteServer.getLog
                 */
                synchronized (stream) {
                    String[] source = getSource();
                    stream.println(unqualifiedName(source[0]) + "." +
                                   source[1] + ": " + message);
                    thrown.printStackTrace(stream);
                }
            }
        }

        public PrintStream getPrintStream() {
            return stream;
        }

        public synchronized void setOutputStream(OutputStream out) {
            if (out != null) {
                if (VERBOSE.intValue() < levelValue) {
                    levelValue = VERBOSE.intValue();
                }
                stream.setOutputStream(out);
            } else {
                /* ensure that messages are not logged */
                levelValue = Level.OFF.intValue();
            }
        }

        /*
         * Mimic old log messages that only contain unqualified names.
         */
        private static String unqualifiedName(String name) {
            int lastDot = name.lastIndexOf(".");
            if (lastDot >= 0) {
                name = name.substring(lastDot + 1);
            }
            name = name.replace('$', '.');
            return name;
        }
    }

    /**
     * Obtain class and method names of code calling a log method.
     */
    private static String[] getSource() {
        StackTraceElement[] trace = (new Exception()).getStackTrace();
        return new String[] {
            trace[3].getClassName(),
            trace[3].getMethodName()
        };
    }
}
