/*
 * 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.
 */

//
// This source code implements specifications defined by the Java
// Community Process. In order to remain compliant with the specification
// DO NOT add / change / or delete method signatures!
//
package javax.persistence;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
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.PersistenceProviderResolver;
import javax.persistence.spi.PersistenceProviderResolverHolder;
import javax.persistence.spi.ProviderUtil;

import org.apache.geronimo.osgi.locator.ProviderLocator;

/**
 * Bootstrap class to obtain {@link javax.persistence.EntityManagerFactory}
 * references.
 *
 * Contains Geronimo implemented code as required by the JPA spec.
 *
 * @version $Rev: 924793 $ $Date: 2010-03-18 14:14:34 +0000 (Thu, 18 Mar 2010) $
 */
public class Persistence {

    // The following variable is only here for TCK backward compatibility
    @Deprecated
    protected static final Set<PersistenceProvider> providers = new HashSet<PersistenceProvider>();

    // The following string is only here for TCK backward compatibility
    @Deprecated
    public static final String PERSISTENCE_PROVIDER = "javax.persistence.spi.PeristenceProvider";

    static final String PERSISTENCE_PROVIDER_PROPERTY = "javax.persistence.provider";

    static final String PERSISTENCE_PROVIDER_SERVICE = "META-INF/services/"
            + PersistenceProvider.class.getName();


    public static EntityManagerFactory createEntityManagerFactory(
            String persistenceUnitName) {
        return createEntityManagerFactory(persistenceUnitName, Collections.EMPTY_MAP);
    }

    /**
     * Geronimo implementation specific code
     */
    public static EntityManagerFactory createEntityManagerFactory(
            String persistenceUnitName, Map properties) {

        EntityManagerFactory factory = null;
        Map props = properties;
        if (props == null) {
            props = Collections.EMPTY_MAP;
        }

        // get the discovered set of providers
        PersistenceProviderResolver resolver =
            PersistenceProviderResolverHolder.getPersistenceProviderResolver();
        // following will throw PersistenceExceptions for invalid services
        List<PersistenceProvider> providers = resolver.getPersistenceProviders();

        /*
         * Geronimo/OpenJPA 1.0 unique behavior - Start by loading a provider
         * explicitly specified in the properties and return any exceptions.
         * The spec doesn't forbid providers that aren't a service - it only
         * states that they "should" be implemented as services in Sect. 9.2.
         *
         * For 2.0 - We only perform the above behavior if the specified
         * provider is not in the discovered list.
         *
         * Note: This special non-spec defined case will rethrow any encountered
         * Exceptions as a PersistenceException.
         */
        Object propVal = props.get(PERSISTENCE_PROVIDER_PROPERTY);
        if ((propVal != null) && (propVal instanceof String)) {
            boolean isLoaded = false;
            String providerName = propVal.toString();
            // search the discovered providers for this explicit provider
            for (PersistenceProvider provider : providers) {
                if (provider.getClass().getName().compareTo(providerName) == 0) {
                    isLoaded = true;
                    break;
                }
            }
            /*
             * Only try to explicitly create this provider if we didn't
             * find it as a service, while rethrowing any exceptions to
             * match the old 1.0 behavior
             */
            if (!isLoaded) {
                factory = createFactory(
                    providerName.toString(),
                    persistenceUnitName,
                    props);
                if (factory != null) {
                    return factory;
                }
            }
        }

        /*
         * Now, the default JPA2 behavior of loading a provider from our resolver
         *
         * Note:  Change in behavior from 1.0, which always returned exceptions:
         *   Spec states that a provider "must" return null if it
         *   cannot fulfill an EMF request, so that if we have more than one
         *   provider, then the other providers have a chance to return an EMF.
         *   Now, we will return any exceptions wrapped in a
         *   PersistenceException to match 1.0 behavior and provide more
         *   diagnostics to the end user.
         */

        // capture any provider returned exceptions
        Map<String, Throwable> exceptions = new HashMap<String, Throwable>();
        // capture the provider names to use in the exception text if needed
        StringBuffer foundProviders = null;

        for (PersistenceProvider provider : providers) {
            String providerName = provider.getClass().getName();
            try {
                factory = provider.createEntityManagerFactory(persistenceUnitName, props);
            } catch (Exception e) {
                // capture the exception details and give other providers a chance
                exceptions.put(providerName, e);
            }
            if (factory != null) {
                // we're done
                return factory;
            } else {
                // update the list of providers we have tried
                if (foundProviders == null) {
                    foundProviders = new StringBuffer(providerName);
                } else {
                    foundProviders.append(", ");
                    foundProviders.append(providerName);
                }
            }
        }

        // make sure our providers list is initialized for the exceptions below
        if (foundProviders == null) {
            foundProviders = new StringBuffer("NONE");
        }

        /*
         * Spec doesn't mention any exceptions thrown by this method if no emf
         * returned, but old 1.0 behavior always generated an EMF or exception.
         */
        if (exceptions.isEmpty()) {
            // throw an exception with the PU name and providers we tried
            throw new PersistenceException("No persistence providers available for \"" + persistenceUnitName +
                "\" after trying the following discovered implementations: " + foundProviders);
        } else {
            // we encountered one or more exceptions, so format and throw as a single exception
            throw createPersistenceException(
                "Explicit persistence provider error(s) occurred for \"" + persistenceUnitName +
                "\" after trying the following discovered implementations: " + foundProviders,
                exceptions);
        }
    }

