/*****************************************************************
* 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.exp.Expression;
import org.apache.cayenne.exp.ExpressionException;
import org.apache.cayenne.util.CayenneMapEntry;
import org.apache.cayenne.util.ToStringBuilder;
import org.apache.cayenne.util.XMLSerializable;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.SortedMap;
import java.util.StringTokenizer;
import java.util.TreeMap;
/**
* An Entity is an abstract descriptor for an entity mapping concept. Entity can represent
* either a descriptor of database table or a persistent object.
*
*/
public abstract class Entity implements CayenneMapEntry, XMLSerializable, Serializable {
public static final String PATH_SEPARATOR = ".";
/**
* A prefix or a suffix that can be used in a path component to indicate that an OUTER
* JOIN should be used when resolving the expression.
*
* @since 3.0
*/
public static final String OUTER_JOIN_INDICATOR = "+";
protected String name;
protected DataMap dataMap;
protected final SortedMap<String, Attribute> attributes = new TreeMap<>();
protected final SortedMap<String, Relationship> relationships = new TreeMap<>();
/**
* Creates an unnamed Entity.
*/
public Entity() {
this(null);
}
/**
* Creates a named Entity.
*/
public Entity(String name) {
setName(name);
}
@Override
public String toString() {
return new ToStringBuilder(this).append("name", getName()).toString();
}
/**
* Returns entity name. Name is a unique identifier of the entity within its DataMap.
*/
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Object getParent() {
return getDataMap();
}
public void setParent(Object parent) {
if (parent != null && !(parent instanceof DataMap)) {
throw new IllegalArgumentException("Expected null or DataMap, got: " + parent);
}
setDataMap((DataMap) parent);
}
/**
* @return parent DataMap of this entity.
*/
public DataMap getDataMap() {
return dataMap;
}
/**
* Sets parent DataMap of this entity.
*/
public void setDataMap(DataMap dataMap) {
this.dataMap = dataMap;
}
/**
* Returns attribute with name <code>attributeName</code> or null if no attribute
* with this name exists.
*/
public Attribute getAttribute(String attributeName) {
return attributes.get(attributeName);
}
/**
* Adds new attribute to the entity, setting its parent entity to be this object. If
* attribute has no name, IllegalArgumentException is thrown.
*/
public void addAttribute(Attribute attribute) {
if (attribute.getName() == null) {
throw new IllegalArgumentException("Attempt to insert unnamed attribute.");
}
// block overrides
// TODO: change method signature to return replaced attribute and make sure the Modeler handles it...
Object existingAttribute = attributes.get(attribute.getName());
if (existingAttribute != null) {
if (existingAttribute == attribute) {
return;
} else {
throw new IllegalArgumentException("An attempt to override attribute '" + attribute.getName() + "'");
}
}
// Check that there aren't any relationships with the same name as the given
// attribute.
Object existingRelationship = relationships.get(attribute.getName());
if (existingRelationship != null) {
throw new IllegalArgumentException(
"Attribute name conflict with existing relationship '" + attribute.getName() + "'");
}
attributes.put(attribute.getName(), attribute);
attribute.setEntity(this);
}
/**
* Removes an attribute named <code>attrName</code>.
*/
public void removeAttribute(String attrName) {
attributes.remove(attrName);
}
/**
*
* @since 4.0
*/
public void updateAttribute(Attribute attribute) {
removeAttribute(attribute.getName());
addAttribute(attribute);
}
public void clearAttributes() {
attributes.clear();
}
/**
* Returns relationship with name <code>relName</code>. Will return null if no
* relationship with this name exists in the entity.
*/
public Relationship getRelationship(String relName) {
return relationships.get(relName);
}
/**
* Adds new relationship to the entity.
*/
public void addRelationship(Relationship relationship) {
if (relationship.getName() == null) {
throw new IllegalArgumentException("Attempt to insert unnamed relationship.");
}
// block overrides
// TODO: change method signature to return replaced attribute and make sure the
// Modeler handles it...
Object existingRelationship = relationships.get(relationship.getName());
if (existingRelationship != null) {
if (existingRelationship == relationship) {
return;
} else {
throw new IllegalArgumentException(
"An attempt to override relationship '" + relationship.getName() + "'");
}
}
// Check that there aren't any attributes with the same name as the given
// relationship.
Object existingAttribute = attributes.get(relationship.getName());
if (existingAttribute != null) {
throw new IllegalArgumentException(
"Relationship name conflict with existing attribute '" + relationship.getName() + "'");
}
relationships.put(relationship.getName(), relationship);
relationship.setSourceEntity(this);
}
/**
* Removes a relationship named <code>attrName</code>.
*/
public void removeRelationship(String relName) {
relationships.remove(relName);
}
public void clearRelationships() {
relationships.clear();
}
/**
* Returns an unmodifiable map of relationships sorted by name.
*/
public SortedMap<String, ? extends Relationship> getRelationshipMap() {
// create a new instance ... earlier attempts to cache it in the entity caused
// serialization issues (esp. with Hessian).
return Collections.unmodifiableSortedMap(relationships);
}
/**
* Returns a relationship that has a specified entity as a target. If there is more
* than one relationship for the same target, it is unpredictable which one will be
* returned.
*
* @since 1.1
*/
public Relationship getAnyRelationship(Entity targetEntity) {
if (getRelationships().isEmpty()) {
return null;
}
for (Relationship r : getRelationships()) {
if (r.getTargetEntity() == targetEntity) {
return r;
}
}
return null;
}
/**
* Returns an unmodifiable collection of Relationships that exist in this entity.
*/
public Collection<? extends Relationship> getRelationships() {
// create a new instance ... earlier attempts to cache it in the entity caused
// serialization issues (esp. with Hessian).
return Collections.unmodifiableCollection(relationships.values());
}
/**
* Returns an unmodifiable sorted map of entity attributes.
*/
public SortedMap<String, ? extends Attribute> getAttributeMap() {
// create a new instance ... earlier attempts to cache it in the entity caused
// serialization issues (esp. with Hessian).
return Collections.unmodifiableSortedMap(attributes);
}
/**
* Returns an unmodifiable collection of entity attributes.
*/
public Collection<? extends Attribute> getAttributes() {
// create a new instance ... earlier attempts to cache it in the entity caused
// serialization issues (esp. with Hessian).
return Collections.unmodifiableCollection(attributes.values());
}
/**
* Translates Expression rooted in this entity to an analogous expression rooted in
* related entity.
*
* @since 1.1
*/
public abstract Expression translateToRelatedEntity(Expression expression, String relationshipPath);
/**
* Convenience method returning the last component in the path iterator. If the last
* component is an alias, it is fully resolved down to the last ObjRelationship.
*
* @since 3.0
*/
@SuppressWarnings("unchecked")
public <T extends Attribute, U extends Relationship> PathComponent<T, U> lastPathComponent(
Expression path,
Map aliasMap) {
for (PathComponent component : resolvePath(path, aliasMap)) {
if (component.isLast()) {
// resolve aliases if needed
return lastPathComponent(component);
}
}
return null;
}
@SuppressWarnings("unchecked")
private PathComponent lastPathComponent(PathComponent<Attribute, Relationship> component) {
if (!component.isAlias()) {
return component;
}
for (PathComponent subcomponent : component.getAliasedPath()) {
if (subcomponent.isLast()) {
return lastPathComponent(subcomponent);
}
}
throw new IllegalStateException("Invalid last path component: " + component.getName());
}
/**
* Returns an Iterable over the path components with elements represented as
* {@link PathComponent} instances, encapsulating a relationship, an attribute or a
* subpath alias. An optional "aliasMap" parameter is used to resolve subpaths from
* aliases.
* <p>
* This method is lazy: if path is invalid and can not be resolved from this entity,
* this method will still return an Iterator, but an attempt to read the first invalid
* path component will result in ExpressionException.
* </p>
*
* @since 3.0
*/
@SuppressWarnings("unchecked")
public abstract <T extends Attribute, U extends Relationship> Iterable<PathComponent<T, U>> resolvePath(
Expression pathExp,
Map aliasMap);
/**
* Processes expression <code>pathExp</code> and returns an Iterator of path
* components that contains a sequence of Attributes and Relationships. Note that if
* path is invalid and can not be resolved from this entity, this method will still
* return an Iterator, but an attempt to read the first invalid path component will
* result in ExpressionException.
*/
public abstract Iterator<CayenneMapEntry> resolvePathComponents(Expression pathExp) throws ExpressionException;
/**
* Returns an Iterator over the path components that contains a sequence of Attributes
* and Relationships. Note that if path is invalid and can not be resolved from this
* entity, this method will still return an Iterator, but an attempt to read the first
* invalid path component will result in ExpressionException.
*/
public Iterator<CayenneMapEntry> resolvePathComponents(String path) throws ExpressionException {
return new PathIterator(path);
}
/**
* An iterator resolving mapping components represented by the path string.
* This entity is assumed to be the root of the path.
*/
final class PathIterator implements Iterator<CayenneMapEntry> {
private final StringTokenizer tokens;
private final String path;
private Entity currentEntity;
PathIterator(String path) {
currentEntity = Entity.this;
tokens = new StringTokenizer(path, PATH_SEPARATOR);
this.path = path;
}
public boolean hasNext() {
return tokens.hasMoreTokens();
}
public CayenneMapEntry next() {
String pathComp = tokens.nextToken();
if(pathComp.endsWith(OUTER_JOIN_INDICATOR)) {
pathComp = pathComp.substring(0, pathComp.length() - 1);
}
// see if this is an attribute
Attribute attr = currentEntity.getAttribute(pathComp);
if (attr != null) {
// do a sanity check...
if (tokens.hasMoreTokens()) {
throw new ExpressionException("Attribute must be the last component of the path: '%s'.",
path, null, pathComp);
}
return attr;
}
Relationship rel = currentEntity.getRelationship(pathComp);
if (rel != null) {
currentEntity = rel.getTargetEntity();
if (currentEntity != null || !tokens.hasMoreTokens()) { //otherwise an exception will be thrown
return rel;
}
}
String entityName = (currentEntity != null) ? currentEntity.getName() : "(?)";
throw new ExpressionException("Can't resolve path component: [%s.%s].", path, null, entityName, pathComp);
}
public void remove() {
throw new UnsupportedOperationException("'remove' operation is not supported.");
}
}
final MappingNamespace getNonNullNamespace() {
MappingNamespace parent = getDataMap();
if (parent == null) {
throw new CayenneRuntimeException("Entity '%s' has no parent MappingNamespace (such as DataMap)", getName());
}
return parent;
}
}