/*
 * Copyright (c) 2003, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package javax.xml.bind;

import java.util.Iterator;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.security.AccessController;

import static javax.xml.bind.JAXBContext.JAXB_CONTEXT_FACTORY;


/**
 * This class is package private and therefore is not exposed as part of the
 * JAXB API.
 *
 * This code is designed to implement the JAXB 1.0 spec pluggability feature
 *
 * @author <ul><li>Ryan Shoemaker, Sun Microsystems, Inc.</li></ul>
 * @see JAXBContext
 */
class ContextFinder {
    private static final Logger logger;
    static {
        logger = Logger.getLogger("javax.xml.bind");
        try {
            if (AccessController.doPrivileged(new GetPropertyAction("jaxb.debug")) != null) {
                // disconnect the logger from a bigger framework (if any)
                // and take the matters into our own hands
                logger.setUseParentHandlers(false);
                logger.setLevel(Level.ALL);
                ConsoleHandler handler = new ConsoleHandler();
                handler.setLevel(Level.ALL);
                logger.addHandler(handler);
            } else {
                // don't change the setting of this logger
                // to honor what other frameworks
                // have done on configurations.
            }
        } catch(Throwable t) {
            // just to be extra safe. in particular System.getProperty may throw
            // SecurityException.
        }
    }

    /**
     * If the {@link InvocationTargetException} wraps an exception that shouldn't be wrapped,
     * throw the wrapped exception.
     */
    private static void handleInvocationTargetException(InvocationTargetException x) throws JAXBException {
        Throwable t = x.getTargetException();
        if( t != null ) {
            if( t instanceof JAXBException )
                // one of our exceptions, just re-throw
                throw (JAXBException)t;
            if( t instanceof RuntimeException )
                // avoid wrapping exceptions unnecessarily
                throw (RuntimeException)t;
            if( t instanceof Error )
                throw (Error)t;
        }
    }


    /**
     * Determine if two types (JAXBContext in this case) will generate a ClassCastException.
     *
     * For example, (targetType)originalType
     *
     * @param originalType
     *          The Class object of the type being cast
     * @param targetType
     *          The Class object of the type that is being cast to
     * @return JAXBException to be thrown.
     */
    private static JAXBException handleClassCastException(Class originalType, Class targetType) {
        final URL targetTypeURL = which(targetType);

        return new JAXBException(Messages.format(Messages.ILLEGAL_CAST,
                // we don't care where the impl class is, we want to know where JAXBContext lives in the impl
                // class' ClassLoader
                getClassClassLoader(originalType).getResource("javax/xml/bind/JAXBContext.class"),
                targetTypeURL));
    }

    /**
     * Create an instance of a class using the specified ClassLoader
     */
    static JAXBContext newInstance( String contextPath,
                               String className,
                               ClassLoader classLoader,
                               Map properties )
        throws JAXBException {
        try {
            Class spFactory = safeLoadClass(className,classLoader);
            return newInstance(contextPath, spFactory, classLoader, properties);
        } catch (ClassNotFoundException x) {
            throw new JAXBException(
                Messages.format( Messages.PROVIDER_NOT_FOUND, className ),
                x);
        } catch (RuntimeException x) {
            // avoid wrapping RuntimeException to JAXBException,
            // because it indicates a bug in this code.
            throw x;
        } catch (Exception x) {
            // can't catch JAXBException because the method is hidden behind
            // reflection.  Root element collisions detected in the call to
            // createContext() are reported as JAXBExceptions - just re-throw it
            // some other type of exception - just wrap it
            throw new JAXBException(
                Messages.format( Messages.COULD_NOT_INSTANTIATE, className, x ),
                x);
        }
    }