    /*
     * Geronimo/OpenJPA private helper code for PERSISTENCE_PROVIDER_PROPERTY
     * @return EntityManagerFactory or null
     * @throws PersistenceException
     */
    private static EntityManagerFactory createFactory(String providerName,
            String persistenceUnitName, Map properties)
            throws PersistenceException {

        Class<?> providerClass;

        // get our class loader
        ClassLoader cl = PrivClassLoader.get(null);
        if (cl == null)
            cl = PrivClassLoader.get(Persistence.class);

        try {
            providerClass = ProviderLocator.loadClass(providerName, Persistence.class, cl);
        } catch (Exception e) {
            throw new PersistenceException("Invalid or inaccessible explicit provider class: " +
                providerName, e);
        }
        try {
            PersistenceProvider provider = (PersistenceProvider) providerClass.newInstance();
            return provider.createEntityManagerFactory(persistenceUnitName, properties);
        } catch (Exception e) {
            throw new PersistenceException("Explicit error returned from provider: " +
                providerName + " for PU: " + persistenceUnitName, e);
        }
    }


    /**
     * Geronimo/OpenJPA private helper code for creating a PersistenceException
     * @param msg String to use as the exception message
     * @param failures Persistence provider exceptions to add to the exception message
     * @return PersistenceException
     */
    private static PersistenceException createPersistenceException(String msg, Map<String, Throwable> failures) {
        String newline = System.getProperty("line.separator");
        StringWriter strWriter = new StringWriter();
        strWriter.append(msg);
        if (failures.size() <= 1) {
            // we caught an exception, so include it as the cause
            Throwable t = null;
            for (String providerName : failures.keySet()) {
                t = failures.get(providerName);
                strWriter.append(" from provider: ");
                strWriter.append(providerName);
                break;
            }
            return new PersistenceException(strWriter.toString(), t);
        } else {
            // we caught multiple exceptions, so format them into the message string and don't set a cause
            strWriter.append(" with the following failures:");
            strWriter.append(newline);
            for (String providerName : failures.keySet()) {
                strWriter.append(providerName);
                strWriter.append(" returned: ");
                failures.get(providerName).printStackTrace(new PrintWriter(strWriter));
            }
            strWriter.append(newline);
            return new PersistenceException(strWriter.toString());
        }
    }

    public static PersistenceUtil getPersistenceUtil() {
        return new PersistenceUtilImpl();
    }

    /**
     * Geronimo implementation specific code
     */
    private static class PersistenceUtilImpl implements PersistenceUtil {

        /**
         * Determines the load state of the attribute of an entity
         * @see javax.persistence.PersistenceUtil#isLoaded(java.lang.Object, java.lang.String)
         */
            public boolean isLoaded(Object entity, String attributeName) {
                boolean isLoaded = true;

                // Get the list of persistence providers from the resolver
                PersistenceProviderResolver ppr =
                    PersistenceProviderResolverHolder.getPersistenceProviderResolver();
                List<PersistenceProvider> pps = ppr.getPersistenceProviders();

                // Iterate through the list using ProviderUtil.isLoadedWithoutReference()
                for (PersistenceProvider pp : pps) {
                    try {
                        ProviderUtil pu = pp.getProviderUtil();
                        LoadState ls = pu.isLoadedWithoutReference(entity, attributeName);
                        if (ls == LoadState.LOADED)
                            return true;
                        if (ls == LoadState.NOT_LOADED)
                            return false;
                    }
                    catch (Throwable t) {
                        // JPA 1.0 providers will not implement the getProviderUtil
                        // method.  Eat the exception and try the next provider.
                    }
                 }
                // Iterate through the list a second time using ProviderUtil.isLoadedWithReference()
                for (PersistenceProvider pp : pps) {
                    try {
                        ProviderUtil pu = pp.getProviderUtil();
                        LoadState ls = pu.isLoadedWithReference(entity, attributeName);
                        if (ls == LoadState.LOADED)
                            return true;
                        if (ls == LoadState.NOT_LOADED)
                            return false;
                    }
                    catch (Throwable t) {
                        // JPA 1.0 providers will not implement the getProviderUtil
                        // method.  Eat the exception and try the next provider.
                    }
                 }

                // All providers returned a load state of unknown.  Return true.
                return true;
            }

            public boolean isLoaded(Object entity) {
                // Get the list of persistence providers from the resolver
                PersistenceProviderResolver ppr =
                    PersistenceProviderResolverHolder.getPersistenceProviderResolver();
                List<PersistenceProvider> pps = ppr.getPersistenceProviders();

                // Iterate through the list of providers, using ProviderUtil to
                // determine the load state
                for (PersistenceProvider pp : pps) {
                    try {
                        ProviderUtil pu = pp.getProviderUtil();
                        LoadState ls = pu.isLoaded(entity);
                        if (ls == LoadState.LOADED)
                            return true;
                        if (ls == LoadState.NOT_LOADED)
                            return false;
                        // Otherwise, load state is unknown.  Query the next provider.
                    }
                    catch (Throwable t) {
                        // JPA 1.0 providers will not implement the getProviderUtil
                        // method.  Eat the exception and try the next provider.
                    }
                }

                // All providers returned a load state of unknown.  Return true.
                return true;
            }
    }

    /**
     * Geronimo/OpenJPA private helper code for handling class loaders
     */
    private static class PrivClassLoader implements PrivilegedAction<ClassLoader> {
        private final Class<?> c;

        public static ClassLoader get(Class<?> c) {
            final PrivClassLoader action = new PrivClassLoader(c);
            if (System.getSecurityManager() != null)
                return AccessController.doPrivileged(action);
            else
                return action.run();
        }

        private PrivClassLoader(Class<?> c) {
            this.c = c;
        }

        public ClassLoader run() {
            if (c != null)
                return c.getClassLoader();
            else
                return Thread.currentThread().getContextClassLoader();
        }
    }
}

