/**
 * 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: AddonUtil.java 22060 2012-01-29 12:43:56Z cazauxj $
 * --------------------------------------------------------------------------
 */
package org.ow2.jonas.addon.deploy.impl.util;

import org.osgi.framework.ServiceRegistration;
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.util.IAddonStructure;
import org.ow2.jonas.addon.deploy.impl.deployable.SortableDeployable;
import org.ow2.jonas.addon.deploy.impl.deployable.SortableDeployableComparator;
import org.ow2.jonas.addon.deploy.impl.deployer.AddonMetaData;
import org.ow2.jonas.lib.bootstrap.JProp;
import org.ow2.jonas.lib.util.ConfigurationConstants;
import org.ow2.jonas.properties.ServerProperties;
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.EARDeployable;
import org.ow2.util.ee.deploy.api.deployable.EJB21Deployable;
import org.ow2.util.ee.deploy.api.deployable.EJB3Deployable;
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.UnknownDeployable;
import org.ow2.util.ee.deploy.api.deployable.WARDeployable;
import org.ow2.util.ee.deploy.api.helper.DeployableHelperException;
import org.ow2.util.ee.deploy.api.helper.IDeployableHelper;
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.deploy.deployable.api.DeploymentPlanDeployable;
import org.ow2.util.plan.deploy.deployable.api.FileDeployable;
import org.ow2.util.url.URLUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.regex.Pattern;

/**
 * Defined some method for Addons
 */
public class AddonUtil {

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

    /**
     * End of line
     */
    public static final String EOL = "\n";

    /**
     * A property separator (between a property name and a property value)
     */
    public static final String PROPERTY_SEPARATOR = " ";

    /**
     * The name of the JONAS_ROOT directory
     */
    public static final String JONAS_ROOT = JProp.getJonasRoot();

    /**
     * The name of the JONAS_BASE directory.
     */
    public static final String JONAS_BASE = JProp.getJonasBase();

    /**
     * JOnAS configuration directory
      */
    public static final String JONAS_CONF_DIRECTORY = JONAS_BASE + File.separator + ConfigurationConstants.DEFAULT_CONFIG_DIR;

    /**
     * JONAS_ROOT repository directory
     */
    public static final String JONAS_ROOT_REPOSITORY = JProp.getRepositoriesRootDir();

    /**
     * JONAS_BASE repository directory
     */
    public static final String JONAS_BASE_REPOSITORY = JProp.getRepositoriesBaseDir();

    /**
     * url-internal directory name
     */
    public static final String URL_INTERNAL_DIRECTORY_NAME = "url-internal";

    /**
     * JONAS_ROOT Url internal directory
     */
    public static final String JONAS_ROOT_URL_INTERNAL_DIRECTORY = JONAS_ROOT_REPOSITORY + File.separator + URL_INTERNAL_DIRECTORY_NAME;

    /**
     * JONAS_BASE Url internal directory
     */
    public static final String JONAS_BASE_URL_INTERNAL_DIRECTORY = JONAS_BASE_REPOSITORY + File.separator + URL_INTERNAL_DIRECTORY_NAME;

    /**
     * JOnAS Addons directory
     */
    public static final String JONAS_ADDONS_DIRECTORY = JONAS_CONF_DIRECTORY + File.separator + "addons";

    /**
     * Addons log file
     */
    public static final String ADDONS_LOG_FILE = "addons.log";

    /**
     * The log for the work cleaner service
     */
    public static final String WORK_CLEANER_LOG_FILE = "workCleaner.log";

    /**
     * jonas.service string
     */
    public static final String JONAS_SERVICE = "jonas.service.";

    /**
     * jonas.service.*.class pattern
     */
    public static final Pattern JONAS_SERVICE_CLASS_PROPERTY_PATTERN = Pattern.compile("jonas.service.*.class");

    /**
     * Dot
     */
    public static final String DOT = ".";

    /**
     * Dash
     */
    public static final String DASH = "-";

    /**
     * The xml extension
     */
    public static final String XML_EXTENSION = DOT + "xml";

    /**
     * The class extension
     */
    public static final String CLASS_EXTENSION = DOT + "class";

    /**
     * Key which allow us to retrieve abstract deployment plan
     */
    public static final String ABSTRACT_DEPLOYMENT_PLAN_KEY = "base";

    /**
     * OSGi deployable priority
     */
    public static final int OSGI_DEPLOYABLE_PRIORITY = 1;

