/*
 * The contents of this file are subject to the terms 
 * of the Common Development and Distribution License 
 * (the License).  You may not use this file except in
 * compliance with the License.
 * 
 * You can obtain a copy of the license at 
 * https://glassfish.dev.java.net/public/CDDLv1.0.html or
 * glassfish/bootstrap/legal/CDDLv1.0.txt.
 * See the License for the specific language governing 
 * permissions and limitations under the License.
 * 
 * When distributing Covered Code, include this CDDL 
 * Header Notice in each file and include the License file 
 * at glassfish/bootstrap/legal/CDDLv1.0.txt.  
 * If applicable, add the following below the CDDL Header, 
 * with the fields enclosed by brackets [] replaced by
 * you own identifying information: 
 * "Portions Copyrighted [year] [name of copyright owner]"
 * 
 * Copyright 2006 Sun Microsystems, Inc. All rights reserved.
 */
package com.sun.enterprise.server.logging;

import java.io.*;
import java.util.Date;
import java.util.LinkedList;
import java.util.ArrayList;
import java.text.MessageFormat;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.text.FieldPosition;
import java.util.logging.StreamHandler;
import java.util.logging.LogRecord;
import java.util.logging.Level;
import java.util.logging.ErrorManager;
import java.util.logging.Formatter;

import javax.management.ObjectName;
import javax.management.MBeanServer;

import com.sun.enterprise.util.StringUtils;
import com.sun.enterprise.server.ApplicationServer;
import com.sun.enterprise.server.ServerContext;

import com.sun.enterprise.server.logging.stats.ErrorStatistics;
import com.sun.enterprise.server.logging.logviewer.backend.LogFilter;
import com.sun.enterprise.server.logging.logviewer.backend.LogFile;

import com.sun.enterprise.config.serverbeans.LogService;

// Delete
import java.util.HashMap;
import java.util.logging.Logger;
import java.util.Vector;

/**
 * FileandSyslogHandler publishes formatted log Messages to a FILE.
 * 
 * @AUTHOR: Hemanth Puttaswamy
 * _REVISIT_:
 * TODO:
 * 1. Ability to lock log files to get exclusive write access 
 */
public class FileandSyslogHandler extends StreamHandler {
    // This is a OutputStream to keep track of number of bytes
    // written out to the stream
    private MeteredStream meter;
 
    private final AMXLoggingHook     mAMXLoggingHook;

    private Date date = new Date( ); 
    private static final String LOGS_DIR = "logs";
    private String logFileName = "server.log"; 

    private String absoluteFileName = null;

    private static boolean isInitialized = false;

    private LogMBean logMBean;

    private LogService logService = null;

    private static final int WARNING = Level.WARNING.intValue();

    private static final int SEVERE = Level.SEVERE.intValue();

    private static final String INSTANCE_ROOT_PROPERTY = 
		"com.sun.aas.instanceRoot";      

    private static final String LOGGING_MAX_HISTORY_FILES = "com.sun.enterprise.server.logging.max_history_files";

    // For now the mimimum rotation value is 0.5 MB.
    private static final int MINIMUM_FILE_ROTATION_VALUE = 500000;

    // Initially the LogRotation will be off until the domain.xml value is read.
    private int limitForFileRotation = 0;

    private Object fileUpdateLock = new Object( );

    private boolean rotationInProgress = false;

    private static final FileandSyslogHandler thisInstance = 
        new FileandSyslogHandler( );

    private LinkedList pendingLogRecordList = new LinkedList();

    // Rotation can be done in 3 ways
    // 1. Based on the Size: Rotate when some Threshold number of bytes are 
    //    written to server.log
    // 2. Based on the Time: Rotate ever 'n' minutes, mostly 24 hrs
    // 3. Rotate now
    // For mechanisms 2 and 3 we will use this flag. The rotate() will always
    // be fired from the publish( ) method for consistency
    private static boolean rotationRequested = false;

    public static synchronized FileandSyslogHandler getInstance( ) {
        return thisInstance;
    }

    private static final String LOG_ROTATE_DATE_FORMAT =
        "yyyy-MM-dd'T'HH-mm-ss";

    private static final SimpleDateFormat logRotateDateFormatter =
        new SimpleDateFormat( LOG_ROTATE_DATE_FORMAT );
   
    // We maintain a list (Vector) of the last MAX_RECENT_ERRORS WARNING 
    // or SEVERE error messages that were logged. The DAS (or any other 
    // client) can then obtain these messages through the ServerRuntimeMBean
    // and determine if the server instance (or Node Agent) is in an 
    // error state.
    private static Vector _recentErrors = new Vector();

