org.apache.cayenne.map.ObjRelationship Maven / Gradle / Ivy
/*****************************************************************
* 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 java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.configuration.ConfigurationNode;
import org.apache.cayenne.configuration.ConfigurationNodeVisitor;
import org.apache.cayenne.exp.ExpressionException;
import org.apache.cayenne.exp.parser.ASTDbPath;
import org.apache.cayenne.util.CayenneMapEntry;
import org.apache.cayenne.util.ToStringBuilder;
import org.apache.cayenne.util.Util;
import org.apache.cayenne.util.XMLEncoder;
/**
* Describes an association between two Java classes mapped as source and target
* ObjEntity. Maps to a path of DbRelationships.
*/
public class ObjRelationship extends Relationship implements ConfigurationNode {
/**
* Denotes a default type of to-many relationship collection which is a Java
* List.
*
* @since 3.0
*/
public static final String DEFAULT_COLLECTION_TYPE = "java.util.List";
boolean readOnly;
protected int deleteRule = DeleteRule.NO_ACTION;
protected boolean usedForLocking;
protected List dbRelationships = new ArrayList(
2);
/**
* Db-relationships path that is set but not yet parsed (turned into
* List) Used during map loading
*/
String deferredPath;
/**
* Stores the type of collection mapped by a to-many relationship. Null for
* to-one relationships.
*
* @since 3.0
*/
protected String collectionType;
/**
* Stores a property name of a target entity used to create a relationship
* map. Only has effect if collectionType property is set to
* "java.util.Map".
*
* @since 3.0
*/
protected String mapKey;
public ObjRelationship() {
this(null);
}
public ObjRelationship(String name) {
super(name);
}
/**
* @since 3.1
*/
public T acceptVisitor(ConfigurationNodeVisitor visitor) {
return visitor.visitObjRelationship(this);
}
/**
* Prints itself as XML to the provided XMLEncoder.
*
* @since 1.1
*/
public void encodeAsXML(XMLEncoder encoder) {
ObjEntity source = (ObjEntity) getSourceEntity();
if (source == null) {
return;
}
encoder.print(" ");
}
/**
* Returns a target ObjEntity of this relationship. Entity is looked up in
* the parent DataMap using "targetEntityName".
*/
@Override
public Entity getTargetEntity() {
String targetName = getTargetEntityName();
if (targetName == null) {
return null;
}
return getNonNullNamespace().getObjEntity(targetName);
}
/**
* Returns the name of a complimentary relationship going in the opposite
* direction or null if it doesn't exist.
*
* @since 1.2
*/
public String getReverseRelationshipName() {
ObjRelationship reverse = getReverseRelationship();
return (reverse != null) ? reverse.getName() : null;
}
/**
* Returns a "complimentary" ObjRelationship going in the opposite
* direction. Returns null if no such relationship is found.
*/
@Override
public ObjRelationship getReverseRelationship() {
// reverse the list
List relationships = getDbRelationships();
List reversed = new ArrayList(
relationships.size());
for (DbRelationship relationship : relationships) {
DbRelationship reverse = relationship.getReverseRelationship();
if (reverse == null) {
return null;
}
reversed.add(0, reverse);
}
ObjEntity target = (ObjEntity) this.getTargetEntity();
if (target == null) {
return null;
}
Entity source = getSourceEntity();
for (ObjRelationship relationship : target.getRelationships()) {
if (relationship.getTargetEntity() != source) {
continue;
}
List> otherRels = relationship.getDbRelationships();
if (reversed.size() != otherRels.size()) {
continue;
}
int len = reversed.size();
boolean relsMatch = true;
for (int i = 0; i < len; i++) {
if (otherRels.get(i) != reversed.get(i)) {
relsMatch = false;
break;
}
}
if (relsMatch) {
return relationship;
}
}
return null;
}
/**
* Creates a complimentary reverse relationship from target entity to the
* source entity. A new relationship is created regardless of whether one
* already exists. Returned relationship is not attached to the source
* entity and has no name. Throws a {@link CayenneRuntimeException} if
* reverse DbRelationship is not mapped.
*
* @since 3.0
*/
public ObjRelationship createReverseRelationship() {
ObjRelationship reverse = new ObjRelationship();
reverse.setSourceEntity(getTargetEntity());
reverse.setTargetEntityName(getSourceEntity().getName());
reverse.setDbRelationshipPath(getReverseDbRelationshipPath());
return reverse;
}
/**
* Returns an immutable list of underlying DbRelationships.
*/
public List getDbRelationships() {
refreshFromDeferredPath();
return Collections.unmodifiableList(dbRelationships);
}
/**
* Appends a DbRelationship to the existing list of DbRelationships.
*/
public void addDbRelationship(DbRelationship dbRel) {
refreshFromDeferredPath();
if (dbRel.getName() == null) {
throw new IllegalArgumentException("DbRelationship has no name");
}
// Adding a second is creating a flattened relationship.
// Ensure that the new relationship properly continues
// on the flattened path
int numDbRelationships = dbRelationships.size();
if (numDbRelationships > 0) {
DbRelationship lastRel = dbRelationships
.get(numDbRelationships - 1);
if (!lastRel.getTargetEntityName().equals(
dbRel.getSourceEntity().getName())) {
throw new CayenneRuntimeException(
"Error adding db relationship "
+ dbRel
+ " to ObjRelationship "
+ this
+ " because the source of the newly added relationship "
+ "is not the target of the previous relationship "
+ "in the chain");
}
}
dbRelationships.add(dbRel);
this.recalculateReadOnlyValue();
this.recalculateToManyValue();
}
/**
* Removes the relationship dbRel
from the list of
* relationships.
*/
public void removeDbRelationship(DbRelationship dbRel) {
refreshFromDeferredPath();
if (dbRelationships.remove(dbRel)) {
this.recalculateReadOnlyValue();
this.recalculateToManyValue();
}
}
public void clearDbRelationships() {
deferredPath = null;
this.dbRelationships.clear();
this.readOnly = false;
this.toMany = false;
}
/**
* Returns a boolean indicating whether the presence of a non-null source
* key(s) will not guarantee a presence of a target record. PK..FK
* relationships are all optional, but there are other more subtle cases,
* such as PK..PK, etc.
*
* @since 3.0
*/
public boolean isOptional() {
if (isToMany() || isFlattened()) {
return true;
}
// entities with qualifiers may result in filtering even existing target
// rows, so
// such relationships are optional
if (isQualifiedEntity((ObjEntity) getTargetEntity())) {
return true;
}
DbRelationship dbRelationship = getDbRelationships().get(0);
// to-one mandatory relationships are either from non-PK or to master pk
if (dbRelationship.isToPK()) {
if (!dbRelationship.isFromPK()) {
return false;
}
DbRelationship reverseRelationship = dbRelationship
.getReverseRelationship();
if (reverseRelationship.isToDependentPK()) {
return false;
}
}
return true;
}
/**
* Returns true if the relationship is non-optional and target has no
* subclasses.
*
* @since 3.0
*/
public boolean isSourceDefiningTargetPrecenseAndType(
EntityResolver entityResolver) {
if (isOptional()) {
return false;
}
EntityInheritanceTree inheritanceTree = entityResolver
.lookupInheritanceTree(getTargetEntityName());
return inheritanceTree == null
|| inheritanceTree.getChildren().isEmpty();
}
/**
* Returns true if the entity or its super entities have a limiting
* qualifier.
*/
private boolean isQualifiedEntity(ObjEntity entity) {
if (entity.getDeclaredQualifier() != null) {
return true;
}
entity = entity.getSuperEntity();
if (entity == null) {
return false;
}
return isQualifiedEntity(entity);
}
/**
* 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 1.1
*/
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() || isFlattened() || isToDependentEntity()
|| !isToPK();
}
/**
* Returns true if underlying DbRelationships point to dependent entity.
*/
public boolean isToDependentEntity() {
return (getDbRelationships().get(0)).isToDependentPK();
}
/**
* Returns true if the underlying DbRelationships point to a at least one of
* the columns of the target entity.
*
* @since 1.1
*/
public boolean isToPK() {
return (getDbRelationships().get(0)).isToPK();
}
/**
* Returns true if the relationship is a "flattened" relationship. A
* relationship is considered "flattened" if it maps to more than one
* DbRelationship. Such chain of DbRelationships is also called
* "relationship path". All flattened relationships are at least readable,
* but only those formed across a many-many join table (with no custom
* attributes other than foreign keys) can be automatically written.
*
* @see #isReadOnly
* @return flag indicating if the relationship is flattened or not.
*/
public boolean isFlattened() {
return getDbRelationships().size() > 1;
}
/**
* Returns true if the relationship is flattened, but is not of the single
* case that can have automatic write support. Otherwise, it returns false.
*
* @return flag indicating if the relationship is read only or not
*/
public boolean isReadOnly() {
refreshFromDeferredPath();
recalculateReadOnlyValue();
return readOnly;
}
@Override
public boolean isToMany() {
refreshFromDeferredPath();
recalculateToManyValue();
return super.isToMany();
}
/**
* Returns the deleteRule. The delete rule is a constant from the DeleteRule
* class, and specifies what should happen to the destination object when
* the source object is deleted.
*
* @return int a constant from DeleteRule
* @see #setDeleteRule
*/
public int getDeleteRule() {
return deleteRule;
}
/**
* Sets the delete rule of the relationship.
*
* @param value
* New delete rule. Must be one of the constants defined in
* DeleteRule class.
* @see DeleteRule
* @throws IllegalArgumentException
* if the value is not a valid delete rule.
*/
public void setDeleteRule(int value) {
if ((value != DeleteRule.CASCADE) && (value != DeleteRule.DENY)
&& (value != DeleteRule.NULLIFY)
&& (value != DeleteRule.NO_ACTION)) {
throw new IllegalArgumentException("Delete rule value " + value
+ " is not a constant from the DeleteRule class");
}
this.deleteRule = value;
}
/**
* Returns whether this attribute should be used for locking.
*
* @since 1.1
*/
public boolean isUsedForLocking() {
return usedForLocking;
}
/**
* Sets whether this attribute should be used for locking.
*
* @since 1.1
*/
public void setUsedForLocking(boolean usedForLocking) {
this.usedForLocking = usedForLocking;
}
/**
* Returns a dot-separated path over mapped DbRelationships.
*
* @since 1.1
*/
public String getDbRelationshipPath() {
refreshFromDeferredPath();
// build path on the fly
if (getDbRelationships().isEmpty()) {
return null;
}
StringBuilder path = new StringBuilder();
Iterator it = getDbRelationships().iterator();
while (it.hasNext()) {
DbRelationship next = it.next();
path.append(next.getName());
if (it.hasNext()) {
path.append(Entity.PATH_SEPARATOR);
}
}
return path.toString();
}
/**
* Returns a reversed dbRelationship path.
*
* @since 1.2
*/
public String getReverseDbRelationshipPath() throws ExpressionException {
List relationships = getDbRelationships();
if (relationships == null || relationships.isEmpty()) {
return null;
}
StringBuilder buffer = new StringBuilder();
// iterate in reverse order
ListIterator it = relationships
.listIterator(relationships.size());
while (it.hasPrevious()) {
DbRelationship relationship = it.previous();
DbRelationship reverse = relationship.getReverseRelationship();
// another sanity check
if (reverse == null) {
throw new CayenneRuntimeException(
"No reverse relationship exist for " + relationship);
}
if (buffer.length() > 0) {
buffer.append(Entity.PATH_SEPARATOR);
}
buffer.append(reverse.getName());
}
return buffer.toString();
}
/**
* Sets mapped DbRelationships as a dot-separated path.
*/
public void setDbRelationshipPath(String relationshipPath) {
if (!Util.nullSafeEquals(getDbRelationshipPath(), relationshipPath)) {
refreshFromPath(relationshipPath, false);
}
}
/**
* Sets relationship path, but does not trigger its conversion to
* List For internal purposes, primarily datamap loading
*/
void setDeferredDbRelationshipPath(String relationshipPath) {
if (!Util.nullSafeEquals(getDbRelationshipPath(), relationshipPath)) {
deferredPath = relationshipPath;
}
}
/**
* Loads path from "deferredPath" variable (if specified)
*/
void refreshFromDeferredPath() {
if (deferredPath != null) {
synchronized(this) {
// check if another thread just
// loaded path from deferredPath
if (deferredPath != null){
refreshFromPath(deferredPath, true);
deferredPath = null;
}
}
}
}
/**
* Returns dot-separated path over DbRelationships, only including
* components that have valid DbRelationships.
*/
String getValidRelationshipPath() {
String path = getDbRelationshipPath();
if (path == null) {
return null;
}
ObjEntity entity = (ObjEntity) getSourceEntity();
if (entity == null) {
throw new CayenneRuntimeException(
"Can't resolve DbRelationships, null source ObjEntity");
}
DbEntity dbEntity = entity.getDbEntity();
if (dbEntity == null) {
return null;
}
StringBuilder validPath = new StringBuilder();
try {
for (PathComponent pathComponent : dbEntity
.resolvePath(new ASTDbPath(path), Collections.emptyMap())) {
if (validPath.length() > 0) {
validPath.append(Entity.PATH_SEPARATOR);
}
validPath.append(pathComponent.getName());
}
} catch (ExpressionException ex) {
}
return validPath.toString();
}
/**
* Rebuild a list of relationships if String relationshipPath has changed.
*/
final void refreshFromPath(String dbRelationshipPath, boolean stripInvalid) {
// remove existing relationships
dbRelationships.clear();
if (dbRelationshipPath != null) {
ObjEntity entity = (ObjEntity) getSourceEntity();
if (entity == null) {
throw new CayenneRuntimeException(
"Can't resolve DbRelationships, null source ObjEntity");
}
try {
// add new relationships from path
Iterator it = entity
.resolvePathComponents(new ASTDbPath(
dbRelationshipPath));
while (it.hasNext()) {
DbRelationship relationship = (DbRelationship) it
.next();
dbRelationships.add(relationship);
}
} catch (ExpressionException ex) {
if (!stripInvalid) {
throw ex;
}
}
}
recalculateToManyValue();
recalculateReadOnlyValue();
}
/**
* Recalculates whether a relationship is toMany or toOne, based on the
* underlying db relationships.
*/
public void recalculateToManyValue() {
// If there is a single toMany along the path, then the flattend
// rel is toMany. If all are toOne, then the rel is toOne.
// Simple (non-flattened) relationships form the degenerate case
// taking the value of the single underlying dbrel.
for (DbRelationship thisRel : this.dbRelationships) {
if (thisRel.isToMany()) {
this.toMany = true;
return;
}
}
this.toMany = false;
}
/**
* Recalculates a new readonly value based on the underlying
* DbRelationships.
*/
public void recalculateReadOnlyValue() {
// not flattened, always read/write
if (dbRelationships.size() < 2) {
this.readOnly = false;
return;
}
// too long, can't handle this yet
if (dbRelationships.size() > 2) {
this.readOnly = true;
return;
}
DbRelationship firstRel = dbRelationships.get(0);
DbRelationship secondRel = dbRelationships.get(1);
// many-to-many with single-step join
// also 1..1..1 (CAY-1744) .. TODO all this should go away eventually
// per CAY-1743
if (!secondRel.isToMany()) {
// allow modifications if the joins are from FKs
if (!secondRel.isToPK()) {
this.readOnly = true;
return;
}
DbRelationship firstReverseRel = firstRel.getReverseRelationship();
if (firstReverseRel == null || !firstReverseRel.isToPK()) {
this.readOnly = true;
return;
}
this.readOnly = false;
}
else {
readOnly = true;
}
}
@Override
public String toString() {
return new ToStringBuilder(this).append("name", getName())
.append("dbRelationshipPath", getDbRelationshipPath())
.toString();
}
/**
* Returns an ObjAttribute stripped of any server-side information, such as
* DbAttribute mapping.
*
* @since 1.2
*/
public ObjRelationship getClientRelationship() {
ObjRelationship reverse = getReverseRelationship();
String reverseName = reverse != null ? reverse.getName() : null;
ObjRelationship relationship = new ClientObjRelationship(getName(),
reverseName, isToMany(), isReadOnly());
relationship.setTargetEntityName(getTargetEntityName());
relationship.setDeleteRule(getDeleteRule());
relationship.setCollectionType(getCollectionType());
// TODO: copy locking flag...
return relationship;
}
/**
* Returns the interface of collection mapped by a to-many relationship.
* Returns null for to-one relationships. Default for to-many is
* "java.util.List". Other possible values are "java.util.Set",
* "java.util.Collection", "java.util.Map".
*
* @since 3.0
*/
public String getCollectionType() {
if (collectionType != null) {
return collectionType;
}
return isToMany() ? DEFAULT_COLLECTION_TYPE : null;
}
/**
* @since 3.0
*/
public void setCollectionType(String collectionType) {
this.collectionType = collectionType;
}
/**
* Returns a property name of a target entity used to create a relationship
* map. Only has effect if collectionType property is set to
* "java.util.Map".
*
* @return The attribute name used for the map key or null
if
* the default (PK) is used as the map key.
* @since 3.0
*/
public String getMapKey() {
return mapKey;
}
/**
* @since 3.0
*/
public void setMapKey(String mapKey) {
this.mapKey = mapKey;
}
@Override
public boolean isMandatory() {
refreshFromDeferredPath();
if (dbRelationships.size() == 0) {
return false;
}
return dbRelationships.get(0).isMandatory();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy