/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2011 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.addon.deploy.impl.deployer;

import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
import org.ow2.jonas.Version;
import org.ow2.jonas.addon.deploy.api.deployable.IAddonDeployable;
import org.ow2.jonas.addon.deploy.api.deployable.ISortableDeployable;
import org.ow2.jonas.addon.deploy.api.deployer.IAddonDeployer;
import org.ow2.jonas.addon.deploy.api.deployer.IAddonDeployerLog;
import org.ow2.jonas.addon.deploy.api.util.IAddonLogEntry;
import org.ow2.jonas.addon.deploy.api.util.IAddonStructure;
import org.ow2.jonas.addon.deploy.impl.deployable.AddonDeployableImpl;
import org.ow2.jonas.addon.deploy.impl.util.AddonUtil;
import org.ow2.jonas.lib.work.LogEntryImpl;
import org.ow2.jonas.management.ServiceManager;
import org.ow2.jonas.multitenant.MultitenantService;
import org.ow2.jonas.properties.ServerProperties;
import org.ow2.jonas.properties.ServiceProperties;
import org.ow2.jonas.workcleaner.DeployerLogException;
import org.ow2.jonas.workcleaner.IDeployerLog;
import org.ow2.jonas.workcleaner.LogEntry;
import org.ow2.util.archive.api.ArchiveException;
import org.ow2.util.archive.api.IArchive;
import org.ow2.util.archive.impl.ArchiveManager;
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.UnknownDeployable;
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.api.helper.DeployableHelperException;
import org.ow2.util.ee.deploy.api.helper.IDeployableHelper;
import org.ow2.util.ee.deploy.api.report.IDeploymentReport;
import org.ow2.util.ee.deploy.impl.deployer.AbsDeployer;
import org.ow2.util.ee.deploy.impl.helper.UnpackDeployableHelper;
import org.ow2.util.file.FileUtils;
import org.ow2.util.file.FileUtilsException;
import org.ow2.util.log.Log;
import org.ow2.util.log.LogFactory;
import org.ow2.util.plan.bindings.exceptions.InvalidRepositoryException;
import org.ow2.util.plan.bindings.repository.Repository;
import org.ow2.util.plan.bindings.repository.RepositoryKind;
import org.ow2.util.plan.deploy.deployable.api.DeploymentPlanDeployable;
import org.ow2.util.plan.repository.api.IRepositoryManager;
import org.ow2.util.plan.repository.api.RepositoryIdCollisionException;
import org.ow2.util.url.URLUtils;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Vector;

/**
 * Represents an Addon Deployer
 * @author Jeremy Cazaux
 */
public class AddonDeployerImpl extends AbsDeployer<IAddonDeployable> implements IAddonDeployer {

    /**
     * The logger
     */
    private static Log logger = LogFactory.getLog(AddonDeployerImpl.class);

    /**
     * Server properties
     */
    private ServerProperties serverProperties;

    /**
     * The DeployableHelper  which analyze an archive and build the associated Deployable object
     */
    private IDeployableHelper deployableHelper;

    /**
     * List of deployed addons.
     */
    private Map<URL, IAddonDeployable> addons = null;

    /**
     * Addons log
     */
    private IAddonDeployerLog<IAddonLogEntry> deployerLog;

    /**
     * Log for the work cleaner service
     */
    private IDeployerLog workCleanerLog;

    /**
     * The deployer manager
     */
    private IDeployerManager deployerManager;

    /*
     * The list of deployed addon names
     */
    private List<String> deployedAddonNames;

    /**
     * OSGi bundle context
     */
    private BundleContext bundleContext;

    /**
     * Configuration deployer
     */
    private ConfDeployerImpl confDeployer;

    /**
     * The service manager.
     * Use to start / stop JOnAS services
     */
    private ServiceManager serviceManager;

    /**
     * True if the adon deployer is initialized
     */
    private boolean isInitialized;

    /**
     * List of OSGi service registration of ServiceProperties instance
     */
    private List<ServiceRegistration> serviceRegistrations;

    /**
     * Multitenant service
     */
    private MultitenantService multitenantService;

    /**
     * Repository manager
     */
    private IRepositoryManager repositoryManager;

    /**
     * List of repositories of each addons
     */
    private Map<IAddonDeployable, Repository> repositories;

    /**
     * The property name to register a new ServiceProperties component as an OSGi service
     */
    public final String SERVICE_REGISTRATION_SERVICE_PROPERTY = "service";

    /**
     * The XSD for the addon metadata file
     */
    public static final String XSD_RESOURCE = "/META-INF/jonas-addon-1.0.xsd";

