org.apache.cayenne.merge.DbMerger.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.cayenne.merge.DbMerger.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.merge;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import javax.sql.DataSource;

import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.access.DataNode;
import org.apache.cayenne.access.DbLoader;
import org.apache.cayenne.access.loader.DbLoaderConfiguration;
import org.apache.cayenne.access.loader.LoggingDbLoaderDelegate;
import org.apache.cayenne.access.loader.filters.FiltersConfig;
import org.apache.cayenne.dba.DbAdapter;
import org.apache.cayenne.map.Attribute;
import org.apache.cayenne.map.DataMap;
import org.apache.cayenne.map.DbAttribute;
import org.apache.cayenne.map.DbEntity;
import org.apache.cayenne.map.DbJoin;
import org.apache.cayenne.map.DbRelationship;
import org.apache.cayenne.map.DetectedDbEntity;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Traverse a {@link DataNode} and a {@link DataMap} and create a group of
 * {@link MergerToken}s to alter the {@link DataNode} data store to match the
 * {@link DataMap}.
 * 
 */
public class DbMerger {

    private static final Log LOGGER = LogFactory.getLog(DbMerger.class);

    private final MergerFactory factory;

    private final ValueForNullProvider valueForNull;

    public DbMerger(MergerFactory factory) {
        this(factory, null);
    }

    public DbMerger(MergerFactory factory, ValueForNullProvider valueForNull) {
        this.factory = factory;
        this.valueForNull = valueForNull == null ? new EmptyValueForNullProvider() : valueForNull;
    }

    /**
     * Create and return a {@link List} of {@link MergerToken}s to alter the
     * given {@link DataNode} to match the given {@link DataMap}
     */
    public List<MergerToken> createMergeTokens(DataSource dataSource, DbAdapter adapter, DataMap existingDataMap,
            DbLoaderConfiguration config) {
        return createMergeTokens(existingDataMap, loadDataMapFromDb(dataSource, adapter, config), config);
    }

    /**
     * Create and return a {@link List} of {@link MergerToken}s to alter the
     * given {@link DataNode} to match the given {@link DataMap}
     */
    public List<MergerToken> createMergeTokens(DataMap existing, DataMap loadedFomDb,
            DbLoaderConfiguration config) {

        loadedFomDb.setQuotingSQLIdentifiers(existing.isQuotingSQLIdentifiers());

        List<MergerToken> tokens = createMergeTokens(filter(existing, config.getFiltersConfig()),
                loadedFomDb.getDbEntities(), config);

        // sort. use a custom Comparator since only toDb tokens are comparable
        // by now
        Collections.sort(tokens, new Comparator<MergerToken>() {

            public int compare(MergerToken o1, MergerToken o2) {
                if (o1 instanceof AbstractToDbToken && o2 instanceof AbstractToDbToken) {

                    return ((AbstractToDbToken) o1).compareTo(o2);
                }
                return 0;
            }
        });

        return tokens;
    }

    private Collection<DbEntity> filter(DataMap existing, FiltersConfig filtersConfig) {
        Collection<DbEntity> existingFiltered = new LinkedList<DbEntity>();
        for (DbEntity entity : existing.getDbEntities()) {
            if (filtersConfig.tableFilter(entity.getCatalog(), entity.getSchema())
                    .isIncludeTable(entity.getName()) != null) {
                existingFiltered.add(entity);
            }
        }
        return existingFiltered;
    }

    private DataMap loadDataMapFromDb(DataSource dataSource, DbAdapter adapter, DbLoaderConfiguration config) {
        try (Connection conn = dataSource.getConnection();) {

            return new DbLoader(conn, adapter, new LoggingDbLoaderDelegate(LOGGER)).load(config);
        } catch (SQLException e) {
            throw new CayenneRuntimeException("Can't doLoad dataMap from db.", e);
        }
    }

