/**
 * JOnAS
 * Copyright (C) 2010 Bull S.A.S.
 * Contact: jonas-team@ow2.org
 *
 * 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 any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.audit.internal.logger;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

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

import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;
import org.ow2.jonas.audit.internal.logger.api.IAuditComponentListener;
import org.ow2.jonas.audit.internal.logger.api.IAuditLogListener;
import org.ow2.jonas.audit.internal.logger.pool.DatedFileLogPool;
import org.ow2.jonas.audit.internal.logger.util.AuditLogReportHelper;
import org.ow2.jonas.audit.internal.logger.util.AuditLogReportHelper.EncodeResult;
import org.ow2.jonas.jmx.JmxService;
import org.ow2.jonas.lib.bootstrap.JProp;
import org.ow2.util.auditreport.api.AuditorJMXObjectNames;
import org.ow2.util.auditreport.impl.GenericAuditReport;
import org.ow2.util.log.Log;
import org.ow2.util.log.LogFactory;

/**
 * The audit log component.
 * @author Vincent Michaud
 */
public class AuditLogService implements IAuditLogListener {

    /**
     * Determine if the component receive audits from the EJB audit component.
     */
    private boolean useEjbAuditComponent = true;

    /**
     * Determine if the component receive audits from the Web audit component.
     */
    private boolean useWebAuditComponent = true;

    /**
     * Determine if the component receive audits from the JNDI audit component.
     */
    private boolean useJNDIAuditComponent = true;

    /**
     * Determine if the component receive audits from the Jaxws audit component.
     */
    private boolean useJaxwsAuditComponent = true;

    /**
     * Determine if a console logger is used.
     */
    private boolean consoleLogger = false;

    /**
     * Determine if a file logger is used.
     */
    private boolean fileLogger = true;

    /**
     * The path of the logs directory.
     */
    private String logDirectoryPath = DEFAULT_LOG_PATH;

    /**
     * The pattern for the name of log file.
     */
    private String logFilenamePattern = DEFAULT_LOG_FILENAME_PATTERN;

    /**
     * The index where the date is inserted in the name of log files.
     */
    private int insertDateIndex = DEFAULT_INSERT_DATE_INDEX;

    /**
     * Determine the maximum number of log files in the pool.
     */
    private int maxLogFiles = DatedFileLogPool.UNLIMITED;

    /**
     * The JMX name of the audit EJB component.
     */
    private ObjectName jmxEjbComponentPatternName = null;

    /**
     * The JMX name of the audit Web component.
     */
    private ObjectName jmxWebComponentPatternName = null;

    /**
     * The JMX name of the audit JNDI component.
     */
    private ObjectName jmxJNDIComponentPatternName = null;

    /**
     * The JMX name of the audit Jaxws Endpoint component.
     */
    private ObjectName jmxJaxwsEndpointComponentPatternName = null;

    /**
     * Listener for the EJB audit component.
     */
    private AuditComponentListener auditEjbListener = null;

    /**
     * Listener for the Web audit component.
     */
    private AuditComponentListener auditWebListener = null;

    /**
     * Listener for the JNDI audit component.
     */
    private AuditComponentListener auditJNDIListener = null;

    /**
     * Listener for the Jaxws Endpoint audit component.
     */
    private AuditComponentListener auditJaxwsEndpointListener = null;

    /**
     * The pool of dated log files.
     */
    private DatedFileLogPool logPool = null;

    /**
     * The default path where are stored logs.
     */
    private static final String DEFAULT_LOG_PATH = JProp.getJonasBase() + File.separator + "logs" + File.separator
            + "audit-logs";

    /**
     * The default pattern for the name of log file.
     */
    private static final String DEFAULT_LOG_FILENAME_PATTERN = "jonas-.log";

    /**
     * The default index where the date is inserted in the name of log files.
     */
    private static final int DEFAULT_INSERT_DATE_INDEX = 6;

    /**
     * JMX Service.
     */
    private JmxService jmxService = null;

    /**
     * ObjectName.
     */
    private ObjectName objectName = null;

    /**
     * Waiter.
     */
    private AuditLogJMXWaiter waiter = null;


    /**
     * List of reports.
     */
    private LinkedList<NodeReport> nodeReports = new LinkedList<NodeReport>();