    /**
     * Default constructor
     * @param serverProperties Server properties
     */
    public AddonDeployerImpl(final ServerProperties serverProperties, final IDeployableHelper deployableHelper,
                             final IAddonDeployerLog<IAddonLogEntry> deployerLog, final IDeployerManager deployerManager,
                             final BundleContext bundleContext, final ServiceManager serviceManager,
                             final IDeployerLog<LogEntry> workCleanerLog, final IRepositoryManager repositoryManager) {
        this.serverProperties = serverProperties;
        this.deployableHelper = deployableHelper;
        this.deployerLog = deployerLog;
        this.workCleanerLog = workCleanerLog;
        this.deployerManager = deployerManager;
        this.addons = new HashMap<URL, IAddonDeployable>();
        this.deployedAddonNames = new ArrayList<String>();
        this.bundleContext = bundleContext;
        this.serviceManager = serviceManager;
        this.confDeployer = new ConfDeployerImpl(this.bundleContext);
        this.repositoryManager = repositoryManager;
        this.repositories = new HashMap<IAddonDeployable, Repository>();
        this.isInitialized = false;
        serviceRegistrations = new ArrayList<ServiceRegistration>();

        //If some addons are removed from the deploy directories, the JOnAS configuration is retrieved
        try {
            checkLogs();
        } catch (DeployerException e) {
            logger.error("Checked log has failed", e);
        }
    }

    /**
     * If some addons have been removed from the deploy directories, the JOnAS configuration is
     * retrieved
     */
    private void checkLogs() throws DeployerException {

        //Map between the name of the addon to undeploy and the path to the deployable of the addon to undeploy
        Map<String, String> addonsToUndeploy = new HashMap<String, String>();

        //get log entries
        Vector<IAddonLogEntry> logEntries = this.deployerLog.getEntries();
        Vector<IAddonLogEntry> logEntriesToRemove = new Vector<IAddonLogEntry>();

        for (IAddonLogEntry logEntry: logEntries) {

            File orginalFile = logEntry.getOriginal();
            File unpackedFile = logEntry.getCopy();

            if (orginalFile == null || !orginalFile.exists()) {
                //an Addon has been removed from deploy directory when the server was not running
                //we need to retrieve JOnAS Configuration (for configurations files, binaries, deployable, ANT tasks, etc...)
                addonsToUndeploy.put(logEntry.getName(), unpackedFile.getAbsolutePath());
                logEntriesToRemove.add(logEntry);

            }
        }

        //retrieve JOnAS configuration
        retrieveJOnASConfiguration(addonsToUndeploy);

        //remove log entries for each addon to undeploy
        for (IAddonLogEntry logEntry: logEntriesToRemove) {
            try {
                this.deployerLog.removeEntry(logEntry);
                this.logger.info("''{0}'' Addon Deployable is now undeployed", logEntry.getOriginal().getAbsolutePath());
            } catch (DeployerLogException e) {
                this.logger.error("Cannot remove log entry " + logEntry.getName() + "." , e);
            }
        }
    }