    public List<MergerToken> createMergeTokens(Collection<DbEntity> existing, Collection<DbEntity> loadedFromDb,
            DbLoaderConfiguration config) {
        Collection<DbEntity> dbEntitiesToDrop = new LinkedList<DbEntity>(loadedFromDb);

        List<MergerToken> tokens = new LinkedList<MergerToken>();
        for (DbEntity dbEntity : existing) {
            String tableName = dbEntity.getName();

            // look for table
            DbEntity detectedEntity = findDbEntity(loadedFromDb, tableName);
            if (detectedEntity == null) {
                tokens.add(factory.createCreateTableToDb(dbEntity));
                // TODO: does this work properly with createReverse?
                for (DbRelationship rel : dbEntity.getRelationships()) {
                    tokens.add(factory.createAddRelationshipToDb(dbEntity, rel));
                }
                continue;
            }

            dbEntitiesToDrop.remove(detectedEntity);

            tokens.addAll(checkRelationshipsToDrop(dbEntity, detectedEntity));
            if (!config.isSkipRelationshipsLoading()) {
                tokens.addAll(checkRelationshipsToAdd(dbEntity, detectedEntity));
            }
            tokens.addAll(checkRows(dbEntity, detectedEntity));

            if (!config.isSkipPrimaryKeyLoading()) {
                MergerToken token = checkPrimaryKeyChange(dbEntity, detectedEntity);
                if (token != null) {
                    tokens.add(token);
                }
            }
        }

        // drop table
        // TODO: support drop table. currently, too many tables are marked for
        // drop
        for (DbEntity e : dbEntitiesToDrop) {
            tokens.add(factory.createDropTableToDb(e));
            for (DbRelationship relationship : e.getRelationships()) {
                DbEntity detectedEntity = findDbEntity(existing, relationship.getTargetEntityName());
                if (detectedEntity != null) {
                    tokens.add(factory.createDropRelationshipToDb(detectedEntity,
                            relationship.getReverseRelationship()));
                }
            }
        }

        return tokens;
    }

    private List<MergerToken> checkRows(DbEntity existing, DbEntity loadedFromDb) {
        List<MergerToken> tokens = new LinkedList<MergerToken>();

        // columns to drop
        for (DbAttribute detected : loadedFromDb.getAttributes()) {
            if (findDbAttribute(existing, detected.getName()) == null) {
                tokens.add(factory.createDropColumnToDb(existing, detected));
            }
        }

        // columns to add or modify
        for (DbAttribute attr : existing.getAttributes()) {
            String columnName = attr.getName().toUpperCase();

            DbAttribute detected = findDbAttribute(loadedFromDb, columnName);

            if (detected == null) {
                tokens.add(factory.createAddColumnToDb(existing, attr));
                if (attr.isMandatory()) {
                    if (valueForNull.hasValueFor(existing, attr)) {
                        tokens.add(factory.createSetValueForNullToDb(existing, attr, valueForNull));
                    }
                    tokens.add(factory.createSetNotNullToDb(existing, attr));
                }
                continue;
            }

            // check for not null
            if (attr.isMandatory() != detected.isMandatory()) {
                if (attr.isMandatory()) {
                    if (valueForNull.hasValueFor(existing, attr)) {
                        tokens.add(factory.createSetValueForNullToDb(existing, attr, valueForNull));
                    }
                    tokens.add(factory.createSetNotNullToDb(existing, attr));
                } else {
                    tokens.add(factory.createSetAllowNullToDb(existing, attr));
                }
            }

            // TODO: check more types than char/varchar
            // TODO: psql report VARCHAR for text column, not clob
            switch (detected.getType()) {
            case Types.VARCHAR:
            case Types.CHAR:
                if (attr.getMaxLength() != detected.getMaxLength()) {
                    tokens.add(factory.createSetColumnTypeToDb(existing, detected, attr));
                }
                break;
            }
        }

        return tokens;
    }

    private List<MergerToken> checkRelationshipsToDrop(DbEntity dbEntity, DbEntity detectedEntity) {
        List<MergerToken> tokens = new LinkedList<MergerToken>();

        // relationships to drop
        for (DbRelationship detected : detectedEntity.getRelationships()) {
            if (findDbRelationship(dbEntity, detected) == null) {

                // alter detected relationship to match entity and attribute
                // names.
                // (case sensitively)

                DbEntity targetEntity = findDbEntity(dbEntity.getDataMap().getDbEntities(),
                        detected.getTargetEntityName());
                if (targetEntity == null) {
                    continue;
                }

                detected.setSourceEntity(dbEntity);
                detected.setTargetEntityName(targetEntity);

                // manipulate the joins to match the DbAttributes in the model
                for (DbJoin join : detected.getJoins()) {
                    DbAttribute sattr = findDbAttribute(dbEntity, join.getSourceName());
                    if (sattr != null) {
                        join.setSourceName(sattr.getName());
                    }
                    DbAttribute tattr = findDbAttribute(targetEntity, join.getTargetName());
                    if (tattr != null) {
                        join.setTargetName(tattr.getName());
                    }
                }

                MergerToken token = factory.createDropRelationshipToDb(dbEntity, detected);
                if (detected.isToMany()) {
                    // default toModel as we can not do drop a toMany in the db.
                    // only
                    // toOne are represented using foreign key
                    token = token.createReverse(factory);
                }
                tokens.add(token);
            }
        }

        return tokens;
    }

