/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2007 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 (at your option) 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.deployablemonitor;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;

import org.ow2.jonas.lib.execution.ExecutionResult;
import org.ow2.jonas.lib.execution.IExecution;
import org.ow2.jonas.lib.execution.RunnableHelper;
import org.ow2.util.archive.api.IArchive;
import org.ow2.util.archive.impl.ArchiveManager;
import org.ow2.util.ee.deploy.api.deployable.EARDeployable;
import org.ow2.util.ee.deploy.api.deployable.EJBDeployable;
import org.ow2.util.ee.deploy.api.deployable.IDeployable;
import org.ow2.util.ee.deploy.api.deployable.OSGiDeployable;
import org.ow2.util.ee.deploy.api.deployable.RARDeployable;
import org.ow2.util.ee.deploy.api.deployable.WARDeployable;
import org.ow2.util.ee.deploy.api.deployer.DeployerException;
import org.ow2.util.ee.deploy.api.deployer.IDeployerManager;
import org.ow2.util.ee.deploy.api.deployer.UnsupportedDeployerException;
import org.ow2.util.ee.deploy.impl.helper.DeployableHelper;
import org.ow2.util.ee.deploy.impl.helper.DeployableHelperException;
import org.ow2.util.log.Log;
import org.ow2.util.log.LogFactory;

/**
 * This monitor will search all the deployable from a list of directories.<br>
 * In development mode, this monitor will detect changes on the deployables and
 * then restart or undeploy them if they have been changed or removed.
 * @author Florent BENOIT
 */
public class DeployableMonitor extends Thread {

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

    /**
     * List of directories to analyze.
     */
    private List<File> directories = null;

    /**
     * Development mode or not ?
     */
    private boolean developmentMode = false;

    /**
     * Interval time when sleeping in development mode (5s).
     */
    private static final int SLEEP_TIME = 5000;

    /**
     * Stop order received ?
     */
    private boolean stopped = false;

    /**
     * Map between an File (monitored) and the last updated file.
     */
    private Map<File, Long> modifiedFiles = null;

    /**
     * List of deployed files (by this monitor).
     */
    private Map<File, IDeployable<?>> deployed = null;

    /**
     * List of File that have a failed deployment (by this monitor).
     */
    private List<File> failed = null;

    /**
     * Deployer Manager. (that allow us to deploy Deployable).
     */
    private IDeployerManager deployerManager = null;

    /**
     * First launch of this monitor.
     */
    private boolean firstCheck = true;

    /**
     * Exclusion filter.
     */
    private ExclusionFilenameFilter filter = null;;

    /**
     * Allow to inform the Service that the firstCheck ended.
     */
    private DeployableMonitorService myService = null;

    /**
     * Constructor of this monitor.
     * @param service the {@link DeployableMonitorService} instance
     */
    public DeployableMonitor(final DeployableMonitorService service) {
        this.directories = new LinkedList<File>();
        this.modifiedFiles = new WeakHashMap<File, Long>();
        this.deployed = new ConcurrentHashMap<File, IDeployable<?>>();
        this.failed = new ArrayList<File>();
        this.filter  = new ExclusionFilenameFilter();
        //this.developmentMode = service.isDevelopmentMode();
        this.myService = service;
    }

    /**
     * Start the thread of this class. <br>
     * It will search and deploy files to deploy.<br>
     * In development mode, it will check the changes.
     */
    @Override
    public void run() {
        for (;;) {
            if (stopped) {
                // Stop the thread
                return;
            }

            if (developmentMode || firstCheck) {
                // Check new archives/containers to start
                checkNewArchives();

                if (firstCheck) {
                    myService.firstCheckEnded();
                }

                // Undeploy/ReDeploy archives for deployed modules
                updateArchives();

                // Next step, we will be in a checking step.
                firstCheck = false;
            }

            try {
                Thread.sleep(SLEEP_TIME);
            } catch (InterruptedException e) {
                throw new RuntimeException("Thread fail to sleep");
            }

        }
    }

    /**
     * Check new archives/containers.
     */
    private void checkNewArchives() {
        try {
            // Deploy the file in an execution block
            IExecution<Void> exec = new IExecution<Void>() {
                public Void execute() throws Exception, Error {
                    detectNewArchives();
                    return null;
                }
            };

            // Execute
            ExecutionResult<Void> result = RunnableHelper.execute(getClass().getClassLoader(), exec);

            // Throw an ServiceException if needed
            if (result.hasException()) {
                throw result.getException();
            }

        } catch (Exception e) { // Catch all exception (including runtime)
            logger.error("Problem when trying to find and deploy new archives", e);
        } catch (Error e) {
            logger.error("Error when trying to find and deploy new archives", e);
        }
    }

