/*
 * Helma License Notice
 *
 * The contents of this file are subject to the Helma License
 * Version 2.0 (the "License"). You may not use this file except in
 * compliance with the License. A copy of the License is available at
 * http://adele.helma.org/download/helma/license.txt
 *
 * Copyright 1998-2003 Helma Software. All Rights Reserved.
 *
 * $RCSfile$
 * $Author$
 * $Revision$
 * $Date$
 */

package helma.objectmodel.db;

import helma.framework.core.Application;
import helma.objectmodel.*;
import java.sql.SQLException;
import java.util.Properties;
import java.util.Vector;
import java.util.Map;
import java.util.HashMap;

/**
 * This describes how a property of a persistent Object is stored in a
 *  relational database table. This can be either a scalar property (string, date, number etc.)
 *  or a reference to one or more other objects.
 */
public final class Relation {
    // these constants define different type of property-to-db-mappings
    // there is an error in the description of this relation
    public final static int INVALID = -1;

    // a mapping of a non-object, scalar type
    public final static int PRIMITIVE = 0;

    // a 1-to-1 relation, i.e. a field in the table is a foreign key to another object
    public final static int REFERENCE = 1;

    // a 1-to-many relation, a field in another table points to objects of this type
    public final static int COLLECTION = 2;

    // a 1-to-1 reference with multiple or otherwise not-trivial constraints
    // this is managed differently than REFERENCE, hence the separate type.
    public final static int COMPLEX_REFERENCE = 3;

    // direct mapping is a very powerful feature: objects of some types can be directly accessed
    // by one of their properties/db fields.
    // public final static int DIRECT = 3;
    // the DbMapping of the type we come from
    DbMapping ownType;

    // the DbMapping of the prototype we link to, unless this is a "primitive" (non-object) relation
    DbMapping otherType;

    // the column type, as defined in java.sql.Types
    int columnType;

    //  if this relation defines a virtual node, we need to provide a DbMapping for these virtual nodes
    DbMapping virtualMapping;
    String propName;
    String columnName;
    int reftype;
    Constraint[] constraints;
    boolean virtual;
    boolean readonly;
    boolean aggressiveLoading;
    boolean aggressiveCaching;
    boolean isPrivate = false;
    boolean referencesPrimaryKey = false;
    String accessName; // db column used to access objects through this relation
    String order;
    String groupbyOrder;
    String groupby;
    String prototype;
    String groupbyPrototype;
    String filter;
    int maxSize = 0;

    /**
     * This constructor makes a copy of an existing relation. Not all fields are copied, just those
     * which are needed in groupby- and virtual nodes defined by this relation.
     */
    public Relation(Relation rel) {
        this.ownType = rel.ownType;
        this.otherType = rel.otherType;
        this.propName = rel.propName;
        this.columnName = rel.columnName;
        this.reftype = rel.reftype;
        this.constraints = rel.constraints;
        this.accessName = rel.accessName;
        this.maxSize = rel.maxSize;
    }

    /**
     * Reads a relation entry from a line in a properties file.
     */
    public Relation(String desc, String propName, DbMapping ownType, Properties props) {
        this.ownType = ownType;
        this.propName = propName;
        otherType = null;
    }

