/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 * 
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, 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/CDDL+GPL.html
 * or glassfish/bootstrap/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
 * Sun designates this particular file as subject to the "Classpath" exception
 * as provided by Sun in the GPL Version 2 section of the License file that
 * accompanied this code.  If applicable, add the following below the License
 * Header, with the fields enclosed by brackets [] replaced by your own
 * identifying information: "Portions Copyrighted [year]
 * [name of copyright owner]"
 * 
 * Contributor(s):
 * 
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */
package com.sun.enterprise.server.logging.logviewer.backend;

import java.io.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.text.SimpleDateFormat;
import java.text.ParseException;
import java.util.Properties;
import java.util.Iterator;
import java.util.StringTokenizer;
import java.util.*;
import javax.management.Attribute;
import javax.management.AttributeList;

import com.sun.enterprise.server.logging.ModuleToLoggerNameMapper;
import com.sun.enterprise.util.StringUtils;
import com.sun.enterprise.util.SystemPropertyConstants;

/**
 * <p>
 * LogFilter will be used by Admin Too Log Viewer Front End to filter the
 * records. LogMBean delegates the getLogRecordsUsingQuery method to this
 * class static method.
 *
 * @AUTHOR: Hemanth Puttaswamy and Ken Paulsen
 */
public class LogFilter {

    // This is the name of the Results Attribute that we send out to the
    // Admin front end.
    private static final String RESULTS_ATTRIBUTE = "Results";


    private static final String NV_SEPARATOR = ";";

    /**
     *  The public method that Log Viewer Front End will be calling on.
     *  The query will be run on records starting from the fromRecord.
     *  If any of the elements for the query is null, then that element will
     *  not be used for the query.  If the user is interested in viewing only
     *  records whose Log Level is SEVERE and WARNING, then the query would
     *  look like:
     *
     *  fromDate = null, toDate = null, logLevel = WARNING, onlyLevel = false,
     *  listOfModules = null, nameValueMap = null.
     *
     *  @param  logFileName     The LogFile to use to run the query. If null
     *                          the current server.log will be used. This is
     *                          not the absolute file name, just the fileName
     *                          needs to be passed. We will use the parent 
     *                          directory of the previous server.log to
     *                          build the absolute file name. 
     *  @param  fromRecord      The location within the LogFile
     *  @param  next            True to get the next set of results, false to
     *                          get the previous set
     *  @param  forward         True to search forward through the log file
     *  @param  requestedCount  The # of desired return values
     *  @param  fromDate        The lower bound date
     *  @param  toDate          The upper bound date
     *  @param  logLevel        The minimum log level to display
     *  @param  onlyLevel       True to only display messsage for "logLevel"
     *  @param  listOfModules   List of modules to match
     *  @param  nameValueMap    NVP's to match
     *
     *  @return
     */
    public static AttributeList getLogRecordsUsingQuery(
        String logFileName, Long fromRecord, Boolean next, Boolean forward,
        Integer requestedCount, Date fromDate, Date toDate,
        String logLevel, Boolean onlyLevel, List listOfModules,
        Properties nameValueMap) 
    {
        LogFile logFile = null;
        if( ( logFileName != null )
          &&( logFileName.length() != 0 ) ) 
        {
            logFile = getLogFile( logFileName );
        } else {
            logFile = getLogFile( );
        } 
        boolean forwd = (forward == null) ? true : forward.booleanValue();
        boolean nxt = (next == null) ? true : next.booleanValue();
        long reqCount = (requestedCount == null) ?
            logFile.getIndexSize() : requestedCount.intValue();
        long startingRecord;
        if (fromRecord == null) {
            // In this case next/previous (before/after) don't mean much since
            // we don't have a reference record number.  So set before/after
            // according to the direction.
            nxt = forwd;

            // We +1 for reverse so that we see the very end of the file (the
            // query will not go past the "startingRecord", so we have to put
            // it after the end of the file)
            startingRecord = forwd ?
                (-1) :((logFile.getLastIndexNumber()+1)*logFile.getIndexSize());
        } else {
            startingRecord = fromRecord.longValue();
            if (startingRecord < -1) {
                
                throw new IllegalArgumentException(
                    "fromRecord must be greater than 0!");
            }
        }

        // TO DO: If the fromRecord count is zero and the fromDate entry is
        // non-null, then the system should take advantage of file Indexing.
        // It should move the file position to the marker where the DateTime
        // query matches.
        try {
            return fetchRecordsUsingQuery(logFile, startingRecord, nxt, forwd,
                reqCount, fromDate, toDate, logLevel,
                onlyLevel.booleanValue(), listOfModules, nameValueMap);
        } catch (Exception ex) {
            System.err.println( "Exception in fetchRecordsUsingQuer.." + ex );       
            // FIXME: Handle this correctly...
            throw new RuntimeException(ex);
        }
    }


