/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2008 Bull S.A.S.
 * Contact: jonas-team@ow2.org
 *
 * Application Versioning System
 * Copyright (C) 2008 France Telecom R&D
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.versioning.internal;

import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.jar.Attributes;

import javax.management.ObjectName;

import org.ow2.jonas.jmx.JmxService;
import org.ow2.jonas.registry.RegistryService;
import org.ow2.jonas.lib.service.AbsServiceImpl;
import org.ow2.jonas.versioning.VersioningService;
import org.ow2.jonas.versioning.VersioningServiceBase;
import org.ow2.jonas.versioning.contexts.VirtualJNDIBinding;
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.helper.DeployableHelperException;
import org.ow2.util.ee.deploy.impl.helper.DeployableHelper;
import org.ow2.util.log.Log;
import org.ow2.util.log.LogFactory;
import org.ow2.util.url.URLUtils;

/**
 * Implements the versioning service.
 * @see org.ow2.jonas.versioning.VersioningService
 *
 * @author Frederic Germaneau
 * @author S. Ali Tokmen
 */
public class VersioningServiceImpl extends AbsServiceImpl implements VersioningService, VersioningServiceImplMBean {

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

    /**
     * Whether versioning is enabled.
     */
    private boolean versioningEnabled = true;

    /**
     * Default deployment policy to use when deploying a new version of a
     * versioned application, can be changed via JMX.
     */
    private String defaultPolicy = "Reserved";

    /**
     * The filtered content types, each type is another element of the
     *         returned array.
     */
    private String[] filteredContentTypes = new String[0];

    /**
     * Registry service.
     */
    private RegistryService registryService = null;

    /**
     * @param registryService Registry service to set.
     */
    public void setRegistryService(final RegistryService registryService) {
        this.registryService = registryService;
    }

    /**
     * Reference to an MBean server.
     */
    private JmxService jmxService = null;

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

    /**
     * @param domainName Server's domain name.
     * @return JMX name of the versioning service management bean.
     */
    public static ObjectName getObjectName(final String domainName) {
        try {
            return ObjectName.getInstance(domainName + ":type=service,name=versioning");
        } catch (Exception e) {
            throw new IllegalStateException("Cannot get versioning service", e);
        }
    }

    /**
     * Registers the management bean with JMX.
     */
    @Override
    protected void doStart() {
        VirtualJNDIBinding.initialize(jmxService, registryService, this);
        jmxService.registerMBean(this, getObjectName(getDomainName()));
        logger.info("Versioning service management bean has been registered successfully");
    }

    /**
     * Unregisters the management bean from JMX.
     */
    @Override
    protected void doStop() {
        jmxService.unregisterMBean(getObjectName(getDomainName()));
        logger.info("Versioning service management bean has been unregistered successfully");
    }

    /**
     * @return Possible policies.
     */
    public String[] getPolicies() {
        return VersioningServiceBase.POLICIES;
    }

    /**
     * @return Whether versioning is started and enabled.
     */
    public boolean isVersioningEnabled() {
        return isStarted() && versioningEnabled;
    }

    /**
     * @param versioningEnabled Whether versioning is enabled.
     */
    public void setVersioningEnabled(final boolean versioningEnabled) {
        this.versioningEnabled = versioningEnabled;
    }

    /**
     * @return Default policy, as set via JMX.
     */
    public String getDefaultDeploymentPolicy() {
        return defaultPolicy;
    }

    /**
     * @param defaultPolicy Default deployment policy to set.
     */
    public void setDefaultDeploymentPolicy(final String defaultPolicy) {
        if (!VersioningServiceBase.DEFAULT.equals(defaultPolicy) && !VersioningServiceBase.DISABLED.equals(defaultPolicy)
                && !VersioningServiceBase.RESERVED.equals(defaultPolicy)
                && !VersioningServiceBase.PRIVATE.equals(defaultPolicy)) {
            throw new IllegalArgumentException("Invalid policy : " + defaultPolicy);
        }

        this.defaultPolicy = defaultPolicy;
    }

    /**
     * @return The filtered content types. Each type is separated with a comma.
     */
    public String getFilteredContentTypes() {
        if (filteredContentTypes == null || filteredContentTypes.length == 0) {
            return "";
        } else {
            StringBuilder sb = new StringBuilder(filteredContentTypes[0]);
            for (int i = 1; i < filteredContentTypes.length; i++) {
                sb.append(',');
                sb.append(filteredContentTypes[i]);
            }
            return sb.toString();
        }
    }

