org.apache.cayenne.map.DbRelationship.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.cayenne.map.DbRelationship.java

Source

/*****************************************************************
 *   Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 ****************************************************************/

package org.apache.cayenne.map;

import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.configuration.ConfigurationNode;
import org.apache.cayenne.configuration.ConfigurationNodeVisitor;
import org.apache.cayenne.util.Util;
import org.apache.cayenne.util.XMLEncoder;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Transformer;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * A DbRelationship is a descriptor of a database inter-table relationship based
 * on one or more primary key/foreign key pairs.
 */
public class DbRelationship extends Relationship implements ConfigurationNode {

    // The columns through which the join is implemented.
    protected List<DbJoin> joins = new ArrayList<>(2);

    // Is relationship from source to target points to dependent primary
    // key (primary key column of destination table that is also a FK to the
    // source
    // column)
    protected boolean toDependentPK;

    public DbRelationship() {
        super();
    }

    public DbRelationship(String name) {
        super(name);
    }

    @Override
    public DbEntity getSourceEntity() {
        return (DbEntity) super.getSourceEntity();
    }

    /**
     * @since 3.1
     */
    public <T> T acceptVisitor(ConfigurationNodeVisitor<T> visitor) {
        return visitor.visitDbRelationship(this);
    }

    /**
     * Prints itself as XML to the provided XMLEncoder.
     * 
     * @since 1.1
     */
    public void encodeAsXML(XMLEncoder encoder) {
        encoder.print("<db-relationship name=\"");
        encoder.print(Util.encodeXmlAttribute(getName()));
        encoder.print("\" source=\"");
        encoder.print(Util.encodeXmlAttribute(getSourceEntity().getName()));

        if (getTargetEntityName() != null && getTargetEntity() != null) {
            encoder.print("\" target=\"");
            encoder.print(Util.encodeXmlAttribute(getTargetEntityName()));
        }

        if (isToDependentPK() && isValidForDepPk()) {
            encoder.print("\" toDependentPK=\"true");
        }

        encoder.print("\" toMany=\"");
        encoder.print(isToMany());
        encoder.println("\">");

        encoder.indent(1);
        encoder.print(getJoins());
        encoder.indent(-1);

        encoder.println("</db-relationship>");
    }

    /**
     * Returns a target of this relationship. If relationship is not attached to
     * a DbEntity, and DbEntity doesn't have a namespace, and exception is
     * thrown.
     */
    @Override
    public DbEntity getTargetEntity() {
        String targetName = getTargetEntityName();
        if (targetName == null) {
            return null;
        }

        return getNonNullNamespace().getDbEntity(targetName);
    }

    /**
     * Returns a Collection of target attributes.
     * 
     * @since 1.1
     */
    @SuppressWarnings("unchecked")
    public Collection<DbAttribute> getTargetAttributes() {
        if (joins.size() == 0) {
            return Collections.emptyList();
        }

        return CollectionUtils.collect(joins, JoinTransformers.targetExtractor);
    }

    /**
     * Returns a Collection of source attributes.
     * 
     * @since 1.1
     */
    @SuppressWarnings("unchecked")
    public Collection<DbAttribute> getSourceAttributes() {
        if (joins.size() == 0) {
            return Collections.emptyList();
        }

        return CollectionUtils.collect(joins, JoinTransformers.sourceExtractor);
    }

    /**
     * Creates a new relationship with the same set of joins, but going in the
     * opposite direction.
     * 
     * @since 1.0.5
     */
    public DbRelationship createReverseRelationship() {
        DbEntity targetEntity = (DbEntity) getTargetEntity();

        DbRelationship reverse = new DbRelationship();
        reverse.setSourceEntity(targetEntity);
        reverse.setTargetEntityName(getSourceEntity().getName());

        // TODO: andrus 12/24/2007 - one more case to handle - set reverse
        // toDepPK = true
        // if this relationship toDepPK is false, but the entities are joined on
        // a PK...
        // on the other hand, these can still be two independent entities...

        if (isToDependentPK() && !toMany && joins.size() == targetEntity.getPrimaryKeys().size()) {
            reverse.setToMany(false);
        } else {
            reverse.setToMany(!toMany);
        }

        for (DbJoin join : joins) {
            DbJoin reverseJoin = join.createReverseJoin();
            reverseJoin.setRelationship(reverse);
            reverse.addJoin(reverseJoin);
        }

        return reverse;
    }