    static JAXBContext newInstance( String contextPath,
                               Class spFactory,
                               ClassLoader classLoader,
                               Map properties )
        throws JAXBException
    {
        try {
            /*
             * javax.xml.bind.context.factory points to a class which has a
             * static method called 'createContext' that
             * returns a javax.xml.JAXBContext.
             */

            Object context = null;

            // first check the method that takes Map as the third parameter.
            // this is added in 2.0.
            try {
                Method m = spFactory.getMethod("createContext",String.class,ClassLoader.class,Map.class);
                // any failure in invoking this method would be considered fatal
                context = m.invoke(null,contextPath,classLoader,properties);
            } catch (NoSuchMethodException e) {
                // it's not an error for the provider not to have this method.
            }

            if(context==null) {
                // try the old method that doesn't take properties. compatible with 1.0.
                // it is an error for an implementation not to have both forms of the createContext method.
                Method m = spFactory.getMethod("createContext",String.class,ClassLoader.class);
                // any failure in invoking this method would be considered fatal
                context = m.invoke(null,contextPath,classLoader);
            }

            if(!(context instanceof JAXBContext)) {
                // the cast would fail, so generate an exception with a nice message
                throw handleClassCastException(context.getClass(), JAXBContext.class);
            }
            return (JAXBContext)context;
        } catch (InvocationTargetException x) {
            handleInvocationTargetException(x);
            // for other exceptions, wrap the internal target exception
            // with a JAXBException
            Throwable e = x;
            if(x.getTargetException()!=null)
                e = x.getTargetException();

            throw new JAXBException( Messages.format( Messages.COULD_NOT_INSTANTIATE, spFactory, e ), e );
        } catch (RuntimeException x) {
            // avoid wrapping RuntimeException to JAXBException,
            // because it indicates a bug in this code.
            throw x;
        } catch (Exception x) {
            // can't catch JAXBException because the method is hidden behind
            // reflection.  Root element collisions detected in the call to
            // createContext() are reported as JAXBExceptions - just re-throw it
            // some other type of exception - just wrap it
            throw new JAXBException(
                Messages.format( Messages.COULD_NOT_INSTANTIATE, spFactory, x ),
                x);
        }
    }


    /**
     * Create an instance of a class using the thread context ClassLoader
     */
    static JAXBContext newInstance(
                              Class[] classes,
                              Map properties,
                              String className) throws JAXBException {
        ClassLoader cl = getContextClassLoader();
        Class spi;
        try {
            spi = safeLoadClass(className,cl);
        } catch (ClassNotFoundException e) {
            throw new JAXBException(e);
        }

        if(logger.isLoggable(Level.FINE)) {
            // extra check to avoid costly which operation if not logged
            logger.log(Level.FINE, "loaded {0} from {1}", new Object[]{className, which(spi)});
        }

        return newInstance(classes, properties, spi);
    }

    static JAXBContext newInstance(Class[] classes,
                                   Map properties,
                                   Class spFactory) throws JAXBException {
        Method m;
        try {
            m = spFactory.getMethod("createContext", Class[].class, Map.class);
        } catch (NoSuchMethodException e) {
            throw new JAXBException(e);
        }
        try {
            Object context = m.invoke(null, classes, properties);
            if(!(context instanceof JAXBContext)) {
                // the cast would fail, so generate an exception with a nice message
                throw handleClassCastException(context.getClass(), JAXBContext.class);
            }
            return (JAXBContext)context;
        } catch (IllegalAccessException e) {
            throw new JAXBException(e);
        } catch (InvocationTargetException e) {
            handleInvocationTargetException(e);

            Throwable x = e;
            if (e.getTargetException() != null)
                x = e.getTargetException();

            throw new JAXBException(x);
        }
    }

