/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.openejb.assembler.classic;

import org.apache.openejb.AppContext;
import org.apache.openejb.BeanContext;
import org.apache.openejb.InterfaceType;
import org.apache.openejb.ModuleContext;
import org.apache.openejb.OpenEJBRuntimeException;
import org.apache.openejb.core.ivm.naming.BusinessLocalBeanReference;
import org.apache.openejb.core.ivm.naming.BusinessLocalReference;
import org.apache.openejb.core.ivm.naming.BusinessRemoteReference;
import org.apache.openejb.core.ivm.naming.IntraVmJndiReference;
import org.apache.openejb.core.ivm.naming.ObjectReference;
import org.apache.openejb.loader.Options;
import org.apache.openejb.loader.SystemInstance;
import org.apache.openejb.spi.ContainerSystem;
import org.apache.openejb.util.LogCategory;
import org.apache.openejb.util.Logger;
import org.apache.openejb.util.StringTemplate;
import org.apache.openejb.util.Strings;

import javax.ejb.embeddable.EJBContainer;
import javax.jms.MessageListener;
import javax.naming.Context;
import javax.naming.NameAlreadyBoundException;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.lang.reflect.Constructor;
import java.rmi.Remote;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;

import static org.apache.openejb.util.Classes.packageName;


/**
 * @version $Rev$ $Date$
 */
public class JndiBuilder {

    public static final String DEFAULT_NAME_KEY = "default";

    final boolean embeddedEjbContainerApi;

    public static final Logger logger = Logger.getInstance(LogCategory.OPENEJB_STARTUP, JndiBuilder.class.getPackage().getName());
    private static final boolean USE_OLD_JNDI_NAMES = SystemInstance.get().getOptions().get("openejb.use-old-jndi-names", false);

    private final Context openejbContext;
    private static final String JNDINAME_STRATEGY_CLASS = "openejb.jndiname.strategy.class";
    private static final String JNDINAME_FAILONCOLLISION = "openejb.jndiname.failoncollision";
    private final boolean failOnCollision;

    public JndiBuilder(final Context openejbContext) {
        this.openejbContext = openejbContext;

        final Options options = SystemInstance.get().getOptions();

        failOnCollision = options.get(JNDINAME_FAILONCOLLISION, true);
        embeddedEjbContainerApi = options.get(EJBContainer.class.getName(), false);
    }

    public void build(final EjbJarInfo ejbJar, final HashMap<String, BeanContext> deployments) {

        final JndiNameStrategy strategy = createStrategy(ejbJar, deployments);

        for (final EnterpriseBeanInfo beanInfo : ejbJar.enterpriseBeans) {
            final BeanContext beanContext = deployments.get(beanInfo.ejbDeploymentId);
            strategy.begin(beanContext);
            try {
                bind(ejbJar, beanContext, beanInfo, strategy);
            } finally {
                strategy.end();
            }
        }
    }

    public static JndiNameStrategy createStrategy(final EjbJarInfo ejbJar, final Map<String, BeanContext> deployments) {
        final Options options = new Options(ejbJar.properties, SystemInstance.get().getOptions());

        final Class strategyClass = options.get(JNDINAME_STRATEGY_CLASS, TemplatedStrategy.class);

        final String strategyClassName = strategyClass.getName();

        try {
            try {
                final Constructor constructor = strategyClass.getConstructor(EjbJarInfo.class, Map.class);
                return (JndiNameStrategy) constructor.newInstance(ejbJar, deployments);
            } catch (final NoSuchMethodException e) {
                // no-op
            }

            final Constructor constructor = strategyClass.getConstructor();
            return (JndiNameStrategy) constructor.newInstance();
        } catch (final InstantiationException e) {
            throw new IllegalStateException("Could not instantiate JndiNameStrategy: " + strategyClassName, e);
        } catch (final IllegalAccessException e) {
            throw new IllegalStateException("Could not access JndiNameStrategy: " + strategyClassName, e);
        } catch (final Throwable t) {
            throw new IllegalStateException("Could not create JndiNameStrategy: " + strategyClassName, t);
        }
    }

