/**
 * 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: Persistence.java 6221 2012-04-02 12:15:36Z benoitf $
 * --------------------------------------------------------------------------
 */

package javax.persistence;

import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.persistence.spi.LoadState;
import javax.persistence.spi.PersistenceProvider;
import javax.persistence.spi.PersistenceProviderResolverHolder;
import javax.persistence.spi.ProviderUtil;

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

    /**
     * Persistence Provider property with a typo for its value !
     */
    @Deprecated
    public static final String PERSISTENCE_PROVIDER = "javax.persistence.spi.PeristenceProvider";

    /**
     * Set of persistence providers.
     */
    @Deprecated
    protected static final Set<PersistenceProvider> providers = new HashSet<PersistenceProvider>();

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

    /**
     * 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) {
        /**
         * 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 = PersistenceProviderResolverHolder.getPersistenceProviderResolver()
                .getPersistenceProviders().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 = PersistenceProviderResolverHolder.getPersistenceProviderResolver()
                .getPersistenceProviders().iterator();
        while (itProvider.hasNext()) {
            PersistenceProvider tmpProvider = itProvider.next();
            if (tmpProvider.getClass().getName().equals(persistenceProviderName)) {
                persistenceProvider = tmpProvider;
                break;
            }
        }
        return persistenceProvider;
    }

    /**
     * Return PersistenceUtil instance
     */
    public static PersistenceUtil getPersistenceUtil() {
        return new PersistenceUtilImpl();
    }

    /**
     * Inner class that needs to have this name in order to match the RI name.
     */
    private static class PersistenceUtilImpl implements PersistenceUtil {

        /**
         * Helper method in order to get the persistence providers.
         */
        private static List<PersistenceProvider> getPersistenceProviders() {
            return PersistenceProviderResolverHolder.getPersistenceProviderResolver().getPersistenceProviders();
        }

        /**
         * Determine the load state of a given persistent attribute.
         * @param entity containing the attribute
         * @param attributeName name of attribute whose load state is to be determined
         * @return false if entity's state has not been loaded or if the attribute state has not been loaded, else true
         */
        public boolean isLoaded(Object entity, String attributeName) {

            List<PersistenceProvider> persistenceProviders = getPersistenceProviders();
            if (persistenceProviders == null) {
                return true;
            }
            
            // Ask each persistence provider (without reference) and then with reference
            for (PersistenceProvider persistenceProvider : persistenceProviders) {

                ProviderUtil providerUtil = null;
                try {
                    providerUtil = persistenceProvider.getProviderUtil();
                } catch (IncompatibleClassChangeError ame) {
                    // JPA 1.0 persistence provider.
                    continue;
                }
                LoadState loadState = providerUtil.isLoadedWithoutReference(entity, attributeName);

                // Check the load state status
                if (LoadState.LOADED == loadState) {
                    return true;
                } else if (LoadState.NOT_LOADED == loadState) {
                    return false;
                }
            }

            for (PersistenceProvider persistenceProvider : persistenceProviders) {

                ProviderUtil providerUtil = null;
                try {
                    providerUtil = persistenceProvider.getProviderUtil();
                } catch (IncompatibleClassChangeError ame) {
                    // JPA 1.0 persistence provider.
                    continue;
                }
                LoadState loadState = providerUtil.isLoadedWithReference(entity, attributeName);

                // Check the load state status
                if (LoadState.LOADED == loadState) {
                    return true;
                } else if (LoadState.NOT_LOADED == loadState) {
                    return false;
                }
            }

            return true;
        }

        /**
         * Determine the load state of an entity. This method can be used to determine the load state of an entity passed as a
         * reference. An entity is considered loaded if all attributes for which FetchType EAGER has been specified have been
         * loaded. The isLoaded(Object, String) method should be used to determine the load state of an attribute. Not doing so
         * might lead to unintended loading of state.
         * @param entity whose load state is to be determined
         * @return false if the entity has not been loaded, else true
         */
        public boolean isLoaded(Object entity) {
            // Ask each persistence provider util
            for (PersistenceProvider persistenceProvider : getPersistenceProviders()) {

                ProviderUtil providerUtil = null;
                try {
                    providerUtil = persistenceProvider.getProviderUtil();
                } catch (IncompatibleClassChangeError ame) {
                    // JPA 1.0 persistence provider.
                    continue;
                }
                LoadState loadState = providerUtil.isLoaded(entity);

                // Check the load state status
                if (LoadState.LOADED == loadState) {
                    return true;
                } else if (LoadState.NOT_LOADED == loadState) {
                    return false;
                }
            }
            return true;
        }

    }

}