    /**
     * @param filteredContentTypes The filtered content types to set. Each type
     *        must be separated with a comma, any spacing character between
     *        each type is omitted.
     */
    public void setFilteredContentTypes(final String filteredContentTypes) {
        if (filteredContentTypes == null || filteredContentTypes.length() == 0) {
            this.filteredContentTypes = new String[0];
        } else {
            List<String> filteredContentTypesList = new ArrayList<String>();
            StringTokenizer tokenizer = new StringTokenizer(filteredContentTypes, ",");
            while (tokenizer.hasMoreTokens()) {
                String filteredContentType = tokenizer.nextToken();
                filteredContentType = filteredContentType.trim();
                filteredContentTypesList.add(filteredContentType);
            }
            String[] filteredContentTypesArray = new String[filteredContentTypesList.size()];
            filteredContentTypesArray = filteredContentTypesList.toArray(filteredContentTypesArray);
            this.filteredContentTypes = filteredContentTypesArray;
        }
    }

    /**
     * @return The filtered content types, each type is another element of the
     *         returned array. DO NOT MODIFY THE RETURNED ARRAY !!
     */
    public String[] getFilteredContentTypesArray() {
        return filteredContentTypes;
    }

    /**
     * @param file JAR, WAR or EAR file to read the version number from.
     * @return Version number of file, null if none found.
     */
    public String getVersionNumber(final File file) {
        try {
            IArchive archive = ArchiveManager.getInstance().getArchive(file);
            IDeployable<?> deployable = DeployableHelper.getDeployable(archive);
            return getVersionNumber(deployable);
        } catch (DeployableHelperException e) {
            logger.info("Failed creating deployable for file ''{0}''", file, e);
            return null;
        }
    }

    /**
     * @param url JAR, WAR or EAR file to read the version number from.
     * @return Version number of url, null if none found.
     */
    public String getVersionNumber(final URL url) {
        return getVersionNumber(URLUtils.urlToFile(url));
    }

    /**
     * @param deployable JAR, WAR or EAR object to read the version number from.
     * @return Version number of deployable, null if none found.
     */
    public String getVersionNumber(final IDeployable<?> deployable) {
        IDeployable<?> originalDeployable = getOriginalDeployable(deployable);
        String versionNumber = originalDeployable.getArchive().getMetadata().get(Attributes.Name.IMPLEMENTATION_VERSION.toString());
        return versionNumber;
    }

    /**
     * @param file JAR, WAR or EAR file to read the version ID from.
     * @return Version ID of file, null if none found.
     */
    public String getVersionID(final File file) {
        try {
            IArchive archive = ArchiveManager.getInstance().getArchive(file);
            IDeployable<?> deployable = DeployableHelper.getDeployable(archive);
            return getVersionID(deployable);
        } catch (DeployableHelperException e) {
            logger.info("Failed creating deployable for file ''{0}''", file, e);
            return null;
        }
    }

    /**
     * @param url JAR, WAR or EAR file to read the version ID from.
     * @return Version ID of url, null if none found.
     */
    public String getVersionID(final URL url) {
        return getVersionID(URLUtils.urlToFile(url));
    }

    /**
     * @param deployable JAR, WAR or EAR object to read the version ID from.
     * @return Version ID of deployable, null if none found.
     */
    public String getVersionID(final IDeployable<?> deployable) {
        IDeployable<?> originalDeployable = getOriginalDeployable(deployable);
        String versionID = getVersionNumber(originalDeployable);
        if (versionID != null) {
            // Do it like Maven2: artifact-version.extension
            versionID = "-" + filterIdentifier(versionID, "version ID", originalDeployable.getArchive().getName());
        }
        return versionID;
    }

    /**
     * @param file JAR or EAR object to create a JNDI prefix from.
     * @return JNDI prefix for file, null if none found.
     */
    public String getPrefix(final File file) {
        try {
            IArchive archive = ArchiveManager.getInstance().getArchive(file);
            IDeployable<?> deployable = DeployableHelper.getDeployable(archive);
            return getPrefix(deployable);
        } catch (DeployableHelperException e) {
            logger.info("Failed creating deployable for file ''{0}''", file, e);
            return null;
        }
    }

    /**
     * @param url JAR or EAR object to create a JNDI prefix from.
     * @return JNDI prefix for url, null if none found.
     */
    public String getPrefix(final URL url) {
        return getPrefix(URLUtils.urlToFile(url));
    }