    private static final int MAX_RECENT_ERRORS = 4;
    
    public synchronized static Vector getRecentErrorMessages() {
        return _recentErrors;
    }
        
    public synchronized static void clearRecentErrorMessages() {
        _recentErrors = new Vector();
    }
    
    private synchronized static void addRecentErrorMessage(String error) {
        if (_recentErrors.size() < MAX_RECENT_ERRORS) {
            _recentErrors.add(error);
        } else {
            _recentErrors.removeElementAt(0);
            _recentErrors.add(MAX_RECENT_ERRORS - 1, error);
        }
    }        

    /**
     *  This method is invoked from LogManager.reInitializeLoggers() to
     *  change the location of the file.
     */
    void changeFileName( String fileName ) {
        // If the file name is same as the current file name, there
        // is no need to change the filename
        if( fileName.trim().equals( absoluteFileName ) ) {
            return;
        }
        synchronized( this ) { 
            super.flush( );
            super.close();
            try {
                openFile( fileName );
                // Change the file for LogViewer
                LogFilter.setLogFile( new LogFile( fileName ) ); 
                absoluteFileName = fileName;
            } catch( IOException ix ) {
                new ErrorManager().error( 
                    "FATAL ERROR: COULD NOT OPEN LOG FILE. " +
                    "Please Check to make sure that the directory for " +
                    "Logfile exists. Currently reverting back to use the " +
                    " default server.log", ix, ErrorManager.OPEN_FAILURE );
                try {
                    // Reverting back to the old server.log
                    openFile( absoluteFileName );
                } catch( Exception e ) {
                    new ErrorManager().error( 
                        "FATAL ERROR: COULD NOT RE-OPEN SERVER LOG FILE. ", e,
                        ErrorManager.OPEN_FAILURE ); 
                }
            }
        }
    }

    /**
     * A simple getter to access the complete fileName used by this FileHandler.
     */
    String getAbsoluteLogFileName( ) {
        return absoluteFileName;
    }

    /**
     *  A package private method to set the limit for File Rotation.
     */
    synchronized void setLimitForRotation( int rotationLimitInBytes ) {
        if ((rotationLimitInBytes == 0) ||
	        (rotationLimitInBytes >= MINIMUM_FILE_ROTATION_VALUE )) {
            limitForFileRotation = rotationLimitInBytes;
        }
    }
    


    // NOTE: This private class is copied from java.util.logging.FileHandler
    // A metered stream is a subclass of OutputStream that
    //   (a) forwards all its output to a target stream
    //   (b) keeps track of how many bytes have been written
    private class MeteredStream extends OutputStream {
        OutputStream out;
        long written;

        MeteredStream(OutputStream out, long written) {
            this.out = out;
            this.written = written;
        }

        public void write(int b) throws IOException {
            out.write(b);
            written++;
        }

        public void write(byte buff[]) throws IOException {
            out.write(buff);
            written += buff.length;
        }

        public void write(byte buff[], int off, int len) throws IOException {
            out.write(buff,off,len);
            written += len;
        }

        public void flush() throws IOException {
            out.flush();
        }

        public void close() throws IOException {
            out.close();
        }
    }


    /**
     * The Default constructor creates the File and sets the 
     * UniformLogFormatter.
     */
    protected FileandSyslogHandler( ) {
        mAMXLoggingHook    = createAMXLoggingHook();
        try {
            setFormatter( new UniformLogFormatter( ) );
        } catch( Exception e ) {
            new ErrorManager().error( 
                "FATAL ERROR: COULD NOT INSTANTIATE FILE AND SYSLOG HANDLER", 
                e, ErrorManager.GENERIC_FAILURE );
        }
    }



    /**
     * Creates the File under the specified instance directory
     */
    public String createFileName( ) {
        ServerContext sc = ApplicationServer.getServerContext();
        String instDir = "";
        if (sc != null) {
            instDir = sc.getInstanceEnvironment().getInstancesRoot();
        } else {
            instDir = System.getProperty( INSTANCE_ROOT_PROPERTY );
        }
        String[] names = {instDir, LOGS_DIR, getLogFileName() };
        // Create an absolute log filename 
        return StringUtils.makeFilePath(names, false);
    }
         