    /**
     * Deploy the given deployable.
     * @param deployable the Addon deployable.
     * @throws DeployerException if the Addon is not deployed.
     */
    @Override
    public void doDeploy(final IDeployable<IAddonDeployable> deployable) throws DeployerException {

        File originalFile = null;
        try {
            originalFile = URLUtils.urlToFile(deployable.getArchive().getURL());
        } catch (ArchiveException e) {
            throw new DeployerException("Cannot get the deployable " + deployable.getShortName(), e);
        }

        IAddonLogEntry logEntry = this.deployerLog.getEntry(originalFile);
        boolean isAlreadyDeployed = (originalFile.exists() && logEntry != null);

        IAddonDeployable unpackedDeployable = null;
        String lastModifiedOriginalFileName = null;
        try {
            lastModifiedOriginalFileName = FileUtils.lastModifiedFileName(originalFile);
        } catch (FileUtilsException e) {
            throw new DeployerException("Cannot get the last modified file name of " + originalFile.getAbsolutePath(), e);
        }
        if (logEntry != null) {
            String unpackedFileName =  logEntry.getCopy().getName();

            //if it's a cold update, the addon need to be redeployed
            if (!lastModifiedOriginalFileName.equals(unpackedFileName)) {
                logger.info("Deployable ''{0}'' has been updated.", deployable);

                // Get the unpacked deployable
                IArchive archive = ArchiveManager.getInstance().getArchive(this.deployerLog.getEntry(originalFile).getCopy());
                try {
                    unpackedDeployable = IAddonDeployable.class.cast(this.deployableHelper.getDeployable(archive));
                } catch (DeployableHelperException e) {
                    throw new DeployerException("Cannot get the deployable " + originalFile, e);
                }

                //get JOnAS Addon metadata
                AddonMetaData addonMetaData = null;
                try {
                    addonMetaData = AddonUtil.getAddonMetadata(AddonUtil.getAddonMetadataFile(unpackedDeployable.getArchive()),
                            logEntry.getCopy().getAbsolutePath());
                } catch (Exception e) {
                    //do nothing...the addon need to be undeployed
                    logger.warn("Addon metadata are incorrect");
                }

                //remove the logEntry
                try {
                    this.deployerLog.removeEntry(this.deployerLog.getEntry(originalFile));
                } catch (DeployerLogException e) {
                    logger.error("Cannot remove log entry of the file " + originalFile.getAbsolutePath());
                }

                //undeploy the addon
                undeploy(unpackedDeployable, addonMetaData);
                isAlreadyDeployed = false;
            }
        }

        //if it's already deployed, don't unpack it!
        if (isAlreadyDeployed) {
            File unpackedFile = logEntry.getCopy();

            //an unpacked deployable has been removed from the work directory. We need to unpack it again
            if (unpackedFile == null|| !unpackedFile.exists()) {
                //unpacked the addon
                File folder = new File(AddonUtil.getAddonsWorkDirectory(this.serverProperties));
                try {
                    unpackedDeployable = UnpackDeployableHelper.unpack(
                            IAddonDeployable.class.cast(deployable), folder, lastModifiedOriginalFileName, false,
                            this.deployableHelper);
                } catch (Exception e) {
                    throw new DeployerException("Cannot unpacked archive for '" + deployable.getArchive() + "'", e);
                }
            } else {
                // Get the unpacked deployable
                IArchive archive = ArchiveManager.getInstance().getArchive(this.deployerLog.getEntry(originalFile).getCopy());
                try {
                    unpackedDeployable = IAddonDeployable.class.cast(this.deployableHelper.getDeployable(archive));
                } catch (DeployableHelperException e) {
                    throw new DeployerException("Cannot get the deployable " + originalFile, e);
                }
            }
            // display as a debug
            logger.debug("Deploying ''{0}''", unpackedDeployable);

        } else {
            //unpacking the addon
            unpackedDeployable = null;
            File folder = new File(AddonUtil.getAddonsWorkDirectory(this.serverProperties));
            try {
                originalFile = URLUtils.urlToFile(deployable.getArchive().getURL());
                String archiveName = FileUtils.lastModifiedFileName(originalFile);
                unpackedDeployable = UnpackDeployableHelper.unpack(IAddonDeployable.class.cast(deployable), folder,
                        archiveName, false, this.deployableHelper);
            } catch (Exception e) {
                throw new DeployerException("Cannot deploy archive for '" + deployable.getArchive() + "'", e);
            }

            // display this line when Addon has been unpacked
            logger.info("Deploying ''{0}''", unpackedDeployable);
        }

        // Archive
        IArchive addonArchive = unpackedDeployable.getArchive();

        //get the unpackedFile
        File unpackedFile;
        try {
            unpackedFile = URLUtils.urlToFile(addonArchive.getURL());
        } catch (Exception e) {
            throw new DeployerException("Cannot get URL from archive '" + addonArchive + "'", e);
        }

        //get the metadata of the addon
        File addonMetadataFile;
        if (isAlreadyDeployed) {
            //get metadata from $JB/conf/addons/{addon.name} directory
            addonMetadataFile = new File(AddonUtil.getAddonDirectoryPath(logEntry.getName()), IAddonStructure.METADATA_FILENAME);
        } else {
            //get metadata from the unpacked deployable
            addonMetadataFile = AddonUtil.getAddonMetadataFile(addonArchive);
        }

        //check the validity of the metadata file
        validate(addonMetadataFile);

        AddonMetaData addonMetaData;
        try {
            addonMetaData = AddonUtil.getAddonMetadata(addonMetadataFile, unpackedFile.getAbsolutePath());
        } catch (Exception e) {
            throw new DeployerException("Addon metadata are incorrect", e);
        }

        URL originalURL;
        try {
            originalURL = deployable.getArchive().getURL();
        } catch (Exception e) {
            throw new DeployerException(
                    "Cannot get the url of the initial deployable for the Addon Module '" + deployable + "'.", e);
        }

        //Check that everythings is OK
        checkAddonMetadata(addonMetaData);

        // Register repository dir if exists
        String repositoryDir = AddonUtil.getAddonRepositoryWorkDirectory(AddonUtil.getAddonWorkDirectory(unpackedDeployable));
        File repositoryFile = new File(repositoryDir);
        if (repositoryFile.exists() && repositoryFile.isDirectory()) {
            Repository repository = new Repository();
            repository.setId(unpackedDeployable.toString());
            repository.setType(RepositoryKind.MAVEN_2);
            repository.setUrl("file:" + repositoryFile.getPath());

            try {
                this.repositoryManager.addRepository(repository);
                repositories.put(unpackedDeployable, repository);
                logger.debug("The repository '" + repository + "' was added");
            } catch (RepositoryIdCollisionException e) {
                throw new DeployerException("Repository '" + repository + "' already added");
            } catch (InvalidRepositoryException e) {
                throw new DeployerException("'" + repository + "' is an invalid repository");
            }
        }

        //Update the list of deployable (OSGi deployable, EJB3 deployable, EAR Deployable, etc...) of the unpacked deployable
        updateDeployables(unpackedFile.getAbsolutePath(), unpackedDeployable, addonMetaData);

        // keep the link original deployable -> unpacked deployable for undeployment
        this.addons.put(originalURL, unpackedDeployable);

        //if the addon is log
        if (this.deployerLog.getEntry(URLUtils.urlToFile(originalURL)) != null) {

            deploy(unpackedDeployable, addonMetaData, true);

            logger.debug("''{0}'' addon is already deployed", deployable.getShortName());

        } else {

            //deploy the addon
            deploy(unpackedDeployable, addonMetaData, false);

            // Log the addon
            if (this.deployerLog != null) {
                try {
                    this.deployerLog.addEntry(addonMetaData.getName(), originalFile, unpackedFile);
                } catch (DeployerLogException e) {
                    logger.info("Cannot added a log entry to the addon logger");
                }
            }

            //add the name of the addon to the list of deployed addon names
            this.deployedAddonNames.add(addonMetaData.getName());

            logger.info("''{0}'' addon is now deployed", deployable.getShortName());
        }

        //log the addon into the workcleaner log file
        try {
            this.workCleanerLog.addEntry(new LogEntryImpl(originalFile, unpackedFile));
        } catch (DeployerLogException e) {
            logger.info("Cannot added a log entry to the addon work cleaner logger");
        }

    }

