/*
 * Copyright 2006 Le Duc Bao, Ralf Joachim
 * 
 * Licensed 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.
 */
package org.castor.ddlgen;

import java.io.OutputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.castor.ddlgen.schemaobject.Field;
import org.castor.ddlgen.schemaobject.ForeignKey;
import org.castor.ddlgen.schemaobject.KeyGenerator;
import org.castor.ddlgen.schemaobject.PrimaryKey;
import org.castor.ddlgen.schemaobject.Schema;
import org.castor.ddlgen.schemaobject.Table;
import org.castor.ddlgen.typeinfo.TypeInfo;
import org.exolab.castor.mapping.Mapping;
import org.exolab.castor.mapping.xml.ClassChoice;
import org.exolab.castor.mapping.xml.ClassMapping;
import org.exolab.castor.mapping.xml.FieldMapping;
import org.exolab.castor.mapping.xml.KeyGeneratorDef;
import org.exolab.castor.mapping.xml.MapTo;
import org.exolab.castor.mapping.xml.MappingRoot;
import org.exolab.castor.mapping.xml.Sql;

/**
 * AbstractGenerator is the base class for various DDL generator of specific DB and
 * handles following tasks: 
 * <li/> Extract information from Mapping to Schema
 * <li/> Loop through the schema and provide a skeleton for DDL creation
 * 
 * <p/>AbstractGenerator will automatically extract necessary information for DDL 
 * creation. That information is handled by Schema.
 * <p/>To create new generator for a DBMS, you should:
 * <li/> Overwrite this class to create new generator for a DBMS. 
 * <li/> If the syntax of DBMS is different to standard DDL syntax, you should 
 * overwrite SchemaObject (Table, Field, KeyGenerator, Index, ForeignKey,...) classes. 
 * The class SchemaObjectFactory who handles the SchemaObject creation must 
 * be overwritten.
 * <li/> You must overwrite the TypeMapper if mapping between JDBC types and 
 * specific DBMS’s types is different among various DBMS.
 * <p/>The example bellow shows how to create a generator for DB2:
 * <li/> <b>Generator for DB2</b>
 * <pre>
 *public class Db2Generator extends AbstractGenerator {
 *
 *    public Db2Generator(final String globConf, final String dbConf)
 *            throws GeneratorException {
 *        super(globConf, dbConf);
 *        setTypeMapper(new Db2TypeMapper(getConf()));
 *    }
 *}   
 * </pre>
 * <li/><b>TypeMapper for DB2</b>
 * <pre>
 *public final class Db2TypeMapper extends AbstractTypeMapper {
 *    public Db2TypeMapper(final Configuration conf) {
 *        super(conf);
 *    }
 * 
 *    protected void initialize(final Configuration conf) {
 *        // numeric types
 *        this.add(new NotSupportedType("bit"));
 *        LOG.warn("Db2 does not support 'TINY' type, use SMALLINT instead.");
 *        this.add(new NoParamType("tinyint", "SMALLINT"));
 *        this.add(new NoParamType("smallint", "SMALLINT"));
 *        this.add(new NoParamType("integer", "INTEGER"));
 *        this.add(new NoParamType("bigint", "BIGINT"));
 *    }
 *}
 *</pre>
 * <li/><b>Field for DB2</b>
 *<pre> 
 *public class Db2Field extends Field {
 *    public Db2Field() {
 *        super();
 *    }
 *
 *    public String toDDL() throws GeneratorException {
 *        StringBuffer buff = new StringBuffer();
 *        buff.append(getName()).append(" ");
 *        buff.append(getType().toDDL(this));
 *        
 *        if (isIdentity()) {
 *            buff.append(" NOT NULL");
 *        }
 *        
 *        KeyGenerator keyGen = getKeyGenerator();
 *        if (keyGen != null && isIdentity()) {
 *            
 *            if (KeyGenerator.IDENTITY_KEY.equalsIgnoreCase(keyGen.getName())) {
 *                buff.append(" GENERATED BY DEFAULT AS IDENTITY ").
 *                    append("START WITH 1 INCREMENT BY 1");
 *            }
 *        }
 *
 *        return buff.toString();
 *    }
 *}
 *</pre>
 * <li/><b>Field for DB2</b>
 *<pre> 
 *public class Db2SchemaFactory extends SchemaFactory {
 *    public Db2SchemaFactory() {
 *        super();
 *    }
 *    public Field createField() {
 *        return new Db2Field();
 *    }
 *
 *}
 *</pre>
 * The GeneratorFactory class handles the specific database generator creation. 
 * For example:
 * <pre>
 *  Generator generator = GeneratorFactory.
 *      createDDLGenerator(“mysql”, “ddl.properties”, “mysql.properties”);
 * </pre>
 *  
 * And to generate DDL, it should specify the printer and call generateDDL method.
 * <pre>
 *  generator.setPrinter(System.out);
 *  Mapping mapping = new Mapping();
 *  mapping.loadMapping("mapping.xml");
 *  generator.generateDDL(mapping);            
 * </pre>
 * 
 * @author <a href="mailto:leducbao AT gmail DOT com">Le Duc Bao</a>
 * @author <a href="mailto:ralf DOT joachim AT syscon DOT eu">Ralf Joachim</a>
 * @version $Revision: 5951 $ $Date: 2006-04-25 16:09:10 -0600 (Tue, 25 Apr 2006) $
 * @since 1.1
 */
