/*****************************************************************
* 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;
}
}