    /**
     * RAR deployable priority
     */
    public static final int RAR_DEPLOYABLE_PRIORITY = OSGI_DEPLOYABLE_PRIORITY + 1;

    /**
     * Datasource deployable priority
     */
    public static final int DATASOURCE_DEPLOYABLE_PRIORITY = RAR_DEPLOYABLE_PRIORITY + 1;

    /**
     * Deployment plan deployable priority
     */
    public static final int DEPLOYMENT_PLAN_DEPLOYABLE_PRIORITY = DATASOURCE_DEPLOYABLE_PRIORITY + 1;

    /**
     * Config Admin deployable priority
     */
    public static final int CONFIG_ADMIN_DEPLOYABLE_PRIORITY = DEPLOYMENT_PLAN_DEPLOYABLE_PRIORITY + 1;

    /**
     * EJB2 deployable priority
     */
    public static final int EJB2_DEPLOYABLE_PRIORITY = CONFIG_ADMIN_DEPLOYABLE_PRIORITY + 1;

    /**
     * EJB3 deployable priority
     */
    public static final int EJB3_DEPLOYABLE_PRIORITY = EJB2_DEPLOYABLE_PRIORITY + 1;

    /**
     * EAR  deployable priority
     */
    public static final int EAR_DEPLOYABLE_PRIORITY = EJB3_DEPLOYABLE_PRIORITY + 1;

    /**
     * WAR deployable priority
     */
    public static final int WAR_DEPLOYABLE_PRIORITY = EAR_DEPLOYABLE_PRIORITY + 1;

    /*
     * The default priority
     */
    public static final int DEFAULT_DEPLOYMENT_PLAN_PRIORITY = WAR_DEPLOYABLE_PRIORITY + 1;

