// --- BEGIN COPYRIGHT BLOCK ---
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; version 2 of the License.
//
// This program 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 for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// (C) 2007 Red Hat, Inc.
// All rights reserved.
// --- END COPYRIGHT BLOCK ---
package com.netscape.cms.logging;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Hashtable;
import java.util.Locale;
import java.util.Vector;

import javax.servlet.ServletException;

import org.mozilla.jss.netscape.security.util.Utils;

import com.netscape.certsrv.base.EBaseException;
import com.netscape.certsrv.base.IConfigStore;
import com.netscape.certsrv.base.IExtendedPluginInfo;
import com.netscape.certsrv.common.Constants;
import com.netscape.certsrv.common.NameValuePairs;
import com.netscape.certsrv.logging.AuditEvent;
import com.netscape.certsrv.logging.ELogException;
import com.netscape.certsrv.logging.ILogEvent;
import com.netscape.certsrv.logging.ILogger;
import com.netscape.cmscore.apps.CMS;

/**
 * A rotating log file for Certificate log events. This class loosely follows
 * the Netscape Common Log API implementing rollover interval, size and file
 * naming conventions. It does not yet implement Disk Usage.
 *
 * @version $Revision$, $Date$
 */
public class RollingLogFile extends LogFile {

    public static org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(RollingLogFile.class);
    private static Logger signedAuditLogger = SignedAuditLogger.getLogger();

    /**
     * The default max file size in bytes
     */
    static final int MAX_FILE_SIZE = 100;

    /**
     * The default rollover interval in seconds
     */
    static final String ROLLOVER_INTERVAL = "2592000";

    /**
     * The default expiration time in seconds
     */
    static final String EXPIRATION_TIME = "0";

    /**
     * The maximum file size in bytes
     */
    protected int mMaxFileSize = 0;

    /**
     * The amount of time in miniseconds between log rotations
     */
    protected long mRolloverInterval = 0;

    /**
     * The thread responsible for rotating the log
     */
    private Thread mRolloverThread = null;

    /**
     * The amount of time before a backed up log is removed in milliseconds
     */
    protected long mExpirationTime = 0;

    /**
     * The thread responsible for removing expired log files
     */
    private Thread mExpirationThread = null;

    /**
     * The object used as a lock for expiration thread synchronization
     */
    private Object mExpLock = new Object();

    /**
     * Construct a RollingLogFile
     */
    public RollingLogFile() {
    }

    /**
     * Initialize and open a RollingLogFile using the prop config store
     *
     * @param config The property config store to find values in
     */
    @Override
    public void init(IConfigStore config) throws IOException,
            EBaseException {
        super.init(config);

        rl_init(config.getInteger(Constants.PR_LOG_MAXFILESIZE, MAX_FILE_SIZE),
                config.getString(Constants.PR_LOG_ROLLEROVER_INTERVAL, ROLLOVER_INTERVAL),
                config.getString(Constants.PR_LOG_EXPIRED_TIME, EXPIRATION_TIME));
    }

    /**
     * Convenience routine to initialized the RollingLogFile specific
     * attributes.
     */
    protected void rl_init(int maxFileSize, String rolloverInterval,
            String expirationTime) {
        mMaxFileSize = maxFileSize * 1024;
        setRolloverTime(rolloverInterval);
        setExpirationTime(expirationTime);
    }

    @Override
    public void startup() throws EBaseException {
        super.startup();
    }

    /**
     * Shutdown this log file.
     */
    @Override
    public synchronized void shutdown() {
        logger.debug("Destroying RollingLogFile(" + mFileName + ")");
        setRolloverTime("0");
        setExpirationTime("0");
        super.shutdown();
    }

    /**
     * Set the rollover interval
     *
     * @param rolloverSeconds The amount of time in seconds until the log
     *            is rotated. A value of 0 will disable log rollover.
     **/
    public synchronized void setRolloverTime(String rolloverSeconds) {
        mRolloverInterval = Long.valueOf(rolloverSeconds).longValue() * 1000;

        if ((mRolloverThread == null) && (mRolloverInterval > 0)) {
            mRolloverThread = new RolloverThread();
            mRolloverThread.setDaemon(true);
            mRolloverThread.start();

        } else if (mRolloverThread != null && mRolloverInterval == 0) {
            mRolloverThread.interrupt();
        }

        this.notify();
    }