    /**
     * Undeploy the given Addon.
     * @param deployable the deployable to remove.
     * @throws DeployerException if the Addon is not deployed.
     */
    @Override
    public void doUndeploy(final IDeployable<IAddonDeployable> deployable) throws DeployerException {

        logger.info("Undeploying {0}", deployable.getShortName());

        // get Addon URL
        URL addonURL;
        try {
            addonURL = deployable.getArchive().getURL();
        } catch (ArchiveException e) {
            throw new DeployerException("Cannot get the URL on the Addon deployable '" + deployable + "'.", e);
        }

        // Check if this archive has been unpacked ?
        IAddonDeployable unpackedDeployable = null;
        if (addons.containsKey(addonURL)) {
            unpackedDeployable = addons.get(addonURL);
        } else {
            throw new DeployerException("Cannot get the URL of the unpacked Addon deployable '" + deployable + "'.");
        }

        File unpackedDeployableFile = null;
        try {
            unpackedDeployableFile = URLUtils.urlToFile( unpackedDeployable.getArchive().getURL());
        } catch (ArchiveException e) {
            throw new DeployerException("Cannot get the URL on the unpacked deployable '" + unpackedDeployable + "'.", e);
        }

        //get JOnAS Addon metadata
        AddonMetaData addonMetaData = null;
        try {
            addonMetaData = AddonUtil.getAddonMetadata(AddonUtil.getAddonMetadataFile(unpackedDeployable.getArchive()),
                    unpackedDeployableFile.getAbsolutePath());
        } catch (Exception e) {
            logger.warn("Addon metadata are incorrect", e);
        }

        //remove the logEntry
        File originalFile = URLUtils.urlToFile(addonURL);
        IAddonLogEntry logEntry =  deployerLog.getEntry(originalFile);
        if (logEntry != null) {
            try {
                deployerLog.removeEntry(logEntry);
            } catch (DeployerLogException e) {
                logger.error("Cannot remove the LogEntry");
            }
        }

        // remove the repository
        Repository repository = repositories.get(unpackedDeployable);
        if (repository != null) {
            this.repositoryManager.removeRepository(repository);
            logger.debug("The repository '" + repository + "' was removed");
        }

        //undeploy the addon
        undeploy(unpackedDeployable, addonMetaData);

        //remove the name of the addon to the list of deployed addon name
        this.deployedAddonNames.remove(addonMetaData.getName());

        // remove link original deployable -> unpacked deployable
        this.addons.remove(addonURL);

        logger.info("''{0}'' addon is now undeployed", deployable.getShortName());
    }

    /**
     * Checks if the given deployable is supported by the Deployer.
     * @param deployable the deployable to be checked
     * @return true if it is supported, else false.
     */
    @Override
    public boolean supports(final IDeployable<?> deployable) {
        return AddonDeployableImpl.class.isInstance(deployable);
    }

