org.apache.cayenne.map.ObjEntity.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.cayenne.map.ObjEntity.java

Source

/*****************************************************************
 *   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.configuration.ConfigurationNode;
import org.apache.cayenne.configuration.ConfigurationNodeVisitor;
import org.apache.cayenne.dba.TypesMapping;
import org.apache.cayenne.di.AdhocObjectFactory;
import org.apache.cayenne.exp.Expression;
import org.apache.cayenne.exp.ExpressionException;
import org.apache.cayenne.exp.ExpressionFactory;
import org.apache.cayenne.map.event.EntityEvent;
import org.apache.cayenne.map.event.ObjEntityListener;
import org.apache.cayenne.util.CayenneMapEntry;
import org.apache.cayenne.util.Util;
import org.apache.cayenne.util.XMLEncoder;
import org.apache.commons.collections.Transformer;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * ObjEntity is a mapping descriptor for a DataObject Java class. It contains
 * the information about the Java class itself, as well as its mapping to the
 * DbEntity layer.
 */
public class ObjEntity extends Entity implements ObjEntityListener, ConfigurationNode {

    public static final int LOCK_TYPE_NONE = 0;
    public static final int LOCK_TYPE_OPTIMISTIC = 1;

    // do not import CayenneDataObject as it introduces unneeded client
    // dependency
    private static final String CAYENNE_DATA_OBJECT_CLASS = "org.apache.cayenne.CayenneDataObject";
    /**
     * A collection of default "generic" entity classes excluded from class
     * generation.
     * 
     * @since 1.2
     */
    protected static final Collection<String> DEFAULT_GENERIC_CLASSES = Collections
            .singletonList(CAYENNE_DATA_OBJECT_CLASS);

    protected String superClassName;
    protected String className;
    protected String dbEntityName;
    protected String superEntityName;
    protected Expression qualifier;
    protected boolean readOnly;
    protected int lockType;

    protected boolean _abstract;
    protected boolean serverOnly;
    protected String clientClassName;
    protected String clientSuperClassName;

    @Deprecated
    protected List<EntityListener> entityListeners;
    protected CallbackMap callbacks;

    @Deprecated
    protected boolean excludingDefaultListeners;

    @Deprecated
    protected boolean excludingSuperclassListeners;

    protected Map<String, String> attributeOverrides;

    public ObjEntity() {
        this(null);
    }

    public ObjEntity(String name) {
        setName(name);
        this.lockType = LOCK_TYPE_NONE;
        this.callbacks = new CallbackMap();
        this.entityListeners = new ArrayList<>(2);
        this.attributeOverrides = new TreeMap<>();
    }

    /**
     * @since 3.1
     */
    @Override
    public <T> T acceptVisitor(ConfigurationNodeVisitor<T> visitor) {
        return visitor.visitObjEntity(this);
    }

    /**
     * Prints itself as XML to the provided XMLEncoder.
     * 
     * @since 1.1
     */
    @Override
    public void encodeAsXML(XMLEncoder encoder) {
        encoder.print("<obj-entity name=\"");
        encoder.print(getName());

        // additionally validate that superentity exists
        if (getSuperEntityName() != null && getSuperEntity() != null) {
            encoder.print("\" superEntityName=\"");
            encoder.print(getSuperEntityName());
        }

        if (isAbstract()) {
            encoder.print("\" abstract=\"true");
        }

        if (isServerOnly()) {
            encoder.print("\" serverOnly=\"true");
        }

        if (getClassName() != null) {
            encoder.print("\" className=\"");
            encoder.print(getClassName());
        }

        if (getClientClassName() != null) {
            encoder.print("\" clientClassName=\"");
            encoder.print(getClientClassName());
        }

        if (isReadOnly()) {
            encoder.print("\" readOnly=\"true");
        }

        if (getDeclaredLockType() == LOCK_TYPE_OPTIMISTIC) {
            encoder.print("\" lock-type=\"optimistic");
        }

        if (getDbEntityName() != null && getDbEntity() != null) {

            // not writing DbEntity name if sub entity has same DbEntity
            // as super entity, see CAY-1477
            if (!(getSuperEntity() != null && getSuperEntity().getDbEntity() == getDbEntity())) {
                encoder.print("\" dbEntityName=\"");
                encoder.print(Util.encodeXmlAttribute(getDbEntityName()));
            }
        }

        if (getSuperEntityName() == null && getSuperClassName() != null) {
            encoder.print("\" superClassName=\"");
            encoder.print(getSuperClassName());
        }

        if (getSuperEntityName() == null && getClientSuperClassName() != null) {
            encoder.print("\" clientSuperClassName=\"");
            encoder.print(getClientSuperClassName());
        }

        // deprecated
        if (isExcludingSuperclassListeners()) {
            encoder.print("\" exclude-superclass-listeners=\"true");
        }

        // deprecated
        if (isExcludingDefaultListeners()) {
            encoder.print("\" exclude-default-listeners=\"true");
        }

        encoder.println("\">");
        encoder.indent(1);

        if (qualifier != null) {
            encoder.print("<qualifier>");
            qualifier.encodeAsXML(encoder);
            encoder.println("</qualifier>");
        }

        // store attributes
        encoder.print(getDeclaredAttributes());

        for (Map.Entry<String, String> override : attributeOverrides.entrySet()) {
            encoder.print("<attribute-override name=\"" + override.getKey() + '\"');
            encoder.print(" db-attribute-path=\"");
            encoder.print(Util.encodeXmlAttribute(override.getValue()));
            encoder.print('\"');
            encoder.println("/>");
        }

        // deprecated
        // write entity listeners
        for (EntityListener entityListener : entityListeners) {
            entityListener.encodeAsXML(encoder);
        }

        // write entity-level callbacks
        getCallbackMap().encodeCallbacksAsXML(encoder);

        encoder.indent(-1);
        encoder.println("</obj-entity>");
    }