public abstract class AbstractGenerator implements Generator {
    //--------------------------------------------------------------------------

    /** handle all configurations (key, value). */
    private final DDLGenConfiguration _configuration;

    /** handle the key gen registry. */
    private KeyGeneratorRegistry _keyGenRegistry;

    /** handle the MappingHelper. */
    private MappingHelper _mappingHelper;

    /** handle the typemapper. */
    private TypeMapper _typeMapper;

    /** handle schema factory. */
    private SchemaFactory _schemaFactory;

    /** handle the _mapping document. */
    private Mapping _mapping;

    /** schema. */
    private Schema _schema;

    /** handle all resolving tables. */
    private final Map<String, ClassMapping> _resolveTable = new HashMap<String, ClassMapping>();

    //--------------------------------------------------------------------------

    /**
     * Constructor for AbstractGenerator.
     * 
     * @param configuration Configuration to use by the generator.
     */
    protected AbstractGenerator(final DDLGenConfiguration configuration) {
        _configuration = configuration;
    }
    
    //--------------------------------------------------------------------------

    /**
     * Get configuration of generator.
     * 
     * @return Configuration of generator.
     */
    public final DDLGenConfiguration getConfiguration() {
        return _configuration;
    }

    /**
     * Set key generator registry.
     * 
     * @param keyGenRegistry Key generator registry.
     */
    public final void setKeyGenRegistry(final KeyGeneratorRegistry keyGenRegistry) {
        _keyGenRegistry = keyGenRegistry;
    }

    /**
     * Set mapping helper.
     * 
     * @param mappingHelper Mapping helper.
     */
    protected final void setMappingHelper(final MappingHelper mappingHelper) {
        _mappingHelper = mappingHelper;
        _mappingHelper.setTypeMapper(_typeMapper);
    }

    /**
     * Get mapping helper.
     * 
     * @return Mapping helper.
     */
    public final MappingHelper getMappingHelper() {
        return _mappingHelper;
    }

    /**
     * Set type mapper.
     * 
     * @param typeMapper Type mapper.
     */
    public final void setTypeMapper(final TypeMapper typeMapper) {
        _typeMapper = typeMapper;
        _mappingHelper.setTypeMapper(_typeMapper);
    }

    /**
     * Get type mapper.
     * 
     * @return Type mapper.
     */
    public final TypeMapper getTypeMapper() {
        return _typeMapper;
    }

    /**
     * Set schema factory.
     * 
     * @param schemaFactory Schema factory.
     */
    protected final void setSchemaFactory(final SchemaFactory schemaFactory) {
        _schemaFactory = schemaFactory;
    }

    /**
     * Get schema factory.
     * 
     * @return Schema factory.
     */
    public final SchemaFactory getSchemaFactory() {
        return _schemaFactory;
    }

    /**
     * Set mapping document.
     * 
     * @param mapping Mapping document.
     */
    public final void setMapping(final Mapping mapping) {
        _mapping = mapping;
        _mappingHelper.setMapping(_mapping);
    }

    /**
     * Get mapping document.
     * 
     * @return Mapping document.
     */
    public final Mapping getMapping() {
        return _mapping;
    }