    /**
     * Check that metadata file  is OK
     * @param addonMetaData The metadata of the addon to deploy
     * @throws DeployerException
     */
    private void checkAddonMetadata(final AddonMetaData addonMetaData) throws DeployerException {

        //check the name of the addon. If an addon with the same name have been previously deployed, throw a DeployerException
        if (this.deployedAddonNames.contains(addonMetaData.getName())) {
            throw new DeployerException("Cannot deploy " + addonMetaData.getName() + " addon. An Addon with the same " +
            "name is already deployed.");
        }

        // if JOnASVersion doesn't match, throw a DeployerException
        String JOnASVersion = Version.getNumber();
        if (!addonMetaData.isJOnASVersionSupported(JOnASVersion)){
            throw new DeployerException("Cannot deploy " + addonMetaData.getName() + " addon. JOnAS version " +
                    JOnASVersion + " doesn't match ");
        }

        //check if the jvm version match.
        String jvmVersion = System.getProperty("java.version");
        if (!addonMetaData.isJvmVersionSupported(jvmVersion)) {
            throw new DeployerException("Cannot deploy " + addonMetaData.getName() + " addon. JVM version " +
                    jvmVersion + " doesn't match ");
        }
    }

    /**
     * Deploy the addon
     * @param unpackedDeployable The unpacked addon to deploy
     * @param addonMetaData Metadata of the addon to unpacked
     * @param isAlreadyDeployed True if the deployable is already deployed. Otherwise, false.
     */
    private void deploy(final IAddonDeployable unpackedDeployable, final AddonMetaData addonMetaData,
                        final boolean isAlreadyDeployed) throws DeployerException {

        if (!isAlreadyDeployed) {
            //deploy the configuration of the Addon
            this.confDeployer.deploy(unpackedDeployable, addonMetaData);

            //TODO: BinDeployerImpl deploy

            //TODO: AntDeployer deploy
        }

        //sort the list of all deployables (persistent and unpersistent)
        List<ISortableDeployable> allDeployables = new ArrayList<ISortableDeployable>();
        allDeployables.addAll(unpackedDeployable.getDeployables());

        //deploy only deployables which are not deployed yet
        if (!allDeployables.isEmpty()) {
            Object oldTenantContext = null;
            if (isMultitenantEnabled()) {
                oldTenantContext = multitenantService.getTenantContext();
            }
            try {
                if (isMultitenantEnabled()) {
                    multitenantService.setTenantIdAndInstanceNameInContext(addonMetaData.getTenantId(), addonMetaData.getInstance());
                }
                //list of deployables which cannot be deployed
                List<IDeployable<?>> failed  = deploySortableDeployables(allDeployables);
                if (!failed.isEmpty()) {
                    throw new DeployerException("The addon " + addonMetaData.getName() + " cannot be deployed");
                }
            } finally {
                if (isMultitenantEnabled()) {
                    multitenantService.setTenantContext(oldTenantContext);
                }
            }
        }

        //start JOnAS services
        String service = addonMetaData.getService();
        if (service != null && addonMetaData.getAutostart()) {
            //if it's a JOnAS service, register the ServiceProperties component
            Dictionary<String, String> dictionary = new Hashtable<String, String>();
            dictionary.put(SERVICE_REGISTRATION_SERVICE_PROPERTY, service);
            this.serviceRegistrations.add(this.bundleContext.registerService(ServiceProperties.class.getName(),
                    addonMetaData.getServiceProperties(), dictionary));

            //Create the service configuration for the given service. It'll start the service
            try {
                this.serviceManager.startService(service, false);
            } catch (Exception e) {
                throw new DeployerException("Cannot create the configuration for the service " + service, e);
            }
        }
    }

    /**
     * Undeploy the addon
     * @param unpackedDeployable The unpacked addon to deploy
     * @param addonMetaData Metadata of the addon to unpacked
     */
    private void undeploy(final IAddonDeployable unpackedDeployable, final AddonMetaData addonMetaData)
            throws DeployerException {

        //undeploy the configuration of the Addon
        this.confDeployer.undeploy(unpackedDeployable.getArchive(), addonMetaData, this.deployerLog);

        String service = addonMetaData.getService();

        if (service != null && addonMetaData.getAutostart()) {

            //Delete the service configuration for the given service.
            try {
                this.serviceManager.stopService(service);
            } catch (Exception e) {
                throw new DeployerException("Cannot delete the configuration for the service " + service, e);
            }

            //unregister the ServiceProperties
            ServiceRegistration serviceRegistration = AddonUtil.getServiceRegistration(this.serviceRegistrations,
                    SERVICE_REGISTRATION_SERVICE_PROPERTY, addonMetaData.getService());
            if (serviceRegistration != null) {
                serviceRegistration.unregister();
                this.serviceRegistrations.remove(serviceRegistration);
            }
        }

        //sort the list (in the reverse order) of all deployables (persistent and unpersistent)
        List<ISortableDeployable> allDeployables = new ArrayList<ISortableDeployable>();
        allDeployables.addAll(unpackedDeployable.getDeployables());

        //undeploy deployables
        if (!allDeployables.isEmpty()) {
            Object oldTenantContext = null;
            List<IDeployable<?>> failed;

            if (isMultitenantEnabled()) {
                oldTenantContext = multitenantService.getTenantContext();
            }
            try {
                if (isMultitenantEnabled()) {
                    multitenantService.setTenantIdAndInstanceNameInContext(addonMetaData.getTenantId(), addonMetaData.getInstance());
                }
                failed = undeploySortableDeployables(allDeployables);
            } finally {
                multitenantService.setTenantContext(oldTenantContext);
            }

            if (!failed.isEmpty()) {
                throw new DeployerException("The addon " + addonMetaData.getName() + " cannot be undeployed");
            }
        }

        //TODO: BinDeployerImpl undeploy

        //TODO: AntDeployer undeploy
    }