    /**
     * Returns an ObjEntity stripped of any server-side information, such as
     * DbEntity mapping. "clientClassName" property of this entity is used to
     * initialize "className" property of returned entity.
     * 
     * @since 1.2
     */
    public ObjEntity getClientEntity() {

        ClientObjEntity entity = new ClientObjEntity(getName());
        entity.setClassName(getClientClassName());
        entity.setSuperClassName(getClientSuperClassName());
        entity.setSuperEntityName(getSuperEntityName());
        entity.setDeclaredQualifier(getDeclaredQualifier());

        // TODO: should we also copy lock type?

        Collection<ObjAttribute> primaryKeys = getMutablePrimaryKeys();
        Collection<ObjAttribute> clientPK = new ArrayList<>(primaryKeys.size());

        for (ObjAttribute attribute : getDeclaredAttributes()) {
            ObjAttribute clientAttribute = attribute.getClientAttribute();
            entity.addAttribute(clientAttribute);

            if (primaryKeys.remove(attribute)) {
                clientPK.add(clientAttribute);
            }
        }

        // after all meaningful pks got removed, here we only have synthetic pks
        // left...
        for (ObjAttribute attribute : primaryKeys) {
            ObjAttribute clientAttribute = attribute.getClientAttribute();
            clientPK.add(clientAttribute);
        }

        entity.setPrimaryKeys(clientPK);

        // copy relationships; skip runtime generated relationships
        for (ObjRelationship relationship : getDeclaredRelationships()) {
            if (relationship.isRuntime()) {
                continue;
            }

            ObjEntity targetEntity = relationship.getTargetEntity();
            // note that 'isClientAllowed' also checks parent DataMap client
            // policy
            // that can be handy in case of cross-map relationships
            if (targetEntity == null || !targetEntity.isClientAllowed()) {
                continue;
            }

            entity.addRelationship(relationship.getClientRelationship());
        }

        // TODO: andrus 2/5/2007 - copy embeddables
        // TODO: andrus 2/5/2007 - copy callback methods

        return entity;
    }

    /**
     * Returns a non-null class name. For generic entities with no class
     * specified explicitly, default DataMap superclass is used, and if it is
     * not set - CayenneDataObject is used.
     * 
     * @since 4.0
     */
    public String getJavaClassName() {
        String name = getClassName();

        if (name == null && getDataMap() != null) {
            name = getDataMap().getDefaultSuperclass();
        }

        if (name == null) {
            name = CAYENNE_DATA_OBJECT_CLASS;
        }

        return name;
    }

    /**
     * Returns Java class of persistent objects described by this entity. For
     * generic entities with no class specified explicitly, default DataMap
     * superclass is used, and if it is not set - CayenneDataObject is used.
     * Casts any thrown exceptions into CayenneRuntimeException.
     * 
     * @since 1.2
     * @deprecated since 4.0 this method based on statically defined class
     *             loading algorithm is not going to work in environments like
     *             OSGi. {@link AdhocObjectFactory} should be used as it can
     *             provide the environment-specific class loading policy. 
     */
    @Deprecated
    public Class<?> getJavaClass() {
        String name = getJavaClassName();

        try {
            return Util.getJavaClass(name);
        } catch (ClassNotFoundException e) {
            throw new CayenneRuntimeException("Failed to doLoad class " + name + ": " + e.getMessage(), e);
        }
    }