    /**
     * Set AddonMetadata object from a jonas-addon-metadata.xml file
     * @param addonMetadataFile JOnAS addon metadata file
     * @param unpackedDeployablePath The path to the unpack deployable
     * @throws Exception if there is no JOnAS deployment plan associated to a JOnAS service (if the addon represents a
     * JOnAS service)
     */
    public static AddonMetaData getAddonMetadata(final File addonMetadataFile, final String unpackedDeployablePath)
            throws Exception {
        InputStream addonInputStream = null;
        try {
            addonInputStream = new FileInputStream(addonMetadataFile);
        } catch (FileNotFoundException e) {
            logger.info("Cannot create inputstream. AdddonMetaData file not found .", e);
        }

        AddonMetaData addonMetaData = new AddonMetaData();
        addonMetaData.setAddonMetaDataFile(addonMetadataFile);

         //metadata to set
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        DocumentBuilder db = null;
        try {
            db = dbf.newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            logger.error("." + e);
        }

        Document dom = null;
        try {
            dom = db.parse(addonInputStream);
        } catch (SAXException e) {
            logger.error("." + e);
        } catch (IOException e) {
            logger.error("." + e);
        }

        Element rootElement = dom.getDocumentElement();

        //set metadata
        for (Node child = rootElement.getFirstChild(); child != null; child = child.getNextSibling()) {

            if (child.getNodeType() == child.ELEMENT_NODE) {

                Node firstChild = child.getFirstChild();
                String value = "";
                if (firstChild != null) {
                    value = child.getFirstChild().getNodeValue();
                }

                if (child.getNodeName().equals("name")) {
                    addonMetaData.setName(value);
                } else if (child.getNodeName().equals("description")) {
                    addonMetaData.setDescription(value);
                } else if (child.getNodeName().equals("author")) {
                    addonMetaData.setAuthor(value);
                } else if (child.getNodeName().equals("licence")) {
                    addonMetaData.setLicence(value);
                } else if (child.getNodeName().equals("jonas-version")) {
                    try {
                        addonMetaData.setJonasVersions(getRange(value));
                    } catch (Exception e) {
                        logger.error("Cannot get the range of compatible JOnAS versions", e);
                    }
                } else if (child.getNodeName().equals("autostart")) {
                    addonMetaData.setAutostart(Boolean.parseBoolean(value));
                } else if (child.getNodeName().equals("jvm-version")) {
                    try {
                        addonMetaData.setJvmVersions(getRange(value));
                    } catch (Exception e) {
                        logger.error("Cannot get the range of compatible Jvm versions", e);
                    }
                } else if (child.getNodeName().equals("provides")) {
                    addonMetaData.setProvides(value);
                } else if (child.getNodeName().equals("requirements")) {
                    addonMetaData.setRequirements(value);
                } else if (child.getNodeName().equals("properties")) {

                    Properties properties = new Properties();

                    for (Node node = ((Element) child).getFirstChild(); node != null; node = node.getNextSibling()) {

                        if (node.getNodeType() == node.ELEMENT_NODE) {

                            Node valueNode = node.getFirstChild();

                            if (valueNode != null) {

                                String key = node.getNodeName();
                                String val = valueNode.getNodeValue();
                                properties.put(key, val);

                                //generate a jonas.services property for the addon if autostart property is true
                                if (JONAS_SERVICE_CLASS_PROPERTY_PATTERN.matcher(key).matches()) {

                                    String service = key.substring(key.indexOf(JONAS_SERVICE) + JONAS_SERVICE.length(),
                                            key.indexOf(CLASS_EXTENSION));

                                    addonMetaData.setService(service);

                                    final String defaultDeploymentPlan = AddonUtil.getDefaultDeploymentPlan(service);
                                    final String abstractDeploymentPlan = AddonUtil.getAbstractDeploymentPlan(service);

                                    final String addonDeployWorkDirectory = AddonUtil.getAddonDeployWorkDirectory(unpackedDeployablePath);
                                    List<File> files = Arrays.asList(new File(addonDeployWorkDirectory).listFiles());

                                    //searching for a default deployment plan (${service}.xml) or a abstract deployment
                                    //plan (${service}-base.xml)
                                    int i = 0;
                                    while (i < files.size() && !files.get(i).getName().equals(abstractDeploymentPlan)) {
                                        i++;
                                    }
                                    if (i < files.size() && files.get(i).getName().equals(abstractDeploymentPlan)) {

                                        //searching implementation name
                                        //implementation name is the (countTokens -1) token
                                        StringTokenizer stringTokenizer = new StringTokenizer(val, DOT);
                                        String implementation = null;
                                        int countTokens = stringTokenizer.countTokens();
                                        for (int y = 1; y < countTokens; y++) {
                                            implementation = stringTokenizer.nextElement().toString();
                                        }

                                        File implDeploymentPlan = new File(addonDeployWorkDirectory,
                                                AddonUtil.getImplDeploymentPlan(service, implementation));
                                        if (!implDeploymentPlan.exists()) {
                                            throw new Exception ("Could not find the deployment plan " + implDeploymentPlan.getAbsolutePath());
                                        } else {
                                            //if an abstract & implementation deployment plan is found, it's a JOnAS service
                                            addonMetaData.setImplementation(implementation);
                                        }
                                    } else {
                                        //Do nothing... JOnAS plans shouldn't be mandatory (we could have JOnAS bundles instead of JOnAS plans)
                                    }
                                }
                            }
                        }
                    }

                    addonMetaData.setServiceProperties(properties);

                }
            }
        }

        return addonMetaData;
    }

    /**
     * Set AddonMetadata object from a jonas-addon.xml file
     * @param addonArchive The Archive
     */
    public static File getAddonMetadataFile(final IArchive addonArchive) {
        Iterator<URL> urls = null;

        try {
            urls  = addonArchive.getResources();
        } catch (ArchiveException e) {
            logger.info("Cannot get resources from the archive .", e);
        }

        if (urls !=null) {
            while (urls.hasNext()) {
                URL url = urls.next();
                if (url.getFile().endsWith(IAddonStructure.JONAS_ADDON_METADATA)) {
                    return URLUtils.urlToFile(url);
                }
            }
        }
        return null;
    }

    /**
     * @param serverProperties ServerProperties
     * @return the work directory for Addon
     */
    public static String getAddonsWorkDirectory(final ServerProperties serverProperties) {
        return serverProperties.getWorkDirectory() + File.separator + "addons";
    }

    /**
     * @param addonWorkDirectory The directory were the addon is unpacked
     * @return the conf directory of an unpacked Addon
     */
    public static String getAddonConfWorkDirectory(final String addonWorkDirectory) {
        return addonWorkDirectory + File.separator + IAddonStructure.CONF_DIRECTORY;
    }

