diff --git a/resources/example_test.sql b/resources/example_test.sql new file mode 100644 index 0000000..7788c59 --- /dev/null +++ b/resources/example_test.sql @@ -0,0 +1 @@ +select * from primes; \ No newline at end of file diff --git a/resources/initialization.sql b/resources/initialization.sql new file mode 100644 index 0000000..b547c0a --- /dev/null +++ b/resources/initialization.sql @@ -0,0 +1,7 @@ +CREATE TABLE primes ( + id SERIAL NOT NULL, + value INT NOT NULL +); + +INSERT INTO primes(value) VALUES +(2), (3), (5), (7), (11); \ No newline at end of file diff --git a/resources/template.sql b/resources/template.sql new file mode 100644 index 0000000..7788c59 --- /dev/null +++ b/resources/template.sql @@ -0,0 +1 @@ +select * from primes; \ No newline at end of file diff --git a/src/nl/andrewlalis/DatabaseHelper.java b/src/nl/andrewlalis/DatabaseHelper.java new file mode 100644 index 0000000..d1b759a --- /dev/null +++ b/src/nl/andrewlalis/DatabaseHelper.java @@ -0,0 +1,123 @@ +package nl.andrewlalis; + +import nl.andrewlalis.log.ExecutionLog; +import nl.andrewlalis.log.QueryAction; +import nl.andrewlalis.log.UpdateAction; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; + +public class DatabaseHelper { + + private String host; + private int port; + private String user; + private String password; + private Window window; + + private ExecutionLog executionLog; + + public DatabaseHelper(String host, int port, String user, String password, Window window) { + this.host = host; + this.port = port; + this.user = user; + this.password = password; + this.window = window; + + this.executionLog = new ExecutionLog(); + } + + /** + * Executes possibly many queries which are contained in one string. + * @param database The database name to connect to, or an empty string to connect to the user's database. + * @param queriesString The string of queries. + */ + public void executeQueries(String database, String queriesString) { + String url = String.format( + "jdbc:postgresql://%s:%4d/%s?user=%s&password=%s", + host, + port, + database, + user, + password); + try { + Connection conn = DriverManager.getConnection(url); + + if (!conn.isValid(1000)) { + throw new SQLException("Invalid connection."); + } + + List queries = splitQueries(queriesString); + + Statement st = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); + + for (String query : queries) { + try { + executeQuery(query, st); + } catch (SQLException e) { + window.appendOutput("Exception while executing statement: " + e.getMessage()); + } + } + + conn.close(); + } catch (SQLException e) { + int previousChannel = window.getOutputChannel(); + window.setOutputChannel(Window.OUTPUT_GENERAL); + window.appendOutput("Unexpected SQL Exception occurred. URL:\n" + url + "\n\tException: " + e.getMessage() + "\n\tSQL State: " + e.getSQLState()); + window.setOutputChannel(previousChannel); + } + } + + /** + * 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. + */ + private void executeQuery(String query, Statement statement) throws SQLException { + if (isSQLStatementQuery(query)) { + // A result set is expected. + window.appendOutput("Executing query:\n" + query); + + QueryAction action = new QueryAction(statement.executeQuery(query)); + window.appendOutput(action.toString()); + this.executionLog.recordAction(action); + } else { + // A result set is not expected. + window.appendOutput("Executing update:\n" + query); + UpdateAction action = new UpdateAction(statement.executeUpdate(query), query); + window.appendOutput(action.toString()); + this.executionLog.recordAction(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.startsWith("SELECT"); + } + +} diff --git a/src/nl/andrewlalis/Main.java b/src/nl/andrewlalis/Main.java new file mode 100644 index 0000000..fe7077e --- /dev/null +++ b/src/nl/andrewlalis/Main.java @@ -0,0 +1,18 @@ +package nl.andrewlalis; + +import javax.swing.*; + +public class Main { + + public static final String APPLICATION_NAME = "SQL-Assesser"; + + public static void main(String[] args) { + Window window = new Window(APPLICATION_NAME); + window.pack(); + window.setSize(1000, 800); + window.setVisible(true); + window.setLocationRelativeTo(null); + window.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + } + +} diff --git a/src/nl/andrewlalis/Window.form b/src/nl/andrewlalis/Window.form new file mode 100644 index 0000000..f693ee9 --- /dev/null +++ b/src/nl/andrewlalis/Window.form @@ -0,0 +1,381 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/nl/andrewlalis/Window.java b/src/nl/andrewlalis/Window.java new file mode 100644 index 0000000..bf44b95 --- /dev/null +++ b/src/nl/andrewlalis/Window.java @@ -0,0 +1,150 @@ +package nl.andrewlalis; + +import javax.swing.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +public class Window extends JFrame { + private JPanel mainPanel; + private JPanel inputPanel; + private JPanel outputPanel; + private JTextArea outputTextArea; + private JButton executeButton; + private JTextArea templateTextArea; + private JTextArea testingTextArea; + private JTextField hostTextField; + private JTextField portTextField; + private JTextField userTextField; + private JTextField passwordTextField; + private JScrollPane outputScrollPane; + private JTextArea testingOutputTextArea; + private JTextArea templateOutputTextArea; + private JTextArea initializationTextArea; + private JPanel initializationPanel; + private JButton clearOutputButton; + private JButton loadTemplateFromFileButton; + private JButton loadTestingFromFileButton; + private JButton loadInitializationFromFileButton; + + public static final int OUTPUT_GENERAL = 0; + public static final int OUTPUT_TEMPLATE = 1; + public static final int OUTPUT_TESTING = 2; + + public static final String DB_TEMPLATE = "sql_assess_template"; + public static final String DB_TESTING = "sql_assess_testing"; + + private int outputChannel; + private int outputIndent; + + public Window(String applicationName) { + super(applicationName); + + this.setOutputChannel(OUTPUT_GENERAL); + + this.setContentPane(mainPanel); + + executeButton.addActionListener(actionEvent -> { + this.executeSQL(); + }); + + clearOutputButton.addActionListener(actionEvent -> { + this.templateOutputTextArea.setText(null); + this.testingOutputTextArea.setText(null); + this.outputTextArea.setText(null); + }); + + loadInitializationFromFileButton.addActionListener(actionEvent -> { + + }); + } + + /** + * Executes the SQL in the two text areas, and provides output. + */ + private void executeSQL() { + this.setOutputChannel(OUTPUT_GENERAL); + String host = this.hostTextField.getText(); + int port = Integer.parseInt(this.portTextField.getText()); + String user = this.userTextField.getText(); + String password = this.passwordTextField.getText(); + String initialization = this.initializationTextArea.getText(); + + // Run the database code in a separate thread to update the UI quickly. + Thread t = new Thread(() -> { + DatabaseHelper dbHelper = new DatabaseHelper(host, port, user, password, this); + + // Setup both databases. + this.appendOutput("Dropping old databases and re-creating them..."); + this.indentOutput(); + String dropDatabases = "DROP DATABASE " + DB_TEMPLATE + "; " + + "DROP DATABASE " + DB_TESTING + ";"; + String createDatabases = "CREATE DATABASE " + DB_TEMPLATE + "; " + + "CREATE DATABASE " + DB_TESTING + ";"; + dbHelper.executeQueries("", dropDatabases); + dbHelper.executeQueries("", createDatabases); + this.unindentOutput(); + + // Run initialization script on each database. + this.appendOutput("Running initialization SQL on databases..."); + this.indentOutput(); + dbHelper.executeQueries(DB_TEMPLATE, initialization); + dbHelper.executeQueries(DB_TESTING, initialization); + this.unindentOutput(); + + // Template-specific output. + this.setOutputChannel(OUTPUT_TEMPLATE); + dbHelper.executeQueries(DB_TEMPLATE, this.templateTextArea.getText()); + + // Testing-specific output. + this.setOutputChannel(OUTPUT_TESTING); + dbHelper.executeQueries(DB_TESTING, this.testingTextArea.getText()); + }); + t.start(); + } + + int getOutputChannel() { + return this.outputChannel; + } + + void setOutputChannel(int channel) { + this.outputChannel = channel; + } + + void indentOutput() { + this.outputIndent++; + } + + void unindentOutput() { + this.outputIndent--; + } + + /** + * Adds some text to the current output channel, followed by a new line. + * @param text The text to append. + */ + void appendOutput(String text) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < this.outputIndent; i++) { + sb.append('\t'); + } + String tabs = sb.toString(); + StringBuilder resultSb = new StringBuilder(); + for (String line : text.split("\n")) { + resultSb.append(tabs).append(line).append('\n'); + } + String result = resultSb.toString(); + switch (this.outputChannel) { + case OUTPUT_GENERAL: + this.outputTextArea.append(result); + break; + + case OUTPUT_TEMPLATE: + this.templateOutputTextArea.append(result); + break; + + case OUTPUT_TESTING: + this.testingOutputTextArea.append(result); + break; + } + } +} diff --git a/src/nl/andrewlalis/log/ExecutionAction.java b/src/nl/andrewlalis/log/ExecutionAction.java new file mode 100644 index 0000000..7cbe2dd --- /dev/null +++ b/src/nl/andrewlalis/log/ExecutionAction.java @@ -0,0 +1,7 @@ +package nl.andrewlalis.log; + +/** + * Represents an action performed on a database. + */ +public abstract class ExecutionAction { +} diff --git a/src/nl/andrewlalis/log/ExecutionLog.java b/src/nl/andrewlalis/log/ExecutionLog.java new file mode 100644 index 0000000..db835f0 --- /dev/null +++ b/src/nl/andrewlalis/log/ExecutionLog.java @@ -0,0 +1,47 @@ +package nl.andrewlalis.log; + +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; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof ExecutionLog)) { + return false; + } + + ExecutionLog otherLog = (ExecutionLog) other; + + if (otherLog.getActions().size() != this.getActions().size()) { + return false; + } + + List otherLogActions = otherLog.getActions(); + + for (int i = 0; i < this.getActions().size(); i++) { + if (!this.getActions().get(i).equals(otherLogActions.get(i))) { + return false; + } + } + + return true; + } +} diff --git a/src/nl/andrewlalis/log/QueryAction.java b/src/nl/andrewlalis/log/QueryAction.java new file mode 100644 index 0000000..970cbe1 --- /dev/null +++ b/src/nl/andrewlalis/log/QueryAction.java @@ -0,0 +1,106 @@ +package nl.andrewlalis.log; + +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * An action in which a query result set is returned. + */ +public class QueryAction extends ExecutionAction { + + private String[] columns; + private String[][] values; + + public QueryAction(ResultSet resultSet) throws SQLException { + // Read the columns into this object's memory. + ResultSetMetaData metaData = resultSet.getMetaData(); + this.columns = new String[metaData.getColumnCount()]; + for (int i = 0; i < metaData.getColumnCount(); i++) { + columns[i] = metaData.getColumnName(i + 1); + } + + resultSet.absolute(1);// Ensure that this result set cursor is at the beginning. + + // Read the rows into this object's memory. + List rows = new ArrayList<>(); + while (resultSet.next()) { + String[] row = new String[columns.length]; + for (int i = 0; i < columns.length; i++) { + row[i] = resultSet.getString(i + 1); + } + rows.add(row); + } + this.values = new String[rows.size()][]; + rows.toArray(this.values); + } + + public String[] getColumns() { + return this.columns; + } + + public String[][] getValues() { + return this.values; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof QueryAction)) { + return false; + } + + QueryAction action = (QueryAction) other; + + if (action.getColumns().length != this.columns.length || action.getValues().length != this.values.length) { + return false; + } + + for (int i = 0; i < this.values.length; i++) { + Map thisColumnValues = new HashMap<>(); + Map otherColumnValues = new HashMap<>(); + for (int k = 0; k < this.values[i].length; k++) { + thisColumnValues.put(this.columns[k], this.values[i][k]); + otherColumnValues.put(action.getColumns()[k], action.getValues()[i][k]); + } + for (String column : this.columns) { + if (thisColumnValues.get(column).equals(otherColumnValues.get(column))) { + return false; + } + } + } + + return true; + } + + @Override + public String toString() { + // First build a list of columns. + StringBuilder sb = new StringBuilder("Query Result:\n\tColumns: ("); + for (int i = 0; i < this.columns.length; i++) { + sb.append(this.columns[i]); + if (i < this.columns.length - 1) { + sb.append(", "); + } + } + sb.append(")\n\tValues:\n"); + + // Then build a list of the rows. + for (int i = 0; i < this.values.length; i++) { + sb.append("\t("); + for (int k = 0; k < this.values[i].length; k++) { + sb.append(this.values[i][k]); + if (k < this.values[i].length - 1) { + sb.append(", "); + } + } + sb.append(")\n"); + } + + return sb.toString(); + } + +} diff --git a/src/nl/andrewlalis/log/UpdateAction.java b/src/nl/andrewlalis/log/UpdateAction.java new file mode 100644 index 0000000..0485f36 --- /dev/null +++ b/src/nl/andrewlalis/log/UpdateAction.java @@ -0,0 +1,29 @@ +package nl.andrewlalis.log; + +/** + * 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(int rowsAffected, String statement) { + this.rowsAffected = rowsAffected; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof UpdateAction)) { + return false; + } + + UpdateAction action = (UpdateAction) other; + + return action.rowsAffected == this.rowsAffected; + } + + @Override + public String toString() { + return "Update result:\n\tRows affected: " + this.rowsAffected + "\n"; + } +}