/*
 * blanco Framework
 * Copyright (C) 2004-2010 IGA Tosiki
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 */
package blanco.log.custom;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.Map.Entry;
import java.util.logging.Handler;
import java.util.logging.LogRecord;

import blanco.log.logging.BlancoLogLoggingConstants;
import blanco.log.resourcebundle.BlancoLogProgramMessage;

/**
 * java.util.logging のログ出力を所定の設定にしたがって仕分出力形式する目的のカスタム・ログ・ハンドラー。
 * 
 * <UL>
 * <LI>
 * Java システムプロパティー「blanco.log.custom.BlancoLogHandlerConfDir」
 * で構成情報ディレクトリをポイントしていることを前提とします。
 * <LI>構成情報ディレクトリに「BlancoLogHandler.properties」というファイルが存在することを前提とします。
 * <LI>GlassFish などのアプリケーション・サーバーに組み込んで利用します。(開発時点では GlassFish 2.1.1 でのみ動作確認実施)
 * <LI>★注意: 実装者に通知。このクラスの内部で System.out などを利用すると、ログの循環が発生してしまう恐れがあります。
 * </UL>
 * 
 * @author IGA Tosiki
 */
public class BlancoLogHandler extends Handler {
    /**
     * blancoLogメッセージクラス。
     */
    protected final BlancoLogProgramMessage fMsg = new BlancoLogProgramMessage();

    /**
     * 構成情報ディレクトリーをあらわす Java システムプロパティー名。
     */
    public static final String ENV_CONF_DIR = "blanco.log.custom.BlancoLogHandlerConfDir";

    /**
     * ログ・ファイル名決定の際に利用する日付形式。
     */
    final SimpleDateFormat sdFormat = new SimpleDateFormat("yyyyMMdd");

    /**
     * ログ・フォーマッター・オブジェクト。
     */
    final BlancoLogFormatter fLogFormatter = new BlancoLogFormatter();

    /**
     * ログのリダイレクト先ファイル名(名称の一部)についてのマップ情報。
     */
    final Map<String, String> fLogMap = new HashMap<String, String>();

    /**
     * ログ出力先ディレクトリ。
     */
    File fLogDir = null;

    /**
     * 連続して同じオブジェクトが渡される現象を防止するために、前回オブジェクトを記憶します。 同一オブジェクトの場合には出力を抑制します。
     */
    LogRecord fPrevRecord = null;

    /**
     * ログフォーマッターのインスタンスを新規作成します。
     */
    public BlancoLogHandler() {
        // Java システムプロパティーによるディレクトリ指定が必要。
        final String confDirName = System.getProperty(ENV_CONF_DIR);
        if (confDirName == null) {
            throw new IllegalArgumentException(fMsg.getMblgch01(ENV_CONF_DIR));
        }

        final File confDir = new File(confDirName);
        if (confDir.exists() == false) {
            throw new IllegalArgumentException(fMsg.getMblgch02(ENV_CONF_DIR,
                    confDir.getAbsolutePath()));
        }

        try {
            // プロパティ・ファイルから設定情報を読み込みます。
            loadProperties(confDir);

            // 指定されたログファイル群が存在するかどうか念押しします。無ければ作成します。
            ensureLogFilesExist();
        } catch (IOException ex) {
            throw new IllegalArgumentException(fMsg.getMblgch03(ex.toString()),
                    ex);
        }
    }

    @Override
    public void publish(final LogRecord rec) {
        // TODO より効率的なファイル出力の実現を検討すること。

        if (rec != fPrevRecord) {
            fPrevRecord = rec;
            try {
                write(rec);
            } catch (IOException ex) {
                throw new IllegalArgumentException(fMsg.getMblgch09(ex
                        .toString()), ex);
            }
        } else {
            // 何もしない。
            // ★理由は不明だが、何度も同じログが出力依頼される場合がある。これのログ出力を抑制。
        }
    }

    @Override
    public void flush() {
    }

    @Override
    public void close() throws SecurityException {
    }

