/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 * 
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, 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/CDDL+GPL.html
 * or glassfish/bootstrap/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
 * Sun designates this particular file as subject to the "Classpath" exception
 * as provided by Sun in the GPL Version 2 section of the License file that
 * accompanied this code.  If applicable, add the following below the License
 * Header, with the fields enclosed by brackets [] replaced by your own
 * identifying information: "Portions Copyrighted [year]
 * [name of copyright owner]"
 * 
 * Contributor(s):
 * 
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

/*
 * MappingGenerator.java
 *
 * Created on Aug 18, 2003
 */

package com.sun.jdo.api.persistence.mapping.ejb;

import java.util.*;
import java.sql.Types;
import java.io.IOException;

import com.sun.jdo.api.persistence.mapping.ejb.beans.*;

import com.sun.jdo.api.persistence.model.Model;
import com.sun.jdo.api.persistence.model.ModelException;
import com.sun.jdo.api.persistence.model.mapping.MappingClassElement;
import com.sun.jdo.api.persistence.model.mapping.MappingFieldElement;
import com.sun.jdo.api.persistence.model.jdo.PersistenceClassElement;
import com.sun.jdo.api.persistence.model.jdo.PersistenceFieldElement;

import com.sun.jdo.spi.persistence.utility.StringHelper;
import com.sun.jdo.spi.persistence.utility.JavaTypeHelper;

import com.sun.jdo.spi.persistence.generator.database.DatabaseGenerator;
import com.sun.jdo.spi.persistence.generator.database.MappingPolicy;

import org.netbeans.modules.dbschema.*;
import org.netbeans.modules.dbschema.jdbcimpl.SchemaElementImpl;
import org.netbeans.modules.dbschema.util.NameUtil;

import org.netbeans.modules.schema2beans.Schema2BeansException;

/*
 * This class will generate mapping classes from sun-cmp-mappings.xml
 * and dbschema if they are available in the jar, or it will generate mapping 
 * classes based on ejb-jar.xml, bean classes and policy by invoking the 
 * database generation backend.
 *
 * @author Jie Leng 
 */
public class MappingGenerator {

    // Since "_JDOState" is defined as private in IASEjbCMPEntityDescriptor,
    // redefined here for passing it in DatabaseGenerator.
    private static final String CLASS_SUFFIX = "_JDOState"; // NOI18N

    private static final String FAKE_NAME = "fakename"; // NOI18N

    private final EJBInfoHelper infoHelper;
    private final Model model;
    private final AbstractNameMapper nameMapper;
    private final ClassLoader loader;
    private final ConversionHelper ddHelper;

    /** a boolean indicating whether the jdo model and mapping model should
     * contain generated fields
     */
    private boolean skipGeneratedFields = false;

    //hold strong reference to mapping class elements 
    private List strongRefs = new ArrayList();

    /** 
     * Constructor
     * @param infoHelper an instance of an EJBInfoHelper
     * @param model a model containing mapping class and 
     * persistence class information
     * @param loader a class loader
     * @param skipGeneratedFields a boolean indicating to remove generated 
     * fields from jdo model and mapping model 
     */
    public MappingGenerator(EJBInfoHelper infoHelper, 
            ClassLoader loader, boolean skipGeneratedFields) {
        this.infoHelper = infoHelper;
        this.model = infoHelper.getModel();
        this.loader = loader;
        this.nameMapper = infoHelper.getNameMapper();
        this.ddHelper = infoHelper.createConversionHelper();
        this.skipGeneratedFields = skipGeneratedFields;
    }

    protected EJBInfoHelper getInfoHelper() {
        return infoHelper;
    }

    protected ClassLoader getClassLoader() {
        return loader;
    }

    protected AbstractNameMapper getNameMapper() {
        return nameMapper;
    }

    protected ConversionHelper getConversionHelper() {
        return ddHelper;
    }

