/*
 * ====================================================================
 * 
 * The ObjectStyle Group Software License, Version 1.0
 * 
 * Copyright (c) 2006 The ObjectStyle Group and individual authors of the
 * software. All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 * 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * 
 * 3. The end-user documentation included with the redistribution, if any, must
 * include the following acknowlegement: "This product includes software
 * developed by the ObjectStyle Group (http://objectstyle.org/)." Alternately,
 * this acknowlegement may appear in the software itself, if and wherever such
 * third-party acknowlegements normally appear.
 * 
 * 4. The names "ObjectStyle Group" and "Cayenne" must not be used to endorse or
 * promote products derived from this software without prior written permission.
 * For written permission, please contact andrus@objectstyle.org.
 * 
 * 5. Products derived from this software may not be called "ObjectStyle" nor
 * may "ObjectStyle" appear in their names without prior written permission of
 * the ObjectStyle Group.
 * 
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * OBJECTSTYLE GROUP OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
 * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * ====================================================================
 * 
 * This software consists of voluntary contributions made by many individuals on
 * behalf of the ObjectStyle Group. For more information on the ObjectStyle
 * Group, please see <http://objectstyle.org/>.
 *  
 */
package org.objectstyle.wolips.eomodeler.core.model;

import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.objectstyle.wolips.baseforplugins.util.ComparisonUtils;
import org.objectstyle.wolips.baseforplugins.util.StringUtils;
import org.objectstyle.wolips.eomodeler.core.Messages;
import org.objectstyle.wolips.eomodeler.core.utils.BooleanUtils;

public class EORelationship extends UserInfoableEOModelObject<EOEntity> implements IEOAttribute, ISortableEOModelObject {
	public static final String TO_MANY = "toMany";

	public static final String TO_ONE = "toOne";

	public static final String CLASS_PROPERTY = "classProperty";

	public static final String CLIENT_CLASS_PROPERTY = "clientClassProperty";

	public static final String COMMON_CLASS_PROPERTY = "commonClassProperty";

	public static final String NAME = "name";

	public static final String DESTINATION = "destination";

	public static final String DEFINITION = "definition";

	public static final String DELETE_RULE = "deleteRule";

	public static final String JOIN_SEMANTIC = "joinSemantic";

	public static final String OPTIONAL = "optional";

	public static final String MANDATORY = "mandatory";

	public static final String OWNS_DESTINATION = "ownsDestination";

	public static final String PROPAGATES_PRIMARY_KEY = "propagatesPrimaryKey";

	public static final String NUMBER_OF_TO_MANY_FAULTS_TO_BATCH_FETCH = "numberOfToManyFaultsToBatchFetch";

	public static final String JOINS = "joins";

	public static final String JOIN = "join";

	private EOEntity myEntity;

	private EOEntity myDestination;

	private String myName;

	private String myDefinition;

	private EORelationshipPath myDefinitionPath;

	private Boolean myMandatory;

	private Boolean myToMany;

	private Boolean myOwnsDestination;

	private Boolean myPropagatesPrimaryKey;

	private Boolean myClassProperty;

	private Boolean myClientClassProperty;

	private Boolean _commonClassProperty;

	private Integer myNumberOfToManyFaultsToBatchFetch;

	private EODeleteRule myDeleteRule;

	private EOJoinSemantic myJoinSemantic;

	private List<EOJoin> myJoins;

	private EOModelMap myRelationshipMap;

	private EOEntity myEntityBeforeCloning;

	public EORelationship() {
		myJoins = new LinkedList<EOJoin>();
		myRelationshipMap = new EOModelMap();
		myDeleteRule = EODeleteRule.getDeleteRuleByID(null);
		myJoinSemantic = EOJoinSemantic.getJoinSemanticByID(null);
	}

	public EORelationship(String _name) {
		this();
		myName = _name;
	}

	public EORelationship(String _name, String _definition) {
		this(_name);
		myDefinition = _definition;
	}

	public Set<EOModelReferenceFailure> getReferenceFailures() {
		Set<EOModelReferenceFailure> referenceFailures = new HashSet<EOModelReferenceFailure>();
		for (EORelationship referencingRelationship : getReferencingFlattenedRelationships()) {
			referenceFailures.add(new EOFlattenedRelationshipRelationshipReferenceFailure(referencingRelationship, this));
		}
		for (EOAttribute referencingAttributes : getReferencingFlattenedAttributes()) {
			referenceFailures.add(new EOFlattenedAttributeRelationshipReferenceFailure(referencingAttributes, this));
		}
		return referenceFailures;
	}

