Added execution of testing and template code.

This commit is contained in:
Andrew Lalis 2020-03-11 00:37:10 +01:00
parent c5d95244b1
commit f2e934620b
10 changed files with 545 additions and 8 deletions

View File

@ -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<Map<String, String>> rows;
private CachedResultSet(String[] columnNames, List<Map<String, String>> rows) {
this.columnNames = columnNames;
this.rows = rows;
}
public int getRowCount() {
return this.rows.size();
}
public String[] getColumnNames() {
return columnNames;
}
public List<Map<String, String>> 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<Map<String, String>> rows = new ArrayList<>();
while (resultSet.next()) {
Map<String, String> 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<String, String> 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<String, String> 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 +
'}';
}
}

View File

@ -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<String> 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<String> splitQueries(String queriesString) {
String[] sections = queriesString.split(";");
List<String> 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");
}
}

View File

@ -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;
}
}

View File

@ -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<ExecutionAction> actions;
public ExecutionLog() {
this.actions = new ArrayList<>();
}
public void recordAction(ExecutionAction action) {
this.actions.add(action);
}
public List<ExecutionAction> getActions() {
return this.actions;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

@ -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;
}
}
}

View File

@ -3,7 +3,7 @@
<grid id="27dc6" binding="mainPanel" layout-manager="GridLayoutManager" row-count="3" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="824" height="507"/>
<xy x="20" y="20" width="936" height="507"/>
</constraints>
<properties/>
<border type="none"/>
@ -221,9 +221,8 @@
</properties>
<border type="none"/>
<children>
<component id="1932b" class="javax.swing.JTextPane" binding="templateOutputTextPane">
<component id="bbbf4" class="com.gyrobian.view.ExecutionLogDisplay" binding="templateOutputTextPane" custom-create="true">
<constraints/>
<properties/>
</component>
</children>
</scrollpane>
@ -246,9 +245,8 @@
</properties>
<border type="none"/>
<children>
<component id="99b7f" class="javax.swing.JTextPane" binding="testingOutputTextPane">
<component id="223ac" class="com.gyrobian.view.ExecutionLogDisplay" binding="testingOutputTextPane" custom-create="true">
<constraints/>
<properties/>
</component>
</children>
</scrollpane>
@ -256,7 +254,7 @@
</grid>
</children>
</grid>
<grid id="ab0b0" binding="mainControlPanel" layout-manager="GridLayoutManager" row-count="3" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<grid id="ab0b0" binding="mainControlPanel" layout-manager="GridLayoutManager" row-count="4" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="1" column="1" row-span="2" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
@ -404,6 +402,51 @@
<text value="Execute Testing Script"/>
</properties>
</component>
<component id="1474d" class="javax.swing.JButton" binding="clearExecutionOutputsButton">
<constraints/>
<properties>
<text value="Clear Outputs"/>
</properties>
</component>
</children>
</grid>
</children>
</grid>
<grid id="1be05" layout-manager="GridLayoutManager" row-count="3" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="3" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="bevel-lowered"/>
<children>
<component id="f1e79" class="javax.swing.JLabel">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="0" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<horizontalAlignment value="11"/>
<text value="Assessment"/>
</properties>
</component>
<vspacer id="23b31">
<constraints>
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
<grid id="89db7" layout-manager="FlowLayout" hgap="5" vgap="5" flow-align="1">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="3cdeb" class="javax.swing.JButton" binding="compareExecutionsButton">
<constraints/>
<properties>
<text value="Compare Template and Testing Scripts"/>
</properties>
</component>
</children>
</grid>
</children>

View File

@ -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();
}
}