    /**
     * Create mapping classes and schema based on database vendor name.
     * @param dbName a string for database vendor name
     * @param uniqueTableNames a string to determin if use unique table names
     * during database generation
     * @param userPolicy a property object holding user overrides
     * @param inputFilesPath a directory where sun-cmp-mappings.xml is located
     * @throws IOException
     * @throws Schema2BeansException
     * @throws ModelException
     * @throws DBException
     * @throws ConversionException
     */
    public DatabaseGenerator.Results generateMappingClasses(String dbName,
            String uniqueTableNames, Properties userPolicy,
            String inputFilesPath)
            throws IOException, Schema2BeansException, ModelException,
            DBException, ConversionException {

        // generate mapping classes and dbschema in memory
        SunCmpMappings sunCmpMappings = null;

        // sun-cmp-mappings.xml does not exist, use DatabaseGenerator
        // to generate sun-cmp-mappings.xml, *.dbschema

        List pcClasses = new ArrayList();
        sunCmpMappings = getPartialSunCmpMappings(
                pcClasses, 
                Boolean.valueOf(uniqueTableNames).booleanValue());

        // load real jdo model and fake mapping model in memory
        ddHelper.setEnsureValidation(false);

        // create fake schema for partial mapping
        SchemaElement fakeSchema = new SchemaElement(new SchemaElementImpl());
        fakeSchema.setName(DBIdentifier.create(FAKE_NAME));

        // add newly created fake schema to SchemaElement cache
        SchemaElement.addToCache(fakeSchema);

        // pass null as class loader in order for MappingFile to load schema
        // from cache not from disk.
        loadMappingClasses(sunCmpMappings, null);

        DatabaseGenerator.Results results = generateSchema(pcClasses,
                dbName, uniqueTableNames, userPolicy);

        SchemaElement schema = results.getSchema();
        Set mappingClasses = results.getMappingClasses();

        // remove fake schema from cache since the correct schema is generated.
        SchemaElement.removeFromCache(FAKE_NAME);

        // clean up old version of schema in SchemaElement cache
        // if there is one
        SchemaElement.removeFromCache(schema.getName().getName());

        // add newly created schema to SchemaElement cache
        SchemaElement.addToCache(schema);

        // update mapping classes
        updateMappingClasses(mappingClasses);

        // If skipGeneratedFields is set to true, the generated fields should 
        // not be kept in jdo model and mapping model. 
        // Remove generated fields from jdo model and mapping 
        // model before returning the result.
        if (skipGeneratedFields) {
            Iterator iter = mappingClasses.iterator();
            while (iter.hasNext()) {
                MappingClassElement mapClassElt = (MappingClassElement)iter.next();
                if (mapClassElt != null) {
                    String className = mapClassElt.getName();
                    String ejbName = nameMapper.getEjbNameForPersistenceClass(
                        className);

                    PersistenceClassElement pce = (PersistenceClassElement)
                            model.getPersistenceClass(className);
                    PersistenceFieldElement[] allFields = pce.getFields();
                    if (allFields != null) {
                        List generatedFieldList = new ArrayList();

                        // In order to avoid concurrentmod exception,
                        // loop through all persistence fields to put generated
                        // fields in a list, loop though the list to remove
                        // the generated fields from the model.
                        for (int i = 0; i < allFields.length; i++) { 
                            PersistenceFieldElement pfe = allFields[i];
                            if (pfe != null) { 
                                String pFieldName = pfe.getName();
                                String ejbFieldName = nameMapper.
                                    getEjbFieldForPersistenceField(className, 
                                    pFieldName);
                                if (nameMapper.isGeneratedField(ejbName, 
                                    ejbFieldName)) {
                                    generatedFieldList.add(pfe);
                                }
                            }
                        }

                        // If the field is a version field, don't remove it 
                        // from the model even though it is generated because 
                        // it is needed to hold the version column information.
                        Iterator iterator = generatedFieldList.iterator();
                        while (iterator.hasNext()) {
                            PersistenceFieldElement pfe = 
                                (PersistenceFieldElement)iterator.next();
                            MappingFieldElement mfe = mapClassElt.
                                 getField(pfe.getName());
                            if (mfe != null && (!mfe.isVersion())) {
                                model.removeFieldElement(pfe);
                                mapClassElt.removeField(mfe);
                            }
                        }
                    }
                }
            }
        }

        return results;
    }

    /**
     * load mapping classes from SunCmpMappings object
     * @param sunMapping a SunCmpMappings object representing
     * sun-cmp-mappings.xml in memory
     * @param classLoader a class loader object
     * @return a map object containing ejb names and mapping classes
     * @throws DBException
     * @throws ModelException
     * @throws ConversionException
     */
    protected Map loadMappingClasses(SunCmpMappings sunMapping, 
        ClassLoader classLoader) 
        throws DBException, ModelException, ConversionException {
        MappingFile mapFile = new MappingFile(classLoader);

        Map allMappings = mapFile.intoMappingClasses(sunMapping, ddHelper);

        updateMappingClasses(allMappings.values());

        return allMappings;
    }

    /** 
     * Clean up strong reference. It should be called by end of deployment
     * or deploytool.
     */
    public void cleanup() {
        // Remove the strong references to MappingClassElements
        // needed during deployment. The mapping class cache
        // can now be cleaned up by the garbage collector.
        strongRefs.clear();
    }