    /**
     * Undeploy/ReDeploy archives for deployed modules.
     */
    private void updateArchives() {
        try {
            // Undeploy in an execution block
            IExecution<Void> exec = new IExecution<Void>() {
                public Void execute() throws Exception, Error {
                    checkModifiedDeployables();
                    return null;
                }
            };

            // Execute
            ExecutionResult<Void> result = RunnableHelper.execute(getClass().getClassLoader(), exec);

            // Throw an ServiceException if needed
            if (result.hasException()) {
                throw result.getException();
            }

        } catch (Exception e) { // Catch all exception (including runtime)
            logger.error("Problem when checking current deployables", e);
        } catch (Error e) {
            logger.error("Error when checking current deployables", e);
        }
    }

    /**
     * Undeploy archives.
     */
    private void undeployArchives() {
        try {
            // Undeploy in an execution block
            IExecution<Void> exec = new IExecution<Void>() {
                public Void execute() throws Exception, Error {
                    undeployDeployables();
                    return null;
                }
            };

            // Execute
            ExecutionResult<Void> result = RunnableHelper.execute(getClass().getClassLoader(), exec);

            // Throw an ServiceException if needed
            if (result.hasException()) {
                throw result.getException();
            }

        } catch (Exception e) { // Catch all exception (including runtime)
            logger.error("Problem when undeploying deployed archives", e);
        } catch (Error e) {
            logger.error("Error when undeploying deployed archives", e);
        }
    }
    /**
     * Scan all files present in the deploy directory and deploy them. (if not
     * deployed).
     * @throws DeployableMonitorException if there is a problem during the scan
     */
    private void detectNewArchives() throws DeployableMonitorException {

        for (File deployDirectory : directories) {
            // get files
            File[] files = deployDirectory.listFiles(filter);

            // next directory if there are no files to scan.
            if (files == null) {
                continue;
            }

            // Sort the files by names
            Arrays.sort(files, new LexicographicallyFileComparator());

            // Sort the file by type (first check only)
            if (firstCheck) {
                // Build a list of deployable
                List<File> osgiDeployables = new LinkedList<File>();
                List<File> rarDeployables = new LinkedList<File>();
                List<File> ejbDeployables = new LinkedList<File>();
                List<File> warDeployables = new LinkedList<File>();
                List<File> earDeployables = new LinkedList<File>();
                List<File> unknownDeployables = new LinkedList<File>();
                for (File f : files) {
                    // Get deployable
                    IArchive archive = ArchiveManager.getInstance().getArchive(f);
                    if (archive == null) {
                        logger.warn("Ignoring invalid file ''{0}''", f);
                        continue;
                    }

                    IDeployable<?> deployable;
                    try {
                        deployable = DeployableHelper.getDeployable(archive);
                    } catch (DeployableHelperException e) {
                        throw new DeployableMonitorException("Cannot get a deployable for the archive '" + archive + "'", e);
                    }

                    if (deployable instanceof OSGiDeployable) {
                        osgiDeployables.add(f);
                    } else if (deployable instanceof RARDeployable) {
                        rarDeployables.add(f);
                    } else if (deployable instanceof EJBDeployable) {
                        ejbDeployables.add(f);
                    } else if (deployable instanceof WARDeployable) {
                        warDeployables.add(f);
                    } else if (deployable instanceof EARDeployable) {
                        earDeployables.add(f);
                    } else {
                        logger.debug("Unknown type of deployable: {0}", deployable);
                        unknownDeployables.add(f);
                    }


                }

                // Create new list (type and lexico order)
                List<File> newList = new LinkedList<File>();

                // First use the OSGi bundles
                newList.addAll(osgiDeployables);
                // then RAR
                newList.addAll(rarDeployables);
                // then EJB
                newList.addAll(ejbDeployables);
                // and then WAR
                newList.addAll(warDeployables);
                // ..EAR
                newList.addAll(earDeployables);
                // other files
                newList.addAll(unknownDeployables);


                if (newList.size() > 0) {
                    logger.info("Deployables to deploy at startup: [{0}]", newList);
                }

                // Update the array
                files = newList.toArray(new File[newList.size()]);

            }

            // analyze each file to detect new modules that are not yet deployed.
            for (File f : files) {
                // already deployed ?
                if (deployed.containsKey(f)) {
                    // yes, then check other files
                    continue;
                }

                // This module has failed previously ?
                if (failed.contains(f)) {
                    // If the module hasn't been updated, no need to deploy it again as it will fails again
                    if (!hasBeenUpdated(f)) {
                        continue;
                    }
                    // Cleanup the previous failure and try again the deployment
                    failed.remove(f);
                }


                // Else, get the deployable
                IArchive archive = ArchiveManager.getInstance().getArchive(f);
                if (archive == null) {
                    logger.warn("Ignoring invalid file ''{0}''", f);
                    continue;
                }
                IDeployable<?> deployable;
                try {
                    deployable = DeployableHelper.getDeployable(archive);
                } catch (DeployableHelperException e) {
                    throw new DeployableMonitorException("Cannot get a deployable for the archive '" + archive + "'", e);
                }

                logger.debug("Detect a new Deployable ''{0}'' and deploying it.", deployable);

                // Now, deploy the file
                try {
                    deployerManager.deploy(deployable);
                    // Perform a garbage collector to avoid file lock
                    System.gc();
                } catch (UnsupportedDeployerException e) {
                    // Deployment of this deployable is not supported.
                    // We will try to deploy this file in the next detection cycle.
                    continue;
                } catch (DeployerException e) {
                    // Deployment of this deployable has failed
                    failed.add(f);
                    throw new DeployableMonitorException("Cannot deploy the deployable '" + deployable + "'", e);
                } catch (RuntimeException e) {
                    // Runtime exception but deployment has failed
                    failed.add(f);
                    throw new DeployableMonitorException("RuntimeException when deploying the deployable '"
                            + deployable + "'", e);
                } catch (Error e) {
                    // Error but deployment has failed
                    failed.add(f);
                    throw new DeployableMonitorException("Error when deploying the deployable '"
                            + deployable + "'", e);
                }

                // deployed is ok
                deployed.put(f, deployable);
            }
        }
    }