    ////////////////////////////////////////////////////////////////////////////////////////////
    // parse methods for new file format
    ////////////////////////////////////////////////////////////////////////////////////////////
    public void update(String desc, Properties props) {
        Application app = ownType.getApplication();

        if ((desc == null) || "".equals(desc.trim())) {
            if (propName != null) {
                reftype = PRIMITIVE;
                columnName = propName;
            } else {
                reftype = INVALID;
                columnName = propName;
            }
        } else {
            desc = desc.trim();

            int open = desc.indexOf("(");
            int close = desc.indexOf(")");

            if ((open > -1) && (close > open)) {
                String ref = desc.substring(0, open).trim();
                String proto = desc.substring(open + 1, close).trim();

                if ("collection".equalsIgnoreCase(ref)) {
                    virtual = !"_children".equalsIgnoreCase(propName);
                    reftype = COLLECTION;
                } else if ("mountpoint".equalsIgnoreCase(ref)) {
                    virtual = true;
                    reftype = COLLECTION;
                    prototype = proto;
                } else if ("object".equalsIgnoreCase(ref)) {
                    virtual = false;
                    if (reftype != COMPLEX_REFERENCE) {
                        reftype = REFERENCE;
                    }
                } else {
                    throw new RuntimeException("Invalid property Mapping: " + desc);
                }

                otherType = app.getDbMapping(proto);

                if (otherType == null) {
                    throw new RuntimeException("DbMapping for " + proto +
                                               " not found from " + ownType.getTypeName());
                }

                // make sure the type we're referring to is up to date!
                if (otherType != null && otherType.needsUpdate()) {
                    otherType.update();
                }

            } else {
                virtual = false;
                columnName = desc;
                reftype = PRIMITIVE;
            }
        }

        readonly = "true".equalsIgnoreCase(props.getProperty(propName + ".readonly"));

        isPrivate = "true".equalsIgnoreCase(props.getProperty(propName + ".private"));

        // the following options only apply to object and collection relations
        if ((reftype != PRIMITIVE) && (reftype != INVALID)) {
            Vector newConstraints = new Vector();

            parseOptions(newConstraints, props);

            constraints = new Constraint[newConstraints.size()];
            newConstraints.copyInto(constraints);


            if (reftype == REFERENCE || reftype == COMPLEX_REFERENCE) {
                if (constraints.length == 0) {
                    referencesPrimaryKey = true;
                } else {
                    boolean rprim = false;
                    for (int i=0; i<constraints.length; i++) {
                        if (constraints[0].foreignKeyIsPrimary()) {
                            rprim = true;
                        }
                    }
                    referencesPrimaryKey = rprim;
                }

                // check if this is a non-trivial reference
                if (constraints.length > 0 && !usesPrimaryKey()) {
                    reftype = COMPLEX_REFERENCE;
                } else {
                    reftype = REFERENCE;
                }
            }

            if (reftype == COLLECTION) {
                referencesPrimaryKey = (accessName == null) ||
                        accessName.equalsIgnoreCase(otherType.getIDField());
            }

            // if DbMapping for virtual nodes has already been created,
            // update its subnode relation.
            // FIXME: needs to be synchronized?
            if (virtualMapping != null) {
                virtualMapping.lastTypeChange = ownType.lastTypeChange;
                virtualMapping.subRelation = getVirtualSubnodeRelation();
                virtualMapping.propRelation = getVirtualPropertyRelation();
            }
        } else {
            referencesPrimaryKey = false;
        }
    }

