/*
 * 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 
 * https://glassfish.dev.java.net/public/CDDLv1.0.html or
 * glassfish/bootstrap/legal/CDDLv1.0.txt.
 * See the License for the specific language governing 
 * permissions and limitations under the License.
 * 
 * When distributing Covered Code, include this CDDL 
 * Header Notice in each file and include the License file 
 * at glassfish/bootstrap/legal/CDDLv1.0.txt.  
 * If applicable, add the following below the CDDL Header, 
 * with the fields enclosed by brackets [] replaced by
 * you own identifying information: 
 * "Portions Copyrighted [year] [name of copyright owner]"
 * 
 * Copyright 2006 Sun Microsystems, Inc. All rights reserved.
 */

package com.sun.jdo.spi.persistence.support.ejb.ejbc;

import com.sun.enterprise.connectors.ConnectorRuntime;
import com.sun.enterprise.deployment.Application;
import com.sun.enterprise.deployment.EjbBundleDescriptor;
import com.sun.enterprise.deployment.PersistenceUnitsDescriptor;
import com.sun.enterprise.deployment.PersistenceUnitDescriptor;
import com.sun.enterprise.deployment.WebBundleDescriptor;
import com.sun.enterprise.deployment.backend.DeploymentEventInfo;
import com.sun.enterprise.deployment.backend.DeploymentStatus;
import com.sun.enterprise.deployment.io.DescriptorConstants;
import com.sun.enterprise.loader.InstrumentableClassLoader;
import com.sun.enterprise.server.PersistenceUnitInfoImpl;
import com.sun.enterprise.server.Constants;

import com.sun.jdo.spi.persistence.support.sqlstore.ejb.EJBHelper;
import com.sun.jdo.spi.persistence.utility.database.DatabaseConstants;
import com.sun.jdo.spi.persistence.utility.I18NHelper;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;

import javax.naming.NamingException;
import javax.persistence.EntityManagerFactory;
import javax.persistence.spi.PersistenceUnitInfo;
import javax.persistence.spi.PersistenceProvider;
import java.sql.Connection;
import javax.sql.DataSource;
import java.sql.Statement;
import java.sql.SQLException;

import oracle.toplink.essentials.ejb.cmp3.EntityManagerFactoryProvider;

/**
 * For each persistence unit descriptors that is defined for 
 * an application create the ddl scripts. Additionally if the
 * user has requested the tables to be created or dropped from
 * the database complete that action too.
 * @author pramodg
 */
