/*****************************************************************
* 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.exp.ExpressionException;
import org.apache.cayenne.exp.parser.ASTDbPath;
import org.apache.cayenne.map.event.RelationshipEvent;
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 {
/**
* 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<DbRelationship> dbRelationships = new ArrayList<DbRelationship>(2);
/**
* Db-relationships path that is set but not yet parsed (turned into List<DbRelationship>)
* 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);
}
/**
* 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("<obj-relationship name=\"" + getName());
encoder.print("\" source=\"" + source.getName());
// looking up a target entity ensures that bogus names are not saved... whether
// this is good or bad is debatable, as users may want to point to non-existent
// entities on purpose.
ObjEntity target = (ObjEntity) getTargetEntity();
if (target != null) {
encoder.print("\" target=\"" + target.getName());
}
if (getCollectionType() != null
&& !DEFAULT_COLLECTION_TYPE.equals(getCollectionType())) {
encoder.print("\" collection-type=\"" + getCollectionType());
}
if (getMapKey() != null) {
encoder.print("\" map-key=\"" + getMapKey());
}
if (isUsedForLocking()) {
encoder.print("\" lock=\"true");
}
String deleteRule = DeleteRule.deleteRuleName(getDeleteRule());
if (getDeleteRule() != DeleteRule.NO_ACTION && deleteRule != null) {
encoder.print("\" deleteRule=\"" + deleteRule);
}
// quietly get rid of invalid path... this is not the best way of doing things,
// but it is consistent across map package
String path = getValidRelationshipPath();
if (path != null) {
encoder.print("\" db-relationship-path=\"" + path);
}
encoder.println("\"/>");
}
/**
* 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.
*/
public ObjRelationship getReverseRelationship() {
// reverse the list
List<DbRelationship> relationships = getDbRelationships();
List<DbRelationship> reversed = new ArrayList<DbRelationship>(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<DbRelationship> 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 <code>dbRel</code> 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) {
return !isOptional()
&& entityResolver.lookupInheritanceTree(getTargetEntityName()) == null;
}
/**
* 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;
}
/**
* @deprecated since 3.0 as ObjRelationship no longer reacts to DbRelationship events.
*/
@Deprecated
public void dbRelationshipDidChange(RelationshipEvent event) {
recalculateToManyValue();
recalculateReadOnlyValue();
}
/**
* 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<DbRelationship> 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<DbRelationship> relationships = getDbRelationships();
if (relationships == null || relationships.isEmpty()) {
return null;
}
StringBuilder buffer = new StringBuilder();
// iterate in reverse order
ListIterator<DbRelationship> 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<DbRelationship>
* For internal purposes, primarily datamap loading
*/
void setDeferredDbRelationshipPath(String relationshipPath) {
if (!Util.nullSafeEquals(getDbRelationshipPath(), relationshipPath)) {
deferredPath = relationshipPath;
}
}
/**
* Loads path from "deferredPath" variable (if specified)
*/
synchronized void refreshFromDeferredPath() {
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<DbAttribute, DbRelationship> 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) {
synchronized (this) {
// 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<CayenneMapEntry> 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);
// only support many-to-many with single-step join
if (!firstRel.isToMany() || secondRel.isToMany()) {
this.readOnly = true;
return;
}
DataMap map = firstRel.getTargetEntity().getDataMap();
if (map == null) {
throw new CayenneRuntimeException(this.getClass().getName()
+ " could not obtain a DataMap for the destination of "
+ firstRel.getName());
}
// 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;
}
@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 <code>null</code> 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;
}
}