    /**
     * Returns DbRelationship that is the opposite of this DbRelationship. This
     * means a relationship from this target entity to this source entity with
     * the same join semantics. Returns null if no such relationship exists.
     */
    public DbRelationship getReverseRelationship() {
        DbEntity target = getTargetEntity();

        if (target == null) {
            return null;
        }

        Entity src = this.getSourceEntity();

        // special case - relationship to self with no joins...
        if (target == src && joins.size() == 0) {
            return null;
        }

        TestJoin testJoin = new TestJoin(this);
        for (DbRelationship rel : target.getRelationships()) {
            if (rel.getTargetEntity() != src) {
                continue;
            }

            List<DbJoin> otherJoins = rel.getJoins();
            if (otherJoins.size() != joins.size()) {
                continue;
            }

            boolean joinsMatch = true;
            for (DbJoin join : otherJoins) {
                // flip join and try to find similar
                testJoin.setSourceName(join.getTargetName());
                testJoin.setTargetName(join.getSourceName());
                if (!joins.contains(testJoin)) {
                    joinsMatch = false;
                    break;
                }
            }

            if (joinsMatch) {
                return rel;
            }
        }

        return null;
    }

    /**
     * Returns true if the relationship points to at least one of the PK columns
     * of the target entity.
     * 
     * @since 1.1
     */
    public boolean isToPK() {
        for (DbJoin join : getJoins()) {

            DbAttribute target = join.getTarget();
            if (target == null) {
                return false;
            }

            if (target.isPrimaryKey()) {
                return true;
            }
        }

        return false;
    }

    /**
     * @since 3.0
     */
    public boolean isFromPK() {
        for (DbJoin join : getJoins()) {
            DbAttribute source = join.getSource();
            if (source == null) {
                return false;
            }

            if (source.isPrimaryKey()) {
                return true;
            }
        }

        return false;
    }

    /**
     * Returns <code>true</code> if a method <code>isToDependentPK</code> of
     * reverse relationship of this relationship returns <code>true</code>.
     */
    public boolean isToMasterPK() {
        if (isToMany() || isToDependentPK()) {
            return false;
        }

        DbRelationship revRel = getReverseRelationship();
        return revRel != null && revRel.isToDependentPK();
    }

    /**
     * Returns a boolean indicating whether modifying a target of such
     * relationship in any way will not change the underlying table row of the
     * source.
     * 
     * @since 4.0
     */
    public boolean isSourceIndependentFromTargetChange() {
        // note - call "isToPK" at the end of the chain, since
        // if it is to a dependent PK, we still should return true...
        return isToMany() || isToDependentPK() || !isToPK();
    }

    /**
     * Returns <code>true</code> if relationship from source to target points to
     * dependent primary key. Dependent PK is a primary key column of the
     * destination table that is also a FK to the source column.
     */
    public boolean isToDependentPK() {
        return toDependentPK;
    }

    public void setToDependentPK(boolean toDependentPK) {
        this.toDependentPK = toDependentPK;
    }

    /**
     * @since 1.1
     */
    public boolean isValidForDepPk() {
        // handle case with no joins
        if (getJoins().size() == 0) {
            return false;
        }

        for (DbJoin join : getJoins()) {
            DbAttribute target = join.getTarget();
            DbAttribute source = join.getSource();

            if (target != null && !target.isPrimaryKey() || source != null && !source.isPrimaryKey()) {
                return false;
            }
        }

        return true;
    }

    /**
     * Returns a list of joins. List is returned by reference, so any
     * modifications of the list will affect this relationship.
     */
    public List<DbJoin> getJoins() {
        return joins;
    }

    /**
     * Adds a join.
     * 
     * @since 1.1
     */
    public void addJoin(DbJoin join) {
        if (join != null) {
            joins.add(join);
        }
    }

    public void removeJoin(DbJoin join) {
        joins.remove(join);
    }

    public void removeAllJoins() {
        joins.clear();
    }

    public void setJoins(Collection<DbJoin> newJoins) {
        this.removeAllJoins();

        if (newJoins != null) {
            joins.addAll(newJoins);
        }
    }