    /**
     * Returns an unmodifiable list of registered {@link EntityListener}
     * objects. Note that since the order of listeners is significant a list,
     * not just a generic Collection is returned.
     * 
     * @since 3.0
     * @deprecated since 4.0 unused, as listeners are no longer mapped in a
     *             DataMap.
     */
    @Deprecated
    public List<EntityListener> getEntityListeners() {
        return Collections.unmodifiableList(entityListeners);
    }

    /**
     * Adds a new EntityListener.
     * 
     * @since 3.0
     * @throws IllegalArgumentException
     *             if a listener for the same class name is already registered.
     * @deprecated since 4.0 unused, as listeners are no longer mapped in a
     *             DataMap.
     */
    @Deprecated
    public void addEntityListener(EntityListener listener) {
        for (EntityListener next : entityListeners) {
            if (listener.getClassName().equals(next.getClassName())) {
                throw new IllegalArgumentException("Duplicate listener for " + next.getClassName());
            }
        }

        entityListeners.add(listener);
    }

    /**
     * Removes a listener matching class name.
     * 
     * @since 3.0
     * @deprecated since 4.0 unused, as listeners are no longer mapped in a
     *             DataMap.
     */
    @Deprecated
    public void removeEntityListener(String className) {
        Iterator<EntityListener> it = entityListeners.iterator();
        while (it.hasNext()) {
            EntityListener next = it.next();
            if (className.equals(next.getClassName())) {
                it.remove();
                break;
            }
        }
    }

    /**
     * @since 3.0
     * @deprecated since 4.0 unused, as listeners are no longer mapped in a
     *             DataMap.
     */
    @Deprecated
    public EntityListener getEntityListener(String className) {
        for (EntityListener next : entityListeners) {
            if (className.equals(next.getClassName())) {
                return next;
            }
        }

        return null;
    }

    /**
     * Returns an object that stores callback methods of this entity.
     * 
     * @since 3.0
     */
    public CallbackMap getCallbackMap() {
        return callbacks;
    }

    /**
     * Returns the type of lock used by this ObjEntity. If this entity is not
     * locked, this method would look in a super entity recursively, until it
     * finds a lock somewhere in the inheritance hierarchy.
     * 
     * @since 1.1
     */
    public int getLockType() {
        // if this entity has an explicit lock,
        // no need to lookup inheritance hierarchy
        if (lockType != LOCK_TYPE_NONE) {
            return lockType;
        }

        ObjEntity superEntity = getSuperEntity();
        return (superEntity != null) ? superEntity.getLockType() : lockType;
    }

    /**
     * Returns the type of lock used by this ObjEntity, regardless of what
     * locking type is used by super entities.
     * 
     * @since 1.1
     */
    public int getDeclaredLockType() {
        return lockType;
    }

    /**
     * Sets the type of lock used by this ObjEntity.
     * 
     * @since 1.1
     */
    public void setDeclaredLockType(int i) {
        lockType = i;
    }

    /**
     * Returns whether this entity is "generic", meaning it is not mapped to a
     * unique Java class. Criterion for generic entities is that it either has
     * no Java class mapped or its class is the same as DataMap's default
     * superclass, or it is CayenneDataObject.
     * 
     * @since 1.2
     */
    public boolean isGeneric() {
        String className = getClassName();
        return className == null || DEFAULT_GENERIC_CLASSES.contains(className)
                || (getDataMap() != null && className.equals(getDataMap().getDefaultSuperclass()));
    }

    /**
     * Returns true if this entity is allowed to be used on the client. Checks
     * that parent DataMap allows client entities and also that this entity is
     * not explicitly disabled for the client use.
     * 
     * @since 1.2
     */
    public boolean isClientAllowed() {
        return getDataMap() != null && !isServerOnly() && getDataMap().isClientSupported();
    }

    public boolean isAbstract() {
        return _abstract;
    }

    /**
     * Sets whether this entity is abstract only.
     */
    public void setAbstract(boolean isAbstract) {
        this._abstract = isAbstract;
    }

    /**
     * Returns true if this entity is not available on the client.
     * 
     * @since 1.2
     */
    public boolean isServerOnly() {
        return serverOnly;
    }

    /**
     * Sets whether this entity is available on the client.
     * 
     * @since 1.2
     */
    public void setServerOnly(boolean serverOnly) {
        this.serverOnly = serverOnly;
    }

    /**
     * Returns a qualifier that imposes a restriction on what objects belong to
     * this entity. Returned qualifier is the one declared in this entity, and
     * does not include qualifiers declared in super entities.
     * 
     * @since 1.1
     */
    public Expression getDeclaredQualifier() {
        return qualifier;
    }

    /**
     * Returns an entity name for a parent entity in the inheritance hierarchy.
     * 
     * @since 1.1
     */
    public String getSuperEntityName() {
        return superEntityName;
    }

    /**
     * Sets a qualifier that imposes a limit on what objects belong to this
     * entity.
     * 
     * @since 1.1
     */
    public void setDeclaredQualifier(Expression qualifier) {
        this.qualifier = qualifier;
    }

    /**
     * Sets an entity name for a parent entity in the inheritance hierarchy.
     * 
     * @since 1.1
     */
    public void setSuperEntityName(String superEntityName) {
        this.superEntityName = superEntityName;
    }

    /**
     * Returns the name of DataObject class described by this entity.
     */
    public String getClassName() {
        return className;
    }

    /**
     * Sets the name of the DataObject class described by this entity.
     */
    public void setClassName(String className) {
        this.className = className;
    }

    /**
     * Returns the name of ClientDataObject class described by this entity.
     * 
     * @since 1.2
     */
    public String getClientClassName() {
        return clientClassName;
    }

    /**
     * Sets the name of the ClientDataObject class described by this entity.
     * 
     * @since 1.2
     */
    public void setClientClassName(String clientClassName) {
        this.clientClassName = clientClassName;
    }

    /**
     * Returns a fully-qualified name of the super class of the DataObject
     * class. This value is used as a hint for class generation. If the entity
     * inherits from another entity, a superclass is the class of that entity.
     */
    public String getSuperClassName() {
        ObjEntity superEntity = getSuperEntity();
        return (superEntity != null) ? superEntity.getClassName() : superClassName;
    }

    /**
     * Sets a fully-qualified name of the super class of the DataObject class.
     * This value is used as a hint for class generation.
     * <p>
     * <i>An attempt to set superclass on an inherited entity has no effect,
     * since a class of the super entity is always used as a superclass.</i>
     * </p>
     */
    public void setSuperClassName(String superClassName) {
        this.superClassName = superClassName;
    }

    /**
     * Returns a fully-qualified name of the client-side super class of the
     * DataObject class. This value is used as a hint for class generation. If
     * the entity inherits from another entity, a superclass is the class of
     * that entity.
     * 
     * @since 1.2
     */
    public String getClientSuperClassName() {
        ObjEntity superEntity = getSuperEntity();
        return (superEntity != null) ? superEntity.getClientClassName() : clientSuperClassName;
    }

    /**
     * Sets a fully-qualified name of the client-side super class of the
     * ClientDataObject class. This value is used as a hint for class
     * generation.
     * <p>
     * <i>An attempt to set superclass on an inherited entity has no effect,
     * since a class of the super entity is always used as a superclass. </i>
     * </p>
     * 
     * @since 1.2
     */
    public void setClientSuperClassName(String clientSuperClassName) {
        this.clientSuperClassName = clientSuperClassName;
    }

    /**
     * Returns a "super" entity in the entity inheritance hierarchy.
     * 
     * @since 1.1
     */
    public ObjEntity getSuperEntity() {
        return (superEntityName != null) ? getNonNullNamespace().getObjEntity(superEntityName) : null;
    }

    /**
     * Returns a DbEntity associated with this ObjEntity.
     */
    public DbEntity getDbEntity() {

        // since 1.2 - allow overriding DbEntity in the inheritance hierarchy...
        if (dbEntityName != null) {
            return getNonNullNamespace().getDbEntity(dbEntityName);
        }

        ObjEntity superEntity = getSuperEntity();
        if (superEntity != null) {
            return superEntity.getDbEntity();
        }

        return null;
    }

    /**
     * Sets the DbEntity used by this ObjEntity.
     * <p>
     * <i>Setting DbEntity on an inherited entity has no effect, since a class
     * of the super entity is always used as a superclass. </i>
     * </p>
     */
    public void setDbEntity(DbEntity dbEntity) {
        this.dbEntityName = (dbEntity != null) ? dbEntity.getName() : null;
    }

    /**
     * Returns an unmodifiable collection of ObjAttributes representing the
     * primary key of the table described by this DbEntity. Note that since PK
     * is very often not an object property, the returned collection may contain
     * "synthetic" ObjAttributes that are created on the fly and are not a part
     * of ObjEntity and will not be a part of entity.getAttributes().
     * 
     * @since 3.0
     */
    public Collection<ObjAttribute> getPrimaryKeys() {
        return Collections.unmodifiableCollection(getMutablePrimaryKeys());
    }

