View Javadoc
1   package fr.ifremer.adagio.synchro.meta;
2   
3   /*
4    * #%L
5    * Tutti :: Persistence
6    * $Id: ReferentialSynchroDatabaseMetadata.java 1573 2014-02-04 16:41:40Z tchemit $
7    * $HeadURL: http://svn.forge.codelutin.com/svn/tutti/trunk/tutti-persistence/src/main/java/fr/ifremer/adagio/core/service/technical/synchro/ReferentialSynchroDatabaseMetadata.java $
8    * %%
9    * Copyright (C) 2012 - 2014 Ifremer
10   * %%
11   * This program is free software: you can redistribute it and/or modify
12   * it under the terms of the GNU Affero General Public License as published by
13   * the Free Software Foundation, either version 3 of the License, or
14   * (at your option) any later version.
15   * 
16   * This program is distributed in the hope that it will be useful,
17   * but WITHOUT ANY WARRANTY; without even the implied warranty of
18   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19   * GNU General Public License for more details.
20   * 
21   * You should have received a copy of the GNU Affero General Public License
22   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
23   * #L%
24   */
25  
26  import static org.nuiton.i18n.I18n.t;
27  
28  import java.lang.reflect.Field;
29  import java.sql.Connection;
30  import java.sql.DatabaseMetaData;
31  import java.sql.ResultSet;
32  import java.sql.SQLException;
33  import java.sql.Statement;
34  import java.util.Collection;
35  import java.util.List;
36  import java.util.Map;
37  import java.util.Set;
38  
39  import org.apache.commons.collections4.CollectionUtils;
40  import org.apache.commons.logging.Log;
41  import org.apache.commons.logging.LogFactory;
42  import org.hibernate.HibernateException;
43  import org.hibernate.cfg.AvailableSettings;
44  import org.hibernate.cfg.Configuration;
45  import org.hibernate.cfg.Environment;
46  import org.hibernate.dialect.Dialect;
47  import org.hibernate.exception.spi.SQLExceptionConverter;
48  import org.hibernate.internal.util.StringHelper;
49  import org.hibernate.internal.util.config.ConfigurationHelper;
50  import org.hibernate.mapping.Table;
51  import org.hibernate.tool.hbm2ddl.DatabaseMetadata;
52  import org.hibernate.tool.hbm2ddl.TableMetadata;
53  
54  import com.google.common.base.Preconditions;
55  import com.google.common.base.Predicate;
56  import com.google.common.collect.Lists;
57  import com.google.common.collect.Maps;
58  import com.google.common.collect.Sets;
59  
60  import fr.ifremer.adagio.synchro.SynchroTechnicalException;
61  import fr.ifremer.adagio.synchro.dao.DaoUtils;
62  import fr.ifremer.adagio.synchro.intercept.SynchroInterceptor;
63  import fr.ifremer.adagio.synchro.intercept.SynchroInterceptorUtils;
64  import fr.ifremer.adagio.synchro.service.SynchroContext;
65  
66  /**
67   * Created on 1/14/14.
68   * 
69   * @author Tony Chemit <chemit@codelutin.com>
70   * @since 3.5
71   */
72  public class SynchroDatabaseMetadata {
73  
74  	/** Logger. */
75  	private static final Log log =
76  			LogFactory.getLog(SynchroDatabaseMetadata.class);
77  
78  	private static final String TABLE_CATALOG_PATTERN = "TABLE_CAT";
79  	private static final String TABLE_TYPE_PATTERN = "TABLE_TYPE";
80  	private static final String TABLE_SCHEMA_PATTERN = "TABLE_SCHEM";
81  	private static final String REMARKS_PATTERN = "REMARKS";
82  	private static final String TABLE_NAME_PATTERN = "TABLE_NAME";
83  
84  	/**
85  	 * Load the datasource schema for the given connection and dialect.
86  	 * 
87  	 * @param connection
88  	 *            connection of the data source
89  	 * @param dialect
90  	 *            dialect to use
91  	 * @param configuration
92  	 * @param tableNames
93  	 *            table names to includes (table patterns are accepted)
94  	 * @return the database schema metadata
95  	 */
96  	public static SynchroDatabaseMetadata loadDatabaseMetadata(Connection connection,
97  			Dialect dialect, Configuration configuration,
98  			SynchroContext context,
99  			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 }