/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.metamodel.jdbc; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Collections; import java.util.Set; import java.util.StringTokenizer; import java.util.concurrent.ConcurrentHashMap; import org.apache.metamodel.MetaModelException; import org.apache.metamodel.schema.Column; import org.apache.metamodel.schema.ColumnType; import org.apache.metamodel.schema.MutableColumn; import org.apache.metamodel.schema.MutableRelationship; import org.apache.metamodel.schema.Schema; import org.apache.metamodel.schema.Table; import org.apache.metamodel.schema.TableType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * {@link MetadataLoader} for JDBC metadata loading. */ final class JdbcMetadataLoader implements MetadataLoader { private static final Logger logger = LoggerFactory.getLogger(JdbcMetadataLoader.class); private final JdbcDataContext _dataContext; private final boolean _usesCatalogsAsSchemas; private final String _identifierQuoteString; // these three sets contains the system identifies of whether specific items // have been loaded for tables/schemas. Using system identities avoid having // to call equals(...) method etc. while doing lazy loading of these items. // Invoking equals(...) would be prone to stack overflows ... private final Set<Integer> _loadedRelations; private final Set<Integer> _loadedColumns; private final Set<Integer> _loadedIndexes; private final Set<Integer> _loadedPrimaryKeys; public JdbcMetadataLoader(JdbcDataContext dataContext, boolean usesCatalogsAsSchemas, String identifierQuoteString) { _dataContext = dataContext; _usesCatalogsAsSchemas = usesCatalogsAsSchemas; _identifierQuoteString = identifierQuoteString; _loadedRelations = Collections.newSetFromMap(new ConcurrentHashMap<Integer, Boolean>()); _loadedColumns = Collections.newSetFromMap(new ConcurrentHashMap<Integer, Boolean>()); _loadedIndexes = Collections.newSetFromMap(new ConcurrentHashMap<Integer, Boolean>()); _loadedPrimaryKeys = Collections.newSetFromMap(new ConcurrentHashMap<Integer, Boolean>()); } @Override public void loadTables(JdbcSchema schema) { final Connection connection = _dataContext.getConnection(); try { loadTables(schema, connection); } finally { _dataContext.close(connection); } } @Override public void loadTables(JdbcSchema schema, Connection connection) { try { final DatabaseMetaData metaData = connection.getMetaData(); // Creates string array to represent the table types final String[] types = JdbcUtils.getTableTypesAsStrings(_dataContext.getTableTypes()); loadTables(schema, metaData, types); } catch (SQLException e) { throw JdbcUtils.wrapException(e, "retrieve table metadata for " + schema.getName()); } } private String getJdbcSchemaName(Schema schema) { if(_usesCatalogsAsSchemas) { return null; } else { return schema.getName(); } } private String getCatalogName(Schema schema) { if(_usesCatalogsAsSchemas) { return schema.getName(); } else { return _dataContext.getCatalogName(); } } private void loadTables(JdbcSchema schema, DatabaseMetaData metaData, String[] types) { try (ResultSet rs = metaData.getTables(getCatalogName(schema), getJdbcSchemaName(schema), null, types)) { logger.debug("Querying for table types {}, in catalog: {}, schema: {}", types, _dataContext.getCatalogName(), schema.getName()); schema.clearTables(); int tableNumber = -1; while (rs.next()) { tableNumber++; String tableCatalog = rs.getString(1); String tableSchema = rs.getString(2); String tableName = rs.getString(3); String tableTypeName = rs.getString(4); TableType tableType = TableType.getTableType(tableTypeName); String tableRemarks = rs.getString(5); if (logger.isDebugEnabled()) { logger.debug("Found table: tableCatalog=" + tableCatalog + ",tableSchema=" + tableSchema + ",tableName=" + tableName); } JdbcTable table = new JdbcTable(tableName, tableType, schema, this); table.setRemarks(tableRemarks); table.setQuote(_identifierQuoteString); schema.addTable(table); } final int tablesReturned = tableNumber + 1; if (tablesReturned == 0) { logger.info("No table metadata records returned for schema '{}'", schema.getName()); } else { logger.debug("Returned {} table metadata records for schema '{}'", new Object[] { tablesReturned, schema.getName() }); } } catch (SQLException e) { throw JdbcUtils.wrapException(e, "retrieve table metadata for " + schema.getName()); } } @Override public void loadIndexes(JdbcTable jdbcTable) { final int identity = System.identityHashCode(jdbcTable); if (_loadedIndexes.contains(identity)) { return; } final Connection connection = _dataContext.getConnection(); try { loadIndexes(jdbcTable, connection); } finally { _dataContext.close(connection); } } @Override public void loadIndexes(JdbcTable table, Connection connection) { final int identity = System.identityHashCode(table); if (_loadedIndexes.contains(identity)) { return; } synchronized (this) { if (_loadedIndexes.contains(identity)) { return; } try { DatabaseMetaData metaData = connection.getMetaData(); loadIndexes(table, metaData); _loadedIndexes.add(identity); } catch (SQLException e) { throw JdbcUtils.wrapException(e, "load indexes"); } } } @Override public void loadPrimaryKeys(JdbcTable jdbcTable) { final int identity = System.identityHashCode(jdbcTable); if (_loadedPrimaryKeys.contains(identity)) { return; } final Connection connection = _dataContext.getConnection(); try { loadPrimaryKeys(jdbcTable, connection); } finally { _dataContext.close(connection); } } @Override public void loadPrimaryKeys(JdbcTable table, Connection connection) { final int identity = System.identityHashCode(table); if (_loadedPrimaryKeys.contains(identity)) { return; } synchronized (this) { if (_loadedPrimaryKeys.contains(identity)) { return; } try { DatabaseMetaData metaData = connection.getMetaData(); loadPrimaryKeys(table, metaData); _loadedPrimaryKeys.add(identity); } catch (SQLException e) { throw JdbcUtils.wrapException(e, "load primary keys"); } } } private void loadPrimaryKeys(JdbcTable table, DatabaseMetaData metaData) throws MetaModelException { Schema schema = table.getSchema(); try (ResultSet rs = metaData.getPrimaryKeys(getCatalogName(schema), getJdbcSchemaName(schema), table.getName());){ while (rs.next()) { String columnName = rs.getString(4); if (columnName != null) { MutableColumn column = (MutableColumn) table.getColumnByName(columnName); if (column != null) { column.setPrimaryKey(true); } else { logger.error("Indexed column \"{}\" could not be found in table: {}", columnName, table); } } } } catch (SQLException e) { throw JdbcUtils.wrapException(e, "retrieve primary keys for " + table.getName()); } } private void loadIndexes(Table table, DatabaseMetaData metaData) throws MetaModelException { Schema schema = table.getSchema(); // Ticket #170: IndexInfo is nice-to-have, not need-to-have, so // we will do a nice failover on SQLExceptions try (ResultSet rs = metaData.getIndexInfo(getCatalogName(schema), getJdbcSchemaName(schema), table.getName(), false, true)) { while (rs.next()) { String columnName = rs.getString(9); if (columnName != null) { MutableColumn column = (MutableColumn) table.getColumnByName(columnName); if (column != null) { column.setIndexed(true); } else { logger.error("Indexed column \"{}\" could not be found in table: {}", columnName, table); } } } } catch (SQLException e) { throw JdbcUtils.wrapException(e, "retrieve index information for " + table.getName()); } } @Override public void loadColumns(JdbcTable jdbcTable) { final int identity = System.identityHashCode(jdbcTable); if (_loadedColumns.contains(identity)) { return; } final Connection connection = _dataContext.getConnection(); try { loadColumns(jdbcTable, connection); } finally { _dataContext.close(connection); } } /** * Loads column metadata (no indexes though) for a table * * @param table */ @Override public void loadColumns(JdbcTable table, Connection connection) { final int identity = System.identityHashCode(table); if (_loadedColumns.contains(identity)) { return; } synchronized (this) { if (_loadedColumns.contains(identity)) { return; } try { DatabaseMetaData metaData = connection.getMetaData(); loadColumns(table, metaData); _loadedColumns.add(identity); } catch (Exception e) { logger.error("Could not load columns for table: " + table, e); } } } private boolean isLobConversionEnabled() { final String systemProperty = System.getProperty(JdbcDataContext.SYSTEM_PROPERTY_CONVERT_LOBS); return "true".equals(systemProperty); } private void loadColumns(JdbcTable table, DatabaseMetaData metaData) { final boolean convertLobs = isLobConversionEnabled(); final Schema schema = table.getSchema(); try (ResultSet rs = metaData.getColumns(getCatalogName(schema), getJdbcSchemaName(schema), table.getName(), null)) { if (logger.isDebugEnabled()) { logger.debug("Querying for columns in table: " + table.getName()); } int columnNumber = -1; while (rs.next()) { columnNumber++; final String columnName = rs.getString(4); if (_identifierQuoteString == null && new StringTokenizer(columnName).countTokens() > 1) { logger.warn("column name contains whitespace: \"" + columnName + "\"."); } final int jdbcType = rs.getInt(5); final String nativeType = rs.getString(6); final Integer columnSize = rs.getInt(7); if (logger.isDebugEnabled()) { logger.debug("Found column: table=" + table.getName() + ",columnName=" + columnName + ",nativeType=" + nativeType + ",columnSize=" + columnSize); } ColumnType columnType = _dataContext.getQueryRewriter().getColumnType(jdbcType, nativeType, columnSize); if (convertLobs) { if (columnType == ColumnType.CLOB || columnType == ColumnType.NCLOB) { columnType = JdbcDataContext.COLUMN_TYPE_CLOB_AS_STRING; } else if (columnType == ColumnType.BLOB) { columnType = JdbcDataContext.COLUMN_TYPE_BLOB_AS_BYTES; } } final int jdbcNullable = rs.getInt(11); final Boolean nullable; if (jdbcNullable == DatabaseMetaData.columnNullable) { nullable = true; } else if (jdbcNullable == DatabaseMetaData.columnNoNulls) { nullable = false; } else { nullable = null; } final String remarks = rs.getString(12); final JdbcColumn column = new JdbcColumn(columnName, columnType, table, columnNumber, nullable); column.setRemarks(remarks); column.setNativeType(nativeType); column.setColumnSize(columnSize); column.setQuote(_identifierQuoteString); table.addColumn(column); } final int columnsReturned = columnNumber + 1; if (columnsReturned == 0) { logger.info("No column metadata records returned for table '{}' in schema '{}'", table.getName(), schema.getName()); } else { logger.debug("Returned {} column metadata records for table '{}' in schema '{}'", columnsReturned, table.getName(), schema.getName()); } } catch (SQLException e) { throw JdbcUtils.wrapException(e, "retrieve table metadata for " + table.getName()); } } @Override public void loadRelations(JdbcSchema jdbcSchema) { final int identity = System.identityHashCode(jdbcSchema); if (_loadedRelations.contains(identity)) { return; } final Connection connection = _dataContext.getConnection(); try { loadRelations(jdbcSchema, connection); } finally { _dataContext.close(connection); } } @Override public void loadRelations(JdbcSchema schema, Connection connection) { final int identity = System.identityHashCode(schema); if (_loadedRelations.contains(identity)) { return; } synchronized (this) { if (_loadedRelations.contains(identity)) { return; } try { final Table[] tables = schema.getTables(); final DatabaseMetaData metaData = connection.getMetaData(); for (Table table : tables) { loadRelations(table, metaData); } _loadedRelations.add(identity); } catch (Exception e) { logger.error("Could not load relations for schema: " + schema, e); } } } private void loadRelations(Table table, DatabaseMetaData metaData) { Schema schema = table.getSchema(); try (ResultSet rs = metaData.getImportedKeys(getCatalogName(schema), getJdbcSchemaName(schema), table.getName())) { loadRelations(rs, schema); } catch (SQLException e) { throw JdbcUtils.wrapException(e, "retrieve imported keys for " + table.getName()); } } private void loadRelations(ResultSet rs, Schema schema) throws SQLException { while (rs.next()) { String pkTableName = rs.getString(3); String pkColumnName = rs.getString(4); Column pkColumn = null; Table pkTable = schema.getTableByName(pkTableName); if (pkTable != null) { pkColumn = pkTable.getColumnByName(pkColumnName); } if (logger.isDebugEnabled()) { logger.debug("Found primary key relation: tableName=" + pkTableName + ",columnName=" + pkColumnName + ", matching column: " + pkColumn); } String fkTableName = rs.getString(7); String fkColumnName = rs.getString(8); Column fkColumn = null; Table fkTable = schema.getTableByName(fkTableName); if (fkTable != null) { fkColumn = fkTable.getColumnByName(fkColumnName); } if (logger.isDebugEnabled()) { logger.debug("Found foreign key relation: tableName=" + fkTableName + ",columnName=" + fkColumnName + ", matching column: " + fkColumn); } if (pkColumn == null || fkColumn == null) { logger.error( "Could not find relation columns: pkTableName={},pkColumnName={},fkTableName={},fkColumnName={}", pkTableName, pkColumnName, fkTableName, fkColumnName); logger.error("pkColumn={}", pkColumn); logger.error("fkColumn={}", fkColumn); } else { MutableRelationship.createRelationship(new Column[] { pkColumn }, new Column[] { fkColumn }); } } } }