	public List<EOAttribute> getReferencingFlattenedAttributes() {
		List<EOAttribute> referencingFlattenedAttributes = new LinkedList<EOAttribute>();
		if (myEntity != null) {
			for (EOModel model : getEntity().getModel().getModelGroup().getModels()) {
				for (EOEntity entity : model.getEntities()) {
					for (EOAttribute attribute : entity.getAttributes()) {
						if (attribute.isFlattened()) {
							EOAttributePath attributePath = attribute.getDefinitionPath();
							if (attributePath != null && attributePath.isRelatedTo(this)) {
								referencingFlattenedAttributes.add(attribute);
							}
						}
					}
				}
			}
		}
		return referencingFlattenedAttributes;
	}

	public List<EORelationship> getReferencingFlattenedRelationships() {
		List<EORelationship> referencingFlattenedRelationships = new LinkedList<EORelationship>();
		if (myEntity != null) {
			for (EOModel model : getEntity().getModel().getModelGroup().getModels()) {
				for (EOEntity entity : model.getEntities()) {
					for (EORelationship relationship : entity.getRelationships()) {
						if (relationship.isFlattened()) {
							EORelationshipPath relationshipPath = relationship.getDefinitionPath();
							if (relationshipPath != null && relationshipPath.isRelatedTo(this)) {
								referencingFlattenedRelationships.add(relationship);
							}
						}
					}
				}
			}
		}
		return referencingFlattenedRelationships;
	}

	public void pasted() throws DuplicateNameException {
		if (myEntityBeforeCloning != null) {
			if (myDestination == myEntityBeforeCloning) {
				myDestination = myEntity;
			} else {
				if (myDestination != null) {
					EOModel model = myEntity.getModel();
					EOModelGroup modelGroup = model.getModelGroup();
					myDestination = modelGroup.getEntityNamed(myDestination.getName());
				}
			}
			for (EOJoin join : myJoins) {
				join.pasted();
			}
			myEntityBeforeCloning = null;
		}
	}

	@SuppressWarnings("unused")
	protected void _joinChanged(EOJoin _join, String _propertyName, Object _oldValue, Object _newValue) {
		firePropertyChange(EORelationship.JOIN, null, _join);
	}

	protected void _propertyChanged(String _propertyName, Object _oldValue, Object _newValue) {
		if (myEntity != null) {
			myEntity._relationshipChanged(this, _propertyName, _oldValue, _newValue);
		}
	}

//	public int hashCode() {
//		return ((myEntity == null) ? 1 : myEntity.hashCode()) * ((myName == null) ? 1 : myName.hashCode());
//	}
//
//	public boolean equals(Object _obj) {
//		boolean equals = false;
//		if (_obj instanceof EORelationship) {
//			EORelationship relationship = (EORelationship) _obj;
//			equals = (relationship == this) || (ComparisonUtils.equals(relationship.myEntity, myEntity) && ComparisonUtils.equals(relationship.myName, myName));
//			if (equals && _obj != this) {
//				throw new IllegalStateException("RUH ROH!");
//			}
//		}
//		return equals;
//	}

	public boolean isInverseRelationship(EORelationship _relationship) {
		boolean isInverse;
		if (_relationship == null) {
			isInverse = false;
		} else {
			List<EOJoin> inverseJoins = new LinkedList<EOJoin>(_relationship.getJoins());
			if (inverseJoins.size() != myJoins.size()) {
				isInverse = false;
			} else {
				Iterator<EOJoin> joinsIter = myJoins.iterator();
				isInverse = true;
				while (isInverse && joinsIter.hasNext()) {
					EOJoin join = joinsIter.next();
					EOJoin inverseJoin = null;
					int inverseJoinsSize = inverseJoins.size();
					for (int inverseJoinNum = 0; inverseJoin == null && inverseJoinNum < inverseJoinsSize; inverseJoinNum++) {
						EOJoin potentialInverseJoin = inverseJoins.get(inverseJoinNum);
						if (potentialInverseJoin.isInverseJoin(join)) {
							inverseJoin = potentialInverseJoin;
						}
					}
					if (inverseJoin == null) {
						isInverse = false;
					}
				}
			}
		}
		return isInverse;
	}