    protected void parseOptions(Vector cnst, Properties props) {
        String loading = props.getProperty(propName + ".loadmode");

        aggressiveLoading = (loading != null) &&
                            "aggressive".equalsIgnoreCase(loading.trim());

        String caching = props.getProperty(propName + ".cachemode");

        aggressiveCaching = (caching != null) &&
                            "aggressive".equalsIgnoreCase(caching.trim());

        // get order property
        order = props.getProperty(propName + ".order");

        if ((order != null) && (order.trim().length() == 0)) {
            order = null;
        }

        // get additional filter property
        filter = props.getProperty(propName + ".filter");

        if (filter != null) {
            if (filter.trim().length() == 0) {
                filter = null;
            } else {
                // parenthesise filter
                filter = "("+filter+")";
            }
        }

        // get max size of collection
        String max = props.getProperty(propName + ".maxSize");

        if (max != null) {
            try {
                maxSize = Integer.parseInt(max);
            } catch (NumberFormatException nfe) {
                maxSize = 0;
            }
        } else {
            maxSize = 0;
        }

        // get group by property
        groupby = props.getProperty(propName + ".group");

        if ((groupby != null) && (groupby.trim().length() == 0)) {
            groupby = null;
        }

        if (groupby != null) {
            groupbyOrder = props.getProperty(propName + ".group.order");

            if ((groupbyOrder != null) && (groupbyOrder.trim().length() == 0)) {
                groupbyOrder = null;
            }

            groupbyPrototype = props.getProperty(propName + ".group.prototype");

            if ((groupbyPrototype != null) && (groupbyPrototype.trim().length() == 0)) {
                groupbyPrototype = null;
            }

            // aggressive loading and caching is not supported for groupby-nodes
            // aggressiveLoading = aggressiveCaching = false;
        }

        // check if subnode condition should be applied for property relations
        accessName = props.getProperty(propName + ".accessname");

        // parse contstraints
        String local = props.getProperty(propName + ".local");
        String foreign = props.getProperty(propName + ".foreign");

        if ((local != null) && (foreign != null)) {
            cnst.addElement(new Constraint(local, foreign, false));
            columnName = local;
        }

        // parse additional contstraints from *.1 to *.9
        for (int i=1; i<10; i++) {
            local = props.getProperty(propName + ".local."+i);
            foreign = props.getProperty(propName + ".foreign."+i);

            if ((local != null) && (foreign != null)) {
                cnst.addElement(new Constraint(local, foreign, false));
            }
        }
    }

    ///////////////////////////////////////////////////////////////////////////////////////////

    /**
     * Does this relation describe a virtual (collection) node?
     */
    public boolean isVirtual() {
        return virtual;
    }

    /**
     * Tell if this relation represents a primitive (scalar) value mapping.
     */
    public boolean isPrimitive() {
        return reftype == PRIMITIVE;
    }

    /**
     *  Returns true if this Relation describes an object reference property
     */
    public boolean isReference() {
        return reftype == REFERENCE;
    }

    /**
     *  Returns true if this Relation describes a collection object property
     */
    public boolean isCollection() {
        return reftype == COLLECTION;
    }

    /**
     *  Returns true if this Relation describes a complex object reference property
     */
    public boolean isComplexReference() {
        return reftype == COMPLEX_REFERENCE;
    }

    /**
     *  Tell wether the property described by this relation is to be handled as private, i.e.
     *  a change on it should not result in any changed object/collection relations.
     */
    public boolean isPrivate() {
        return isPrivate;
    }

    /**
     *  Check whether aggressive loading is set for this relation
     */
    public boolean loadAggressively() {
        return aggressiveLoading;
    }

    /**
     *  Returns the number of constraints for this relation.
     */
    public int countConstraints() {
        if (constraints == null)
            return 0;
        return constraints.length;
    }

    /**
     *  Returns true if the object represented by this Relation has to be
     *  created on demand at runtime by the NodeManager. This is true for:
     *
     *  - collection (aka virtual) nodes
     *  - nodes accessed via accessname
     *  - group nodes
     *  - complex reference nodes
     */
    public boolean createOnDemand() {
        return virtual ||
            (otherType.isRelational() && accessName != null) ||
            (groupby != null) || isComplexReference();
    }

    /**
     *  Returns true if the object represented by this Relation has to be
     *  persisted in the internal db in order to be functional. This is true if
     *  the subnodes contained in this collection are stored in the embedded
     *  database. In this case, the collection itself must also be an ordinary
     *  object stored in the db, since a virtual collection would lose its
     *  its content after restarts.
     */
    public boolean needsPersistence() {
        if (!virtual) {
            return false;
        }

        if (prototype == null) {
            return !otherType.isRelational();
        }

        DbMapping sub = otherType.getSubnodeMapping();

        return (sub != null) && !sub.isRelational();
    }

    /**
     * Return the prototype to be used for object reached by this relation
     */
    public String getPrototype() {
        return prototype;
    }

