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}