/*
 * The contents of this file are subject to the terms 
 * of the Common Development and Distribution License 
 * (the "License").  You may not use this file except 
 * in compliance with the License.
 * 
 * You can obtain a copy of the license at 
 * glassfish/bootstrap/legal/CDDLv1.0.txt or 
 * https://glassfish.dev.java.net/public/CDDLv1.0.html. 
 * See the License for the specific language governing 
 * permissions and limitations under the License.
 * 
 * When distributing Covered Code, include this CDDL 
 * HEADER in each file and include the License file at 
 * glassfish/bootstrap/legal/CDDLv1.0.txt.  If applicable, 
 * add the following below this CDDL HEADER, with the 
 * fields enclosed by brackets "[]" replaced with your 
 * own identifying information: Portions Copyright [yyyy] 
 * [name of copyright owner]
 */
// Copyright (c) 1998, 2006, Oracle. All rights reserved.  
package oracle.toplink.essentials.internal.ejb.cmp3;

import java.util.*;
import java.net.URL;
import java.net.URLClassLoader;
import java.lang.instrument.*;
import java.security.ProtectionDomain;

import oracle.toplink.essentials.ejb.cmp3.persistence.PersistenceUnitInfo;

import oracle.toplink.essentials.logging.AbstractSessionLog;
import oracle.toplink.essentials.internal.ejb.cmp3.EntityManagerSetupImpl;
import oracle.toplink.essentials.threetier.ServerSession;
import oracle.toplink.essentials.ejb.cmp3.persistence.PersistenceUnitProcessor;
import oracle.toplink.essentials.exceptions.*;
import oracle.toplink.essentials.logging.SessionLog;
import oracle.toplink.essentials.ejb.cmp3.EntityManagerFactoryProvider;
import oracle.toplink.essentials.PersistenceProvider;

import javax.persistence.spi.ClassTransformer;

/**
 * INTERNAL:
 *
 * JavaSECMPInitializer is used to bootstrap the deployment of EntityBeans in EJB 3.0
 * when deployed in a non-managed setting
 *
 * It is called internally by our Provider
 *
 * @see oracle.toplink.essentials.ejb.cmp3.EntityManagerFactoryProvider
 */
public class JavaSECMPInitializer {
    // Config data passed in at initialization time
    // This is the list of Entities that should be deployed
    protected HashMap<String, Set> classMap = null;

    // Used when byte code enhancing
    public static Instrumentation globalInstrumentation;

    // The internal loader is used by applications that do weaving to pre load classes
    // When this flag is set to false, we will not be able to weave.
    protected boolean shouldCreateInternalLoader = true;

    // The JavaSECMPInitializer - a singleton
    protected static JavaSECMPInitializer javaSECMPInitializer;

    // We create an EntityManagerSetupImpl for each persistence unit the following
    // two variables maintain a dictionary of both the EntityManagerSetupImpls and
    // their associated PersistenceUnitInfo objects
    protected HashMap<String, EntityManagerSetupImpl> emSetupImpls = null;
    protected HashMap<String, PersistenceUnitInfo> emSetupPersistenceUnitInfos = null;

    protected ClassLoader sessionClassLoader = null;

    /**
     * INTERNAL:
     * Get the singleton entityContainer.
     * @return EnityContainer
     */
    public static JavaSECMPInitializer getJavaSECMPInitializer(Map properties) {
        if (javaSECMPInitializer == null) {
           initializeFromMain(properties);
        }   
        return javaSECMPInitializer;
    }

  /**
   * Return whether initialization has occured without actually triggering
   * initialization
   */
    public static boolean isSingletonInitialized(){
        return javaSECMPInitializer != null;
    }

  /**
   * Look in the System properties for a logging level property and return a integer
   * that can be used to set the logging level in TopLink
   * @return 
   */
    public static int getTopLinkLoggingLevel(){
        String logLevel = System.getProperty(EntityManagerFactoryProvider.TOPLINK_LOGGING_LEVEL);
        return AbstractSessionLog.translateStringToLoggingLevel(logLevel);  
    }

    /**
     * INTERNAL:
     * User should not instantiate JavaSECMPInitializer.
     */
    protected JavaSECMPInitializer() {
        super();
        emSetupImpls = new HashMap<String, EntityManagerSetupImpl>();
        emSetupPersistenceUnitInfos = new HashMap<String, PersistenceUnitInfo>();
        classMap = new HashMap<String, Set>();
    }

