FileDocCategorySizeDatePackage
DatabaseGenerator.javaAPI DocGlassfish v2 API41255Fri May 04 22:34:40 BST 2007com.sun.jdo.spi.persistence.generator.database

DatabaseGenerator.java

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

/*
 * DatabaseGenerator.java
 *
 * Created on Jan 14, 2003
 */

package com.sun.jdo.spi.persistence.generator.database;

import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.ResourceBundle;

import org.netbeans.modules.dbschema.*;
import org.netbeans.modules.dbschema.util.NameUtil;
import com.sun.jdo.api.persistence.model.mapping.*;
import com.sun.jdo.api.persistence.model.mapping.impl.*;
import com.sun.jdo.api.persistence.model.jdo.*;
import com.sun.jdo.api.persistence.model.jdo.impl.*;
import com.sun.jdo.api.persistence.model.Model;
import com.sun.jdo.api.persistence.model.ModelException;
import com.sun.jdo.spi.persistence.utility.JavaTypeHelper;

import com.sun.jdo.spi.persistence.utility.I18NHelper;
import com.sun.jdo.spi.persistence.utility.logging.Logger;


/**
 * This class generates a database schema and a Map of mapping classes from a
 * set of JDO classes.
 */
public class DatabaseGenerator {
    /** @see DatabaseGenerationConstants#DOT */
    private static final char DOT = DatabaseGenerationConstants.DOT;

    /** Holds Java type information */
    private final Model model;

    /** Provides information on how database info should be generated */
    private final MappingPolicy mappingPolicy;

    /** List of NameTuple objects which holds persistence class name,
     * desired table name and hash class name for database generation. */
    private final List pcClasses;

    /** Map from persistence-capable class names to MappingClassElement's. */
    // See also DatabaseGenerator.Results.mappingClasses.
    private final Map mappingClasses = new HashMap();
    
    /** Generated database schema. */
    private final SchemaElement schema;

    /**
     * Used to recognize and remove classname suffixes.  See {@link
     * #getShortClassName}.
     */
    private final String classSuffix;

    /** The logger */
    private static final Logger logger =
        LogHelperDatabaseGenerator.getLogger();
    
    /** I18N message handler */
    private static final ResourceBundle messages =
        I18NHelper.loadBundle(DatabaseGenerator.class);


    /**
     * Contains the results of invoking DatabaseGenerator.generate()
     */
    public class Results {
        /** Generated database schema. */
        private final SchemaElement schema;

        /** Set of generated MappingClassElement's. */
        // Notice that this is a Set, while the outer class has a Map.  This
        // is intentional: DatabaseGenerator.addRelationships needs to get a
        // MappingClassElement for a pc class name, but the clients of the
        // DatabaseGenerator should only need the mapping classes.
        private final Set mappingClasses;

        Results(SchemaElement schema, Map mappingClasses) {
            this.schema = schema;
            this.mappingClasses = new HashSet(mappingClasses.values());
        }

        /** @return Generated SchemaElement. */
        public SchemaElement getSchema() {
            return schema;
        }

        /** @return Generated mapping classes. */
        public Set getMappingClasses() {
            return mappingClasses;
        }
    }
    

    /**
     * This class holds three strings which contain three type of information.
     * For database generation, for each persistence class, we need to have 
     * persistence class name to look up persistence class, desired table name
     * for table name and hash class name for unique table name. Depending on 
     * the caller's option, hash class name can be same as persistence class 
     * name or it can be different from persistence class name.
     */
    public static class NameTuple {

        /** persistence class name */
        private final String persistenceClassName;

        /** desired table name */
        private final String desiredTableName;

        /** hash class name */
        private final String hashClassName;