    /**
     * Default maximum nodes in memory.
     */
    private static final long DEFAULT_MAX_NODE_MEMORY = 1000L;

    /**
     * Nodes that can be kept in memory (these are the root nodes).
     */
    private long maxNodesInMemory = DEFAULT_MAX_NODE_MEMORY;

    /**
     * Init the component.
     * @throws AuditLogServiceException If connection with audit component can
     *         not be etablished
     */
    public void start() throws AuditLogServiceException {
        if (this.maxLogFiles < 0) {
            this.maxLogFiles = DatedFileLogPool.UNLIMITED;
        }

        if (this.insertDateIndex < 0 || this.insertDateIndex >= this.logFilenamePattern.length()) {
            this.insertDateIndex = 0;
        }

        try {
            this.logPool = new DatedFileLogPool(this.logDirectoryPath, AuditLogService.class.getName(),
                    this.logFilenamePattern, this.insertDateIndex, "%m%n", this.maxLogFiles, this.fileLogger,
                    this.consoleLogger);
        } catch (IOException e) {
            throw new AuditLogServiceException("Cannot create log file", e);
        }

        try {
            jmxEjbComponentPatternName = new ObjectName(jmxService.getDomainName()
                    + AuditorJMXObjectNames.EJBAUDITOR_TYPE_COMPONENT + ",*");
        } catch (Exception e) {
            throw new AuditLogServiceException("Cannot build ObjectName", e);
        }

        try {
            jmxWebComponentPatternName = new ObjectName(jmxService.getDomainName()
                    + AuditorJMXObjectNames.WEBAUDITOR_TYPE_COMPONENT + ",*");
        } catch (Exception e) {
            throw new AuditLogServiceException("Cannot build ObjectName", e);
        }

        try {
            jmxJNDIComponentPatternName = new ObjectName(jmxService.getDomainName()
                    + AuditorJMXObjectNames.JNDIAUDITOR_TYPE_COMPONENT + ",*");
        } catch (Exception e) {
            throw new AuditLogServiceException("Cannot build ObjectName", e);
        }

        try {
            jmxJaxwsEndpointComponentPatternName = new ObjectName(jmxService.getDomainName()
                    + AuditorJMXObjectNames.BASE_AUDIT_COMPONENT + ",auditorType=wsendpoint,*");
        } catch (Exception e) {
            throw new AuditLogServiceException("Cannot build ObjectName", e);
        }

        this.auditEjbListener = new AuditComponentListener(this.jmxService.getJmxServer());
        this.auditEjbListener.addLogListener(this);

        this.auditWebListener = new AuditComponentListener(this.jmxService.getJmxServer());
        this.auditWebListener.addLogListener(this);

        this.auditJNDIListener = new AuditComponentListener(this.jmxService.getJmxServer());
        this.auditJNDIListener.addLogListener(this);

        this.auditJaxwsEndpointListener = new AuditComponentListener(this.jmxService.getJmxServer());
        this.auditJaxwsEndpointListener.addLogListener(this);

        // ObjectName
        try {
            this.objectName = new ObjectName(jmxService.getDomainName() + AuditorJMXObjectNames.LOGGERAUDITOR_TYPE_COMPONENT
                    + ",name=JOnAS");
        } catch (MalformedObjectNameException e) {
            throw new AuditLogServiceException("Cannot build the objectname", e);
        } catch (NullPointerException e) {
            throw new AuditLogServiceException("Cannot build the objectname", e);
        }

        // Load descriptors
        jmxService.loadDescriptors(AuditLogService.class.getPackage().getName(), AuditLogService.class.getClassLoader());

        // Register Mbean
        try {
            jmxService.registerModelMBean(this, objectName);
        } catch (Exception e) {
            throw new AuditLogServiceException("Cannot register MBean", e);
        }

        // Wait the availability of the JNDI names before starting the audit
        waiter = new AuditLogJMXWaiter(jmxService.getJmxServer());
        waiter.addPatternEntry(new PatternEntry(auditEjbListener, jmxEjbComponentPatternName));
        waiter.addPatternEntry(new PatternEntry(auditWebListener, jmxWebComponentPatternName));
        waiter.addPatternEntry(new PatternEntry(auditJNDIListener, jmxJNDIComponentPatternName));
        waiter.addPatternEntry(new PatternEntry(auditJaxwsEndpointListener, jmxJaxwsEndpointComponentPatternName));

        new Thread(waiter).start();

    }