    /**
     * Get schema.
     * 
     * @return Schema
     */
    public final Schema getSchema() {
        return _schema;
    }

    //--------------------------------------------------------------------------

    /**
     * {@inheritDoc}
     */
    public final void generateDDL(final OutputStream output) throws GeneratorException {
        DDLWriter writer = new DDLWriter(output, _configuration);
        
        // Create schema.
        createSchema();
        
        // Generate DDL.
        String groupBy = _configuration.getStringValue(
                DDLGenConfiguration.GROUP_DDL_KEY,
                DDLGenConfiguration.GROUP_DDL_BY_TABLE);
        if (DDLGenConfiguration.GROUP_DDL_BY_TABLE.equalsIgnoreCase(groupBy)) {
            generateDDLGroupByTable(writer);
        } else if (DDLGenConfiguration.GROUP_DDL_BY_DDLTYPE.equalsIgnoreCase(groupBy)) {
            generateDDLGroupByDDLType(writer);
        } else {
            throw new GeneratorException("group ddl by do not support: " + groupBy);
        }
        
        writer.close();
    }

    /**
     * Generating ddl grouped by ddl type of DDL (e.g DROP, CREATE TABLE, create 
     * Primary key, create foreign key).
     * 
     * @param writer DDLWriter to write schema objects to.
     * @throws GeneratorException If failed to generate DDL.
     */
    private void generateDDLGroupByDDLType(final DDLWriter writer) throws GeneratorException {
        boolean genSchema = _configuration.getBoolValue(
                DDLGenConfiguration.GENERATE_DDL_FOR_SCHEMA_KEY, true);
        boolean genDrop = _configuration.getBoolValue(
                DDLGenConfiguration.GENERATE_DDL_FOR_DROP_KEY, true);
        boolean genCreate = _configuration.getBoolValue(
                DDLGenConfiguration.GENERATE_DDL_FOR_CREATE_KEY, true);
        boolean genPrimaryKey = _configuration.getBoolValue(
                DDLGenConfiguration.GENERATE_DDL_FOR_PRIMARYKEY_KEY, true);
        boolean genForeignKey = _configuration.getBoolValue(
                DDLGenConfiguration.GENERATE_DDL_FOR_FOREIRNKEY_KEY, true);
        boolean genIndex = _configuration.getBoolValue(
                DDLGenConfiguration.GENERATE_DDL_FOR_INDEX_KEY, true);
        boolean genKeyGen = _configuration.getBoolValue(
                DDLGenConfiguration.GENERATE_DDL_FOR_KEYGENERATOR_KEY, true);

        generateHeader(writer);

        //generate ddl for schema
        if (genSchema) { _schema.toCreateDDL(writer); }

        //generate drop statemetn
        if (genDrop) { generateDrop(writer); }

        //generate create statement
        if (genCreate) { generateCreate(writer); }

        //generate primary key creation statement
        if (genPrimaryKey) { generatePrimaryKey(writer); }

        //generate foreign key creation statement
        if (genForeignKey) { generateForeignKey(writer); }

        //generate index creation statement
        if (genIndex) { generateIndex(writer); }

        if (genKeyGen) { generateKeyGenerator(writer); }
    }

    /**
     * Generate DDL for drop statement of table.
     * 
     * @param writer DDLWriter to write schema objects to.
     * @throws GeneratorException If failed to generate DDL.
     */
    public final void generateDrop(final DDLWriter writer) throws GeneratorException {
        for (int i = 0; i < _schema.getTableCount(); i++) {
            _schema.getTable(i).toDropDDL(writer);
        }
    }

    /**
     * Generate DDL for create statementof table.
     * <pre>
     * CREATE TABLE prod (
     *  id INTEGER NOT NULL,
     *  name CHAR(16)
     * );
     * 
     * CREATE TABLE prod_detail (
     *  id INTEGER NOT NULL,
     *  prod_id CHAR(16)
     * );
     * </pre>
     * 
     * @param writer DDLWriter to write schema objects to.
     * @throws GeneratorException If failed to generate DDL.
     */
    public final void generateCreate(final DDLWriter writer) throws GeneratorException {
        for (int i = 0; i < _schema.getTableCount(); i++) {
            _schema.getTable(i).toCreateDDL(writer);
        }
    }