        /** 
         * An object holds three string objects.
         * @param persistenceClassName persistence class name for 
         * persistence class look up 
         * @param desiredTableName it can be used for table name
         * @param hashClassName it can be used for unique table name
         */
        public NameTuple(
            String persistenceClassName, String desiredTableName, 
                    String hashClassName) {

            this.persistenceClassName = persistenceClassName;
            this.desiredTableName = desiredTableName;
            this.hashClassName = (hashClassName != null) ? 
                hashClassName : persistenceClassName;
        }

        /** 
         * An object holds three string objects.
         * @param persistenceClassName persistence class name
         * @param desiredTableName name for creating table name
         */
        public NameTuple(String persistenceClassName, String desiredTableName) {
            this(persistenceClassName, desiredTableName, null);
        }

        /** @return persistence class name. */
        public String getPersistenceClassName() {
            return persistenceClassName;
        }

        /** @return hash class name. */
        public String getHashClassName() {
            return hashClassName;
        }

        /** @return desired table name. */
        public String getDesiredTableName() {
            return desiredTableName;
        }
    }

    /**
     * Generate database schema and mapping model from given map of
     * persistence class name.
     * @param model Holds java type information.
     * @param pcClasses A list of NameTuple objects containing persistence 
     * class name and table names.
     * @param mappingPolicy Determines how dbvendor and user influence
     * generated schema.
     * @param schemaName Identifies the generated schema.
     * @param classSuffix Class name suffix that should be removed when
     * creating table names from class names.
     */
    private DatabaseGenerator(
            Model model, List pcClasses, MappingPolicy mappingPolicy,
            String schemaName, String classSuffix)
            throws DBException {

        this.model = model;
        this.pcClasses = pcClasses;
        this.mappingPolicy = mappingPolicy;
        this.schema = DBElementFactory.createSchema(schemaName);
        this.classSuffix = classSuffix;
    }

    /**
     * Generate database schema and mapping model from given map of 
     * persistence class names.  The schema is not put into the SchemaElement
     * cache as a result of this generation.  The next call to 
     * SchemaElement.forName will result in the schema being placed in the 
     * cache if it is not already there.  If it is already there, it is the 
     * caller's responsibility to remove the old version if desired using 
     * SchemaElement's removeFromCache method before a SchemaElement.forName 
     * call.  The generated schema is saved in <i>outputDir.schemaName</i>.
     * @param model Holds java type information.
     * @param pcClasses A List of NameTuple objects containing persistence
     * class name and table names.
     * @param mappingPolicy Determines how dbvendor and user influence
     * generated schema.
     * @param schemaName Identifies the generated schema.
     * @param classSuffix Class name suffix that should be removed when
     * creating table names from class names.
     * @param generateMappingClasses if it is true, generate mapping classes
     * also.  <em>Currently MappingClassElement's are <b>always</b>
     * generated, and the parameter's value is ignored.</em>
     * @return A DatabaseGenerator.Results instance which holds the results
     * of generation.
     */
    public static Results generate(
            Model model, List pcClasses, MappingPolicy mappingPolicy,
            String schemaName, String classSuffix,
            boolean generateMappingClasses) 
            throws DBException, IOException, ModelException {

        DatabaseGenerator generator = new DatabaseGenerator(
                model, pcClasses, mappingPolicy,
                schemaName, classSuffix);

        Results rc = generator.generate();

        mappingPolicy.resetCounter();

        return rc;
    }