     /**
     * @param addonWorkDirectory The directory were the addon is unpacked
     * @return the deploy directory of an unpacked Addon
     */
    public static String getAddonDeployWorkDirectory(final String addonWorkDirectory) {
        return addonWorkDirectory + File.separator + IAddonStructure.DEPLOY_DIRECTORY;
    }

     /**
     * @param addonWorkDirectory The directory were the addon is unpacked
     * @return the ANT directory of an unpacked Addon
     */
    public static String getAddonAntWorkDirectory(final String addonWorkDirectory) {
        return addonWorkDirectory + File.separator + IAddonStructure.ANT_DIRECTORY;
    }

     /**
     * @param addonWorkDirectory The directory were the addon is unpacked
     * @return the bin directory of an unpacked Addon
     */
    public static String getAddonBinWorkDirectory(final String addonWorkDirectory) {
        return addonWorkDirectory + File.separator + IAddonStructure.BIN_DIRECTORY;
    }

    /**
     * @param serverProperties ServerProperties
     * @return The path to the addon log file
     */
    public static String getAddonLogFile(final ServerProperties serverProperties) {
        return getAddonsWorkDirectory(serverProperties) + File.separator + ADDONS_LOG_FILE;
    }

    /**
     * @param serverProperties ServerProperties
     * @return The path to the addon work cleaner log file
     */
    public static String getWorkCleanerLogFile(final ServerProperties serverProperties) {
        return getAddonsWorkDirectory(serverProperties) + File.separator + WORK_CLEANER_LOG_FILE;
    }

    /**
     * @param unpackedDeployable The unpacked deployable
     * @return the path to the unpacked deployable
     */
    public static String getAddonWorkDirectory(final IAddonDeployable unpackedDeployable) {
        try {
            return unpackedDeployable.getArchive().getURL().getPath();
        } catch (ArchiveException e) {
            logger.error("Cant get the unpacked URL", e);
            return null;
        }
    }

    /**
     * @param addonName The name of the addon
     * @return the addon directory path (in JONAS_BASE/conf/...)
     */
    public static String getAddonDirectoryPath(final String addonName) {
        return AddonUtil.JONAS_ADDONS_DIRECTORY + File.separator + addonName;
    }

    /**
     * @param file A file
     * @return the deployable assoaciated to the file
     */
    public static IDeployable getDeployable(final IDeployableHelper deployableHelper, final File file) {

        IArchive archive = ArchiveManager.getInstance().getArchive(file);
        if (archive == null) {
            logger.warn("Ignoring invalid file ''{0}''", file);
        }

        try {
            return deployableHelper.getDeployable(archive);
        } catch (DeployableHelperException e) {
            logger.error("Cannot get a deployable for the archive ''{0}''", archive, e);
            return null;
        }
    }

    /**
     * @param service The name of the JOnAS service
     * @return the name of the JOnAS default deployment plan
     */
    public static String getDefaultDeploymentPlan(final String service) {
        return service + XML_EXTENSION;
    }

    /**
     * @param service The name of the JOnAS service
     * @return the name of the JOnAS abstract deployment plan
     */
    public static String getAbstractDeploymentPlan(final String service) {
        return service + DASH + ABSTRACT_DEPLOYMENT_PLAN_KEY + XML_EXTENSION;
    }

    /**
     * @param service The name of the JOnAS service
     * @param implementation The name of the implementation service
     * @return the name of the JOnAS implementation deployment plan
     */
    public static String getImplDeploymentPlan(final String service, final String implementation) {
        return service + DASH + implementation + XML_EXTENSION;
    }

    /**
     * @param sourceFile The source file
     * @param destFile The destination file
     */
    public static void copyFile(final File sourceFile, final File destFile) {
        try {
            FileUtils.copyFile(sourceFile.getAbsolutePath(), destFile.getAbsolutePath());
        } catch (FileUtilsException e) {
            logger.error("Cannot copy file " + sourceFile.getAbsolutePath() + " to " +
                    destFile.getAbsolutePath() + ".", e);
        }
    }

    /**
     * Sort SortableDeployable list by priority
     * @param sortableDeployables the list of SortableDeployable
     */
    public static void sortSortableDeployable(final List<ISortableDeployable> sortableDeployables){
        Collections.sort(sortableDeployables, new SortableDeployableComparator());
    }