    /**
     * Gets the last modified attribute of a given archive.<br> If it is a
     * directory, returns the last modified file of the archive.
     * @param archive the archive to monitor.
     * @return the last modified version of the given archive.
     */
    protected long getLastModified(final File archive) {
        if (archive.isFile()) {
            return archive.lastModified();
        }
        // else
        File[] files = archive.listFiles();
        long last = 0;
        if (files != null) {
            for (File f : files) {
                last = Math.max(last, getLastModified(f));
            }
        }
        return last;
    }

    /**
     * Check if the given file has been updated since the last check.
     * @param file the file to test
     * @return true if the archive has been updated
     */
    protected boolean hasBeenUpdated(final File file) {

        // get lastmodified for this URL
        long previousLastModified = 0;
        Long l = modifiedFiles.get(file);
        if (l != null) {
            previousLastModified = l.longValue();
        }

        long updatedModified = getLastModified(file);

        // first check. nothing to do
        if (previousLastModified == 0) {
            // Store initial time
            modifiedFiles.put(file, Long.valueOf(updatedModified));
            return false;
        }
        // URL has been updated since the last time
        if (updatedModified > previousLastModified) {
            modifiedFiles.put(file, Long.valueOf(updatedModified));
            return true;
        }

        return false;
    }

    private void undeployDeployables() {
        // Get list of files that are deployed
        Set<File> files = deployed.keySet();

        // Nothing to do if no modules are deployed.
        if (files == null) {
            return;
        }

        // For each deployed module undeploy it
        for (File f : files) {
            IDeployable<?> deployable = deployed.get(f);

            // Not yet deployed ?
            if (deployable == null) {
                continue;
            }

            // undeploy
            logger.info("Undeploy deployable ''{0}''", deployable);
            try {
                deployerManager.undeploy(deployable);
                // Perform a garbage collector to avoid file lock during redeployment
                System.gc();
            } catch (UnsupportedDeployerException e) {
                // Undeployment of this deployable is not supported.
                // We will try to undeploy this file in the next detection cycle.
                continue;
            } catch (DeployerException e) {
                logger.error("Undeploy of the deployable '" + deployable + "' has failed", e);
                failed.add(f);
            } catch (RuntimeException e) {
                logger.error("Undeploy of the deployable '" + deployable + "' has failed", e);
                failed.add(f);
            } finally {
                // even in error case, the file should have been removed
                deployed.remove(f);
            }
            continue;

        }
    }

