diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/SQLiteViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/SQLiteViewer.java index 40a667e3d7f2d90bf0863a1c3799aaead1b8e675..6988e46527e7e36476b57475005e407fb6f54e76 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/SQLiteViewer.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/SQLiteViewer.java @@ -22,23 +22,17 @@ import java.awt.Component; import java.awt.Cursor; import java.io.File; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.ResultSet; -import java.sql.ResultSetMetaData; import java.sql.SQLException; -import java.sql.Statement; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.TreeMap; import java.util.logging.Level; +import java.util.stream.Collectors; import javax.swing.JComboBox; import javax.swing.JFileChooser; import javax.swing.JOptionPane; @@ -48,14 +42,11 @@ import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; -import org.sleuthkit.autopsy.casemodule.services.FileManager; -import org.sleuthkit.autopsy.casemodule.services.Services; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.datamodel.ContentUtils; import org.sleuthkit.datamodel.AbstractFile; -import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import org.sleuthkit.autopsy.sqlitereader.SQLiteReader; /** * A file content viewer for SQLite database files. @@ -70,7 +61,7 @@ class SQLiteViewer extends javax.swing.JPanel implements FileTypeViewer { private final SQLiteTableView selectedTableView = new SQLiteTableView(); private AbstractFile sqliteDbFile; private File tmpDbFile; - private Connection connection; + private SQLiteReader sqliteReader; private int numRows; // num of rows in the selected table private int currPage = 0; // curr page of rows being displayed @@ -347,10 +338,10 @@ public void resetComponent() { numEntriesField.setText(""); // close DB connection to file - if (null != connection) { + if (null != sqliteReader) { try { - connection.close(); - connection = null; + sqliteReader.close(); + sqliteReader = null; } catch (SQLException ex) { logger.log(Level.SEVERE, "Failed to close DB connection to file.", ex); //NON-NLS } @@ -370,41 +361,15 @@ public void resetComponent() { "SQLiteViewer.errorMessage.failedToQueryDatabase=The database tables in the file could not be read.", "SQLiteViewer.errorMessage.failedToinitJDBCDriver=The JDBC driver for SQLite could not be loaded.", "# {0} - exception message", "SQLiteViewer.errorMessage.unexpectedError=An unexpected error occurred:\n{0).",}) - private void processSQLiteFile() { - + private void processSQLiteFile() { tablesDropdownList.removeAllItems(); - - // Copy the file to temp folder - String tmpDBPathName; try { - tmpDBPathName = Case.getCurrentCaseThrows().getTempDirectory() + File.separator + sqliteDbFile.getName(); - } catch (NoCurrentCaseException ex) { - logger.log(Level.SEVERE, "Current case has been closed", ex); //NON-NLS - MessageNotifyUtil.Message.error(Bundle.SQLiteViewer_errorMessage_noCurrentCase()); - return; - } - - tmpDbFile = new File(tmpDBPathName); - if (! tmpDbFile.exists()) { - try { - ContentUtils.writeToFile(sqliteDbFile, tmpDbFile); - - // Look for any meta files associated with this DB - WAL, SHM, etc. - findAndCopySQLiteMetaFile(sqliteDbFile, sqliteDbFile.getName() + "-wal"); - findAndCopySQLiteMetaFile(sqliteDbFile, sqliteDbFile.getName() + "-shm"); - } catch (IOException | NoCurrentCaseException | TskCoreException ex) { - logger.log(Level.SEVERE, String.format("Failed to create temp copy of DB file '%s' (objId=%d)", sqliteDbFile.getName(), sqliteDbFile.getId()), ex); //NON-NLS - MessageNotifyUtil.Message.error(Bundle.SQLiteViewer_errorMessage_failedToExtractFile()); - return; - } - } - - try { - // Load the SQLite JDBC driver, if necessary. - Class.forName("org.sqlite.JDBC"); //NON-NLS - connection = DriverManager.getConnection("jdbc:sqlite:" + tmpDBPathName); //NON-NLS - - Map<String, String> dbTablesMap = getTables(); + String localDiskPath = Case.getCurrentCaseThrows().getTempDirectory() + + File.separator + sqliteDbFile.getName(); + sqliteReader = new SQLiteReader(sqliteDbFile, localDiskPath); + + Map<String, String> dbTablesMap = sqliteReader.getTableSchemas(); + if (dbTablesMap.isEmpty()) { tablesDropdownList.addItem(Bundle.SQLiteViewer_comboBox_noTableEntry()); tablesDropdownList.setEnabled(false); @@ -413,78 +378,36 @@ private void processSQLiteFile() { tablesDropdownList.addItem(tableName); }); } + } catch (NoCurrentCaseException ex) { + logger.log(Level.SEVERE, "Current case has been closed", ex); //NON-NLS + MessageNotifyUtil.Message.error(Bundle.SQLiteViewer_errorMessage_noCurrentCase()); + } catch (IOException | TskCoreException ex) { + logger.log(Level.SEVERE, String.format( + "Failed to create temp copy of DB file '%s' (objId=%d)", //NON-NLS + sqliteDbFile.getName(), sqliteDbFile.getId()), ex); + MessageNotifyUtil.Message.error( + Bundle.SQLiteViewer_errorMessage_failedToExtractFile()); } catch (ClassNotFoundException ex) { - logger.log(Level.SEVERE, String.format("Failed to initialize JDBC SQLite '%s' (objId=%d)", sqliteDbFile.getName(), sqliteDbFile.getId()), ex); //NON-NLS - MessageNotifyUtil.Message.error(Bundle.SQLiteViewer_errorMessage_failedToinitJDBCDriver()); + logger.log(Level.SEVERE, String.format( + "Failed to initialize JDBC SQLite '%s' (objId=%d)", //NON-NLS + sqliteDbFile.getName(), sqliteDbFile.getId()), ex); + MessageNotifyUtil.Message.error( + Bundle.SQLiteViewer_errorMessage_failedToinitJDBCDriver()); } catch (SQLException ex) { - logger.log(Level.SEVERE, String.format("Failed to get tables from DB file '%s' (objId=%d)", sqliteDbFile.getName(), sqliteDbFile.getId()), ex); //NON-NLS - MessageNotifyUtil.Message.error(Bundle.SQLiteViewer_errorMessage_failedToQueryDatabase()); - } - } - - /** - * Searches for a meta file associated with the give SQLite db If found, - * copies the file to the temp folder - * - * @param sqliteFile - SQLIte db file being processed - * @param metaFileName name of meta file to look for - */ - private void findAndCopySQLiteMetaFile(AbstractFile sqliteFile, String metaFileName) throws NoCurrentCaseException, TskCoreException, IOException { - Case openCase = Case.getCurrentCaseThrows(); - SleuthkitCase sleuthkitCase = openCase.getSleuthkitCase(); - Services services = new Services(sleuthkitCase); - FileManager fileManager = services.getFileManager(); - List<AbstractFile> metaFiles = fileManager.findFiles(sqliteFile.getDataSource(), metaFileName, sqliteFile.getParent().getName()); - if (metaFiles != null) { - for (AbstractFile metaFile : metaFiles) { - String tmpMetafilePathName = openCase.getTempDirectory() + File.separator + metaFile.getName(); - File tmpMetafile = new File(tmpMetafilePathName); - ContentUtils.writeToFile(metaFile, tmpMetafile); - } + logger.log(Level.SEVERE, String.format( + "Failed to get tables from DB file '%s' (objId=%d)", //NON-NLS + sqliteDbFile.getName(), sqliteDbFile.getId()), ex); + MessageNotifyUtil.Message.error( + Bundle.SQLiteViewer_errorMessage_failedToQueryDatabase()); } } - /** - * Gets the table names and schemas from the SQLite database file. - * - * @return A mapping of table names to SQL CREATE TABLE statements. - */ - private Map<String, String> getTables() throws SQLException { - Map<String, String> dbTablesMap = new TreeMap<>(); - Statement statement = null; - ResultSet resultSet = null; - try { - statement = connection.createStatement(); - resultSet = statement.executeQuery( - "SELECT name, sql FROM sqlite_master " - + " WHERE type= 'table' " - + " ORDER BY name;"); //NON-NLS - while (resultSet.next()) { - String tableName = resultSet.getString("name"); //NON-NLS - String tableSQL = resultSet.getString("sql"); //NON-NLS - dbTablesMap.put(tableName, tableSQL); - } - } finally { - if (null != resultSet) { - resultSet.close(); - } - if (null != statement) { - statement.close(); - } - } - return dbTablesMap; - } - @NbBundle.Messages({"# {0} - tableName", "SQLiteViewer.selectTable.errorText=Error getting row count for table: {0}" }) private void selectTable(String tableName) { - - try (Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery( - "SELECT count (*) as count FROM " + tableName)) { //NON-NLS{ - - numRows = resultSet.getInt("count"); + try { + numRows = sqliteReader.getTableRowCount(tableName); numEntriesField.setText(numRows + " entries"); currPage = 1; @@ -504,8 +427,11 @@ private void selectTable(String tableName) { } } catch (SQLException ex) { - logger.log(Level.SEVERE, String.format("Failed to load table %s from DB file '%s' (objId=%d)", tableName, sqliteDbFile.getName(), sqliteDbFile.getId()), ex); //NON-NLS - MessageNotifyUtil.Message.error(Bundle.SQLiteViewer_selectTable_errorText(tableName)); + logger.log(Level.SEVERE, String.format( + "Failed to load table %s from DB file '%s' (objId=%d)", tableName, //NON-NLS + sqliteDbFile.getName(), sqliteDbFile.getId()), ex); + MessageNotifyUtil.Message.error( + Bundle.SQLiteViewer_selectTable_errorText(tableName)); } } @@ -513,109 +439,108 @@ private void selectTable(String tableName) { "SQLiteViewer.readTable.errorText=Error getting rows for table: {0}"}) private void readTable(String tableName, int startRow, int numRowsToRead) { - try ( - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery( - "SELECT * FROM " + tableName - + " LIMIT " + Integer.toString(numRowsToRead) - + " OFFSET " + Integer.toString(startRow - 1))) { - - ArrayList<Map<String, Object>> rows = resultSetToArrayList(resultSet); + try { + List<Map<String, Object>> rows = sqliteReader.getRowsFromTable( + tableName, startRow, numRowsToRead); if (Objects.nonNull(rows)) { selectedTableView.setupTable(rows); } else { selectedTableView.setupTable(Collections.emptyList()); } } catch (SQLException ex) { - logger.log(Level.SEVERE, String.format("Failed to read table %s from DB file '%s' (objId=%d)", tableName, sqliteDbFile.getName(), sqliteDbFile.getId()), ex); //NON-NLS - MessageNotifyUtil.Message.error(Bundle.SQLiteViewer_readTable_errorText(tableName)); + logger.log(Level.SEVERE, String.format( + "Failed to read table %s from DB file '%s' (objId=%d)", tableName, //NON-NLS + sqliteDbFile.getName(), sqliteDbFile.getId()), ex); + MessageNotifyUtil.Message.error( + Bundle.SQLiteViewer_readTable_errorText(tableName)); } } + + /** + * Converts a sqlite table into a CSV file. + * + * @param file + * @param tableName + * @param rowMap -- A list of rows in the table, where each row is represented as a column-value + * map. + * @throws FileNotFoundException + * @throws IOException + */ + @NbBundle.Messages({ + "SQLiteViewer.exportTableToCsv.FileName=File name: ", + "SQLiteViewer.exportTableToCsv.TableName=Table name: " + }) + public void exportTableToCSV(File file, String tableName, + List<Map<String, Object>> rowMap) throws FileNotFoundException, IOException{ + + File csvFile; + String fileName = file.getName(); + if (FilenameUtils.getExtension(fileName).equalsIgnoreCase("csv")) { + csvFile = file; + } else { + csvFile = new File(file.toString() + ".csv"); + } - @NbBundle.Messages("SQLiteViewer.BlobNotShown.message=BLOB Data not shown") - private ArrayList<Map<String, Object>> resultSetToArrayList(ResultSet rs) throws SQLException { - ResultSetMetaData metaData = rs.getMetaData(); - int columns = metaData.getColumnCount(); - ArrayList<Map<String, Object>> rowlist = new ArrayList<>(); - while (rs.next()) { - Map<String, Object> row = new LinkedHashMap<>(columns); - for (int i = 1; i <= columns; ++i) { - if (rs.getObject(i) == null) { - row.put(metaData.getColumnName(i), ""); - } else { - if (metaData.getColumnTypeName(i).compareToIgnoreCase("blob") == 0) { - row.put(metaData.getColumnName(i), Bundle.SQLiteViewer_BlobNotShown_message()); - } else { - row.put(metaData.getColumnName(i), rs.getObject(i)); - } - } + try (FileOutputStream out = new FileOutputStream(csvFile, false)) { + + out.write((Bundle.SQLiteViewer_exportTableToCsv_FileName() + csvFile.getName() + "\n").getBytes()); + out.write((Bundle.SQLiteViewer_exportTableToCsv_TableName() + tableName + "\n").getBytes()); + + String header = createColumnHeader(rowMap.get(0)).concat("\n"); + out.write(header.getBytes()); + + for (Map<String, Object> maps : rowMap) { + String row = maps.values() + .stream() + .map(Object::toString) + .collect(Collectors.joining(",")) + .concat("\n"); + out.write(row.getBytes()); } - rowlist.add(row); } - - return rowlist; } - @NbBundle.Messages({"SQLiteViewer.exportTableToCsv.write.errText=Failed to export table content to csv file.", - "SQLiteViewer.exportTableToCsv.FileName=File name: ", - "SQLiteViewer.exportTableToCsv.TableName=Table name: " + @NbBundle.Messages({ + "SQLiteViewer.exportTableToCsv.write.errText=Failed to export table content to csv file.", }) private void exportTableToCsv(File file) { String tableName = (String) this.tablesDropdownList.getSelectedItem(); - try ( - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("SELECT * FROM " + tableName)) { - List<Map<String, Object>> currentTableRows = resultSetToArrayList(resultSet); + try { + List<Map<String, Object>> currentTableRows = + sqliteReader.getRowsFromTable(tableName); if (Objects.isNull(currentTableRows) || currentTableRows.isEmpty()) { - logger.log(Level.INFO, String.format("The table %s is empty. (objId=%d)", tableName, sqliteDbFile.getId())); //NON-NLS + logger.log(Level.INFO, String.format( + "The table %s is empty. (objId=%d)", tableName, //NON-NLS + sqliteDbFile.getId())); } else { - File csvFile; - String fileName = file.getName(); - if (FilenameUtils.getExtension(fileName).equalsIgnoreCase("csv")) { - csvFile = file; - } else { - csvFile = new File(file.toString() + ".csv"); - } - - try (FileOutputStream out = new FileOutputStream(csvFile, false)) { - - out.write((Bundle.SQLiteViewer_exportTableToCsv_FileName() + csvFile.getName() + "\n").getBytes()); - out.write((Bundle.SQLiteViewer_exportTableToCsv_TableName() + tableName + "\n").getBytes()); - // Set up the column names - Map<String, Object> row = currentTableRows.get(0); - StringBuffer header = new StringBuffer(); - for (Map.Entry<String, Object> col : row.entrySet()) { - String colName = col.getKey(); - if (header.length() > 0) { - header.append(',').append(colName); - } else { - header.append(colName); - } - } - out.write(header.append('\n').toString().getBytes()); - - for (Map<String, Object> maps : currentTableRows) { - StringBuffer valueLine = new StringBuffer(); - maps.values().forEach((value) -> { - if (valueLine.length() > 0) { - valueLine.append(',').append(value.toString()); - } else { - valueLine.append(value.toString()); - } - }); - out.write(valueLine.append('\n').toString().getBytes()); - } - } + exportTableToCSV(file, tableName, currentTableRows); } } catch (SQLException ex) { - logger.log(Level.SEVERE, String.format("Failed to read table %s from DB file '%s' (objId=%d)", tableName, sqliteDbFile.getName(), sqliteDbFile.getId()), ex); //NON-NLS - MessageNotifyUtil.Message.error(Bundle.SQLiteViewer_readTable_errorText(tableName)); + logger.log(Level.SEVERE, String.format( + "Failed to read table %s from DB file '%s' (objId=%d)", //NON-NLS + tableName, sqliteDbFile.getName(), sqliteDbFile.getId()), ex); + MessageNotifyUtil.Message.error( + Bundle.SQLiteViewer_readTable_errorText(tableName)); } catch (IOException ex) { - logger.log(Level.SEVERE, String.format("Failed to export table %s to file '%s'", tableName, file.getName()), ex); //NON-NLS - MessageNotifyUtil.Message.error(Bundle.SQLiteViewer_exportTableToCsv_write_errText()); + logger.log(Level.SEVERE, String.format( + "Failed to export table %s to file '%s'", tableName, file.getName()), ex); //NON-NLS + MessageNotifyUtil.Message.error( + Bundle.SQLiteViewer_exportTableToCsv_write_errText()); } } - + /** + * Returns a comma seperated header string from the keys of the column + * row map. + * + * @param row -- column header row map + * @return -- comma seperated header string + */ + private String createColumnHeader(Map<String, Object> row) { + return row.entrySet() + .stream() + .map(Map.Entry::getKey) + .collect(Collectors.joining(",")); + } } diff --git a/Core/src/org/sleuthkit/autopsy/sqlitereader/SQLiteReader.java b/Core/src/org/sleuthkit/autopsy/sqlitereader/SQLiteReader.java new file mode 100755 index 0000000000000000000000000000000000000000..34cd2969e7c79b55d7ba7d5048f52ab96b656b3e --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/sqlitereader/SQLiteReader.java @@ -0,0 +1,272 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2018-2018 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed 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.sleuthkit.autopsy.sqlitereader; + +import java.io.File; +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.casemodule.services.FileManager; +import org.sleuthkit.autopsy.casemodule.services.Services; +import org.sleuthkit.autopsy.datamodel.ContentUtils; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Reads rows from SQLite tables and returns results in a list collection. + */ +public class SQLiteReader implements AutoCloseable { + + private final Connection connection; + + /** + * Writes data source file contents to local disk and opens a sqlite JDBC + * connection. + * + * @param sqliteDbFile Data source abstract file + * @param localDiskPath Location for database contents to be copied to + * @throws ClassNotFoundException missing SQLite JDBC class + * @throws SQLException Exception opening JDBC connection + * @throws IOException Exception writing file contents + * @throws NoCurrentCaseException Current case closed during file copying + * @throws TskCoreException Exception finding files from abstract file + */ + public SQLiteReader(AbstractFile sqliteDbFile, String localDiskPath) throws ClassNotFoundException, + SQLException, IOException, NoCurrentCaseException, TskCoreException{ + + writeDataSourceToLocalDisk(sqliteDbFile, localDiskPath); + connection = getDatabaseConnection(localDiskPath); + } + + /** + * Copies the data source file contents to local drive for processing. + * + * @param file AbstractFile from the data source + * @param localDiskPath Local drive path to copy AbstractFile contents + * @throws IOException Exception writing file contents + * @throws NoCurrentCaseException Current case closed during file copying + * @throws TskCoreException Exception finding files from abstract file + */ + private void writeDataSourceToLocalDisk(AbstractFile file, String localDiskPath) + throws IOException, NoCurrentCaseException, TskCoreException { + + File localDatabaseFile = new File(localDiskPath); + if (!localDatabaseFile.exists()) { + ContentUtils.writeToFile(file, localDatabaseFile); + + // Look for any meta files associated with this DB - WAL, SHM, etc. + findAndCopySQLiteMetaFile(file, file.getName() + "-wal"); + findAndCopySQLiteMetaFile(file, file.getName() + "-shm"); + } + } + + /** + * Searches for a meta file associated with the give SQLite database. If found, + * copies the file to the local disk folder + * + * @param sqliteFile SQLIte db file being processed + * @param metaFileName name of meta file to look for + * @throws NoCurrentCaseException Case has been closed. + * @throws TskCoreException fileManager cannot find AbstractFile files. + * @throws IOException Issue during writing to file. + */ + private void findAndCopySQLiteMetaFile(AbstractFile sqliteFile, + String metaFileName) throws NoCurrentCaseException, TskCoreException, IOException { + + Case openCase = Case.getCurrentCaseThrows(); + SleuthkitCase sleuthkitCase = openCase.getSleuthkitCase(); + Services services = new Services(sleuthkitCase); + FileManager fileManager = services.getFileManager(); + + List<AbstractFile> metaFiles = fileManager.findFiles( + sqliteFile.getDataSource(), metaFileName, + sqliteFile.getParent().getName()); + + if (metaFiles != null) { + for (AbstractFile metaFile : metaFiles) { + String tmpMetafilePathName = openCase.getTempDirectory() + + File.separator + metaFile.getName(); + File tmpMetafile = new File(tmpMetafilePathName); + ContentUtils.writeToFile(metaFile, tmpMetafile); + } + } + } + + /** + * Opens a JDBC connection to the sqlite database specified by the path + * parameter. + * + * @param databasePath Local path of sqlite database + * @return Connection JDBC connection, to be maintained and closed by the reader + * @throws ClassNotFoundException missing SQLite JDBC class + * @throws SQLException Exception during opening database connection + */ + private Connection getDatabaseConnection(String databasePath) + throws ClassNotFoundException, SQLException { + + // Load the SQLite JDBC driver, if necessary. + Class.forName("org.sqlite.JDBC"); //NON-NLS + return DriverManager.getConnection( + "jdbc:sqlite:" + databasePath); //NON-NLS + } + + + /** + * Retrieves a map view of table names to table schemas (in the form of + * CREATE TABLE statments). + * + * @return A map of table names to table schemas + * @throws SQLException + */ + public Map<String, String> getTableSchemas() + throws SQLException { + + Map<String, String> dbTablesMap = new TreeMap<>(); + + try (Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery( + "SELECT name, sql FROM sqlite_master " //NON-NLS + + " WHERE type= 'table' " //NON-NLS + + " ORDER BY name;")){ //NON-NLS + + while (resultSet.next()) { + String tableName = resultSet.getString("name"); //NON-NLS + String tableSQL = resultSet.getString("sql"); //NON-NLS + dbTablesMap.put(tableName, tableSQL); + } + } + + return dbTablesMap; + } + + /** + * Retrieves the total number of rows from a table in the SQLite database. + * + * @param tableName + * @return Row count from tableName + * @throws SQLException + */ + public Integer getTableRowCount(String tableName) throws SQLException { + try (Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery( + "SELECT count (*) as count FROM " + tableName)){ //NON-NLS + return resultSet.getInt("count"); //NON-NLS + } + } + + /** + * Retrieves all rows from a given table in the SQLite database. If only a + * subset of rows are desired, see the overloaded function below. + * + * @param tableName + * @return List of rows, where each row is + * represented as a column-value map. + * @throws SQLException + */ + public List<Map<String, Object>> getRowsFromTable(String tableName) throws SQLException { + + //This method does not directly call its overloaded counterpart + //since the second parameter would need to be retreived from a call to + //getTableRowCount(). + try(Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery( + "SELECT * FROM " + tableName)) { //NON-NLS + return resultSetToList(resultSet); + } + } + + /** + * Retrieves a subset of the rows from a given table in the SQLite database. + * + * @param tableName + * @param startRow Desired start index (rows begin at 1) + * @param numRowsToRead Number of rows past the start index + * @return List of rows, where each row is + * represented as a column-value map. + * @throws SQLException + */ + public List<Map<String, Object>> getRowsFromTable(String tableName, + int startRow, int numRowsToRead) throws SQLException{ + + try(Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery( + "SELECT * FROM " + tableName //NON-NLS + + " LIMIT " + Integer.toString(numRowsToRead) //NON-NLS + + " OFFSET " + Integer.toString(startRow - 1))) { //NON-NLS + return resultSetToList(resultSet); + } + } + + /** + * Converts a ResultSet (row results from a table read) into a list. + * + * @param resultSet row results from a table read + * @return List of rows, where each row is + * represented as a column-value map. + * @throws SQLException occurs if ResultSet is closed while attempting to + * access it's data. + */ + @NbBundle.Messages("SQLiteReader.BlobNotShown.message=BLOB Data not shown") + private List<Map<String, Object>> resultSetToList(ResultSet resultSet) throws SQLException { + + ResultSetMetaData metaData = resultSet.getMetaData(); + int columns = metaData.getColumnCount(); + List<Map<String, Object>> rowMap = new ArrayList<>(); + while (resultSet.next()) { + Map<String, Object> row = new LinkedHashMap<>(columns); + for (int i = 1; i <= columns; ++i) { + if (resultSet.getObject(i) == null) { + row.put(metaData.getColumnName(i), ""); + } else { + if (metaData.getColumnTypeName(i).compareToIgnoreCase("blob") == 0) { + row.put(metaData.getColumnName(i), Bundle.SQLiteReader_BlobNotShown_message()); + } else { + row.put(metaData.getColumnName(i), resultSet.getObject(i)); + } + } + } + rowMap.add(row); + } + + return rowMap; + } + + /** + * Closes underlying JDBC connection. + * + * @throws SQLException + */ + @Override + public void close() throws SQLException { + connection.close(); + } +}