    /**
     * Generate database schema and mapping classes.  Iterate over all
     * persistence-capable classes, generating a table for each.  Within each
     * persistence-capable class, iterate over all fields and make columns
     * for each.  Then handle relationships separately, see {@link
     * #addRelationships}.
     * @return A DatabaseGenerator.Results instance which holds the results
     * of generation.
     */
    private Results generate() throws DBException, ModelException {
        for (Iterator i = pcClasses.iterator(); i.hasNext();) {
            NameTuple nameTuple = (NameTuple) i.next();
            String pcClassName = nameTuple.getPersistenceClassName();
            String desiredTableName = nameTuple.getDesiredTableName();

            PersistenceClassElement pcClass =
                    model.getPersistenceClass(pcClassName);

            String tableName = mappingPolicy.getTableName(
                    desiredTableName, getShortClassName(nameTuple.getHashClassName()));
            TableElement table = DBElementFactory.createAndAttachTable(
                    schema, tableName);
            UniqueKeyElement pKey = DBElementFactory.createAndAttachPrimaryKey(
                    table,
                    mappingPolicy.getPrimaryKeyConstraintName(
                            table.getName().getName()));
            MappingClassElement mappingClass = createMappingClass(
                    pcClass, table);

            PersistenceFieldElement[] fields = pcClass.getFields();
            if (fields != null) {
                for (int j = 0; j < fields.length; j++) {
                    PersistenceFieldElement field = fields[j];
                    String fieldName = field.getName();
                    if (!(field instanceof RelationshipElement)) {
                        String columnName = mappingPolicy.getColumnName(
                                desiredTableName, fieldName, tableName);
                        String fieldType = model.getFieldType(
                                pcClassName, fieldName);
                        String fullFieldName =
                                new StringBuffer(desiredTableName)
                                .append(DOT).append(fieldName).toString();
                        JDBCInfo columnType =
                            DBElementFactory.getColumnType(
                                    fullFieldName,
                                    fieldType,
                                    mappingPolicy);
                        if (logger.isLoggable(Logger.FINEST)) {
                            logger.fine(
                                    "DBGenerator.generate: " // NOI18N
                                    + tableName + "." + columnName + ": " // NOI18N
                                    + columnType.toString());
                        }
                        ColumnElement column =
                                DBElementFactory.createAndAttachColumn(
                                        columnName, table, columnType);
                        MappingFieldElement mappingField = 
                                createAndAttachMappingField(
                                        fieldName, mappingClass, column);

                        if (field.isKey()) {
                            column.setNullable(false);
                            pKey.addColumn(column);
                            pKey.getAssociatedIndex().addColumn(column);
                            mappingClass.getTable(tableName).addKeyColumn(
                                    column);
                        }
                    }
                }
            }
            mappingClasses.put(pcClassName, mappingClass);
        }
        addRelationships();

        return new Results(schema, mappingClasses);
    }

    /**
     * Get the primary table element for the mapping class.
     * @param mappingClass That which is associated with the table.
     * @return Table that is associated with mapping class.
     * @throws DBException
     */
    private TableElement getPrimaryTable(MappingClassElement mappingClass)
            throws DBException {

        List tables = mappingClass.getTables();

        MappingTableElement tbl = (MappingTableElement) tables.get(0);
        if (tbl != null) {
            DBIdentifier tblName = DBIdentifier.create(tbl.getTable());
            return schema.getTable(tblName);
        } else {
            return null;
        }
    }

    /**
     * Create mapping class and associated table and PC class
     * @param pcClass PC class that mapping class associated
     * @param table table element that mapping class associated
     * @return MappingClassElement associated with table and PC class
     * @throws ModelException
     */
    private MappingClassElement createMappingClass(
            PersistenceClassElement pcClass, TableElement table)
            throws ModelException {

         MappingClassElement mappingClass =
             new MappingClassElementImpl(pcClass);

         mappingClass.setDatabaseRoot(schema);
         mappingClass.addTable(table);
         return mappingClass;
    }

    /**
     * Create mapping field and add to mapping class
     * @param fieldName a String for field name
     * @param mappingClass mapping class object that field belong to
     * @return mapping field object
     * @throws ModelException
     */
    private MappingFieldElement createAndAttachMappingField(
            String fieldName, MappingClassElement mappingClass,
            ColumnElement column) throws ModelException {

        MappingFieldElement mappingField =
                new MappingFieldElementImpl(fieldName, mappingClass);

        mappingClass.addField(mappingField);
        mappingField.addColumn(column);
        if (column.isBlobType()) {
            mappingField.setFetchGroup(MappingFieldElement.GROUP_NONE);
        } else {
            mappingField.setFetchGroup(MappingFieldElement.GROUP_DEFAULT);
        }
        return mappingField;
    }


