001package fr.ifremer.adagio.synchro.meta; 002 003/* 004 * #%L 005 * Tutti :: Persistence 006 * $Id: ReferentialSynchroDatabaseMetadata.java 1573 2014-02-04 16:41:40Z tchemit $ 007 * $HeadURL: http://svn.forge.codelutin.com/svn/tutti/trunk/tutti-persistence/src/main/java/fr/ifremer/adagio/core/service/technical/synchro/ReferentialSynchroDatabaseMetadata.java $ 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 static org.nuiton.i18n.I18n.t; 027 028import java.lang.reflect.Field; 029import java.sql.Connection; 030import java.sql.DatabaseMetaData; 031import java.sql.ResultSet; 032import java.sql.SQLException; 033import java.sql.Statement; 034import java.util.Collection; 035import java.util.List; 036import java.util.Map; 037import java.util.Set; 038 039import org.apache.commons.collections4.CollectionUtils; 040import org.apache.commons.logging.Log; 041import org.apache.commons.logging.LogFactory; 042import org.hibernate.HibernateException; 043import org.hibernate.cfg.AvailableSettings; 044import org.hibernate.cfg.Configuration; 045import org.hibernate.cfg.Environment; 046import org.hibernate.dialect.Dialect; 047import org.hibernate.exception.spi.SQLExceptionConverter; 048import org.hibernate.internal.util.StringHelper; 049import org.hibernate.internal.util.config.ConfigurationHelper; 050import org.hibernate.mapping.Table; 051import org.hibernate.tool.hbm2ddl.DatabaseMetadata; 052import org.hibernate.tool.hbm2ddl.TableMetadata; 053 054import com.google.common.base.Preconditions; 055import com.google.common.base.Predicate; 056import com.google.common.collect.Lists; 057import com.google.common.collect.Maps; 058import com.google.common.collect.Sets; 059 060import fr.ifremer.adagio.synchro.SynchroTechnicalException; 061import fr.ifremer.adagio.synchro.dao.DaoUtils; 062import fr.ifremer.adagio.synchro.intercept.SynchroInterceptor; 063import fr.ifremer.adagio.synchro.intercept.SynchroInterceptorUtils; 064import fr.ifremer.adagio.synchro.service.SynchroContext; 065 066/** 067 * Created on 1/14/14. 068 * 069 * @author Tony Chemit <chemit@codelutin.com> 070 * @since 3.5 071 */ 072public class SynchroDatabaseMetadata { 073 074 /** Logger. */ 075 private static final Log log = 076 LogFactory.getLog(SynchroDatabaseMetadata.class); 077 078 private static final String TABLE_CATALOG_PATTERN = "TABLE_CAT"; 079 private static final String TABLE_TYPE_PATTERN = "TABLE_TYPE"; 080 private static final String TABLE_SCHEMA_PATTERN = "TABLE_SCHEM"; 081 private static final String REMARKS_PATTERN = "REMARKS"; 082 private static final String TABLE_NAME_PATTERN = "TABLE_NAME"; 083 084 /** 085 * Load the datasource schema for the given connection and dialect. 086 * 087 * @param connection 088 * connection of the data source 089 * @param dialect 090 * dialect to use 091 * @param configuration 092 * @param tableNames 093 * table names to includes (table patterns are accepted) 094 * @return the database schema metadata 095 */ 096 public static SynchroDatabaseMetadata loadDatabaseMetadata(Connection connection, 097 Dialect dialect, Configuration configuration, 098 SynchroContext context, 099 Set<String> tableNames, 100 boolean enableJoinMetadataLoading 101 ) { 102 SynchroDatabaseMetadata result = new SynchroDatabaseMetadata(connection, dialect, configuration, context); 103 result.prepare(dialect, configuration, tableNames, null, null, enableJoinMetadataLoading); 104 return result; 105 } 106 107 /** 108 * Load the datasource schema for the given connection and dialect. 109 * 110 * @param connection 111 * connection of the data source 112 * @param dialect 113 * dialect to use 114 * @param configuration 115 * @param tableNames 116 * table names to includes (table patterns are accepted) (optional if tabkleFilter not null) 117 * @param tableFilter 118 * filter tables (optional) 119 * @param columnFilter 120 * filter columns (optional) 121 * @return the database schema metadata 122 */ 123 public static SynchroDatabaseMetadata loadDatabaseMetadata(Connection connection, 124 Dialect dialect, 125 Configuration configuration, 126 SynchroContext context, 127 Set<String> tableNames, 128 Predicate<String> tableFilter, 129 Predicate<SynchroColumnMetadata> columnFilter, 130 boolean enableJoinMetadataLoading 131 ) { 132 SynchroDatabaseMetadata result = new SynchroDatabaseMetadata(connection, dialect, configuration, context); 133 result.prepare(dialect, configuration, tableNames, tableFilter, columnFilter, enableJoinMetadataLoading); 134 return result; 135 } 136 137 protected final DatabaseMetadata delegate; 138 139 protected final Map<String, SynchroTableMetadata> tables; 140 141 protected final DatabaseMetaData meta; 142 143 protected final Configuration configuration; 144 145 protected final Dialect dialect; 146 147 protected final Set<String> sequences; 148 149 protected final String[] types; 150 151 private SQLExceptionConverter sqlExceptionConverter; 152 153 protected List<SynchroInterceptor> interceptors; 154 155 protected SynchroContext context; 156 157 public SynchroDatabaseMetadata(Connection connection, Dialect dialect, Configuration configuration, 158 SynchroContext context) { 159 Preconditions.checkNotNull(connection); 160 Preconditions.checkNotNull(dialect); 161 Preconditions.checkNotNull(configuration); 162 163 this.configuration = configuration; 164 this.dialect = dialect; 165 this.sqlExceptionConverter = DaoUtils.newSQLExceptionConverter(dialect); 166 this.context = context; 167 168 try { 169 this.delegate = new DatabaseMetadata(connection, dialect, configuration, true); 170 171 Field sqlExceptionConverterField = DatabaseMetadata.class.getDeclaredField("sqlExceptionConverter"); 172 sqlExceptionConverterField.setAccessible(true); 173 sqlExceptionConverterField.set(this.delegate, sqlExceptionConverter); 174 175 sequences = initSequences(connection, dialect); 176 177 Field typesField = DatabaseMetadata.class.getDeclaredField("types"); 178 typesField.setAccessible(true); 179 this.types = (String[]) typesField.get(this.delegate); 180 181 this.meta = connection.getMetaData(); 182 183 } catch (SQLException e) { 184 throw new SynchroTechnicalException(t("adagio.persistence.dbMetadata.instanciation.error", connection), e); 185 } catch (Exception e) { 186 throw new SynchroTechnicalException(t("adagio.persistence.dbMetadata.instanciation.error", connection), e); 187 } 188 tables = Maps.newTreeMap(); 189 } 190 191 public int getTableCount() { 192 return tables.size(); 193 } 194 195 public SynchroContext getContext() { 196 return this.context; 197 } 198 199 public Dialect getDialect() { 200 return this.dialect; 201 } 202 203 public int getInExpressionCountLimit() { 204 return dialect.getInExpressionCountLimit(); 205 } 206 207 public boolean isSequence(String tableName) { 208 String[] strings = StringHelper.split(".", (String) tableName); 209 return sequences.contains(StringHelper.toLowerCase(strings[strings.length - 1])); 210 } 211 212 public void prepare(Dialect dialect, 213 Configuration configuration, 214 Set<String> tableNames, 215 Predicate<String> tableFilter, 216 Predicate<SynchroColumnMetadata> columnFilter, 217 boolean enableJoinMetadataLoading) { 218 Preconditions.checkArgument(CollectionUtils.isNotEmpty(tableNames) || tableFilter != null, 219 "One of 'tableNames' or 'tableFilter' must be set and not empty"); 220 221 // Getting tables names to process 222 boolean enableFilter = tableFilter != null; 223 if (!enableFilter) { 224 for (String tablePattern : tableNames) { 225 enableFilter = tablePattern.contains("%"); 226 if (enableFilter) 227 break; 228 } 229 } 230 231 Set<String> filteredTableNames = tableNames; 232 if (enableFilter) { 233 if (CollectionUtils.isEmpty(tableNames)) { 234 filteredTableNames = getTableNames(tableFilter); 235 } 236 else { 237 filteredTableNames = getTableNames(tableNames, tableFilter); 238 } 239 } 240 241 // Getting schema 242 String jdbcCatalog = configuration.getProperty(Environment.DEFAULT_CATALOG); 243 String jdbcSchema = configuration.getProperty(Environment.DEFAULT_SCHEMA); 244 245 for (String tableName : filteredTableNames) { 246 247 if (log.isDebugEnabled()) { 248 log.debug("Load metas of table: " + tableName); 249 } 250 251 getTable(dialect, tableName, jdbcSchema, jdbcCatalog, false, columnFilter, false); 252 } 253 254 Map<String, SynchroTableMetadata> tablesByNames = Maps.newHashMap(); 255 for (SynchroTableMetadata table : tables.values()) { 256 tablesByNames.put(table.getName(), table); 257 258 // Init joins metadata (must be call AFTER getTable()) 259 if (enableJoinMetadataLoading) { 260 261 if (log.isDebugEnabled()) { 262 log.debug("Load joins of table: " + table.getName()); 263 } 264 table.initJoins(this); 265 } 266 267 fireOnTableLoad(table); 268 } 269 } 270 271 public SynchroTableMetadata getTable(String name) throws HibernateException { 272 String defaultSchema = ConfigurationHelper.getString(AvailableSettings.DEFAULT_SCHEMA, configuration.getProperties()); 273 String defaultCatalog = ConfigurationHelper.getString(AvailableSettings.DEFAULT_CATALOG, configuration.getProperties()); 274 275 return getTable(this.dialect, name, defaultSchema, defaultCatalog, false, null, true); 276 } 277 278 public SynchroTableMetadata getLoadedTable(String name) throws HibernateException { 279 String defaultSchema = ConfigurationHelper.getString(AvailableSettings.DEFAULT_SCHEMA, configuration.getProperties()); 280 String defaultCatalog = ConfigurationHelper.getString(AvailableSettings.DEFAULT_CATALOG, configuration.getProperties()); 281 return getLoadedTable(name, defaultSchema, defaultCatalog); 282 } 283 284 public SynchroTableMetadata getLoadedTable(String name, 285 String schema, 286 String catalog) throws HibernateException { 287 String key = Table.qualify(catalog, schema, name).toLowerCase(); 288 return tables.get(key); 289 } 290 291 /** 292 * Load tables names from database schema, using the given table filter.<br/> 293 * This method call {@link #getTableNames(Set<String>,Predicate<String>)} with the table pattern "%". 294 * 295 * @param tableFilter 296 * A filter predicate, to filter tables to retrieve. If null: process all tables found. 297 * @return All tables names found in database, filtered using the given tableFilter 298 * @see #getTableNames(Set<String>,Predicate<String>) 299 */ 300 public Set<String> getTableNames(Predicate<String> tableFilter) { 301 return getTableNames(Sets.newHashSet("%"), tableFilter); 302 } 303 304 /** 305 * Load tables names from database schema, using the given table patterns list, and a optional filter. This use the 306 * JDBC metadata API.<br/> 307 * This will include Tables and View objects. Synonyms ar includes only if enable in connection properties {@see 308 * org.hibernate.cfg.AvailableSettings.ENABLE_SYNONYMS}. 309 * 310 * @param tablePatterns 311 * A list of table pattern. Use the pattern '%' to get all tables. 312 * @param tableFilter 313 * Optional. A filter predicate, to filter tables to retrieve. If null: process tables found from pattern 314 * will be return. 315 * @return All tables names found in database 316 * @see org.hibernate.cfg.AvailableSettings.ENABLE_SYNONYMS 317 * @see java.sql.DatabaseMetaData#getTables(String,String,String,String[]) 318 */ 319 public Set<String> getTableNames(Set<String> tablePatterns, Predicate<String> tableFilter) { 320 Preconditions.checkArgument(CollectionUtils.isNotEmpty(tablePatterns)); 321 322 Set<String> tablenames = Sets.newHashSet(); 323 324 String defaultSchema = ConfigurationHelper.getString(AvailableSettings.DEFAULT_SCHEMA, configuration.getProperties()); 325 String defaultCatalog = ConfigurationHelper.getString(AvailableSettings.DEFAULT_CATALOG, configuration.getProperties()); 326 327 String[] types = null; // available types are: "TABLE", "VIEW", "SYSTEM TABLE", "GLOBAL TEMPORARY", 328 // "LOCAL TEMPORARY", "ALIAS", "SYNONYM" 329 if (configuration != null 330 && ConfigurationHelper.getBoolean(AvailableSettings.ENABLE_SYNONYMS, configuration.getProperties(), false)) { 331 types = new String[] { "TABLE", "VIEW", "SYNONYM" }; 332 } 333 else { 334 types = new String[] { "TABLE", "VIEW" }; 335 } 336 337 ResultSet res = null; 338 try { 339 if (log.isDebugEnabled()) { 340 log.debug("Getting table names, using filter"); 341 } 342 343 for (String tablePattern : tablePatterns) { 344 // first pass on the main schema 345 res = meta.getTables(defaultCatalog, defaultSchema, tablePattern, types); 346 while (res.next()) { 347 String tableName = res.getString(TABLE_NAME_PATTERN); //$NON-NLS-1$ 348 if (!delegate.isSequence(tableName) && (tableFilter == null || tableFilter.apply(tableName))) { 349 if (log.isTraceEnabled()) { 350 log.trace(" " + TABLE_CATALOG_PATTERN + "=" + res.getString(TABLE_CATALOG_PATTERN) 351 + " " + TABLE_SCHEMA_PATTERN + "=" + res.getString(TABLE_SCHEMA_PATTERN) 352 + " " + TABLE_NAME_PATTERN + "=" + res.getString(TABLE_NAME_PATTERN) 353 + " " + TABLE_TYPE_PATTERN + "=" + res.getString(TABLE_TYPE_PATTERN) 354 + " " + REMARKS_PATTERN + "=" + res.getString(REMARKS_PATTERN)); 355 } 356 tablenames.add(tableName); 357 } 358 } 359 } 360 } catch (SQLException e) { 361 throw sqlExceptionConverter.convert(e, "Retrieving database table names", "n/a"); 362 } finally { 363 DaoUtils.closeSilently(res); 364 } 365 366 return tablenames; 367 } 368 369 /** 370 * Return all root tables (top level tables).<br/> 371 * Return only tables previously loaded using methods getTable() or loadDatabaseMetadata() 372 * 373 * @return All loaded tables metadata 374 */ 375 public Set<String> getLoadedRootTableNames() { 376 Set<String> tablenames = Sets.newHashSet(); 377 for (SynchroTableMetadata table : tables.values()) { 378 if (table.isRoot()) { 379 tablenames.add(table.getName()); 380 } 381 } 382 383 return tablenames; 384 } 385 386 /** 387 * Return all tables (already loaded).<br/> 388 * Return only tables previously loaded using methods getTable() or loadDatabaseMetadata() 389 * 390 * @return All loaded tables names 391 */ 392 public Set<String> getLoadedTableNames() { 393 Set<String> tablenames = Sets.newHashSet(); 394 for (SynchroTableMetadata table : tables.values()) { 395 tablenames.add(table.getName()); 396 } 397 398 return tablenames; 399 } 400 401 /** 402 * @see java.sql.DatabaseMetaData.getExportedKeys(String,String,String) 403 */ 404 public ResultSet getExportedKeys(String catalog, String schema, String table) throws SQLException { 405 return meta.getExportedKeys(catalog, schema, table); 406 } 407 408 /** 409 * @see java.sql.DatabaseMetaData.getImportedKeys(String,String,String) 410 */ 411 public ResultSet getImportedKeys(String catalog, String schema, String table) throws SQLException { 412 return meta.getImportedKeys(catalog, schema, table); 413 } 414 415 /** 416 * @see java.sql.DatabaseMetaData.getPrimaryKeys(String,String,String) 417 */ 418 public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException { 419 return meta.getPrimaryKeys(catalog, schema, table); 420 } 421 422 /* -- Internal methods -- */ 423 424 protected SynchroTableMetadata getTable( 425 Dialect dialect, 426 String name, 427 String schema, 428 String catalog, 429 boolean isQuoted, 430 Predicate<SynchroColumnMetadata> columnFilter, 431 boolean withJoinedTables) throws HibernateException { 432 String key = Table.qualify(catalog, schema, name).toLowerCase(); 433 SynchroTableMetadata synchroTableMetadata = tables.get(key); 434 if (synchroTableMetadata == null) { 435 436 TableMetadata tableMetadata = delegate.getTableMetadata( 437 name.toLowerCase(), schema, catalog, isQuoted); 438 Preconditions.checkNotNull(tableMetadata, String.format("Could not find db table '%s' (schema=%s, catalog=%s)", name, schema, catalog)); 439 440 List<SynchroInterceptor> interceptors = getInterceptors(tableMetadata); 441 442 synchroTableMetadata = new SynchroTableMetadata( 443 this, 444 tableMetadata, 445 interceptors, 446 name, sequences, columnFilter); 447 Preconditions.checkNotNull(synchroTableMetadata, 448 "Could not load metadata for table: " + name); 449 450 tables.put(key, synchroTableMetadata); 451 } 452 return synchroTableMetadata; 453 } 454 455 protected Set<String> initSequences(Connection connection, Dialect dialect) throws SQLException { 456 Set<String> sequences = Sets.newHashSet(); 457 if (dialect.supportsSequences()) { 458 String sql = dialect.getQuerySequencesString(); 459 if (sql != null) { 460 461 Statement statement = null; 462 ResultSet rs = null; 463 try { 464 statement = connection.createStatement(); 465 rs = statement.executeQuery(sql); 466 467 while (rs.next()) { 468 sequences.add(StringHelper.toLowerCase(rs.getString(1)).trim()); 469 } 470 } finally { 471 rs.close(); 472 statement.close(); 473 } 474 475 } 476 } 477 return sequences; 478 } 479 480 protected List<SynchroInterceptor> getInterceptors(final TableMetadata table) { 481 if (interceptors == null) { 482 interceptors = SynchroInterceptorUtils.load(SynchroInterceptor.class, this.context); 483 } 484 485 Collection<SynchroInterceptor> filteredInterceptors = SynchroInterceptorUtils.filter( 486 interceptors, 487 this, 488 table 489 ); 490 491 return Lists.newArrayList(filteredInterceptors); 492 } 493 494 protected void fireOnTableLoad(SynchroTableMetadata table) { 495 List<SynchroInterceptor> interceptors = table.getInterceptors(); 496 if (CollectionUtils.isNotEmpty(interceptors)) { 497 for (SynchroInterceptor interceptor : interceptors) { 498 interceptor.onTableLoad(table); 499 } 500 } 501 } 502}