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

import java.net.ContentHandler;
import java.net.ContentHandlerFactory;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import org.apache.felix.framework.util.FelixConstants;
import org.apache.felix.framework.util.SecureAction;
import org.apache.felix.framework.util.SecurityManagerEx;
import org.osgi.service.url.URLStreamHandlerService;

/**
 * <p>
 * This class is a singleton and implements the stream and content handler
 * factories for all framework instances executing within the JVM. Any
 * calls to retrieve stream or content handlers is routed through this class
 * and it acts as a multiplexer for all framework instances. To achieve this,
 * all framework instances register with this class when they are created so
 * that it can maintain a centralized registry of instances.
 * </p>
 * <p>
 * When this class receives a request for a stream or content handler, it
 * always returns a proxy handler instead of only returning a proxy if a
 * handler currently exists. This approach is used for three reasons:
 * </p>
 * <ol>
 *   <li>Potential caching behavior by the JVM of stream handlers does not give
 *       you a second chance to provide a handler.
 *   </li>
 *   <li>Due to the dynamic nature of OSGi services, handlers may appear at
 *       any time, so always creating a proxy makes sense.
 *   </li>
 *   <li>Since these handler factories service all framework instances,
 *       some instances may have handlers and others may not, so returning
 *       a proxy is the only answer that makes sense.
 *   </li>
 * </ol>
 * <p>
 * It is possible to disable the URL Handlers service by setting the
 * <tt>framework.service.urlhandlers</tt> configuration property to <tt>false</tt>.
 * When multiple framework instances are in use, if no framework instances enable
 * the URL Handlers service, then the singleton stream and content factories will
 * never be set (i.e., <tt>URL.setURLStreamHandlerFactory()</tt> and
 * <tt>URLConnection.setContentHandlerFactory()</tt>). However, if one instance
 * enables URL Handlers service, then the factory methods will be invoked. In
 * that case, framework instances that disable the URL Handlers service will
 * simply not provide that services to their contained bundles, while framework
 * instances with the service enabled will.
 * </p>
**/
class URLHandlers implements URLStreamHandlerFactory, ContentHandlerFactory
{
    private static final Class[] CLASS_TYPE = new Class[]{Class.class};

    private static final Class URLHANDLERS_CLASS = URLHandlers.class;

    private static final SecureAction m_secureAction = new SecureAction();

    private static volatile SecurityManagerEx m_sm = null;
    private static volatile URLHandlers m_handler = null;

    // This maps classloaders of URLHandlers in other classloaders to lists of
    // their frameworks.
    private final static Map m_classloaderToFrameworkLists = new HashMap();

    // The list to hold all enabled frameworks registered with this handlers
    private static final List m_frameworks = new ArrayList();
    private static int m_counter = 0;

    private static Map m_contentHandlerCache = null;
    private static Map m_streamHandlerCache = null;
    private static URLStreamHandlerFactory m_streamHandlerFactory;
    private static ContentHandlerFactory m_contentHandlerFactory;
    private static final String STREAM_HANDLER_PACKAGE_PROP = "java.protocol.handler.pkgs";
    private static final String DEFAULT_STREAM_HANDLER_PACKAGE = "sun.net.www.protocol|com.ibm.oti.net.www.protocol|gnu.java.net.protocol|wonka.net|com.acunia.wonka.net|org.apache.harmony.luni.internal.net.www.protocol|weblogic.utils|weblogic.net|javax.net.ssl|COM.newmonics.www.protocols";
    private static Object m_rootURLHandlers;

    private static final String m_streamPkgs;
    private static final Map m_builtIn = new HashMap();
    private static final boolean m_loaded;