    /**
     * Check if the current deployables that are deployed have been updated.
     * If it is the case, undeploy them and then deploy it again (except for EJB3 Deployable where there is a stop/start).
     * @throws DeployableMonitorException if the redeployment fails
     */
    private void checkModifiedDeployables() throws DeployableMonitorException {
        // Get list of files that are deployed
        Set<File> files = deployed.keySet();

        // Nothing to do if no modules are deployed.
        if (files == null) {
            return;
        }

        // For each deployed module that is not an EJB3, check if the module has been updated
        for (File f : files) {
            IDeployable<?> deployable = deployed.get(f);

            // Not yet deployed ?
            if (deployable == null) {
                continue;
            }

            // File has been removed
            if (!f.exists()) {
                // undeploy
                logger.info("Deployable ''{0}'' has been removed on the filesystem, undeploy it", deployable);
                try {
                    deployerManager.undeploy(deployable);
                    // Perform a garbage collector to avoid file lock during redeployment
                    System.gc();
                } catch (UnsupportedDeployerException e) {
                    // Undeployment of this deployable is not supported.
                    // We will try to undeploy this file in the next detection cycle.
                    continue;
                } catch (DeployerException e) {
                    logger.error("Undeploy of the deployable '" + deployable + "' has failed", e);
                    failed.add(f);
                } catch (RuntimeException e) {
                    logger.error("Undeploy of the deployable '" + deployable + "' has failed", e);
                    failed.add(f);
                } finally {
                    // even in error case, the file should have been removed
                    deployed.remove(f);
                }
                continue;
            }

            // Update has been detected, need to undeploy and then to deploy again
            if (hasBeenUpdated(f)) {
                logger.info("Deployable ''{0}'' has been updated, reloading it", deployable);
                try {
                    deployerManager.undeploy(deployable);
                    deployed.remove(f);
                    // Perform a garbage collector to avoid file lock during redeployment
                    System.gc();
                } catch (UnsupportedDeployerException e) {
                    // Undeployment of this deployable is not supported.
                    // We will try to undeploy this file in the next detection cycle.
                    continue;
                } catch (DeployerException e) {
                    logger.error("Undeploy of the deployable '" + deployable + "' has failed", e);
                    // Deployment has failed, it is now undeployed
                    deployed.remove(f);
                    failed.add(f);
                }

                // Get a new deployable
                IArchive archive = ArchiveManager.getInstance().getArchive(f);
                if (archive == null) {
                    logger.warn("Ignoring invalid file ''{0}''", f);
                    continue;
                }
                IDeployable<?> newDeployable;
                try {
                    newDeployable = DeployableHelper.getDeployable(archive);
                } catch (DeployableHelperException e) {
                    logger.error("Cannot get a deployable for the archive '" + archive + "'", e);
                    continue;
                }
                try {
                    deployerManager.deploy(newDeployable);
                    // Store the new deployable
                    deployed.put(f, newDeployable);
                    // Perform a garbage collector to avoid file lock
                    System.gc();
                } catch (UnsupportedDeployerException e) {
                    // Deployment of this deployable is not supported.
                    // We will try to deploy this file in the next detection cycle.
                    continue;
                } catch (DeployerException e) {
                    // Deployment of this deployable has failed
                    failed.add(f);
                    throw new DeployableMonitorException("Cannot redeploy the deployable '" + newDeployable + "'.", e);
                }
            }

        }

    }


    /**
     * @return the list of directories that this monitor is monitoring.
     */
    public List<File> getDirectories() {
        return directories;
    }

    /**
     * Sets the list of directories to monitor.
     * @param directories the list of directories to use.
     */
    public void setDirectories(final List<File> directories) {
        this.directories = directories;
    }

    /**
     * @return true if the development is enabled, else false (production mode).
     */
    public boolean isDevelopmentMode() {
        return developmentMode;
    }

    /**
     * Enable or disable the development mode.
     * @param developmentMode true if it has to be enabled.
     */
    public void setDevelopmentMode(final boolean developmentMode) {
        this.developmentMode = developmentMode;
    }

    /**
     * Receives a stop order.
     */
    public void stopOrder() {
        this.stopped = true;
        undeployArchives();
    }

    /**
     * @return the instance of the deployer manager.
     */
    public IDeployerManager getDeployerManager() {
        return deployerManager;
    }

    /**
     * Sets the deployer manager for deployers.
     * @param deployerManager the instance of the deployer manager.
     */
    public void setDeployerManager(final IDeployerManager deployerManager) {
        this.deployerManager = deployerManager;
    }

    /**
     * @param patterns List of exclusion patterns.
     */
    public void setExclusionPatterns(final List<String> patterns) {
        filter.setExclusionList(patterns);
    }

}