    /** 
     * Call DatabaseGenerator to generate database model and mapping model
     * @param pcClasses a list of DatabaseGenerator.GeneratorNameTuple objects 
     * @param dbVendorName the string of database name
     * @param useUniqueTableNames the string to determine use of unique table
     * names for database generation
     * @param userPolicy the property having user defined mappings between
     * class field and jdbc type
     * @return DatabaseGenerator.Results contains mapping classes and schema
     * @throws IOException
     * @throws DBException
     * @throws ModelException
     */
    private DatabaseGenerator.Results generateSchema(List pcClasses, 
            String dbName, String useUniqueTableNames, 
            Properties userPolicy) 
            throws IOException, DBException, ModelException {

        MappingPolicy mappingPolicy = MappingPolicy.getMappingPolicy(dbName);
        mappingPolicy.setUserPolicy(userPolicy);

        if (isPropertyDefined(useUniqueTableNames)) {
            // It was explicitly set.
            mappingPolicy.setUniqueTableName(
                Boolean.valueOf(useUniqueTableNames).booleanValue());
        }

       return DatabaseGenerator.generate(
                model, pcClasses, mappingPolicy, 
                infoHelper.getSchemaNameToGenerate(), CLASS_SUFFIX, true);
    }

    /** 
     * Puts mapping classes into model's cache
     * @param mappingClasses a collection of mapping classes
     */ 
    private void updateMappingClasses(Collection mappingClasses) {
        Iterator iter = mappingClasses.iterator();
        while (iter.hasNext()) {
            MappingClassElement mapClassElt = (MappingClassElement)iter.next();
            //put it in the models' cache
            model.updateKeyForClass(mapClassElt, null);
            //keep a strong ref
            strongRefs.add(mapClassElt);
        }
    }

    /** 
     * Generates partial sun-cmp-mapping (contains fake table name and 
     * fake column name) for MappingFile.intoMappings()
     * @param pcClasses a list of DatabaseGenerator.NameTuple objects 
     * @param useUniqueTableNames a boolean to determine whether to use 
     * unique table names during database generation
     * @return a SunCmpMappings object
     * @throws Schema2BeansException
     */
    private SunCmpMappings getPartialSunCmpMappings(List pcClasses, 
             boolean useUniqueTableNames) throws Schema2BeansException {

       // Create a new name mapper with perisistence class name differing
        // from bean name if useUniqueTableName flag is true. 
        // So persistence class name can be used for unique table name.
        AbstractNameMapper nameMapper2 = (useUniqueTableNames) ? 
                infoHelper.createUniqueNameMapper() : nameMapper;

        SunCmpMappings mappings = null;
        mappings = new SunCmpMappings();
        SunCmpMapping mapping = new SunCmpMapping();
        mapping.setSchema(FAKE_NAME);

        Iterator iter = infoHelper.getEjbNames().iterator();
        while (iter.hasNext()) {
            String ejbName = (String)iter.next();
            String pcClass = ddHelper.getMappedClassName(ejbName);
            String hashClassName = JavaTypeHelper.getShortClassName(pcClass);

            // Make sure hash class name differs from ejb name 
            // if useUniqueTableName flag is true.
            // if useUniqueTableName flag is false, ejb name is used for 
            // table name and hash class name is ignored. 
            if (useUniqueTableNames & hashClassName.equals(ejbName)) {
                hashClassName = JavaTypeHelper.getShortClassName(
                       nameMapper2.getPersistenceClassForEjbName(ejbName));
                pcClasses.add(new DatabaseGenerator.NameTuple(
                       pcClass, ejbName, hashClassName));
            }
            else {
                pcClasses.add(new DatabaseGenerator.NameTuple(
                       pcClass, ejbName));
            }

            EntityMapping entity = new EntityMapping();
            entity.setEjbName(ejbName);
            entity.setTableName(FAKE_NAME);
            Collection fields = infoHelper.getFieldsForEjb(ejbName);
            Collection rels = infoHelper.getRelationshipsForEjb(ejbName);
            fields.removeAll(rels);
            // cmp field
            Iterator fIter = fields.iterator();
            while (fIter.hasNext()) {
                String fieldName = (String)fIter.next();
                CmpFieldMapping cmpField = new CmpFieldMapping();
                cmpField.setFieldName(fieldName);
                cmpField.addColumnName(FAKE_NAME);
                entity.addCmpFieldMapping(cmpField);
            } 
            // cmr field
            fIter = rels.iterator();
            while (fIter.hasNext()) {
                String fieldName = (String)fIter.next();
                CmrFieldMapping cmrField = new CmrFieldMapping();
                cmrField.setCmrFieldName(fieldName);
                ColumnPair columnPair = new ColumnPair();
                columnPair.addColumnName(FAKE_NAME);
                columnPair.addColumnName(FAKE_NAME);
                cmrField.addColumnPair(columnPair);
                entity.addCmrFieldMapping(cmrField);
            }
            mapping.addEntityMapping(entity);
        }

        mappings.addSunCmpMapping(mapping);

        return mappings;
    }

