/**
* 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 });
}
}
}
}