/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2010, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This 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 software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.jboss.as.naming.context;

import java.security.AccessController;
import java.util.Hashtable;
import java.util.StringTokenizer;

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.NamingException;
import javax.naming.RefAddr;
import javax.naming.Reference;
import javax.naming.StringRefAddr;
import javax.naming.directory.Attributes;
import javax.naming.spi.DirObjectFactory;
import javax.naming.spi.ObjectFactory;

import org.jboss.as.naming.ServiceAwareObjectFactory;
import org.jboss.as.server.CurrentServiceContainer;
import org.jboss.modules.Module;
import org.jboss.msc.service.ServiceContainer;
import org.wildfly.security.manager.WildFlySecurityManager;

import static org.jboss.as.naming.NamingMessages.MESSAGES;

/**
 * ObjectFactoryBuilder implementation used to support custom object factories being loaded from modules. This class
 * also provides the default object factory implementation.
 *
 * @author John Bailey
 */
public class ObjectFactoryBuilder implements javax.naming.spi.ObjectFactoryBuilder, DirObjectFactory {

    public static final ObjectFactoryBuilder INSTANCE = new ObjectFactoryBuilder();

    private ObjectFactoryBuilder() {
    }

    /**
     * Create an object factory.  If the object parameter is a reference it will attempt to create an {@link javax.naming.spi.ObjectFactory}
     * from the reference.  If the parameter is not a reference, or the reference does not create an {@link javax.naming.spi.ObjectFactory}
     * it will return {@code this} as the {@link javax.naming.spi.ObjectFactory} to use.
     *
     * @param obj         The object bound in the naming context
     * @param environment The environment information
     * @return The object factory the object resolves to
     * @throws NamingException If any problems occur
     */
    public ObjectFactory createObjectFactory(final Object obj, Hashtable<?, ?> environment) throws NamingException {
        try {
            if (obj instanceof Reference) {
                return factoryFromReference((Reference) obj, environment);
            }
        } catch (Throwable ignored) {
        }
        return this;
    }

    /**
     * Create an object instance.
     *
     * @param ref         Object containing reference information
     * @param name        The name relative to nameCtx
     * @param nameCtx     The naming context
     * @param environment The environment information
     * @return The object
     * @throws Exception If any error occur
     */
    public Object getObjectInstance(final Object ref, final Name name, final Context nameCtx, final Hashtable<?, ?> environment) throws Exception {
        final ClassLoader classLoader = WildFlySecurityManager.getCurrentContextClassLoaderPrivileged();
        if (classLoader == null) {
            return ref;
        }
        final String factoriesProp = (String) environment.get(Context.OBJECT_FACTORIES);
        if (factoriesProp != null) {
            final String[] classes = factoriesProp.split(":");
            for (String className : classes) {
                try {
                    final Class<?> factoryClass = classLoader.loadClass(className);
                    final ObjectFactory objectFactory = ObjectFactory.class.cast(factoryClass.newInstance());
                    final Object result = objectFactory.getObjectInstance(ref, name, nameCtx, environment);
                    if (result != null) {
                        return result;
                    }
                } catch (Throwable ignored) {
                }
            }
        }
        return ref;
    }

    /**
     * Create an object instance.
     *
     * @param ref         Object containing reference information
     * @param name        The name relative to nameCtx
     * @param nameCtx     The naming context
     * @param environment The environment information
     * @param attributes  The directory attributes
     * @return The object
     * @throws Exception If any error occur
     */
    public Object getObjectInstance(final Object ref, final Name name, final Context nameCtx, final Hashtable<?, ?> environment, final Attributes attributes) throws Exception {
        final ClassLoader classLoader = WildFlySecurityManager.getCurrentContextClassLoaderPrivileged();
        if (classLoader == null) {
            return ref;
        }
        final String factoriesProp = (String) environment.get(Context.OBJECT_FACTORIES);
        if (factoriesProp != null) {
            final String[] classes = factoriesProp.split(":");
            for (String className : classes) {
                try {
                    final Class<?> factoryClass = classLoader.loadClass(className);
                    final ObjectFactory objectFactory = ObjectFactory.class.cast(factoryClass.newInstance());
                    final Object result;
                    if (objectFactory instanceof DirObjectFactory) {
                        result = DirObjectFactory.class.cast(objectFactory).getObjectInstance(ref, name, nameCtx, environment, attributes);
                    } else {
                        result = objectFactory.getObjectInstance(ref, name, nameCtx, environment);
                    }
                    if (result != null) {
                        return result;
                    }
                } catch (Throwable ignored) {
                }
            }
        }
        return ref;
    }