    /** 
     * Returns <code>true</code> if the specified propertyValue represents
     * a defined value, <code>false</code> otherwise.  This implementation 
     * returns <code>true</code> if the value is not empty, but subclasses
     * may override this method to compare to a constant which represents an
     * undefined value.
     * @param propertyValue the value to be tested for defined
     * @return <code>true</code> if the specified propertyValue represents
     * a defined value, <code>false</code> otherwise
     */
    protected boolean isPropertyDefined(String propertyValue) {
        return !StringHelper.isEmpty(propertyValue);
    }

    /** 
     * Update column in the SchemaElement with jdbc type and its length, 
     * scale and precision.
     * @param column a ColumnElement to be updated
     * @param jdbcType jdbc type from java.sql.Types
     * @param length an Integer for length or <code>null</code> 
     * if it does not apply
     * @param scale an Integer for scale or <code>null</code> 
     * if it does not apply
     * @param precision an Integer for precision or <code>null</code> 
     * if it does not apply
     */
    public static void updateColumn(ColumnElement column, int jdbcType, 
            Integer length, Integer scale, Integer precision)
            throws DBException {

        column.setType(jdbcType);
        column.setLength(length);
        column.setScale(scale);
        column.setPrecision(precision);
    }

    /**
     * This method updates properties which stores user override policy.
     * @param prop the property for user override
     * @param className a string for bean class
     * @param fieldName a string for field
     * @param jdbcType jdbc type from java.sql.Types
     * @param length an Integer for length or <code>null</code>
     * if it does not apply
     * @param scale an Integer for scale or <code>null</code>
     * if it does not apply
     * @param precision an Integer for precision or <code>null</code>
     * if it does not apply
     */
    public static void updateProperties(Properties prop, String className, 
            String fieldName, int jdbcType, Integer length, Integer scale, 
            Integer precision) {

        prop.setProperty(
                MappingPolicy.getOverrideForType(className, fieldName),
                MappingPolicy.getJdbcTypeName(jdbcType));

        updateProperty(prop, MappingPolicy.getOverrideForLength(
                className, fieldName), length);

        updateProperty(prop, MappingPolicy.getOverrideForScale(
                className, fieldName), scale);

        updateProperty(prop, MappingPolicy.getOverrideForPrecision(
                className, fieldName), precision);
    }

    /**
     * This method updates property. If the value is not <code>null</code>,
     * update the property. If the value is <code>null</code>, 
     * remove the property.
     * @param prop a property object which needs to be updated
     * @param key a key for the property
     * @param value a value for the propety
     */
    private static void updateProperty(Properties prop, String key, 
            Integer value) {
        if (value != null) {
            prop.setProperty(key, value.toString());
        }
        else {
           prop.remove(key);
        }
    }

    /**
     * The contents of this class will eventually be added to SQLTypeUtil 
     * in dbmodel. It is an util class which provides methods for jdbc type 
     * compatible and jdbc attribute.
     */
    public static class SQLTypeUtil {

        private static final Map characterMap = new HashMap();
        private static final Map numericMap = new HashMap();
        private static final Map blobMap = new HashMap();
        private static final Map timeMap = new HashMap();
        
        private static final String NONE_ATTRIBUTE = "none";
        private static final String LENGTH_ATTRIBUTE = "length";
        private static final String SCALE_ATTRIBUTE = "scale";
        private static final String SCALE_PRECISION_ATTRIBUTE = "scale-precision";