    static JAXBContext find(String factoryId, String contextPath, ClassLoader classLoader, Map properties ) throws JAXBException {

        // TODO: do we want/need another layer of searching in $java.home/lib/jaxb.properties like JAXP?

        final String jaxbContextFQCN = JAXBContext.class.getName();

        // search context path for jaxb.properties first
        StringBuilder propFileName;
        StringTokenizer packages = new StringTokenizer( contextPath, ":" );
        String factoryClassName;

        if(!packages.hasMoreTokens())
            // no context is specified
            throw new JAXBException(Messages.format(Messages.NO_PACKAGE_IN_CONTEXTPATH));


        logger.fine("Searching jaxb.properties");

        while( packages.hasMoreTokens() ) {
            String packageName = packages.nextToken(":").replace('.','/');
            // com.acme.foo - > com/acme/foo/jaxb.properties
             propFileName = new StringBuilder().append(packageName).append("/jaxb.properties");

            Properties props = loadJAXBProperties( classLoader, propFileName.toString() );
            if (props != null) {
                if (props.containsKey(factoryId)) {
                    factoryClassName = props.getProperty(factoryId);
                    return newInstance( contextPath, factoryClassName, classLoader, properties );
                } else {
                    throw new JAXBException(Messages.format(Messages.MISSING_PROPERTY, packageName, factoryId));
                }
            }
        }

        logger.fine("Searching the system property");

        // search for a system property second (javax.xml.bind.JAXBContext)
        factoryClassName = AccessController.doPrivileged(new GetPropertyAction(JAXBContext.JAXB_CONTEXT_FACTORY));
        if(  factoryClassName != null ) {
            return newInstance( contextPath, factoryClassName, classLoader, properties );
        } else { // leave this here to assure compatibility
            factoryClassName = AccessController.doPrivileged(new GetPropertyAction(jaxbContextFQCN));
            if(  factoryClassName != null ) {
                return newInstance( contextPath, factoryClassName, classLoader, properties );
            }
        }

        // OSGi search
        Class jaxbContext = lookupJaxbContextUsingOsgiServiceLoader();
        if (jaxbContext != null) {
            logger.fine("OSGi environment detected");
            return newInstance(contextPath, jaxbContext, classLoader, properties);
        }

        logger.fine("Searching META-INF/services");
        // search META-INF services next
        BufferedReader r = null;
        try {
            final StringBuilder resource = new StringBuilder().append("META-INF/services/").append(jaxbContextFQCN);
            final InputStream resourceStream =
                    classLoader.getResourceAsStream(resource.toString());

            if (resourceStream != null) {
                r = new BufferedReader(new InputStreamReader(resourceStream, "UTF-8"));
                factoryClassName = r.readLine();
                if (factoryClassName != null) {
                    factoryClassName = factoryClassName.trim();
                }
                r.close();
                return newInstance(contextPath, factoryClassName, classLoader, properties);
            } else {
                logger.log(Level.FINE, "Unable to load:{0}", resource.toString());
            }
        } catch (UnsupportedEncodingException e) {
            // should never happen
            throw new JAXBException(e);
        } catch (IOException e) {
            throw new JAXBException(e);
        } finally {
            try {
                if (r != null) {
                    r.close();
                }
            } catch (IOException ex) {
                Logger.getLogger(ContextFinder.class.getName()).log(Level.SEVERE, null, ex);
            }
        }

        // else no provider found
        logger.fine("Trying to create the platform default provider");
        return newInstance(contextPath, PLATFORM_DEFAULT_FACTORY_CLASS, classLoader, properties);
    }