    private Collection<ObjAttribute> getMutablePrimaryKeys() {
        if (getDbEntity() == null) {
            throw new CayenneRuntimeException("No DbEntity for ObjEntity: %s", getName());
        }

        Collection<DbAttribute> pkAttributes = getDbEntity().getPrimaryKeys();
        Collection<ObjAttribute> attributes = new ArrayList<>(pkAttributes.size());

        for (DbAttribute pk : pkAttributes) {
            ObjAttribute attribute = getAttributeForDbAttribute(pk);

            // create synthetic attribute
            if (attribute == null) {
                attribute = new SyntheticPKObjAttribute(Util.underscoredToJava(pk.getName(), false));
                attribute.setDbAttributePath(pk.getName());
                attribute.setType(TypesMapping.getJavaBySqlType(pk.getType()));
            }

            attributes.add(attribute);
        }

        return attributes;
    }

    /**
     * Returns a named attribute that is either declared in this ObjEntity or is
     * inherited. In any case returned attribute 'getEntity' method will return
     * this entity. Returns null if no matching attribute is found.
     */
    @Override
    public ObjAttribute getAttribute(String name) {
        ObjAttribute attribute = (ObjAttribute) super.getAttribute(name);
        if (attribute != null) {
            return attribute;
        }

        // check embedded attribute
        int dot = name.indexOf('.');
        if (dot > 0 && dot < name.length() - 1) {
            ObjAttribute embedded = getAttribute(name.substring(0, dot));
            if (embedded instanceof EmbeddedAttribute) {
                return ((EmbeddedAttribute) embedded).getAttribute(name.substring(dot + 1));
            }
        }

        // check super attribute
        ObjEntity superEntity = getSuperEntity();
        if (superEntity != null) {

            ObjAttribute superAttribute = superEntity.getAttribute(name);
            if (superAttribute == null) {
                return null;
            }

            // decorate returned attribute to make it appear as if it belongs to
            // this
            // entity

            ObjAttribute decoratedAttribute = new ObjAttribute(superAttribute);
            decoratedAttribute.setEntity(this);

            String pathOverride = attributeOverrides.get(name);
            if (pathOverride != null) {
                decoratedAttribute.setDbAttributePath(pathOverride);
            }

            return decoratedAttribute;
        }

        return null;
    }

    /**
     * Returns a SortedMap of all attributes that either belong to this
     * ObjEntity or inherited.
     */
    @Override
    public SortedMap<String, ObjAttribute> getAttributeMap() {
        if (superEntityName == null) {
            return getAttributeMapInternal();
        }

        SortedMap<String, ObjAttribute> attributeMap = new TreeMap<>();
        appendAttributes(attributeMap);
        return attributeMap;
    }

    /**
     * Recursively appends all attributes in the entity inheritance hierarchy.
     */
    final void appendAttributes(Map<String, ObjAttribute> map) {
        map.putAll(getAttributeMapInternal());

        ObjEntity superEntity = getSuperEntity();
        if (superEntity != null) {
            SortedMap<String, ObjAttribute> attributeMap = new TreeMap<>();
            superEntity.appendAttributes(attributeMap);
            for (String attributeName : attributeMap.keySet()) {

                String overridedDbPath = attributeOverrides.get(attributeName);

                ObjAttribute attribute = new ObjAttribute(attributeMap.get(attributeName));
                attribute.setEntity(this);
                if (overridedDbPath != null) {
                    attribute.setDbAttributePath(overridedDbPath);
                }
                map.put(attributeName, attribute);
            }
        }
    }

    @SuppressWarnings("unchecked")
    final SortedMap<String, ObjAttribute> getAttributeMapInternal() {
        return (SortedMap<String, ObjAttribute>) super.getAttributeMap();
    }

    /**
     * @since 3.0
     */
    public void addAttributeOverride(String attributeName, String dbPath) {
        attributeOverrides.put(attributeName, dbPath);
    }

    /**
     * @since 4.0
     */
    public void removeAttributeOverride(String attributeName) {
        attributeOverrides.remove(attributeName);
    }

    /**
     * @since 3.0
     */
    public Map<String, String> getDeclaredAttributeOverrides() {
        return Collections.unmodifiableMap(attributeOverrides);
    }

    /**
     * Returns a Collection of all attributes that either belong to this
     * ObjEntity or inherited.
     */
    @Override
    public Collection<ObjAttribute> getAttributes() {
        return getAttributeMap().values();
    }

    /**
     * Returns a Collection of all attributes that belong to this ObjEntity,
     * excluding inherited attributes.
     * 
     * @since 1.1
     */
    @SuppressWarnings("unchecked")
    public Collection<ObjAttribute> getDeclaredAttributes() {
        return (Collection<ObjAttribute>) super.getAttributes();
    }