    static
    {
        String pkgs = new SecureAction().getSystemProperty(STREAM_HANDLER_PACKAGE_PROP, "");
        m_streamPkgs = (pkgs.equals(""))
            ? DEFAULT_STREAM_HANDLER_PACKAGE
            : pkgs + "|" + DEFAULT_STREAM_HANDLER_PACKAGE;
        m_loaded = (null != URLHandlersStreamHandlerProxy.class) &&
            (null != URLHandlersContentHandlerProxy.class) && (null != URLStreamHandlerService.class);
    }


    private static final Map m_handlerToURL = new HashMap();
    private void init(String protocol, URLStreamHandlerFactory factory)
    {
        try
        {
            URLStreamHandler handler = getBuiltInStreamHandler(protocol, factory);
            if (handler != null)
            {
                URL url = new URL(protocol, null, -1, "", handler);
                m_handlerToURL.put(handler, url);
            }
        }
        catch (Throwable ex)
        {
            // Ignore, this is a best effort (maybe log it or something).
        }
    }

    /**
     * <p>
     * Only one instance of this class is created per classloader
     * and that one instance is registered as the stream and content handler
     * factories for the JVM. Unless, we already register one from a different
     * classloader. In this case we attach to this root.
     * </p>
    **/
    private URLHandlers()
    {
        m_sm = new SecurityManagerEx();
        synchronized (URL.class)
        {
            URLStreamHandlerFactory currentFactory = null;
            try
            {
                currentFactory = (URLStreamHandlerFactory) m_secureAction.swapStaticFieldIfNotClass(URL.class,
                    URLStreamHandlerFactory.class, URLHANDLERS_CLASS, "streamHandlerLock");
            }
            catch (Throwable ex)
            {
                // Ignore, this is a best effort (maybe log it or something)
            }

            init("file", currentFactory);
            init("ftp", currentFactory);
            init("http", currentFactory);
            init("https", currentFactory);
            try
            {
                getBuiltInStreamHandler("jar", currentFactory);
            }
            catch (Throwable ex)
            {
                // Ignore, this is a best effort (maybe log it or something)
            }

            if (currentFactory != null)
            {
                try
                {
                    URL.setURLStreamHandlerFactory(currentFactory);
                }
                catch (Throwable ex)
                {
                    // Ignore, this is a best effort (maybe log it or something)
                }
            }

            try
            {
                URL.setURLStreamHandlerFactory(this);
                m_streamHandlerFactory = this;
                m_rootURLHandlers = this;
                // try to flush the cache (gnu/classpath doesn't do it itself)
                try
                {
                    m_secureAction.flush(URL.class, URL.class);
                }
                catch (Throwable t)
                {
                    // Not much we can do
                }
            }
            catch (Error err)
            {
                try
                {
                    // there already is a factory set so try to swap it with ours.
                    m_streamHandlerFactory = (URLStreamHandlerFactory)
                        m_secureAction.swapStaticFieldIfNotClass(URL.class,
                        URLStreamHandlerFactory.class, URLHANDLERS_CLASS, "streamHandlerLock");

                    if (m_streamHandlerFactory == null)
                    {
                        throw err;
                    }
                    if (!m_streamHandlerFactory.getClass().getName().equals(URLHANDLERS_CLASS.getName()))
                    {
                        URL.setURLStreamHandlerFactory(this);
                        m_rootURLHandlers = this;
                    }
                    else if (URLHANDLERS_CLASS != m_streamHandlerFactory.getClass())
                    {
                        try
                        {
                            m_secureAction.invoke(
                                m_secureAction.getDeclaredMethod(m_streamHandlerFactory.getClass(),
                                "registerFrameworkListsForContextSearch",
                                new Class[]{ClassLoader.class, List.class}),
                                m_streamHandlerFactory, new Object[]{ URLHANDLERS_CLASS.getClassLoader(),
                                    m_frameworks });
                            m_rootURLHandlers = m_streamHandlerFactory;
                        }
                        catch (Exception ex)
                        {
                            throw new RuntimeException(ex.getMessage());
                        }
                    }
                }
                catch (Exception e)
                {
                    throw err;
                }
            }

            try
            {
                URLConnection.setContentHandlerFactory(this);
                m_contentHandlerFactory = this;
                // try to flush the cache (gnu/classpath doesn't do it itself)
                try
                {
                    m_secureAction.flush(URLConnection.class, URLConnection.class);
                }
                catch (Throwable t)
                {
                    // Not much we can do
                }
            }
            catch (Error err)
            {
                // there already is a factory set so try to swap it with ours.
                try
                {
                    m_contentHandlerFactory = (ContentHandlerFactory)
                        m_secureAction.swapStaticFieldIfNotClass(
                            URLConnection.class, ContentHandlerFactory.class,
                            URLHANDLERS_CLASS, null);
                    if (m_contentHandlerFactory == null)
                    {
                        throw err;
                    }
                    if (!m_contentHandlerFactory.getClass().getName().equals(
                        URLHANDLERS_CLASS.getName()))
                    {
                        URLConnection.setContentHandlerFactory(this);
                    }
                }
                catch (Exception ex)
                {
                    throw err;
                }
            }
        }
        // are we not the new root?
        if (!((m_streamHandlerFactory == this) || !URLHANDLERS_CLASS.getName().equals(
            m_streamHandlerFactory.getClass().getName())))
        {
            m_sm = null;
            m_handlerToURL.clear();
            m_builtIn.clear();
        }
    }