public class PersistenceProcessor 
        extends BaseProcessor {
    
    /**
     * The object that would be used by the
     * toplink code when creating a container.
     */
    private PersistenceUnitInfo pi;
   
    /**
     * Creates a new instance of PersistenceProcessor
     * @param info the deployment info object.
     * @param deploy true if this is a deploy event.
     * @param redeploy true if this application is being redeployed.
     * @param cliCreateTables the cli string related to creating tables
     * at deployment time.
     * @param cliDropAndCreateTables the cli string that indicates that
     * old tables have to be dropped and new tables created.
     * @param cliDropTables the cli string to indicate that the tables
     * have to dropped at undeploy time.
     */
    public PersistenceProcessor(
            DeploymentEventInfo info, boolean deploy, boolean redeploy, 
            String cliCreateTables, String cliDropAndCreateTables, String cliDropTables) {
        super(info, deploy, redeploy, cliCreateTables,
            cliDropAndCreateTables, cliDropTables);
    }
 
    /**
     * The entry point into this class. Process
     * any persistence unit descriptors if
     * found for this application.
     */
    protected void processApplication() {
        Collection<PersistenceUnitsDescriptor> bundleCollection = 
                getAllPersistenceUnitDescriptors(application);
        
        if ( bundleCollection.size()  == 0) {     
            return;
        }

        // At this point of time we are sure that we would need to create 
        // the sql/jdbc files required to create or drop objects from the 
        // database. Hence setup the required directories from the info object.
        setApplicationLocation();
        setGeneratedLocation();

        for (PersistenceUnitsDescriptor pus : bundleCollection) {
            for (PersistenceUnitDescriptor bundle : pus.getPersistenceUnitDescriptors()) {
                processAppBundle(bundle); 
            } 
        } 
    }
    
    /**
     * For a given application get all the
     * persistence descriptors that have been 
     * defined.
     * @param application the application that is being 
     *   deployed/redeployed/undeployed.
     * @return a collection of persistenceunitsdescriptors.
     */
    private Collection<PersistenceUnitsDescriptor> 
            getAllPersistenceUnitDescriptors(Application application) {
        List<PersistenceUnitsDescriptor> allPUs = 
                new ArrayList<PersistenceUnitsDescriptor>(
                    application.getPersistenceUnitsDescriptors());
        
        // step #2: load ejb-jar level PUs
        for (Object o : application.getEjbBundleDescriptors()) {
            EjbBundleDescriptor bundle = EjbBundleDescriptor.class.cast(o);
            allPUs.addAll(bundle.getPersistenceUnitsDescriptors()); 
        }

        // step #3: load war level PUs
        for (Object o : application.getWebBundleDescriptors()) {
            WebBundleDescriptor bundle = WebBundleDescriptor.class.cast(o);
            allPUs.addAll(bundle.getPersistenceUnitsDescriptors());
        }
        
        return allPUs;
    }
    
    /**
     * This method does all the work of checking
     * and processing each persistence unit 
     * descriptor.
     * @param bundle the persistence unit descriptor that is being worked on.
     */
    private void processAppBundle(PersistenceUnitDescriptor bundle) { 
       String ddlGenerate = getPersistencePropVal(bundle, 
                EntityManagerFactoryProvider.DDL_GENERATION,
                EntityManagerFactoryProvider.NONE);
        boolean createTables = 
                getCreateTablesValue(bundle, ddlGenerate);                
        boolean dropTables = 
                getDropTablesValue(bundle, ddlGenerate);

        if (debug) {            
            logger.fine("ejb.PersistenceProcessor.createanddroptables", //NOI18N
                new Object[] {new Boolean(createTables), new Boolean(dropTables)});
        }
        constructJdbcFileNames(bundle);        
        if (debug) {
            logger.fine("ejb.PersistenceProcessor.createanddropfilenames", createJdbcFileName, dropJdbcFileName); //NOI18N            
        }
        if(deploy)
            generateDDLFiles(bundle, ddlGenerate);
        
        if (!createTables && !dropTables) { 
            // Nothing to do.
            return;
        }        

        dropTablesFromDB(dropTables, bundle);
        createTablesInDB(createTables, bundle);
    }   
    
    /**
     * We need to create tables only on deploy, and only
     * if the CLI options cliCreateTables or cliDropAndCreateTables
     * are not set to false. If those options are not set (UNDEFINED)
     * the value is taken from the ddl-generate property if defined
     * in persistence.xml.
     * @param bundle the persistence unit descriptor that is being worked on.
     * @param ddlGenerate the string value specified by the user.
     * @return true if tables have to created.
     */
    protected boolean getCreateTablesValue(
            PersistenceUnitDescriptor bundle, String ddlGenerate) {
        boolean createTables = 
            deploy 
                && (cliCreateTables.equals(Constants.TRUE) 
                    || (ddlGenerate.equals(EntityManagerFactoryProvider.CREATE_ONLY)
                    || ddlGenerate.equals(EntityManagerFactoryProvider.DROP_AND_CREATE)
                        && cliCreateTables.equals(Constants.UNDEFINED)));
        
        return createTables;        
    }
    
    /**
     * We need to drop tables on deploy and redeploy, if the 
     * corresponding CLI options cliDropAndCreateTables 
     * (for redeploy) or cliDropTables (for undeploy) are not 
     * set to false.
     * If the corresponding option is not set (UNDEFINED) 
     * the value is taken from the ddl-generate property
     * if defined in persistence.xml.
     * @param bundle the persistence unit descriptor that is being worked on.
     * @param ddlGenerate the string value specified by the user.
     * @return true if the tables have to be dropped.
     */
    protected boolean getDropTablesValue(
            PersistenceUnitDescriptor bundle, String ddlGenerate) {        
        boolean dropTables = 
            (redeploy 
                && (cliDropAndCreateTables.equals(Constants.TRUE) 
                    || (ddlGenerate.equals(EntityManagerFactoryProvider.DROP_AND_CREATE)
                        && cliDropAndCreateTables.equals(Constants.UNDEFINED))))
            || (!deploy
                && (cliDropTables.equals(Constants.TRUE) 
                    || (ddlGenerate.equals(EntityManagerFactoryProvider.DROP_AND_CREATE)
                        && cliDropTables.equals(Constants.UNDEFINED)))); 
          
        return dropTables;
    }
 
    /**
     * Construct the name of the create and 
     * drop jdbc ddl files that would be 
     * created. These name would be either
     * obtained from the persistence.xml file
     * (if the user has defined them) or we would
     * create default filenames
     * @param parBundle the persistence unit descriptor that is being worked on.
     */
    private void constructJdbcFileNames(PersistenceUnitDescriptor parBundle)  {
        createJdbcFileName = 
                getPersistencePropVal(parBundle, 
                EntityManagerFactoryProvider.CREATE_JDBC_DDL_FILE, null);
        dropJdbcFileName = 
                getPersistencePropVal(parBundle, 
                EntityManagerFactoryProvider.DROP_JDBC_DDL_FILE, null);

        if((null != createJdbcFileName) && (null != dropJdbcFileName)) {
            return;
        }         
        
        String filePrefix = 
                    EJBHelper.getDDLNamePrefix(parBundle.getParent().getParent());

        if(null == createJdbcFileName) {
            createJdbcFileName = filePrefix + NAME_SEPARATOR + parBundle.getName() +
                CREATE_DDL_JDBC_FILE_SUFFIX;            
        }
        if(null == dropJdbcFileName) {
            dropJdbcFileName = filePrefix + NAME_SEPARATOR + parBundle.getName() +
                DROP_DDL_JDBC_FILE_SUFFIX;
        } 
    }
    
    /**
     * If this is a deploy event and the user 
     * has defined property to generate the 
     * ddl files, call into the toplink code and
     * get the ddl files generated.
     * If the user has defined 'createtables'
     * or 'dropandcreate' we would create the
     * ddl files.
     * @param bundle the persistence unit descriptor that is being worked on.
     * @param ddlGenerate the string value defined by user.
     */
    private void generateDDLFiles(PersistenceUnitDescriptor bundle, 
            String ddlGenerate) {
        if(ddlGenerate.equals(EntityManagerFactoryProvider.NONE))
            return;
        
        pi = loadPersistenceUnitBundle(bundle, 
            createJdbcFileName, dropJdbcFileName);
    }
        
    /**
     * We need to get the datasource  information if
     * that has been defined  in the persistence.xml. 
     * Did not want to duplicate code here. So we 
     * create a persistence unit info object and get
     * the nonJTADatasource from that object
     * Then if the drop file is present, drop the tables
     * from the database.
     * @param dropTables true if the table need to be dropped.
     * @param bundle the persistence unit descriptor that is being worked on.
     * @return true if tables were successfully dropped.
     */
    protected boolean dropTablesFromDB(boolean dropTables,
            PersistenceUnitDescriptor bundle) {
        boolean dropResult = false;
        File dropFile = null;
        
        if(dropTables) {

            dropFile = getDropDDLFile(dropJdbcFileName,  deploy);
            if (dropFile.exists()) {
                PersistenceUnitInfo pi = new PersistenceUnitInfoImpl(
                    bundle,
                    appDeployedLocation,
                    null);
                dropResult = executeDDLStatement(dropFile, 
                    pi.getNonJtaDataSource());
            }
        }
        return dropResult;
    }
    
    /**
     * Call into toplink code to generate the ddl files.
     * Then get the create jdbc ddl file and execute it
     * against the database
     * @param createTables true if tables have to be created.
     * @param bundle the persistence unit descriptor that is being worked on
     * @return true if tables were successfully created.
     */
    protected boolean createTablesInDB(boolean createTables, 
            PersistenceUnitDescriptor bundle) {
        boolean createResult = false;
        if(createTables) {
            pi = loadPersistenceUnitBundle(bundle, 
                    createJdbcFileName, dropJdbcFileName);

            File createFile =  getCreateDDLFile(
                        appGeneratedLocation +  createJdbcFileName);
            if(createFile.exists()) {
                createResult = executeDDLStatement( createFile, 
                        pi.getNonJtaDataSource());
            } 
            createResult = true;
        }
        return createResult;
    }
    

    /**
     * Get the ddl files eventually executed 
     * against the database. This method deals 
     * with both create and drop ddl files.
     * @param fileName the create or drop jdbc ddl file.
     * @param nonJtaDataSource the nonJtaDataSource
     *    that is obtained from the persistence info object that we had created.
     * @return true if the tables were successfully 
     *    created/dropped from the database.
     */
    private boolean executeDDLStatement(File fileName, DataSource nonJtaDataSource ) {
        boolean result = false;
        Connection conn = null;
        Statement sql = null;
        try {           
            try {
                conn = nonJtaDataSource.getConnection();
                sql = conn.createStatement();
                result = true;
            } catch (SQLException ex) {
                logI18NWarnMessage(
                     "ejb.BaseProcessor.cannotConnect", 
                    appRegisteredName,  null, ex);                
            } 
            if(result) {               
                executeDDLs(fileName, sql);
            }
        } catch (IOException e) {
            fileIOError(application.getRegistrationName(), e);            
        } finally {
            closeConn(conn);
        }
        return result;        
    }

    
    /**
     * Since the ddl files are actually created 
     * by the toplink code, we ensure that the 
     * correct properties are put in the persistence 
     * unit info object.
     * @param puDescriptor the persistence unit descriptor that is being worked on.
     */
    private void addPropertiesToPU(PersistenceUnitDescriptor puDescriptor) {
        addPropertyToDescriptor(puDescriptor, 
                EntityManagerFactoryProvider.APP_LOCATION, 
                appGeneratedLocation);
        addPropertyToDescriptor(puDescriptor, 
                EntityManagerFactoryProvider.DDL_GENERATION, 
                EntityManagerFactoryProvider.DROP_AND_CREATE);
        addPropertyToDescriptor(puDescriptor, 
                EntityManagerFactoryProvider.CREATE_JDBC_DDL_FILE, 
                createJdbcFileName);
        addPropertyToDescriptor(puDescriptor, 
                EntityManagerFactoryProvider.DROP_JDBC_DDL_FILE, 
                dropJdbcFileName);
    }
    
    /**
     * Utility method that is used to actually set the property into the persistence unit descriptor.
     * @param descriptor the persistence unit descriptor that is being worked on.
     * @param propertyName the name of the property.
     * @param propertyValue the value of the property.
     */
    private void addPropertyToDescriptor(PersistenceUnitDescriptor descriptor, 
            String propertyName, String propertyValue) {
        String oldPropertyValue = descriptor.getProperties().getProperty(propertyName);
        if(null == oldPropertyValue) {
            descriptor.addProperty(propertyName, propertyValue);
        }        
    }
    
    /**
     * Since the actual jdbc files are generated
     * by the toplink code, we need to create
     * a persistence unit info object and then
     * call into the toplink code.
     * @param persistenceUnitDescriptor the persistence unit descriptor that is
     * being worked on.
     * @param createJdbcFileName the string name of the create
     * jdbc ddl file.
     * @param dropJdbcFileName the string name of the drop 
     * jdbc ddl file.
     * @return the persistence unit info object that 
     * would used by  toplink code.
     */
    private PersistenceUnitInfo loadPersistenceUnitBundle(
            PersistenceUnitDescriptor persistenceUnitDescriptor,
            String createJdbcFileName, String dropJdbcFileName) {
        logger.entering("loadPersistenceUnitBundle", "load",
                new Object[]{persistenceUnitDescriptor});

        PersistenceProvider provider;
        addPropertiesToPU(persistenceUnitDescriptor);  

        final InstrumentableClassLoader cl =
                InstrumentableClassLoader.class.cast(
                    persistenceUnitDescriptor.getClassLoader());
        PersistenceUnitInfo pi = new PersistenceUnitInfoImpl(
                persistenceUnitDescriptor,
                appDeployedLocation,
                cl);
        if(debug)
            logger.fine("PersistenceInfo for PU is :\n" + pi);
        
        provider = new EntityManagerFactoryProvider();
        EntityManagerFactory emf = null;
        try {
            emf = provider.createContainerEntityManagerFactory(pi, null);
            if(debug)
                logger.fine("PersistenceProcessor", "loadPersistenceUnitBundle",
                    "emf = {0}", emf);
        } finally {
            if(emf != null) {
                emf.close();
            }
        }
        return pi;
    }
    
    /**
     * Given a persistence unit descriptor 
     * return the value of a property if the
     * user has specified it.
     * If the user has not defined this property
     * return the default value.
     * @param parBundle the persistence unit descriptor that is being worked on.
     * @param propertyName the property name being checked.
     * @param defaultValue the default value to be used.
     * @return the property value.
     */
    private String getPersistencePropVal(PersistenceUnitDescriptor parBundle, 
        String propertyName, String defaultValue) {
        String propertyValue;
        if(null == defaultValue)
            propertyValue = parBundle.getProperties().getProperty(propertyName);
        else
             propertyValue = parBundle.getProperties().
                     getProperty(propertyName, defaultValue);

        return propertyValue;
    }      
    
} 