    /**
     * Enable or disable the EJB logger.
     * @param enable True for enable the logger, false otherwise
     * @throws AuditLogServiceException If connection with audit component can
     *         not be etablished
     */
    private void manageEJBLogger(final boolean enable) throws AuditLogServiceException {
        if (this.useEjbAuditComponent) {
            if (!enable) {
                this.auditEjbListener.unregister();
            }
        }
    }

    /**
     * Enable or disable the JNDI logger.
     * @param enable True for enable the logger, false otherwise
     * @throws AuditLogServiceException If connection with audit component can
     *         not be etablished
     */
    private void manageJNDILogger(final boolean enable) throws AuditLogServiceException {
        if (this.useJNDIAuditComponent) {
            if (!enable) {
                this.auditJNDIListener.unregister();
            }
        }
    }

    /**
     * Enable or disable the Web logger.
     * @param enable True for enable the logger, false otherwise
     * @throws AuditLogServiceException If connection with audit component can
     *         not be etablished
     */
    private void manageWebLogger(final boolean enable) throws AuditLogServiceException {
        if (this.useWebAuditComponent) {
            if (!enable) {
                this.auditWebListener.unregister();
            }
        }
    }

    /**
     * Enable or disable the Jaxws logger.
     * @param enable True for enable the logger, false otherwise
     * @throws AuditLogServiceException If connection with audit component can
     *         not be etablished
     */
    private void manageJaxwsLogger(final boolean enable) throws AuditLogServiceException {
        if (this.useJaxwsAuditComponent) {
            if (!enable) {
                this.auditJaxwsEndpointListener.unregister();
            }
        }
    }

    /**
     * Stop the component.
     * @throws AuditLogServiceException If connection with audit component can
     *         not be stopped
     */
    public void stop() throws AuditLogServiceException {
        waiter.end();
        jmxService.unregisterModelMBean(objectName);

        manageEJBLogger(false);
        manageWebLogger(false);
        manageJNDILogger(false);
        manageJaxwsLogger(false);
    }

    /**
     * Enable the EJB logger.
     * @throws AuditLogServiceException If connection with audit component can
     *         not be etablished
     */
    public void enableEJBLogger() throws AuditLogServiceException {
        this.useEjbAuditComponent = true;
        manageEJBLogger(true);
    }

    /**
     * Disable the EJB logger.
     * @throws AuditLogServiceException If connection with audit component can
     *         not be etablished
     */
    public void disableEJBLogger() throws AuditLogServiceException {
        manageEJBLogger(false);
        this.useEjbAuditComponent = false;
    }

    /**
     * Enable the Web logger.
     * @throws AuditLogServiceException If connection with audit component can
     *         not be etablished
     */
    public void enableWebLogger() throws AuditLogServiceException {
        this.useWebAuditComponent = true;
        manageWebLogger(true);
    }

    /**
     * Disable the Web logger.
     * @throws AuditLogServiceException If connection with audit component can
     *         not be etablished
     */
    public void disableWebLogger() throws AuditLogServiceException {
        manageWebLogger(false);
        this.useWebAuditComponent = false;
    }

