/**
 * OW2 Specifications
 * Copyright (C) 2010 Bull S.A.S.
 * Contact: easybeans@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id: PersistenceProviderResolverHolder.java 6267 2012-05-29 13:47:20Z benoitf $
 * --------------------------------------------------------------------------
 */

package javax.persistence.spi;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.WeakHashMap;

import javax.persistence.PersistenceException;

/**
 * Holds the global PersistenceProviderResolver instance. If no PersistenceProviderResolver is set by the environment, the default
 * PersistenceProviderResolver is used. Implementations must be thread-safe.
 * @see <a href="http://jcp.org/en/jsr/detail?id=317">JPA 2.0 specification</a>
 * @author Florent Benoit
 * @since JPA 2.0 version.
 */
public class PersistenceProviderResolverHolder {

    /**
     * The instance of the persistence provider resolver (which is initialized with a default resolver).
     */
    private static PersistenceProviderResolver persistenceProviderResolver = new DefaultPersistenceProviderResolver();

    /**
     * Returns the current persistence provider resolver.
     * @return persistence provider resolver in use
     */
    public static PersistenceProviderResolver getPersistenceProviderResolver() {
        return persistenceProviderResolver;
    }

    /**
     * Defines the persistence provider resolver used. <br>
     * The implementation of PersistenceProviderResolver- Holder must be threadsafe, but no guarantee is made against multiple
     * threads setting the resolver.
     * @param resolver PersistenceProviderResolver to be used
     */
    public static void setPersistenceProviderResolver(PersistenceProviderResolver resolver) {

        // Reset previous one
        persistenceProviderResolver.clearCachedProviders();

        // Assign a new default one or the given resolver ?
        if (resolver == null) {
            persistenceProviderResolver = new DefaultPersistenceProviderResolver();
        } else {
            persistenceProviderResolver = resolver;
        }

    }

    /**
     * The name of this class should be kept as it is present in the RI implementation
     * 
     */
    private static class DefaultPersistenceProviderResolver implements PersistenceProviderResolver {

        /**
         * Persistence Provider property.
         */
        private static final String PERSISTENCE_PROVIDER = PersistenceProvider.class.getName();

        /**
         * The provider supplies the provider configuration file by creating a text file named
         * javax.persistence.spi.PersistenceProvider and placing it in the META-INF/services directory of one of its JAR files.
         * The contents of the file should be the name of the provider implementation class of the
         * javax.persistence.spi.PersistenceProvider interface.
         */
        private static final String PERSISTENCE_PROVIDER_JAR_PROPERTY = "META-INF/services/" + PERSISTENCE_PROVIDER;

        /**
         * UID for serialization.
         */
        private static final long serialVersionUID = 5768927739496807544L;

        /**
         * Map of persistence providers between a given classloader and the list of persistence providers.
         */
        private WeakHashMap<ClassLoader, List<PersistenceProvider>> persistenceProviders = new WeakHashMap<ClassLoader, List<PersistenceProvider>>();

        /**
         * Initialize the list of persistence providers. (it not done).
         */
        private void findPersistenceProviders(ClassLoader classLoader) {

            // List of PERSISTENCE_PROVIDER available in the given classloader.
            Enumeration<URL> urls = null;
            try {
                urls = classLoader.getResources(PERSISTENCE_PROVIDER_JAR_PROPERTY);
            } catch (IOException e) {
                throw new PersistenceException("Cannot get resources named '" + PERSISTENCE_PROVIDER_JAR_PROPERTY
                        + "' on the current classloader '" + classLoader + "'.", e);
            }

            // Analyze URLs
            while (urls.hasMoreElements()) {
                URL url = urls.nextElement();
                URLConnection urlConnection = null;
                try {
                    urlConnection = url.openConnection();
                } catch (IOException e) {
                    throw new PersistenceException("Cannot open connection on URL '" + url + "'.", e);
                }

                // avoid lock
                urlConnection.setDefaultUseCaches(false);
                InputStream is = null;
                try {
                    is = urlConnection.getInputStream();
                    Reader reader = null;
                    BufferedReader bufferedReader = null;
                    try {
                        reader = new InputStreamReader(is);
                        bufferedReader = new BufferedReader(reader);
                        String line = bufferedReader.readLine();
                        if (line == null) {
                            throw new PersistenceException("No lines found in the file available at the URL '" + url + "'.");
                        }
                        // add The persistence provider found.
                        addPersistenceProvider(classLoader, line.trim());
                    } finally {
                        reader.close();
                        bufferedReader.close();
                    }
                } catch (IOException e) {
                    throw new PersistenceException("Cannot get InputStream on URL '" + url + "'.", e);
                } finally {
                    if (is != null) {
                        try {
                            is.close();
                        } catch (IOException e) {
                            throw new PersistenceException("Cannot close InputStream on URL '" + url + "'.", e);
                        }
                    }
                }

            }

        }

