org.jumpmind.db.alter.ModelComparator.java Source code

Java tutorial

Introduction

Here is the source code for org.jumpmind.db.alter.ModelComparator.java

Source

package org.jumpmind.db.alter;

/*
 * 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.
 */

import java.sql.Types;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import org.apache.commons.lang.StringUtils;
import org.jumpmind.db.model.Column;
import org.jumpmind.db.model.Database;
import org.jumpmind.db.model.ForeignKey;
import org.jumpmind.db.model.IIndex;
import org.jumpmind.db.model.Table;
import org.jumpmind.db.platform.DatabaseInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Compares two database models and creates change objects that express how to
 * adapt the first model so that it becomes the second one. Neither of the
 * models are changed in the process, however, it is also assumed that the
 * models do not change in between.
 */
public class ModelComparator {

    /** The log for this comparator. */
    private final Logger log = LoggerFactory.getLogger(ModelComparator.class);

    /** The platform information. */
    protected DatabaseInfo platformInfo;

    /** Whether comparison is case sensitive. */
    protected boolean caseSensitive;

    protected String databaseName;

    /**
     * Creates a new model comparator object.
     * 
     * @param platformInfo
     *            The platform info
     * @param caseSensitive
     *            Whether comparison is case sensitive
     */
    public ModelComparator(String databaseName, DatabaseInfo platformInfo, boolean caseSensitive) {
        this.databaseName = databaseName;
        this.platformInfo = platformInfo;
        this.caseSensitive = caseSensitive;
    }

    /**
     * Compares the two models and returns the changes necessary to create the
     * second model from the first one.
     * 
     * @param sourceModel
     *            The source model
     * @param targetModel
     *            The target model
     * @return The changes
     */
    public List<IModelChange> compare(Database sourceModel, Database targetModel) {
        ArrayList<IModelChange> changes = new ArrayList<IModelChange>();

        for (int tableIdx = 0; tableIdx < targetModel.getTableCount(); tableIdx++) {
            Table targetTable = targetModel.getTable(tableIdx);
            Table sourceTable = sourceModel.findTable(targetTable.getName(), caseSensitive);

            if (sourceTable == null) {
                log.debug("Table {} needs to be added", targetTable.getName());
                changes.add(new AddTableChange(targetTable));
                if (platformInfo.isForeignKeysSupported()) {
                    for (int fkIdx = 0; fkIdx < targetTable.getForeignKeyCount(); fkIdx++) {
                        // we have to use target table's definition here because the
                        // complete table is new
                        changes.add(new AddForeignKeyChange(targetTable, targetTable.getForeignKey(fkIdx)));
                    }
                }
            } else {
                changes.addAll(compareTables(sourceModel, sourceTable, targetModel, targetTable));
            }
        }

        for (int tableIdx = 0; tableIdx < sourceModel.getTableCount(); tableIdx++) {
            Table sourceTable = sourceModel.getTable(tableIdx);
            Table targetTable = targetModel.findTable(sourceTable.getName(), caseSensitive);

            if ((targetTable == null) && (sourceTable.getName() != null) && (sourceTable.getName().length() > 0)) {
                log.debug("Table {} needs to be removed", sourceTable.getName());
                changes.add(new RemoveTableChange(sourceTable));
                /*
                 * we assume that the target model is sound, ie. that there are
                 * no longer any foreign keys to this table in the target model;
                 * thus we already have removeFK changes for these from the
                 * compareTables method and we only need to create changes for
                 * the fks originating from this table
                 */
                if (platformInfo.isForeignKeysSupported()) {
                    for (int fkIdx = 0; fkIdx < sourceTable.getForeignKeyCount(); fkIdx++) {
                        changes.add(new RemoveForeignKeyChange(sourceTable, sourceTable.getForeignKey(fkIdx)));
                    }
                }
            }
        }
        return changes;
    }