    /**
     *  Internal method that will be called from getLogRecordsUsingQuery()
     */
    protected static AttributeList fetchRecordsUsingQuery(
        LogFile logFile, long startingRecord, boolean next, boolean forward, 
        long requestedCount, Date fromDate, Date toDate, String logLevel, 
        boolean onlyLevel, List listOfModules, Properties nameValueMap) 
    {
        // If !next, then set to search in reverse
        boolean origForward = forward;
        if (next) {
            startingRecord++;
            forward = true;
        } else {
            forward = false;
        }

        // Setup forward/reverse stuff
        int inc = 1;
        int start = 0;  // depends on length of results (for reverse)
        int end = -1;   // depends on length of results (for forward)
        long searchChunkIncrement = requestedCount;
        if (!forward) {
            inc = -1;
            // Move back to find records before the startingRecord
            // -1 because we still want to see the starting record (only if in
            // "next" mode)
            startingRecord -=
                ((next) ? (searchChunkIncrement-1) : (searchChunkIncrement));
            if (startingRecord < 0) {
                // Don't go past the original startingRecord
                searchChunkIncrement += startingRecord;
                startingRecord = 0;
            }
        }

        // Make sure the module names are correct
        //updateModuleList(listOfModules);

        // Keep pulling records to search through until we get enough matches
        List results = new ArrayList();
        List records = null;
        LogFile.LogEntry entry = null;
        while (results.size() < requestedCount) {
            // The following will always return unfiltered forward records
            records = logFile.getLogEntries(
                startingRecord, searchChunkIncrement);
            if (records == null) {
                break;
            }

            // Determine end/start
            if (forward) {
                end = records.size();
            } else {
                start = records.size()-1;
            }

            // Loop through the records, filtering and storing the matches
            for (int count=start; 
                (count != end) && (results.size() < requestedCount); 
                count += inc) 
            {
                entry = (LogFile.LogEntry)records.get(count);
                if (allChecks(entry, fromDate, toDate, logLevel, onlyLevel,
                        listOfModules, nameValueMap)) {
                    results.add(entry);
                }
            }

            // Update startingRecord / searchChunkIncrement & check for finish
            if (forward) {
                // If the record size is smaller than requested, then there
                // are no more records.
                if (records.size() < searchChunkIncrement) {
                    break;
                }

                // Get starting record BEFORE updating searchChunkIncrement to
                // skip all the records we already saw
                startingRecord += searchChunkIncrement*inc;
                searchChunkIncrement = requestedCount-results.size();
            } else {
                // If we already searched from 0, then there are no more
                if (startingRecord == 0) {
                    break;
                }

                // Get starting record AFTER updating searchChunkIncrement
                searchChunkIncrement = requestedCount-results.size();
                startingRecord += searchChunkIncrement*inc;
                if (startingRecord < 1) {
                    searchChunkIncrement += startingRecord;
                    startingRecord = 0;
                }
            }
        }

        // Deal with previous&forward or next&reverse
        if (next ^ origForward) {
            List reversedResults = new ArrayList();
            // Reverse the results
            for (int count=results.size()-1; count>-1; count--) {
                reversedResults.add(results.get(count));
            }
            results = reversedResults;
        }

        // Return the matches.  If this is less than requested, then there are
        // no more.
        return convertResultsToTheStructure( results );
    }

