com.strider.datadefender.database.metadata.MetaData   A
last analyzed

Complexity

Total Complexity 37

Size/Duplication

Total Lines 320
Duplicated Lines 0 %

Test Coverage

Coverage 61.47%

Importance

Changes 7
Bugs 0 Features 0
Metric Value
eloc 130
c 7
b 0
f 0
dl 0
loc 320
ccs 67
cts 109
cp 0.6147
rs 9.44
wmc 37

18 Methods

Rating   Name   Duplication   Size   Complexity  
A getColumnResultSet(DatabaseMetaData,String) 0 2 1
A addColumnMetaData(DatabaseMetaData,TableMetaData) 0 14 2
A getNumberOfRows(String) 0 12 2
A includeTable(String) 0 8 3
A getTableResultSet(DatabaseMetaData) 0 2 1
A getMetaDataFor(ResultSet) 0 18 3
B skipTable(String) 0 18 7
A getForeignKeysResultSet(DatabaseMetaData,String) 0 2 1
A getPrimaryKeysList(DatabaseMetaData,String) 0 10 2
A getPrimaryKeysResultSet(DatabaseMetaData,String) 0 2 1
A checkPatternsWithTableName(List,String,boolean) 0 16 4
A MetaData(DbConfig,Connection,SqlTypeToClass) 0 4 1
A getForeignKeysMap(DatabaseMetaData,String) 0 11 2
A MetaData(DbConfig,Connection) 0 2 1
A getColumnName(ResultSet) 0 2 1
A getColumnType(ResultSet) 0 3 1
A getMetaData() 0 23 3
A getColumnSize(ResultSet) 0 2 1
1
/*
2
 * Copyright 2014, Armenak Grigoryan, and individual contributors as indicated
3
 * by the @authors tag. See the copyright.txt in the distribution for a
4
 * full listing of individual contributors.
5
 *
6
 * This is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU Lesser General Public License as
8
 * published by the Free Software Foundation; either version 2.1 of
9
 * the License, or (at your option) any later version.
10
 *
11
 * This software is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14
 * Lesser General Public License for more details.
15
 */
16
package com.strider.datadefender.database.metadata;
17
18
import com.strider.datadefender.DbConfig;
19
import com.strider.datadefender.DbConfig.Vendor;
20
21
import java.sql.Connection;
22
import java.sql.DatabaseMetaData;
23
import java.sql.ResultSet;
24
import java.sql.ResultSetMetaData;
25
import java.sql.SQLException;
26
import java.sql.Statement;
27
import java.util.ArrayList;
28
import java.util.HashMap;
29
import java.util.List;
30
import java.util.Locale;
31
import java.util.Map;
32
import java.util.regex.Pattern;
33
34
import org.apache.commons.lang3.StringUtils;
35
import org.apache.commons.collections4.CollectionUtils;
36
37
import lombok.extern.log4j.Log4j2;
38
39
/**
40
 * Class to hold common logic between different metadata implementations.
41
 *
42
 * @author Akira Matsuo
43
 */