    /**
     * Generate DDL for primany keys.
     * 
     * @param writer DDLWriter to write schema objects to.
     * @throws GeneratorException If failed to generate DDL.
     */
    public final void generatePrimaryKey(final DDLWriter writer) throws GeneratorException {
        for (int i = 0; i < _schema.getTableCount(); i++) {
            _schema.getTable(i).getPrimaryKey().toCreateDDL(writer);
        }
    }

    /**
     * Generate DDL for foreign keys.
     * <pre>
     * ALTER TABLE `prod_group` ADD CONSTRAINT `FK_prod_group_1` 
     * FOREIGN KEY `FK_prod_group_1` (`id`, `name`)
     * REFERENCES `category` (`id`, `name`)
     * ON DELETE SET NULL
     * ON UPDATE CASCADE;
     * </pre>
     * 
     * @param writer DDLWriter to write schema objects to.
     * @throws GeneratorException If failed to generate DDL.
     */
    public final void generateForeignKey(final DDLWriter writer) throws GeneratorException {
        for (int i = 0; i < _schema.getTableCount(); i++) {
            createForeignKeyDDL(_schema.getTable(i), writer);
        }
    }

    /**
     * Generate DDL for indices.
     * 
     * @param writer DDLWriter to write schema objects to.
     * @throws GeneratorException If failed to generate DDL.
     */
    public final void generateIndex(final DDLWriter writer) throws GeneratorException {
        for (int i = 0; i < _schema.getTableCount(); i++) {
            createIndex(_schema.getTable(i), writer);
        }
    }

    /**
     * Generate DDL for key generators (sequence/trigger).
     * 
     * @param writer DDLWriter to write schema objects to.
     * @throws GeneratorException If failed to generate DDL.
     */
    public final void generateKeyGenerator(final DDLWriter writer) throws GeneratorException {
        for (int i = 0; i < _schema.getTableCount(); i++) {
            Table table = _schema.getTable(i);
            if (table.getKeyGenerator() != null) {
                table.getKeyGenerator().setTable(table);
                table.getKeyGenerator().toCreateDDL(writer);
            }
        }
    }

    /**
     * Generating ddl group by table.
     * 
     * @param writer DDLWriter to write schema objects to.
     * @throws GeneratorException If failed to generate DDL.
     */
    private void generateDDLGroupByTable(final DDLWriter writer) throws GeneratorException {
        boolean genSchema = _configuration.getBoolValue(
                DDLGenConfiguration.GENERATE_DDL_FOR_SCHEMA_KEY, true);
        boolean genDrop = _configuration.getBoolValue(
                DDLGenConfiguration.GENERATE_DDL_FOR_DROP_KEY, true);
        boolean genCreate = _configuration.getBoolValue(
                DDLGenConfiguration.GENERATE_DDL_FOR_CREATE_KEY, true);
        boolean genPrimaryKey = _configuration.getBoolValue(
                DDLGenConfiguration.GENERATE_DDL_FOR_PRIMARYKEY_KEY, true);
        boolean genForeignKey = _configuration.getBoolValue(
                DDLGenConfiguration.GENERATE_DDL_FOR_FOREIRNKEY_KEY, true);
        boolean genIndex = _configuration.getBoolValue(
                DDLGenConfiguration.GENERATE_DDL_FOR_INDEX_KEY, true);
        boolean genKeyGen = _configuration.getBoolValue(
                DDLGenConfiguration.GENERATE_DDL_FOR_KEYGENERATOR_KEY, true);

        generateHeader(writer);
        
        if (genSchema) { _schema.toCreateDDL(writer); }
        
        for (int i = 0; i < _schema.getTableCount(); i++) {
            Table table = _schema.getTable(i);

            if (genDrop) { table.toDropDDL(writer); }
            if (genCreate) { table.toCreateDDL(writer); }
            if (genPrimaryKey) { table.getPrimaryKey().toCreateDDL(writer); }
            if (genForeignKey) { createForeignKeyDDL(table, writer); }
            if (genIndex) { createIndex(table, writer); }
            if (genKeyGen && (table.getKeyGenerator() != null)) {
                table.getKeyGenerator().setTable(table);
                table.getKeyGenerator().toCreateDDL(writer);
            }
        }
    }

