/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2004 Bull S.A.
 * Contact: jonas-team@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */
package org.ow2.jonas.deployment.ejb;

import java.util.HashMap;
import java.util.Iterator;

import org.ow2.jonas.deployment.common.DeploymentDescException;
import org.ow2.jonas.deployment.ejb.xml.EjbRelationshipRole;
import org.ow2.jonas.deployment.ejb.xml.ForeignKeyJdbcMapping;
import org.ow2.jonas.deployment.ejb.xml.JonasEjbRelationshipRole;

import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;

/**
 * Class to hold meta-information related to an ejb-relation-role
 * Created on July 7, 2002
 * @author Christophe Ney [cney@batisseurs.com] : Initial developer
 * @author Helene Joanin on May 2003: code cleanup
 * @author Helene Joanin on May 2003: complement for legacy first version
 */
public class EjbRelationshipRoleDesc {

    private Logger logger = null;

    /**
     * This constant can be used to represent an index of a gen class.
     */
    public static final byte INDEX = 1;

    /**
     * This constant can be used to represent the source of the relation, ie the
     * element which references the other.
     */
    public static final byte SOURCE = 2;

    /**
     * This constant can be used to represent the target of the relation, ie the
     * element which is referenced by the other.
     */
    public static final byte TARGET = 4;

    /**
     * Don't change these values without looking at getRelationType() !
     */
    public static final byte OOU = 0;

    public static final byte OOB = 1;

    public static final byte OMU = 2;

    public static final byte OMB = OMU + OOB;

    public static final byte MOU = 4;

    public static final byte MOB = MOU + OOB;

    public static final byte MMU = OMU + MOU;

    public static final byte MMB = MMU + OOB;

    private String rsrName;

    private String ejbSourceName;

    private EjbRelationDesc ejbRelationDesc;

    private EntityCmp2Desc sourceEntityCmp2Desc;

    private boolean isSourceMultiple;

    private EntityCmp2Desc targetEntityCmp2Desc;

    private boolean isTargetMultiple;

    private boolean isSlave;

    protected String cmrFieldName = null;

    protected Class cmrFieldType = null;

    protected boolean isJOnASCMR = false;

    private byte relationType = -1;

    private boolean mustCascade;

    // Zeus objects for the associated mapping information (needed in
    // fillMappingInfo);
    // They may be null.
    private JonasEjbRelationshipRole jSourceRsRole = null;

    // mapping information build by fillMappingInfo and
    // fillMappingInfoWithDefault
    private HashMap foreignKeyMap = new HashMap();

    private boolean hasJdbcMapping = false;

    /**
     * constructor to be used by parent node
     * @param rd parent node = EjbRelationDesc
     * @param role this role (standard EjbRelationshipRole)
     * @param jrole this Jonas role (JonasEjbRelationshipRole). This param may
     *        be null.
     * @param opposite opposite role in the relation (standard
     *        EjbRelationshipRole)
     * @throws DeploymentDescException in error case.
     */
    public EjbRelationshipRoleDesc(EjbRelationDesc rd, String name, EjbRelationshipRole role,
            JonasEjbRelationshipRole jrole, EjbRelationshipRole opposite, boolean isSlave, Logger logger)
            throws DeploymentDescException {

        this.logger = logger;
        ejbRelationDesc = rd;
        this.isSlave = isSlave;
        rsrName = name;
        mustCascade = opposite.isCascadeDelete();
        ejbSourceName = role.getRelationshipRoleSource().getEjbName();

        // mutiplicity is One or Many
        if (opposite.getMultiplicity().equalsIgnoreCase("Many")) {
            isTargetMultiple = true;
            if (role.isCascadeDelete()) {
                throw new DeploymentDescException("Cascade delete not allowed for relationshipRole for relationship '"
                        + rd.getName() + "(because opposite role has a multiplicity of Many)");
            }
        } else if (opposite.getMultiplicity().equalsIgnoreCase("One")) {
            isTargetMultiple = false;
        } else {
            throw new DeploymentDescException("Invalid multiplicity value for relationshipRole for relationship '"
                    + rd.getName() + "'(must be One or Many)");
        }
        if (role.getMultiplicity().equalsIgnoreCase("Many")) {
            isSourceMultiple = true;
        } else if (role.getMultiplicity().equalsIgnoreCase("One")) {
            isSourceMultiple = false;
        } else {
            throw new DeploymentDescException("Invalid multiplicity value for relationshipRole for relationship '"
                    + rd.getName() + "'(must be One or Many)");
        }

        // store cmr field if any
        if (role.getCmrField() != null) {
            setCmrFieldName(role.getCmrField().getCmrFieldName());
            if (isTargetMultiple) {
                String type = role.getCmrField().getCmrFieldType();
                if (type == null) {
                    throw new DeploymentDescException(
                            "You must specify a cmr-field-type in case where the relation is 'Many' in the cmr-field '"
                                    + cmrFieldName + "' of bean " + ejbSourceName);
                }
                setCmrFieldType(type);
            }
        }

        // store the JonasEjbRelationshipRole
        jSourceRsRole = jrole;
    }