    /**
     * Finds attribute declared by this ObjEntity,
     * excluding inherited attributes.
     *
     * @param name of the attribute
     * @return declared attribute or null if no attribute is found
     *
     * @see ObjEntity#getAttribute(String)
     *
     * @since 4.0
     */
    public ObjAttribute getDeclaredAttribute(String name) {
        return (ObjAttribute) super.getAttribute(name);
    }

    /**
     * Returns a named Relationship that either belongs to this ObjEntity or is
     * inherited. Returns null if no matching attribute is found.
     */
    @Override
    public ObjRelationship getRelationship(String name) {
        ObjRelationship relationship = (ObjRelationship) super.getRelationship(name);
        if (relationship != null) {
            return relationship;
        }

        if (superEntityName == null) {
            return null;
        }

        ObjEntity superEntity = getSuperEntity();
        return (superEntity != null) ? superEntity.getRelationship(name) : null;
    }

    @Override
    public SortedMap<String, ObjRelationship> getRelationshipMap() {
        if (superEntityName == null) {
            return getRelationshipMapInternal();
        }

        SortedMap<String, ObjRelationship> relationshipMap = new TreeMap<>();
        appendRelationships(relationshipMap);
        return relationshipMap;
    }

    /**
     * Recursively appends all relationships in the entity inheritance
     * hierarchy.
     */
    final void appendRelationships(Map<String, ObjRelationship> map) {
        map.putAll(getRelationshipMapInternal());

        ObjEntity superEntity = getSuperEntity();
        if (superEntity != null) {
            superEntity.appendRelationships(map);
        }
    }

    @Override
    public Collection<ObjRelationship> getRelationships() {
        return getRelationshipMap().values();
    }

    @SuppressWarnings("unchecked")
    final SortedMap<String, ObjRelationship> getRelationshipMapInternal() {
        return (SortedMap<String, ObjRelationship>) super.getRelationshipMap();
    }

    /**
     * Returns a Collection of all relationships that belong to this ObjEntity,
     * excluding inherited attributes.
     * 
     * @since 1.1
     */
    @SuppressWarnings("unchecked")
    public Collection<ObjRelationship> getDeclaredRelationships() {
        return (Collection<ObjRelationship>) super.getRelationships();
    }

    /**
     * Returns ObjAttribute of this entity that maps to <code>dbAttribute</code>
     * parameter. Returns null if no such attribute is found.
     */
    public ObjAttribute getAttributeForDbAttribute(DbAttribute dbAttribute) {

        for (ObjAttribute next : getAttributeMap().values()) {

            if (next instanceof EmbeddedAttribute) {
                ObjAttribute embeddedAttribute = ((EmbeddedAttribute) next)
                        .getAttributeForDbPath(dbAttribute.getName());
                if (embeddedAttribute != null) {
                    return embeddedAttribute;
                }
            } else {
                if (next.getDbAttribute() == dbAttribute) {
                    return next;
                }
            }
        }

        return null;
    }

    /**
     * Returns the names of DbAtributes that comprise the primary key of the
     * parent DbEntity.
     * 
     * @since 3.0
     */
    public Collection<String> getPrimaryKeyNames() {
        DbEntity dbEntity = getDbEntity();

        // abstract entities may have no DbEntity mapping
        if (dbEntity == null) {
            return Collections.emptyList();
        }

        Collection<DbAttribute> pkAttributes = dbEntity.getPrimaryKeys();
        Collection<String> names = new ArrayList<>(pkAttributes.size());

        for (DbAttribute pk : pkAttributes) {
            names.add(pk.getName());
        }

        return Collections.unmodifiableCollection(names);
    }

    /**
     * Returns ObjRelationship of this entity that maps to
     * <code>dbRelationship</code> parameter. Returns null if no such
     * relationship is found.
     */
    public ObjRelationship getRelationshipForDbRelationship(DbRelationship dbRelationship) {

        for (ObjRelationship objRel : getRelationshipMap().values()) {
            List<DbRelationship> relList = objRel.getDbRelationships();
            if (relList.size() != 1) {
                continue;
            }

            if (relList.get(0) == dbRelationship) {
                return objRel;
            }
        }
        return null;
    }

    /**
     * Clears all the mapping between this obj entity and its current db entity.
     * Clears mapping between entities, attributes and relationships.
     */
    public void clearDbMapping() {
        if (dbEntityName == null) {
            return;
        }

        for (ObjAttribute attribute : getAttributeMap().values()) {
            DbAttribute dbAttr = attribute.getDbAttribute();
            if (dbAttr != null) {
                attribute.setDbAttributePath(null);
            }
        }

        for (ObjRelationship relationship : getRelationships()) {
            relationship.clearDbRelationships();
        }

        dbEntityName = null;
    }