    /**
     * Retrieve JOnAS configuration when the server is starting
     * @param addonsToUndeploy List of name of addon to undeploy
     */
    private void retrieveJOnASConfiguration(final Map<String, String> addonsToUndeploy) {

        //retrieve JOnAS configuration for configuration files
        this.confDeployer.retrieveJOnASConfiguration(this.deployerLog, addonsToUndeploy, true);

        //TODO: retrieve JOnAS configuration for binaries

        //TODO: retrieve JOnAS configuration for ANT tasks

        //uninstall bundles of undeployed addon
        for (Map.Entry<String, String> entry : addonsToUndeploy.entrySet()) {

            //get persistent deployables
            String unpackedDeployablePath = entry.getValue();
            IAddonDeployable unpackedDeployable = (IAddonDeployable) AddonUtil.getDeployable(this.deployableHelper,
                    new File(unpackedDeployablePath));
            updateDeployables(unpackedDeployablePath, unpackedDeployable);
            List<ISortableDeployable> sortableDeployables = unpackedDeployable.getDeployables();

            //undeploy persistent deployables (OSGi deployable, DeploymentPlan deployable)
            if (sortableDeployables != null) {

                List<ISortableDeployable> persistentSortableDeployable = new ArrayList<ISortableDeployable>();

                for (ISortableDeployable sortableDeployable: sortableDeployables) {
                    IDeployable deployable = sortableDeployable.getDeployable();
                    if (deployable instanceof OSGiDeployable /*|| deployable instanceof DeploymentPlanDeployable*/) {
                        persistentSortableDeployable.add(sortableDeployable);
                    }
                }

                //undeploy persistent deployable
                List<IDeployable<?>> failed = undeploySortableDeployables(persistentSortableDeployable);
                if (!failed.isEmpty()) {
                    logger.error("The addon " + entry.getKey() + " cannot be undeployed");
                }
            }
        }
    }

    /**
     *  Update the list of deployables (OSGi deployable, EJB3 deployable, EAR Deployable, etc...)
     *  of the unpacked deployable & copy JOnAS deployment plan to $JONAS_BASE/repositories/url-internal
     * @param unpackedDeployablePath The path to the unpacked deployable
     * @param unpackedDeployable The unpacked deployable
     * @param addonMetaData Metadata of the addon
     */
    public void updateDeployables(final String unpackedDeployablePath, final IAddonDeployable unpackedDeployable,
                                          final AddonMetaData addonMetaData) {

        final File addonDeployWorkDirectory = new File(AddonUtil.getAddonDeployWorkDirectory(unpackedDeployablePath));

        if (addonDeployWorkDirectory.isDirectory()) {

            for (File file: addonDeployWorkDirectory.listFiles()) {

                //get the deployable from the file
                IDeployable deployable = AddonUtil.getDeployable(this.deployableHelper, file);
                //get the sortable deployable from the deployable
                ISortableDeployable sortableDeployable = AddonUtil.getSortableDeployable(deployable);

                if (deployable instanceof DeploymentPlanDeployable) {

                    //2 cases:
                    // - persistent deployment plan (if it's a JOnAS deployment plan)
                    // - unpersistent deployment plan (if it's an user deployment plan
                    String service = addonMetaData.getService();

                    //case 1: persistent deployment plan (if it's a JOnAS deployment plan)
                    if (service != null) {

                        //get the JOnAS url-internal directory
                        File urlInternalDirectory = new File(AddonUtil.JONAS_ROOT_URL_INTERNAL_DIRECTORY);
                        if (!urlInternalDirectory.exists()) {
                            urlInternalDirectory.getParentFile().mkdirs();
                        }

                        String implementation = addonMetaData.getImplementation();
                        if (implementation == null) {
                            //it's a default deployment plan
                            //we need to copy this default deployment plan in $JONAS_BASE/repositories/url-internal
                            final String defaultDeploymentPlanName = AddonUtil.getDefaultDeploymentPlan(service);
                            File defaultDeploymentPlan = new File(addonDeployWorkDirectory.getAbsolutePath(),
                                    defaultDeploymentPlanName);

                            if (defaultDeploymentPlan.exists()) {
                                AddonUtil.copyFile(defaultDeploymentPlan,  new File(urlInternalDirectory.getAbsolutePath(),
                                        defaultDeploymentPlanName));
                            }

                        } else {
                            //it's a deployment plan for an implementation of an abstract service
                            //we need to copy the abstract deployment plan and it's implementation in $JONAS_BASE/repositories/url-internal
                            final String jonasAbstractDeploymentPlanName = AddonUtil.getAbstractDeploymentPlan(service);
                            final String jonasDeploymentPlanImplName = AddonUtil.getImplDeploymentPlan(service, implementation);
                            File jonasAbstractDeploymentPlan = new File(addonDeployWorkDirectory.getAbsolutePath(),
                                    jonasAbstractDeploymentPlanName);

                            if (jonasAbstractDeploymentPlan.exists()) {
                                AddonUtil.copyFile(jonasAbstractDeploymentPlan,  new File(urlInternalDirectory.getAbsolutePath(),
                                        jonasAbstractDeploymentPlanName));
                            }

                            File jonasDeploymentPlanImpl = new File(addonDeployWorkDirectory.getAbsolutePath(),
                                    jonasDeploymentPlanImplName);
                            if (jonasDeploymentPlanImpl.exists()) {
                                AddonUtil.copyFile(jonasDeploymentPlanImpl, new File(urlInternalDirectory.getAbsolutePath(),
                                        jonasDeploymentPlanImplName));
                            }
                        }
                    }

                    unpackedDeployable.addDeployable(sortableDeployable);

                } else if (!(deployable instanceof UnknownDeployable)) {
                    //cases: OSGi deployables, EARDeployable,EJB3Deployable, EJB21Deployable, WARDeployable, RARDeployable, FileDeployable
                    unpackedDeployable.addDeployable(sortableDeployable);
                }
            }
        }
    }