    static void registerFrameworkListsForContextSearch(ClassLoader index,
        List frameworkLists)
    {
        synchronized (URL.class)
        {
            synchronized (m_classloaderToFrameworkLists)
            {
                m_classloaderToFrameworkLists.put(index, frameworkLists);
            }
        }
    }

    static void unregisterFrameworkListsForContextSearch(ClassLoader index)
    {
        synchronized (URL.class)
        {
            synchronized (m_classloaderToFrameworkLists)
            {
                m_classloaderToFrameworkLists.remove(index);
                if (m_classloaderToFrameworkLists.isEmpty() )
                {
                    synchronized (m_frameworks)
                    {
                        if (m_frameworks.isEmpty())
                        {
                            try
                            {
                                m_secureAction.swapStaticFieldIfNotClass(URL.class,
                                    URLStreamHandlerFactory.class, null, "streamHandlerLock");
                            }
                            catch (Exception ex)
                            {
                                // TODO log this
                                ex.printStackTrace();
                            }

                            if (m_streamHandlerFactory.getClass() != URLHANDLERS_CLASS)
                            {
                                URL.setURLStreamHandlerFactory(m_streamHandlerFactory);
                            }
                            try
                            {
                                m_secureAction.swapStaticFieldIfNotClass(
                                    URLConnection.class, ContentHandlerFactory.class,
                                    null, null);
                            }
                            catch (Exception ex)
                            {
                                // TODO log this
                                ex.printStackTrace();
                            }

                            if (m_contentHandlerFactory.getClass() != URLHANDLERS_CLASS)
                            {
                                URLConnection.setContentHandlerFactory(m_contentHandlerFactory);
                            }
                        }
                    }
                }
            }
        }
    }

    private URLStreamHandler getBuiltInStreamHandler(String protocol, URLStreamHandlerFactory factory)
    {
        synchronized (m_builtIn)
        {
            if (m_builtIn.containsKey(protocol))
            {
                return (URLStreamHandler) m_builtIn.get(protocol);
            }
        }
        if (factory != null)
        {
            URLStreamHandler result = factory.createURLStreamHandler(protocol);
            if (result != null)
            {
                return addToCache(protocol, result);
            }
        }
        // Check for built-in handlers for the mime type.
        // Iterate over built-in packages.
        URLStreamHandler handler = loadBuiltInStreamHandler(protocol, null);
        if (handler == null)
        {
            handler = loadBuiltInStreamHandler(protocol, ClassLoader.getSystemClassLoader());
        }
        return addToCache(protocol, handler);
    }

