/**
 * 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.contexts;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.naming.Context;
import javax.naming.LinkRef;
import javax.naming.NameClassPair;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.ow2.jonas.jmx.JmxService;
import org.ow2.jonas.registry.RegistryService;
import org.ow2.jonas.versioning.VersioningService;
import org.ow2.jonas.versioning.VersioningServiceBase;

/**
 * Handles virtual JNDI binding management.
 * @author S. Ali Tokmen
 */
public class VirtualJNDIBinding implements VirtualJNDIBindingMBean {
    /**
     * Logger.
     */
    private static Log logger = LogFactory.getLog(VirtualJNDIBinding.class);

    /**
     * The JMX server.
     */
    private static JmxService jmxService = null;

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

    /**
     * Versioning service.
     */
    private static VersioningService versioningService = null;

    /**
     * All JNDI bindings.
     */
    private static List<VirtualJNDIBinding> bindings = new ArrayList<VirtualJNDIBinding>();

    /**
     * Application name.
     */
    private String applicationName = null;

    /**
     * Management bean name for this virtual JNDI binding.
     */
    private String mBeanName;

    /**
     * Map of prefixes and associated policy.
     */
    private Map<String, String> policies = new HashMap<String, String>();

    /**
     * Map of prefixes and associated list of beans. Note that the bean names
     * don't include prefixes.
     */
    private Map<String, List<String>> beans = new HashMap<String, List<String>>();

    /**
     * Saves static server information.
     *
     * @param jmx JMX service.
     * @param registry Registry service.
     * @param versioning Versioning service.
     */
    public static void initialize(final JmxService jmx, final RegistryService registry, final VersioningService versioning) {
        jmxService = jmx;
        registryService = registry;
        versioningService = versioning;
    }

    /**
     * Creates and registers a virtual JNDI binding.
     * @param appName J2EE application name.
     * @param prefix The first context prefix, will be mapped as default.
     */
    protected VirtualJNDIBinding(String appName, final String prefix) {
        if (appName == null) {
            appName = "none";
        }
        this.applicationName = appName;
        // J2EEApplication=none since this virtual JNDI binding manager
        // is shared between multiple J2EE applications
        mBeanName = jmxService.getDomainName() + ":j2eeType=EJBModule,J2EEServer=" + jmxService.getJonasServerName()
                + ",J2EEApplication=none,name=VirtualContainer-" + appName + ",virtualContext=true";
        logger.debug("New VirtualJNDIBinding: registering MBean with name : " + mBeanName);
        jmxService.registerMBean(this, mBeanName);
        addContext(prefix, VersioningServiceBase.DEFAULT);
        bindings.add(this);
    }

    /**
     * Adds a JNDI prefix to this virtual JNDI naming manager using a policy.
     * @param prefix Prefix instance to add.
     * @param policy Prefix' policy.
     * @throws IllegalArgumentException Policy is invalid.
     */
    public void addContext(final String prefix, final String policy) throws IllegalArgumentException {
        if (!VersioningServiceBase.DEFAULT.equals(policy) && !VersioningServiceBase.DISABLED.equals(policy)
                && !VersioningServiceBase.RESERVED.equals(policy) && !VersioningServiceBase.PRIVATE.equals(policy)) {
            throw new IllegalArgumentException("Invalid policy : " + policy);
        }

        if (hasContext(prefix)) {
            throw new IllegalArgumentException("JNDI prefix " + prefix + " already bound!");
        }

        List<String> beans = new ArrayList<String>();

        try {
            Context jndi = VirtualJNDIBinding.registryService.getRegistryContext();
            NamingEnumeration<NameClassPair> elements = jndi.list("");
            while (elements.hasMoreElements()) {
                NameClassPair current = elements.nextElement();
                String name;
                try {
                    name = current.getNameInNamespace();
                } catch (Exception e) {
                    name = current.getName();
                }
                if (prefix.length() > 0 && name.contains(prefix)) {
                    name = name.replace(prefix, "");
                    beans.add(name);
                }
            }
        } catch (NamingException e) {
            throw new IllegalStateException("Failed creating JNDI binding MBeans", e);
        }

        this.beans.put(prefix, beans);
        rebindContext(prefix, policy);
    }