    private ObjectFactory factoryFromReference(final Reference reference, final Hashtable<?, ?> environment) throws Exception {
        if (reference.getFactoryClassName() == null) {
            return lookForURLs(reference, environment);
        }
        if (reference instanceof ModularReference) {
            return factoryFromModularReference(ModularReference.class.cast(reference), environment);
        }
        return factoryFromReference(reference, WildFlySecurityManager.getCurrentContextClassLoaderPrivileged(), environment);
    }

    private ObjectFactory factoryFromModularReference(ModularReference modularReference, final Hashtable<?, ?> environment) throws Exception {
        final Module module = Module.getCallerModuleLoader().loadModule(modularReference.getModuleIdentifier());
        final ClassLoader classLoader = module.getClassLoader();
        return factoryFromReference(modularReference, classLoader, environment);
    }

    private ObjectFactory factoryFromReference(final Reference reference, final ClassLoader classLoader, final Hashtable<?, ?> environment) throws Exception {
        try {
            final Class<?> factoryClass = classLoader.loadClass(reference.getFactoryClassName());
            ObjectFactory factory = ObjectFactory.class.cast(factoryClass.newInstance());
            if (factory instanceof ServiceAwareObjectFactory) {
                ((ServiceAwareObjectFactory) factory).injectServiceRegistry(currentServiceContainer());
            }
            return factory;
        } catch (Throwable t) {
            throw MESSAGES.objectFactoryCreationFailure(t);
        }
    }

    static ObjectFactory lookForURLs(Reference ref, Hashtable environment)
            throws NamingException {

        for (int i = 0; i < ref.size(); i++) {
            RefAddr addr = ref.get(i);
            if (addr instanceof StringRefAddr &&
                    addr.getType().equalsIgnoreCase("URL")) {

                String url = (String) addr.getContent();
                ObjectFactory answer = processURL(url, environment);
                if (answer != null) {
                    return answer;
                }
            }
        }
        return null;
    }

    private static ObjectFactory processURL(Object refInfo, Hashtable environment) throws NamingException {

        if (refInfo instanceof String) {
            String url = (String) refInfo;
            String scheme = getURLScheme(url);
            if (scheme != null) {
                ObjectFactory answer = getURLObjectFactory(scheme, url, environment);
                if (answer != null) {
                    return answer;
                }
            }
        }

        if (refInfo instanceof String[]) {
            String[] urls = (String[]) refInfo;
            for (int i = 0; i < urls.length; i++) {
                String scheme = getURLScheme(urls[i]);
                if (scheme != null) {
                    ObjectFactory answer = getURLObjectFactory(scheme, urls[i], environment);
                    if (answer != null) {
                        return answer;
                    }
                }
            }
        }
        return null;
    }

    private static ObjectFactory getURLObjectFactory(String scheme, String url, Hashtable environment) throws NamingException {

        String facProp = (String) environment.get(Context.URL_PKG_PREFIXES);
        if (facProp != null) {
            facProp += ":" + "com.sun.jndi.url";
        } else {
            facProp = "com.sun.jndi.url";
        }

        ClassLoader loader = WildFlySecurityManager.getCurrentContextClassLoaderPrivileged();

        String suffix = "." + scheme + "." + scheme + "URLContextFactory";

        // Not cached; find first factory and cache
        StringTokenizer parser = new StringTokenizer(facProp, ":");
        String className;
        ObjectFactory factory = null;
        while (parser.hasMoreTokens()) {
            className = parser.nextToken() + suffix;
            try {
                Class<?> clazz;
                if (loader == null) {
                    clazz = Class.forName(className);
                } else {
                    clazz = Class.forName(className, true, loader);
                }
                return new ReferenceUrlContextFactoryWrapper((ObjectFactory) clazz.newInstance(), url);
            } catch (InstantiationException | IllegalAccessException e) {
                NamingException ne = new NamingException(className);
                ne.setRootCause(e);
                throw ne;
            } catch (Exception e) {
            }
        }

        return factory;
    }


    private static String getURLScheme(String str) {
        int colon = str.indexOf(':');
        int slash = str.indexOf('/');

        if (colon > 0 && (slash == -1 || colon + 1 == slash))
            return str.substring(0, colon);
        return null;
    }

    private static ServiceContainer currentServiceContainer() {
        if(System.getSecurityManager() == null) {
            return CurrentServiceContainer.getServiceContainer();
        }
        return AccessController.doPrivileged(CurrentServiceContainer.GET_ACTION);
    }

    private static final class ReferenceUrlContextFactoryWrapper implements ObjectFactory {

        private final ObjectFactory factory;
        private final String url;

        private ReferenceUrlContextFactoryWrapper(final ObjectFactory factory, final String url) {
            this.factory = factory;
            this.url = url;
        }

        @Override
        public Object getObjectInstance(final Object obj, final Name name, final Context nameCtx, final Hashtable<?, ?> environment) throws Exception {
            return factory.getObjectInstance(url, name, nameCtx, environment);
        }
    }
}