44
@Log4j2
45
public class MetaData implements IMetaData {
46
47
    private final Connection connection;
48
    protected final DbConfig config;
49
    protected final SqlTypeToClass sqlTypeMap;
50
51
    public MetaData(DbConfig config, Connection connection) {
52
        this(config, connection, new SqlTypeToClass());
53
    }
54
55 1
    public MetaData(DbConfig config, Connection connection, SqlTypeToClass sqlTypeMap) {
56 1
        this.config = config;
57 1
        this.connection = connection;
58 1
        this.sqlTypeMap = sqlTypeMap;
59
    }
60
61
    /**
62
     * Returns a list of metadata information for tables in the current
63
     * database/schema.
64
     *
65
     * @return
66
     */
67 1
    @Override
68
    public List<TableMetaData> getMetaData() throws SQLException {
69
70 1
        final List<TableMetaData> tables = new ArrayList<>();
71
72
        // Getting all tables name
73 1
        final DatabaseMetaData md = connection.getMetaData();
74 1
        log.info("Fetching table names");
75
76 2
        try (ResultSet trs = getTableResultSet(md)) {
77 3
            while (trs.next()) {
78 1
                final String tableName = trs.getString("TABLE_NAME");
79 1
                log.info("Processing table [" + tableName + "]");
80 2
                if (skipTable(tableName)) {
81
                    continue;
82
                }
83 1
                TableMetaData table = new TableMetaData(config.getSchema(), tableName);
84 1
                addColumnMetaData(md, table);
85 1
                tables.add(table);
86
            }
87
        }
88
89 1
        return tables;
90
    }
91
92
    /**
93
     * Returns a list of metadata information for columns in the passed
94
     * ResultSet.
95
     *
96
     * @param rs
97
     * @return
98
     * @throws SQLException
99
     */
100
    @Override
101
    public TableMetaData getMetaDataFor(final ResultSet rs) throws SQLException {
102
        TableMetaData table = null;
103
        final ResultSetMetaData rsmd = rs.getMetaData();
104
        for (int i = 1; i <= rsmd.getColumnCount(); ++i) {
105
            if (table == null) {
106
                table = new TableMetaData(rsmd.getSchemaName(i), rsmd.getTableName(i));
107
            }
108
            table.addColumn(
109
                i,
110
                rsmd.getColumnName(i),
111
                sqlTypeMap.getTypeFrom(rsmd.getColumnType(i)),
112
                rsmd.getColumnDisplaySize(i),
113
                false,
114
                null
115
            );
116
        }
117
        return table;
118
    }
119
120
    /**
121
     * Loops over passed patterns, and returns true if one of them matches the
122
     * tableName.
123
     *
124
     * @param patterns
125
     * @param tableName
126
     * @param isIncludePatterns Used for debug output (either "included" or
127
     *  "excluded" in output)
128
     * @return
129
     */
130
    private boolean checkPatternsWithTableName(List<Pattern> patterns, String tableName, boolean isIncludePatterns) {
131
        String upperName = tableName.toUpperCase(Locale.ENGLISH);
132
        boolean ret = patterns.stream().anyMatch(
133
            (p) -> p.matcher(upperName).matches()
134
        );
135
        String debugLogFound = (isIncludePatterns) ? "Table {} included by pattern: {}" : "Table {} excluded by pattern: {}";
136
        String debugLogNotFound = (isIncludePatterns) ? "Table {} did not match any inclusion patterns" : "Table {} did not match any exclusion patterns";
137
138
        log.debug(
139
            (ret) ? debugLogFound : debugLogNotFound,
140
            () -> tableName,
141
            () -> patterns.stream().filter(
142
                (p) -> p.matcher(upperName).matches()
143
            ).findFirst().map((p) -> p.pattern()).orElse("")
144
        );
145
        return ret;
146
    }
147
148
    /**
149
     * Returns true if the table should be included based on configured/passed
150
     * 'include' and 'exclude' patterns.
151
     *
152
     * @param tableName
153
     * @return
154
     */
155 1
    private boolean includeTable(String tableName) {
156 1
        List<Pattern> exclude = config.getExcludeTablePatterns();
157 1
        List<Pattern> include = config.getIncludeTablePatterns();
158 2
        if (!CollectionUtils.isEmpty(include) && !checkPatternsWithTableName(include, tableName, true)) {
159
            return false;
160
        }
161 1
        return CollectionUtils.isEmpty(exclude)
162
            || !checkPatternsWithTableName(exclude, tableName, false);
163
    }
164
165
    /**
166
     * Returns true if the table should be skipped for metadata extraction.
167
     *
168
     * @param tableName
169
     * @return 
170
     */
171 1
    protected boolean skipTable(String tableName) {
172 2
        if (config.getVendor() == Vendor.POSTGRESQL && tableName.startsWith("sql_")) {
173
            log.info("Skipping postgresql 'sql_' table: {}", tableName);
174
            return true;
175
        }
176 2
        if (!includeTable(tableName)) {
177
            log.info("Excluding table by inclusion/exclusion rules: {}", tableName);
178
            return true;
179
        }
180 1
        String schemaTableName = tableName;
181 2
        if (!StringUtils.isBlank(config.getSchema())) {
182 1
            schemaTableName = config.getSchema() + "." + tableName;
183
        }
184 2
        if (config.isSkipEmptyTables() && getNumberOfRows(schemaTableName) == 0) {
185
            log.info("Skipping empty table: {}", tableName);
186
            return true;
187
        }
188 1
        return false;
189
    }
190
191
    /**
192
     * Returns ResultSet for tables.
193
     * 
194
     * @param md
195
     * @return
196
     * @throws SQLException
197
     */
198 1
    protected ResultSet getTableResultSet(final DatabaseMetaData md) throws SQLException {
199 1
        return md.getTables(null, config.getSchema(), null, new String[] { "TABLE" });
200
    }
201
202
    /**
203
     * Performs a COUNT(*) query on the passed table to determine number of rows
204
     * in a table.
205
     *
206
     * @param table
207
     * @return
208
     */
209
    private int getNumberOfRows(final String table) {
210
        int rowNum = 0;
211
        try (
212
            Statement stmt = connection.createStatement();
213
            ResultSet rs = stmt.executeQuery("SELECT count(*) FROM " + table)
214
        ) {
215
            rs.next();
216
            rowNum = rs.getInt(1);
217
        } catch (SQLException sqle) {
218
            log.error(sqle.toString());
219
        }
220
        return rowNum;
221
    }
222
223
    /**
224
     * Returns a ResultSet representing columns in the passed DatabaseMetaData
225
     * object.  Overridable to account for differences between databases, but
226
     * essentially a call to DatabaseMetaData.getColumns().
227
     *
228
     * @param md
229
     * @param tableName
230
     * @return
231
     * @throws SQLException
232
     */
233 1
    protected ResultSet getColumnResultSet(final DatabaseMetaData md, final String tableName) throws SQLException {
234 1
        return md.getColumns(null, config.getSchema(), tableName, null);
235
    }
236
237
    /**
238
     * For a DatabaseMetaData.getColumns ResultSet, calls
239
     * rs.getString("COLUMN_NAME") to return the name of the column.
240
     *
241
     * @param rs
242
     * @return
243
     * @throws SQLException
244
     */
245 1
    private String getColumnName(final ResultSet rs) throws SQLException {
246 1
        return rs.getString("COLUMN_NAME");
247
    }
248
249
    /**
250
     * For a DatabaseMetaData.getColumns ResultSet, calls 
251
     * rs.getInt("COLUMN_SIZE") to return the size of the column.
252
     *
253
     * @param rs
254
     * @return
255
     * @throws SQLException 
256
     */
257 1
    private int getColumnSize(final ResultSet rs) throws SQLException {
258 1
        return rs.getInt("COLUMN_SIZE");
259
    }
260
261
    /**
262
     * For a DatabaseMetaData.getColumns ResultSet, calls
263
     * rs.getString("DATA_TYPE") and uses SqlTypeToClass.getTypeFrom() to get a
264
     * Class to represent the type of column.
265
     *
266
     * @param rs
267
     * @return
268
     * @throws SQLException
269
     */
270 1
    private Class getColumnType(final ResultSet rs) throws SQLException {
271 1
        int type = rs.getInt("DATA_TYPE");
272 1
        return sqlTypeMap.getTypeFrom(type);
273
    }
274
275
    /**
276
     * Returns a Map of column names as keys, and values representing the
277
     * foreign key relationship for the passed DatabaseMetaData and tableName.
278
     *
279
     * @param md
280
     * @param tableName
281
     * @return
282
     * @throws SQLException
283
     */
284 1
    private Map<String, String> getForeignKeysMap(final DatabaseMetaData md, final String tableName) throws SQLException {
285 1
        Map<String, String> ret = new HashMap<>();
286 2
        try (ResultSet rs = getForeignKeysResultSet(md, tableName)) {
287 2
            while (rs.next()) {
288
                String col = rs.getString("FKCOLUMN_NAME").toLowerCase(Locale.ENGLISH);
289
                String fkey = (rs.getString("PKTABLE_NAME") + "." + rs.getString("PKCOLUMN_NAME")).toLowerCase(Locale.ENGLISH);
290
                log.debug("Found foreign key for column: {} referencing: {}", col, fkey);
291
                ret.put(col, fkey);
292
            }
293
        }
294 1
        return ret;
295
    }
296
297
    /**
298
     * Returns a ResultSet for foreign keys of the passed DatabaseMetaData and
299
     * tableName.
300
     *
301
     * @param md
302
     * @param tableName
303
     * @return
304
     * @throws SQLException
305
     */
306 1
    protected ResultSet getForeignKeysResultSet(final DatabaseMetaData md, final String tableName) throws SQLException {
307 1
        return md.getImportedKeys(null, config.getSchema(), tableName);
308
    }
309
310
    /**
311
     * Returns a List of column names representing primary keys for the passed
312
     * DatabaseMetaData and tableName.
313
     *
314
     * @param md
315
     * @param tableName
316
     * @return
317
     * @throws SQLException
318
     */
319 1
    private List<String> getPrimaryKeysList(final DatabaseMetaData md, final String tableName) throws SQLException {
320 1
        List<String> ret = new ArrayList<>();
321 2
        try (ResultSet rs = getPrimaryKeysResultSet(md, tableName)) {
322 2
            while (rs.next()) {
323
                String pkey = rs.getString("COLUMN_NAME");
324
                log.debug("Found primary key: {}", pkey);
325
                ret.add(pkey.toLowerCase(Locale.ENGLISH));
326
            }
327
        }
328 1
        return ret;
329
    }
330
331
    /**
332
     * Returns a ResultSet for primary keys of the passed DatabaseMetaData and
333
     * tableName.
334
     *
335
     * @param md
336
     * @param tableName
337
     * @return
338
     * @throws SQLException
339
     */
340 1
    protected ResultSet getPrimaryKeysResultSet(final DatabaseMetaData md, final String tableName) throws SQLException {
341 1
        return md.getPrimaryKeys(null, config.getSchema(), tableName);
342
    }
343
344
    /**
345
     * Finds and adds columns to the passed TableMetaData.
346
     * @param md
347
     * @param table
348
     * @throws SQLException
349
     */
350 1
    private void addColumnMetaData(DatabaseMetaData md, TableMetaData table) throws SQLException {
351
352 1
        final List<String> pKeys = getPrimaryKeysList(md, table.getTableName());
353 1
        final Map<String, String> fKeys = getForeignKeysMap(md, table.getTableName());
354
355 2
        try (ResultSet crs = getColumnResultSet(md, table.getTableName())) {
356 1
            int index = 1;
357 3
            while (crs.next()) {
358 1
                String columnName = getColumnName(crs);
359 1
                boolean isPrimaryKey = pKeys.contains(columnName.toLowerCase(Locale.ENGLISH));
360 1
                String foreignKey = fKeys.get(columnName.toLowerCase(Locale.ENGLISH));
361 1
                log.debug("Found metadata for column {} in table {}", columnName, table);
362 1
                table.addColumn(index, columnName, getColumnType(crs), getColumnSize(crs), isPrimaryKey, foreignKey);
363 1
                ++index;
364
            }
365
        }
366
    }
367
}
368