    /**
     * Determine the action performed when the audit report is received.
     * @param typeAudit Type of the log
     * @param encodedAuditReport The encoded audit report
     */
    public synchronized void onAuditReceived(final String typeAudit, final String encodedAuditReport) {
        // Do nothiing for Attribute change notifications, etc.
        if (typeAudit != null && typeAudit.startsWith("jmx.attribute")) {
            return;
        }

        // Get report
        GenericAuditReport report = AuditLogReportHelper.decodeAuditReport(typeAudit, encodedAuditReport);

        // Get Formatted message
        String formatedMessage = AuditLogReportHelper.getLogdecodeAuditReport(typeAudit, encodedAuditReport);

        // Get key ID
        String keyID = report.getKeyID();

        // Handle key (if present)
        if (keyID != null) {

            // Cleanup if required
            if (nodeReports.size() > maxNodesInMemory) {
                nodeReports.removeFirst();
            }

            // Get parent and child part
            String[] keyIDParts = keyID.split(":");
            String parentID = keyIDParts[0];
            String localID = keyIDParts[1];

            // Parent is here ?
            String parentIDHash = null;
            Integer parentIDCount = null;
            if (parentID != null && parentID.length() > 0) {
                String[] parentIDParts = parentID.split("/");
                parentIDHash = parentIDParts[0];
                parentIDCount = Integer.valueOf(parentIDParts[1]);
            }

            // Local Hash and local count
            String[] localIDParts = localID.split("/");
            String localIDHash = localIDParts[0];
            Integer localIDCount = Integer.valueOf(localIDParts[1]);

            // Parent ?
            NodeReport reportNode = null;
            if (parentIDHash != null) {

                // There is a parent, needs to search the parent hash in the
                // tree
                NodeReport parentNode = search(parentIDHash, true);

                // The parent is not yet here
                if (parentNode == null) {
                    // Create this parent node at root level but flag it as
                    // a missing parent
                    parentNode = new NodeReport(parentIDHash, false);
                    parentNode.setMissingParent(true);
                    nodeReports.add(parentNode);
                }

                // The child hash is present or not?
                TreeMap<Integer, NodeReport> treeMap = parentNode.getTreeMap();
                reportNode = treeMap.get(parentIDCount);
                // N/A so create a node which is not a leaf
                if (reportNode == null) {

                    // add this node
                    reportNode = new NodeReport(localIDHash, false);
                    treeMap.put(parentIDCount, reportNode);

                    // Try to see if this node is also present at the root level
                    // (A node created before the right parent)
                    NodeReport existingRootLevelNode = search(localIDHash, false);
                    // Node is present and its parent was missing ?
                    if (existingRootLevelNode != null && existingRootLevelNode.isMissingParent()) {
                        // Needs to delete the node at the root level and to
                        // move existing elements to the current tree node
                        TreeMap<Integer, NodeReport> map = existingRootLevelNode.getTreeMap();
                        Iterator<Map.Entry<Integer, NodeReport>> iterator = map.entrySet().iterator();
                        while (iterator.hasNext()) {
                            Map.Entry<Integer, NodeReport> entry = iterator.next();
                            reportNode.getTreeMap().put(entry.getKey(), entry.getValue());
                        }
                        nodeReports.remove(existingRootLevelNode);
                    }

                }
            } else {
                // No parent, search the local hash
                reportNode = searchOrBuild(localIDHash);
            }

            // localID count == 0 means that this is the description
            if (localIDCount == 0) {
                // The node is now there, remove the flag if present
                if (reportNode.isMissingParent()) {
                    reportNode.setMissingParent(false);
                }
                reportNode.setReport(report);
                reportNode.setDetails(formatedMessage);
            } else {
                // Now, add the node with its localID
                NodeReport nodeChild = new NodeReport(report.getKeyID(), true);
                reportNode.getTreeMap().put(localIDCount, nodeChild);
            }
        }


        Logger logger = this.logPool.getLogger();
        if (logger != null) {
            logger.log(BasicLevel.INFO, formatedMessage);
        }

    }

    /**
     * Search or build a node for the given name.
     * @param name the node name
     * @return the built node
     */
    private NodeReport searchOrBuild(final String name) {
        NodeReport foundNode = search(name, true);
        if (foundNode == null) {
            foundNode = new NodeReport(name, false);
            nodeReports.add(foundNode);
        }

        return foundNode;

    }

    /**
     * Search or build a node for the given name.
     * @param id the node's id
     * @param inDepth search in depth or limit the search at the first level
     * @return the built node
     */
    protected NodeReport search(final String id, final boolean inDepth) {
        Iterator<NodeReport> itNode = nodeReports.iterator();
        NodeReport foundNode = null;
        while (itNode.hasNext() && foundNode == null) {
            if (inDepth) {
                foundNode = searchNode(id, itNode.next());
            } else {
                NodeReport tmpNode = itNode.next();
                if (id.equals(tmpNode.getID())) {
                    foundNode = tmpNode;
                }
            }
        }
        return foundNode;

    }