    static JAXBContext find( Class[] classes, Map properties ) throws JAXBException {

        final String jaxbContextFQCN = JAXBContext.class.getName();
        String factoryClassName;

        // search for jaxb.properties in the class loader of each class first
        for (final Class c : classes) {
            // this classloader is used only to load jaxb.properties, so doing this should be safe.
            ClassLoader classLoader = getClassClassLoader(c);
            Package pkg = c.getPackage();
            if(pkg==null)
                continue;       // this is possible for primitives, arrays, and classes that are loaded by poorly implemented ClassLoaders
            String packageName = pkg.getName().replace('.', '/');

            // TODO: do we want to optimize away searching the same package?  org.Foo, org.Bar, com.Baz
            //       classes from the same package might come from different class loades, so it might be a bad idea

            // TODO: it's easier to look things up from the class
            // c.getResourceAsStream("jaxb.properties");

            // build the resource name and use the property loader code
            String resourceName = packageName+"/jaxb.properties";
            logger.log(Level.FINE, "Trying to locate {0}", resourceName);
            Properties props = loadJAXBProperties(classLoader, resourceName);
            if (props == null) {
                logger.fine("  not found");
            } else {
                logger.fine("  found");
                if (props.containsKey(JAXB_CONTEXT_FACTORY)) {
                    // trim() seems redundant, but adding to satisfy customer complaint
                    factoryClassName = props.getProperty(JAXB_CONTEXT_FACTORY).trim();
                    return newInstance(classes, properties, factoryClassName);
                } else {
                    throw new JAXBException(Messages.format(Messages.MISSING_PROPERTY, packageName, JAXB_CONTEXT_FACTORY));
                }
            }
        }

        // search for a system property second (javax.xml.bind.JAXBContext)
        logger.log(Level.FINE, "Checking system property {0}", JAXBContext.JAXB_CONTEXT_FACTORY);
        factoryClassName = AccessController.doPrivileged(new GetPropertyAction(JAXBContext.JAXB_CONTEXT_FACTORY));
        if (factoryClassName != null) {
            logger.log(Level.FINE, "  found {0}", factoryClassName);
            return newInstance( classes, properties, factoryClassName );
        } else { // leave it here for compatibility reasons
            logger.fine("  not found");
            logger.log(Level.FINE, "Checking system property {0}", jaxbContextFQCN);
            factoryClassName = AccessController.doPrivileged(new GetPropertyAction(jaxbContextFQCN));
            if (factoryClassName != null) {
                logger.log(Level.FINE, "  found {0}", factoryClassName);
                return newInstance( classes, properties, factoryClassName );
            } else {
                logger.fine("  not found");
            }
        }

        // OSGi search
        Class jaxbContext = lookupJaxbContextUsingOsgiServiceLoader();
        if (jaxbContext != null) {
            logger.fine("OSGi environment detected");
            return newInstance(classes, properties, jaxbContext);
        }

        // search META-INF services next
        logger.fine("Checking META-INF/services");
        BufferedReader r = null;
        try {
            final String resource = new StringBuilder("META-INF/services/").append(jaxbContextFQCN).toString();
            ClassLoader classLoader = getContextClassLoader();
            URL resourceURL;
            if(classLoader==null)
                resourceURL = ClassLoader.getSystemResource(resource);
            else
                resourceURL = classLoader.getResource(resource);

            if (resourceURL != null) {
                logger.log(Level.FINE, "Reading {0}", resourceURL);
                r = new BufferedReader(new InputStreamReader(resourceURL.openStream(), "UTF-8"));
                factoryClassName = r.readLine();
                if (factoryClassName != null) {
                    factoryClassName = factoryClassName.trim();
                }
                return newInstance(classes, properties, factoryClassName);
            } else {
                logger.log(Level.FINE, "Unable to find: {0}", resource);
            }
        } catch (UnsupportedEncodingException e) {
            // should never happen
            throw new JAXBException(e);
        } catch (IOException e) {
            throw new JAXBException(e);
        } finally {
            if (r != null) {
                try {
                    r.close();
                } catch (IOException ex) {
                    logger.log(Level.FINE, "Unable to close stream", ex);
                }
            }
        }

        // else no provider found
        logger.fine("Trying to create the platform default provider");
        return newInstance(classes, properties, PLATFORM_DEFAULT_FACTORY_CLASS);
    }

    private static Class lookupJaxbContextUsingOsgiServiceLoader() {
        try {
            // Use reflection to avoid having any dependency on ServiceLoader class
            Class target = Class.forName("com.sun.org.glassfish.hk2.osgiresourcelocator.ServiceLoader");
            Method m = target.getMethod("lookupProviderClasses", Class.class);
            Iterator iter = ((Iterable) m.invoke(null, JAXBContext.class)).iterator();
            return iter.hasNext() ? (Class)iter.next() : null;
        } catch(Exception e) {
            logger.log(Level.FINE, "Unable to find from OSGi: javax.xml.bind.JAXBContext");
            return null;
        }
    }

    private static Properties loadJAXBProperties( ClassLoader classLoader,
                                                  String propFileName )
        throws JAXBException {

        Properties props = null;

        try {
            URL url;
            if(classLoader==null)
                url = ClassLoader.getSystemResource(propFileName);
            else
                url = classLoader.getResource( propFileName );

            if( url != null ) {
                logger.log(Level.FINE, "loading props from {0}", url);
                props = new Properties();
                InputStream is = url.openStream();
                props.load( is );
                is.close();
            }
        } catch( IOException ioe ) {
            logger.log(Level.FINE,"Unable to load "+propFileName,ioe);
            throw new JAXBException( ioe.toString(), ioe );
        }

        return props;
    }