    /**
     *  Creates the file and initialized MeteredStream and passes it on to
     *  Superclass (java.util.logging.StreamHandler). 
     */
    private void openFile( String fileName ) throws IOException {
        File file = new File( fileName );
        FileOutputStream fout = new FileOutputStream( fileName, true );
        BufferedOutputStream bout = new BufferedOutputStream( fout );
        meter = new MeteredStream( bout, file.length() ); 
        setOutputStream( meter );
    } 

    /**
     * Request Rotation called from Rotation Timer Task or LogMBean
     */
    void requestRotation( ) {
        synchronized( this ) {
            rotationRequested = true;
        }
    }

    /**
     * cleanup the history log file based on JVM system property "max_history_files".
     * 
     * If it is defined with valid number, we only keep that number of history logfiles;
     * If "max_history_files" is defined without value, then default that number to be 10;
     * If "max_history_files" is defined with value 0, no histry log file will
     * be keeped; and server.log is the only log file. 
     */
    public void cleanUpHistoryLogFiles() {
        String nStr = System.getProperty(LOGGING_MAX_HISTORY_FILES);
        if (nStr==null) return;

        int maxHistryFiles = 10;
        if (!"".equals(nStr)) {
            try {
                maxHistryFiles = Integer.parseInt(nStr);
            } catch (NumberFormatException e) {};
        }
        if (maxHistryFiles<0) return;

        File   dir  = new File(absoluteFileName).getParentFile();
        if (dir==null) return;

        File[] 	fset = dir.listFiles();
        ArrayList candidates = new ArrayList();
        for (int i=0; fset!=null && i<fset.length; i++) {
            if ( !logFileName.equals(fset[i].getName()) &&
                 fset[i].isFile() && 
                 fset[i].getName().startsWith(logFileName) ) {
                 candidates.add(fset[i].getAbsolutePath() );
            }
        }
        if (candidates.size() <= maxHistryFiles) return;

        Object[] pathes = candidates.toArray();
        java.util.Arrays.sort(pathes);
        try {
            for (int i=0; i<pathes.length-maxHistryFiles; i++) {
		new File((String)pathes[i]).delete();
            }
        } catch (Exception e) {
            new ErrorManager().error("FATAL ERROR: COULD NOT DELETE LOG FILE..", 
                                     e, ErrorManager.GENERIC_FAILURE );
        }
    }


    /**
     * A Simple rotate method to close the old file and start the new one
     * when the limit is reached.
     */
    private void rotate( ) {
        final FileandSyslogHandler thisInstance = this; 
        java.security.AccessController.doPrivileged(
            new java.security.PrivilegedAction() {
                public Object run( ) {
                    thisInstance.flush( );
                    thisInstance.close();
                    StringBuffer renamedFileName = null;
                    try {
                        File oldFile = new File( absoluteFileName );
                        renamedFileName = 
                            new StringBuffer( absoluteFileName + "_" );
                                logRotateDateFormatter.format( 
                                    new Date(), renamedFileName, 
                                    new FieldPosition( 0 ) );
                        File rotatedFile = new File( 
                            renamedFileName.toString() );
                        boolean renameSuccess = oldFile.renameTo( rotatedFile );
                        if( !renameSuccess ) {
                            // If we don't succeed with file rename which
                            // most likely can happen on Windows because
                            // of multiple file handles opened. We go through
                            // Plan B to copy bytes explicitly to a renamed 
                            // file.
                            planBLogRotate(absoluteFileName, 
                                renamedFileName.toString( ) ); 
                            String freshServerLogFileName = createFileName( );
                            // We do this to make sure that server.log
                            // contents are flushed out to start from a 
                            // clean file again after the rename..
                            FileOutputStream fo = 
                                new FileOutputStream( freshServerLogFileName );
                            fo.close( );
                        }
                        openFile( createFileName( ) );
                        LogFilter.setLogFile( new LogFile( absoluteFileName) ); 
                        // This will ensure that the log rotation timer
                        // will be restarted if there is a value set
                        // for time based log rotation
                        LogRotationTimer.getInstance( ).restartTimer( );

                        cleanUpHistoryLogFiles();
                    } catch( IOException ix ) {
                        new ErrorManager().error( 
                            "FATAL ERROR: COULD NOT OPEN LOG FILE..", ix,
                            ErrorManager.OPEN_FAILURE );
                    }
                    return null;
                }
            }
        );
    }