    public interface JndiNameStrategy {

        enum Interface {

            REMOTE_HOME(InterfaceType.EJB_HOME, "RemoteHome", "home", ""),
            LOCAL_HOME(InterfaceType.EJB_LOCAL_HOME, "LocalHome", "local-home", "Local"),
            BUSINESS_LOCAL(InterfaceType.BUSINESS_LOCAL, "Local", "business-local", "BusinessLocal"),
            LOCALBEAN(InterfaceType.LOCALBEAN, "LocalBean", "localbean", "LocalBean"),
            BUSINESS_REMOTE(InterfaceType.BUSINESS_REMOTE, "Remote", "business-remote", "BusinessRemote"),
            SERVICE_ENDPOINT(InterfaceType.SERVICE_ENDPOINT, "Endpoint", "service-endpoint", "ServiceEndpoint");

            private final InterfaceType type;
            private final String annotatedName;
            private final String xmlName;
            private final String xmlNameCc;
            private final String openejbLegacy;

            Interface(final InterfaceType type, final String annotatedName, final String xmlName, final String openejbLegacy) {
                this.type = type;
                this.annotatedName = annotatedName;
                this.xmlName = xmlName;
                this.xmlNameCc = Strings.camelCase(xmlName);
                this.openejbLegacy = openejbLegacy;
            }


            public InterfaceType getType() {
                return type;
            }

            public String getAnnotationName() {
                return annotatedName;
            }

            public String getXmlName() {
                return xmlName;
            }

            public String getXmlNameCc() {
                return xmlNameCc;
            }

            public String getOpenejbLegacy() {
                return openejbLegacy;
            }

        }

        void begin(BeanContext beanContext);

        String getName(Class interfce, String key, Interface type);

        Map<String, String> getNames(Class interfce, Interface type);

        void end();
    }

    // TODO: put these into the classpath and get them with xbean-finder

    public static class TemplatedStrategy implements JndiNameStrategy {
        private static final String JNDINAME_FORMAT = "openejb.jndiname.format";
        private static final String KEYS = "default,local,global,app";
        private final StringTemplate template;
        private final HashMap<String, EnterpriseBeanInfo> beanInfos;

        // Set in begin()
        private BeanContext bean;

        // Set in begin()
        private HashMap<String, Map<String, StringTemplate>> templates;

        private String format;
        private Map<String, String> appContext;
        private HashMap<String, String> beanContext;

        public TemplatedStrategy(final EjbJarInfo ejbJarInfo, final Map<String, BeanContext> deployments) {
            final Options options = new Options(ejbJarInfo.properties, SystemInstance.get().getOptions());

            format = options.get(JNDINAME_FORMAT, "{deploymentId}{interfaceType.annotationName}");

            { // illegal format check
                final int index = format.indexOf(":");
                if (index > -1) {
                    logger.error("Illegal " + JNDINAME_FORMAT + " contains a colon ':'.  Everything before the colon will be removed, '" + format + "' ");
                    format = format.substring(index + 1);
                }
            }

            this.template = new StringTemplate(format);

            beanInfos = new HashMap<String, EnterpriseBeanInfo>();
            for (final EnterpriseBeanInfo beanInfo : ejbJarInfo.enterpriseBeans) {
                beanInfos.put(beanInfo.ejbDeploymentId, beanInfo);
            }

            final Iterator<BeanContext> it = deployments.values().iterator();
            if (!it.hasNext()) {
                return;
            }

            // TODO we should just pass in the ModuleContext
            final ModuleContext moduleContext = it.next().getModuleContext();

            appContext = new HashMap<String, String>();
            putAll(appContext, SystemInstance.get().getProperties());
            putAll(appContext, moduleContext.getAppContext().getProperties());
            putAll(appContext, moduleContext.getProperties());

            appContext.put("appName", moduleContext.getAppContext().getId());
            appContext.put("appId", moduleContext.getAppContext().getId());

            appContext.put("moduleName", moduleContext.getId());
            appContext.put("moduleId", moduleContext.getId());
        }