	public EORelationship getInverseRelationship() {
		EORelationship inverseRelationship = null;
		if (myDestination != null) {
			Iterator<EORelationship> relationshipsIter = myDestination.getRelationships().iterator();
			while (inverseRelationship == null && relationshipsIter.hasNext()) {
				EORelationship potentialInverseRelationship = relationshipsIter.next();
				if (potentialInverseRelationship.isInverseRelationship(this)) {
					inverseRelationship = potentialInverseRelationship;
				}
			}
		}
		return inverseRelationship;
	}

	public EORelationship createInverseRelationshipNamed(String _name, boolean _toMany) {
		EORelationship inverseRelationship = new EORelationship(myDestination.findUnusedRelationshipName(_name));
		inverseRelationship.setClassProperty(getClassProperty());
		inverseRelationship.setToMany(Boolean.valueOf(_toMany));
		inverseRelationship.setDestination(myEntity);
		for (EOJoin join : getJoins()) {
			join.addInverseJoinInto(inverseRelationship, false);
		}
		inverseRelationship._setEntity(myDestination);
		return inverseRelationship;
	}

	public boolean isRelatedTo(EOEntity _entity) {
		return _entity.equals(myDestination);
	}

	public boolean isRelatedTo(EOAttribute _attribute) {
		boolean isRelated = false;
		Iterator<EOJoin> joinsIter = myJoins.iterator();
		while (!isRelated && joinsIter.hasNext()) {
			EOJoin join = joinsIter.next();
			isRelated = join.isRelatedTo(_attribute);
		}
		return isRelated;
	}

	public Set<EOAttribute> getRelatedAttributes() {
		Set<EOAttribute> relatedAttributes = new HashSet<EOAttribute>();
		Iterator<EOJoin> joinsIter = myJoins.iterator();
		while (joinsIter.hasNext()) {
			EOJoin join = joinsIter.next();
			EOAttribute sourceAttribute = join.getSourceAttribute();
			if (sourceAttribute != null) {
				relatedAttributes.add(sourceAttribute);
			}
			EOAttribute destinationAttribute = join.getDestinationAttribute();
			if (destinationAttribute != null) {
				relatedAttributes.add(destinationAttribute);
			}
		}
		return relatedAttributes;
	}

	public void setDefinition(String _definition) {
		String oldDefinition = myDefinition;
		myDefinition = _definition;
		updateDefinitionPath();
		firePropertyChange(EORelationship.DEFINITION, oldDefinition, myDefinition);
	}

	public String getDefinition() {
		String definition;
		if (isFlattened() && myDefinitionPath != null) {
			definition = myDefinitionPath.toKeyPath();
		} else {
			definition = _getDefinition();
		}
		return definition;
	}

	public EORelationshipPath getDefinitionPath() {
		if (myDefinitionPath == null) {
			updateDefinitionPath();
		}
		return myDefinitionPath;
	}

	public String _getDefinition() {
		return myDefinition;
	}

	public void updateDefinitionBecauseRelationshipNameChanged(EORelationship relationship) {
		if (isFlattened()) {
			EORelationshipPath definitionPath = getDefinitionPath();
			if (definitionPath != null && definitionPath.isRelatedTo(relationship)) {
				setDefinition(definitionPath.toKeyPath());
			}
		}
	}

	protected void updateDefinitionPath() {
		if (isFlattened()) {
			EOEntity entity = getEntity();
			if (entity != null) {
				AbstractEOAttributePath definitionPath = entity.resolveKeyPath(_getDefinition());
				if (definitionPath instanceof EORelationshipPath && definitionPath.isValid()) {
					myDefinitionPath = (EORelationshipPath) definitionPath;
				} else {
					myDefinitionPath = null;
				}
			}
			else {
				myDefinition = null;
			}
		} else {
			myDefinitionPath = null;
		}
	}

	public void setClassProperty(Boolean _classProperty) {
		setClassProperty(_classProperty, true);
	}

	public void setClassProperty(Boolean _classProperty, boolean _fireEvents) {
		Boolean oldClassProperty = myClassProperty;
		myClassProperty = _classProperty;
		if (_fireEvents) {
			firePropertyChange(EORelationship.CLASS_PROPERTY, oldClassProperty, myClassProperty);
		}
	}

	public Boolean isClassProperty() {
		return myClassProperty;
	}

	public Boolean getClassProperty() {
		return isClassProperty();
	}

	public void setCommonClassProperty(Boolean commonClassProperty) {
    setCommonClassProperty(commonClassProperty, true);
  }

