/*
 * 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.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.security.AccessController;
import java.security.PrivilegedExceptionAction;

/**
 * A {@code Handler} object accepts a logging request and exports the desired
 * messages to a target, for example, a file, the console, etc. It can be
 * disabled by setting its logging level to {@code Level.OFF}.
 */
public abstract class Handler {

    private static final Level DEFAULT_LEVEL = Level.ALL;

    // the error manager to report errors during logging
    private ErrorManager errorMan;

    // the character encoding used by this handler
    private String encoding;

    // the logging level
    private Level level;

    // the formatter used to export messages
    private Formatter formatter;

    // the filter used to filter undesired messages
    private Filter filter;

    // class name, used for property reading
    private String prefix;

    /**
     * Constructs a {@code Handler} object with a default error manager instance
     * {@code ErrorManager}, the default encoding, and the default logging
     * level {@code Level.ALL}. It has no filter and no formatter.
     */
    protected Handler() {
        this.errorMan = new ErrorManager();
        this.level = DEFAULT_LEVEL;
        this.encoding = null;
        this.filter = null;
        this.formatter = null;
        this.prefix = this.getClass().getName();
    }

    // get a instance from given class name, using Class.forName()
    private Object getDefaultInstance(String className) {
        Object result = null;
        if (null == className) {
            return result;
        }
        try {
            result = Class.forName(className).newInstance();
        } catch (Exception e) {
            // ignore
        }
        return result;
    }

