From f2e934620b1def043a0ea2c10898f1a8af4fdcb8 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Wed, 11 Mar 2020 00:37:10 +0100 Subject: [PATCH] Added execution of testing and template code. --- .../gyrobian/database/CachedResultSet.java | 95 ++++++++++++++ .../com/gyrobian/database/DatabaseHelper.java | 106 +++++++++++++++ .../gyrobian/database/ExecutionAction.java | 46 +++++++ .../com/gyrobian/database/ExecutionLog.java | 24 ++++ .../com/gyrobian/database/QueryAction.java | 24 ++++ .../com/gyrobian/database/UpdateAction.java | 18 +++ .../listener/ScriptExecutionListener.java | 41 ++++++ .../gyrobian/view/ExecutionLogDisplay.java | 122 ++++++++++++++++++ src/main/java/com/gyrobian/view/Window.form | 55 +++++++- src/main/java/com/gyrobian/view/Window.java | 22 +++- 10 files changed, 545 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/gyrobian/database/CachedResultSet.java create mode 100644 src/main/java/com/gyrobian/database/DatabaseHelper.java create mode 100644 src/main/java/com/gyrobian/database/ExecutionAction.java create mode 100644 src/main/java/com/gyrobian/database/ExecutionLog.java create mode 100644 src/main/java/com/gyrobian/database/QueryAction.java create mode 100644 src/main/java/com/gyrobian/database/UpdateAction.java create mode 100644 src/main/java/com/gyrobian/listener/ScriptExecutionListener.java create mode 100644 src/main/java/com/gyrobian/view/ExecutionLogDisplay.java diff --git a/src/main/java/com/gyrobian/database/CachedResultSet.java b/src/main/java/com/gyrobian/database/CachedResultSet.java new file mode 100644 index 0000000..bf581f3 --- /dev/null +++ b/src/main/java/com/gyrobian/database/CachedResultSet.java @@ -0,0 +1,95 @@ +package com.gyrobian.database; + +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.*; + +/** + * Represents a result set from a SELECT query. + */ +public class CachedResultSet { + private String[] columnNames; + + private List> rows; + + private CachedResultSet(String[] columnNames, List> rows) { + this.columnNames = columnNames; + this.rows = rows; + } + + public int getRowCount() { + return this.rows.size(); + } + + public String[] getColumnNames() { + return columnNames; + } + + public List> getRows() { + return this.rows; + } + + public static CachedResultSet fromResultSet(ResultSet resultSet) throws SQLException { + ResultSetMetaData metaData = resultSet.getMetaData(); + + int columnCount = metaData.getColumnCount(); + String[] columnNames = new String[columnCount]; + for (int i = 1; i <= columnCount; i++) { + columnNames[i - 1] = metaData.getColumnName(i); + } + + List> rows = new ArrayList<>(); + while (resultSet.next()) { + Map row = new HashMap<>(); + for (int columnIndex = 1; columnIndex <= columnCount; columnIndex++) { + row.put(columnNames[columnIndex - 1], resultSet.getString(columnIndex)); + } + rows.add(row); + } + + return new CachedResultSet(columnNames, rows); + } + + public String toFormattedTableString() { + int[] optimumColumnWidths = new int[this.columnNames.length]; + int totalTableWidth = 0; + for (int columnIndex = 0; columnIndex < this.columnNames.length; columnIndex++) { + int optimumWidth = 8; + for (Map row : this.rows) { + int thisRowsValueLength = row.get(this.columnNames[columnIndex]).trim().length(); + if (thisRowsValueLength > optimumWidth) { + optimumWidth = thisRowsValueLength; + } + } + optimumColumnWidths[columnIndex] = optimumWidth; + totalTableWidth += optimumWidth; + } + totalTableWidth += this.columnNames.length + 1; // Account for vertical separators. + totalTableWidth += this.columnNames.length; // Account for one space of padding before each value. + + StringBuilder sb = new StringBuilder("-".repeat(totalTableWidth) + '\n'); + for (int columnIndex = 0; columnIndex < this.columnNames.length; columnIndex++) { + sb.append('|'); + sb.append(String.format(" %-" + optimumColumnWidths[columnIndex] + "s", this.columnNames[columnIndex])); + } + sb.append("|\n").append("-".repeat(totalTableWidth)).append('\n'); + + for (Map row : this.rows) { + for (int columnIndex = 0; columnIndex < this.columnNames.length; columnIndex++) { + sb.append('|'); + sb.append(String.format(" %-" + optimumColumnWidths[columnIndex] + "s", row.get(this.columnNames[columnIndex]))); + } + sb.append("|\n").append("-".repeat(totalTableWidth)).append('\n'); + } + return sb.toString(); + } + + @Override + public String toString() { + return "CachedResultSet{" + + "columnNames=" + Arrays.toString(columnNames) + + ", rows=" + rows + + '}'; + } +} diff --git a/src/main/java/com/gyrobian/database/DatabaseHelper.java b/src/main/java/com/gyrobian/database/DatabaseHelper.java new file mode 100644 index 0000000..ca80ed6 --- /dev/null +++ b/src/main/java/com/gyrobian/database/DatabaseHelper.java @@ -0,0 +1,106 @@ +package com.gyrobian.database; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; + +/** + * A helper class that performs step-by-step SQL code execution. + */ +public class DatabaseHelper { + + private String jdbcUrl; + + public DatabaseHelper(String jdbcUrl) { + this.jdbcUrl = jdbcUrl; + } + + /** + * Executes an SQL script and provides an execution log as a result. + * @param script The script to execute, containing possibly many individual queries. + * @return A log of all the actions that took place. + */ + public ExecutionLog executeSQLScript(String script) { + ExecutionLog log = new ExecutionLog(); + try { + Connection connection = DriverManager.getConnection(this.jdbcUrl); + + if (!connection.isValid(1000)) { + throw new SQLException("Invalid connection."); + } + + List queries = splitQueries(script); + Statement statement = connection.createStatement( + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY + ); + + queries.forEach(query -> log.recordAction(this.executeQuery(query, statement))); + + } catch (SQLException e) { + // In case some exception occurred that wasn't related to any particular query. + log.recordAction(new ExecutionAction(null, e)); + } + + return log; + } + + /** + * Executes a single query and outputs the results. + * @param query The query to execute. Must be only one query in the string. + * @param statement The statement used to execute the query. + * @return The execution action which was done by executing this query. + */ + private ExecutionAction executeQuery(String query, Statement statement) { + ExecutionAction action; + try { + if (isSQLStatementQuery(query)) { + action = new QueryAction(query, statement.executeQuery(query), isQueryOrdered(query)); + } else { + action = new UpdateAction(query, statement.executeUpdate(query)); + } + } catch (SQLException e) { + action = new ExecutionAction(query, e); + } + return action; + } + + /** + * Splits and cleans each query so that it will run properly. + * @param queriesString A string containing one or more queries to execute. + * @return A list of individual queries. + */ + private static List splitQueries(String queriesString) { + String[] sections = queriesString.split(";"); + List strings = new ArrayList<>(); + + for (String section : sections) { + String s = section.trim(); + if (!s.isEmpty()) { + strings.add(s); + } + } + + return strings; + } + + /** + * Determines if an SQL string is a query (it should return a result set) + * @param str The string to check. + * @return True if this is a query, or false if it is an update. + */ + private static boolean isSQLStatementQuery(String str) { + String upper = str.toUpperCase(); + return upper.trim().startsWith("SELECT"); + } + + /** + * Determines if a query is ordered by something. + * @param query The query to check. + * @return True if the query makes use of the 'ORDER BY' clause. + */ + private static boolean isQueryOrdered(String query) { + return query.toUpperCase().contains("ORDER BY"); + } + +} diff --git a/src/main/java/com/gyrobian/database/ExecutionAction.java b/src/main/java/com/gyrobian/database/ExecutionAction.java new file mode 100644 index 0000000..4b59645 --- /dev/null +++ b/src/main/java/com/gyrobian/database/ExecutionAction.java @@ -0,0 +1,46 @@ +package com.gyrobian.database; + +import java.sql.SQLException; +import java.time.LocalDateTime; + +/** + * Represents an action performed on a database. + */ +public class ExecutionAction { + /** + * The time at which this action occurred. + */ + private final LocalDateTime occurredAt; + + /** + * The query that was executed. + */ + private final String query; + + /** + * An exception that occurred when this action was executed, if any. + */ + private SQLException exception; + + public ExecutionAction(String query) { + this.query = query; + this.occurredAt = LocalDateTime.now(); + } + + public ExecutionAction(String query, SQLException exception) { + this(query); + this.exception = exception; + } + + public String getQuery() { + return this.query; + } + + public SQLException getException() { + return this.exception; + } + + public LocalDateTime getOccurredAt() { + return this.occurredAt; + } +} diff --git a/src/main/java/com/gyrobian/database/ExecutionLog.java b/src/main/java/com/gyrobian/database/ExecutionLog.java new file mode 100644 index 0000000..ad826f7 --- /dev/null +++ b/src/main/java/com/gyrobian/database/ExecutionLog.java @@ -0,0 +1,24 @@ +package com.gyrobian.database; + +import java.util.ArrayList; +import java.util.List; + +/** + * Contains a log of all actions performed to a database. + */ +public class ExecutionLog { + + private List actions; + + public ExecutionLog() { + this.actions = new ArrayList<>(); + } + + public void recordAction(ExecutionAction action) { + this.actions.add(action); + } + + public List getActions() { + return this.actions; + } +} diff --git a/src/main/java/com/gyrobian/database/QueryAction.java b/src/main/java/com/gyrobian/database/QueryAction.java new file mode 100644 index 0000000..d2a00c3 --- /dev/null +++ b/src/main/java/com/gyrobian/database/QueryAction.java @@ -0,0 +1,24 @@ +package com.gyrobian.database; + +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * An action in which a query result set is returned. Note that SCROLL_INSENSITIVE statements must be used, otherwise + * an SQL exception will be thrown at each attempt to go through the result set. + */ +public class QueryAction extends ExecutionAction { + + private CachedResultSet resultSet; + private boolean isOrdered; + + public QueryAction(String query, ResultSet resultSet, boolean isOrdered) throws SQLException { + super(query); + this.resultSet = CachedResultSet.fromResultSet(resultSet); + this.isOrdered = isOrdered; + } + + public CachedResultSet getResultSet() { + return this.resultSet; + } +} diff --git a/src/main/java/com/gyrobian/database/UpdateAction.java b/src/main/java/com/gyrobian/database/UpdateAction.java new file mode 100644 index 0000000..fd0d1af --- /dev/null +++ b/src/main/java/com/gyrobian/database/UpdateAction.java @@ -0,0 +1,18 @@ +package com.gyrobian.database; + +/** + * Represents an action in which the schema or data was updated and no result set was returned. + */ +public class UpdateAction extends ExecutionAction { + + private int rowsAffected; + + public UpdateAction(String query, int rowsAffected) { + super(query); + this.rowsAffected = rowsAffected; + } + + public int getRowsAffected() { + return this.rowsAffected; + } +} diff --git a/src/main/java/com/gyrobian/listener/ScriptExecutionListener.java b/src/main/java/com/gyrobian/listener/ScriptExecutionListener.java new file mode 100644 index 0000000..ed28228 --- /dev/null +++ b/src/main/java/com/gyrobian/listener/ScriptExecutionListener.java @@ -0,0 +1,41 @@ +package com.gyrobian.listener; + +import com.gyrobian.database.DatabaseHelper; +import com.gyrobian.view.ExecutionLogDisplay; + +import javax.swing.text.JTextComponent; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +/** + * Listener that, when triggered by a button press, executes an SQL script from some text component, + * and then renders the resulting execution log in another component. + */ +public class ScriptExecutionListener implements ActionListener { + + private final JTextComponent jdbcUrlInput; + private final JTextComponent scriptContainer; + private final ExecutionLogDisplay executionLogDisplay; + + public ScriptExecutionListener( + JTextComponent jdbcUrlInput, + JTextComponent scriptContainer, + ExecutionLogDisplay executionLogDisplay + ) { + this.jdbcUrlInput = jdbcUrlInput; + this.scriptContainer = scriptContainer; + this.executionLogDisplay = executionLogDisplay; + } + + /** + * Invoked when an action occurs. + * + * @param e the event to be processed + */ + @Override + public void actionPerformed(ActionEvent e) { + String script = this.scriptContainer.getText(); + DatabaseHelper helper = new DatabaseHelper(this.jdbcUrlInput.getText().trim()); + this.executionLogDisplay.displayExecutionLog(helper.executeSQLScript(script)); + } +} diff --git a/src/main/java/com/gyrobian/view/ExecutionLogDisplay.java b/src/main/java/com/gyrobian/view/ExecutionLogDisplay.java new file mode 100644 index 0000000..5c4e6e0 --- /dev/null +++ b/src/main/java/com/gyrobian/view/ExecutionLogDisplay.java @@ -0,0 +1,122 @@ +package com.gyrobian.view; + +import com.gyrobian.database.*; + +import javax.swing.*; +import javax.swing.text.BadLocationException; +import javax.swing.text.Style; +import javax.swing.text.StyleConstants; +import javax.swing.text.StyledDocument; +import java.awt.*; + +/** + * A type of text pane that's built for displaying SQL script execution logs. + */ +public class ExecutionLogDisplay extends JTextPane { + + private Style timestampStyle; + private Style actionSubsectionLabelStyle; + private Style queryStyle; + private Style exceptionStyle; + + private ExecutionLog lastExecutionLogDisplayed; + + public ExecutionLogDisplay() { + this.setEditable(false); + this.initializeStyles(); + } + + public ExecutionLog getLastExecutionLogDisplayed() { + return this.lastExecutionLogDisplayed; + } + + /** + * Displays an execution log which was generated from an SQL script. + * @param log The log to display. + */ + public void displayExecutionLog(ExecutionLog log) { + this.setText(null); + + for (ExecutionAction action : log.getActions()) { + this.appendToDocument(action.getOccurredAt().toString() + ":\n", this.timestampStyle); + this.appendExecutionAction(action); + this.appendToDocument("\n", null); + } + + this.lastExecutionLogDisplayed = log; + } + + /** + * Appends a single action to this display's styled document. + * @param action The action to display. + */ + private void appendExecutionAction(ExecutionAction action) { + if (action.getQuery() != null) { + this.appendToDocument("Executing query:\n", this.actionSubsectionLabelStyle); + this.appendToDocument(action.getQuery() + '\n', this.queryStyle); + } + if (action.getException() != null) { + this.appendToDocument("An exception occurred:\n", this.actionSubsectionLabelStyle); + this.appendToDocument(action.getException().getMessage() + '\n', this.exceptionStyle); + action.getException().printStackTrace(); + } + + if (action instanceof UpdateAction) { + UpdateAction updateAction = (UpdateAction) action; + this.appendToDocument("Schema updated: " + updateAction.getRowsAffected() + " rows affected.\n", this.actionSubsectionLabelStyle); + } else if (action instanceof QueryAction) { + QueryAction queryAction = (QueryAction) action; + CachedResultSet resultSet = queryAction.getResultSet(); + this.appendToDocument("Schema queried: " + resultSet.getRowCount() + " rows returned.\n", this.actionSubsectionLabelStyle); + this.appendToDocument(resultSet.toFormattedTableString(), this.queryStyle); + } + } + + /** + * Initializes some styling that's needed for showing the various things that happen. + */ + private void initializeStyles() { + this.timestampStyle = this.addStyle("timestamp", null); + StyleConstants.setForeground(timestampStyle, Color.GRAY); + StyleConstants.setItalic(timestampStyle, true); + + this.actionSubsectionLabelStyle = this.addStyle("subsection_label", null); + StyleConstants.setBold(this.actionSubsectionLabelStyle, true); + + this.queryStyle = this.addStyle("query", null); + StyleConstants.setFontSize(this.queryStyle, 12); + StyleConstants.setFontFamily(this.queryStyle, "monospaced"); + StyleConstants.setLeftIndent(this.queryStyle, 8.0f); + + this.exceptionStyle = this.addStyle("exception", null); + StyleConstants.setForeground(this.exceptionStyle, Color.red); + StyleConstants.setBold(this.exceptionStyle, true); + } + + /** + * Appends a styled string to the document. + * @param str The string to append. + * @param style The style to use for the string. + */ + private void appendToDocument(String str, Style style) { + StyledDocument document = this.getStyledDocument(); + try { + document.insertString(document.getLength(), str, style); + } catch (BadLocationException e) { + e.printStackTrace(); + } + } + + /** + * Overrides the behavior of JTextPane so that any time this display's text disappears, we wipe + * any memory of a previous execution log. + * @param t The string to set the text to. + */ + @Override + public void setText(String t) { + super.setText(t); + if (t == null) { + this.lastExecutionLogDisplayed = null; + } + } +} diff --git a/src/main/java/com/gyrobian/view/Window.form b/src/main/java/com/gyrobian/view/Window.form index 75da2c0..b1208b0 100644 --- a/src/main/java/com/gyrobian/view/Window.form +++ b/src/main/java/com/gyrobian/view/Window.form @@ -3,7 +3,7 @@ - + @@ -221,9 +221,8 @@ - + - @@ -246,9 +245,8 @@ - + - @@ -256,7 +254,7 @@ - + @@ -404,6 +402,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/com/gyrobian/view/Window.java b/src/main/java/com/gyrobian/view/Window.java index e557c4e..aaddfcf 100644 --- a/src/main/java/com/gyrobian/view/Window.java +++ b/src/main/java/com/gyrobian/view/Window.java @@ -2,9 +2,11 @@ package com.gyrobian.view; import com.gyrobian.listener.ClearTextComponentListener; import com.gyrobian.listener.LoadTextComponentFromFileListener; +import com.gyrobian.listener.ScriptExecutionListener; import javax.swing.*; import java.awt.*; +import java.awt.event.ActionListener; /** * The window that's used for displaying the application. @@ -29,8 +31,8 @@ public class Window extends JFrame { private JLabel outputPanelTitle; private JPanel templateOutputPanel; private JPanel testingOutputPanel; - private JTextPane templateOutputTextPane; - private JTextPane testingOutputTextPane; + private ExecutionLogDisplay templateOutputTextPane; + private ExecutionLogDisplay testingOutputTextPane; private JLabel assessmentPanelTitle; private JTextPane assessmentTextPane; private JPanel mainControlPanel; @@ -41,6 +43,8 @@ public class Window extends JFrame { private JPanel scriptExecutionPanel; private JTextField jdbcUrlInput; private JCheckBox enableForeignKeysCheckbox; + private JButton clearExecutionOutputsButton; + private JButton compareExecutionsButton; public Window() { super("SQL-Assesser-2"); @@ -67,8 +71,22 @@ public class Window extends JFrame { private void initializeEventListeners() { this.clearTemplateButton.addActionListener(new ClearTextComponentListener(this.templateTextArea)); this.clearTestingButton.addActionListener(new ClearTextComponentListener(this.testingTextArea)); + this.clearExecutionOutputsButton.addActionListener(new ClearTextComponentListener(this.templateOutputTextPane)); + this.clearExecutionOutputsButton.addActionListener(new ClearTextComponentListener(this.testingOutputTextPane)); this.loadTemplateFromFileButton.addActionListener(new LoadTextComponentFromFileListener(this.templateTextArea)); this.loadTestingFromFileButton.addActionListener(new LoadTextComponentFromFileListener(this.testingTextArea)); + + ActionListener executeTemplateListener = new ScriptExecutionListener(this.jdbcUrlInput, this.templateTextArea, this.templateOutputTextPane); + ActionListener executeTestingListener = new ScriptExecutionListener(this.jdbcUrlInput, this.testingTextArea, this.testingOutputTextPane); + this.executeTemplateButton.addActionListener(executeTemplateListener); + this.executeTestingButton.addActionListener(executeTestingListener); + this.executeBothButton.addActionListener(executeTemplateListener); + this.executeBothButton.addActionListener(executeTestingListener); + } + + protected void createUIComponents() { + this.templateOutputTextPane = new ExecutionLogDisplay(); + this.testingOutputTextPane = new ExecutionLogDisplay(); } }