    /**
     * Create and add mapping relationship with column pairs to mapping class.
     * The column pair for mappingRelationship is same order as the column
     * pair from foreign key. It is used for 1-1 or 1-M relationship
     * It is column pair between local table and foreign table
     * @param relationName relationship name for the declaring mapping class
     * @param mappingClass mapping class that holds the relationship
     * @param foreign key which hold column pair for the relationship
     * @throws ModelException
     */
    private void addMappingRelationship(String relationName,
            MappingClassElement declaringClass, ForeignKeyElement fkey)
            throws ModelException {

        MappingRelationshipElement impl = new MappingRelationshipElementImpl(
                relationName, declaringClass);
        ColumnPairElement [] pairs = fkey.getColumnPairs();

        for (int i = 0; i < pairs.length; i++) {
            ColumnPairElement pair = pairs[i];
            impl.addColumn(pair);
        }
        declaringClass.addField(impl);
    }

    /**
     * Create and add MappingRelationship with associated column pairs
     * for join table. The column pair for mappingRelationship is same
     * order as the column pair from foreign key.
     * It is for column pairs between the join table and the foreign table
     * @param relationName a String for relation name
     * @param mappingClass mapping class that holds the relationship
     * @param fkeyForeign holding column pair information for the relationship
     * @throws ModelException
     */
    private void addAssocMappingRelationship(String relationName,
            MappingClassElement declaringClass, ForeignKeyElement fkeyForeign)
            throws ModelException {

        MappingRelationshipElement impl =
                (MappingRelationshipElement) declaringClass.getField(
                        relationName);

        if (null == impl) {
            impl = new MappingRelationshipElementImpl(
                    relationName, declaringClass);
            declaringClass.addField(impl);
        }

        // Add column pair for join table and foreign table
        ColumnPairElement [] pairs = fkeyForeign.getColumnPairs();
        for (int i = 0; i < pairs.length; i++) {
            ColumnPairElement pair = pairs[i];
            impl.addAssociatedColumn(pair);
        }
    }

    /**
     * Create and add MappingRelationship with local inverse column pair 
     * (for join table) or inverse column pair 
     * (for non join table) for referenced table.
     * It is for column pairs between local table and join table
     * or for column pairs between local table and foreign table (contains 
     * foreign key)
     * The column pair for mappingRelationship is inverse order
     * as the column pair from foreign key. Foreign key is not in the passing
     * mapping class but in the inverse mapping class
     * @param relationName a String for relation name
     * @param mappingClass mapping class that holds the relationship
     * @param fkeyForeign holding column pair information for the relationship
     * @throws ModelException
     * @throws DBException
     */
    private void addInverseMappingRelationship(String relationName,
            MappingClassElement declaringClass, ForeignKeyElement fkey,
            boolean isJoin)
            throws ModelException, DBException {

        MappingRelationshipElement impl =
                (MappingRelationshipElement) declaringClass.getField(
                        relationName);

        // for join table, need to add two MappingRelationshipElement
        if (null == impl) {
            impl = new MappingRelationshipElementImpl(relationName,
                declaringClass);
            declaringClass.addField(impl);
        }

        TableElement declaringTbl = getPrimaryTable(declaringClass);
        ColumnPairElement [] pairs = fkey.getColumnPairs();

        // Column pair get inverted since adding to referenced table
        for (int i = 0; i < pairs.length; i++) {
            ColumnPairElement pair = pairs[i];
            ColumnPairElement inversePair = DBElementFactory.createColumnPair(
                    pair.getReferencedColumn(), pair.getLocalColumn(), 
                    declaringTbl);

            if (isJoin) {
                impl.addLocalColumn(inversePair);
            } else {
                impl.addColumn(inversePair);
            }
        }
    }