    // get a instance from given class name, using context classloader
    private Object getCustomizeInstance(final String className)
            throws Exception {
        Class<?> c = AccessController
                .doPrivileged(new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws Exception {
                        ClassLoader loader = Thread.currentThread()
                                .getContextClassLoader();
                        if (null == loader) {
                            loader = ClassLoader.getSystemClassLoader();
                        }
                        return loader.loadClass(className);
                    }
                });
        return c.newInstance();
    }

    // print error message in some format
    void printInvalidPropMessage(String key, String value, Exception e) {
        String msg = "Invalid property value for " + prefix + ":" + key + "/" + value;
        errorMan.error(msg, e, ErrorManager.GENERIC_FAILURE);
    }

    /**
     * init the common properties, including filter, level, formatter, and
     * encoding
     */
    void initProperties(String defaultLevel, String defaultFilter,
            String defaultFormatter, String defaultEncoding) {
        LogManager manager = LogManager.getLogManager();

        // set filter
        final String filterName = manager.getProperty(prefix + ".filter");
        if (null != filterName) {
            try {
                filter = (Filter) getCustomizeInstance(filterName);
            } catch (Exception e1) {
                printInvalidPropMessage("filter", filterName, e1);
                filter = (Filter) getDefaultInstance(defaultFilter);
            }
        } else {
            filter = (Filter) getDefaultInstance(defaultFilter);
        }

        // set level
        String levelName = manager.getProperty(prefix + ".level");
        if (null != levelName) {
            try {
                level = Level.parse(levelName);
            } catch (Exception e) {
                printInvalidPropMessage("level", levelName, e);
                level = Level.parse(defaultLevel);
            }
        } else {
            level = Level.parse(defaultLevel);
        }

        // set formatter
        final String formatterName = manager.getProperty(prefix + ".formatter");
        if (null != formatterName) {
            try {
                formatter = (Formatter) getCustomizeInstance(formatterName);
            } catch (Exception e) {
                printInvalidPropMessage("formatter", formatterName, e);
                formatter = (Formatter) getDefaultInstance(defaultFormatter);
            }
        } else {
            formatter = (Formatter) getDefaultInstance(defaultFormatter);
        }

        // set encoding
        final String encodingName = manager.getProperty(prefix + ".encoding");
        try {
            internalSetEncoding(encodingName);
        } catch (UnsupportedEncodingException e) {
            printInvalidPropMessage("encoding", encodingName, e);
        }
    }

    /**
     * Closes this handler. A flush operation will be performed and all the
     * associated resources will be freed. Client applications should not use
     * this handler after closing it.
     *
     * @throws SecurityException
     *             if a security manager determines that the caller does not
     *             have the required permission.
     */
    public abstract void close();

    /**
     * Flushes any buffered output.
     */
    public abstract void flush();

    /**
     * Accepts a logging request and sends it to the the target.
     *
     * @param record
     *            the log record to be logged; {@code null} records are ignored.
     */
    public abstract void publish(LogRecord record);

    /**
     * Gets the character encoding used by this handler, {@code null} for
     * default encoding.
     *
     * @return the character encoding used by this handler.
     */
    public String getEncoding() {
        return this.encoding;
    }

    /**
     * Gets the error manager used by this handler to report errors during
     * logging.
     *
     * @return the error manager used by this handler.
     * @throws SecurityException
     *             if a security manager determines that the caller does not
     *             have the required permission.
     */
    public ErrorManager getErrorManager() {
        LogManager.getLogManager().checkAccess();
        return this.errorMan;
    }

    /**
     * Gets the filter used by this handler.
     *
     * @return the filter used by this handler (possibly {@code null}).
     */
    public Filter getFilter() {
        return this.filter;
    }

    /**
     * Gets the formatter used by this handler to format the logging messages.
     *
     * @return the formatter used by this handler (possibly {@code null}).
     */
    public Formatter getFormatter() {
        return this.formatter;
    }

    /**
     * Gets the logging level of this handler, records with levels lower than
     * this value will be dropped.
     *
     * @return the logging level of this handler.
     */
    public Level getLevel() {
        return this.level;
    }

    /**
     * Determines whether the supplied log record needs to be logged. The
     * logging levels will be checked as well as the filter.
     *
     * @param record
     *            the log record to be checked.
     * @return {@code true} if the supplied log record needs to be logged,
     *         otherwise {@code false}.
     */
    public boolean isLoggable(LogRecord record) {
        if (null == record) {
            throw new NullPointerException();
        }
        if (this.level.intValue() == Level.OFF.intValue()) {
            return false;
        } else if (record.getLevel().intValue() >= this.level.intValue()) {
            return null == this.filter || this.filter.isLoggable(record);
        }
        return false;
    }

    /**
     * Reports an error to the error manager associated with this handler,
     * {@code ErrorManager} is used for that purpose. No security checks are
     * done, therefore this is compatible with environments where the caller
     * is non-privileged.
     *
     * @param msg
     *            the error message, may be {@code null}.
     * @param ex
     *            the associated exception, may be {@code null}.
     * @param code
     *            an {@code ErrorManager} error code.
     */
    protected void reportError(String msg, Exception ex, int code) {
        this.errorMan.error(msg, ex, code);
    }

    /**
     * Sets the character encoding used by this handler. A {@code null} value
     * indicates the use of the default encoding. This internal method does
     * not check security.
     *
     * @param newEncoding
     *            the character encoding to set.
     * @throws UnsupportedEncodingException
     *             if the specified encoding is not supported by the runtime.
     */
    void internalSetEncoding(String newEncoding) throws UnsupportedEncodingException {
        // accepts "null" because it indicates using default encoding
        if (newEncoding == null) {
            this.encoding = null;
        } else {
            if (Charset.isSupported(newEncoding)) {
                this.encoding = newEncoding;
            } else {
                throw new UnsupportedEncodingException(newEncoding);
            }
        }
    }

    /**
     * Sets the character encoding used by this handler, {@code null} indicates
     * a default encoding.
     *
     * @param encoding
     *            the character encoding to set.
     * @throws SecurityException
     *             if a security manager determines that the caller does not
     *             have the required permission.
     * @throws UnsupportedEncodingException
     *             if the specified encoding is not supported by the runtime.
     */
    public void setEncoding(String encoding) throws SecurityException,
            UnsupportedEncodingException {
        LogManager.getLogManager().checkAccess();
        internalSetEncoding(encoding);
    }

    /**
     * Sets the error manager for this handler.
     *
     * @param em
     *            the error manager to set.
     * @throws NullPointerException
     *             if {@code em} is {@code null}.
     * @throws SecurityException
     *             if a security manager determines that the caller does not
     *             have the required permission.
     */
    public void setErrorManager(ErrorManager em) {
        LogManager.getLogManager().checkAccess();
        if (null == em) {
            throw new NullPointerException();
        }
        this.errorMan = em;
    }

    /**
     * Sets the filter to be used by this handler.
     *
     * @param newFilter
     *            the filter to set, may be {@code null}.
     * @throws SecurityException
     *             if a security manager determines that the caller does not
     *             have the required permission.
     */
    public void setFilter(Filter newFilter) {
        LogManager.getLogManager().checkAccess();
        this.filter = newFilter;
    }

    /**
     * Sets the formatter to be used by this handler. This internal method does
     * not check security.
     *
     * @param newFormatter
     *            the formatter to set.
     */
    void internalSetFormatter(Formatter newFormatter) {
        if (null == newFormatter) {
            throw new NullPointerException();
        }
        this.formatter = newFormatter;
    }

    /**
     * Sets the formatter to be used by this handler.
     *
     * @param newFormatter
     *            the formatter to set.
     * @throws NullPointerException
     *             if {@code newFormatter} is {@code null}.
     * @throws SecurityException
     *             if a security manager determines that the caller does not
     *             have the required permission.
     */
    public void setFormatter(Formatter newFormatter) {
        LogManager.getLogManager().checkAccess();
        internalSetFormatter(newFormatter);
    }

    /**
     * Sets the logging level of the messages logged by this handler, levels
     * lower than this value will be dropped.
     *
     * @param newLevel
     *            the logging level to set.
     * @throws NullPointerException
     *             if {@code newLevel} is {@code null}.
     * @throws SecurityException
     *             if a security manager determines that the caller does not
     *             have the required permission.
     */
    public void setLevel(Level newLevel) {
        if (null == newLevel) {
            throw new NullPointerException();
        }
        LogManager.getLogManager().checkAccess();
        this.level = newLevel;
    }
}