    /**
     * Compares the two tables and returns the changes necessary to create the
     * second table from the first one.
     * 
     * @param sourceModel
     *            The source model which contains the source table
     * @param sourceTable
     *            The source table
     * @param targetModel
     *            The target model which contains the target table
     * @param targetTable
     *            The target table
     * @return The changes
     */
    public List<IModelChange> compareTables(Database sourceModel, Table sourceTable, Database targetModel,
            Table targetTable) {
        ArrayList<IModelChange> changes = new ArrayList<IModelChange>();

        if (platformInfo.isForeignKeysSupported()) {

            for (int fkIdx = 0; fkIdx < sourceTable.getForeignKeyCount(); fkIdx++) {
                ForeignKey sourceFk = sourceTable.getForeignKey(fkIdx);
                ForeignKey targetFk = findCorrespondingForeignKey(targetTable, sourceFk);

                if (targetFk == null) {
                    if (log.isDebugEnabled()) {
                        log.debug(sourceFk + " needs to be removed from table " + sourceTable.getName());
                    }
                    changes.add(new RemoveForeignKeyChange(sourceTable, sourceFk));
                }
            }

            for (int fkIdx = 0; fkIdx < targetTable.getForeignKeyCount(); fkIdx++) {
                ForeignKey targetFk = targetTable.getForeignKey(fkIdx);
                ForeignKey sourceFk = findCorrespondingForeignKey(sourceTable, targetFk);

                if (sourceFk == null) {
                    if (log.isDebugEnabled()) {
                        log.debug(targetFk + " needs to be created for table " + sourceTable.getName());
                    }
                    /*
                     * we have to use the target table here because the foreign
                     * key might reference a new column
                     */
                    changes.add(new AddForeignKeyChange(targetTable, targetFk));
                }
            }
        }
        if (platformInfo.isIndicesSupported()) {
            for (int indexIdx = 0; indexIdx < sourceTable.getIndexCount(); indexIdx++) {
                IIndex sourceIndex = sourceTable.getIndex(indexIdx);
                IIndex targetIndex = findCorrespondingIndex(targetTable, sourceIndex);

                if (targetIndex == null) {
                    if (log.isDebugEnabled()) {
                        log.debug("Index " + sourceIndex.getName() + " needs to be removed from table "
                                + sourceTable.getName());
                    }
                    changes.add(new RemoveIndexChange(sourceTable, sourceIndex));
                }
            }
            for (int indexIdx = 0; indexIdx < targetTable.getIndexCount(); indexIdx++) {
                IIndex targetIndex = targetTable.getIndex(indexIdx);
                IIndex sourceIndex = findCorrespondingIndex(sourceTable, targetIndex);

                if (sourceIndex == null) {
                    if (log.isDebugEnabled()) {
                        log.debug("Index " + targetIndex.getName() + " needs to be created for table "
                                + sourceTable.getName());
                    }
                    // we have to use the target table here because the index might
                    // reference a new column
                    changes.add(new AddIndexChange(targetTable, targetIndex));
                }
            }
        }

        HashMap<Column, TableChange> addColumnChanges = new HashMap<Column, TableChange>();

        for (int columnIdx = 0; columnIdx < targetTable.getColumnCount(); columnIdx++) {
            Column targetColumn = targetTable.getColumn(columnIdx);
            Column sourceColumn = sourceTable.findColumn(targetColumn.getName(), caseSensitive);

            if (sourceColumn == null) {
                log.debug("Column {} needs to be created for table {}",
                        new Object[] { targetColumn.getName(), sourceTable.getName() });

                AddColumnChange change = new AddColumnChange(sourceTable, targetColumn,
                        columnIdx > 0 ? targetTable.getColumn(columnIdx - 1) : null,
                        columnIdx < targetTable.getColumnCount() - 1 ? targetTable.getColumn(columnIdx + 1) : null);

                changes.add(change);
                addColumnChanges.put(targetColumn, change);
            } else {
                changes.addAll(compareColumns(sourceTable, sourceColumn, targetTable, targetColumn));
            }
        }
        // if the last columns in the target table are added, then we note this
        // at the changes
        for (int columnIdx = targetTable.getColumnCount() - 1; columnIdx >= 0; columnIdx--) {
            Column targetColumn = targetTable.getColumn(columnIdx);
            AddColumnChange change = (AddColumnChange) addColumnChanges.get(targetColumn);

            if (change == null) {
                // column was not added, so we can ignore any columns before it
                // that were added
                break;
            } else {
                change.setAtEnd(true);
            }
        }

        Column[] sourcePK = sourceTable.getPrimaryKeyColumns();
        Column[] targetPK = targetTable.getPrimaryKeyColumns();

        if ((sourcePK.length == 0) && (targetPK.length > 0)) {
            if (log.isDebugEnabled()) {
                log.debug("A primary key needs to be added to the table " + sourceTable.getName());
            }
            // we have to use the target table here because the primary key
            // might
            // reference a new column
            changes.add(new AddPrimaryKeyChange(targetTable, targetPK));
        } else if ((targetPK.length == 0) && (sourcePK.length > 0)) {
            if (log.isDebugEnabled()) {
                log.debug("The primary key needs to be removed from the table " + sourceTable.getName());
            }
            changes.add(new RemovePrimaryKeyChange(sourceTable, sourcePK));
        } else if ((sourcePK.length > 0) && (targetPK.length > 0)) {
            boolean changePK = false;

            if (sourcePK.length != targetPK.length) {
                changePK = true;
            } else {
                for (int pkColumnIdx = 0; (pkColumnIdx < sourcePK.length) && !changePK; pkColumnIdx++) {
                    if ((caseSensitive && !sourcePK[pkColumnIdx].getName().equals(targetPK[pkColumnIdx].getName()))
                            || (!caseSensitive && !sourcePK[pkColumnIdx].getName()
                                    .equalsIgnoreCase(targetPK[pkColumnIdx].getName()))) {
                        changePK = true;
                    }
                }
            }
            if (changePK) {
                if (log.isDebugEnabled()) {
                    log.debug("The primary key of table " + sourceTable.getName() + " needs to be changed");
                }
                changes.add(new PrimaryKeyChange(sourceTable, sourcePK, targetPK));
            }
        }

        for (int columnIdx = 0; columnIdx < sourceTable.getColumnCount(); columnIdx++) {
            Column sourceColumn = sourceTable.getColumn(columnIdx);
            Column targetColumn = targetTable.findColumn(sourceColumn.getName(), caseSensitive);

            if (targetColumn == null) {
                if (log.isDebugEnabled()) {
                    log.debug("Column " + sourceColumn.getName() + " needs to be removed from table "
                            + sourceTable.getName());
                }
                changes.add(new RemoveColumnChange(sourceTable, sourceColumn));
            }
        }

        return changes;
    }