    /**
     * Search a node with the given name starting from the given root node.
     * @param id the id of the node to search
     * @param rootNode the starting root node for the search
     * @return the corresponding node
     */
    protected NodeReport searchNode(final String id, final NodeReport rootNode) {
        if (rootNode == null) {
            return null;
        }

        // Matching node
        if (id.equals(rootNode.getID())) {
            return rootNode;
        }


        // else search on each subnode
        TreeMap<Integer, NodeReport> map = rootNode.getTreeMap();
        NodeReport matchingNode = null;
        Set<Map.Entry<Integer, NodeReport>> entries = map.entrySet();
        Iterator<Map.Entry<Integer, NodeReport>> iterator = entries.iterator();
        while (matchingNode == null && iterator.hasNext()) {
            Map.Entry<Integer, NodeReport> entry = iterator.next();
            matchingNode = searchNode(id, entry.getValue());
        }

        return matchingNode;
    }


    /**
     * Count the number of log files in the pool.
     * @return The number of log files in the pool
     */
    public int countLogFiles() {
        return this.logPool.countLogFiles();
    }

    /**
     * Get all audit reports for a specific date. Audit reports can be decode
     * with the ObjectEncoder.
     * @param year The year of the date
     * @param month The month of the date
     * @param day The day of the date
     * @return An array of encoded audit reports. The array can contains 'null'
     *         if the class of the audit report is not found in the classpath.
     *         Return null if there is not any audit report found
     */
    public String[] getAuditReports(final int year, final int month, final int day) {
        File file = this.logPool.findFile(year, month, day);
        if (file != null) {
            EncodeResult result = AuditLogReportHelper.encodeLogMessages(file, 0);
            if (result != null) {
                return result.getEncodedAuditReports();
            }
        }
        return null;
    }

    /**
     * Get the last stored audit reports. Audit reports can be decode with the
     * ObjectEncoder.
     * @param number The number of audit reports
     * @return An array of encoded audit reports with size less or equal to
     *         'number'. The array can contains 'null' if the class of the audit
     *         report is not found in the classpath. Return null if the
     *         specified number of audit report is not positive. Return null if
     *         there is not any audit report found
     */
    public String[] getLastAuditReports(final int number) {
        if (number > 0) {
            File[] files = this.logPool.listLogFiles();
            if (files != null) {
                LinkedList<String[]> listEncodedReports = new LinkedList<String[]>();
                EncodeResult encodeResult;
                String[] encodedReports;
                int i, totalReports = 0;

                for (i = files.length - 1; i >= 0 && totalReports < number; i--) {
                    encodeResult = AuditLogReportHelper.encodeLogMessages(files[i], number - totalReports);
                    if (encodeResult != null) {
                        encodedReports = encodeResult.getEncodedAuditReports();
                        totalReports += encodedReports.length;
                        listEncodedReports.addFirst(encodedReports);
                    }
                }
                return collectAuditReports(listEncodedReports, totalReports);
            }
        }
        return null;
    }

    /**
     * Create an array of encoded audits report with a list of audits reports.
     * Audit reports can be decode with the ObjectEncoder.
     * @param listEncodedReports The list of audits reports
     * @param totalReports The total number of audit reports contained in the
     *        list
     * @return An array of encoded audit reports with size equal to
     *         'totalReports'. The array can contains 'null' if the class of the
     *         audit report is not found in the classpath
     */
    private String[] collectAuditReports(final LinkedList<String[]> listEncodedReports, final int totalReports) {
        String[] encodedReports = new String[totalReports];
        int i = 0;
        for (String[] auditReports : listEncodedReports) {
            for (String auditReport : auditReports) {
                encodedReports[i++] = auditReport;
            }
        }
        return encodedReports;
    }

    /**
     * Get all audit reports before or after a specific date. If an audit report
     * has been created at the specified date, it is not included in the result.
     * Audit reports can be decode with the ObjectEncoder.
     * @param year The year of the date
     * @param month The month of the date
     * @param day The day of the date
     * @param old If true, audit reports older than the specified date are
     *        listed otherwise, audit reports more recent than the specified
     *        filename are listed
     * @return An array of encoded audit reports. The array can contains 'null'
     *         if the class of the audit report is not found in the classpath.
     *         Return null if there is not any audit report found
     */
    private String[] getAuditReports(final int year, final int month, final int day, final boolean old) {
        File[] files = this.logPool.listLogFiles(year, month, day, old, false);
        if (files != null) {
            LinkedList<String[]> listEncodedReports = new LinkedList<String[]>();
            EncodeResult encodeResult;
            String[] encodedReports;
            int totalReports = 0;

            for (File file : files) {
                encodeResult = AuditLogReportHelper.encodeLogMessages(file, 0);
                if (encodeResult != null) {
                    encodedReports = encodeResult.getEncodedAuditReports();
                    totalReports += encodedReports.length;
                    listEncodedReports.add(encodedReports);
                }
            }
            return collectAuditReports(listEncodedReports, totalReports);
        }
        return null;
    }