    /**
     * 与えられたログ・レコードを出力します。
     * 
     * @param rec
     * @throws IOException
     */
    void write(final LogRecord rec) throws IOException {
        // レベルによる、有無をいわせずログ書き込み。
        if (BlancoLogLoggingConstants.SEVERE == rec.getLevel().intValue()) {
            // SEVERE レベルのものは、別途 severe.log ファイルに出力します。
            appendLogLine(fLogFormatter.format(rec), fLogDir, "severe");
        }

        // ロガー名によるログファイルの書き分け。
        String logFileName = fLogMap.get(rec.getLoggerName());
        if (logFileName == null) {
            // 仕分け先に登録されていないので、これは無視します。
        } else {
            appendLogLine(fLogFormatter.format(rec), fLogDir, logFileName);
        }
    }

    /**
     * 所定のファイル名のログ・ファイルに、ログ・メッセージ 1 行を追加します。
     * 
     * @param mesg
     * @param logDir
     * @param logFileName
     * @throws IOException
     */
    void appendLogLine(final String mesg, final File logDir,
            final String logFileName) throws IOException {
        BufferedWriter writer = null;
        for (int retry = 0; retry < 60; retry++) {
            try {
                final FileOutputStream outStream = new FileOutputStream(
                        new File(logDir, getLogFileName(logFileName)), true);
                outStream.getChannel().lock();
                writer = new BufferedWriter(new OutputStreamWriter(outStream));
                break;
            } catch (IOException ex) {
                // ロック取得できず。
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // 無視します。
                }
            }
        }

        if (writer == null) {
            throw new IOException(fMsg.getMblgch04(getLogFileName(logFileName)));
        }

        try {
            writer.write(mesg);
            writer.newLine();
        } finally {
            writer.close();
        }
    }

    /**
     * プロパティ・ファイルから設定情報を読み込みます。
     * 
     * @param confDir
     * @throws IOException
     */
    void loadProperties(final File confDir) throws IOException {
        final Properties prop = new Properties();
        prop.load(new FileInputStream(new File(confDir,
                "BlancoLogHandler.properties")));

        // 出力先ログディレクトリ。
        fLogDir = new File(prop.getProperty("LogDir"));

        if (fLogDir.exists() == false) {
            // 指定のログ・ディレクトリーは存在しません。ログ出力をあきらめます。
            throw new IllegalArgumentException(fMsg.getMblgch05(fLogDir
                    .getAbsolutePath()));
        }
        if (fLogDir.isDirectory() == false) {
            throw new IllegalArgumentException(fMsg.getMblgch06(fLogDir
                    .getAbsolutePath()));
        }

        // プロパティファイル内の記述を、ログ・マッピングに利用します。
        final Set<Entry<Object, Object>> entries = prop.entrySet();
        for (Entry<Object, Object> entry : entries) {
            if ("LogDir".equals(entry.getKey()) == false) {
                fLogMap.put((String) entry.getKey(), (String) entry.getValue());
            }
        }
    }

    /**
     * 指定されたログファイル群が存在するかどうか念押しします。
     * 
     * 無ければ作成します。
     * 
     * @throws IOException
     */
    void ensureLogFilesExist() throws IOException {
        // ログファイルについて存在するように念押し。
        ensureLogFileExist(fLogDir, "severe");

        for (Entry<String, String> entry : fLogMap.entrySet()) {
            ensureLogFileExist(fLogDir, (String) entry.getValue());
        }
    }

    /**
     * 指定されたログファイルが存在するかどうか念押しします。
     * 
     * 無ければ作成します。
     * 
     * @param logDir
     * @param logFileName
     * @throws IOException
     */
    void ensureLogFileExist(final File logDir, String logFileName)
            throws IOException {
        if (logFileName == null) {
            logFileName = "null";
        }

        final File file = new File(logDir, getLogFileName(logFileName));
        if (file.exists() == false) {
            try {
                if (file.createNewFile() == false) {
                    throw new IllegalArgumentException(fMsg.getMblgch07(file
                            .getAbsolutePath()));
                }
            } catch (IOException ex) {
                throw new IllegalArgumentException(fMsg.getMblgch08(file
                        .getAbsolutePath(), ex.toString()), ex);
            }
        }
    }

    /**
     * ログのファイル名を決定します。
     * 
     * @param logFileName
     *            基本となるファイル名。
     * @return
     */
    String getLogFileName(String logFileName) {
        synchronized (sdFormat) {
            final Date now = new Date();
            return logFileName + ".log-" + sdFormat.format(now);
        }
    }
}