  public void setCommonClassProperty(Boolean commonClassProperty, boolean fireEvents) {
    Boolean oldCommonClassProperty = _commonClassProperty;
    _commonClassProperty = commonClassProperty;
    if (fireEvents) {
      firePropertyChange(EORelationship.COMMON_CLASS_PROPERTY, oldCommonClassProperty, _commonClassProperty);
    }
  }

  public Boolean isCommonClassProperty() {
    return _commonClassProperty;
  }

  public Boolean getCommonClassProperty() {
    return isCommonClassProperty();
  }

	public void setClientClassProperty(Boolean _clientClassProperty) {
		setClientClassProperty(_clientClassProperty, true);
	}

	public void setClientClassProperty(Boolean _clientClassProperty, boolean _fireEvents) {
		Boolean oldClientClassProperty = myClientClassProperty;
		myClientClassProperty = _clientClassProperty;
		if (_fireEvents) {
			firePropertyChange(EORelationship.CLIENT_CLASS_PROPERTY, oldClientClassProperty, myClientClassProperty);
		}
	}

	public Boolean isClientClassProperty() {
		return myClientClassProperty;
	}

	public Boolean getClientClassProperty() {
		return isClientClassProperty();
	}

	public boolean isFlattened() {
		return StringUtils.isKeyPath(_getDefinition());
	}

	public EORelationship getParentRelationship() {
		EORelationship parentRelationship = null;
		EOEntity parent = myEntity.getParent();
		if (parent != null) {
			parentRelationship = parent.getRelationshipNamed(myName);
		}
		return parentRelationship;
	}

	public boolean isInherited() {
		return getParentRelationship() != null;
	}

	public void _setEntity(EOEntity _entity) {
		myEntity = _entity;
	}

	public EOEntity getEntity() {
		return myEntity;
	}

	public void setName(String _name) throws DuplicateNameException {
		setName(_name, true);
	}

	public void setName(String _name, boolean _fireEvents) throws DuplicateNameException {
		if (_name == null) {
			throw new NullPointerException(Messages.getString("EORelationship.noBlankRelationshipNames"));
		}
		String oldName = myName;
		if (myEntity != null) {
			myEntity._checkForDuplicateRelationshipName(this, _name, null);
		}
		myName = _name;
		if (myEntity != null && myEntity.getModel() != null) {
			EOModelGroup modelGroup = myEntity.getModel().getModelGroup();
			for (EOEntity entity : modelGroup.getEntities()) {
				for (EOAttribute attribute : entity.getAttributes()) {
					attribute.updateDefinitionBecauseRelationshipNameChanged(this);
				}
				for (EORelationship relationship : entity.getRelationships()) {
					relationship.updateDefinitionBecauseRelationshipNameChanged(this);
				}
			}
		}
		if (_fireEvents) {
			firePropertyChange(EORelationship.NAME, oldName, myName);
		}
	}

	public String getName() {
		return myName;
	}

	public String getUppercaseName() {
		return getName().toUpperCase();
	}
	
	public String getUppercaseUnderscoreName() {
		return StringUtils.camelCaseToUnderscore(getName()).toUpperCase();
	}

	public String getCapitalizedName() {
		String name = getName();
		if (name != null) {
			name = StringUtils.toUppercaseFirstLetter(name);
		}
		return name;
	}

	public EODeleteRule getDeleteRule() {
		return myDeleteRule;
	}

	public void setDeleteRule(EODeleteRule _deleteRule) {
		EODeleteRule oldDeleteRule = myDeleteRule;
		myDeleteRule = _deleteRule;
		firePropertyChange(EORelationship.DELETE_RULE, oldDeleteRule, myDeleteRule);
	}

	public EOEntity getActualDestination() {
		EOEntity destination = getDestination();
		if (destination != null && destination.isPartialEntitySet()) {
			destination = destination.getPartialEntity();
		}
		return destination;
	}

	public EOEntity getDestination() {
		EOEntity destination;
		if (isFlattened()) {
			AbstractEOAttributePath targetAttributePath = myEntity.resolveKeyPath(getDefinition());
			if (targetAttributePath != null && targetAttributePath.getChildIEOAttribute() != null) {
				destination = ((EORelationshipPath) targetAttributePath).getChildRelationship().getDestination();
			} else {
				destination = null;
			}
		} else {
			destination = myDestination;
		}
		return destination;
	}

	public void setDestination(EOEntity _destination) {
		setDestination(_destination, true);
	}