    private URLStreamHandler loadBuiltInStreamHandler(String protocol, ClassLoader classLoader) {
        StringTokenizer pkgTok = new StringTokenizer(m_streamPkgs, "| ");
        while (pkgTok.hasMoreTokens())
        {
            String pkg = pkgTok.nextToken().trim();
            String className = pkg + "." + protocol + ".Handler";
            try
            {
                // If a built-in handler is found then cache and return it
                Class handler = m_secureAction.forName(className, classLoader);
                if (handler != null)
                {
                    return (URLStreamHandler) handler.newInstance();
                }
            }
            catch (Throwable ex)
            {
                // This could be a class not found exception or an
                // instantiation exception, not much we can do in either
                // case other than ignore it.
            }
        }
        // This is a workaround for android - Starting with 4.1 the built-in core handler
        // are not following the normal naming nore package schema :-(
        String androidHandler = null;
        if ("file".equalsIgnoreCase(protocol))
        {
            androidHandler = "libcore.net.url.FileHandler";
        }
        else if ("ftp".equalsIgnoreCase(protocol))
        {
            androidHandler = "libcore.net.url.FtpHandler";
        }
        else if ("http".equalsIgnoreCase(protocol))
        {
            androidHandler = "libcore.net.http.HttpHandler";
        }
        else if ("https".equalsIgnoreCase(protocol))
        {
            androidHandler = "libcore.net.http.HttpsHandler";
        }
        else if ("jar".equalsIgnoreCase(protocol))
        {
            androidHandler = "libcore.net.url.JarHandler";
        }
        if (androidHandler != null)
        {
            try
            {
                // If a built-in handler is found then cache and return it
                Class handler = m_secureAction.forName(androidHandler, classLoader);
                if (handler != null)
                {
                    return (URLStreamHandler) handler.newInstance();
                }
            }
            catch (Throwable ex)
            {
                // This could be a class not found exception or an
                // instantiation exception, not much we can do in either
                // case other than ignore it.
            }
        }
        return null;
    }

    private synchronized URLStreamHandler addToCache(String protocol, URLStreamHandler result)
    {
        if (!m_builtIn.containsKey(protocol))
            {
                m_builtIn.put(protocol, result);
                return result;
            }
        return (URLStreamHandler) m_builtIn.get(protocol);
    }

    /**
     * <p>
     * This is a method implementation for the <tt>URLStreamHandlerFactory</tt>
     * interface. It simply creates a stream handler proxy object for the
     * specified protocol. It caches the returned proxy; therefore, subsequent
     * requests for the same protocol will receive the same handler proxy.
     * </p>
     * @param protocol the protocol for which a stream handler should be returned.
     * @return a stream handler proxy for the specified protocol.
    **/
    public URLStreamHandler createURLStreamHandler(String protocol)
    {
        // See if there is a cached stream handler.
        // IMPLEMENTATION NOTE: Caching is not strictly necessary for
        // stream handlers since the Java runtime caches them. Caching is
        // performed for code consistency between stream and content
        // handlers and also because caching behavior may not be guaranteed
        // across different JRE implementations.
        URLStreamHandler handler = getFromStreamCache(protocol);

        if (handler != null)
        {
            return handler;
        }
        // If this is the framework's "bundle:" protocol, then return
        // a handler for that immediately, since no one else can be
        // allowed to deal with it.
        if (protocol.equals(FelixConstants.BUNDLE_URL_PROTOCOL))
        {
            return addToStreamCache(protocol,
                new URLHandlersBundleStreamHandler(m_secureAction));
        }

       handler = getBuiltInStreamHandler(protocol,
           (m_streamHandlerFactory != this) ? m_streamHandlerFactory : null);

        // If built-in content handler, then create a proxy handler.
        return addToStreamCache(protocol,
            new URLHandlersStreamHandlerProxy(protocol, m_secureAction,
                handler, (URL) m_handlerToURL.get(handler)));
    }