    /**
     * Search the given ClassLoader for an instance of the specified class and
     * return a string representation of the URL that points to the resource.
     *
     * @param clazz
     *          The class to search for
     * @param loader
     *          The ClassLoader to search.  If this parameter is null, then the
     *          system class loader will be searched
     * @return
     *          the URL for the class or null if it wasn't found
     */
    static URL which(Class clazz, ClassLoader loader) {

        String classnameAsResource = clazz.getName().replace('.', '/') + ".class";

        if(loader == null) {
            loader = getSystemClassLoader();
        }

        return loader.getResource(classnameAsResource);
    }

    /**
     * Get the URL for the Class from it's ClassLoader.
     *
     * Convenience method for {@link #which(Class, ClassLoader)}.
     *
     * Equivalent to calling: which(clazz, clazz.getClassLoader())
     *
     * @param clazz
     *          The class to search for
     * @return
     *          the URL for the class or null if it wasn't found
     */
    static URL which(Class clazz) {
        return which(clazz, getClassClassLoader(clazz));
    }

    /**
     * When JAXB is in J2SE, rt.jar has to have a JAXB implementation.
     * However, rt.jar cannot have META-INF/services/javax.xml.bind.JAXBContext
     * because if it has, it will take precedence over any file that applications have
     * in their jar files.
     *
     * <p>
     * When the user bundles his own JAXB implementation, we'd like to use it, and we
     * want the platform default to be used only when there's no other JAXB provider.
     *
     * <p>
     * For this reason, we have to hard-code the class name into the API.
     */
    private static final String PLATFORM_DEFAULT_FACTORY_CLASS = "com.sun.xml.internal.bind.v2.ContextFactory";

    /**
     * Loads the class, provided that the calling thread has an access to the class being loaded.
     */
    private static Class safeLoadClass(String className, ClassLoader classLoader) throws ClassNotFoundException {
       logger.log(Level.FINE, "Trying to load {0}", className);
       try {
          // make sure that the current thread has an access to the package of the given name.
          SecurityManager s = System.getSecurityManager();
          if (s != null) {
              int i = className.lastIndexOf('.');
              if (i != -1) {
                  s.checkPackageAccess(className.substring(0,i));
              }
          }

          if (classLoader == null) {
              return Class.forName(className);
          } else {
              return classLoader.loadClass(className);
          }
       } catch (SecurityException se) {
           // anyone can access the platform default factory class without permission
           if (PLATFORM_DEFAULT_FACTORY_CLASS.equals(className)) {
              return Class.forName(className);
           }
           throw se;
       }
    }

    private static ClassLoader getContextClassLoader() {
        if (System.getSecurityManager() == null) {
            return Thread.currentThread().getContextClassLoader();
        } else {
            return (ClassLoader) java.security.AccessController.doPrivileged(
                    new java.security.PrivilegedAction() {
                        public java.lang.Object run() {
                            return Thread.currentThread().getContextClassLoader();
                        }
                    });
        }
    }

    private static ClassLoader getClassClassLoader(final Class c) {
        if (System.getSecurityManager() == null) {
            return c.getClassLoader();
        } else {
            return (ClassLoader) java.security.AccessController.doPrivileged(
                    new java.security.PrivilegedAction() {
                        public java.lang.Object run() {
                            return c.getClassLoader();
                        }
                    });
        }
    }

    private static ClassLoader getSystemClassLoader() {
        if (System.getSecurityManager() == null) {
            return ClassLoader.getSystemClassLoader();
        } else {
            return (ClassLoader) java.security.AccessController.doPrivileged(
                    new java.security.PrivilegedAction() {
                        public java.lang.Object run() {
                            return ClassLoader.getSystemClassLoader();
                        }
                    });
        }
    }

}