        private void putAll(final Map<String, String> map, final Properties properties) {
            for (final Map.Entry<Object, Object> e : properties.entrySet()) {
                if (!(e.getValue() instanceof String)) {
                    continue;
                }
                if (!(e.getKey() instanceof String)) {
                    continue;
                }

                map.put((String) e.getKey(), (String) e.getValue());
            }
        }

        private Map<String, StringTemplate> addTemplate(final Map<String, StringTemplate> map, final String key, final StringTemplate template) {
            Map<String, StringTemplate> m = map;
            if (m == null) {
                m = new TreeMap<String, StringTemplate>();
            }
            m.put(key, template);
            return m;
        }

        public void begin(final BeanContext bean) {
            this.bean = bean;

            final EnterpriseBeanInfo beanInfo = beanInfos.get(bean.getDeploymentID());

            templates = new HashMap<String, Map<String, StringTemplate>>();
            templates.put("", addTemplate(null, DEFAULT_NAME_KEY, template));

            for (final JndiNameInfo nameInfo : beanInfo.jndiNamess) {
                String intrface = nameInfo.intrface;
                if (intrface == null) {
                    intrface = "";
                }
                templates.put(intrface, addTemplate(templates.get(intrface), getType(nameInfo.name), new StringTemplate(nameInfo.name)));
            }
            beanInfo.jndiNames.clear();
            beanInfo.jndiNamess.clear();

            this.beanContext = new HashMap<String, String>(appContext);
            putAll(this.beanContext, bean.getProperties());
            this.beanContext.put("ejbType", bean.getComponentType().name());
            this.beanContext.put("ejbClass", bean.getBeanClass().getName());
            this.beanContext.put("ejbClass.simpleName", bean.getBeanClass().getSimpleName());
            this.beanContext.put("ejbClass.packageName", packageName(bean.getBeanClass()));
            this.beanContext.put("ejbName", bean.getEjbName());
            this.beanContext.put("deploymentId", bean.getDeploymentID().toString());
        }

        private static String getType(final String name) {
            int start = 0;
            if (name.charAt(0) == '/') {
                start = 1;
            }
            final int end = name.substring(start).indexOf('/');
            if (end < 0) {
                return DEFAULT_NAME_KEY;
            }
            return name.substring(start, end);
        }

        public void end() {
        }

        public String getName(final Class interfce, final String key, final Interface type) {
            Map<String, StringTemplate> template = templates.get(interfce.getName());
            if (template == null) {
                template = templates.get(type.getAnnotationName());
            }
            if (template == null) {
                template = templates.get("");
            }

            final Map<String, String> contextData = new HashMap<String, String>(beanContext);
            contextData.put("interfaceType", type.getAnnotationName());
            contextData.put("interfaceType.annotationName", type.getAnnotationName());
            contextData.put("interfaceType.annotationNameLC", type.getAnnotationName().toLowerCase());
            contextData.put("interfaceType.xmlName", type.getXmlName());
            contextData.put("interfaceType.xmlNameCc", type.getXmlNameCc());
            contextData.put("interfaceType.openejbLegacyName", type.getOpenejbLegacy());
            contextData.put("interfaceClass", interfce.getName());
            contextData.put("interfaceClass.simpleName", interfce.getSimpleName());
            contextData.put("interfaceClass.packageName", packageName(interfce));

            StringTemplate stringTemplate = null;

            if (template.containsKey(key)) {
                stringTemplate = template.get(key);
            } else {
                stringTemplate = template.get(DEFAULT_NAME_KEY);
            }

            if (stringTemplate == null) {
                stringTemplate = template.values().iterator().next();
            }

            return stringTemplate.apply(contextData);

        }