    /**
     *  Simply Update the list of deployables (OSGi deployable, EJB3 deployable, EAR Deployable, etc...)
     *  of the unpacked deployable
     * @param unpackedDeployablePath The path to the unpacked deployable
     * @param unpackedDeployable The unpacked deployable
     */
    public void updateDeployables(final String unpackedDeployablePath, final IAddonDeployable unpackedDeployable) {

        final File addonDeployWorkDirectory = new File(AddonUtil.getAddonDeployWorkDirectory(unpackedDeployablePath));

        if (addonDeployWorkDirectory.isDirectory()) {

            for (File file: addonDeployWorkDirectory.listFiles()) {

                //get the deployable from the file
                IDeployable deployable = AddonUtil.getDeployable(this.deployableHelper, file);
                //get the sortable deployable from the deployable
                ISortableDeployable sortableDeployable = AddonUtil.getSortableDeployable(deployable);

                if (!(deployable instanceof UnknownDeployable)) {
                    //cases: EARDeployable,EJB3Deployable, EJB21Deployable, WARDeployable, RARDeployable, FileDeployable
                    unpackedDeployable.addDeployable(sortableDeployable);
                }
            }
        }
    }

    /**
     * Sort a multi type list of sortables deployables and deploy each element which is not yet deployed
     * @param sortableDeployables The list of sortable deployables to deploy
     * @return the list of deployable which have a failed deployment
     */
    private List<IDeployable<?>> deploySortableDeployables(final List<ISortableDeployable> sortableDeployables) {

        //sort deployables
        AddonUtil.sortSortableDeployable(sortableDeployables);

        //get deployables which are not yet deployed
        List<IDeployable<?>> deployables = new ArrayList<IDeployable<?>>();
        for (ISortableDeployable sortableDeployable: sortableDeployables) {
            IDeployable deployable = sortableDeployable.getDeployable();
            try {
                if (!this.deployerManager.isDeployed(deployable)) {
                    deployables.add(sortableDeployable.getDeployable());
                }
            } catch (DeployerException e) {
                this.logger.error("Could not find if the deployable " + deployable.getShortName() + " is already deployed", e);
            } catch (UnsupportedDeployerException e) {
                this.logger.error("Could not find if the deployable " + deployable.getShortName() + " is already deployed", e);
            }
        }

        //deploy all deployables
        return getFailedDeployables(this.deployerManager.deploy(deployables));
    }

    /**
     * Sort a multi type list of sortables deployables in the reverse order and undeploy each element
     * @param sortableDeployables The list of sortable deployables to undeploy
     * @return the list of deployable which have a failed undeployment
     */
    private List<IDeployable<?>> undeploySortableDeployables(final List<ISortableDeployable> sortableDeployables) {

        //sort deployables in the reverse order
        AddonUtil.sortSortableDeployable(sortableDeployables);
        Collections.reverse(sortableDeployables);

        //get deployables
        List<IDeployable<?>> deployables = new ArrayList<IDeployable<?>>();
        for (ISortableDeployable sortableDeployable: sortableDeployables) {
            deployables.add(sortableDeployable.getDeployable());
        }

        //undeploy all deployables
        return getFailedDeployables(this.deployerManager.undeploy(deployables));
    }