    /**
     *  This method converts the results to the appropriate structure for
     *  LogMBean to return to the Admin Front End.
     *
     *  AttributeList Results contain 2 attributes 
     *
     *  Attribute 1: Contains the Header Information, that lists out all the
     *               Field Names and Positions
     *  Attribute 2: Contains the actual Results, Each Log record is an entry
     *               of this result. The LogRecord itself is an ArrayList of
     *               all fields.
     *
     */
    private static AttributeList convertResultsToTheStructure( List results ) {
        if( results == null ) { return null; }
        AttributeList resultsInTemplate = new AttributeList( );
        resultsInTemplate.add( LogRecordTemplate.getHeader( ) );
        Iterator iterator = results.iterator( ) ; 
        ArrayList listOfResults = new ArrayList( ); 
        Attribute resultsAttribute =  new Attribute( RESULTS_ATTRIBUTE,
            listOfResults );
        resultsInTemplate.add( resultsAttribute );
        while( iterator.hasNext() ) { 
            LogFile.LogEntry entry = (LogFile.LogEntry) iterator.next();
            ArrayList logRecord = new ArrayList( );
            logRecord.add( new Long(entry.getRecordNumber()) );
            logRecord.add( entry.getLoggedDateTime() );
            logRecord.add( entry.getLoggedLevel() );
            logRecord.add( entry.getLoggedProduct() );
            logRecord.add( entry.getLoggedLoggerName() );
            logRecord.add( entry.getLoggedNameValuePairs() );
            logRecord.add( entry.getMessageId() );
            logRecord.add( entry.getLoggedMessage() );
            listOfResults.add( logRecord );
        }
        return resultsInTemplate;
    }      
   
    


    /**
     *  This provides access to the LogFile object.
     */
    public static LogFile getLogFile() {
        return _logFile;
    }

    /**
     * This fetches or updates logFileCache entries.
     * 
     * _REVISIT_: We may want to limit the entries here as each logFile
     * takes up so much of memory to maintain indexes
     */
    public static LogFile getLogFile( String fileName ) {
        // No need to check for null or zero length string as the
        // test is already done before.
        String logFileName = fileName.trim( );
        LogFile logFile = (LogFile) logFileCache.get( fileName );
        String parent = null;
        if( logFile == null ) {
            try { 
                // First check if the fileName provided is an absolute filename
                // if yes, then we don't have to construct the parent element
                // path with the parent.
                if( new File( fileName ).exists( ) ) {
                    logFile = new LogFile( fileName );
                    logFileCache.put( fileName, logFile );
                    return logFile;
                }
                                                                                
                // If the absolute path is not provided, the burden of
                // constructing the parent path falls on us. We try
                // using the default parent path used for the current LogFile.
                if( getLogFile() != null ) {
                    parent = new File( 
                        getLogFile().getLogFileName() ).getParent( );     
                } else {
                    String[] logPath = { System.getProperty( 
                        SystemPropertyConstants.INSTANCE_ROOT_PROPERTY ), 
                        "logs"};
                    parent = StringUtils.makeFilePath( logPath, false );
                }
            } catch( Exception e ) {
                System.err.println( "Exception " + e + 
                    "thrown in Logviewer backend" );
            }
            if( parent != null ) {
                // Just use the parent directory from the other server.log
                // file.
                String[] logFileNameParts = { parent, logFileName }; 
                logFileName = StringUtils.makeFilePath( 
                    logFileNameParts , false );
            } 
            logFile = new LogFile( logFileName );
            logFileCache.put( fileName, logFile ); 
        }
        return logFile;
    }


    /**
     *
     */
    public static synchronized void setLogFile(LogFile logFile) {
        _logFile=logFile;
    }


    /**
     *  Utility method to replace the Module Names with their actual logger
     *  names.
     */
    protected static void updateModuleList(List listOfModules) {
        if (listOfModules == null) {
            return;
        }
        Iterator iterator = listOfModules.iterator();
        int index = 0;
        while (iterator.hasNext()) {
            String[] loggerNames = ModuleToLoggerNameMapper.getLoggerNames(
                ((String)iterator.next()).trim());
            if (loggerNames!=null && loggerNames.length>0) {
               listOfModules.set(index, loggerNames[0]);  //todo: support multiple loggers per module
            }
            index++;
        }
    }


    /**
     *  This method accepts the first line of the Log Record and checks
     *  to see if it matches the query.
     */
    protected static boolean allChecks(LogFile.LogEntry entry,
            Date fromDate, Date toDate, String queryLevel, boolean onlyLevel,
            List listOfModules, Properties nameValueMap) {

        if ((!dateTimeCheck(entry.getLoggedDateTime(), fromDate, toDate)) 
           || (!levelCheck(entry.getLoggedLevel(), queryLevel, onlyLevel)) 
           || (!moduleCheck(entry.getLoggedLoggerName(), listOfModules)) 
           || (!nameValueCheck(entry.getLoggedNameValuePairs(), nameValueMap)))
        {
            return false;
        }

        return true;
    }