	public void setDestination(EOEntity _destination, boolean _fireEvents) {
		EOEntity oldDestination = myDestination;
		myDestination = _destination;
		if (_fireEvents) {
			firePropertyChange(EORelationship.DESTINATION, oldDestination, myDestination);
		}
	}

	public EOJoinSemantic getJoinSemantic() {
		return myJoinSemantic;
	}

	public void setJoinSemantic(EOJoinSemantic _joinSemantic) {
		EOJoinSemantic oldJoinSemantic = myJoinSemantic;
		myJoinSemantic = _joinSemantic;
		firePropertyChange(EORelationship.JOIN_SEMANTIC, oldJoinSemantic, myJoinSemantic);
	}

	public Boolean getMandatory() {
		return isMandatory();
	}

	public Boolean isMandatory() {
		return myMandatory;
	}

	public void setMandatory(Boolean _mandatory) {
		_setMandatory(_mandatory);
		if (BooleanUtils.isTrue(isToOne())) {
			EOEntity entity = getEntity();
			if (entity != null && !entity.isSingleTableInheritance()) {
				Iterator<EOJoin> joinsIter = getJoins().iterator();
				while (joinsIter.hasNext()) {
					EOJoin join = joinsIter.next();
					EOAttribute sourceAttribute = join.getSourceAttribute();
					if (sourceAttribute != null) {
						sourceAttribute._setAllowsNull(BooleanUtils.negate(_mandatory), true);
					}
				}
			}
		}
	}

	public void _setMandatory(Boolean _mandatory) {
		Boolean oldMandatory = myMandatory;
		myMandatory = _mandatory;
		firePropertyChange(EORelationship.MANDATORY, oldMandatory, myMandatory);
		firePropertyChange(EORelationship.OPTIONAL, BooleanUtils.negate(oldMandatory), BooleanUtils.negate(myMandatory));
	}

	public void setMandatoryIfNecessary() {
		boolean mandatory = false;
		if (BooleanUtils.isTrue(isToOne())) {
			Iterator<EOJoin> joinsIter = getJoins().iterator();
			while (!mandatory && joinsIter.hasNext()) {
				EOJoin join = joinsIter.next();
				EOAttribute sourceAttribute = join.getSourceAttribute();
				if (sourceAttribute != null) {
					mandatory = BooleanUtils.isFalse(sourceAttribute.isAllowsNull());
				}
			}
		}
		setMandatory(Boolean.valueOf(mandatory));
	}

	public Boolean getOptional() {
		return isOptional();
	}

	public Boolean isOptional() {
		return BooleanUtils.negate(isMandatory());
	}

	public void setOptional(Boolean _optional) {
		setMandatory(BooleanUtils.negate(_optional));
	}

	public Boolean getOwnsDestination() {
		return isOwnsDestination();
	}

	public Boolean isOwnsDestination() {
		return myOwnsDestination;
	}

	public void setOwnsDestination(Boolean _ownsDestination) {
		Boolean oldOwnsDestination = myOwnsDestination;
		myOwnsDestination = _ownsDestination;
		firePropertyChange(EORelationship.OWNS_DESTINATION, oldOwnsDestination, myOwnsDestination);
	}

	public Boolean getPropagatesPrimaryKey() {
		return isPropagatesPrimaryKey();
	}

	public Boolean isPropagatesPrimaryKey() {
		return myPropagatesPrimaryKey;
	}

	public void setPropagatesPrimaryKey(Boolean _propagatesPrimaryKey) {
		Boolean oldPropagatesPrimaryKey = myPropagatesPrimaryKey;
		myPropagatesPrimaryKey = _propagatesPrimaryKey;
		firePropertyChange(EORelationship.PROPAGATES_PRIMARY_KEY, oldPropagatesPrimaryKey, myPropagatesPrimaryKey);
	}

	public Boolean getToMany() {
		return isToMany();
	}

	public Boolean isToMany() {
		Boolean toMany = null;
		if (isFlattened() && myEntity != null) {
			AbstractEOAttributePath targetAttributePath = myEntity.resolveKeyPath(getDefinition());
			if (targetAttributePath != null && targetAttributePath.getChildIEOAttribute() != this) {
				toMany = targetAttributePath.isToMany();
			}
		} else {
			toMany = myToMany;
		}
		return toMany;
	}

	public void setToMany(Boolean _toMany) {
		if (!isFlattened()) {
			Boolean oldToMany = myToMany;
			myToMany = _toMany;
			firePropertyChange(EORelationship.TO_MANY, oldToMany, myToMany);
			firePropertyChange(EORelationship.TO_ONE, BooleanUtils.negate(oldToMany), BooleanUtils.negate(myToMany));
		}
	}