        @Override
        public Map<String, String> getNames(final Class interfce, final Interface type) {
            final Map<String, String> names = new HashMap<String, String>();
            for (final String key : KEYS.split(",")) {
                names.put(key, getName(interfce, key, type));
            }
            return names;
        }
    }

    public static class LegacyAddedSuffixStrategy implements JndiNameStrategy {
        private BeanContext beanContext;

        public void begin(final BeanContext beanContext) {
            this.beanContext = beanContext;
        }

        public void end() {
        }

        public String getName(final Class interfce, final String key, final Interface type) {
            String id = String.valueOf(beanContext.getDeploymentID());
            if (id.charAt(0) == '/') {
                id = id.substring(1);
            }

            switch (type) {
                case REMOTE_HOME:
                    return id;
                case LOCAL_HOME:
                    return id + "Local";
                case BUSINESS_LOCAL:
                    return id + "BusinessLocal";
                case BUSINESS_REMOTE:
                    return id + "BusinessRemote";
            }
            return id;
        }

        @Override
        public Map<String, String> getNames(final Class interfce, final Interface type) {
            final Map<String, String> names = new HashMap<String, String>();
            names.put("", getName(interfce, DEFAULT_NAME_KEY, type));
            return names;
        }
    }

    public void bind(final EjbJarInfo ejbJarInfo, final BeanContext bean, final EnterpriseBeanInfo beanInfo, final JndiNameStrategy strategy) {

        // in an ear ejbmodule, webmodule etc can get the same name so avoid Comp binding issue
        // and we shouldn't need it
        if (BeanContext.Comp.class.equals(bean.getBeanClass())) {
            return;
        }

        final Bindings bindings = new Bindings();
        bean.set(Bindings.class, bindings);

        Reference simpleNameRef = null;

        final Object id = bean.getDeploymentID();

        // Our openejb.jndiname.format concept works such that there doesn't need to be one explicit jndi name
        // for each view that the bean may offer.  If the user configured a name that results in few possible
        // jndi names than views, this is ok.  The 'optionalBind' method will do its best and log the results.
        // This openejb.jndiname.format affects only the OpenEJB-specific global jndi tree.
        //
        // Should there be a so described "deficit" of names, we give precedence to the most universal and local first
        // Essentially this:
        //     1. Local Bean view as it implements all business interfaces of the bean, local or remote
        //     2. The business local view -- "the" is applicable as create proxies with all possible local interfaces
        //     3. The business remote view -- same note on "the" as above
        //     4. The EJBLocalHome
        //     5. The EJBHome
        //
        // This ordering also has an affect on which view wins the "java:global/{app}/{module}/{ejbName}" jndi name.
        // In the case that the bean has just one view, the name refers to that view.  Otherwise, the name is unspecified

        try {
            if (bean.isLocalbean()) {
                final Class beanClass = bean.getBeanClass();

                final BeanContext.BusinessLocalBeanHome home = bean.getBusinessLocalBeanHome();
                final BusinessLocalBeanReference ref = new BusinessLocalBeanReference(home);

                optionalBind(bindings, ref, "openejb/Deployment/" + format(id, beanClass.getName(), InterfaceType.LOCALBEAN));

                // if the user inject the EJB using a parent class
                if (!bean.getBeanClass().isInterface()) {
                    for (Class<?> clazz = bean.getBeanClass().getSuperclass(); !clazz.equals(Object.class); clazz = clazz.getSuperclass()) {
                        optionalBind(bindings, ref, "openejb/Deployment/" + format(id, clazz.getName(), InterfaceType.LOCALBEAN));
                    }
                }

                final String internalName = "openejb/Deployment/" + format(id, beanClass.getName(), InterfaceType.BUSINESS_LOCALBEAN_HOME);
                bind(internalName, ref, bindings, beanInfo, beanClass);

                final String name = strategy.getName(beanClass, DEFAULT_NAME_KEY, JndiNameStrategy.Interface.LOCALBEAN);
                bind("openejb/local/" + name, ref, bindings, beanInfo, beanClass);
                bindJava(bean, beanClass, ref, bindings, beanInfo);
                if (USE_OLD_JNDI_NAMES) {
                    bean.getModuleContext().getAppContext().getBindings().put(name, ref);
                }

                simpleNameRef = ref;
            }
        } catch (final NamingException e) {
            throw new OpenEJBRuntimeException("Unable to bind business remote deployment in jndi.", e);
        }

        try {

            for (final Class interfce : bean.getBusinessLocalInterfaces()) {

                final BeanContext.BusinessLocalHome home = bean.getBusinessLocalHome(interfce);
                final BusinessLocalReference ref = new BusinessLocalReference(home);

                optionalBind(bindings, ref, "openejb/Deployment/" + format(id, interfce.getName()));

                final String internalName = "openejb/Deployment/" + format(id, interfce.getName(), InterfaceType.BUSINESS_LOCAL);
                bind(internalName, ref, bindings, beanInfo, interfce);

                final String name = strategy.getName(interfce, DEFAULT_NAME_KEY, JndiNameStrategy.Interface.BUSINESS_LOCAL);
                final String externalName = "openejb/local/" + name;
                bind(externalName, ref, bindings, beanInfo, interfce);
                bindJava(bean, interfce, ref, bindings, beanInfo);
                if (USE_OLD_JNDI_NAMES) {
                    bean.getModuleContext().getAppContext().getBindings().put(name, ref);
                }

                if (simpleNameRef == null) {
                    simpleNameRef = ref;
                }
            }
        } catch (final NamingException e) {
            throw new OpenEJBRuntimeException("Unable to bind business local interface for deployment " + id, e);
        }

        try {

            for (final Class interfce : bean.getBusinessRemoteInterfaces()) {

                final BeanContext.BusinessRemoteHome home = bean.getBusinessRemoteHome(interfce);
                final BusinessRemoteReference ref = new BusinessRemoteReference(home);

                optionalBind(bindings, ref, "openejb/Deployment/" + format(id, interfce.getName(), null));

                final String internalName = "openejb/Deployment/" + format(id, interfce.getName(), InterfaceType.BUSINESS_REMOTE);
                bind(internalName, ref, bindings, beanInfo, interfce);

                final String name = strategy.getName(interfce, DEFAULT_NAME_KEY, JndiNameStrategy.Interface.BUSINESS_REMOTE);
                bind("openejb/local/" + name, ref, bindings, beanInfo, interfce);
                bind("openejb/remote/" + name, ref, bindings, beanInfo, interfce);
                bind("openejb/remote/" + computeGlobalName(bean, interfce), ref, bindings, beanInfo, interfce);
                bindJava(bean, interfce, ref, bindings, beanInfo);
                if (USE_OLD_JNDI_NAMES) {
                    bean.getModuleContext().getAppContext().getBindings().put(name, ref);
                }

                if (simpleNameRef == null) {
                    simpleNameRef = ref;
                }
            }
        } catch (final NamingException e) {
            throw new OpenEJBRuntimeException("Unable to bind business remote deployment in jndi.", e);
        }

        try {
            final Class localHomeInterface = bean.getLocalHomeInterface();
            if (localHomeInterface != null) {

                final ObjectReference ref = new ObjectReference(bean.getEJBLocalHome());

                String name = strategy.getName(bean.getLocalHomeInterface(), DEFAULT_NAME_KEY, JndiNameStrategy.Interface.LOCAL_HOME);
                bind("openejb/local/" + name, ref, bindings, beanInfo, localHomeInterface);

                optionalBind(bindings, ref, "openejb/Deployment/" + format(id, localHomeInterface.getName(), InterfaceType.EJB_LOCAL_HOME));

                name = "openejb/Deployment/" + format(id, bean.getLocalInterface().getName());
                bind(name, ref, bindings, beanInfo, localHomeInterface);

                name = "openejb/Deployment/" + format(id, bean.getLocalInterface().getName(), InterfaceType.EJB_LOCAL);
                bind(name, ref, bindings, beanInfo, localHomeInterface);
                bindJava(bean, localHomeInterface, ref, bindings, beanInfo);

                if (simpleNameRef == null) {
                    simpleNameRef = ref;
                }
            }
        } catch (final NamingException e) {
            throw new OpenEJBRuntimeException("Unable to bind local home interface for deployment " + id, e);
        }

        try {
            final Class homeInterface = bean.getHomeInterface();
            if (homeInterface != null) {

                final ObjectReference ref = new ObjectReference(bean.getEJBHome());

                String name = strategy.getName(homeInterface, DEFAULT_NAME_KEY, JndiNameStrategy.Interface.REMOTE_HOME);
                bind("openejb/local/" + name, ref, bindings, beanInfo, homeInterface);
                bind("openejb/remote/" + name, ref, bindings, beanInfo, homeInterface);

                optionalBind(bindings, ref, "openejb/Deployment/" + format(id, homeInterface.getName(), InterfaceType.EJB_HOME));

                name = "openejb/Deployment/" + format(id, bean.getRemoteInterface().getName());
                bind(name, ref, bindings, beanInfo, homeInterface);

                name = "openejb/Deployment/" + format(id, bean.getRemoteInterface().getName(), InterfaceType.EJB_OBJECT);
                bind(name, ref, bindings, beanInfo, homeInterface);
                bindJava(bean, homeInterface, ref, bindings, beanInfo);

                if (simpleNameRef == null) {
                    simpleNameRef = ref;
                }
            }
        } catch (final NamingException e) {
            throw new OpenEJBRuntimeException("Unable to bind remote home interface for deployment " + id, e);
        }

        try {
            if (simpleNameRef != null) {
                bindJava(bean, null, simpleNameRef, bindings, beanInfo);
            }
        } catch (final NamingException e) {
            throw new OpenEJBRuntimeException("Unable to bind simple java:global name in jndi", e);
        }

        try {
            if (MessageListener.class.equals(bean.getMdbInterface())) {

                final String destinationId = bean.getDestinationId();
                final String jndiName = "openejb/Resource/" + destinationId;
                final Reference reference = new IntraVmJndiReference(jndiName);

                final String deploymentId = id.toString();
                bind("openejb/local/" + deploymentId, reference, bindings, beanInfo, MessageListener.class);
                bind("openejb/remote/" + deploymentId, reference, bindings, beanInfo, MessageListener.class);
            }
        } catch (final NamingException e) {
            throw new OpenEJBRuntimeException("Unable to bind mdb destination in jndi.", e);
        } catch (final NoClassDefFoundError ncdfe) {
            // no-op: no jms API
        }
    }

    private void optionalBind(final Bindings bindings, final Reference ref, final String name) throws NamingException {
        try {
            openejbContext.bind(name, ref);
            logger.debug("bound ejb at name: " + name + ", ref: " + ref);
            bindings.add(name);
        } catch (final NamingException okIfBindFails) {
            logger.debug("failed to bind ejb at name: " + name + ", ref: " + ref);
        }
    }

    public static String format(final Object deploymentId, final String interfaceClassName) {
        return format((String) deploymentId, interfaceClassName, null);
    }

    public static String format(final Object deploymentId, final String interfaceClassName, final InterfaceType interfaceType) {
        return format((String) deploymentId, interfaceClassName, interfaceType);
    }

    public static String format(final String deploymentId, final String interfaceClassName, final InterfaceType interfaceType) {
        return deploymentId + "/" + interfaceClassName + (interfaceType == null ? "" : "!" + interfaceType.getSpecName());
    }

    private void bind(final String name, final Reference ref, final Bindings bindings, final EnterpriseBeanInfo beanInfo, final Class intrface) throws NamingException {

        if (name.startsWith("openejb/local/") || name.startsWith("openejb/remote/") || name.startsWith("openejb/localbean/") || name.startsWith("openejb/global/")) {

            final String externalName = name.replaceFirst("openejb/[^/]+/", "");

            if (bindings.contains(name)) {
                // We bind under two sections of jndi, only warn once.. the user doesn't need to be bothered with that detail
                if (name.startsWith("openejb/local/")) {
                    logger.debug("Duplicate: Jndi(name=" + externalName + ")");
                }
                return;
            }

            try {
                openejbContext.bind(name, ref);
                bindings.add(name);

                if (!beanInfo.jndiNames.contains(externalName)) {
                    beanInfo.jndiNames.add(externalName);

                    final JndiNameInfo nameInfo = new JndiNameInfo();
                    nameInfo.intrface = intrface == null ? null : intrface.getName();
                    nameInfo.name = externalName;
                    beanInfo.jndiNamess.add(nameInfo);

                    if (!embeddedEjbContainerApi
                        // filtering internal bean
                        && !(beanInfo instanceof ManagedBeanInfo && ((ManagedBeanInfo) beanInfo).hidden)) {
                        logger.info("Jndi(name=" + externalName + ") --> Ejb(deployment-id=" + beanInfo.ejbDeploymentId + ")");
                    }
                }
            } catch (final NameAlreadyBoundException e) {
                final BeanContext deployment = findNameOwner(name);
                if (deployment != null) {
                    logger.error("Jndi(name=" + externalName + ") cannot be bound to Ejb(deployment-id=" + beanInfo.ejbDeploymentId + ").  Name already taken by Ejb(deployment-id=" + deployment.getDeploymentID() + ")");
                } else {
                    logger.error("Jndi(name=" + externalName + ") cannot be bound to Ejb(deployment-id=" + beanInfo.ejbDeploymentId + ").  Name already taken by another object in the system.");
                }
                // Construct a new exception as the IvmContext doesn't include
                // the name in the exception that it throws
                if (failOnCollision) {
                    throw new NameAlreadyBoundException(externalName);
                }
            }
        } else {
            try {
                openejbContext.bind(name, ref);
                logger.debug("bound ejb at name: " + name + ", ref: " + ref);
                bindings.add(name);
            } catch (final NameAlreadyBoundException e) {
                logger.error("Jndi name could not be bound; it may be taken by another ejb.  Jndi(name=" + name + ")");
                // Construct a new exception as the IvmContext doesn't include
                // the name in the exception that it throws
                throw new NameAlreadyBoundException(name);
            }
        }

    }

    //ee6 specified ejb bindings in module, app, and global contexts

    private String computeGlobalName(final BeanContext cdi, final Class<?> intrface) {
        final ModuleContext module = cdi.getModuleContext();
        final AppContext application = module.getAppContext();

        final String appName = application.isStandaloneModule() ? "" : application.getId() + "/";
        final String moduleName = moduleName(cdi);
        String beanName = cdi.getEjbName();
        if (intrface != null) {
            beanName = beanName + "!" + intrface.getName();
        }

        return "global/" + appName + moduleName + beanName;
    }

    private String moduleName(BeanContext cdi) {
        String moduleName = cdi.getModuleName() + "/";
        if (moduleName.startsWith("ear-scoped-cdi-beans_")) {
            moduleName = moduleName.substring("ear-scoped-cdi-beans_".length());
        }
        return moduleName;
    }

    private void bindJava(final BeanContext cdi, final Class intrface, final Reference ref, final Bindings bindings, final EnterpriseBeanInfo beanInfo) throws NamingException {
        final ModuleContext module = cdi.getModuleContext();
        final AppContext application = module.getAppContext();

        final Context moduleContext = module.getModuleJndiContext();
        final Context appContext = application.getAppJndiContext();
        final Context globalContext = application.getGlobalJndiContext();

        final String appName = application.isStandaloneModule() ? "" : application.getId() + "/";
        String moduleName = moduleName(cdi);
        if (moduleName.startsWith("/")) {
            moduleName = moduleName.substring(1);
        }
        String beanName = cdi.getEjbName();
        if (intrface != null) {
            beanName = beanName + "!" + intrface.getName();
        }
        final String globalName = "global/" + appName + moduleName + beanName;
        try {

            if (embeddedEjbContainerApi
                && !(beanInfo instanceof ManagedBeanInfo && ((ManagedBeanInfo) beanInfo).hidden)) {
                logger.info(String.format("Jndi(name=\"java:%s\")", globalName));
            }
            globalContext.bind(globalName, ref);
            application.getBindings().put(globalName, ref);

            bind("openejb/global/" + globalName, ref, bindings, beanInfo, intrface);
        } catch (final NameAlreadyBoundException e) {
            //one interface in more than one role (e.g. both Local and Remote
            return;
        }

        appContext.bind("app/" + moduleName + beanName, ref);
        application.getBindings().put("app/" + moduleName + beanName, ref);

        final String moduleJndi = "module/" + beanName;
        moduleContext.bind(moduleJndi, ref);

        // contextual if the same ejb (api) is deployed in 2 wars of an ear
        ContextualEjbLookup contextual = ContextualEjbLookup.class.cast(application.getBindings().get(moduleJndi));
        if (contextual == null) {
            final Map<BeanContext, Object> potentials = new HashMap<>();
            contextual = new ContextualEjbLookup(potentials, ref);
            application.getBindings().put(moduleJndi, contextual); // TODO: we shouldn't do it but use web bindings
        }
        contextual.potentials.put(cdi, ref);
    }