        /**
         * Add to the set of persistence provider the given persistence provider (by using its name).
         * @param persistenceProviderName name of the persistence provider.
         */
        private void addPersistenceProvider(final ClassLoader classLoader, final String persistenceProviderName) {
            // load the class
            Class<?> persistenceProviderClass = null;
            try {
                persistenceProviderClass = classLoader.loadClass(persistenceProviderName);
            } catch (ClassNotFoundException e) {
                throw new PersistenceException("Cannot load the persistence provider class with the name '"
                        + persistenceProviderName + "' in the ClassLoader '" + classLoader + "'.", e);
            }

            // build a new instance
            Object object = null;
            try {
                object = persistenceProviderClass.newInstance();
            } catch (InstantiationException e) {
                throw new PersistenceException("Cannot build an instance of the persistence provider class with the name '"
                        + persistenceProviderName + "' in the ClassLoader '" + classLoader + "'.", e);
            } catch (IllegalAccessException e) {
                throw new PersistenceException("Cannot build an instance of the persistence provider class with the name '"
                        + persistenceProviderName + "' in the ClassLoader '" + classLoader + "'.", e);
            }

            if (!(object instanceof PersistenceProvider)) {
                throw new PersistenceException("The instance of the object with the class name '" + persistenceProviderName
                        + "' in the ClassLoader '" + classLoader + "' is not an instance of PersistenceProvider interface.");
            }

            // Get list
            List<PersistenceProvider> existingPersistenceProviders = persistenceProviders.get(classLoader);
            if (existingPersistenceProviders == null) {
                existingPersistenceProviders = new ArrayList<PersistenceProvider>();
                persistenceProviders.put(classLoader, existingPersistenceProviders);
            }

            // Add it.
            existingPersistenceProviders.add((PersistenceProvider) object);

        }

        /**
         * Note that the PersistenceProviderResolver.getPersistenceProviders() method can potentially be called many times. It is
         * therefore recommended that the implementation of this method make use of caching.<br>
         * This is why there is a cache mechanism
         * @return current persistence providers associated to the current CL.
         */
        public List<PersistenceProvider> getPersistenceProviders() {
            // Get current classloader
            ClassLoader currentCL = Thread.currentThread().getContextClassLoader();

            // Get current list of providers for the given classloader
            List<PersistenceProvider> availablePersistenceProviders = null;
            synchronized (persistenceProviders) {
                availablePersistenceProviders = persistenceProviders.get(currentCL);
                if (availablePersistenceProviders == null) {
                    findPersistenceProviders(currentCL);
                    availablePersistenceProviders = persistenceProviders.get(currentCL);
                }
            }
            return availablePersistenceProviders;

        }

        /**
         * Clear the given hashmap
         */
        public void clearCachedProviders() {
            this.persistenceProviders.clear();
        }

        /**
         * Class that is present in the RI but used by ourself.
         * Here to be compliant to the .class file
         */
        private class ProviderName {

            public ProviderName(String name, URL source) {

            }

            public String getName() {
                return null;
            }

            public URL getSource() {
                return null;
            }
            
            public String toString() {
                return super.toString();
            }
        }
    }
}