    /**
     * Create and add a relationship.
     * @param srcTable Source table of the relationship.
     * @param relTable Related table.
     * @param relName Name of the relationship.
     * @param inverseRelName Name of the inverse relationship.
     * @param mappingClass Mapping information for the source table.
     * @param relMappingClass Mapping information for the related table.
     * @param uniqueId Id that can be appened to relName to distinguish it
     * from other relNames in the database.
     * @param srcIsJoin True if srcTable is a join table
     * @return ForeignKeyElement representing the relationship.
     */
    private ForeignKeyElement createRelationship(TableElement srcTable,
            TableElement relTable, String relName, String inverseRelName,
            MappingClassElement mappingClass,
            MappingClassElement relMappingClass,
            String uniqueId, boolean srcIsJoin) 
            throws DBException, ModelException {

        ForeignKeyElement fKey = DBElementFactory.createAndAttachForeignKey(
                 srcTable, relTable, relName, mappingPolicy, uniqueId);

        if (srcIsJoin) {
            addInverseMappingRelationship(relName, mappingClass,
                    fKey, true);
            addAssocMappingRelationship(inverseRelName, relMappingClass, fKey);
        } else {
            addMappingRelationship(relName, mappingClass, fKey);
            addInverseMappingRelationship(inverseRelName, relMappingClass,
                    fKey, false);
        }
        return fKey;
    }