	public Boolean getToOne() {
		return isToOne();
	}

	public Boolean isToOne() {
		return BooleanUtils.negate(isToMany());
	}

	public void setToOne(Boolean _toOne) {
		setToMany(BooleanUtils.negate(_toOne));
	}

	public void setNumberOfToManyFaultsToBatchFetch(Integer _numberOfToManyFaultsToBatchFetch) {
		Integer oldNumberOfToManyFaultsToBatchFetch = myNumberOfToManyFaultsToBatchFetch;
		myNumberOfToManyFaultsToBatchFetch = _numberOfToManyFaultsToBatchFetch;
		firePropertyChange(EORelationship.NUMBER_OF_TO_MANY_FAULTS_TO_BATCH_FETCH, oldNumberOfToManyFaultsToBatchFetch, myNumberOfToManyFaultsToBatchFetch);
	}

	public Integer getNumberOfToManyFaultsToBatchFetch() {
		return myNumberOfToManyFaultsToBatchFetch;
	}

	public void clearJoins() {
		myJoins.clear();
		firePropertyChange(EORelationship.JOINS, null, null);
	}

	public void setJoins(List<EOJoin> _joins) {
		myJoins.clear();
		myJoins.addAll(_joins);
		firePropertyChange(EORelationship.JOINS, null, null);
	}

	public void addJoin(EOJoin _join) {
		addJoin(_join, true);
	}

	public void addJoin(EOJoin _join, boolean _fireEvents) {
		// TODO: Check duplicates?
		_join._setRelationship(this);
		List<EOJoin> oldJoins = null;
		if (_fireEvents) {
			oldJoins = myJoins;
			List<EOJoin> newJoins = new LinkedList<EOJoin>();
			newJoins.addAll(myJoins);
			newJoins.add(_join);
			myJoins = newJoins;
			firePropertyChange(EORelationship.JOINS, oldJoins, myJoins);
		} else {
			myJoins.add(_join);
		}
	}

	public void removeAllJoins() {
		List<EOJoin> oldJoins = myJoins;
		List<EOJoin> newJoins = new LinkedList<EOJoin>();
		myJoins = newJoins;
		firePropertyChange(EORelationship.JOINS, oldJoins, newJoins);
		for (EOJoin join : oldJoins) {
			join._setRelationship(null);
		}
	}

	public void removeJoin(EOJoin _join) {
		List<EOJoin> oldJoins = myJoins;
		List<EOJoin> newJoins = new LinkedList<EOJoin>();
		newJoins.addAll(myJoins);
		newJoins.remove(_join);
		myJoins = newJoins;
		firePropertyChange(EORelationship.JOINS, oldJoins, newJoins);
		_join._setRelationship(null);
	}

	public List<EOJoin> getJoins() {
		return myJoins;
	}

	public EOJoin getFirstJoin() {
		EOJoin join = null;
		Iterator<EOJoin> joinsIter = myJoins.iterator();
		if (joinsIter.hasNext()) {
			join = joinsIter.next();
		}
		return join;
	}

	public void loadFromMap(EOModelMap _relationshipMap, Set<EOModelVerificationFailure> _failures) {
		myRelationshipMap = _relationshipMap;
		if (_relationshipMap.containsKey("dataPath")) {
			myDefinition = _relationshipMap.getString("dataPath", true);
		} else {
			myDefinition = _relationshipMap.getString("definition", true);
		}
		myMandatory = _relationshipMap.getBoolean("isMandatory");
		myToMany = _relationshipMap.getBoolean("isToMany");
		String joinSemanticID = _relationshipMap.getString("joinSemantic", true);
		myJoinSemantic = EOJoinSemantic.getJoinSemanticByID(joinSemanticID);
		myName = _relationshipMap.getString("name", true);
		String deleteRuleID = _relationshipMap.getString("deleteRule", true);
		myDeleteRule = EODeleteRule.getDeleteRuleByID(deleteRuleID);
		myOwnsDestination = _relationshipMap.getBoolean("ownsDestination");
		myNumberOfToManyFaultsToBatchFetch = _relationshipMap.getInteger("numberOfToManyFaultsToBatchFetch");
		myPropagatesPrimaryKey = _relationshipMap.getBoolean("propagatesPrimaryKey");
		Set<Map> joins = _relationshipMap.getSet("joins");
		if (joins != null) {
			for (Map originalJoinMap : joins) {
				EOModelMap joinMap = new EOModelMap(originalJoinMap);
				EOJoin join = new EOJoin();
				join.loadFromMap(joinMap, _failures);
				addJoin(join, false);
			}
		}
		loadUserInfo(_relationshipMap);
	}