    /**
     * Get all audit reports before a specific date. If an audit report has been
     * created at the specified date, it is not included in the result. Audit
     * reports can be decode with the ObjectEncoder.
     * @param year The year of the date
     * @param month The month of the date
     * @param day The day of the date
     * @return An array of encoded audit reports. The array can contains 'null'
     *         if the class of the audit report is not found in the classpath.
     *         Return null if there is not any audit report found
     */
    public String[] getOldAuditReports(final int year, final int month, final int day) {
        return getAuditReports(year, month, day, true);
    }

    /**
     * Get all audit reports after a specific date. If an audit report has been
     * created at the specified date, it is not included in the result. Audit
     * reports can be decode with the ObjectEncoder.
     * @param year The year of the date
     * @param month The month of the date
     * @param day The day of the date
     * @return An array of encoded audit reports. The array can contains 'null'
     *         if the class of the audit report is not found in the classpath.
     *         Return null if there is not any audit report found
     */
    public String[] getRecentAuditReports(final int year, final int month, final int day) {
        return getAuditReports(year, month, day, false);
    }

    /**
     * Get all audit reports before or after a specific hour. If an audit report
     * has been created at the specified hour, it is not included in the result.
     * Audit reports can be decode with the ObjectEncoder.
     * @param timestamp The hour
     * @param old If true, audit reports older than the specified date are
     *        listed otherwise, audit reports more recent than the specified
     *        filename are listed
     * @return An array of encoded audit reports. The array can contains 'null'
     *         if the class of the audit report is not found in the classpath.
     *         Return null if there is not any audit report found
     */
    private String[] getAuditReports(final long timestamp, final boolean old) {
        Calendar cal = new GregorianCalendar();
        cal.setTimeInMillis(timestamp);
        int year = cal.get(Calendar.YEAR);
        int month = cal.get(Calendar.MONTH) + 1;
        int day = cal.get(Calendar.DAY_OF_MONTH);

        File tsFile = this.logPool.findFile(year, month, day);
        if (tsFile == null) {
            return getAuditReports(year, month, day, old);
        }

        EncodeResult encodeResult;
        String[] encodedReports, tsEncodedReports;
        LinkedList<String[]> listEncodedReports = new LinkedList<String[]>();
        long[] timestamps;
        int i, totalReports = 0;

        File[] files = this.logPool.listLogFiles(year, month, day, old, false);
        if (files != null) {
            for (File file : files) {
                encodeResult = AuditLogReportHelper.encodeLogMessages(file, 0);
                if (encodeResult != null) {
                    encodedReports = encodeResult.getEncodedAuditReports();
                    totalReports += encodedReports.length;
                    listEncodedReports.add(encodedReports);
                }
            }
        }

        encodeResult = AuditLogReportHelper.encodeLogMessages(tsFile, 0);
        if (encodeResult != null) {
            encodedReports = encodeResult.getEncodedAuditReports();
            timestamps = encodeResult.getAuditReportsTimestamp();
            int index = -1, length = timestamps.length;

            for (i = 0; i < length; i++) {
                if (timestamp < timestamps[i]) {
                    index = i;
                    break;
                }
            }
            if (index != -1) {
                if (old) {
                    if (index > 0) {
                        tsEncodedReports = new String[index];
                        totalReports += index;
                        for (i = 0; i < index; i++) {
                            tsEncodedReports[i] = encodedReports[i];
                        }
                        listEncodedReports.add(tsEncodedReports);
                    }
                } else {
                    length -= index;
                    if (length > 0) {
                        tsEncodedReports = new String[length];
                        totalReports += length;
                        for (i = 0; i < length; i++) {
                            tsEncodedReports[i] = encodedReports[i + index];
                        }
                        listEncodedReports.addFirst(tsEncodedReports);
                    }
                }
            }
        }
        return collectAuditReports(listEncodedReports, totalReports);
    }

