All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.cayenne.map.ObjRelationship Maven / Gradle / Ivy

There is a newer version: 4.2.1
Show newest version
/*****************************************************************
 *   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