	public EOModelMap toMap() {
		EOModelMap relationshipMap = myRelationshipMap.cloneModelMap();
		if (myDestination != null) {
			relationshipMap.setString("destination", myDestination.getName(), true);
		} else {
			relationshipMap.remove("destination");
		}
		relationshipMap.setString("definition", getDefinition(), true);
		relationshipMap.remove("dataPath");
		relationshipMap.setBoolean("isMandatory", myMandatory, EOModelMap.YNOptionalDefaultNo);
		relationshipMap.setBoolean("isToMany", myToMany, EOModelMap.YN);
		if (!isFlattened() && myJoinSemantic != null) {
			relationshipMap.setString("joinSemantic", myJoinSemantic.getID(), true);
		} else if (isFlattened() && myRelationshipMap.get("joinSemantic") != null) {
			relationshipMap.setString("joinSemantic", (String) myRelationshipMap.get("joinSemantic"), true);
		} else {
			relationshipMap.remove("joinSemantic");
		}
		relationshipMap.setString("name", myName, true);
		if (myDeleteRule != null && myDeleteRule != EODeleteRule.NULLIFY) {
			relationshipMap.setString("deleteRule", myDeleteRule.getID(), true);
		} else {
			relationshipMap.remove("deleteRule");
		}
		relationshipMap.setBoolean("ownsDestination", myOwnsDestination, EOModelMap.YNOptionalDefaultNo);
		relationshipMap.setBoolean("propagatesPrimaryKey", myPropagatesPrimaryKey, EOModelMap.YNOptionalDefaultNo);
		relationshipMap.setInteger("numberOfToManyFaultsToBatchFetch", myNumberOfToManyFaultsToBatchFetch);
		Set<Map> joins = new PropertyListSet<Map>();
		for (EOJoin join : myJoins) {
			EOModelMap joinMap = join.toMap();
			joins.add(joinMap);
		}
		relationshipMap.setSet("joins", joins, true);
		writeUserInfo(relationshipMap);
		// remove join semantic for flattened if anything else is changing
		if (isFlattened()) {
			if (!ComparisonUtils.deepEquals(relationshipMap, myRelationshipMap)) {
				relationshipMap.remove("joinSemantic");
			}
		}

		return relationshipMap;
	}

	public void resolve(Set<EOModelVerificationFailure> _failures) {
		if (!isFlattened()) {
			String destinationName = myRelationshipMap.getString("destination", true);
			if (destinationName == null) {
				_failures.add(new EOModelVerificationFailure(myEntity.getModel(), this, "The relationship " + getName() + " has no destination entity.", false));
			} else {
				myDestination = myEntity.getModel().getModelGroup().getEntityNamed(destinationName);
				if (myDestination == null) {
					_failures.add(new MissingEntityFailure(myEntity.getModel(), destinationName));
				}
			}
		} else {
			updateDefinitionPath();
		}

		for (EOJoin join : myJoins) {
			join.resolve(_failures);
		}
	}