    /**
     * INTERNAL
     * predeploy (with deploy) is one of the two steps required in deployment of entities
     * This method will prepare to call predeploy, call it and finally register the
     * transformer returned to be used for weaving.
     */
    protected boolean callPredeploy(PersistenceUnitInfo persistenceUnitInfo, Map m) {
        // we will only attempt to deploy when TopLink is specified as the provider or the provider is unspecified
        String providerClassName = persistenceUnitInfo.getPersistenceProviderClassName();
        if (providerClassName == null || providerClassName.equals("") || providerClassName.equals(EntityManagerFactoryProvider.class.getName()) || providerClassName.equals(PersistenceProvider.class.getName())){
            Set tempLoaderSet = PersistenceUnitProcessor.buildClassSet(persistenceUnitInfo);
            // Create the temp loader
            ClassLoader tempLoader = createTempLoader(tempLoaderSet);
    
            Set classSet = PersistenceUnitProcessor.buildPersistentClassSet(persistenceUnitInfo, tempLoader);
            
            EntityManagerSetupImpl emSetupImpl = new EntityManagerSetupImpl();
            emSetupImpls.put(persistenceUnitInfo.getPersistenceUnitName(), emSetupImpl);
            emSetupPersistenceUnitInfos.put(persistenceUnitInfo.getPersistenceUnitName(), persistenceUnitInfo);   
            // append the list of entity classes that are defined in the XML descriptor
            classSet.addAll(emSetupImpl.buildPersistentClassSet(tempLoader, persistenceUnitInfo));
            
            // Make the callback
            AbstractSessionLog.getLog().log(SessionLog.FINER, "cmp_init_invoke_predeploy", persistenceUnitInfo.getPersistenceUnitName());

            // Store the set of entity class names keyed on PU name - for use in deploy
            classMap.put(persistenceUnitInfo.getPersistenceUnitName(), classSet);
            
            Set entityClasses = loadEntityClasses(classSet, tempLoader);
            
            //Bug#4452468  When globalInstrumentation is null, there is no weaving
            if (globalInstrumentation == null) {
                m.put(EntityManagerSetupImpl.WEAVING, "false");
            }
    
            final ClassTransformer transformer = emSetupImpl.predeploy(entityClasses, tempLoader, persistenceUnitInfo, null);
    
            // If we got a transformer then register it 
            if ((transformer != null) && (globalInstrumentation != null)) {
                AbstractSessionLog.getLog().log(SessionLog.FINER, "cmp_init_register_transformer", persistenceUnitInfo.getPersistenceUnitName());
                globalInstrumentation.addTransformer(new ClassFileTransformer(){
                    // adapt ClassTransformer to ClassFileTransformer interface
                    public byte[] transform(
                            ClassLoader loader, String className,
                            Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain,
                            byte[] classfileBuffer) throws IllegalClassFormatException {
                        return transformer.transform(loader, className, classBeingRedefined, protectionDomain, classfileBuffer);
                    }
                });
            } else if (transformer == null) {
                AbstractSessionLog.getLog().log(SessionLog.FINER, "cmp_init_transformer_is_null");
            } else if (globalInstrumentation == null) {
                AbstractSessionLog.getLog().log(SessionLog.FINER, "cmp_init_globalInstrumentation_is_null");
            }
            return true;
        }
        return false;
    }
    
    /**
     * INTERNAL
     * deploy (with predeploy) is one of the two steps required in deployment of entities
     * This method will call deploy and return the ServerSession returned by deploy
     */
    protected ServerSession callDeploy(String emName) {
        // Call with the classes loaded by the "real" loader
        AbstractSessionLog.getLog().log(SessionLog.FINER, "cmp_init_invoke_deploy", emName);
        ClassLoader loader = getMainLoader();
        Collection entityClasses = loadEntityClasses(getClassSet(emName), loader);
        ServerSession ss = ((EntityManagerSetupImpl)getEntityManagerSetupImpl(emName)).deploy(entityClasses, loader);

        AbstractSessionLog.getLog().log(SessionLog.FINER, "cmp_init_completed_deploy", emName);

        return ss;
    }
   
    /**
     *  Create a temporary class loader that can be used to inspect classes for
     *  a given persistence unit and then thrown away.  This allows classes to 
     *  be introspected prior to loading them with application's main class 
     *  loader to enabling weaving.
     */
    protected ClassLoader createTempLoader(String persistenceUnitName) {
        return createTempLoader(getClassSet(persistenceUnitName));
    }

    /**
 *  Create a temporary class loader that can be used to inspect classes and then
 *  thrown away.  This allows classes to be introspected prior to loading them
 *  with application's main class loader enabling weaving.
 */
    protected ClassLoader createTempLoader(Collection col) {
        return createTempLoader(col, true);
    }