        static {
            characterMap.put(new Integer(Types.CHAR), LENGTH_ATTRIBUTE);
            characterMap.put(new Integer(Types.VARCHAR), LENGTH_ATTRIBUTE);
            characterMap.put(new Integer(Types.CLOB), LENGTH_ATTRIBUTE);

            numericMap.put(new Integer(Types.BIT), NONE_ATTRIBUTE);
            numericMap.put(new Integer(Types.TINYINT), NONE_ATTRIBUTE);
            numericMap.put(new Integer(Types.SMALLINT), NONE_ATTRIBUTE);
            numericMap.put(new Integer(Types.BIGINT), NONE_ATTRIBUTE);
            numericMap.put(new Integer(Types.INTEGER), NONE_ATTRIBUTE);
            numericMap.put(new Integer(Types.DOUBLE), NONE_ATTRIBUTE);
            numericMap.put(new Integer(Types.DECIMAL), SCALE_PRECISION_ATTRIBUTE);
            numericMap.put(new Integer(Types.REAL), NONE_ATTRIBUTE);

            blobMap.put(new Integer(Types.BLOB), LENGTH_ATTRIBUTE);

            timeMap.put(new Integer(Types.DATE), NONE_ATTRIBUTE);
            timeMap.put(new Integer(Types.TIME), NONE_ATTRIBUTE);
            timeMap.put(new Integer(Types.TIMESTAMP), NONE_ATTRIBUTE);
        }

        /** Returns if the given data type is numeric type or not.
         * @param jdbcType the type from java.sql.Types
         * @return <code>true</code> if the given type is numeric type; 
         * <code>false</code> otherwise
         */
        public static boolean isNumeric (int jdbcType) {
            return checkType(jdbcType, numericMap);
        }

        /** Returns if the given data type is character type or not.
         * @param jdbcType the type from java.sql.Types
         * @return <code>true</code> if the given type is character type; 
         * <code>false</code> otherwise
         */
        public static boolean isCharacter (int jdbcType) {
            return checkType(jdbcType, characterMap);
        }

        /** Returns if a given data type is blob type or not.
         * @param jdbcType the type from java.sql.Types
         * @return <code>true</code> if the give type is blob type; 
         * <code>false</code> otherwise
         */
        public static boolean isBlob (int jdbcType) {
            return checkType(jdbcType, blobMap);
        }

        /** Returns if a given data type is time type or not.
         * @param jdbcType the type from java.sql.Types
         * @return <code>true</code> if the give type is time type; 
         * <code>false</code> otherwise
         */
        public static boolean isTime (int jdbcType) {
            return checkType(jdbcType, timeMap);
        }

        private static boolean checkType(int jdbcType, Map jdbcTypes) {
            return jdbcTypes.containsKey(new Integer(jdbcType));
        }

        /** Returns a collection of compatible jdbc types.
         * @param jdbcType the type from java.sql.Types 
         * @return a collection of compatible jdbc types 
         */
        public static Collection getCompatibleTypes(int jdbcType) {
            if (isNumeric(jdbcType)) {
                return numericMap.keySet();
            }
            else if (isCharacter(jdbcType)) {
                return characterMap.keySet();
            }
            else if (isBlob(jdbcType)) {
                return blobMap.keySet();
            }
            else if (isTime(jdbcType)) {
                return timeMap.keySet();
            }
            return null;
        }

        /**
         * This method returns true if the jdbc type has scale. 
         * @param jdbcType a jdbc type from java.sql.Types
         * @return <code>true</code> if the type has scale;
         * <code>false</code> otherwise 
         */
        public static boolean hasScale(int jdbcType) {
            if (getAttribute(jdbcType).equals(SCALE_ATTRIBUTE)
                    || getAttribute(jdbcType).equals(SCALE_PRECISION_ATTRIBUTE))
                return true;
            return false;
        }

        /**
         * This method returns true if the jdbc type has precision. 
         * If the jdbc type has the precision, it means it also has scale. 
         * @param jdbcType a jdbc type from java.sql.Types
         * @return <code>true</code> if the type has precision; 
         * <code>false</code> otherwise 
         */
        public static boolean hasPrecision(int jdbcType) {
            if (getAttribute(jdbcType).equals(SCALE_PRECISION_ATTRIBUTE))
                return true;
            return false;
        }

        /**
         * This method returns true if the jdbc type has length
         * @param jdbcType a jdbc type from java.sql.Types
         * @return <code>true</code> if the type has length;
         * <code>false</code> otherwise
         */
        public static boolean hasLength(int jdbcType) {
            if (getAttribute(jdbcType).equals(LENGTH_ATTRIBUTE))
                return true;
            return false;
        }

        private static String getAttribute(int jdbcType) {
            if (isNumeric(jdbcType)) {
                return (String)numericMap.get(new Integer(jdbcType));
            }
            else if (isCharacter(jdbcType)) {
                return (String)characterMap.get(new Integer(jdbcType));
            }
            else if (isBlob(jdbcType)) {
                return (String)blobMap.get(new Integer(jdbcType));
            }
            else if (isTime(jdbcType)) {
                return (String)timeMap.get(new Integer(jdbcType));
            }
            return NONE_ATTRIBUTE;
        }
    }
}