    /**
     * Return the name of the local property this relation is defined for
     */
    public String getPropName() {
        return propName;
    }

    /**
     *
     *
     * @param ct ...
     */
    public void setColumnType(int ct) {
        columnType = ct;
    }

    /**
     *
     *
     * @return ...
     */
    public int getColumnType() {
        return columnType;
    }

    /**
     *  Get the group for a collection relation, if defined.
     *
     * @return the name of the column used to group child objects, if any.
     */
    public String getGroup() {
        return groupby;
    }

    /**
     * Add a constraint to the current list of constraints
     */
    protected void addConstraint(Constraint c) {
        if (constraints == null) {
            constraints = new Constraint[1];
            constraints[0] = c;
        } else {
            Constraint[] nc = new Constraint[constraints.length + 1];

            System.arraycopy(constraints, 0, nc, 0, constraints.length);
            nc[nc.length - 1] = c;
            constraints = nc;
        }
    }

    /**
     *
     *
     * @return true if the foreign key used for this relation is the
     * other object's primary key.
     */
    public boolean usesPrimaryKey() {
        return referencesPrimaryKey;
    }

    /**
     *
     *
     * @return ...
     */
    public boolean hasAccessName() {
        return accessName != null;
    }

    /**
     *
     *
     * @return ...
     */
    public String getAccessName() {
        return accessName;
    }

    /**
     *
     *
     * @return ...
     */
    public Relation getSubnodeRelation() {
        // return subnoderelation;
        return null;
    }

    /**
     * Return the local field name for updates.
     */
    public String getDbField() {
        return columnName;
    }

    /**
     *  get a DbMapping to use for virtual aka collection nodes.
     */
    public DbMapping getVirtualMapping() {
        // return null unless this relation describes a virtual/collection node.
        if (!virtual) {
            return null;
        }

        // if the collection node is prototyped, return the app's DbMapping
        // for that prototype
        if (prototype != null) {
            return otherType;
        }

        // create a synthetic DbMapping that describes how to fetch the
        // collection's child objects.
        if (virtualMapping == null) {
            virtualMapping = new DbMapping(ownType.app);
            virtualMapping.subRelation = getVirtualSubnodeRelation();
            virtualMapping.propRelation = getVirtualPropertyRelation();
        }

        return virtualMapping;
    }

    /**
     * Return a Relation that defines the subnodes of a virtual node.
     */
    Relation getVirtualSubnodeRelation() {
        if (!virtual) {
            throw new RuntimeException("getVirtualSubnodeRelation called on non-virtual relation");
        }

        Relation vr = new Relation(this);

        vr.groupby = groupby;
        vr.groupbyOrder = groupbyOrder;
        vr.groupbyPrototype = groupbyPrototype;
        vr.order = order;
        vr.filter = filter;
        vr.maxSize = maxSize;
        vr.constraints = constraints;
        vr.aggressiveLoading = aggressiveLoading;
        vr.aggressiveCaching = aggressiveCaching;

        return vr;
    }

    /**
     * Return a Relation that defines the properties of a virtual node.
     */
    Relation getVirtualPropertyRelation() {
        if (!virtual) {
            throw new RuntimeException("getVirtualPropertyRelation called on non-virtual relation");
        }

        Relation vr = new Relation(this);

        vr.groupby = groupby;
        vr.groupbyOrder = groupbyOrder;
        vr.groupbyPrototype = groupbyPrototype;
        vr.order = order;
        vr.filter = filter;
        vr.maxSize = maxSize;
        vr.constraints = constraints;

        return vr;
    }

    /**
     * Return a Relation that defines the subnodes of a group-by node.
     */
    Relation getGroupbySubnodeRelation() {
        if (groupby == null) {
            throw new RuntimeException("getGroupbySubnodeRelation called on non-group-by relation");
        }

        Relation vr = new Relation(this);

        vr.order = order;
        vr.prototype = groupbyPrototype;
        vr.filter = filter;
        vr.constraints = constraints;
        vr.addConstraint(new Constraint(null, groupby, true));
        vr.aggressiveLoading = aggressiveLoading;
        vr.aggressiveCaching = aggressiveCaching;

        return vr;
    }