    protected ClassLoader createTempLoader(Collection col, boolean shouldOverrideLoadClassForCollectionMembers) {
        if (!shouldCreateInternalLoader) {
            return Thread.currentThread().getContextClassLoader();
        }

        //        ClassLoader currentLoader = ClassLoader.getSystemClassLoader();
        ClassLoader currentLoader = Thread.currentThread().getContextClassLoader();
        if (!(currentLoader instanceof URLClassLoader)) {
            throw ValidationException.currentLoaderNotValid(currentLoader);
        }
        URL[] urlPath = ((URLClassLoader)currentLoader).getURLs();
        ClassLoader tempLoader = new TempEntityLoader(urlPath, currentLoader, col, shouldOverrideLoadClassForCollectionMembers);

        AbstractSessionLog.getLog().log(SessionLog.FINER, "cmp_init_tempLoader_created", tempLoader);
        AbstractSessionLog.getLog().log(SessionLog.FINER, "cmp_init_shouldOverrideLoadClassForCollectionMembers", new Boolean(shouldOverrideLoadClassForCollectionMembers));

        return tempLoader;
    }

    /**
     * INTERNAL:
     * Method to return a set of entity class names for a given persistence
     * unit.  The map containing the sets of entity names is keyed on 
     * persistence unit name.
     * 
     * @param persistenceUnitName
     * @return
     */
    private Set getClassSet(String persistenceUnitName) {
    	if (classMap.containsKey(persistenceUnitName)) {
    		return (HashSet) classMap.get(persistenceUnitName);
    	}
    	return new HashSet();
    }
    
  /**
   * Return the setup class for a given entity manager name 
   * @param emName 
   */
    public EntityManagerSetupImpl getEntityManagerSetupImpl(String emName){
        if (emName == null){
            return (EntityManagerSetupImpl)emSetupImpls.get("");
        }
        return (EntityManagerSetupImpl)emSetupImpls.get(emName);
    }

    protected ClassLoader getMainLoader() {
        return Thread.currentThread().getContextClassLoader();
    }

    /**
     * Get a ServerSession that was bootstraped by this JavaSECMPInitializer
     */
    public ServerSession getServerSession(String emName, Map m) {
        PersistenceUnitInfo persistenceInfo = emSetupPersistenceUnitInfos.get(emName);
        if (persistenceInfo == null){
            return null;
        }

        // build a map that contains the properties from the parameter 'm' and from the
        // properties stored in the persistenceInfo.  Properties from the 'm' will win.
        // when there are conflicts
        Map configMap = new HashMap();
        Object key = null;
        if (m != null) {
            Iterator mapKeys = m.keySet().iterator();
            while (mapKeys.hasNext()) {
                key = mapKeys.next();
                configMap.put(key, m.get(key));
            }
        }
        if (persistenceInfo.getProperties() != null) {
            Iterator propertyKeys = persistenceInfo.getProperties().keySet().iterator();
            while (propertyKeys.hasNext()) {
                key = propertyKeys.next();
                if (configMap.get(key) == null) {
                    configMap.put(key, persistenceInfo.getProperties().get(key));
                }
            }
        }
        
        return getEntityManagerSetupImpl(emName).getServerSession(configMap, sessionClassLoader);
    }

  /**
   * Initialize one persistence unit.
   * Initialization is a two phase process.  First the predeploy process builds the metadata
   * and creates any required transformers.
   * Second the deploy process creates a TopLink session based on that metadata.
   */
    protected void initPersistenceUnits(URL url, Map m) {
        Vector<String> predeployedPersistenceUnits = new Vector();
        Iterator<PersistenceUnitInfo> persistenceUnits = PersistenceUnitProcessor.getPersistenceUnits(url, sessionClassLoader).iterator();
        while (persistenceUnits.hasNext()){
            PersistenceUnitInfo persistenceUnitInfo = persistenceUnits.next();
            // Make the callback on the persistence layer 
            boolean predeployed = callPredeploy(persistenceUnitInfo, m);
            if (predeployed){
                predeployedPersistenceUnits.add(persistenceUnitInfo.getPersistenceUnitName());
            }
        }
        // all predeployment calls must occur before any deployment calls get made since
        // deployment calls can result in the loading of classes and predeployment relies 
        // on classes not being loaded
        Iterator<String> predeployed = predeployedPersistenceUnits.iterator();
        while (predeployed.hasNext()){
            // Make the next callback on the persistence layer 
            callDeploy(predeployed.next());
        }
    }

    /**
     * INTERNAL
     * This method initializes the container.  Essentially, it will try to load the
     * class that contains the list of entities and reflectively call the method that
     * contains that list.  It will then initialize the container with that list.
     * If succeeded return true, false otherwise.
     */
    public void initialize(Map m) {
        sessionClassLoader = getMainLoader();
        for (URL url: PersistenceUnitProcessor.findPersistenceArchives()){
            AbstractSessionLog.getLog().log(SessionLog.FINER, "cmp_init_initialize", url);
            initPersistenceUnits(url, m);
        }
    }