    /**
     * <p>
     * This is a method implementation for the <tt>ContentHandlerFactory</tt>
     * interface. It simply creates a content handler proxy object for the
     * specified mime type. It caches the returned proxy; therefore, subsequent
     * requests for the same content type will receive the same handler proxy.
     * </p>
     * @param mimeType the mime type for which a content handler should be returned.
     * @return a content handler proxy for the specified mime type.
    **/
    public ContentHandler createContentHandler(String mimeType)
    {
        // See if there is a cached stream handler.
        // IMPLEMENTATION NOTE: Caching is not strictly necessary for
        // stream handlers since the Java runtime caches them. Caching is
        // performed for code consistency between stream and content
        // handlers and also because caching behavior may not be guaranteed
        // across different JRE implementations.
        ContentHandler handler = getFromContentCache(mimeType);

        if (handler != null)
        {
            return handler;
        }

        return addToContentCache(mimeType,
            new URLHandlersContentHandlerProxy(mimeType, m_secureAction,
            (m_contentHandlerFactory != this) ? m_contentHandlerFactory : null));
    }

    private synchronized ContentHandler addToContentCache(String mimeType, ContentHandler handler)
    {
        if (m_contentHandlerCache == null)
        {
            m_contentHandlerCache = new HashMap();
        }
        return (ContentHandler) addToCache(m_contentHandlerCache, mimeType, handler);
    }

    private synchronized ContentHandler getFromContentCache(String mimeType)
    {
        return (ContentHandler) ((m_contentHandlerCache != null) ?
            m_contentHandlerCache.get(mimeType) : null);
    }

    private synchronized URLStreamHandler addToStreamCache(String protocol, URLStreamHandler handler)
    {
        if (m_streamHandlerCache == null)
        {
            m_streamHandlerCache = new HashMap();
        }
        return (URLStreamHandler) addToCache(m_streamHandlerCache, protocol, handler);
    }

    private synchronized URLStreamHandler getFromStreamCache(String protocol)
    {
        return (URLStreamHandler) ((m_streamHandlerCache != null) ?
            m_streamHandlerCache.get(protocol) : null);
    }

    private Object addToCache(Map cache, String key, Object value)
    {
        if (value == null)
        {
            return null;
        }

        Object result = cache.get(key);

        if (result == null)
        {
            cache.put(key, value);
            result = value;
        }
        return result;
    }

    /**
     * <p>
     * Static method that adds a framework instance to the centralized
     * instance registry.
     * </p>
     * @param framework the framework instance to be added to the instance
     *        registry.
     * @param enable a flag indicating whether or not the framework wants to
     *        enable the URL Handlers service.
    **/
    public static void registerFrameworkInstance(Object framework, boolean enable)
    {
        boolean register = false;
        synchronized (m_frameworks)
        {
            // If the URL Handlers service is not going to be enabled,
            // then return immediately.
            if (enable)
            {
                // We need to create an instance if this is the first
                // time this method is called, which will set the handler
                // factories.
                if (m_handler == null )
                {
                    register = true;
                }
                else
                {
                    m_frameworks.add(framework);
                    m_counter++;
                }
            }
            else
            {
                m_counter++;
            }
        }
        if (register)
        {
            synchronized (URL.class)
            {
                synchronized (m_classloaderToFrameworkLists)
                {
                    synchronized (m_frameworks)
                    {
                        if (m_handler == null )
                        {
                            m_handler = new URLHandlers();
                        }
                        m_frameworks.add(framework);
                        m_counter++;
                    }
                }
            }
        }
    }