    /**
     * Get all audit reports after a specific hour. If an audit report has been
     * created at the specified hour, it is not included in the result. Audit
     * reports can be decode with the ObjectEncoder.
     * @param timestamp The hour
     * @return An array of encoded audit reports. The array can contains 'null'
     *         if the class of the audit report is not found in the classpath.
     *         Return null if there is not any audit report found
     */
    public String[] getRecentAuditReports(final long timestamp) {
        return getAuditReports(timestamp, false);
    }

    /**
     * Get all audit reports before a specific hour. If an audit report has been
     * created at the specified hour, it is not included in the result. Audit
     * reports can be decode with the ObjectEncoder.
     * @param timestamp The hour
     * @return An array of encoded audit reports. The array can contains 'null'
     *         if the class of the audit report is not found in the classpath.
     *         Return null if there is not any audit report found
     */
    public String[] getOldAuditReports(final long timestamp) {
        return getAuditReports(timestamp, true);
    }

    /**
     * Clean the log pool.
     */
    public void cleanLogPool() {
        this.logPool.cleanLogPool();
    }

    /**
     * Remove a specific log file.
     * @param year The year of the log file
     * @param month The month of the log file
     * @param day The day of the log file
     */
    public void cleanLogFile(final int year, final int month, final int day) {
        this.logPool.removeLogFile(year, month, day);
    }

    /**
     * Clean the log pool by removing old files.
     * @param nbFiles Number of files to remove
     */
    public void cleanOldLogFiles(final int nbFiles) {
        this.logPool.cleanLogPool(nbFiles, true);
    }

    /**
     * Clean the log pool by removing recent files.
     * @param nbFiles Number of files to remove
     */
    public void cleanRecentLogFiles(final int nbFiles) {
        this.logPool.cleanLogPool(nbFiles, false);
    }

    /**
     * Clean all log files created before a date. If a log file with the
     * specified date exists, the log file is not removed.
     * @param year The year of the date
     * @param month The month of the date
     * @param day The day of the date
     */
    public void cleanOldLogFiles(final int year, final int month, final int day) {
        this.logPool.cleanLogPool(year, month, day, true);
    }

    /**
     * Clean all log files created after a date. If a log file with the
     * specified date exists, the log file is not removed.
     * @param year The year of the date
     * @param month The month of the date
     * @param day The day of the date
     */
    public void cleanRecentLogFiles(final int year, final int month, final int day) {
        this.logPool.cleanLogPool(year, month, day, false);
    }

    /************************************************/
    /* Setters */
    /************************************************/
    /**
     * Determine if a console logger is used.
     * @param consoleLogger Boolean
     */
    public void setConsoleLogger(final boolean consoleLogger) {
        this.consoleLogger = consoleLogger;
    }

    /**
     * Determine if a file logger is used.
     * @param fileLogger Boolean
     */
    public void setFileLogger(final boolean fileLogger) {
        this.fileLogger = fileLogger;
    }

    /**
     * Determine the index where the date is inserted in the name of log files.
     * @param insertDateIndex The index
     */
    public void setInsertDateIndex(final int insertDateIndex) {
        this.insertDateIndex = insertDateIndex;
    }

    /**
     * Determine the path of the logs directory.
     * @param logDirectoryPath The path
     */
    public void setLogDirectoryPath(final String logDirectoryPath) {
        this.logDirectoryPath = logDirectoryPath;
    }

    /**
     * Set the pattern for the name of log file.
     * @param logFilenamePattern The pattern
     */
    public void setLogFilenamePattern(final String logFilenamePattern) {
        this.logFilenamePattern = logFilenamePattern;
    }

    /**
     * Determine the maximum number of log files in the pool.
     * @param maxLogFiles The number
     */
    public void setMaxLogFiles(final int maxLogFiles) {
        this.maxLogFiles = maxLogFiles;
    }

    /**
     * Determine if the component receive audits from the EJB audit component.
     * @param useEjbAuditComponent Boolean
     */
    public void setUseEjbAuditComponent(final boolean useEjbAuditComponent) {
        this.useEjbAuditComponent = useEjbAuditComponent;
    }

    /**
     * Determine if the component receive audits from the Web audit component.
     * @param useWebAuditComponent Boolean
     */
    public void setUseWebAuditComponent(final boolean useWebAuditComponent) {
        this.useWebAuditComponent = useWebAuditComponent;
    }