    protected static boolean dateTimeCheck(Date loggedDateTime,
            Date fromDateTime, Date toDateTime) 
    {
        if ((fromDateTime == null) || (toDateTime == null)) {
            // If user doesn't specify fromDate and toDate, then S/He is
            // not interested in DateTime filter
            return true;
        }
        // Now do a range check
        if (!(loggedDateTime.before(fromDateTime) ||
	        loggedDateTime.after(toDateTime))) { 
            return true;
        }

        return false;
    }


    protected static boolean levelCheck(
        final String loggedLevel,
        final String queryLevelIn,
        final boolean isOnlyLevelFlag) 
    {
        // If queryLevel is null, that means user is not interested in
        // running the query on the Log Level field.
        if (queryLevelIn == null) {
            return true;
        }
        final String queryLevel = queryLevelIn.trim();
        
        if (isOnlyLevelFlag) {
            // This means the user is interested in seeing log messages whose
            // log level is equal to what is specified
            if (loggedLevel.equals(queryLevel)) {
                return true;
            }
        } else {
// FIXME: rework this...
            for (int idx=0; idx<LOG_LEVELS.length; idx++) {
                if (loggedLevel.equals(LOG_LEVELS[idx])) {
                    return true;
                }
                if (LOG_LEVELS[idx].equals(queryLevel)) {
                    break;
                }
            }
        }
        return false;
    }

    protected static boolean moduleCheck(String loggerName, List modules) {
        if ((modules == null) || (modules.size() == 0)) {
            return true;
        }

        Iterator iterator = modules.iterator();
        while (iterator.hasNext()) {
            if (loggerName.startsWith((String)iterator.next())) {
                return true;
            }
        }
        return false;
    }

    protected static boolean nameValueCheck(String loggedNameValuePairs,
            Properties queriedNameValueMap) {
        if (queriedNameValueMap == null) {
            return true;
        }
        if (loggedNameValuePairs == null) {
            // We didn't match the name values...
            return false;
        }
        StringTokenizer nvListTokenizer = 
            new StringTokenizer( loggedNameValuePairs, NV_SEPARATOR );
        while( nvListTokenizer.hasMoreTokens( ) ) {
            String nameandvalue = nvListTokenizer.nextToken( );
            StringTokenizer nvToken = new StringTokenizer( nameandvalue, "=" );
            if (nvToken.countTokens() < 2)
                continue;
            String loggedName = nvToken.nextToken( );
            String loggedValue = nvToken.nextToken( );

            // Reset the iterator to start from the first entry AGAIN
            // FIXME: Is there any other cleaner way to reset the iterator 
            // position to zero than recreating a new iterator everytime
            Iterator queriedNameValueMapIterator = 
                queriedNameValueMap.entrySet().iterator( );

            while( queriedNameValueMapIterator.hasNext( ) ) {
                Map.Entry entry = 
                    (Map.Entry) queriedNameValueMapIterator.next();
                if(entry.getKey().equals( loggedName ) ) {
                    Object value = entry.getValue( );
                    // We have a key with multiple values to match.
                    // This will happen if the match condition is like
                    // _ThreadID=10 or _ThreadID=11 
                    // _REVISIT_: There is an opportunity to improve performance
                    // for this search.
                    Iterator iterator = ((java.util.List) value).iterator( );
                    while( iterator.hasNext( ) ) {
                        if( ((String)iterator.next()).equals( 
                            loggedValue ) ) 
                        {
                            return true;  
                        } 
                    }
                }
            }
        }
        return false;
    }


    protected static final String[] LOG_LEVELS = { "SEVERE", "WARNING",
        "INFO", "CONFIG", "FINE", "FINER", "FINEST" };

    private static SimpleDateFormat SIMPLE_DATE_FORMAT =
        new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");

    private static String[] serverLogElements = 
        {System.getProperty("com.sun.aas.instanceRoot"), "logs", "server.log"};

    private static LogFile _logFile = new LogFile( 
        StringUtils.makeFilePath( serverLogElements, false ) );

    private static Hashtable logFileCache = new Hashtable( );
}