    /**
     * Return a Relation that defines the properties of a group-by node.
     */
    Relation getGroupbyPropertyRelation() {
        if (groupby == null) {
            throw new RuntimeException("getGroupbyPropertyRelation called on non-group-by relation");
        }

        Relation vr = new Relation(this);

        vr.order = order;
        vr.prototype = groupbyPrototype;
        vr.filter = filter;
        vr.constraints = constraints;
        vr.addConstraint(new Constraint(null, groupby, true));

        return vr;
    }

    /**
     *  Build the second half of an SQL select statement according to this relation
     *  and a local object.
     */
    public String buildQuery(INode home, INode nonvirtual, String kstr, String pre,
                             boolean useOrder) throws SQLException {
        StringBuffer q = new StringBuffer();
        String prefix = pre;

        if (kstr != null && !isComplexReference()) {
            q.append(prefix);

            String accessColumn = (accessName == null) ? otherType.getIDField() : accessName;

            q.append(otherType.getTableName());
            q.append(".");
            q.append(accessColumn);
            q.append(" = ");

            // check if column is string type and value needs to be quoted
            if (otherType.needsQuotes(accessColumn)) {
                q.append("'");
                q.append(escape(kstr));
                q.append("'");
            } else {
                q.append(escape(kstr));
            }

            prefix = " AND ";
        }

        for (int i = 0; i < constraints.length; i++) {
            q.append(prefix);
            constraints[i].addToQuery(q, home, nonvirtual);
            prefix = " AND ";
        }

        if (filter != null) {
            q.append(prefix);
            q.append(filter);
        }

        if (groupby != null) {
            q.append(" GROUP BY " + groupby);

            if (useOrder && (groupbyOrder != null)) {
                q.append(" ORDER BY " + groupbyOrder);
            }
        } else if (useOrder && (order != null)) {
            q.append(" ORDER BY " + order);
        }

        return q.toString();
    }

    /**
     *
     *
     * @param home ...
     * @param nonvirtual ...
     *
     * @return ...
     *
     * @throws SQLException ...
     */
    public String renderConstraints(INode home, INode nonvirtual)
                             throws SQLException {
        StringBuffer q = new StringBuffer();
        String prefix = " AND ";

        for (int i = 0; i < constraints.length; i++) {
            q.append(prefix);
            constraints[i].addToQuery(q, home, nonvirtual);
        }

        if (filter != null) {
            q.append(prefix);
            q.append(filter);
        }

        return q.toString();
    }

    public void renderJoinConstraints(StringBuffer select) {
        for (int i = 0; i < constraints.length; i++) {
            select.append(ownType.getTableName());
            select.append(".");
            select.append(constraints[i].localName);
            select.append(" = _HLM_");
            select.append(propName);
            select.append(".");
            select.append(constraints[i].foreignName);
            if (i == constraints.length-1) {
                select.append(" ");
            } else {
                select.append(" AND ");
            }
        }

    }

    /**
     * Get the order section to use for this relation
     */
    public String getOrder() {
        if (groupby != null) {
            return groupbyOrder;
        } else {
            return order;
        }
    }

    /**
     *  Tell wether the property described by this relation is to be handled
     *  as readonly/write protected.
     */
    public boolean isReadonly() {
        return readonly;
    }