    /**
     * Determine if the component receive audits from the JNDI audit component.
     * @param useJNDIAuditComponent Boolean
     */
    public void setUseJndiAuditComponent(final boolean useJNDIAuditComponent) {
        this.useJNDIAuditComponent = useJNDIAuditComponent;
    }

    /**
     * Determine if the component receive audits from the Jaxws audit component.
     * @param useJaxwsAuditComponent Boolean
     */
    public void setUseJaxwsAuditComponent(final boolean useJaxwsAuditComponent) {
        this.useJaxwsAuditComponent = useJaxwsAuditComponent;
    }

    /**
     * @param jmxService the jmxService to set
     */
    public void setJmxService(final JmxService jmxService) {
        this.jmxService = jmxService;
    }

    public LinkedList<NodeReport> getNodeReports() {
        return nodeReports;
    }
}

/**
 * Search for a given pattern and once one ObjectName for this pattern has been
 * found, register a listener with the given objectname.
 * @author Florent Benoit
 */
class AuditLogJMXWaiter implements Runnable {

    /**
     * Sleeping time.
     */
    private static final long SLEEP_TIME = 2000L;

    /**
     * Logger.
     */
    private Log logger = LogFactory.getLog(AuditLogJMXWaiter.class);

    /**
     * Needs to wait.
     */
    private boolean wait = true;

    /**
     * List of pattern entries to check.
     */
    private List<PatternEntry> patternEntries = null;

    /**
     * MBean server used to do queries.
     */
    private MBeanServer jmxServer = null;

    /**
     * Default constructor.
     * @param jmxServer the MBean server
     */
    public AuditLogJMXWaiter(final MBeanServer jmxServer) {
        this.jmxServer = jmxServer;
        this.patternEntries = new ArrayList<PatternEntry>();
    }

    /**
     * Add an entry to the given list.
     * @param patternEntry the given listener/pattern mapping
     */
    public void addPatternEntry(final PatternEntry patternEntry) {
        synchronized (patternEntries) {
            patternEntries.add(patternEntry);
        }
    }

    /**
     * Starts to perform MBeans requests on the given patterns.
     */
    @SuppressWarnings("unchecked")
    public void run() {
        while (wait) {

            // Check if the entry is available in JMX ?
            synchronized (patternEntries) {
                Iterator<PatternEntry> itPatternEntry = patternEntries.iterator();
                while (itPatternEntry.hasNext()) {
                    PatternEntry patternEntry = itPatternEntry.next();
                    Set<ObjectName> objectNames = jmxServer.queryNames(patternEntry.getObjectNamePattern(), null);

                    // item found
                    if (objectNames.size() > 0) {

                        // Get first objectName found
                        ObjectName objectName = objectNames.iterator().next();

                        // Remove entry if one item found
                        itPatternEntry.remove();

                        // Set the correct Name in the listener
                        try {
                            patternEntry.getListener().register(objectName);
                            logger.info("Add Notifications from ''{0}''", objectName);
                        } catch (AuditLogServiceException e) {
                            logger.error("Unable to register the listener with objectName ''{0}''.", objectName);
                        }
                    }
                }
            }

            try {
                Thread.sleep(SLEEP_TIME);
            } catch (InterruptedException e) {
                logger.debug("Unable to wait", e);
            }
        }

    }

    /**
     * Ends the infinite loop that is waiting MBeans.
     */
    public void end() {
        this.wait = false;
    }

}

/**
 * Defines a mapping between a listener and the objectname pattern.
 * @author Florent Benoit
 */
class PatternEntry {

    /**
     * Listener.
     */
    private IAuditComponentListener listener = null;

    /**
     * ObjectName.
     */
    private ObjectName objectNamePattern = null;

    /**
     * @return the listener.
     */
    public IAuditComponentListener getListener() {
        return listener;
    }

    /**
     * @return the objectname pattern to search !
     */
    public ObjectName getObjectNamePattern() {
        return objectNamePattern;
    }

    /**
     * Default constructor.
     * @param listener the given listener
     * @param objectNamePattern the pattern
     */
    public PatternEntry(final IAuditComponentListener listener, final ObjectName objectNamePattern) {
        this.listener = listener;
        this.objectNamePattern = objectNamePattern;
    }



}