    /**
     * Compares the two columns and returns the changes necessary to create the
     * second column from the first one.
     * 
     * @param sourceTable
     *            The source table which contains the source column
     * @param sourceColumn
     *            The source column
     * @param targetTable
     *            The target table which contains the target column
     * @param targetColumn
     *            The target column
     * @return The changes
     */
    public List<TableChange> compareColumns(Table sourceTable, Column sourceColumn, Table targetTable,
            Column targetColumn) {
        ArrayList<TableChange> changes = new ArrayList<TableChange>();

        int actualTypeCode = sourceColumn.getMappedTypeCode();
        int desiredTypeCode = targetColumn.getMappedTypeCode();
        boolean sizeMatters = platformInfo.hasSize(targetColumn.getMappedTypeCode());
        boolean scaleMatters = platformInfo.hasPrecisionAndScale(targetColumn.getMappedTypeCode());

        boolean compatible = (actualTypeCode == Types.NUMERIC || actualTypeCode == Types.DECIMAL)
                && (desiredTypeCode == Types.INTEGER || desiredTypeCode == Types.BIGINT);

        if (sourceColumn.isAutoIncrement() && targetColumn.isAutoIncrement()
                && (desiredTypeCode == Types.NUMERIC || desiredTypeCode == Types.DECIMAL)
                && (actualTypeCode == Types.INTEGER || actualTypeCode == Types.BIGINT)) {
            compatible = true;

            // This is the rare case where size doesnt matter!
            sizeMatters = false;
            scaleMatters = false;
        }

        if (!compatible && targetColumn.getMappedTypeCode() != sourceColumn.getMappedTypeCode() && platformInfo
                .getTargetJdbcType(targetColumn.getMappedTypeCode()) != sourceColumn.getMappedTypeCode()) {
            log.debug("The {} column on the {} table changed type codes from {} to {} ",
                    new Object[] { sourceColumn.getName(), sourceTable.getName(), sourceColumn.getMappedTypeCode(),
                            targetColumn.getMappedTypeCode() });
            changes.add(new ColumnDataTypeChange(sourceTable, sourceColumn, targetColumn.getMappedTypeCode()));
        }

        String targetSize = targetColumn.getSize();
        if (targetSize == null) {
            Integer defaultSize = platformInfo
                    .getDefaultSize(platformInfo.getTargetJdbcType(targetColumn.getMappedTypeCode()));
            if (defaultSize != null) {
                targetSize = defaultSize.toString();
            } else {
                targetSize = "0";
            }
        }
        if (sizeMatters && !StringUtils.equals(sourceColumn.getSize(), targetSize)) {
            log.debug("The {} column on the {} table changed size from ({}) to ({})",
                    new Object[] { sourceColumn.getName(), sourceTable.getName(), sourceColumn.getSizeAsInt(),
                            targetColumn.getSizeAsInt() });

            changes.add(new ColumnSizeChange(sourceTable, sourceColumn, targetColumn.getSizeAsInt(),
                    targetColumn.getScale()));
        } else if (scaleMatters && (!StringUtils.equals(sourceColumn.getSize(), targetSize) ||
        // ojdbc6.jar returns -127 for the scale of NUMBER that was not given a
        // size or precision
                (!(sourceColumn.getScale() < 0 && targetColumn.getScale() == 0)
                        && sourceColumn.getScale() != targetColumn.getScale()))) {
            log.debug("The {} column on the {} table changed scale from ({},{}) to ({},{})",
                    new Object[] { sourceColumn.getName(), sourceTable.getName(), sourceColumn.getSizeAsInt(),
                            sourceColumn.getScale(), targetColumn.getSizeAsInt(), targetColumn.getScale() });
            changes.add(new ColumnSizeChange(sourceTable, sourceColumn, targetColumn.getSizeAsInt(),
                    targetColumn.getScale()));
        }

        Object sourceDefaultValue = sourceColumn.getParsedDefaultValue();
        Object targetDefaultValue = targetColumn.getParsedDefaultValue();

        if ((sourceDefaultValue == null && targetDefaultValue != null)
                || (sourceDefaultValue != null && targetDefaultValue == null)
                || (sourceDefaultValue != null && targetDefaultValue != null
                        && !sourceDefaultValue.toString().equals(targetDefaultValue.toString()))) {
            log.debug("The {} column on the {} table changed default value from {} to {} ",
                    new Object[] { sourceColumn.getName(), sourceTable.getName(), sourceColumn.getDefaultValue(),
                            targetColumn.getDefaultValue() });
            changes.add(new ColumnDefaultValueChange(sourceTable, sourceColumn, targetColumn.getDefaultValue()));
        }

        if (sourceColumn.isRequired() != targetColumn.isRequired()) {
            log.debug("The {} column on the {} table changed required status from {} to {}",
                    new Object[] { sourceColumn.getName(), sourceTable.getName(), sourceColumn.isRequired(),
                            targetColumn.isRequired() });
            changes.add(new ColumnRequiredChange(sourceTable, sourceColumn));
        }

        if (sourceColumn.isAutoIncrement() != targetColumn.isAutoIncrement()) {
            log.debug("The {} column on the {} table changed auto increment status from {} to {} ",
                    new Object[] { sourceColumn.getName(), sourceTable.getName(), sourceColumn.isAutoIncrement(),
                            targetColumn.isAutoIncrement() });
            changes.add(new ColumnAutoIncrementChange(sourceTable, sourceColumn));
        }

        return changes;
    }