    /**
     * @param deployable JAR or EAR object to create a JNDI prefix from.
     * @return JNDI prefix for deployable, null if none found.
     */
    public String getPrefix(final IDeployable<?> deployable) {
        IDeployable<?> originalDeployable = getOriginalDeployable(deployable);

        String version = originalDeployable.getArchive().getMetadata().get(Attributes.Name.IMPLEMENTATION_VERSION.toString());
        String versionID = getVersionID(originalDeployable);
        if (version == null || versionID == null) {
            return null;
        }

        String applicationName = originalDeployable.getModuleName();
        String prefix = applicationName;
        if (applicationName.endsWith(versionID)) {
            // Nothing to do in this case
        } else if (applicationName.endsWith("-" + version)) {
            // Prefix contains the version with a "-", but not the version ID; replace
            prefix = prefix.replace("-" + version, versionID);
        } else if (applicationName.endsWith(version)) {
            // Prefix contains the version, but not the version ID; replace
            prefix = prefix.replace(version, versionID);
        } else {
            // Prefix doesn't contain any version info
            prefix += versionID;
        }

        return filterIdentifier(prefix, "JNDI prefix", applicationName) + '/';
    }

    /**
     * @param deployable Deployable to get the original deployable from
     *        (recursive).
     * @return Original deployable.
     */
    private IDeployable<?> getOriginalDeployable(final IDeployable<?> deployable) {
        IDeployable<?> originalDeployable = deployable;
        while (originalDeployable.getOriginalDeployable() != null) {
            originalDeployable = originalDeployable.getOriginalDeployable();
        }
        return originalDeployable;
    }

    /**
     * @param identifier Identifier to filter.
     * @param type Type of identifier, will be used when warning.
     * @param file File name of archive.
     * @return Filtered identifier, for the list of filtered characters see
     *         {@link VersioningService#ALLOWED_SPECIAL_CHARS}.
     */
    private String filterIdentifier(final String identifier, final String type, final String file) {
        boolean identifierFiltered = false;
        StringBuffer identifierFilter = new StringBuffer();
        for (char c : identifier.toCharArray()) {
            if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')
                    || VersioningService.ALLOWED_SPECIAL_CHARS.indexOf(c) >= 0) {
                identifierFilter.append(c);
            } else {
                identifierFiltered = true;
                identifierFilter.append('-');
            }
        }
        if (identifierFiltered) {
            String filteredIdentifier = identifierFilter.toString();
            logger.debug("Invalid characters detected in {0} for archive {1}, setting {0} to {2}",
                         type, file, filteredIdentifier);
            return filteredIdentifier;
        } else {
            return identifier;
        }
    }

    /**
     * Creates JNDI binding management beans for a given archive.
     * @param deployable JAR, WAR or EAR object to read the JNDI prefix from.
     */
    public void createJNDIBindingMBeans(final IDeployable<?> deployable) {
        IDeployable<?> originalDeployable = getOriginalDeployable(deployable);

        String versionID = getVersionID(originalDeployable);
        if (versionID == null) {
            logger.warn("IDeployable {0} is not versioned! Ignoring call to createJNDIBindingMBeans",
                        deployable.toString());
            return;
        }

        String applicationName = originalDeployable.getModuleName();
        if (applicationName.endsWith(versionID)) {
            applicationName = applicationName.replace(versionID, "");
        }
        String prefix = getPrefix(originalDeployable);

        VirtualJNDIBinding.createJNDIBindingMBeans(applicationName, prefix);
    }

    /**
     * Removes JNDI binding management beans that are not in the JNDI directory
     * anymore.
     */
    public void garbageCollectJNDIBindingMBeans() {
        VirtualJNDIBinding.garbageCollectJNDIBindingMBeans();
    }

    /**
     * @param url JAR, WAR or EAR object to get the base name for. The base name
     *        is used when creating the JNDI naming prefix.
     * @return Base name for URL, null if none found.
     */
    public String getBaseName(final URL url) {
        File file = URLUtils.urlToFile(url);
        try {
            IArchive archive = ArchiveManager.getInstance().getArchive(file);
            IDeployable<?> originalDeployable = getOriginalDeployable(DeployableHelper.getDeployable(archive));
            String moduleName = originalDeployable.getModuleName();
            String versionID = getVersionID(originalDeployable);
            if (versionID != null) {
                moduleName = moduleName.replace(versionID, "");
            }
            return moduleName;
        } catch (DeployableHelperException e) {
            logger.info("Failed creating deployable for file {0}", file, e);
            return null;
        }
    }
}