    /**
     * <p>
     * Static method that removes a framework instance from the centralized
     * instance registry.
     * </p>
     * @param framework the framework instance to be removed from the instance
     *        registry.
    **/
    public static void unregisterFrameworkInstance(Object framework)
    {
        boolean unregister = false;
        synchronized (m_frameworks)
        {
            if (m_frameworks.contains(framework))
            {
                if (m_frameworks.size() == 1 && m_handler != null)
                {
                    unregister = true;
                }
                else
                {
                    m_frameworks.remove(framework);
                    m_counter--;
                }
            }
            else
            {
                m_counter--;
            }
        }
        if (unregister)
        {
            synchronized (URL.class)
            {
                synchronized (m_classloaderToFrameworkLists)
                {
                    synchronized (m_frameworks)
                    {
                        m_frameworks.remove(framework);
                        m_counter--;
                        if (m_frameworks.isEmpty() && m_handler != null)
                        {

                            m_handler = null;
                            try
                            {
                                m_secureAction.invoke(m_secureAction.getDeclaredMethod(
                                    m_rootURLHandlers.getClass(),
                                    "unregisterFrameworkListsForContextSearch",
                                    new Class[]{ ClassLoader.class}),
                                    m_rootURLHandlers,
                                    new Object[] {URLHANDLERS_CLASS.getClassLoader()});
                            }
                            catch (Exception e)
                            {
                                // This should not happen
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * <p>
     * This method returns the system bundle context for the caller.
     * It determines the appropriate system bundle by retrieving the
     * class call stack and find the first class that is loaded from
     * a bundle. It then checks to see which of the registered framework
     * instances owns the class and returns its system bundle context.
     * </p>
     * @return the system bundle context associated with the caller or
     *         <tt>null</tt> if no associated framework was found.
    **/
    public static Object getFrameworkFromContext()
    {
        // This is a hack. The idea is to return the only registered framework
        synchronized (m_classloaderToFrameworkLists)
        {
            if (m_classloaderToFrameworkLists.isEmpty())
            {
                synchronized (m_frameworks)
                {
                    if ((m_counter == 1) && (m_frameworks.size() == 1))
                    {
                        return m_frameworks.get(0);
                    }
                }
            }
        }
        // get the current class call stack.
        Class[] stack = m_sm.getClassContext();
        // Find the first class that is loaded from a bundle.
        Class targetClass = null;
        for (int i = 0; i < stack.length; i++)
        {
            if (stack[i].getClassLoader() != null)
            {
                String name = stack[i].getClassLoader().getClass().getName();
                if (name.startsWith("org.apache.felix.framework.ModuleImpl$ModuleClassLoader")
                    || name.equals("org.apache.felix.framework.searchpolicy.ContentClassLoader")
                    || name.startsWith("org.apache.felix.framework.BundleWiringImpl$BundleClassLoader"))
                {
                    targetClass = stack[i];
                    break;
                }
            }
        }

        // If we found a class loaded from a bundle, then iterate
        // over the framework instances and see which framework owns
        // the bundle that loaded the class.
        if (targetClass != null)
        {
            synchronized (m_classloaderToFrameworkLists)
            {
                ClassLoader index = targetClass.getClassLoader().getClass().getClassLoader();

                List frameworks = (List) m_classloaderToFrameworkLists.get(
                    index);

                if ((frameworks == null) && (index == URLHANDLERS_CLASS.getClassLoader()))
                {
                    frameworks = m_frameworks;
                }
                if (frameworks != null)
                {
                    synchronized (frameworks)
                    {
                        // Check the registry of framework instances
                        for (int i = 0; i < frameworks.size(); i++)
                        {
                            Object framework = frameworks.get(i);
                            try
                            {
                                if (m_secureAction.invoke(
                                    m_secureAction.getDeclaredMethod(framework.getClass(),
                                    "getBundle", CLASS_TYPE),
                                    framework, new Object[]{targetClass}) != null)
                                {
                                    return framework;
                                }
                            }
                            catch (Exception ex)
                            {
                                // This should not happen but if it does there is
                                // not much we can do other then ignore it.
                                // Maybe log this or something.
                                ex.printStackTrace();
                            }
                        }
                    }
                }
            }
        }
        return null;
    }
}