    /**
     * Generate DDL for foreign key.
     * 
     * @param table Table to generate DDL of foreign key for.
     * @param writer DDLWriter to write schema objects to.
     * @throws GeneratorException If failed to generate DDL.
     */
    protected final void createForeignKeyDDL(final Table table, final DDLWriter writer)
    throws GeneratorException {
        for (int i = 0; i < table.getForeignKeyCount(); i++) {
            table.getForeignKey(i).toCreateDDL(writer);
        }
    }

    /**
     * Generate DDL for indices of given table.
     * 
     * @param table Table to generate DDL of indices for.
     * @param writer DDLWriter to write schema objects to.
     * @throws GeneratorException If failed to generate DDL.
     */
    public final void createIndex(final Table table, final DDLWriter writer)
    throws GeneratorException {
        for (int i = 0; i < table.getIndexCount(); i++) {
            table.getIndex(i).toCreateDDL(writer);
        }
    }

    //--------------------------------------------------------------------------

    /**
     * Generate header comment.
     * 
     * @param writer DDLWriter to write schema objects to.
     */
    public abstract void generateHeader(final DDLWriter writer);

    //--------------------------------------------------------------------------

    /**
     * Extracting informations from mapping to schema, this is done by 3 steps.
     * <ul>
     *   <li>Create key generators</li>
     *   <li>Create tables</li>
     *   <li>Create additional tables for many-many relations</li>
     * </ul>
     * 
     * @throws GeneratorException If failed to create schema objects.
     */
    public final void createSchema() throws GeneratorException {
        // Create schema.
        MappingRoot root = _mapping.getRoot();
        _schema = _schemaFactory.createSchema();
        _schema.setConfiguration(_configuration);

        // Create key generators.
        Enumeration<? extends KeyGeneratorDef> ekg = root.enumerateKeyGeneratorDef();
        while (ekg.hasMoreElements()) {
            KeyGeneratorDef definition = ekg.nextElement();
            _keyGenRegistry.createKeyGenerator(definition);
        }

        // Create tables.
        Enumeration<? extends ClassMapping> ec = root.enumerateClassMapping();
        while (ec.hasMoreElements()) {
            ClassMapping cm = ec.nextElement();
            Table table = createTable(cm);
            if (table != null) { _schema.addTable(table); }
        }
        
        // Create N:M relation tables.
        Iterator<String> i = _resolveTable.keySet().iterator();
        while (i.hasNext()) {
            ClassMapping cm = _resolveTable.get(i.next());
            Table table = createTable(cm);
            if (table != null) { _schema.addTable(table); }
        }
    }

