/**
 * EasyBeans
 * Copyright (C) 2006 Bull S.A.S.
 * Contact: easybeans@objectweb.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: Persistence.java 4383 2008-12-12 08:24:15Z joaninh $
 * --------------------------------------------------------------------------
 */

package javax.persistence;

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.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import javax.persistence.spi.PersistenceProvider;

/**
 * Bootstrap class that is used to obtain an EntityManagerFactory.
 * @see <a href="http://www.jcp.org/en/jsr/detail?id=220">EJB 3.0 specification</a>
 * @author Florent Benoit
 * @since EJB 3.0 version.
 */
public class Persistence {

    // defaut public constructor as it is defined in the spec... Even with
    // static methods !
    /**
     * Persistence Provider property with a typo for its value !
     */
    public static final String PERSISTENCE_PROVIDER = "javax.persistence.spi.PeristenceProvider";
    /**
     * Persistence Provider property without a typo for its value !
     */
    private static final String PERSISTENCE_PROVIDER_BIS = PersistenceProvider.class.getName();

    // Should use this name as it is a protected field to be compliant with RI.
    /**
     * Set of persistence providers.
     */
    protected static final Set<PersistenceProvider> providers = new HashSet<PersistenceProvider>();

    /**
     * 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_BIS;

    /**
     * Property present in the map ?<br>
     * The javax.persistence.provider property was included in the Map passed to
     * createEntityManagerFactory and the value of the property is the
     * provider's implementation class.
     */
    private static final String PERSISTENCE_PROVIDER_MAP_PROPERTY = "javax.persistence.provider";

    /**
     * Persistence providers have been initialized ?
     */
    private static boolean initialized = false;

    /**
     * Create and return an EntityManagerFactory for the named persistence unit.
     * @param persistenceUnitName The name of the persistence unit
     * @return The factory that creates EntityManagers configured according to
     *         the specified persistence unit
     */
    public static EntityManagerFactory createEntityManagerFactory(final String persistenceUnitName) {
        // Use the other method without using properties.
        return createEntityManagerFactory(persistenceUnitName, null);
    }

    /**
     * Create and return an EntityManagerFactory for the named persistence unit
     * using the given properties.
     * @param persistenceUnitName The name of the persistence unit
     * @param properties Additional properties to use when creating the factory.
     *        The values of these properties override any values that may have
     *        been configured elsewhere.
     * @return The factory that creates EntityManagers configured according to
     *         the specified persistence unit.
     */
    public static EntityManagerFactory createEntityManagerFactory(final String persistenceUnitName, final Map properties) {
        /**
         * The Persistence bootstrap class will locate all of the persistence
         * providers by their provider configuration files.
         */
        // Init the set of providers (as we have to provide this var to be
        // compliant)
        init();

        /**
         * A provider may deem itself as appropriate for the persistence unit if
         * any of the following are true:<br>
         * The javax.persistence.provider property was included in the Map
         * passed to createEntityManagerFactory and the value of the property is
         * the provider's implementation class.
         */
        if (properties != null) {
            // check property
            Object object = properties.get(PERSISTENCE_PROVIDER_MAP_PROPERTY);
            if (!(object instanceof String)) {
                throw new PersistenceException("Found '" + PERSISTENCE_PROVIDER_MAP_PROPERTY
                        + "' property in the map but the value is not a String. Found object : '" + object + "'.");
            }
            String persistenceProviderName = (String) object;

            PersistenceProvider persistenceProvider = getProviderForName(persistenceProviderName);
            if (persistenceProvider == null) {
                throw new PersistenceException("Property '" + PERSISTENCE_PROVIDER_MAP_PROPERTY + "' with value '"
                        + persistenceProviderName
                        + "' was provided in the Map properties but no persistence provider with this name has been found");
            }
            // not null, create a factory.
            EntityManagerFactory entityManagerFactory = persistenceProvider.createEntityManagerFactory(persistenceUnitName,
                    properties);
            if (entityManagerFactory == null) {
                throw new PersistenceException("Property '" + PERSISTENCE_PROVIDER_MAP_PROPERTY + "' with value '"
                        + persistenceProviderName + "' was provided in the Map properties but the persistence provider returns "
                        + "an empty factory for the given persistence unit '" + persistenceUnitName + "'.");
            }
            return entityManagerFactory;
        }

        // Property was not given, search the first factory available.
        /**
         * [..] call createEntityManagerFactory() on them in turn until an
         * appropriate backing provider returns an EntityManagerFactory.
         */
        EntityManagerFactory entityManagerFactory = null;
        Iterator<PersistenceProvider> itProvider = providers.iterator();
        while (itProvider.hasNext()) {
            PersistenceProvider persistenceProvider = itProvider.next();
            entityManagerFactory = persistenceProvider.createEntityManagerFactory(persistenceUnitName, properties);
            // Found it ?
            if (entityManagerFactory != null) {
                return entityManagerFactory;
            }
            // else, continue the loop.
        }

        // Not found, what is the error case ? null or exception ?
        throw new PersistenceException("No EntityManagerFactory have been created in the list of '" + providers.size()
                + "' providers available.");

    }

    /**
     * Gets a persistence provider for the given persistence provider name.
     * @param persistenceProviderName the given persistence provider name.
     * @return an instance of persistence provider if found, else null.
     */
    private static PersistenceProvider getProviderForName(final String persistenceProviderName) {
        PersistenceProvider persistenceProvider = null;
        // Persistence Provider is in the list ? (should have be better if we
        // got a map and not a Set)
        Iterator<PersistenceProvider> itProvider = providers.iterator();
        while (itProvider.hasNext()) {
            PersistenceProvider tmpProvider = itProvider.next();
            if (tmpProvider.getClass().getName().equals(persistenceProviderName)) {
                persistenceProvider = tmpProvider;
                break;
            }
        }
        return persistenceProvider;
    }

    /**
     * Initialize the list of persistence providers. (it not done).
     */
    private static void init() {
        // already init, exit.
        if (initialized) {
            return;
        }

        // List of PERSISTENCE_PROVIDER available in the current classloader.
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        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(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);
                    }
                }
            }

        }

        // init done !
        initialized = true;
    }

    /**
     * Add to the set of persistence provider the given persistence provider (by
     * using its name).
     * @param persistenceProviderName name of the persistence provider.
     */
    private static void addPersistenceProvider(final String persistenceProviderName) {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        // 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.");
        }

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

    }

}