    /**
     * Creates a snapshot of primary key attributes of a target object of this
     * relationship based on a snapshot of a source. Only "to-one" relationships
     * are supported. Returns null if relationship does not point to an object.
     * Throws CayenneRuntimeException if relationship is "to many" or if
     * snapshot is missing id components.
     */
    public Map<String, Object> targetPkSnapshotWithSrcSnapshot(Map<String, Object> srcSnapshot) {

        if (isToMany()) {
            throw new CayenneRuntimeException("Only 'to one' relationships support this method.");
        }

        Map<String, Object> idMap;

        int numJoins = joins.size();
        int foundNulls = 0;

        // optimize for the most common single column join
        if (numJoins == 1) {
            DbJoin join = joins.get(0);
            Object val = srcSnapshot.get(join.getSourceName());
            if (val == null) {
                foundNulls++;
                idMap = Collections.emptyMap();
            } else {
                idMap = Collections.singletonMap(join.getTargetName(), val);
            }
        }
        // handle generic case: numJoins > 1
        else {
            idMap = new HashMap<>(numJoins * 2);
            for (DbJoin join : joins) {
                DbAttribute source = join.getSource();
                Object val = srcSnapshot.get(join.getSourceName());

                if (val == null) {

                    // some keys may be nulls and some not in case of multi-key
                    // relationships where PK and FK partially overlap (see
                    // CAY-284)
                    if (!source.isMandatory()) {
                        return null;
                    }

                    foundNulls++;
                } else {
                    idMap.put(join.getTargetName(), val);
                }
            }
        }

        if (foundNulls == 0) {
            return idMap;
        } else if (foundNulls == numJoins) {
            return null;
        } else {
            throw new CayenneRuntimeException("Some parts of FK are missing in snapshot, relationship: %s", this);
        }
    }

    /**
     * Common code to srcSnapshotWithTargetSnapshot. Both are functionally the
     * same, except for the name, and whether they operate on a toMany or a
     * toOne.
     */
    private Map<String, Object> srcSnapshotWithTargetSnapshot(Map<String, Object> targetSnapshot) {
        int len = joins.size();

        // optimize for the most common single column join
        if (len == 1) {
            DbJoin join = joins.get(0);
            Object val = targetSnapshot.get(join.getTargetName());
            return Collections.singletonMap(join.getSourceName(), val);
        }

        // general case
        Map<String, Object> idMap = new HashMap<>(len * 2);
        for (DbJoin join : joins) {
            Object val = targetSnapshot.get(join.getTargetName());
            idMap.put(join.getSourceName(), val);
        }

        return idMap;
    }

    /**
     * Creates a snapshot of foreign key attributes of a source object of this
     * relationship based on a snapshot of a target. Only "to-one" relationships
     * are supported. Throws CayenneRuntimeException if relationship is
     * "to many".
     */
    public Map<String, Object> srcFkSnapshotWithTargetSnapshot(Map<String, Object> targetSnapshot) {

        if (isToMany()) {
            throw new CayenneRuntimeException("Only 'to one' relationships support this method.");
        }

        return srcSnapshotWithTargetSnapshot(targetSnapshot);
    }

    /**
     * Creates a snapshot of primary key attributes of a source object of this
     * relationship based on a snapshot of a target. Only "to-many"
     * relationships are supported. Throws CayenneRuntimeException if
     * relationship is "to one".
     */
    public Map<String, Object> srcPkSnapshotWithTargetSnapshot(Map<String, Object> targetSnapshot) {
        if (!isToMany()) {
            throw new CayenneRuntimeException("Only 'to many' relationships support this method.");
        }

        return srcSnapshotWithTargetSnapshot(targetSnapshot);
    }

    /**
     * Sets relationship multiplicity.
     */
    public void setToMany(boolean toMany) {
        this.toMany = toMany;
    }

    @Override
    public boolean isMandatory() {
        for (DbJoin join : getJoins()) {
            if (join.getSource().isMandatory()) {
                return true;
            }
        }

        return false;
    }

    static final class JoinTransformers {

        static final Transformer targetExtractor = new Transformer() {

            public Object transform(Object input) {
                return (input instanceof DbJoin) ? ((DbJoin) input).getTarget() : input;
            }
        };

        static final Transformer sourceExtractor = new Transformer() {

            public Object transform(Object input) {
                return (input instanceof DbJoin) ? ((DbJoin) input).getSource() : input;
            }
        };
    }

    // a join used for comparison
    static final class TestJoin extends DbJoin {

        TestJoin(DbRelationship relationship) {
            super(relationship);
        }

        @Override
        public boolean equals(Object o) {
            if (o == null) {
                return false;
            }

            if (o == this) {
                return true;
            }

            if (!(o instanceof DbJoin)) {
                return false;
            }

            DbJoin j = (DbJoin) o;
            return j.relationship == this.relationship && Util.nullSafeEquals(j.sourceName, this.sourceName)
                    && Util.nullSafeEquals(j.targetName, this.targetName);
        }
    }

    public String toString() {
        StringBuilder res = new StringBuilder("Db Relationship : ");
        res.append(toMany ? "toMany" : "toOne ");

        String sourceEntityName = getSourceEntityName();
        for (DbJoin join : joins) {
            res.append(" (").append(sourceEntityName).append(".").append(join.getSourceName()).append(", ")
                    .append(targetEntityName).append(".").append(join.getTargetName()).append(")");
        }
        return res.toString();
    }

    public String getSourceEntityName() {
        if (this.sourceEntity == null) {
            return null;
        }
        return this.sourceEntity.name;
    }
}