    /**
     * Rebinds a prefix.
     * @param prefix Prefix to rebind.
     * @param policy New policy.
     * @return true if succeeded, false otherwise.
     * @throws IllegalArgumentException Policy is invalid.
     */
    public boolean rebindContext(final String prefix, final String policy) throws IllegalArgumentException {
        if (!VersioningServiceBase.DEFAULT.equals(policy) && !VersioningServiceBase.DISABLED.equals(policy)
                && !VersioningServiceBase.RESERVED.equals(policy) && !VersioningServiceBase.PRIVATE.equals(policy)) {
            throw new IllegalArgumentException("Invalid policy : " + policy);
        }

        List<String> beanNames = this.beans.get(prefix);
        if (beanNames == null) {
            return false;
        }

        // If the prefix' old policy was DEFAULT or the new policy to apply is
        // DEFAULT, we need to unbind the old DEFAULT
        String oldDefault = null;
        String oldPolicy = this.policies.get(prefix);
        if (VersioningServiceBase.DEFAULT.equals(oldPolicy)) {
            oldDefault = prefix;
        } else if (VersioningServiceBase.DEFAULT.equals(policy)) {
            for (Map.Entry<String, String> entry : this.policies.entrySet()) {
                if (entry.getValue().equals(VersioningServiceBase.DEFAULT)) {
                    oldDefault = entry.getKey();
                    break;
                }
            }
        }
        if (oldDefault != null) {
            for (String beanName : this.beans.get(oldDefault)) {
                try {
                    Context jndi = VirtualJNDIBinding.registryService.getRegistryContext();
                    jndi.unbind(beanName);
                } catch (NamingException ignored) {
                    // Ignore
                }
            }
            this.policies.put(oldDefault, VersioningServiceBase.DISABLED);
        }

        this.policies.put(prefix, policy);
        checkJNDIBindings();

        return true;
    }

    /**
     * Checks whether a given prefix has been registered.
     * @param prefix Prefix to check.
     * @return true if found, false otherwise.
     */
    public boolean hasContext(final String prefix) {
        return (this.policies.containsKey(prefix) && this.beans.containsKey(prefix));
    }

    /**
     * Removes a prefix, including all {@link VirtualJNDIBindingMBean#DEFAULT}
     * bindings for this prefix. If no more JNDI prefixes left, will also remove
     * the JNDI binding management bean.
     * @param prefix Prefix to remove.
     * @return true if succeeded, false otherwise.
     */
    public boolean removePrefix(final String prefix) {
        rebindContext(prefix, VersioningServiceBase.DISABLED);
        this.policies.remove(prefix);
        this.beans.remove(prefix);
        if (this.beans.size() == 0 || this.policies.size() == 0) {
            return removeVirtualContext();
        }
        return true;
    }

    /**
     * Removes this virtual JNDI binding.
     * @return true if succeeded, false otherwise.
     */
    public boolean removeVirtualContext() {
        List<Map.Entry<String, String>> prefixes = new ArrayList<Map.Entry<String, String>>(getContexts().entrySet());
        for (Map.Entry<String, String> prefix : prefixes) {
            rebindContext(prefix.getKey(), VersioningServiceBase.DISABLED);
            this.policies.remove(prefix);
            this.beans.remove(prefix);
        }
        try {
            jmxService.unregisterMBean(ObjectName.getInstance(mBeanName));
        } catch (MalformedObjectNameException e) {
            logger.error("Error unbinding Virtual JNDI Binding Manager " + mBeanName, e);
            return false;
        }
        bindings.remove(this);
        return true;
    }

    /**
     * Creates or adds into an existing binding a prefix.
     * @param appName J2EE application name.
     * @param prefix Prefix to add.
     */
    public static void createJNDIBindingMBeans(final String appName, final String prefix) {
        if (jmxService == null || versioningService == null || registryService == null) {
            throw new IllegalStateException("Services not set: call VirtualJNDIBinding.initialize");
        }

        for (VirtualJNDIBinding binding : bindings) {
            if (binding.applicationName.equals(appName)) {
                binding.addContext(prefix, versioningService.getDefaultDeploymentPolicy());
                return;
            }
        }
        new VirtualJNDIBinding(appName, prefix);
    }