    /**
     *  This method is solely provided as a plan B for Windows. On Windows
     *  the file rename fails if there are open handles to the file. 
     *  We want to make sure that the log rotation succeeds in this case also.
     *  Basically we copy the contents of the file instead of renaming.
     */
    private void planBLogRotate( String originalFileName, 
        String renamedFileName )
    {
        FileOutputStream fo = null;
        FileInputStream fi = null;
        try {
            fo = new FileOutputStream( renamedFileName );
            fi = new FileInputStream( originalFileName );

            int BUF_SIZE = 4096;

            byte[] buffer = new byte[BUF_SIZE];
            int i = -1;
            do {
                i = fi.read( buffer );
                // Reached EOF. 
                if( i == -1 ) { break; }
                if( i == BUF_SIZE ) {
                    fo.write( buffer );
                } else {
                    // We have less number of bytes to read than the BUF_SIZE
                    // So, just create a temporary buffer with the exact
                    // number of bytes read and write that to the rotated
                    // file.
                    byte[] tempBuffer = new byte[i];
                    int j = 0;
                    for( j = 0; j < i; j++ ) {
                        tempBuffer[j] = buffer[j];
                    }
                    fo.write( tempBuffer );
                } 
            } while( true );
        } catch( Exception e ) {
           // If we fail in Plan B. Too bad, we should not
           // log a message here as it may lead to recursion.
        } finally {
            try {
                fo.close( );
                fi.close( );
            } catch( Exception e ) { }
        }
    }


    /**
     * Publishes the logrecord using the super class and checks to see
     * if a file rotation is required.
     */ 
    public synchronized void publish( LogRecord record ) {
        // This is provided to avoid the recursion. When Log Rotation
        // is called, there is a chance that any of the called routines
        // in the Log Rotation call path may log a message, which may
        // result in a recursion. Hence, we shall log those log messages
        // after the rotation is completed, so as to avoid the recursion.
        if( rotationInProgress ) {
	    pendingLogRecordList.addLast(record);
	    return;
	}
        // Important: There are issues with double instantiation of
        // FileandSyslogHandler if we move the createFile/openFile calls
        // to the constructor. For now, we will do this for the first 
        // publish call.
        if( meter == null ) {
            try {
                absoluteFileName = createFileName( );
                openFile( absoluteFileName );
            } catch( Exception e ) {
                throw new RuntimeException(
                    "Serious Error Couldn't open Log File" + e );
            }
        }
        super.publish( record );  
        flush( );
        if ( ( rotationRequested )  
        || ( ( limitForFileRotation > 0 )
        &&  ( meter.written >= limitForFileRotation ) ) )  
        {
            rotationInProgress = true;  
            // If we have written more than the limit set for the
            // file, or rotation requested from the Timer Task or LogMBean
            // start fresh with a new file after renaming the old file.
            rotate( );
            rotationInProgress = false; 
            rotationRequested = false;
	    while (pendingLogRecordList.size() != 0) {
		publish((LogRecord) pendingLogRecordList.removeFirst());
	    }
        } 
        
        if (mAMXLoggingHook != null) {
            mAMXLoggingHook.publish( record, getFormatter() );
        }
        
        int level = record.getLevel( ).intValue();
        // The processing here is to raise alarms if
        // alarm flag in domain.xml is on and the log level is SEVERE or
        // WARNING 
        if(( level != WARNING ) && (level != SEVERE)) {
            return;
        }
        
        //Any WARNING or SEVERE message is placed onto a singleton rolling 
        // error message list. This can be queried as part of a running 
        // server's status. This allows the last N error messages to be easily 
        // viewed without having to consult the log file.
        String logMessage = getFormatter().format(record);
        addRecentErrorMessage(logMessage);
        ErrorStatistics.singleton().updateStatistics(record); // error stats.
        
        if( logService == null ) { 
            logService = ServerLogManager.getLogService( );
        }
        // _REVISIT_: Deprecate Alarms flag
        if( logService == null ) return;
       
        // Raise an alarm if the logged message is a SEVERE or WARNING
        // message.
        if( record.getLevel( ).intValue() == WARNING ) {
            LogMBean.getInstance().raiseWarningAlarm( record );
        } else if( record.getLevel( ).intValue() == SEVERE ) {
            LogMBean.getInstance().raiseSevereAlarm( record );
        }
    }
    
    /**
     *Returns the AMXLoggingHook to use for this logger.
     *<p>
     *This method is here primarily so a subclass can override it if needed.
     *@return AMXLoggingHook to use
     */
    protected AMXLoggingHook createAMXLoggingHook() {
        return new AMXLoggingHook();
    }
    
    protected String getLogFileName() {
        return logFileName;
    }
}