    /**
     * @param deploymentReports The list of {@link IDeploymentReport}
     * @return the list of deployables with a failed deployment operation
     */
    private List<IDeployable<?>> getFailedDeployables(final List<IDeploymentReport> deploymentReports) {
        List<IDeployable<?>> failedDeployables = new ArrayList<IDeployable<?>>();
        for (IDeploymentReport deploymentReport: deploymentReports) {
            if (!deploymentReport.isDeploymentOk()) {
                failedDeployables.add(deploymentReport.getDeployable());
            }
        }
        return failedDeployables;
    }

    /**
     * {@inheritDoc}
     */
    public boolean isAddonDeployedByWorkName(final String unpackName) {

        //if the addon deployer isn't initialize yet, we shouldn't use the addon clean task
        if (!isInitialized()) {
            return true;
        }

        for (IAddonDeployable addonDeployable : this.addons.values()) {
            try {
                File unpackedFile = URLUtils.urlToFile(addonDeployable.getArchive().getURL());
                if (unpackName.equals(unpackedFile.getName())) {
                    return true;
                }
            } catch (ArchiveException e) {
                logger.debug("Cannot retrieve the name of the unpacked addon {0}", unpackName);
            }
        }
        return false;
    }

    /**
     * @return True is the addon deployer is initialized
     */
    private boolean isInitialized() {

        if (this.isInitialized == false) {
            int deployerLogSize = this.deployerLog.getEntries().size();
            this.isInitialized = (deployerLogSize == 0 || deployerLogSize == this.addons.size());
        }

        return this.isInitialized;
    }

    /**
     * Allow to validate an addon Metadata file
     * @param addonMetadataFile The addon metadata file to validate
     * @throws DeployerException {@link DeployerException}
     * @return true if the node has been valide. Otherwise, false.
     */
    private void validate(File addonMetadataFile) throws DeployerException {
        //First create a Validator from the XSD file
        InputStream xsdInputStream = getClass().getResourceAsStream(XSD_RESOURCE);

        if (xsdInputStream != null) {
            SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
            Schema schema = null;
            try {
                schema = factory.newSchema(new StreamSource(xsdInputStream));
            } catch (SAXException e) {
                throw new DeployerException("Cannot create a new Schema from the resource " + XSD_RESOURCE, e);
            }
            Validator validator = schema.newValidator();

            //then get the XML resource
            DocumentBuilderFactory xmlFactory = DocumentBuilderFactory.newInstance();
            xmlFactory.setNamespaceAware(true);
            DocumentBuilder builder = null;
            try {
                builder = xmlFactory.newDocumentBuilder();
            } catch (ParserConfigurationException e) {
                throw new DeployerException("Cannot create a new DocumentBuilder", e);
            }
            Document document = null;
            try {
                document = builder.parse(addonMetadataFile);
            } catch (SAXException e) {
                throw new DeployerException("Cannot parse the file " + addonMetadataFile.getAbsolutePath(), e);
            } catch (IOException e) {
                throw new DeployerException("Cannot parse the file " + addonMetadataFile.getAbsolutePath(), e);
            }

            //finally try to validate the XML file
            try {
                validator.validate(new DOMSource(document));
            } catch (SAXException e) {
                throw new DeployerException("Cannot validate the file " + addonMetadataFile.getAbsolutePath() + " from the XSD " +
                        "resource " + XSD_RESOURCE, e);
            } catch (IOException e) {
                throw new DeployerException("Cannot validate the Node " + addonMetadataFile.getAbsolutePath() + " from the XSD " +
                        "resource " + XSD_RESOURCE, e);
            }

        } else {
            throw new DeployerException("Cannot get the inputstream of the resource " + XSD_RESOURCE);
        }

    }

    /**
     * Whether Multitenant service is enabled
     * @return
     */
    protected boolean isMultitenantEnabled() {
        return (multitenantService != null);
    }

    /**
     * Set Multitenant service impl
     * @param multitenantService
     */
    public void setMultitenantService(MultitenantService multitenantService) {
        this.multitenantService = multitenantService;
    }

    /**
     * Unset Multitenant service impl
     */
    public void unsetMultitenantService() {
        multitenantService = null;
    }

    //TODO overidde the doDeploy method with a list of IDeployable as parameters, in order to fix errors dependencies between addons
    //TODO check if there is some troubles in work directories
    //TODO some troubles when the user remove the work directory (or files of the work directory) when the server is running
}