    /**
     * @param deployable The deployable
     * @return a SortableDeployable object associate tho the given deployable
     */
    public static ISortableDeployable getSortableDeployable(final IDeployable deployable) {
        Integer priority = null;
        if (deployable instanceof OSGiDeployable) {
            priority = AddonUtil.OSGI_DEPLOYABLE_PRIORITY;
        } else if (deployable instanceof DeploymentPlanDeployable) {
            priority = AddonUtil.DEPLOYMENT_PLAN_DEPLOYABLE_PRIORITY;
        } else if (deployable instanceof EARDeployable) {
            priority = AddonUtil.EAR_DEPLOYABLE_PRIORITY;
        } else if (deployable instanceof EJB3Deployable) {
            priority = AddonUtil.EJB3_DEPLOYABLE_PRIORITY;
        } else if (deployable instanceof EJB21Deployable) {
            priority = AddonUtil.EJB2_DEPLOYABLE_PRIORITY;
        } else if (deployable instanceof WARDeployable) {
            priority = AddonUtil.WAR_DEPLOYABLE_PRIORITY;
        } else if (deployable instanceof RARDeployable) {
            priority = AddonUtil.RAR_DEPLOYABLE_PRIORITY;
        } else if (deployable instanceof FileDeployable) {
            priority = AddonUtil.CONFIG_ADMIN_DEPLOYABLE_PRIORITY;
        } else if (!(deployable instanceof UnknownDeployable)) {
            priority = AddonUtil.DEFAULT_DEPLOYMENT_PLAN_PRIORITY;
        }
        return  new SortableDeployable(deployable, priority);
    }

    /**
     * @param rangeProperty A range property
     * @return a range
     */
    private static Range getRange(final String rangeProperty) throws Exception {
        Range range = null;

        //jonas version format: {versionMin,versionMax]
        //with '{' for incluion and '[' for exclusion.
        if ((rangeProperty.startsWith(Range.EXCLUSION_OPEN_TAG) || rangeProperty.startsWith(Range.INCLUSION_OPEN_TAG)) &&
            (rangeProperty.endsWith(Range.EXCLUSION_CLOSE_TAG) || rangeProperty.endsWith(Range.INCLUSION_CLOSE_TAG))) {

            final boolean isInclusionOpenTag = rangeProperty.startsWith(Range.INCLUSION_OPEN_TAG);
            final boolean isInclusionCloseTag = rangeProperty.endsWith(Range.INCLUSION_CLOSE_TAG);

            //it's a well formed range metadata
            StringTokenizer stringTokenizer = new StringTokenizer(rangeProperty.substring(1, rangeProperty.length() - 1), ",");
            int countTockens = stringTokenizer.countTokens();

            if ((countTockens == 1 && (isInclusionOpenTag != isInclusionCloseTag)) || (countTockens > 2) || (countTockens < 0)) {
                throw new Exception("Cannot find a valid range for property " + rangeProperty);
            }

            range = new Range(isInclusionOpenTag, isInclusionCloseTag, stringTokenizer.nextToken());
            if (countTockens == 2) {
                range.setMaxRange((String) stringTokenizer.nextToken());
            }
        }

        return range;
    }

    /**
     * Copy all files of a directory into an other directory
     * @param originalDirectoryPath The path to the original directory
     * @param targetDirectoryPath The path to the target directory
     */
    public static void copyFiles(final String originalDirectoryPath, final String targetDirectoryPath) {
        File configurationDirectory = new File(originalDirectoryPath);

        if (configurationDirectory.exists()) {

            //copy others configuration files
            for (File configurationFile: configurationDirectory.listFiles()) {
                String filename = configurationFile.getName();
                String confFile = targetDirectoryPath + File.separator + filename;

                try {
                    FileUtils.copyFile(configurationFile.getAbsolutePath(), confFile);
                } catch (FileUtilsException e) {
                    logger.error("Cannot copy file " + filename + ".", e);
                }
            }
        }
    }

    /**
     *
     * @param serviceRegistrations The list of ServiceRegistration
     * @param propertyKey The key
     * @param propertyValue The value
     * @return the service registration which  property value of the key parameter equals to the property value parameter
     */
    public static ServiceRegistration getServiceRegistration(final List<ServiceRegistration> serviceRegistrations,
                                                       final String propertyKey, final String propertyValue) {
        for (ServiceRegistration serviceRegistration: serviceRegistrations) {
            if (serviceRegistration.getReference().getProperty(propertyKey).equals(propertyValue)) {
                return serviceRegistration;
            }
        }
        return null;
    }
}