    /**
     * Removes JNDI binding management beans that are not in the JNDI directory
     * anymore.
     */
    public static void garbageCollectJNDIBindingMBeans() {
        if (jmxService == null || versioningService == null || registryService == null) {
            throw new IllegalStateException("Services not set: call VirtualJNDIBinding.initialize");
        }

        List<String> beansOnJNDI = new ArrayList<String>();
        try {
            Context jndi = VirtualJNDIBinding.registryService.getRegistryContext();
            NamingEnumeration<NameClassPair> elements = jndi.list("");
            while (elements.hasMoreElements()) {
                NameClassPair current = elements.nextElement();
                beansOnJNDI.add(current.getName());
            }
        } catch (NamingException e) {
            throw new IllegalStateException("Failed garbage collecting JNDI binding MBeans", e);
        }

        for (VirtualJNDIBinding binding : bindings.toArray(new VirtualJNDIBinding[bindings.size()])) {
            gcJNDIBinding(binding, beansOnJNDI);
        }
    }

    /**
     * Garbage-collects (if necessary) a virtual JNDI binding management bean.
     * @param binding Virtual binding to check.
     * @param beansOnJNDI List of JNDI beans to check against.
     */
    private static void gcJNDIBinding(final VirtualJNDIBinding binding, final List<String> beansOnJNDI) {
        for (Map.Entry<String, List<String>> beanList : binding.beans.entrySet()) {
            for (String beanName : beanList.getValue()) {
                if (!beansOnJNDI.contains(beanList.getKey() + beanName)) {
                    binding.removePrefix(beanList.getKey());
                    binding.checkJNDIBindings();
                    return;
                }
            }
        }
    }

    /**
     * Checks that the default JNDI binding is still present.
     */
    public void checkJNDIBindings() {
        if (jmxService == null || versioningService == null || registryService == null) {
            throw new IllegalStateException("Services not set: call VirtualJNDIBinding.initialize");
        }

        try {
            for (Map.Entry<String, String> entry : this.policies.entrySet()) {
                if (entry.getValue().equals(VersioningServiceBase.DEFAULT)) {
                    Context jndi = VirtualJNDIBinding.registryService.getRegistryContext();
                    for (String beanName : this.beans.get(entry.getKey())) {
                        // If we bind an alias, CAROL lookups fail. As a
                        // result, we lookup for the link (this way, the
                        // JNDI link is not "executed" -for example,
                        // stateful beans don't get instanciated) and bind
                        // it as the non-versioned name.
                        jndi.rebind(beanName, jndi.lookupLink(entry.getKey() + beanName));
                    }

                    return;
                }
            }
            logger.warn("No " + VersioningServiceBase.DEFAULT + " JNDI binding present for application '"
                    + this.applicationName + "'. You need to set a version as default or expect lookup and unbind failures.");
        } catch (NamingException e) {
            throw new IllegalStateException("Failed checking default JNDI bindings", e);
        }
    }

    /**
     * @return All prefixes with their policies for this virtual JNDI binding.
     */
    public Map<String, String> getContexts() {
        Map<String, String> result = new HashMap<String, String>(this.policies.size());
        result.putAll(this.policies);
        return result;
    }

    /**
     * @return All JNDI names managed by this virtual JNDI binding grouped by
     *         prefix.
     */
    public Map<String, List<String>> getNames() {
        Map<String, List<String>> result = new HashMap<String, List<String>>(this.beans.size());
        for (Map.Entry<String, List<String>> bean : this.beans.entrySet()) {
            List<String> beanNames = new ArrayList<String>(bean.getValue().size());
            for (String beanName : bean.getValue()) {
                beanNames.add(beanName);
            }
            result.put(bean.getKey(), beanNames);
        }
        return result;
    }

    /**
     * @return Unique URL for this application.
     */
    public URL geturl() {
        try {
            return new URL("file:///dev/null/VirtualEJBContainer-" + applicationName);
        } catch (MalformedURLException e) {
            throw new IllegalStateException("Cannot create URL", e);
        }
    }

    /**
     * @return new String[0].
     */
    public String[] getejbs() {
        return new String[0];
    }
}