    /**
     * Check if the child node fullfills the constraints defined by this relation.
     */
    public boolean checkConstraints(Node parent, Node child) {
        // problem: if a filter property is defined for this relation,
        // i.e. a piece of static SQL-where clause, we'd have to evaluate it
        // in order to check the constraints. Because of this, if a filter
        // is defined, we return false as soon as the modified-time is greater
        // than the create-time of the child, i.e. if the child node has been
        // modified since it was first fetched from the db.
        if ((filter != null) && (child.lastModified() > child.created())) {
            return false;
        }

        for (int i = 0; i < constraints.length; i++) {
            String propname = constraints[i].foreignProperty();

            if (propname != null) {
                INode home = constraints[i].isGroupby ? parent
                                                      : parent.getNonVirtualParent();
                String value = null;

                if (constraints[i].localKeyIsPrimary(home.getDbMapping())) {
                    value = home.getID();
                } else if (ownType.isRelational()) {
                    value = home.getString(constraints[i].localProperty());
                } else {
                    value = home.getString(constraints[i].localName);
                }

                if ((value != null) && !value.equals(child.getString(propname))) {
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * Make sure that the child node fullfills the constraints defined by this relation by setting the
     * appropriate properties
     */
    public void setConstraints(Node parent, Node child) {
        Node home = parent.getNonVirtualParent();
        for (int i = 0; i < constraints.length; i++) {
            // don't set groupby constraints since we don't know if the
            // parent node is the base node or a group node
            if (constraints[i].isGroupby) {
                continue;
            }

            // check if we update the local or the other object, depending on
            // whether the primary key of either side is used.

            if (constraints[i].foreignKeyIsPrimary()) {
                String localProp = constraints[i].localProperty();
                if (localProp == null) {
                    System.err.println ("Error: column "+constraints[i].localName+
                       " must be mapped in order to be used as constraint in "+
                       Relation.this);
                } else {
                    home.setString(localProp, child.getID());
                }
                continue;
            }

            Relation crel = otherType.columnNameToRelation(constraints[i].foreignName);
            if (crel != null) {
                // INode home = constraints[i].isGroupby ? parent : nonVirtual;

                if (constraints[i].localKeyIsPrimary(home.getDbMapping())) {
                    // only set node if property in child object is defined as reference.
                    if (crel.reftype == REFERENCE) {
                        INode currentValue = child.getNode(crel.propName);

                        // we set the backwards reference iff the reference is currently unset, if
                        // is set to a transient object, or if the new target is not transient. This
                        // prevents us from overwriting a persistent refererence with a transient one,
                        // which would most probably not be what we want.
                        if ((currentValue == null) ||
                                ((currentValue != home) &&
                                ((currentValue.getState() == Node.TRANSIENT) ||
                                (home.getState() != Node.TRANSIENT)))) {
                            child.setNode(crel.propName, home);
                        }
                    } else if (crel.reftype == PRIMITIVE) {
                        child.setString(crel.propName, home.getID());
                    }
                } else if (crel.reftype == PRIMITIVE) {
                    Property prop = null;

                    if (ownType.isRelational()) {
                        prop = home.getProperty(constraints[i].localProperty());
                    } else {
                        prop = home.getProperty(constraints[i].localName);
                    }

                    if (prop != null) {
                        child.set(crel.propName, prop.getValue(), prop.getType());
                    }
                }
            }
        }
    }

    /**
     * Unset the constraints that link two objects together.
     */
    public void unsetConstraints(Node parent, INode child) {
        Node home = parent.getNonVirtualParent();
        for (int i = 0; i < constraints.length; i++) {
            // don't set groupby constraints since we don't know if the
            // parent node is the base node or a group node
            if (constraints[i].isGroupby) {
                continue;
            }

            // check if we update the local or the other object, depending on
            // whether the primary key of either side is used.

            if (constraints[i].foreignKeyIsPrimary()) {
                String localProp = constraints[i].localProperty();
                if (localProp != null) {
                    home.setString(localProp, null);
                }
                continue;
            }

            Relation crel = otherType.columnNameToRelation(constraints[i].foreignName);
            if (crel != null) {
                // INode home = constraints[i].isGroupby ? parent : nonVirtual;

                if (constraints[i].localKeyIsPrimary(home.getDbMapping())) {
                    // only set node if property in child object is defined as reference.
                    if (crel.reftype == REFERENCE) {
                        INode currentValue = child.getNode(crel.propName);

                        if ((currentValue == home)) {
                            child.setString(crel.propName, null);
                        }
                    } else if (crel.reftype == PRIMITIVE) {
                        child.setString(crel.propName, null);
                    }
                } else if (crel.reftype == PRIMITIVE) {
                    Property prop = null;

                    if (ownType.isRelational()) {
                        prop = home.getProperty(constraints[i].localProperty());
                    } else {
                        prop = home.getProperty(constraints[i].localName);
                    }

                    if (prop != null) {
                        child.setString(crel.propName, null);
                    }
                }
            }
        }
    }

    /**
     *  Returns a map containing the key/value pairs for a specific Node
     */
    public Map getKeyParts(INode home) {
        Map map = new HashMap();
        for (int i=0; i<constraints.length; i++) {
            if (ownType.getIDField().equals(constraints[i].localName)) {
                map.put(constraints[i].foreignName, home.getID());
            } else {
                map.put(constraints[i].foreignName, home.getString(constraints[i].localProperty()));
            }
        }
        return map;
    }

    // a utility method to escape single quotes
    String escape(String str) {
        if (str == null) {
            return null;
        }

        if (str.indexOf("'") < 0) {
            return str;
        }

        int l = str.length();
        StringBuffer sbuf = new StringBuffer(l + 10);

        for (int i = 0; i < l; i++) {
            char c = str.charAt(i);

            if (c == '\'') {
                sbuf.append('\'');
            }

            sbuf.append(c);
        }

        return sbuf.toString();
    }

    /**
     *
     *
     * @return ...
     */
    public String toString() {
        String c = "";
        String spacer = "";

        if (constraints != null) {
            c = " constraints: ";
            for (int i = 0; i < constraints.length; i++) {
                c += spacer;
                c += constraints[i].toString();
                spacer = ", ";
            }
        }

        String target = otherType == null ? columnName : otherType.toString();

        return "Relation " + ownType+"."+propName + " -> " + target + c;
    }

    /**
     * The Constraint class represents a part of the where clause in the query used to
     * establish a relation between database mapped objects.
     */
    class Constraint {
        String localName;
        String foreignName;
        boolean isGroupby;

        Constraint(String local, String foreign, boolean groupby) {
            localName = local;
            foreignName = foreign;
            isGroupby = groupby;
        }

        public void addToQuery(StringBuffer q, INode home, INode nonvirtual)
                        throws SQLException {
            String local = null;
            INode ref = isGroupby ? home : nonvirtual;

            if ((localName == null) ||
                    localName.equalsIgnoreCase(ref.getDbMapping().getIDField())) {
                local = ref.getID();
            } else {
                String homeprop = ownType.columnNameToProperty(localName);

                local = ref.getString(homeprop);
            }

            q.append(otherType.getTableName());
            q.append(".");
            q.append(foreignName);
            q.append(" = ");

            if (otherType.needsQuotes(foreignName)) {
                q.append("'");
                q.append(escape(local));
                q.append("'");
            } else {
                q.append(escape(local));
            }
        }

        public boolean foreignKeyIsPrimary() {
            return (foreignName == null) ||
                   foreignName.equalsIgnoreCase(otherType.getIDField());
        }

        public boolean localKeyIsPrimary(DbMapping homeMapping) {
            return (homeMapping == null) || (localName == null) ||
                   localName.equalsIgnoreCase(homeMapping.getIDField());
        }

        public String foreignProperty() {
            return otherType.columnNameToProperty(foreignName);
        }

        public String localProperty() {
            return ownType.columnNameToProperty(localName);
        }

        public String toString() {
            return localName + "=" + otherType.getTypeName() + "." + foreignName;
        }
    }
}