    /**
     * Searches in the given table for a corresponding foreign key. If the given
     * key has no name, then a foreign key to the same table with the same
     * columns (but not necessarily in the same order) is searched. If the given
     * key has a name, then the corresponding key also needs to have the same
     * name, or no name at all, but not a different one.
     * 
     * @param table
     *            The table to search in
     * @param fk
     *            The original foreign key
     * @return The corresponding foreign key if found
     */
    private ForeignKey findCorrespondingForeignKey(Table table, ForeignKey fk) {
        for (int fkIdx = 0; fkIdx < table.getForeignKeyCount(); fkIdx++) {
            ForeignKey curFk = table.getForeignKey(fkIdx);

            if ((caseSensitive && fk.equals(curFk)) || (!caseSensitive && fk.equalsIgnoreCase(curFk))) {
                return curFk;
            }
        }
        return null;
    }

    /**
     * Searches in the given table for a corresponding index. If the given index
     * has no name, then a index to the same table with the same columns in the
     * same order is searched. If the given index has a name, then the a
     * corresponding index also needs to have the same name, or no name at all,
     * but not a different one.
     * 
     * @param table
     *            The table to search in
     * @param index
     *            The original index
     * @return The corresponding index if found
     */
    private IIndex findCorrespondingIndex(Table table, IIndex index) {
        for (int indexIdx = 0; indexIdx < table.getIndexCount(); indexIdx++) {
            IIndex curIndex = table.getIndex(indexIdx);

            if ((caseSensitive && index.equals(curIndex)) || (!caseSensitive && index.equalsIgnoreCase(curIndex))) {
                return curIndex;
            }
        }
        return null;
    }
}