    /**
     * Generate relationships for schema and mapping model from 
     * mappingClasses which already have all mapping fields populated. 
     * @throws DBException
     * @throws ModelExpception
     */
    private void addRelationships() 
        throws DBException, ModelException {
        if (logger.isLoggable(Logger.FINE)) {
            logger.fine("add relationship"); // NOI18N
        }

        Map relationFKey = new HashMap();

        // This is a list of 1-1 relationships that are deferred for
        // processing until all other relationships are processed.  Deferral
        // allows us to concentrate foreign keys on one side of the
        // relationship.
        List deferredRelationships = new ArrayList();

        for (Iterator i = mappingClasses.values().iterator(); i.hasNext();) {
            MappingClassElement mappingClass = (MappingClassElement) i.next();
            String pcClassName = mappingClass.getName();
            PersistenceClassElement pcClass =
                model.getPersistenceClass(pcClassName);
            validateModel(pcClass, "pcClass", pcClassName); // NOI18N
            TableElement sourceTable = getPrimaryTable(mappingClass);
            validateModel(sourceTable, "sourceTable", pcClassName); // NOI18N

            // Create a string that can keep names unique
            String uniqueId = getShortClassName(pcClassName);
            int want = 8; // Ideally, take this many chars from end
            int end = uniqueId.length();
            int start = want > end ? 0 : end - want;
            uniqueId = uniqueId.substring(start, end);

            RelationshipElement [] rels = pcClass.getRelationships();
            if (rels != null) {
                for (int j = 0; j < rels.length; j++) {

                    // relationship
                    RelationshipElement relation = rels[j];
                    String relationName = relation.getName();
                    int upperBound = relation.getUpperBound();

                    // inverseRelationship
                    String inverseRelName =
                            relation.getInverseRelationshipName();
                    validateModel(inverseRelName,
                                  "inverseRelName", relationName); // NOI18N
                    String relClassName = model.getRelatedClass(relation);
                    validateModel(relClassName,
                                  "relClassName", relationName); // NOI18N

                    // get related MappingClass and PersistenceClass
                    MappingClassElement relMappingClass =
                        (MappingClassElement) mappingClasses.get(relClassName);
                    validateModel(relMappingClass,
                                  "relMappingClass", relClassName); // NOI18N
                    PersistenceClassElement relClass =
                            model.getPersistenceClass(relClassName);
                    validateModel(relClass,
                                  "relClass", relClassName); // NOI18N
                    RelationshipElement inverseRelation =
                            relClass.getRelationship(inverseRelName);
                    validateModel(inverseRelation,
                                  "inverseRelation", inverseRelName); // NOI18N
                    TableElement relTable = getPrimaryTable(relMappingClass);
                    validateModel(relTable,
                                  "relTable", relClassName); // NOI18N
                    int relUpperBound = inverseRelation.getUpperBound();

                    if (logger.isLoggable(Logger.FINE)) {
                        logger.fine(
                                "Before adding relationship:" // NOI18N
                                + getTblInfo("sourceTable", sourceTable, relationName) // NOI18N
                                + getTblInfo("relTable", relTable, inverseRelName)); // NOI18N
                    }

                    // XXX Suggest making each block below a separate method.
                    
                    if ((upperBound > 1) && (relUpperBound > 1)) {
                        // M-N relationship, create new table
                        if (logger.isLoggable(Logger.FINE)) {
                            logger.fine("M-N relationship"); // NOI18N
                        }

                        ForeignKeyElement fKey = getMappedForeignKey(
                                relation, inverseRelation, relationFKey);
                        if (fKey == null) {
                            TableElement joinTable =
                                DBElementFactory.createAndAttachTable(
                                        schema,
                                        mappingPolicy.getJoinTableName(
                                                sourceTable.getName().getName(),
                                                relTable.getName().getName()));
                            fKey = createRelationship(
                                    joinTable, sourceTable, relationName,
                                    inverseRelName, mappingClass,
                                    relMappingClass, uniqueId, true);
                            relationFKey.put(relation, fKey);
                            ForeignKeyElement fKey2 = createRelationship(
                                    joinTable, relTable, inverseRelName,
                                    relationName, relMappingClass,
                                    mappingClass, uniqueId, true);
                            relationFKey.put(inverseRelation, fKey2);
                        }

                    } else if ((upperBound > 1) && (relUpperBound == 1)) {
                        // M-1 relationship, add foreign key at upper bound
                        // equal 1 side.  So here, we do nothing: We add
                        // relationships at the 1 side for 1-M relationships,
                        // and the current mapping class is the many side.

                        if (logger.isLoggable(Logger.FINE)) {
                            logger.fine("M-1 relationship: skip"); // NOI18N
                        }

                    } else if ((upperBound == 1) && (relUpperBound >1)) {
                        // 1-M relationship, add foreign key at upperBound =
                        // 1 side

                        if (logger.isLoggable(Logger.FINE)) {
                            logger.fine("1-M relationship"); // NOI18N
                        }

                        ForeignKeyElement fKey = getMappedForeignKey(relation, 
                            inverseRelation, relationFKey);
                        if (fKey == null) {
                            fKey = createRelationship(sourceTable, relTable, 
                                relationName, inverseRelName, mappingClass, 
                                relMappingClass, uniqueId, false);
                            relationFKey.put(relation, fKey);
                        }

                    } else if ((upperBound == 1) && (relUpperBound == 1)) {
                        // 1-1 relationship, add foreign key at either side.
                        // Check existence of foreign key at the other side
                        // before adding one to here.  If there is cascade
                        // delete in this side, add FK.  Otherwise, defer
                        // adding it until all other relationships are added.

                        ForeignKeyElement fKey = getMappedForeignKey(relation, 
                                inverseRelation, relationFKey);
                        if (fKey == null) {
                            if (relation.getDeleteAction() == 
                                RelationshipElement.CASCADE_ACTION) {
                                if (logger.isLoggable(Logger.FINE)) {
                                    logger.fine("1-1 relationship: cascade(this)"); // NOI18N
                                }
                                fKey = createRelationship(
                                        sourceTable, relTable, relationName,
                                        inverseRelName, mappingClass, 
                                        relMappingClass, uniqueId, false);
                                relationFKey.put(relation, fKey);
                            } else if (inverseRelation.getDeleteAction() == 
                                       RelationshipElement.CASCADE_ACTION) {
                                if (logger.isLoggable(Logger.FINE)) {
                                    logger.fine("1-1 relationship: cascade(inverse)"); // NOI18N
                                }
                                fKey = createRelationship(
                                        relTable, sourceTable, 
                                        inverseRelName, relationName, 
                                        relMappingClass, mappingClass,
                                        uniqueId, false);
                                relationFKey.put(inverseRelation, fKey);
                            } else {
                                if (logger.isLoggable(Logger.FINE)) {
                                    logger.fine("1-1 relationship: defer"); // NOI18N
                                }
                                deferredRelationships.add(
                                        new DeferredRelationship(
                                                relation, inverseRelation,
                                                sourceTable, relTable,
                                                relationName, inverseRelName,
                                                mappingClass, relMappingClass,
                                                uniqueId));
                            }
                        }
                    }
                    if (logger.isLoggable(Logger.FINE)) {
                        logger.fine(
                                "After adding relationship:" // NOI18N
                                + getTblInfo("sourceTable", sourceTable, relationName) // NOI18N
                                + getTblInfo("relTable", relTable, inverseRelName)); // NOI18N
                    }
                }
            }
        }
        
        if (deferredRelationships.size() > 0) {
            addDeferredRelationships(deferredRelationships, relationFKey);
        }
    }

