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}