    /**
     * This may not be that performant, but it's certain to be faster than the
     * user having to track down which deployment is using a particular jndi name
     *
     * @param name
     * @return .
     */
    private BeanContext findNameOwner(final String name) {
        final ContainerSystem containerSystem = SystemInstance.get().getComponent(ContainerSystem.class);
        for (final BeanContext beanContext : containerSystem.deployments()) {
            final Bindings bindings = beanContext.get(Bindings.class);
            if (bindings != null && bindings.getBindings().contains(name)) {
                return beanContext;
            }
        }
        return null;
    }

    protected static final class Bindings {
        private final List<String> bindings = new ArrayList<String>();

        public List<String> getBindings() {
            return bindings;
        }

        public boolean add(final String o) {
            return bindings.add(o);
        }

        public boolean contains(final String o) {
            return bindings.contains(o);
        }
    }

    public static class RemoteInterfaceComparator implements Comparator<Class> {

        public int compare(final Class a, final Class b) {
            final boolean aIsRmote = Remote.class.isAssignableFrom(a);
            final boolean bIsRmote = Remote.class.isAssignableFrom(b);

            if (aIsRmote == bIsRmote) {
                return 0;
            }
            return aIsRmote ? 1 : -1;
        }
    }

    public static class ContextualEjbLookup extends org.apache.openejb.core.ivm.naming.Reference {
        private final Map<BeanContext, Object> potentials;
        private final Object defaultValue;

        public ContextualEjbLookup(final Map<BeanContext, Object> potentials, final Object defaultValue) {
            this.potentials = potentials;
            this.defaultValue = defaultValue;
        }

        @Override
        public Object getObject() throws NamingException {
            if (potentials.size() == 1) {
                return unwrap(defaultValue);
            }
            final ClassLoader loader = Thread.currentThread().getContextClassLoader();
            if (loader != null) {
                for (final Map.Entry<BeanContext, Object> o : potentials.entrySet()) {
                    if (loader.equals(o.getKey().getClassLoader())) {
                        return unwrap(o.getValue());
                    }
                }
            }
            return unwrap(defaultValue);
        }

        private Object unwrap(final Object value) throws NamingException {
            if (org.apache.openejb.core.ivm.naming.Reference.class.isInstance(value)) { // pretty sure
                return org.apache.openejb.core.ivm.naming.Reference.class.cast(value).getObject();
            }
            return value;
        }
    }
}