    /**
     * Generate foreign keys for relationships that were deferred; see {@link
     * addRelationships}.
     * @param deferredRelationships List of 1-1 relationships which are not
     * yet mapped by foreign keys
     * @param relationFKey Map from RelationshipElement to ForeignKeyElement,
     * indicating which relationships have already been mapped.
     */
    private void addDeferredRelationships(
            List deferredRelationships, Map relationFKey) 
            throws DBException, ModelException {

        for (Iterator i = deferredRelationships.iterator(); i.hasNext();) {
            DeferredRelationship dr = (DeferredRelationship)i.next();

            RelationshipElement relation = dr.getRelation();
            RelationshipElement inverseRelation = dr.getInverseRelation();

            ForeignKeyElement fKey =
                getMappedForeignKey(relation, inverseRelation, relationFKey);

            // Only map if not already mapped
            if (fKey == null) {

                TableElement sourceTable = dr.getSourceTable();
                TableElement relTable = dr.getRelTable();

                String relationName = dr.getRelationName();
                String inverseRelName = dr.getInverseRelName();

                MappingClassElement mappingClass = dr.getMappingClass();
                MappingClassElement relMappingClass = dr.getRelMappingClass();

                String uniqueId = dr.getUniqueId();
                
                // If this side already has any foreign keys, map it on this
                // side, else on the other side.
                ForeignKeyElement keys[] = sourceTable.getForeignKeys();
                if (null != keys && keys.length > 0) {
                    fKey = createRelationship(
                            sourceTable, relTable, 
                            relationName, inverseRelName,
                            mappingClass, relMappingClass,
                            uniqueId, false);
                    if (logger.isLoggable(Logger.FINE)) {
                        logger.fine(
                                "1-1 deferred relationship (this)" // NOI18N
                                + getTblInfo("sourceTable", sourceTable, relationName) // NOI18N
                                + getTblInfo("relTable", relTable, inverseRelName)); // NOI18N
                    }
                } else {
                    fKey = createRelationship(
                            relTable, sourceTable,
                            inverseRelName, relationName,
                            relMappingClass, mappingClass,
                            uniqueId, false);
                    if (logger.isLoggable(Logger.FINE)) {
                        logger.fine(
                                "1-1 deferred relationship (inverse)" // NOI18N
                                + getTblInfo("sourceTable", sourceTable, relationName) // NOI18N
                                + getTblInfo("relTable", relTable, inverseRelName)); // NOI18N
                    }
                }
                relationFKey.put(relation, fKey);
            }
        }
    }