    /**
     * INTERNAL:
     * Should be called only by the agent. (when weaving classes)
     * If succeeded return true, false otherwise.
     */
    protected static void initializeFromAgent(Instrumentation instrumentation) throws Exception {
        AbstractSessionLog.getLog().setLevel(JavaSECMPInitializer.getTopLinkLoggingLevel());

        // Squirrel away the instrumentation for later
        globalInstrumentation = instrumentation;
        // Create JavaSECMPInitializer singleton
        javaSECMPInitializer = new JavaSECMPInitializer();
        // Initialize it
        javaSECMPInitializer.initialize(new HashMap());
    }

    /**
     *  Initialize the static entityContainer from a main method.  The entityContainer
     *  can also be initialized from a premain method. with slightly different behavior
     * @param cls the class of the JavaSECMPInitializer.  We will instantiate it reflectively
     * @param m a map containing the set of properties to intantiate with.
     */
    public static void initializeFromMain(Map m) {
        if (javaSECMPInitializer != null) {
            return;
        }

        javaSECMPInitializer = new JavaSECMPInitializer();
        AbstractSessionLog.getLog().setLevel(JavaSECMPInitializer.getTopLinkLoggingLevel());

        AbstractSessionLog.getLog().log(SessionLog.FINER, "cmp_init_initialize_from_main");

        // Initialize it
        javaSECMPInitializer.initialize(m);
    }

    /**
     *  INTERNAL:
     *  Create a list of java.lang.Class that contains the classes of all the entities
     *  that we will be deploying
     */
    protected Set loadEntityClasses(Collection entityNames, ClassLoader classLoader) {
        Set entityClasses = new HashSet();

        // Load the classes using the loader passed in
        AbstractSessionLog.getLog().log(SessionLog.FINER, "cmp_loading_entities_using_loader", classLoader);
        for (Iterator iter = entityNames.iterator(); iter.hasNext();) {
            String entityClassName = (String)iter.next();
            try {
                entityClasses.add(classLoader.loadClass(entityClassName));
            } catch (ClassNotFoundException cnfEx) {
                throw ValidationException.entityClassNotFound(entityClassName, classLoader, cnfEx);
            }
        }
        return entityClasses;
    }

    public Properties getPersistenceUnitProperties(String emName) {
           return emSetupPersistenceUnitInfos.get(emName).getProperties();
    }
            
    /*********************************/
    /***** Temporary Classloader *****/
    /*********************************/
    /** This class loader is provided at initialization time to allow us to temporarily load
     * domain classes so we can examine them for annotations.  After they are loaded we will throw this
     * class loader away.  Transformers can then be registered on the real class loader to allow
     * weaving to occur.
     * 
     * It selectively loads classes based on the list of classnames it is instantiated with.  Classes
     * not on that list are allowed to be loaded by the parent.
     */
    public class TempEntityLoader extends URLClassLoader {
        Collection classNames;
        boolean shouldOverrideLoadClassForCollectionMembers;

        public TempEntityLoader(URL[] urls, ClassLoader parent, Collection classNames, boolean shouldOverrideLoadClassForCollectionMembers) {
            super(urls, parent);
            this.classNames = classNames;
            this.shouldOverrideLoadClassForCollectionMembers = shouldOverrideLoadClassForCollectionMembers;
        }

        public TempEntityLoader(URL[] urls, ClassLoader parent, Collection classNames) {
            this(urls, parent, classNames, true);
        }

        // Indicates if the classLoad should be overridden for the passed className.
        // Returns true in case the class should NOT be loaded by parent classLoader.
        protected boolean shouldOverrideLoadClass(String name) {
            if (shouldOverrideLoadClassForCollectionMembers) {
                // Override classLoad if the name is in collection
                return (classNames != null) && classNames.contains(name);
            } else {
                // Directly opposite: Override classLoad if the name is NOT in collection.
                // Forced to check for java. and javax. packages here, because even if the class
                // has been loaded by parent loader we would load it again
                // (see comment in loadClass)
                return !name.startsWith("java.") && !name.startsWith("javax.") && ((classNames == null) || !classNames.contains(name));
            }
        }

        protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
            if (shouldOverrideLoadClass(name)) {
                // First, check if the class has already been loaded.
                // Note that the check only for classes loaded by this loader,
                // it doesn't return true if the class has been loaded by parent loader
                // (forced to live with that because findLoadedClass method defined as final protected:
                //  neither can override it nor call it on the parent loader)
                Class c = findLoadedClass(name);
                if (c == null) {
                    c = findClass(name);
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            } else {
                return super.loadClass(name, resolve);
            }
        }
    }

}
