/**
 * 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 1any 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.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.management.ObjectName;

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

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

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

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

    /**
     * List of contexts' JMX names.
     */
    private Map<String, List<String>> contexts = new HashMap<String, List<String>>();

    /**
     * Reference to a 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.
     */
    @Override
    protected void doStart() {
        VirtualJNDIBinding.initialize(this, jmxService);
        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 Default policy, as set via JMX.
     */
    public String getDefaultPolicy() {
        return defaultPolicy;
    }

    /**
     * @param defaultPolicy Default policy to set.
     */
    public void setDefaultPolicy(final String defaultPolicy) {
        this.defaultPolicy = defaultPolicy;
    }

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

    /**
     * @param appName Name of the application the context belongs to.
     * @param context Virtual context's JMX name to add.
     */
    public void addContext(final String appName, final String context) {
        List<String> contexts = this.contexts.get(appName);
        if (contexts == null) {
            contexts = new ArrayList<String>();
            this.contexts.put(appName, contexts);
        }
        contexts.add(context);
    }

    /**
     * @param context Virtual context's JMX name to remove.
     */
    public void removeContext(final String context) {
        List<Map.Entry<String, List<String>>> entries = new ArrayList<Map.Entry<String, List<String>>>(this.contexts.entrySet());
        for (Map.Entry<String, List<String>> entry : entries) {
            entry.getValue().remove(context);
            if (entry.getValue().size() < 1) {
                this.contexts.remove(entry.getKey());
            }
        }
    }

    /**
     * @return Names of all deployed versioned applications.
     */
    public String[] getContextNames() {
        Set<String> names = this.contexts.keySet();
        return names.toArray(new String[names.size()]);
    }

    /**
     * @param name Name of the versioned application to get the details for, see
     * @link{VersioningServiceBase#getContextNames()}.
     * @return Versioned context's management beans' names, null if name is not
     *         found.
     */
    public String[] getContextBeans(final String name) {
        List<String> beans = this.contexts.get(name);
        if (beans == null) {
            return null;
        } else {
            return beans.toArray(new String[beans.size()]);
        }
    }

    /**
     * @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 " + 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 = originalDeployable.getArchive().getMetadata().get("Implementation-Version");
        if (versionID != null) {
            final String allowedSpecialChars = "-_,.";
            boolean versionIDFiltered = false;
            StringBuffer versionIDFilter = new StringBuffer();
            for (char c : versionID.toCharArray()) {
                if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')
                        || allowedSpecialChars.indexOf(c) >= 0) {
                    versionIDFilter.append(c);
                } else {
                    versionIDFiltered = true;
                    versionIDFilter.append('-');
                }
            }
            if (versionIDFiltered) {
                versionID = versionIDFilter.toString();
                logger.info("Invalid characters detected in version ID for archive "
                        + originalDeployable.getArchive().getName() + ", setting version ID to " + versionID);
            }
            versionID = "-version" + versionID;
        }
        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 " + 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 applicationName = originalDeployable.getModuleName();

        String prefix = applicationName;
        String versionID = getVersionID(originalDeployable);
        if (versionID != null && !applicationName.endsWith(versionID)) {
            prefix += versionID;
        }
        prefix += '/';

        final String allowedSpecialChars = "_./";
        boolean jndiPrefixFiltered = false;
        StringBuffer jndiPrefixFilter = new StringBuffer();
        for (char c : prefix.toCharArray()) {
            if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')
                    || allowedSpecialChars.indexOf(c) >= 0) {
                jndiPrefixFilter.append(c);
            } else {
                jndiPrefixFiltered = true;
                jndiPrefixFilter.append('_');
            }
        }
        if (jndiPrefixFiltered) {
            prefix = jndiPrefixFilter.toString();
            logger.info("Invalid characters detected in the JNDI prefix for application " + applicationName
                    + ", setting JNDI prefix to " + prefix);
        }

        return prefix;
    }

    /**
     * @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 = (IDeployable<?>) originalDeployable.getOriginalDeployable();
        }
        return originalDeployable;
    }

    /**
     * Creates JNDI binding management beans for a given JNDI prefix.
     * @param appName Name of the application new beans should belong to
     * @param version Application version ID, may be null.
     * @param prefix JNDI prefix to look for.
     */
    public void createJNDIBindingMBeans(final String appName, final String version, final String prefix) {
        String applicationName;
        if (version != null && appName.endsWith(version)) {
            applicationName = appName.substring(0, appName.length() - version.length());
        } else {
            applicationName = appName;
        }
        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("-version" + versionID, "");
            }
            return moduleName;
        } catch (DeployableHelperException e) {
            logger.info("Failed creating deployable for file " + file, e);
            return null;
        }
    }
}