    /**
     * Get the rollover interval
     *
     * @return The interval in seconds in which the log is rotated
     **/
    public synchronized int getRolloverTime() {
        return (int) (mRolloverInterval / 1000);
    }

    /**
     * Set the file expiration time
     *
     * @param expirationSeconds The amount of time in seconds until log files
     *            are deleted
     **/
    public void setExpirationTime(String expirationSeconds) {

        // Need to completely protect changes to mExpiration time
        // and make sure they only happen while the thread is sleeping
        synchronized (mExpLock) {
            mExpirationTime = Long.valueOf(expirationSeconds).longValue() * 1000;

            if (mExpirationThread == null) {
                if (mExpirationTime > 0) {
                    mExpirationThread = new ExpirationThread();
                    mExpirationThread.setDaemon(true);
                    mExpirationThread.start();
                }

            } else if (mExpirationThread != null && mExpirationTime == 0) {
                mExpirationThread.interrupt();

            } else {
                mExpLock.notify();
            }
        }
    }

    /**
     * Get the expiration time
     *
     * @return The age in seconds in which log files are delete
     **/
    public int getExpirationTime() {
        return (int) (mExpirationTime / 1000);
    }

    /**
     * Rotate the log file to a backup file with timestamp
     **/
    public synchronized void rotate()
            throws IOException {

        File backupFile = new File(mFileName + "." + mLogFileDateFormat.format(mDate));

        // close, backup, and reopen the log file zeroizing its contents
        super.close();
        try {
            if (Utils.isNT()) {
                // NT is very picky on the path
                Utils.exec("copy " +
                            mFile.getCanonicalPath().replace('/', '\\') +
                            " " +
                            backupFile.getCanonicalPath().replace('/',
                                                                   '\\'));
            } else {
                // Create a copy of the original file which
                // preserves the original file permissions.
                Utils.exec("cp -p " + mFile.getCanonicalPath() + " " +
                             backupFile.getCanonicalPath());
            }

            // Zeroize the original file if and only if
            // the backup copy was successful.
            if (backupFile.exists()) {

                // Make certain that the backup file has
                // the correct permissions.
                if (!Utils.isNT()) {
                    Utils.exec("chmod 00640 " + backupFile.getCanonicalPath());
                }

                try {
                    // Open and close the original file
                    // to zeroize its contents.
                    PrintWriter pw = new PrintWriter(mFile);
                    pw.close();

                    // Make certain that the original file retains
                    // the correct permissions.
                    if (!Utils.isNT()) {
                        Utils.exec("chmod 00640 " + mFile.getCanonicalPath());
                    }
                } catch (FileNotFoundException e) {
                    logger.warn("Unable to zeroize " + mFile + ": " + e.getMessage(), e);
                }
            } else {
                logger.warn("Unable to backup " + mFile + " to " + backupFile);
            }
        } catch (Exception e) {
            logger.warn("Unable to backup " + mFile + " to " + backupFile + ": " + e.getMessage(), e);
        }
        super.open(); // will reset mBytesWritten
    }

