Skip to content

Commit

Permalink
Add ScriptRunner utility class
Browse files Browse the repository at this point in the history
  • Loading branch information
Exlll committed Jul 4, 2019
1 parent a97e662 commit 4167800
Show file tree
Hide file tree
Showing 11 changed files with 467 additions and 12 deletions.
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
language: java
jdk:
- openjdk8
- openjdk10
- openjdk11
- openjdk12
2 changes: 1 addition & 1 deletion DatabaseLib-Bukkit/src/main/resources/plugin.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: DatabaseLib
author: Exlll

version: 3.1.0
version: 3.2.0
main: de.exlll.databaselib.DatabaseLib

depend: ['ConfigLib']
2 changes: 1 addition & 1 deletion DatabaseLib-Bungee/src/main/resources/plugin.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: DatabaseLib
author: Exlll

version: 3.1.0
version: 3.2.0
main: de.exlll.databaselib.DatabaseLib

depends: ['ConfigLib']
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package de.exlll.databaselib.sql.util;

import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;

final class QueryReader {
private final Reader reader;
private final char delimiter;

QueryReader(Reader reader, char delimiter) {
this.reader = reader;
this.delimiter = delimiter;
}

public List<String> readQueries() throws IOException {
List<String> queries = new ArrayList<>();

String query;
while (!(query = readQuery()).isEmpty()) {
queries.add(query.trim());
}

return queries;
}

private String readQuery() throws IOException {
StringBuilder builder = new StringBuilder();

int value;
while ((value = reader.read()) != -1) {
char c = (char) value;

if (c == '"') {
readToClosingQuote(builder, '"');
} else if (c == '\'') {
readToClosingQuote(builder, '\'');
} else if (c == delimiter) {
return builder.toString();
} else {
builder.append((c == '\n') ? ' ' : c);
}
}

return builder.toString();
}

private void readToClosingQuote(StringBuilder builder, char quoteChar)
throws IOException {
builder.append(quoteChar);

int value;
boolean escaped = false;
while ((value = reader.read()) != -1) {
char c = (char) value;

if (c == quoteChar && !escaped) {
builder.append(quoteChar);
return;
}

escaped = (c == '\\' && !escaped);
builder.append(c);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package de.exlll.databaselib.sql.util;

import java.io.IOException;
import java.io.Reader;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.logging.Logger;

/**
* A {@code ScriptRunner} is a utility class to execute SQL scripts.
* <p>
* An SQL script is a file that contains SQL queries delimited by ';' (semicolon).
* You can create a {@code ScriptRunner} by passing an instance of a {@link Reader}
* and a {@link Connection} to its constructor. A {@code ScriptRunner} has the ability
* to replace any part of a query prior to executing it.
*/
public final class ScriptRunner {
private static final Logger queryLogger = Logger.getLogger(
ScriptRunner.class.getName()
);
private final QueryReader queryReader;
private final Connection connection;
private Map<String, ?> replacements = Collections.emptyMap();
private boolean logQueries;

/**
* Creates a new {@code ScriptRunner} that executes queries read from the
* given {@code Reader} using the given {@code Connection}.
*
* @param reader {@code Reader} queries are read from
* @param connection {@code Connection} used to execute queries
* @throws NullPointerException if any argument is null
*/
public ScriptRunner(Reader reader, Connection connection) {
this.queryReader = new QueryReader(
Objects.requireNonNull(reader), ';'
);
this.connection = Objects.requireNonNull(connection);
}

/**
* Executes all queries from the given {@code Reader}.
*
* @throws IOException if an I/O error occurs while reading from the {@code Reader}
* @throws SQLException if a database access error occurred or
* if at least on of the queries failed to execute
*/
public void runScript() throws IOException, SQLException {
List<String> queries = queryReader.readQueries();
try (Statement stmt = connection.createStatement()) {
for (String query : queries) {
executeQuery(stmt, query);
}
}
}

private void executeQuery(Statement stmt, String query) throws SQLException {
query = preProcess(query);
if (logQueries) {
queryLogger.info(query);
}
stmt.execute(query);
}

private String preProcess(String query) {
for (Map.Entry<String, ?> entry : replacements.entrySet()) {
String key = entry.getKey();
String replacement = (entry.getValue() == null)
? "null"
: entry.getValue().toString();
query = query.replace(key, replacement);
}
return query;
}

/**
* Sets the query replacements. Defaults to an empty map.
* <p>
* Before a query is executed, all parts of the query that
* match a key of the map are replaced with the value to
* which the key is mapped.
*
* @param replacements the query replacements
* @return this {@code ScriptRunner}
* @throws NullPointerException if {@code replacements} or any of its keys is null
*/
public ScriptRunner setReplacements(Map<String, ?> replacements) {
for (String key : replacements.keySet()) {
Objects.requireNonNull(key, "Map must not contain null keys.");
}
this.replacements = replacements;
return this;
}

/**
* Enables or disables query logging. Default value is false.
* Each query is logged right before it is executed.
*
* @param logQueries true if queries should be logged, false otherwise
* @return this {@code ScriptRunner}
*/
public ScriptRunner setLogQueries(boolean logQueries) {
this.logQueries = logQueries;
return this;
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
package de.exlll.databaselib.sql;

import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executor;

public class DummyConnection implements Connection {
private DummyStatement lastStatement;

public DummyStatement getLastStatement() {
return lastStatement;
}

public static class DummyStatement implements Statement {
private final List<String> executedQueries = new ArrayList<>();

public List<String> getExecutedQueries() {
return executedQueries;
}

@Override
public ResultSet executeQuery(String sql) {
return null;
Expand Down Expand Up @@ -79,7 +93,7 @@ public void setCursorName(String name) {

@Override
public boolean execute(String sql) {
return false;
return executedQueries.add(sql);
}

@Override
Expand Down Expand Up @@ -231,7 +245,7 @@ public boolean isWrapperFor(Class<?> iface) {

@Override
public Statement createStatement() {
return null;
return this.lastStatement = new DummyStatement();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package de.exlll.databaselib.sql.util;

import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;

class QueryReaderTest {
public static final String SQL_INPUT = "CREATE TABLE IF NOT EXISTS `test`\n" +
"(\n" +
" `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,\n" +
" `name` VARCHAR(32)\n" +
") DEFAULT CHARACTER SET utf8;\n" +
"\n" +
"SELECT * FROM `test` WHERE `id` = 10;\n" +
"SELECT * FROM `test` WHERE `name` = \";\";\n" +
"SELECT * FROM `test` WHERE `name` = \"\\\"\";\n" +
"SELECT * FROM `test` WHERE `name` = \"\\\\\";\n" +
"SELECT * FROM `test` WHERE `name` = \"\\\\\\\"\";\n" +
"SELECT * FROM `test` WHERE `name` = ';';\n" +
"SELECT * FROM `test` WHERE `name` = '\\'';\n" +
"SELECT * FROM `test` WHERE `name` = '\\\\';\n" +
"SELECT * FROM `test` WHERE `name` = '\\\\\\'';";
private static final String SQL_INPUT_PIPE_DELIM = "CREATE TABLE IF NOT EXISTS `test`\n" +
"(\n" +
" `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,\n" +
" `name` VARCHAR(32)\n" +
") DEFAULT CHARACTER SET utf8|\n" +
"\n" +
"SELECT * FROM `test` WHERE `id` = 10|\n" +
"SELECT * FROM `test` WHERE `name` = \"|\"|\n" +
"SELECT * FROM `test` WHERE `name` = \"\\\"\"|\n" +
"SELECT * FROM `test` WHERE `name` = \"\\\\\"|\n" +
"SELECT * FROM `test` WHERE `name` = \"\\\\\\\"\"|\n" +
"SELECT * FROM `test` WHERE `name` = '|'|\n" +
"SELECT * FROM `test` WHERE `name` = '\\''|\n" +
"SELECT * FROM `test` WHERE `name` = '\\\\'|\n" +
"SELECT * FROM `test` WHERE `name` = '\\\\\\''|";
public static final List<String> QUERIES = Arrays.asList(
"CREATE TABLE IF NOT EXISTS `test` ( " +
" `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, " +
" `name` VARCHAR(32) ) " +
"DEFAULT CHARACTER SET utf8",
"SELECT * FROM `test` WHERE `id` = 10",
"SELECT * FROM `test` WHERE `name` = \";\"",
"SELECT * FROM `test` WHERE `name` = \"\\\"\"",
"SELECT * FROM `test` WHERE `name` = \"\\\\\"",
"SELECT * FROM `test` WHERE `name` = \"\\\\\\\"\"",
"SELECT * FROM `test` WHERE `name` = ';'",
"SELECT * FROM `test` WHERE `name` = '\\''",
"SELECT * FROM `test` WHERE `name` = '\\\\'",
"SELECT * FROM `test` WHERE `name` = '\\\\\\''"
);


@Test
void readQueriesReadsAllQueries() throws IOException {
QueryReader reader = new QueryReader(
new StringReader(SQL_INPUT), ';'
);
assertThat(reader.readQueries(), is(QUERIES));
}

@Test
void readQueriesUsesDelimiter() throws IOException {
QueryReader reader = new QueryReader(
new StringReader(SQL_INPUT_PIPE_DELIM), '|'
);
List<String> queries = new ArrayList<>(QueryReaderTest.QUERIES);
queries.replaceAll(s -> s.replace(';', '|'));
assertThat(reader.readQueries(), is(queries));
}
}
Loading

0 comments on commit 4167800

Please sign in to comment.