    private List<MergerToken> checkRelationshipsToAdd(DbEntity dbEntity, DbEntity detectedEntity) {

        List<MergerToken> tokens = new LinkedList<MergerToken>();

        for (DbRelationship rel : dbEntity.getRelationships()) {
            if (findDbRelationship(detectedEntity, rel) == null) {
                AddRelationshipToDb token = (AddRelationshipToDb) factory.createAddRelationshipToDb(dbEntity, rel);

                if (token.shouldGenerateFkConstraint()) {
                    // TODO I guess we should add relationship always; in order
                    // to have ability
                    // TODO generate reverse relationship. If it doesn't have
                    // anything to execute it will be passed
                    // TODO through execution without any affect on db
                    tokens.add(token);
                }
            }
        }

        return tokens;
    }

    private MergerToken checkPrimaryKeyChange(DbEntity dbEntity, DbEntity detectedEntity) {
        Collection<DbAttribute> primaryKeyOriginal = detectedEntity.getPrimaryKeys();
        Collection<DbAttribute> primaryKeyNew = dbEntity.getPrimaryKeys();

        String primaryKeyName = null;
        if (detectedEntity instanceof DetectedDbEntity) {
            primaryKeyName = ((DetectedDbEntity) detectedEntity).getPrimaryKeyName();
        }

        if (upperCaseEntityNames(primaryKeyOriginal).equals(upperCaseEntityNames(primaryKeyNew))) {
            return null;
        }

        return factory.createSetPrimaryKeyToDb(dbEntity, primaryKeyOriginal, primaryKeyNew, primaryKeyName);
    }

    private Set<String> upperCaseEntityNames(Collection<? extends Attribute> attrs) {
        Set<String> names = new HashSet<String>();
        for (Attribute attr : attrs) {
            names.add(attr.getName().toUpperCase());
        }
        return names;
    }

    /**
     * case insensitive search for a {@link DbEntity} in a {@link DataMap} by
     * name
     */
    private DbEntity findDbEntity(Collection<DbEntity> dbEntities, String caseInsensitiveName) {
        // TODO: create a Map with upper case keys?
        for (DbEntity e : dbEntities) {
            if (e.getName().equalsIgnoreCase(caseInsensitiveName)) {
                return e;
            }
        }
        return null;
    }

    /**
     * case insensitive search for a {@link DbAttribute} in a {@link DbEntity}
     * by name
     */
    private DbAttribute findDbAttribute(DbEntity entity, String caseInsensitiveName) {
        for (DbAttribute a : entity.getAttributes()) {
            if (a.getName().equalsIgnoreCase(caseInsensitiveName)) {
                return a;
            }
        }
        return null;
    }

    /**
     * search for a {@link DbRelationship} like rel in the given
     * {@link DbEntity}
     */
    private DbRelationship findDbRelationship(DbEntity entity, DbRelationship rel) {
        for (DbRelationship candidate : entity.getRelationships()) {
            if (equalDbJoinCollections(candidate.getJoins(), rel.getJoins())) {
                return candidate;
            }
        }
        return null;
    }

    /**
     * Return true if the two unordered {@link Collection}s of {@link DbJoin}s
     * are equal. Entity and Attribute names are compared case insensitively.
     *
     * TODO complexity n^2; sort both collection and go through them to compare
     * = 2*n*log(n) + n
     */
    private static boolean equalDbJoinCollections(Collection<DbJoin> j1s, Collection<DbJoin> j2s) {
        if (j1s.size() != j2s.size()) {
            return false;
        }

        for (DbJoin j1 : j1s) {
            if (!havePair(j2s, j1)) {
                return false;
            }
        }

        return true;
    }

    private static boolean havePair(Collection<DbJoin> j2s, DbJoin j1) {
        for (DbJoin j2 : j2s) {
            if (!isNull(j1.getSource()) && !isNull(j1.getTarget()) && !isNull(j2.getSource())
                    && !isNull(j2.getTarget())
                    && j1.getSource().getEntity().getName().equalsIgnoreCase(j2.getSource().getEntity().getName())
                    && j1.getTarget().getEntity().getName().equalsIgnoreCase(j2.getTarget().getEntity().getName())
                    && j1.getSourceName().equalsIgnoreCase(j2.getSourceName())
                    && j1.getTargetName().equalsIgnoreCase(j2.getTargetName())) {

                return true;
            }
        }
        return false;
    }

    private static boolean isNull(DbAttribute attribute) {
        return attribute == null || attribute.getEntity() == null;
    }
}