001package fr.ifremer.adagio.synchro.service;
002
003/*
004 * #%L
005 * SIH-Adagio :: Synchronization
006 * $Id:$
007 * $HeadURL:$
008 * %%
009 * Copyright (C) 2012 - 2014 Ifremer
010 * %%
011 * This program is free software: you can redistribute it and/or modify
012 * it under the terms of the GNU Affero General Public License as published by
013 * the Free Software Foundation, either version 3 of the License, or
014 * (at your option) any later version.
015 * 
016 * This program is distributed in the hope that it will be useful,
017 * but WITHOUT ANY WARRANTY; without even the implied warranty of
018 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
019 * GNU General Public License for more details.
020 * 
021 * You should have received a copy of the GNU Affero General Public License
022 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
023 * #L%
024 */
025
026import java.util.Set;
027
028import org.apache.commons.collections.CollectionUtils;
029import org.apache.commons.logging.Log;
030import org.apache.commons.logging.LogFactory;
031
032import com.google.common.collect.Sets;
033
034import fr.ifremer.adagio.synchro.SynchroTechnicalException;
035import fr.ifremer.adagio.synchro.meta.SynchroColumnMetadata;
036import fr.ifremer.adagio.synchro.meta.SynchroDatabaseMetadata;
037import fr.ifremer.adagio.synchro.meta.SynchroMetadataUtils;
038import fr.ifremer.adagio.synchro.meta.SynchroTableMetadata;
039
040/**
041 * Helper class to :
042 * <ul>
043 * <li>Check two database schemas.
044 * </ul>
045 * 
046 * @author Benoit Lavenier <benoit.lavenier@e-is.pro>
047 * @since 3.5.3
048 * 
049 */
050public class SynchroServiceUtils {
051    /** Logger. */
052    private static final Log log =
053            LogFactory.getLog(SynchroServiceUtils.class);
054
055    /**
056     * Check that the tow given shemas are compatible for a
057     * synchronize operation (same tables with same columns).
058     * <p/>
059     * If <code>allowMissingOptionalColumn=true</code> then missing columns are allowed. Missing columns will be added
060     * to the given result.
061     * <p/>
062     * If <code>allowAdditionalMandatoryColumnInSourceSchema=true</code> then additional mandatory columns in the source
063     * schema are allowed. It could be set to
064     * <code>false<code> for data synchronization, to avoid getting data from tables that could not be export later.  
065     * <p/>
066     * If schemas are incompatible, then a {@link SynchroTechnicalException} exception will be thrown.
067     * 
068     * @param targetSchema
069     *            schema 1 to check
070     * @param sourceSchema
071     *            schema 2 to check
072     * @param allowMissingOptionalColumn
073     *            Is missing optional columns are allowed (in source or target schema) ? If true, missing column will be
074     *            ignore in synchronization.
075     * @param allowAdditionalMandatoryColumnInSourceSchema
076     *            Is additional mandatory columns are allowed in source schema ? If true, source schema could have more
077     *            mandatory columns.
078     * @param result
079     *            Synchro result. Use to store missing column is any
080     */
081    public static void checkSchemas(
082            SynchroDatabaseMetadata sourceSchema,
083            SynchroDatabaseMetadata targetSchema,
084            boolean allowMissingOptionalColumn,
085            boolean allowAdditionalMandatoryColumnInSourceSchema,
086            SynchroResult result) {
087        try {
088            if (allowMissingOptionalColumn) {
089                checkSchemasAllowMissingOptionalColumn(
090                        sourceSchema,
091                        targetSchema,
092                        allowAdditionalMandatoryColumnInSourceSchema,
093                        result);
094            }
095            else {
096                checkSchemasStrict(
097                        sourceSchema,
098                        targetSchema);
099            }
100        } catch (SynchroTechnicalException e) {
101            result.setError(e);
102        }
103    }
104
105    /**
106     * Check that the tow given shemas are compatible for a synchronize operation (same tables with same columns). *
107     * <p/>
108     * This method allow missing columns (if define as nullable in the target schema)
109     * <p/>
110     * If schemas are incompatible, then a {@link SynchroTechnicalException} exception will be thrown.
111     * 
112     * @param synchroContext
113     *            Synchro context
114     * @param targetSchema
115     *            schema 1 to check
116     * @param sourceSchema
117     *            schema 2 to check
118     */
119    protected static void checkSchemasAllowMissingOptionalColumn(
120            SynchroDatabaseMetadata sourceSchema,
121            SynchroDatabaseMetadata targetSchema,
122            boolean allowAdditionalMandatoryColumnInSourceSchema,
123            SynchroResult result) {
124        Set<String> targetSchemaTableNames = targetSchema.getLoadedTableNames();
125        Set<String> sourceSchemaTableNames = sourceSchema.getLoadedTableNames();
126
127        // Check if table names are equals
128        if (!targetSchemaTableNames.equals(sourceSchemaTableNames)) {
129            Set<String> missingTargetTables = Sets.newHashSet();
130            for (String targetTableName : targetSchemaTableNames) {
131                if (!sourceSchemaTableNames.contains(targetTableName)) {
132                    missingTargetTables.add(targetTableName);
133                }
134            }
135            Set<String> missingSourceTables = Sets.newHashSet();
136            for (String sourceTableName : sourceSchemaTableNames) {
137                if (!targetSchemaTableNames.contains(sourceTableName)) {
138                    missingSourceTables.add(sourceTableName);
139                }
140            }
141
142            throw new SynchroTechnicalException(String.format(
143                    "Incompatible schemas.\nMissing tables in source database: %s\nMissing tables in target database: %s", missingTargetTables,
144                    missingSourceTables));
145        }
146
147        for (String tableName : targetSchemaTableNames) {
148            SynchroTableMetadata targetTable = targetSchema.getTable(tableName);
149            SynchroTableMetadata sourceTable = sourceSchema.getTable(tableName);
150            Set<String> targetColumnNames = Sets.newHashSet(targetTable.getColumnNames());
151            Set<String> sourceColumnNames = sourceTable.getColumnNames();
152
153            // Check if columns names are equals
154            if (!targetColumnNames.equals(sourceColumnNames)) {
155                Set<String> missingMandatoryColumns = Sets.newTreeSet();
156                Set<String> missingSourceColumnNames = Sets.newHashSet(sourceColumnNames);
157
158                // Check if missing column (in source) are optional
159                for (String targetColumnName : targetTable.getColumnNames()) {
160                    if (!sourceColumnNames.contains(targetColumnName)) {
161                        SynchroColumnMetadata targetColumn = targetTable.getColumnMetadata(targetColumnName);
162
163                        // Optional column: add it to the context (will be ignore in during synchronization)
164                        if (targetColumn.isNullable()) {
165                            log.debug(String.format("Optional column not found in source database: %s.%s. Will be ignore.",
166                                    tableName, targetColumnName));
167                            result.addMissingOptionalColumnName(tableName, targetColumnName);
168                            targetColumnNames.remove(targetColumnName);
169                        }
170
171                        // Mandatory columns: add to list to check later
172                        else {
173                            log.warn(String.format("Column not found in source database: %s.%s", tableName, targetColumnName));
174                            missingMandatoryColumns.add(targetColumnName);
175                        }
176                    }
177
178                    missingSourceColumnNames.remove(targetColumnName);
179                }
180
181                // Check if missing column (in target) are optional
182                for (String sourceColumnName : missingSourceColumnNames) {
183                    SynchroColumnMetadata sourceColumn = sourceTable.getColumnMetadata(sourceColumnName);
184                    if (allowAdditionalMandatoryColumnInSourceSchema || sourceColumn.isNullable()) {
185                        log.debug(String.format("Optional column not found in target database: %s.%s. Will be ignore.",
186                                tableName, sourceColumnName));
187                        result.addMissingOptionalColumnName(tableName, sourceColumnName);
188                    }
189                    else {
190                        log.warn(String.format("Column not found in target database: %s.%s. Will be ignore.", tableName, sourceColumnName));
191                        missingMandatoryColumns.add(sourceColumnName);
192                    }
193                }
194
195                // Throw an exception if exists any missing column
196                if (CollectionUtils.isNotEmpty(missingMandatoryColumns)) {
197                    throw new SynchroTechnicalException(String.format("Incompatible schema of table: %s. Missing mandatory columns: %s",
198                            tableName, missingMandatoryColumns));
199                }
200            }
201
202            // Check column types compatibility
203            for (String columnName : targetColumnNames) {
204                SynchroColumnMetadata targetColumn = targetTable.getColumnMetadata(columnName);
205                SynchroColumnMetadata sourceColumn = sourceTable.getColumnMetadata(columnName);
206                if (!targetColumn.isProtected()) {
207                    SynchroMetadataUtils.checkType(tableName, targetColumn, sourceColumn);
208                }
209            }
210        }
211    }
212
213    /**
214     * Check that the tow given datasource shemas are compatible for a
215     * synchronize operation (same tables with same columns).
216     * <p/>
217     * If schemas are incompatible, then a {@link SynchroTechnicalException} exception will be thrown.
218     * 
219     * @param schema1
220     *            schema 1 to check
221     * @param schema2
222     *            schema 2 to check
223     */
224    protected static void checkSchemasStrict(
225            SynchroDatabaseMetadata sourceSchema,
226            SynchroDatabaseMetadata targetSchema) {
227        Set<String> sourceSchemaTableNames = sourceSchema.getLoadedTableNames();
228        Set<String> targetSchemaTableNames = targetSchema.getLoadedTableNames();
229
230        // Check if table names are equals
231        if (!targetSchemaTableNames.equals(sourceSchemaTableNames)) {
232            throw new SynchroTechnicalException("Incompatible schemas: missing tables");
233        }
234
235        for (String tableName : sourceSchemaTableNames) {
236            SynchroTableMetadata sourceTable = sourceSchema.getTable(tableName);
237            SynchroTableMetadata targetTable = targetSchema.getTable(tableName);
238            Set<String> sourceColumnNames = sourceTable.getColumnNames();
239            Set<String> targetColumnNames = targetTable.getColumnNames();
240
241            // Check if columns names are equals
242            if (!targetColumnNames.equals(sourceColumnNames)) {
243                throw new SynchroTechnicalException("Incompatible schema of table: " + tableName);
244            }
245
246            // Check column types compatibility
247            for (String columnName : targetColumnNames) {
248                SynchroColumnMetadata sourceColumn = sourceTable.getColumnMetadata(columnName);
249                SynchroColumnMetadata targetColumn = targetTable.getColumnMetadata(columnName);
250                if (!targetColumn.isProtected()) {
251                    SynchroMetadataUtils.checkType(tableName, targetColumn, sourceColumn);
252                }
253            }
254        }
255    }
256
257}