    /**
     * A DeferredRelationship instance represents all the information required
     * to create a foreign key from the relationship.
     */
    static class DeferredRelationship {
        private final RelationshipElement relation;
        private final RelationshipElement inverseRelation;
        private final TableElement sourceTable;
        private final TableElement relTable;
        private final String relationName;
        private final String inverseRelName;
        private final MappingClassElement mappingClass;
        private final MappingClassElement relMappingClass;
        private final String uniqueId;
        
        
        DeferredRelationship(RelationshipElement relation,
                             RelationshipElement inverseRelation,
                             TableElement sourceTable,
                             TableElement relTable,
                             String relationName,
                             String inverseRelName,
                             MappingClassElement mappingClass,
                             MappingClassElement relMappingClass,
                             String uniqueId) {

            this.relation = relation;
            this.inverseRelation = inverseRelation;
            this.sourceTable = sourceTable;
            this.relTable = relTable;
            this.relationName = relationName;
            this.inverseRelName = inverseRelName;
            this.mappingClass = mappingClass;
            this.relMappingClass = relMappingClass;
            this.uniqueId = uniqueId;
        }

        RelationshipElement getRelation() { return relation; }
        RelationshipElement getInverseRelation() { return inverseRelation; }

        TableElement getSourceTable() { return sourceTable; }
        TableElement getRelTable() { return relTable; }

        String getRelationName() { return relationName; }
        String getInverseRelName() { return inverseRelName; }

        MappingClassElement getMappingClass() { return mappingClass; }
        MappingClassElement getRelMappingClass() { return relMappingClass; }

        String getUniqueId() { return uniqueId; }
    }


    
    /**
     * Check if the relationship has been visited
     * @param relation current visiting relationship
     * @param inverseRelation inverse relationship 
     * @param relationFKey a map to hold relation and foreign key
     * @return the foreign key element or null 
     */
    private ForeignKeyElement getMappedForeignKey(
            RelationshipElement relation, RelationshipElement inverseRelation,
            Map relationFKey) {

        ForeignKeyElement fkey =
                (ForeignKeyElement) relationFKey.get(relation);

        if (fkey == null) {
            return (ForeignKeyElement) relationFKey.get(inverseRelation);
        } else {
            return fkey;
        }
    }

    /**
     * Computes the class name (without package) for the supplied
     * class name and trims off the class suffix.
     * @param className the fully qualified name of the class
     * @return the class name (without package and class suffix) 
     * for the supplied class name
     */
    private String getShortClassName(String className) {
        String shortName = JavaTypeHelper.getShortClassName(className);

        if ((classSuffix != null) && (!shortName.equals(classSuffix))) {
            int index = shortName.lastIndexOf(classSuffix);
            if (index != -1) {
                shortName = shortName.substring(0, index);
            }
        }
        return shortName;
    }

    /**
     * Assert that the given object reference is <em>not</em> null.
     * @param o Object reference that is checked for null.
     * @param failedItem String that names the item that is being checked.
     * @param accessor String which names an object that was use to try and
      * get object <code>o</code>.
     * @throws ModelException If o is null.
     */
    private void validateModel(Object o,
                               String failedItem,
                               String accessor) throws ModelException {
        if (null == o) {
            String msg = I18NHelper.getMessage(
                    messages,
                    "EXC_InvalidRelationshipMapping",  // NOI18N
                    failedItem,
                    accessor);
            logger.log(Logger.SEVERE, msg);
            throw new ModelException(msg);
        }
    }

    /**
     * Debug support. Returns a string describing the table and it's keys
     * (only the first key is listed).
     * @param tblName name of the table described
     * @param tbl table being described
     * @param relName name of a relationship 
     */
    private static String getTblInfo(String tblName, TableElement tbl, String relName) {
        int numFK = tbl.getForeignKeys().length;
        ForeignKeyElement fk = null;
        if (numFK > 0) {
            fk = tbl.getForeignKeys()[0];
        }
        return " " + tblName + "=" + tbl.toString()
            + ", # keys=" + numFK // NOI18N
            + ", 1st key=" + fk // NOI18N
            + "; relationship Name=" + relName; // NOI18N
    }
}