	public void verify(Set<EOModelVerificationFailure> _failures) {
		String name = getName();
		if (name == null || name.trim().length() == 0) {
			_failures.add(new EOModelVerificationFailure(myEntity.getModel(), this, "The relationship " + getName() + " has an empty name.", false));
		} else {
			if (name.indexOf(' ') != -1) {
				_failures.add(new EOModelVerificationFailure(myEntity.getModel(), this, "The relationship " + getName() + "'s name has a space in it.", false));
			}
			if (!StringUtils.isLowercaseFirstLetter(name)) {
				_failures.add(new EOModelVerificationFailure(myEntity.getModel(), this, "The relationship " + getName() + "'s name is capitalized, but should not be.", true));
			}
		}
		if (isFlattened()) {
			if (myEntity.resolveKeyPath(getDefinition()) == null) {
				_failures.add(new EOModelVerificationFailure(myEntity.getModel(), this, "The relationship " + getName() + " is flattened and either creates a loop or points to a non-existent target.", false));
			}
		} else {
			if (myDestination == null) {
				_failures.add(new EOModelVerificationFailure(myEntity.getModel(), this, "The relationship " + getName() + " has no destination entity.", false));
			}
		}
		// Q: EOF supports this for some specific cases - disabling until I work out a more concise check
		//if (BooleanUtils.isTrue(isPropagatesPrimaryKey()) && BooleanUtils.isTrue(isToMany())) {
		//	EOEntity destination = getDestination();
		//	if (destination != null && destination.getPrimaryKeyAttributes().size() == 1) {
		//		_failures.add(new EOModelVerificationFailure(myEntity.getModel(), this, "The relationship " + getName() + " is a to-many but also propagates its primary key.", false));
		//	}
		//}
		EOEntity entity = getEntity();
		boolean singleTableInheritance = entity != null && entity.isSingleTableInheritance();
		boolean mandatory = BooleanUtils.isTrue(isMandatory());
		boolean toOne = BooleanUtils.isTrue(isToOne());
		Iterator<EOJoin> joinsIter = myJoins.iterator();
		if (!joinsIter.hasNext() && !isFlattened()) {
			_failures.add(new EOModelVerificationFailure(myEntity.getModel(), this, "The relationship " + getName() + " does not have any joins.", false));
		}
		while (joinsIter.hasNext()) {
			EOJoin join = joinsIter.next();
			join.verify(_failures);
			EOAttribute sourceAttribute = join.getSourceAttribute();
			if (toOne && mandatory && !singleTableInheritance && sourceAttribute != null && BooleanUtils.isTrue(sourceAttribute.isAllowsNull())) {
				_failures.add(new EORelationshipOptionalityMismatchFailure(myEntity.getModel(), this, "The relationship " + getName() + " is mandatory but the attribute " + sourceAttribute.getName() + " allows nulls.", true));
			} else if (toOne && !mandatory && sourceAttribute != null && !BooleanUtils.isTrue(sourceAttribute.isAllowsNull())) {
				_failures.add(new EORelationshipOptionalityMismatchFailure(myEntity.getModel(), this, "The relationship " + getName() + " is optional but the attribute " + sourceAttribute.getName() + " does not allow nulls.", true));
			}
		}
	}

	public String getFullyQualifiedName() {
		return ((myEntity == null) ? "?" : myEntity.getFullyQualifiedName()) + "/rel: " + getName();
	}

	@Override
	public EORelationship _cloneModelObject() {
		EORelationship relationship = new EORelationship(myName);
		if (myEntity == null) {
			relationship.myEntityBeforeCloning = myEntityBeforeCloning;
		} else {
			relationship.myEntityBeforeCloning = myEntity;
		}
		relationship.myDestination = myDestination;
		relationship.myDefinition = myDefinition;
		relationship.myMandatory = myMandatory;
		relationship.myToMany = myToMany;
		relationship.myOwnsDestination = myOwnsDestination;
		relationship.myPropagatesPrimaryKey = myPropagatesPrimaryKey;
		relationship.myClassProperty = myClassProperty;
		relationship.myClientClassProperty = myClientClassProperty;
		relationship.myNumberOfToManyFaultsToBatchFetch = myNumberOfToManyFaultsToBatchFetch;
		relationship.myDeleteRule = myDeleteRule;
		relationship.myJoinSemantic = myJoinSemantic;
		for (EOJoin join : myJoins) {
			EOJoin newJoin = join._cloneModelObject();
			relationship.addJoin(newJoin, false);
		}
		_cloneUserInfoInto(relationship);
		return relationship;
	}

	@Override
	public Class<EOEntity> _getModelParentType() {
		return EOEntity.class;
	}

	public EOEntity _getModelParent() {
		return getEntity();
	}

	public void _removeFromModelParent(Set<EOModelVerificationFailure> failures) {
		if (getEntity() != null) {
			getEntity().removeRelationship(this, true);
		}
	}

	public void _addToModelParent(EOEntity modelParent, boolean findUniqueName, Set<EOModelVerificationFailure> failures) throws EOModelException {
		if (findUniqueName) {
			setName(modelParent.findUnusedRelationshipName(getName()));
		}
		modelParent.addRelationship(this);
	}

	public boolean getSqlGenerationCreateProperty() {
		return !isInherited() || getEntity().getSqlGenerationCreateInheritedProperties(); 
	}

	public String toString() {
		return "[EORelationship: name = " + myName + "; destination = " + ((myDestination == null) ? "null" : myDestination.getName()) + "; joins = " + myJoins + "]"; //$NON-NLS-4$ //$NON-NLS-5$
	}
}