    /**
     * Create table from a ClassMapping.
     * 
     * @param cm ClassMapping.
     * @return Table schema object.
     * @throws GeneratorException If failed to create schema objects.
     */
    private Table createTable(final ClassMapping cm) throws GeneratorException {
        String tableName = cm.getMapTo().getTable();
        if (tableName == null) { return null; }

        Table table = _schemaFactory.createTable();
        table.setName(tableName);
        table.setConfiguration(_configuration);
        table.setSchema(_schema);
        
        PrimaryKey primarykey = _schemaFactory.createPrimaryKey();
        primarykey.setConfiguration(_configuration);
        primarykey.setTable(table);
        primarykey.setName("pk_" + tableName);
        table.setPrimaryKey(primarykey);

        // Return if there are no field in the table.
        if (cm.getClassChoice() == null) { return table; }

        boolean isUseFieldIdentity = _mappingHelper.isUseFieldIdentity(cm);
        Enumeration<? extends FieldMapping> ef = cm.getClassChoice().enumerateFieldMapping();

        // Process key generator.
        String keygenerator = cm.getKeyGenerator();
        KeyGenerator keyGen = null;
        if (keygenerator != null) {
            keyGen = _keyGenRegistry.getKeyGenerator(keygenerator.toUpperCase());
        }
        table.setKeyGenerator(keyGen);

        while (ef.hasMoreElements()) {
            FieldMapping fm = ef.nextElement();

            // Skip if <sql> tag is not defined and we have no mapping to DB.
            if (fm.getSql() == null) { continue; }

            boolean isFieldIdentity = fm.getIdentity();
            if (!isUseFieldIdentity) {
                isFieldIdentity = _mappingHelper.isIdentity(cm, fm);
            }
            
            // Checke for many-key, many-table definition.
            if (fm.getSql().getManyTable() != null) {
                    // Generate resolving table for many-many relationship
                    addResolveField(fm, cm);
            }
                    
            // Process column creation if sql name is defined.
            String[] sqlnames = fm.getSql().getName();              
            if ((sqlnames != null) && (sqlnames.length > 0)
                    && (fm.getSql().getManyTable() == null)) {
                // Normal case, using sql name as column name.
                String sqltype = fm.getSql().getType();

                TypeInfo typeInfo = null;
                ClassMapping cmRef = null;
                String[] refIdTypes = null;
                boolean isUseReferenceType = false;

                // Get type info.
                if (sqltype != null) {
                    typeInfo = _typeMapper.getType(sqltype);
                }

                // If typeInfo is null, this table has a reference to another one.
                if (typeInfo == null) {
                    cmRef = _mappingHelper.getClassMappingByName(fm.getType());
                    // Use field type if reference class could not be found.
                    if (cmRef == null) {                        
                        typeInfo = _typeMapper.getType(fm.getType());
                        
                        if (typeInfo == null) {
                            throw new TypeNotFoundException("can not resolve type "
                                + fm.getType() + " in class '" + cm.getName() + "'");
                        }
                    } else {
                        isUseReferenceType = true;
                        refIdTypes = _mappingHelper.resolveTypeReferenceForIds(
                                fm.getType());

                        // If number of reference table's Id's differ from number of
                        // field elements.
                        if (refIdTypes.length != sqlnames.length) {
                            throw new TypeNotFoundException(
                                    "number of reference table's Id differs"
                                            + " to number of field elements '"
                                            + fm.getName() + "' of class '" 
                                            + cm.getName() + "'"
                                            + refIdTypes.length + "," + sqlnames.length);
                        }
                    }
                }

                // Create fields.
                for (int i = 0; i < sqlnames.length; i++) {
                    Field field = _schemaFactory.createField();
                    field.setConfiguration(_configuration);

                    if (isUseReferenceType) {
                        // Each sqlname correspond to a identity of the reference table.
                        // Should be able to get the original type of the reference 
                        // field.
                        typeInfo = _typeMapper.getType(refIdTypes[i]);
                        if (typeInfo == null) {
                            throw new TypeNotFoundException(
                                    "can not find reference type "
                                    + refIdTypes[i] + " of class " + cm.getName());
                        }
                    }

                    // process attributes of field
                    field.setName(sqlnames[i]);
                    field.setTable(table);
                    field.setType(typeInfo);
                    field.setIdentity(isFieldIdentity);
                    field.setRequired(fm.getRequired());
                    field.setKeyGenerator(keyGen);

                    table.addField(field);
                    
                    if (isFieldIdentity) {
                       primarykey.addField(field);
                    }
                }
                
                // Create foreign keys.
                if (isUseReferenceType) { addOneOneForeignKey(table, fm); }
            }            
        }
        
        // Process extends, if extends is defined.
        processExtendedClass(table, cm);
        
        return table;
    }