    /**
     * Remove any log files which have not been modified in the specified
     * time
     * <P>
     *
     * NOTE: automatic removal of log files is currently NOT supported!
     * <P>
     *
     * <ul>
     * <li>signed.audit LOGGING_SIGNED_AUDIT_LOG_DELETE used AFTER audit log expires (authorization should not allow,
     * but in case authorization gets compromised make sure it is written AFTER the log expiration happens)
     * </ul>
     *
     * @param expirationSeconds The number of seconds since the expired files
     *            have been modified.
     * @return the time in milliseconds when the next file expires
     **/
    public long expire(long expirationSeconds) throws ELogException {
        String auditMessage = null;

        if (expirationSeconds <= 0)
            throw new ELogException(CMS.getUserMessage("CMS_LOG_EXPIRATION_TIME_ZERO"));

        long expirationTime = expirationSeconds * 1000;
        long currentTime = System.currentTimeMillis();
        long oldestFile = currentTime;

        String dirName = mFile.getParent();

        if (dirName == null)
            dirName = ".";
        File dir = new File(dirName);

        // Get just the base name, minus the .date extension
        //int len = mFile.getName().length() - LogFile.DATE_PATTERN.length() - 1;
        //String baseName = mFile.getName().substring(0, len);
        String fileName = mFile.getName();
        String baseName = null, pathName = null;
        int index = fileName.lastIndexOf("/");

        if (index != -1) { // "/"  exist in fileName
            pathName = fileName.substring(0, index);
            baseName = fileName.substring(index + 1);
            dirName = dirName.concat("/" + pathName);
        } else { // "/" NOT exist in fileName
            baseName = fileName;
        }

        fileFilter ff = new fileFilter(baseName + ".");
        String[] filelist = dir.list(ff);

        if (filelist == null) { // Crap!  Something is wrong.
            throw new ELogException(CMS.getUserMessage("CMS_LOG_DIRECTORY_LIST_FAILED",
                    dirName, ff.toString()));
        }

        // Walk through the list of files which match this log file name
        // and delete the old ones.
        for (int i = 0; i < filelist.length; i++) {
            if (pathName != null) {
                filelist[i] = pathName + "/" + filelist[i];
            } else {
                filelist[i] = dirName + "/" + filelist[i];
            }

            String fullname = dirName + File.separatorChar + filelist[i];
            File file = new File(fullname);
            long fileTime = file.lastModified();

            // Java documentation on File says lastModified() should not
            // be interpeted.  The doc is wrong.  See JavaSoft bug #4094538
            if ((currentTime - fileTime) > expirationTime) {
                file.delete();

                if (file.exists()) {
                    // log failure in deleting an expired signed audit log file
                    auditMessage = CMS.getLogMessage(
                                AuditEvent.AUDIT_LOG_DELETE,
                                ILogger.SYSTEM_UID,
                                ILogger.FAILURE,
                                fullname);
                } else {
                    // log success in deleting an expired signed audit log file
                    auditMessage = CMS.getLogMessage(
                                AuditEvent.AUDIT_LOG_DELETE,
                                ILogger.SYSTEM_UID,
                                ILogger.SUCCESS,
                                fullname);
                }

                signedAuditLogger.log(auditMessage);
            } else if (fileTime < oldestFile) {
                oldestFile = fileTime;
            }
        }
        return oldestFile + expirationTime;
    }

    //
    // Rollover and Expiration threads
    //
    // At first glance you may think it's a waste of thread resources to have
    // two threads for every log file, but the truth is that these threads are
    // sleeping 99% of the time.  NxN thread implementations (Solaris, NT,
    // IRIX 6.4, Unixware, etc...) will handle these in user space.
    //
    // You may be able to join these into one thread, and deal with
    // multiple wakeup times, but the code would sure look ugly, and the race
    // conditions are numerous as is. Furthermore, this is what user space
    // threads will do for you anyways.
    //

    /**
     * Log rotation thread. Sleep for the rollover interval and rotate the
     * log. Changing rollover interval to 0 will cause this thread to exit.
     */
    final class RolloverThread extends Thread {

        /**
         * Rollover thread constructor including thread name
         */
        public RolloverThread() {
            super();
            super.setName(mFileName + ".rollover-" + (Thread.activeCount() + 1));
        }

