/* * Copyright 2014, Armenak Grigoryan, and individual contributors as indicated * by the @authors tag. See the copyright.txt in the distribution for a * full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package com.strider.datadefender.database.metadata; import com.strider.datadefender.DbConfig; import com.strider.datadefender.DbConfig.Vendor; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.regex.Pattern; import org.apache.commons.lang3.StringUtils; import org.apache.commons.collections4.CollectionUtils; import lombok.extern.log4j.Log4j2; /** * Class to hold common logic between different metadata implementations. * * @author Akira Matsuo */ @Log4j2 public class MetaData implements IMetaData { private final Connection connection; protected final DbConfig config; protected final SqlTypeToClass sqlTypeMap; public MetaData(DbConfig config, Connection connection) { this(config, connection, new SqlTypeToClass()); } public MetaData(DbConfig config, Connection connection, SqlTypeToClass sqlTypeMap) { this.config = config; this.connection = connection; this.sqlTypeMap = sqlTypeMap; } /** * Returns a list of metadata information for tables in the current * database/schema. * * @return */ @Override public List<TableMetaData> getMetaData() throws SQLException { final List<TableMetaData> tables = new ArrayList<>(); // Getting all tables name final DatabaseMetaData md = connection.getMetaData(); log.info("Fetching table names"); try (ResultSet trs = getTableResultSet(md)) { while (trs.next()) { final String tableName = trs.getString("TABLE_NAME"); log.info("Processing table [" + tableName + "]"); if (skipTable(tableName)) { continue; } TableMetaData table = new TableMetaData(config.getSchema(), tableName); addColumnMetaData(md, table); tables.add(table); } } return tables; } /** * Returns a list of metadata information for columns in the passed * ResultSet. * * @param rs * @return * @throws SQLException */ @Override public TableMetaData getMetaDataFor(final ResultSet rs) throws SQLException { TableMetaData table = null; final ResultSetMetaData rsmd = rs.getMetaData(); for (int i = 1; i <= rsmd.getColumnCount(); ++i) { if (table == null) { table = new TableMetaData(rsmd.getSchemaName(i), rsmd.getTableName(i)); } table.addColumn( i, rsmd.getColumnName(i), sqlTypeMap.getTypeFrom(rsmd.getColumnType(i)), rsmd.getColumnDisplaySize(i), false, null ); } return table; } /** * Loops over passed patterns, and returns true if one of them matches the * tableName. * * @param patterns * @param tableName * @param isIncludePatterns Used for debug output (either "included" or * "excluded" in output) * @return */ private boolean checkPatternsWithTableName(List<Pattern> patterns, String tableName, boolean isIncludePatterns) { String upperName = tableName.toUpperCase(Locale.ENGLISH); boolean ret = patterns.stream().anyMatch( (p) -> p.matcher(upperName).matches() ); String debugLogFound = (isIncludePatterns) ? "Table {} included by pattern: {}" : "Table {} excluded by pattern: {}"; String debugLogNotFound = (isIncludePatterns) ? "Table {} did not match any inclusion patterns" : "Table {} did not match any exclusion patterns"; log.debug( (ret) ? debugLogFound : debugLogNotFound, () -> tableName, () -> patterns.stream().filter( (p) -> p.matcher(upperName).matches() ).findFirst().map((p) -> p.pattern()).orElse("") ); return ret; } /** * Returns true if the table should be included based on configured/passed * 'include' and 'exclude' patterns. * * @param tableName * @return */ private boolean includeTable(String tableName) { List<Pattern> exclude = config.getExcludeTablePatterns(); List<Pattern> include = config.getIncludeTablePatterns(); if (!CollectionUtils.isEmpty(include) && !checkPatternsWithTableName(include, tableName, true)) { return false; } return CollectionUtils.isEmpty(exclude) || !checkPatternsWithTableName(exclude, tableName, false); } /** * Returns true if the table should be skipped for metadata extraction. * * @param tableName * @return */ protected boolean skipTable(String tableName) { if (config.getVendor() == Vendor.POSTGRESQL && tableName.startsWith("sql_")) { log.info("Skipping postgresql 'sql_' table: {}", tableName); return true; } if (!includeTable(tableName)) { log.info("Excluding table by inclusion/exclusion rules: {}", tableName); return true; } String schemaTableName = tableName; if (!StringUtils.isBlank(config.getSchema())) { schemaTableName = config.getSchema() + "." + tableName; } if (config.isSkipEmptyTables() && getNumberOfRows(schemaTableName) == 0) { log.info("Skipping empty table: {}", tableName); return true; } return false; } /** * Returns ResultSet for tables. * * @param md * @return * @throws SQLException */ protected ResultSet getTableResultSet(final DatabaseMetaData md) throws SQLException { return md.getTables(null, config.getSchema(), null, new String[] { "TABLE" }); } /** * Performs a COUNT(*) query on the passed table to determine number of rows * in a table. * * @param table * @return */ private int getNumberOfRows(final String table) { int rowNum = 0; try ( Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery("SELECT count(*) FROM " + table) ) { rs.next(); rowNum = rs.getInt(1); } catch (SQLException sqle) { log.error(sqle.toString()); } return rowNum; } /** * Returns a ResultSet representing columns in the passed DatabaseMetaData * object. Overridable to account for differences between databases, but * essentially a call to DatabaseMetaData.getColumns(). * * @param md * @param tableName * @return * @throws SQLException */ protected ResultSet getColumnResultSet(final DatabaseMetaData md, final String tableName) throws SQLException { return md.getColumns(null, config.getSchema(), tableName, null); } /** * For a DatabaseMetaData.getColumns ResultSet, calls * rs.getString("COLUMN_NAME") to return the name of the column. * * @param rs * @return * @throws SQLException */ private String getColumnName(final ResultSet rs) throws SQLException { return rs.getString("COLUMN_NAME"); } /** * For a DatabaseMetaData.getColumns ResultSet, calls * rs.getInt("COLUMN_SIZE") to return the size of the column. * * @param rs * @return * @throws SQLException */ private int getColumnSize(final ResultSet rs) throws SQLException { return rs.getInt("COLUMN_SIZE"); } /** * For a DatabaseMetaData.getColumns ResultSet, calls * rs.getString("DATA_TYPE") and uses SqlTypeToClass.getTypeFrom() to get a * Class to represent the type of column. * * @param rs * @return * @throws SQLException */ private Class getColumnType(final ResultSet rs) throws SQLException { int type = rs.getInt("DATA_TYPE"); return sqlTypeMap.getTypeFrom(type); } /** * Returns a Map of column names as keys, and values representing the * foreign key relationship for the passed DatabaseMetaData and tableName. * * @param md * @param tableName * @return * @throws SQLException */ private Map<String, String> getForeignKeysMap(final DatabaseMetaData md, final String tableName) throws SQLException { Map<String, String> ret = new HashMap<>(); try (ResultSet rs = getForeignKeysResultSet(md, tableName)) { while (rs.next()) { String col = rs.getString("FKCOLUMN_NAME").toLowerCase(Locale.ENGLISH); String fkey = (rs.getString("PKTABLE_NAME") + "." + rs.getString("PKCOLUMN_NAME")).toLowerCase(Locale.ENGLISH); log.debug("Found foreign key for column: {} referencing: {}", col, fkey); ret.put(col, fkey); } } return ret; } /** * Returns a ResultSet for foreign keys of the passed DatabaseMetaData and * tableName. * * @param md * @param tableName * @return * @throws SQLException */ protected ResultSet getForeignKeysResultSet(final DatabaseMetaData md, final String tableName) throws SQLException { return md.getImportedKeys(null, config.getSchema(), tableName); } /** * Returns a List of column names representing primary keys for the passed * DatabaseMetaData and tableName. * * @param md * @param tableName * @return * @throws SQLException */ private List<String> getPrimaryKeysList(final DatabaseMetaData md, final String tableName) throws SQLException { List<String> ret = new ArrayList<>(); try (ResultSet rs = getPrimaryKeysResultSet(md, tableName)) { while (rs.next()) { String pkey = rs.getString("COLUMN_NAME"); log.debug("Found primary key: {}", pkey); ret.add(pkey.toLowerCase(Locale.ENGLISH)); } } return ret; } /** * Returns a ResultSet for primary keys of the passed DatabaseMetaData and * tableName. * * @param md * @param tableName * @return * @throws SQLException */ protected ResultSet getPrimaryKeysResultSet(final DatabaseMetaData md, final String tableName) throws SQLException { return md.getPrimaryKeys(null, config.getSchema(), tableName); } /** * Finds and adds columns to the passed TableMetaData. * @param md * @param table * @throws SQLException */ private void addColumnMetaData(DatabaseMetaData md, TableMetaData table) throws SQLException { final List<String> pKeys = getPrimaryKeysList(md, table.getTableName()); final Map<String, String> fKeys = getForeignKeysMap(md, table.getTableName()); try (ResultSet crs = getColumnResultSet(md, table.getTableName())) { int index = 1; while (crs.next()) { String columnName = getColumnName(crs); boolean isPrimaryKey = pKeys.contains(columnName.toLowerCase(Locale.ENGLISH)); String foreignKey = fKeys.get(columnName.toLowerCase(Locale.ENGLISH)); log.debug("Found metadata for column {} in table {}", columnName, table); table.addColumn(index, columnName, getColumnType(crs), getColumnSize(crs), isPrimaryKey, foreignKey); ++index; } } } }