    /**
     * Returns <code>true</code> if this ObjEntity represents a set of read-only
     * objects.
     * 
     * @return boolean
     */
    public boolean isReadOnly() {
        return readOnly;
    }

    public void setReadOnly(boolean readOnly) {
        this.readOnly = readOnly;
    }

    /**
     * Returns true if this entity directly or indirectly inherits from a given
     * entity, false otherwise.
     * 
     * @since 1.1
     */
    public boolean isSubentityOf(ObjEntity entity) {
        if (entity == null) {
            return false;
        }

        if (entity == this) {
            return false;
        }

        ObjEntity superEntity = getSuperEntity();
        if (superEntity == entity) {
            return true;
        }

        return (superEntity != null) ? superEntity.isSubentityOf(entity) : false;
    }

    /**
     * @since 3.0
     */
    @Override
    @SuppressWarnings("unchecked")
    public PathComponent<ObjAttribute, ObjRelationship> lastPathComponent(Expression path, Map aliasMap) {
        return super.lastPathComponent(path, aliasMap);
    }

    /**
     * Returns an Iterable instance over expression path components based on
     * this entity.
     * 
     * @since 3.0
     */
    @Override
    @SuppressWarnings("unchecked")
    public Iterable<PathComponent<ObjAttribute, ObjRelationship>> resolvePath(final Expression pathExp,
            final Map aliasMap) {

        if (pathExp.getType() == Expression.OBJ_PATH) {

            return new Iterable<PathComponent<ObjAttribute, ObjRelationship>>() {

                public Iterator iterator() {
                    return new PathComponentIterator(ObjEntity.this, (String) pathExp.getOperand(0), aliasMap);
                }
            };
        }

        throw new ExpressionException(
                "Invalid expression type: '" + pathExp.expName() + "',  OBJ_PATH is expected.");
    }

    @Override
    public Iterator<CayenneMapEntry> resolvePathComponents(Expression pathExp) throws ExpressionException {

        // resolve DB_PATH if we can
        if (pathExp.getType() == Expression.DB_PATH) {
            if (getDbEntity() == null) {
                throw new ExpressionException("Can't resolve DB_PATH '" + pathExp + "', DbEntity is not set.");
            }

            return getDbEntity().resolvePathComponents(pathExp);
        }

        if (pathExp.getType() == Expression.OBJ_PATH) {
            return new PathIterator((String) pathExp.getOperand(0));
        }

        throw new ExpressionException(
                "Invalid expression type: '" + pathExp.expName() + "',  OBJ_PATH is expected.");
    }

    /**
     * Transforms an Expression to an analogous expression in terms of the
     * underlying DbEntity.
     * 
     * @since 1.1
     */
    public Expression translateToDbPath(Expression expression) {

        if (expression == null) {
            return null;
        }

        if (getDbEntity() == null) {
            throw new CayenneRuntimeException("Can't translate expression to DB_PATH, no DbEntity for '%s'.",
                    getName());
        }

        // converts all OBJ_PATH expressions to DB_PATH expressions
        // and pass control to the DB entity
        return expression.transform(new DBPathConverter());
    }

    /**
     * Transforms an Expression rooted in this entity to an analogous expression
     * rooted in related entity.
     * 
     * @since 1.1
     */
    @Override
    public Expression translateToRelatedEntity(Expression expression, String relationshipPath) {

        if (expression == null) {
            return null;
        }

        if (relationshipPath == null) {
            return expression;
        }

        if (getDbEntity() == null) {
            throw new CayenneRuntimeException("Can't transform expression, no DbEntity for '%s'.", getName());
        }

        // converts all OBJ_PATH expressions to DB_PATH expressions
        // and pass control to the DB entity

        DBPathConverter transformer = new DBPathConverter();

        String dbPath = transformer.toDbPath(createPathIterator(relationshipPath));
        Expression dbClone = expression.transform(transformer);

        return getDbEntity().translateToRelatedEntity(dbClone, dbPath);
    }

    private PathComponentIterator createPathIterator(String path) {
        return new PathComponentIterator(ObjEntity.this, path, new HashMap<String, String>());
        // TODO: do we need aliases here?
    }

    /**
     * @since 4.0
     */
    public Set<String> getCallbackMethods() {
        Set<String> res = new LinkedHashSet<>();
        for (CallbackDescriptor descriptor : getCallbackMap().getCallbacks()) {
            res.addAll(descriptor.getCallbackMethods());
        }
        return res;
    }

