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

Java tutorial

Introduction

Here is the source code for org.apache.cayenne.map.DbEntity.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.ObjectId;
import org.apache.cayenne.configuration.ConfigurationNode;
import org.apache.cayenne.configuration.ConfigurationNodeVisitor;
import org.apache.cayenne.exp.Expression;
import org.apache.cayenne.exp.ExpressionException;
import org.apache.cayenne.exp.ExpressionFactory;
import org.apache.cayenne.map.event.AttributeEvent;
import org.apache.cayenne.map.event.DbAttributeListener;
import org.apache.cayenne.map.event.DbEntityListener;
import org.apache.cayenne.map.event.DbRelationshipListener;
import org.apache.cayenne.map.event.EntityEvent;
import org.apache.cayenne.map.event.MapEvent;
import org.apache.cayenne.map.event.RelationshipEvent;
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.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;

/**
 * A DbEntity is a mapping descriptor that defines a structure of a database
 * table.
 */
public class DbEntity extends Entity
        implements ConfigurationNode, DbEntityListener, DbAttributeListener, DbRelationshipListener {

    protected String catalog;
    protected String schema;
    protected Collection<DbAttribute> primaryKey;

    /**
     * @since 1.2
     */
    protected Collection<DbAttribute> generatedAttributes;
    protected DbKeyGenerator primaryKeyGenerator;

    /**
     * Qualifier, that will be applied to all select queries and joins with this
     * DbEntity
     */
    protected Expression qualifier;

    /**
     * Creates an unnamed DbEntity.
     */
    public DbEntity() {
        super();

        this.primaryKey = new ArrayList<>(2);
        this.generatedAttributes = new ArrayList<>(2);
    }

    /**
     * Creates a named DbEntity.
     */
    public DbEntity(String name) {
        this();
        this.setName(name);
    }

    @Override
    public DbRelationship getRelationship(String relName) {
        return (DbRelationship) super.getRelationship(relName);
    }

    @Override
    public DbAttribute getAttribute(String attributeName) {
        return (DbAttribute) super.getAttribute(attributeName);
    }

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

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

        if (getSchema() != null && getSchema().trim().length() > 0) {
            encoder.print(" schema=\"");
            encoder.print(Util.encodeXmlAttribute(getSchema().trim()));
            encoder.print('\"');
        }

        if (getCatalog() != null && getCatalog().trim().length() > 0) {
            encoder.print(" catalog=\"");
            encoder.print(Util.encodeXmlAttribute(getCatalog().trim()));
            encoder.print('\"');
        }

        encoder.println('>');

        encoder.indent(1);
        encoder.print(getAttributeMap());

        if (getPrimaryKeyGenerator() != null) {
            getPrimaryKeyGenerator().encodeAsXML(encoder);
        }

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

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

    /**
     * Returns table name including catalog and schema, if any of those are
     * present.
     */
    public String getFullyQualifiedName() {
        return ((catalog != null && !catalog.isEmpty()) ? catalog + '.' : "")
                + ((schema != null && !schema.isEmpty()) ? schema + '.' : "") + name;
    }

    /**
     * Returns database schema of this table.
     *
     * @return table's schema, null if not set.
     */
    public String getSchema() {
        return schema;
    }

    /**
     * Sets the database schema name of the table described by this DbEntity.
     */
    public void setSchema(String schema) {
        this.schema = schema;
    }

    /**
     * Returns the catalog name of the table described by this DbEntity.
     */
    public String getCatalog() {
        return catalog;
    }

    /**
     * Sets the catalog name of the table described by this DbEntity.
     */
    public void setCatalog(String catalog) {
        this.catalog = catalog;
    }

    /**
     * Returns an unmodifiable collection of DbAttributes representing the
     * primary key of the table described by this DbEntity.
     *
     * @since 3.0
     */
    public Collection<DbAttribute> getPrimaryKeys() {
        return Collections.unmodifiableCollection(primaryKey);
    }

    /**
     * Returns a Collection of all attributes that either belong to this
     * DbEntity or inherited.
     */
    @SuppressWarnings("unchecked")
    @Override
    public Collection<DbAttribute> getAttributes() {
        return (Collection<DbAttribute>) super.getAttributes();
    }

    /**
     * Returns an unmodifiable collection of DbAttributes that are generated by
     * the database.
     *
     * @since 1.2
     */
    public Collection<DbAttribute> getGeneratedAttributes() {
        return Collections.unmodifiableCollection(generatedAttributes);
    }

    /**
     * Adds a new attribute to this entity.
     *
     * @throws IllegalArgumentException if Attribute has no name or there is an existing attribute
     *                                  with the same name
     * @throws IllegalArgumentException if a relationship has the same name as this attribute
     * @since 3.0
     */
    public void addAttribute(DbAttribute attr) {
        super.addAttribute(attr);
        this.dbAttributeAdded(new AttributeEvent(this, attr, this, MapEvent.ADD));
    }

    /**
     * Removes attribute from the entity, removes any relationship joins
     * containing this attribute. Does nothing if the attribute name is not
     * found.
     *
     * @see org.apache.cayenne.map.Entity#removeAttribute(String)
     */
    @Override
    public void removeAttribute(String attrName) {
        Attribute attr = getAttribute(attrName);
        if (attr == null) {
            return;
        }

        DataMap map = getDataMap();
        if (map != null) {
            for (DbEntity ent : map.getDbEntities()) {
                for (DbRelationship relationship : ent.getRelationships()) {
                    Iterator<DbJoin> joins = relationship.getJoins().iterator();
                    while (joins.hasNext()) {
                        DbJoin join = joins.next();
                        if (join.getSource() == attr || join.getTarget() == attr) {
                            joins.remove();
                        }
                    }
                }
            }
        }

        super.removeAttribute(attrName);
        this.dbAttributeRemoved(new AttributeEvent(this, attr, this, MapEvent.REMOVE));
    }

    @Override
    public void clearAttributes() {
        super.clearAttributes();
        // post dummy event for no specific attribute
        this.dbAttributeRemoved(new AttributeEvent(this, null, this, MapEvent.REMOVE));
    }

    /**
     * Returns a Collection of relationships from this entity or inherited.
     */
    @SuppressWarnings("unchecked")
    @Override
    public Collection<DbRelationship> getRelationships() {
        return (Collection<DbRelationship>) super.getRelationships();
    }

    @SuppressWarnings("unchecked")
    @Override
    public SortedMap<String, DbRelationship> getRelationshipMap() {
        return (SortedMap<String, DbRelationship>) super.getRelationshipMap();
    }

    /**
     * @since 3.0
     */
    @Override
    @SuppressWarnings("unchecked")
    public PathComponent<DbAttribute, DbRelationship> 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<DbAttribute, DbRelationship>> resolvePath(final Expression pathExp,
            final Map aliasMap) {

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

            return new Iterable<PathComponent<DbAttribute, DbRelationship>>() {

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

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

    @Override
    public Iterator<CayenneMapEntry> resolvePathComponents(Expression pathExp) throws ExpressionException {
        if (pathExp.getType() != Expression.DB_PATH) {
            throw new ExpressionException(
                    "Invalid expression type: '" + pathExp.expName() + "',  DB_PATH is expected.");
        }

        return new PathIterator((String) pathExp.getOperand(0));
    }

    /**
     * Set the primary key generator for this entity. If null is passed, nothing
     * is changed.
     */
    public void setPrimaryKeyGenerator(DbKeyGenerator primaryKeyGenerator) {
        this.primaryKeyGenerator = primaryKeyGenerator;
        if (primaryKeyGenerator != null) {
            primaryKeyGenerator.setDbEntity(this);
        }
    }

    /**
     * Return the primary key generator for this entity.
     */
    public DbKeyGenerator getPrimaryKeyGenerator() {
        return primaryKeyGenerator;
    }

    /**
     * DbEntity property changed event. 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 dbEntityChanged(EntityEvent e) {
        if (e == null || e.getEntity() != this) {
            // not our concern
            return;
        }

        // handle entity name changes
        if (e.getId() == EntityEvent.CHANGE && e.isNameChange()) {
            String newName = e.getNewName();
            DataMap map = getDataMap();
            if (map != null) {
                // handle all of the relationship target names that need to be
                // changed
                for (DbEntity dbe : map.getDbEntities()) {
                    for (DbRelationship relationship : dbe.getRelationships()) {
                        if (relationship.getTargetEntity() == this) {
                            relationship.setTargetEntityName(newName);
                        }
                    }
                }
                // get all of the related object entities
                for (ObjEntity oe : map.getMappedEntities(this)) {
                    if (oe.getDbEntity() == this) {
                        oe.setDbEntityName(newName);
                    }
                }
            }
        }
    }

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

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

    public void dbAttributeAdded(AttributeEvent e) {
        this.handleAttributeUpdate(e);
    }

    public void dbAttributeChanged(AttributeEvent e) {
        this.handleAttributeUpdate(e);
    }

    public void dbAttributeRemoved(AttributeEvent e) {
        this.handleAttributeUpdate(e);
    }

    private void handleAttributeUpdate(AttributeEvent e) {
        if (e == null || e.getEntity() != this) {
            // not our concern
            return;
        }

        // catch clearing (event with null ('any') DbAttribute)
        Attribute attribute = e.getAttribute();
        if (attribute == null && this.attributes.isEmpty()) {
            this.primaryKey.clear();
            this.generatedAttributes.clear();
            return;
        }

        // make sure we handle a DbAttribute
        if (!(attribute instanceof DbAttribute)) {
            return;
        }

        DbAttribute dbAttribute = (DbAttribute) attribute;

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

            DataMap map = getDataMap();
            if (map != null) {
                for (DbEntity ent : map.getDbEntities()) {

                    // handle all of the dependent object entity attribute changes
                    for (ObjEntity oe : map.getMappedEntities(ent)) {
                        for (ObjAttribute attr : oe.getAttributes()) {
                            if (attr.getDbAttribute() == dbAttribute) {
                                attr.setDbAttributePath(newName);
                            }
                        }
                    }

                    // handle all of the relationships / joins that use the
                    // changed attribute
                    for (DbRelationship rel : ent.getRelationships()) {
                        for (DbJoin join : rel.getJoins()) {
                            if (join.getSource() == dbAttribute) {
                                join.setSourceName(newName);
                            }
                            if (join.getTarget() == dbAttribute) {
                                join.setTargetName(newName);
                            }
                        }
                    }
                }
            }

            // clear the attribute out of the collection
            attributes.remove(oldName);

            // add the attribute back in with the new name
            super.addAttribute(dbAttribute);
        }

        // handle PK refresh
        if (primaryKey.contains(dbAttribute) || dbAttribute.isPrimaryKey()) {
            switch (e.getId()) {
            case MapEvent.ADD:
                this.primaryKey.add(dbAttribute);
                break;

            case MapEvent.REMOVE:
                this.primaryKey.remove(dbAttribute);
                break;

            default:
                // generic update
                this.primaryKey.clear();
                for (DbAttribute next : getAttributes()) {
                    if (next.isPrimaryKey()) {
                        this.primaryKey.add(next);
                    }
                }
            }
        }

        // handle generated key refresh
        if (generatedAttributes.contains(dbAttribute) || dbAttribute.isGenerated()) {
            switch (e.getId()) {
            case MapEvent.ADD:
                this.generatedAttributes.add(dbAttribute);
                break;

            case MapEvent.REMOVE:
                this.generatedAttributes.remove(dbAttribute);
                break;

            default:
                // generic update
                this.generatedAttributes.clear();
                for (DbAttribute next : getAttributes()) {
                    if (next.isGenerated()) {
                        this.generatedAttributes.add(next);
                    }
                }
            }
        }
    }

    /**
     * Relationship property changed.
     */
    public void dbRelationshipChanged(RelationshipEvent e) {
        if (e == null || e.getEntity() != this) {
            // not our concern
            return;
        }

        Relationship rel = e.getRelationship();
        // make sure we handle a DbRelationship
        if (!(rel instanceof DbRelationship)) {
            return;
        }

        DbRelationship dbRel = (DbRelationship) rel;

        // handle relationship name changes
        if (e.getId() == RelationshipEvent.CHANGE && e.isNameChange()) {
            String oldName = e.getOldName();

            DataMap map = getDataMap();
            if (map != null) {

                // updating dbAttributePaths for attributes of all ObjEntities
                for (ObjEntity objEntity : map.getObjEntities()) {

                    for (ObjAttribute attribute : objEntity.getAttributes()) {
                        attribute.updateDbAttributePath();
                    }
                }
            }

            // clear the relationship out of the collection
            relationships.remove(oldName);

            // add the relationship back in with the new name
            super.addRelationship(dbRel);
        }
    }

    /**
     * Relationship has been created/added.
     */
    public void dbRelationshipAdded(RelationshipEvent e) {
        // does nothing currently
    }

    /**
     * Relationship has been removed.
     */
    public void dbRelationshipRemoved(RelationshipEvent e) {
        // does nothing currently
    }

    /**
     * @return qualifier that will be ANDed to all select queries with this
     * entity
     */
    public Expression getQualifier() {
        return qualifier;
    }

    /**
     * Sets qualifier for this entity
     */
    public void setQualifier(Expression qualifier) {
        this.qualifier = qualifier;
    }

    /**
     * Returns true if there is full replacement id is attached to an ObjectId.
     * "Full" means that all PK columns are present and only PK columns are
     * present.
     *
     * @since 1.2
     */
    public boolean isFullReplacementIdAttached(ObjectId id) {
        if (!id.isReplacementIdAttached()) {
            return false;
        }

        Map<String, Object> replacement = id.getReplacementIdMap();
        Collection<DbAttribute> pk = getPrimaryKeys();
        if (pk.size() != replacement.size()) {
            return false;
        }

        for (DbAttribute attribute : pk) {
            if (!replacement.containsKey(attribute.getName())) {
                return false;
            }
        }

        return true;
    }

    public Collection<ObjEntity> mappedObjEntities() {
        Collection<ObjEntity> objEntities = new HashSet<ObjEntity>();
        MappingNamespace mns = getDataMap().getNamespace();
        if (mns != null) {
            for (ObjEntity objEntity : mns.getObjEntities()) {
                if (equals(objEntity.getDbEntity())) {
                    objEntities.add(objEntity);
                }
            }
        }
        return objEntities;
    }

    /**
     * Transforms 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;
        }

        return expression.transform(new RelationshipPathConverter(relationshipPath));
    }

    final class RelationshipPathConverter implements Transformer {

        String relationshipPath;
        boolean toMany;

        RelationshipPathConverter(String relationshipPath) {
            this.relationshipPath = relationshipPath;

            Iterator<CayenneMapEntry> relationshipIt = resolvePathComponents(relationshipPath);
            while (relationshipIt.hasNext()) {
                // relationship path components must be DbRelationships
                DbRelationship nextDBR = (DbRelationship) relationshipIt.next();

                if (nextDBR.isToMany()) {
                    toMany = true;
                    break;
                }
            }
        }

        public Object transform(Object input) {
            if (!(input instanceof Expression)) {
                return input;
            }

            Expression expression = (Expression) input;

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

            String path = (String) expression.getOperand(0);
            String converted = translatePath(path);
            Expression transformed = ExpressionFactory.expressionOfType(Expression.DB_PATH);
            transformed.setOperand(0, converted);
            return transformed;
        }

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

        String translatePath(String path) {

            // algorithm to determine the translated path:
            // 0. If relationship has at least one to-many component, travel all
            // the way
            // back, and then all the way forward
            // 1. If relationship path equals to input, travel one step back,
            // and then one
            // step forward.
            // 2. If input completely includes relationship path, use input's
            // remaining
            // tail.
            // 3. If relationship path and input have none or some leading
            // components in
            // common,
            // (a) strip common leading part;
            // (b) reverse the remaining relationship part;
            // (c) append remaining input to the reversed remaining
            // relationship.

            // case (0)
            if (toMany) {
                PathComponentIterator pathIt = createPathIterator(path);
                Iterator<CayenneMapEntry> relationshipIt = resolvePathComponents(relationshipPath);

                // for inserts from the both ends use LinkedList
                LinkedList<String> finalPath = new LinkedList<String>();

                // append remainder of the relationship, reversing it
                while (relationshipIt.hasNext()) {
                    DbRelationship nextDBR = (DbRelationship) relationshipIt.next();
                    prependReversedPath(finalPath, nextDBR);
                }

                while (pathIt.hasNext()) {
                    // components may be attributes or relationships
                    PathComponent<Attribute, Relationship> component = pathIt.next();
                    appendPath(finalPath, component);
                }

                return convertToPath(finalPath);
            }
            // case (1)
            if (path.equals(relationshipPath)) {

                LinkedList<String> finalPath = new LinkedList<String>();
                PathComponentIterator pathIt = createPathIterator(path);

                // just do one step back and one step forward to create correct
                // joins...
                // find last rel...
                DbRelationship lastDBR = null;

                while (pathIt.hasNext()) {
                    // relationship path components must be DbRelationships
                    lastDBR = (DbRelationship) pathIt.next().getRelationship();
                }

                if (lastDBR != null) {
                    prependReversedPath(finalPath, lastDBR);
                    appendPath(finalPath, lastDBR);
                }

                return convertToPath(finalPath);
            }

            // case (2)
            String relationshipPathWithDot = relationshipPath + Entity.PATH_SEPARATOR;
            if (path.startsWith(relationshipPathWithDot)) {
                return path.substring(relationshipPathWithDot.length());
            }

            // case (3)
            PathComponentIterator pathIt = createPathIterator(path);
            Iterator<CayenneMapEntry> relationshipIt = resolvePathComponents(relationshipPath);

            // for inserts from the both ends use LinkedList
            LinkedList<String> finalPath = new LinkedList<String>();

            while (relationshipIt.hasNext() && pathIt.hasNext()) {
                // relationship path components must be DbRelationships
                DbRelationship nextDBR = (DbRelationship) relationshipIt.next();

                // expression components may be attributes or relationships
                PathComponent<Attribute, Relationship> component = pathIt.next();

                if (nextDBR != component.getRelationship()) {
                    // found split point
                    // consume the last iteration components,
                    // then break out to finish the iterators independently
                    prependReversedPath(finalPath, nextDBR);
                    appendPath(finalPath, component);
                    break;
                }

                break;
            }

            // append remainder of the relationship, reversing it
            while (relationshipIt.hasNext()) {
                DbRelationship nextDBR = (DbRelationship) relationshipIt.next();
                prependReversedPath(finalPath, nextDBR);
            }

            while (pathIt.hasNext()) {
                // components may be attributes or relationships
                PathComponent<Attribute, Relationship> component = pathIt.next();
                appendPath(finalPath, component);
            }

            return convertToPath(finalPath);
        }

        private String convertToPath(List<String> path) {
            StringBuilder converted = new StringBuilder();
            int len = path.size();
            for (int i = 0; i < len; i++) {
                if (i > 0) {
                    converted.append(Entity.PATH_SEPARATOR);
                }

                converted.append(path.get(i));
            }

            return converted.toString();
        }

        private void prependReversedPath(LinkedList<String> finalPath, DbRelationship relationship) {
            DbRelationship revNextDBR = relationship.getReverseRelationship();

            if (revNextDBR == null) {
                throw new CayenneRuntimeException("Unable to find reverse DbRelationship for %s.%s.",
                        relationship.getSourceEntity().getName(), relationship.getName());
            }

            finalPath.addFirst(revNextDBR.getName());
        }

        private void appendPath(LinkedList<String> finalPath, CayenneMapEntry pathComponent) {
            finalPath.addLast(pathComponent.getName());
        }

        private void appendPath(LinkedList<String> finalPath,
                PathComponent<Attribute, Relationship> pathComponent) {
            CayenneMapEntry mapEntry = pathComponent.getAttribute() != null ? pathComponent.getAttribute()
                    : pathComponent.getRelationship();
            String name = mapEntry.getName();
            if (pathComponent.getJoinType() == JoinType.LEFT_OUTER) {
                name += OUTER_JOIN_INDICATOR;
            }

            finalPath.addLast(name);
        }
    }
}