    /**
     * Fills the mapping information of this relation-ship role with the values
     * defined in jonas DD.
     * @throws DeploymentDescException in error case.
     */
    protected void fillMappingInfo() throws DeploymentDescException {
        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "" + (jSourceRsRole != null) + " for " + rsrName);
        }
        if (jSourceRsRole != null) {
            for (Iterator i = jSourceRsRole.getForeignKeyJdbcMappingList().iterator(); i.hasNext();) {
                ForeignKeyJdbcMapping fkMapping = (ForeignKeyJdbcMapping) i.next();
                String fkc = fkMapping.getForeignKeyJdbcName();
                String kc = null;
                if (fkMapping.getKeyJdbcName() != null) {
                    kc = fkMapping.getKeyJdbcName();
                }
                if (kc == null) {
                    // if the target bean has a primary-key-field, this value
                    // may not be defined
                    // in this case, this is the column name of the
                    // primary-key-field
                    if (targetEntityCmp2Desc.hasSimplePkField()) {
                        kc = ((FieldJdbcDesc) targetEntityCmp2Desc.getSimplePkField()).getJdbcFieldName();
                    } else {
                        // error
                        throw new DeploymentDescException(
                                "key-jdbc-name must be provided for foreign-key-jdbc-mapping " + fkc
                                        + " of relation-ship role " + rsrName + "of relation "
                                        + ejbRelationDesc.getName());
                    }
                }
                if (logger.isLoggable(BasicLevel.DEBUG)) {
                    logger.log(BasicLevel.DEBUG, "explicit fk mapping = " + fkc + " for " + kc);
                }
                foreignKeyMap.put(kc, fkc);
            }
            hasJdbcMapping = true;
        }
    }

    /**
     * Fills the mapping information of this relation-ship role with default
     * values if the mapping information is not already initialized.
     */
    protected void fillMappingInfoWithDefault() {
        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "" + hasJdbcMapping);
        }
        if (!hasJdbcMapping) {
            if (targetEntityCmp2Desc.hasSimplePkField()) {
                // target entity has a simple pk (primary-key-field)
                String fn = targetEntityCmp2Desc.getSimplePkFieldName();
                FieldJdbcDesc fd = (FieldJdbcDesc) targetEntityCmp2Desc.getCmpFieldDesc(fn);
                String kc = fd.getJdbcFieldName();
                String fkc = targetEntityCmp2Desc.getAbstractSchemaName() + "_" + kc;
                if (logger.isLoggable(BasicLevel.DEBUG)) {
                    logger.log(BasicLevel.DEBUG, "default fk mapping = " + fkc + " for " + kc);
                }
                foreignKeyMap.put(kc, fkc);
            } else {
                // target entity has a composite pk
                for (Iterator i = targetEntityCmp2Desc.getCmpFieldDescIterator(); i.hasNext();) {
                    FieldJdbcDesc fd = (FieldJdbcDesc) i.next();
                    String kc = fd.getJdbcFieldName();
                    String fkc = targetEntityCmp2Desc.getAbstractSchemaName() + "_" + kc;
                    if (logger.isLoggable(BasicLevel.DEBUG)) {
                        logger.log(BasicLevel.DEBUG, "default fk mapping = " + fkc + " for " + kc);
                    }
                    foreignKeyMap.put(kc, fkc);
                }
            }
            hasJdbcMapping = true;
        }
    }

    /**
     * return the name of this relationship role.
     * @return the String name of this relationship role.
     */
    public String getName() {
        return rsrName;
    }

    protected void setCmrFieldName(String name) throws DeploymentDescException {
        cmrFieldName = name;
    }

    protected void setCmrFieldType(String type) throws DeploymentDescException {
        try {
            cmrFieldType = Class.forName(type);
        } catch (ClassNotFoundException e) {
            throw new DeploymentDescException("class name not found for cmr-field " + cmrFieldName + " of bean "
                    + ejbSourceName, e);
        }
        if (!(cmrFieldType.getName().equals("java.util.Collection") || cmrFieldType.getName().equals("java.util.Set"))) {
            throw new DeploymentDescException("value of cmr-field-type " + cmrFieldName + " of bean " + ejbSourceName
                    + " should be java.util.Set or java.util.Collection if set");
        }
    }

    /**
     * mark the cmr as added by JOnAS
     */
    protected void setIsJOnASCmrField() {
        isJOnASCMR = true;
    }

    /**
     * set the source bean of this relation-ship role.
     * @param led EntityCmp2Desc for the source bean of this relation-ship role.
     */
    protected void setSourceBean(EntityCmp2Desc led) {
        sourceEntityCmp2Desc = led;
    }

    /**
     * set the target bean of this relation-ship role.
     * @param led EntityCmp2Desc for the target bean of this relation-ship role.
     */
    protected void setTargetBean(EntityCmp2Desc led) {
        targetEntityCmp2Desc = led;
        if (cmrFieldType == null) {
            cmrFieldType = led.getLocalClass();
        }
    }

    /**
     * get the parent ejb relation of this relation-ship-role.
     * @return the EjbRelationDesc of this relation-ship-role.
     */
    public EjbRelationDesc getRelation() {
        return ejbRelationDesc;
    }

    /**
     * get the opposite relation-ship-role of this relation-ship-role.
     * @return the opposite EjbRelationshipRoleDesc of this relation-ship-role.
     */
    public EjbRelationshipRoleDesc getOppositeRelationshipRole() {
        EjbRelationshipRoleDesc res = ejbRelationDesc.getRelationshipRole1();
        if (res == this) {
            return ejbRelationDesc.getRelationshipRole2();
        } else {
            return res;
        }
    }

    /**
     * Get the name of the ejb involved in this relation-ship-role. This is the
     * source bean name of this relation.
     * @return the String ejb-name of the source bean.
     */
    public String getSourceBeanName() {
        return ejbSourceName;
    }

    /**
     * Get the ejb involved in this relation-ship-role. this is the source bean
     * of this relation.
     * @return the EntityCmp2Desc of the source bean.
     */
    public EntityCmp2Desc getSourceBean() {
        return sourceEntityCmp2Desc;
    }

    /**
     * It retrieves the EntityCmp2Desc which is linked to the EntityCmp2Desc
     * associated to this EjbRelationshipRoleDesc. This is the target bean of
     * this relationship role
     * @return the EntityCmp2Desc of the target bean.
     */
    public EntityCmp2Desc getTargetBean() {
        return targetEntityCmp2Desc;
    }

    /**
     * Get state of opposite relationship-role is relation multiple.
     * @return true if the opposite relationship-role is relation multiple.
     */
    public boolean isSourceMultiple() {
        return isSourceMultiple;
    }

    /**
     * Get state of this relationship-role is relation multiple. (get state of
     * field is relation multiple).
     * @return true if the relationship-role is relation multiple.
     */
    public boolean isTargetMultiple() {
        return isTargetMultiple;
    }

    /**
     * @return true if this bean must cascade delete the other bean in this
     *         relation.
     */
    public boolean mustCascade() {
        return mustCascade;
    }

    /**
     * It returns a boolean value which indicates if the cmr has been added by
     * JOnAS (true) or if the user has specified a cmr field in the descriptor.
     * A CMR field is be added to manage the coherence of the relation OXu
     * @return true if the CMR field is not a bean's programmer CMR field.
     */
    public boolean isJOnASCmrField() {
        return isJOnASCMR;
    }

    /**
     * It retrieves true if the EntityCmp2Desc associated to this
     * EjbRelationshipRoleDesc has a cmr field to the linked EntityCmp2Desc
     * @return true if the relation-ship-role has a CMR field.
     */
    // TODO: is this method really needed (return always true??)
    public boolean hasCmrField() {
        return cmrFieldName != null;
    }

    /**
     * get the name of the cmr-field.
     * @return the String name of the cmr-field.
     */
    public String getCmrFieldName() {
        return cmrFieldName;
    }

    /**
     * get the type of the cmr-field when set in the deployment descriptor.
     * @return Collection or Set for multiple rel. and null for non multiple
     *         rel.
     */
    public Class getCmrFieldType() {
        return cmrFieldType;
    }

    /**
     * This method depends on static values OOB,OOU,... defined upper !
     * @return the type of the relation: OO-u, OO-b, OM-u, ....
     */
    public byte getRelationType() {
        if (relationType == -1) {
            relationType = OOU;
            EjbRelationshipRoleDesc rsr2 = getOppositeRelationshipRole();
            if (rsr2.hasCmrField() && hasCmrField()) {
                relationType += OOB;
            }
            if (isTargetMultiple()) {
                relationType += OMU;
            }
            if (rsr2.isTargetMultiple()) {
                relationType += MOU;
            }
        }
        return relationType;
    }

    /**
     * Is a jdbc mapping is defined for this relationship role ?
     * @return true if a jdbc mapping is defined for this relationship role.
     */
    public boolean hasJdbcMapping() {
        return hasJdbcMapping;
    }

    /**
     * In M-N relationships, only 1 role will write data on DB.
     * @return true if role will not write MN relations on database
     */
    public boolean isSlave() {
        return isSlave;
    }

    /**
     * @param jdbcFieldName a primary key column name of the table associated to
     *        the target bean.
     * @return the foreign key column name associated to the given primary key
     *         column name.
     */
    public String getForeignKeyJdbcName(String jdbcFieldName) {
        return (String) foreignKeyMap.get(jdbcFieldName);
    }

    /**
     * String representation of the object for test purpose
     * @return String representation of this object
     */
    public String toString() {
        StringBuffer ret = new StringBuffer();
        ret.append("\ngetName() = " + getName());
        ret.append("\ngetRelation().getName() = " + getRelation().getName());
        ret.append("\ngetOppositeRelationshipRole().getName() = " + getOppositeRelationshipRole().getName());
        ret.append("\ngetSourceBeanName() = " + getSourceBeanName());
        ret.append("\ngetTargetBean().getName() = " + getTargetBean().getEjbName());
        ret.append("\nisSourceMultiple() = " + isSourceMultiple());
        ret.append("\nisTargetMultiple() = " + isTargetMultiple());
        ret.append("\nmustCascade() = " + mustCascade());
        ret.append("\nisJOnASCmrField() = " + isJOnASCmrField());
        ret.append("\ngetCmrFieldName() = " + getCmrFieldName());
        ret.append("\ngetCmrFieldType() = " + getCmrFieldType());
        ret.append("\ngetRelationType() = " + getRelationType());
        if (hasJdbcMapping()) {
            for (Iterator i = foreignKeyMap.keySet().iterator(); i.hasNext();) {
                String key = (String) i.next();
                String fkey = (String) foreignKeyMap.get(key);
                ret.append("\ngetForeignKeyJdbcName(" + key + ")=" + fkey);
            }
        }
        return ret.toString();
    }

}