    final class DBPathConverter implements Transformer {

        // TODO: make it a public method - resolveDBPathComponents or
        // something...
        // seems generally useful

        String toDbPath(PathComponentIterator objectPathComponents) {
            StringBuilder buf = new StringBuilder();
            while (objectPathComponents.hasNext()) {
                PathComponent<Attribute, Relationship> component = objectPathComponents.next();

                Iterator<?> dbSubpath;

                if (component.getAttribute() != null) {
                    dbSubpath = ((ObjAttribute) component.getAttribute()).getDbPathIterator();
                } else if (component.getRelationship() != null) {
                    dbSubpath = ((ObjRelationship) component.getRelationship()).getDbRelationships().iterator();
                } else {
                    throw new CayenneRuntimeException("Unknown path component: %s", component);
                }

                while (dbSubpath.hasNext()) {
                    CayenneMapEntry subComponent = (CayenneMapEntry) dbSubpath.next();
                    if (buf.length() > 0) {
                        buf.append(Entity.PATH_SEPARATOR);
                    }

                    buf.append(subComponent.getName());

                    // use OUTER join for all components of the path is Obj path is OUTER
                    if (component.getJoinType() == JoinType.LEFT_OUTER) {
                        buf.append(OUTER_JOIN_INDICATOR);
                    }
                }
            }

            return buf.toString();
        }

        public Object transform(Object input) {

            if (!(input instanceof Expression)) {
                return input;
            }

            Expression expression = (Expression) input;

            if (expression.getType() != Expression.OBJ_PATH) {
                return input;
            }

            // convert obj_path to db_path

            String converted = toDbPath(createPathIterator((String) expression.getOperand(0)));
            Expression exp = ExpressionFactory.expressionOfType(Expression.DB_PATH);
            exp.setOperand(0, converted);
            return exp;
        }
    }

    /**
     * Returns the name of the underlying DbEntity.
     * 
     * @since 1.1
     */
    public String getDbEntityName() {
        return dbEntityName;
    }

    /**
     * Sets the name of underlying DbEntity.
     * 
     * @since 1.1
     */
    public void setDbEntityName(String string) {
        dbEntityName = string;
    }

    /**
     * ObjEntity property changed. May be name, attribute or relationship added
     * or removed, etc. Attribute and relationship property changes are handled
     * in respective listeners.
     * 
     * @since 1.2
     */
    public void objEntityChanged(EntityEvent e) {
        if ((e == null) || (e.getEntity() != this)) {
            // not our concern
            return;
        }

        // handle entity name changes
        if (e.getId() == EntityEvent.CHANGE && e.isNameChange()) {
            String oldName = e.getOldName();
            String newName = e.getNewName();

            DataMap map = getDataMap();
            if (map != null) {
                ObjEntity oe = (ObjEntity) e.getEntity();
                for (ObjRelationship relationship : oe.getRelationships()) {
                    relationship = relationship.getReverseRelationship();
                    if (null != relationship && relationship.targetEntityName.equals(oldName)) {
                        relationship.targetEntityName = newName;
                    }
                }
            }
        }
    }

    /** New entity has been created/added. */
    public void objEntityAdded(EntityEvent e) {
        // does nothing currently
    }

    /** Entity has been removed. */
    public void objEntityRemoved(EntityEvent e) {
        // does nothing currently
    }

    /**
     * Returns true if the default lifecycle listeners should not be notified of
     * this entity lifecycle events.
     * 
     * @since 3.0
     * @deprecated since 4.0 unused, as listeners are no longer mapped in a
     *             DataMap.
     */
    @Deprecated
    public boolean isExcludingDefaultListeners() {
        return excludingDefaultListeners;
    }

    /**
     * @deprecated since 4.0 unused, as listeners are no longer mapped in a
     *             DataMap.
     */
    @Deprecated
    public void setExcludingDefaultListeners(boolean excludingDefaultListeners) {
        this.excludingDefaultListeners = excludingDefaultListeners;
    }

    /**
     * Returns true if the lifeycle listeners defined on the superclasses should
     * not be notified of this entity lifecycle events.
     * 
     * @since 3.0
     * @deprecated since 4.0 unused, as listeners are no longer mapped in a
     *             DataMap.
     */
    @Deprecated
    public boolean isExcludingSuperclassListeners() {
        return excludingSuperclassListeners;
    }

    /**
     * @deprecated since 4.0 unused, as listeners are no longer mapped in a
     *             DataMap.
     */
    @Deprecated
    public void setExcludingSuperclassListeners(boolean excludingSuperclassListeners) {
        this.excludingSuperclassListeners = excludingSuperclassListeners;
    }
}