    /**
     * Extract identities from extended ClassMapping and add them to table.
     * 
     * @param table Table to add identities of extended mapping to.
     * @param cm ClassMapping of table to extract get mappings from.
     * @throws GeneratorException throw exception if key-gen is not found.
     */
    private void processExtendedClass(final Table table, final ClassMapping cm)
    throws GeneratorException {
        Object extendClass = cm.getExtends();
        if (extendClass == null) { return; }
        
        ClassMapping extendCm = (ClassMapping) extendClass;
        String[] childIds = _mappingHelper.getClassMappingSqlIdentity(cm, false);
        
        if (childIds.length != 0) {
            // Check consistency.
            String[] childTypes = _mappingHelper.resolveTypeReferenceForIds(cm);
            String[] parentTypes = _mappingHelper.resolveTypeReferenceForIds(extendCm);
            
            if (childTypes.length != parentTypes.length) {
                throw new GeneratorException("Cannot resolve type for class '" 
                        + cm.getName() + "' from extend class '" 
                        + extendCm.getName() + "'");
            }
            for (int i = 0; i < childTypes.length; i++) {
                if (!childTypes[i].equalsIgnoreCase(parentTypes[i])) {
                    throw new GeneratorException("Cannot resolve type for class '" 
                            + cm.getName() + "' from extend class '" 
                            + extendCm.getName() + "'");
                }
            }
            return;
        }
        
        boolean isUseFieldIdentity = _mappingHelper.isUseFieldIdentity(extendCm);
        Enumeration<? extends FieldMapping> extendEf =
            extendCm.getClassChoice().enumerateFieldMapping();

        // Process key generator.
        String keygenerator = extendCm.getKeyGenerator();
        KeyGenerator keyGen = null;
        if (keygenerator != null) {
            keyGen = _keyGenRegistry.getKeyGenerator(keygenerator.toUpperCase());
        }
        table.setKeyGenerator(keyGen);

        while (extendEf.hasMoreElements()) {
            FieldMapping extendFm = extendEf.nextElement();

            // Skip if <sql> tag is not defined.
            if (extendFm.getSql() == null) { continue; }

            boolean isFieldIdentity = extendFm.getIdentity();
            if (!isUseFieldIdentity) {
                isFieldIdentity = _mappingHelper.isIdentity(extendCm, extendFm);
            }
            
            // Checke for many-key, many-table definition.
            if (isFieldIdentity && extendFm.getSql().getManyKeyCount() <= 0) {
                // Column is defiend as normal column in child, but it is id which is
                // inherited from parent.
                if (mergeIfDefInBothClasses(table, cm, extendFm)) { continue; }
                
                String[] sqlnames = extendFm.getSql().getName();
                String sqltype = extendFm.getSql().getType();

                TypeInfo typeInfo = null;
                ClassMapping cmRef = null;
                String[] refIdTypes = null;
                boolean isUseReferenceType = false;
                
                if (sqltype != null) {
                    typeInfo = _typeMapper.getType(sqltype);
                }

                // If typeInfo is null, this table has a reference to another one.
                if (typeInfo == null) {
                    cmRef = _mappingHelper.getClassMappingByName(extendFm.getType());
                    // If cmRef is null, the reference class could not be found.
                    if (cmRef == null) {                        
                        typeInfo = _typeMapper.getType(extendFm.getType());
                        
                        if (typeInfo == null) {
                            throw new TypeNotFoundException("can not resolve type "
                                + extendFm.getType());
                        }
                    } else {
                        isUseReferenceType = true;
                        refIdTypes = _mappingHelper.resolveTypeReferenceForIds(extendFm
                                .getType());

                        // If number of reference table's Ids differ from number of
                        // field elements.
                        if (refIdTypes.length != sqlnames.length) {
                            throw new TypeNotFoundException(
                                    "number of reference table's Id differs"
                                            + " to number of field elements '"
                                            + extendFm.getName() + "' of class '" 
                                            + extendCm.getName() + "'"
                                            + refIdTypes.length + "," + sqlnames.length);
                        }
                    }
                }

                // Create fields.
                for (int i = 0; i < sqlnames.length; i++) {
                    Field field = _schemaFactory.createField();
                    field.setConfiguration(_configuration);

                    if (isUseReferenceType) {
                        // Each sqlname is correspond to an identity of the reference
                        // table so, it should be possible to get the original type of
                        // the reference field.
                        typeInfo = _typeMapper.getType(refIdTypes[i]);
                        if (typeInfo == null) {
                            throw new TypeNotFoundException(
                                    "can not find reference type "
                                            + refIdTypes[i] + " of class "
                                            + extendCm.getName());
                        }
                    }

                    field.setName(sqlnames[i]);
                    field.setTable(table);
                    field.setType(typeInfo);
                    field.setIdentity(isFieldIdentity);                    
                    field.setKeyGenerator(keyGen);
                    
                    if (isFieldIdentity) {
                        table.getPrimaryKey().addField(field);
                    }

                    table.addField(field);
                }
            }
        }
        
        // Process extends.
        if (extendCm.getExtends() != null) {
            processExtendedClass(table, extendCm); 
        }
    }