        @Override
        public void run() {
            while (mRolloverInterval > 0) {
                // Sleep for the interval and then rotate the log
                synchronized (RollingLogFile.this) {
                    try {
                        RollingLogFile.this.wait(mRolloverInterval);
                    } catch (InterruptedException e) {
                        // shutdown
                    }
                }

                if (mRolloverInterval == 0) {
                    break;
                }

                if (mBytesWritten > 0) {
                    try {
                        rotate();
                    } catch (IOException e) {
                        System.err.println(CMS.getUserMessage("CMS_LOG_ROTATE_LOG_FAILED", mFile.getName(), e.toString()));
                        break;
                    }
                }
                // else
                //   Don't rotate empty logs
                //   flag in log summary file?
            }
            mRolloverThread = null;
        }
    }

    /**
     * Log expiration thread. Sleep for the expiration interval and
     * delete any files which are too old.
     * Changing expiration interval to 0 will cause this thread to exit.
     */
    final class ExpirationThread extends Thread {

        /**
         * ExpirationThread thread constructor including thread name
         */
        public ExpirationThread() {
            super();
            super.setName(mFileName + ".expiration-" + (Thread.activeCount() + 1));
        }

        @Override
        public void run() {
            synchronized (mExpLock) {
                while (mExpirationTime > 0) {
                    long wakeupTime = 0;
                    long sleepTime = 0;

                    // First, remove any old log files and figure out when the
                    // next one expires
                    try {
                        wakeupTime = expire(mExpirationTime / 1000);
                    } catch (SecurityException e) {
                        System.err.println(CMS.getUserMessage("CMS_LOG_EXPIRE_LOG_FAILED", e.toString()));
                        break;
                    } catch (ELogException e) {
                        System.err.println(CMS.getUserMessage("CMS_LOG_EXPIRE_LOG_FAILED", e.toString()));
                        break;
                    }

                    sleepTime = wakeupTime - System.currentTimeMillis();
                    //System.out.println("wakeup " + wakeupTime);
                    //System.out.println("current "+System.currentTimeMillis());
                    //System.out.println("sleep " + sleepTime);
                    // Sleep for the interval and then check the directory
                    // Note: mExpirationTime can only change while we're
                    // sleeping
                    if (sleepTime > 0) {
                        try {
                            mExpLock.wait(sleepTime);
                        } catch (InterruptedException e) {
                            // shutdown
                        }
                    }

                    if (mExpirationTime == 0) {
                        break;
                    }
                }
            }
            mExpirationThread = null;
        }
    }

    /**
     * Write an event to the log file
     *
     * @param ev The event to be logged.
     **/
    @Override
    public synchronized void log(ILogEvent ev) throws ELogException {
        //xxx, Shall we log first without checking if it exceed the maximum?
        super.log(ev); // Will increment mBytesWritten

        if ((0 != mMaxFileSize) && (mBytesWritten > mMaxFileSize)) {
            flush();
            try {
                rotate();
            } catch (IOException e) {
                throw new ELogException(CMS.getUserMessage("CMS_LOG_ROTATE_LOG_FAILED", mFile.getName(), e.toString()));
            }
        }
    }

    /**
     * Retrieve log file list.
     */
    @Override
    public synchronized NameValuePairs retrieveLogList(Hashtable<String, String> req
            ) throws ServletException,
                    IOException, EBaseException {
        NameValuePairs params = new NameValuePairs();
        String[] files = null;

        files = fileList();
        for (int i = 0; i < files.length; i++) {
            params.put(files[i], "");
        }
        return params;
    }

    /**
     * Get the log file list in the log directory
     *
     * @return an array of filenames with related path to cert server root
     */
    protected String[] fileList() {
        String pathName = null, baseName = null;

        String dirName = mFile.getParent();
        String fileName = mFile.getName();
        int index = fileName.lastIndexOf("/");

        if (index != -1) { // "/"  exist in fileName
            pathName = fileName.substring(0, index);
            baseName = fileName.substring(index + 1);
            if (dirName == null) {
                dirName = pathName;
            } else {
                dirName = dirName.concat("/" + pathName);
            }
        } else { // "/" NOT exist in fileName
            baseName = fileName;
        }

        File dir = new File(dirName);

        fileFilter ff = new fileFilter(baseName + ".");
        //There are some difference here. both should work
        //error,logs,logs/error jdk115
        //logs/system,., logs/system jdk116
        //System.out.println(mFile.getName()+","+dirName+","+mFile.getPath()); //log/system,.

        String[] filelist = dir.list(ff);

        for (int i = 0; i < filelist.length; i++) {
            if (pathName != null) {
                filelist[i] = pathName + "/" + filelist[i];
            } else {
                filelist[i] = dirName + "/" + filelist[i];
            }
        }
        return filelist;
    }