    /**
     * This function is used to merge a table if it is mapped to many classes.
     * 
     * @param table Table to merge.
     * @param cm ClassMapping of table.
     * @param extendFm FieldMapping of extended class to be merged into table.
     * @return <code>true</code> if column is defiend as normal column in child, but is
     *         identity which is inherited from parent. 
     */
    private boolean mergeIfDefInBothClasses(final Table table, 
            final ClassMapping cm, final FieldMapping extendFm) {
        Enumeration<? extends FieldMapping> ef = cm.getClassChoice().enumerateFieldMapping();
        
        while (ef.hasMoreElements()) {
            FieldMapping fm = ef.nextElement();
            String fname = fm.getName();
            // If extend field has the same name with one of parent's fields.
            if (fname != null && fname.equalsIgnoreCase(extendFm.getName())) {
                if (fm.getSql() == null) { continue; }
                String[] sqlnames = fm.getSql().getName();
                for (int i = 0; i < sqlnames.length; i++) {
                    table.getField(sqlnames[i]).setIdentity(true);
                }
                return true;
            }
        }
        
        return false;
    }

    /**
     * Add foreign key for 1:1 relations to table schema object.
     * 
     * @param table Table to add foreign key to.
     * @param fm FieldMapping of relation.
     * @throws GeneratorException If failed to create foreign key schema object.
     */
    private void addOneOneForeignKey(final Table table, final FieldMapping fm)
    throws GeneratorException {
        ForeignKey fk = _schemaFactory.createForeignKey();
        fk.setConfiguration(_configuration);

        fk.setTable(table);
        fk.setName(table.getName() + "_" + fm.getName());
        
        String[] fieldNames = fm.getSql().getName();
        for (int i = 0; i < fieldNames.length; i++) {
            for (int j = 0; j < table.getFieldCount(); j++) {
                Field field = table.getField(j);
                if (fieldNames[i].equals(field.getName())) { fk.addField(field); }
            }
        }

        ClassMapping cm = _mappingHelper.getClassMappingByName(fm.getType());

        if (cm == null) {
            throw new GeneratorException("can not find class " + fm.getType());
        }
        
        String referenceTableName = cm.getMapTo().getTable();
        Table referenceTable = null;
        referenceTable = table.getSchema().getTable(referenceTableName);
        fk.setReferenceTable(referenceTable);
        
        String[] manykeys = fm.getSql().getManyKey();
        if (manykeys == null || manykeys.length == 0) {
            manykeys = _mappingHelper.getClassMappingSqlIdentity(cm, true);
        }
        for (int i = 0; i < manykeys.length; i++) {
            for (int j = 0; j < referenceTable.getFieldCount(); j++) {
                Field field = referenceTable.getField(j);
                if (manykeys[i].equals(field.getName())) { fk.addReferenceField(field); }
            }
        }

        fk.setRelationType(ForeignKey.ONE_ONE);
        table.addForeignKey(fk);
    }

    /**
     * Add column for a resolving table which is required by M:N relationship.
     * 
     * @param fm FieldMapping.
     * @param cm ClassMapping.
     */
    private void addResolveField(final FieldMapping fm, final ClassMapping cm) {
        String keyGen = cm.getKeyGenerator();
        ClassMapping resolveCm = null;
        
        // Get table, if not existe, create one.
        if (_resolveTable.containsKey(fm.getSql().getManyTable())) {
            resolveCm = _resolveTable.get(fm.getSql().getManyTable());
        } else {
            resolveCm = new ClassMapping();
            resolveCm.setName(fm.getSql().getManyTable());
            resolveCm.setKeyGenerator(keyGen);

            MapTo mapto = new MapTo();
            mapto.setTable(fm.getSql().getManyTable());
            resolveCm.setMapTo(mapto);
            _resolveTable.put(fm.getSql().getManyTable(), resolveCm);
        }

        FieldMapping resolveFm = new FieldMapping();
        resolveFm.setIdentity(true);
        resolveFm.setName(cm.getMapTo().getTable());
        resolveFm.setType(cm.getName());

        ClassChoice cc = resolveCm.getClassChoice();
        if (cc == null) {
            cc = new ClassChoice();
            resolveCm.setClassChoice(cc); 
        }
        cc.addFieldMapping(resolveFm);

        Sql sql = new Sql();
        String[] sqlname = fm.getSql().getManyKey();
        if (sqlname == null || sqlname.length == 0) {
            _mappingHelper.getClassMappingSqlIdentity(cm, true);
        }
        sql.setName(sqlname);
        resolveFm.setSql(sql);
    }

    //--------------------------------------------------------------------------
}