    @Override
    public String getImplName() {
        return "RollingLogFile";
    }

    @Override
    public String getDescription() {
        return "RollingLogFile";
    }

    @Override
    public Vector<String> getDefaultParams() {
        Vector<String> v = super.getDefaultParams();

        v.addElement(Constants.PR_LOG_MAXFILESIZE + "=");
        v.addElement(Constants.PR_LOG_ROLLEROVER_INTERVAL + "=");
        //v.addElement(Constants.PR_LOG_EXPIRED_TIME + "=");
        return v;
    }

    @Override
    public Vector<String> getInstanceParams() {
        Vector<String> v = super.getInstanceParams();

        try {
            v.addElement(Constants.PR_LOG_MAXFILESIZE + "=" + mMaxFileSize / 1024);
            if (mRolloverInterval / 1000 <= 60 * 60)
                v.addElement(Constants.PR_LOG_ROLLEROVER_INTERVAL + "=" + "Hourly");
            else if (mRolloverInterval / 1000 <= 60 * 60 * 24)
                v.addElement(Constants.PR_LOG_ROLLEROVER_INTERVAL + "=" + "Daily");
            else if (mRolloverInterval / 1000 <= 60 * 60 * 24 * 7)
                v.addElement(Constants.PR_LOG_ROLLEROVER_INTERVAL + "=" + "Weekly");
            else if (mRolloverInterval / 1000 <= 60 * 60 * 24 * 30)
                v.addElement(Constants.PR_LOG_ROLLEROVER_INTERVAL + "=" + "Monthly");
            else if (mRolloverInterval / 1000 <= 60 * 60 * 24 * 366)
                v.addElement(Constants.PR_LOG_ROLLEROVER_INTERVAL + "=" + "Yearly");

            //v.addElement(PROP_EXPIRATION_TIME + "=" + mExpirationTime / 1000);
        } catch (Exception e) {
        }
        return v;
    }

    @Override
    public String[] getExtendedPluginInfo(Locale locale) {
        String[] p = super.getExtendedPluginInfo(locale);
        Vector<String> info = new Vector<>();

        for (int i = 0; i < p.length; i++) {
            if (!p[i].startsWith(IExtendedPluginInfo.HELP_TOKEN) && !p[i].startsWith(IExtendedPluginInfo.HELP_TEXT))
                info.addElement(p[i]);
        }
        info.addElement(Constants.PR_LOG_MAXFILESIZE
                + ";integer;If the current log file size if bigger than this parameter in kilobytes(KB), the file will be rotated.");
        info.addElement(Constants.PR_LOG_ROLLEROVER_INTERVAL
                + ";choice(Hourly,Daily,Weekly,Monthly,Yearly);The frequency of the log being rotated.");
        info.addElement(Constants.PR_LOG_EXPIRED_TIME
                + ";integer;The amount of time before a backed up log is removed in seconds");
        info.addElement(IExtendedPluginInfo.HELP_TOKEN +
                //";configuration-logrules-rollinglogfile");
                ";configuration-adminbasics");
        info.addElement(IExtendedPluginInfo.HELP_TEXT +
                ";Write the log messages to a file which will be rotated automatically.");
        String[] params = new String[info.size()];

        info.copyInto(params);
        return params;

    }
}

/**
 * A file filter to select the file with a given prefix
 */
class fileFilter implements FilenameFilter {
    String patternToMatch = null;

    public fileFilter(String pattern) {
        patternToMatch = pattern;
    }

    @Override
    public boolean accept(File dir, String name) {
        if (name.startsWith(patternToMatch))
            return true;
        else
            return false;
    }
}
