Skip to content

Commit 4167800

Browse files
committed
Add ScriptRunner utility class
1 parent a97e662 commit 4167800

File tree

11 files changed

+467
-12
lines changed

11 files changed

+467
-12
lines changed

.travis.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
language: java
22
jdk:
33
- openjdk8
4-
- openjdk10
54
- openjdk11
65
- openjdk12
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: DatabaseLib
22
author: Exlll
33

4-
version: 3.1.0
4+
version: 3.2.0
55
main: de.exlll.databaselib.DatabaseLib
66

77
depend: ['ConfigLib']
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: DatabaseLib
22
author: Exlll
33

4-
version: 3.1.0
4+
version: 3.2.0
55
main: de.exlll.databaselib.DatabaseLib
66

77
depends: ['ConfigLib']
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package de.exlll.databaselib.sql.util;
2+
3+
import java.io.IOException;
4+
import java.io.Reader;
5+
import java.util.ArrayList;
6+
import java.util.List;
7+
8+
final class QueryReader {
9+
private final Reader reader;
10+
private final char delimiter;
11+
12+
QueryReader(Reader reader, char delimiter) {
13+
this.reader = reader;
14+
this.delimiter = delimiter;
15+
}
16+
17+
public List<String> readQueries() throws IOException {
18+
List<String> queries = new ArrayList<>();
19+
20+
String query;
21+
while (!(query = readQuery()).isEmpty()) {
22+
queries.add(query.trim());
23+
}
24+
25+
return queries;
26+
}
27+
28+
private String readQuery() throws IOException {
29+
StringBuilder builder = new StringBuilder();
30+
31+
int value;
32+
while ((value = reader.read()) != -1) {
33+
char c = (char) value;
34+
35+
if (c == '"') {
36+
readToClosingQuote(builder, '"');
37+
} else if (c == '\'') {
38+
readToClosingQuote(builder, '\'');
39+
} else if (c == delimiter) {
40+
return builder.toString();
41+
} else {
42+
builder.append((c == '\n') ? ' ' : c);
43+
}
44+
}
45+
46+
return builder.toString();
47+
}
48+
49+
private void readToClosingQuote(StringBuilder builder, char quoteChar)
50+
throws IOException {
51+
builder.append(quoteChar);
52+
53+
int value;
54+
boolean escaped = false;
55+
while ((value = reader.read()) != -1) {
56+
char c = (char) value;
57+
58+
if (c == quoteChar && !escaped) {
59+
builder.append(quoteChar);
60+
return;
61+
}
62+
63+
escaped = (c == '\\' && !escaped);
64+
builder.append(c);
65+
}
66+
}
67+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package de.exlll.databaselib.sql.util;
2+
3+
import java.io.IOException;
4+
import java.io.Reader;
5+
import java.sql.Connection;
6+
import java.sql.SQLException;
7+
import java.sql.Statement;
8+
import java.util.Collections;
9+
import java.util.List;
10+
import java.util.Map;
11+
import java.util.Objects;
12+
import java.util.logging.Logger;
13+
14+
/**
15+
* A {@code ScriptRunner} is a utility class to execute SQL scripts.
16+
* <p>
17+
* An SQL script is a file that contains SQL queries delimited by ';' (semicolon).
18+
* You can create a {@code ScriptRunner} by passing an instance of a {@link Reader}
19+
* and a {@link Connection} to its constructor. A {@code ScriptRunner} has the ability
20+
* to replace any part of a query prior to executing it.
21+
*/
22+
public final class ScriptRunner {
23+
private static final Logger queryLogger = Logger.getLogger(
24+
ScriptRunner.class.getName()
25+
);
26+
private final QueryReader queryReader;
27+
private final Connection connection;
28+
private Map<String, ?> replacements = Collections.emptyMap();
29+
private boolean logQueries;
30+
31+
/**
32+
* Creates a new {@code ScriptRunner} that executes queries read from the
33+
* given {@code Reader} using the given {@code Connection}.
34+
*
35+
* @param reader {@code Reader} queries are read from
36+
* @param connection {@code Connection} used to execute queries
37+
* @throws NullPointerException if any argument is null
38+
*/
39+
public ScriptRunner(Reader reader, Connection connection) {
40+
this.queryReader = new QueryReader(
41+
Objects.requireNonNull(reader), ';'
42+
);
43+
this.connection = Objects.requireNonNull(connection);
44+
}
45+
46+
/**
47+
* Executes all queries from the given {@code Reader}.
48+
*
49+
* @throws IOException if an I/O error occurs while reading from the {@code Reader}
50+
* @throws SQLException if a database access error occurred or
51+
* if at least on of the queries failed to execute
52+
*/
53+
public void runScript() throws IOException, SQLException {
54+
List<String> queries = queryReader.readQueries();
55+
try (Statement stmt = connection.createStatement()) {
56+
for (String query : queries) {
57+
executeQuery(stmt, query);
58+
}
59+
}
60+
}
61+
62+
private void executeQuery(Statement stmt, String query) throws SQLException {
63+
query = preProcess(query);
64+
if (logQueries) {
65+
queryLogger.info(query);
66+
}
67+
stmt.execute(query);
68+
}
69+
70+
private String preProcess(String query) {
71+
for (Map.Entry<String, ?> entry : replacements.entrySet()) {
72+
String key = entry.getKey();
73+
String replacement = (entry.getValue() == null)
74+
? "null"
75+
: entry.getValue().toString();
76+
query = query.replace(key, replacement);
77+
}
78+
return query;
79+
}
80+
81+
/**
82+
* Sets the query replacements. Defaults to an empty map.
83+
* <p>
84+
* Before a query is executed, all parts of the query that
85+
* match a key of the map are replaced with the value to
86+
* which the key is mapped.
87+
*
88+
* @param replacements the query replacements
89+
* @return this {@code ScriptRunner}
90+
* @throws NullPointerException if {@code replacements} or any of its keys is null
91+
*/
92+
public ScriptRunner setReplacements(Map<String, ?> replacements) {
93+
for (String key : replacements.keySet()) {
94+
Objects.requireNonNull(key, "Map must not contain null keys.");
95+
}
96+
this.replacements = replacements;
97+
return this;
98+
}
99+
100+
/**
101+
* Enables or disables query logging. Default value is false.
102+
* Each query is logged right before it is executed.
103+
*
104+
* @param logQueries true if queries should be logged, false otherwise
105+
* @return this {@code ScriptRunner}
106+
*/
107+
public ScriptRunner setLogQueries(boolean logQueries) {
108+
this.logQueries = logQueries;
109+
return this;
110+
}
111+
}

DatabaseLib-Core/src/test/java/de/exlll/databaselib/sql/DummyConnection.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,26 @@
11
package de.exlll.databaselib.sql;
22

33
import java.sql.*;
4+
import java.util.ArrayList;
5+
import java.util.List;
46
import java.util.Map;
57
import java.util.Properties;
68
import java.util.concurrent.Executor;
79

810
public class DummyConnection implements Connection {
11+
private DummyStatement lastStatement;
12+
13+
public DummyStatement getLastStatement() {
14+
return lastStatement;
15+
}
16+
917
public static class DummyStatement implements Statement {
18+
private final List<String> executedQueries = new ArrayList<>();
19+
20+
public List<String> getExecutedQueries() {
21+
return executedQueries;
22+
}
23+
1024
@Override
1125
public ResultSet executeQuery(String sql) {
1226
return null;
@@ -79,7 +93,7 @@ public void setCursorName(String name) {
7993

8094
@Override
8195
public boolean execute(String sql) {
82-
return false;
96+
return executedQueries.add(sql);
8397
}
8498

8599
@Override
@@ -231,7 +245,7 @@ public boolean isWrapperFor(Class<?> iface) {
231245

232246
@Override
233247
public Statement createStatement() {
234-
return null;
248+
return this.lastStatement = new DummyStatement();
235249
}
236250

237251
@Override
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package de.exlll.databaselib.sql.util;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.io.IOException;
6+
import java.io.StringReader;
7+
import java.util.ArrayList;
8+
import java.util.Arrays;
9+
import java.util.List;
10+
11+
import static org.hamcrest.Matchers.is;
12+
import static org.junit.Assert.assertThat;
13+
14+
class QueryReaderTest {
15+
public static final String SQL_INPUT = "CREATE TABLE IF NOT EXISTS `test`\n" +
16+
"(\n" +
17+
" `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,\n" +
18+
" `name` VARCHAR(32)\n" +
19+
") DEFAULT CHARACTER SET utf8;\n" +
20+
"\n" +
21+
"SELECT * FROM `test` WHERE `id` = 10;\n" +
22+
"SELECT * FROM `test` WHERE `name` = \";\";\n" +
23+
"SELECT * FROM `test` WHERE `name` = \"\\\"\";\n" +
24+
"SELECT * FROM `test` WHERE `name` = \"\\\\\";\n" +
25+
"SELECT * FROM `test` WHERE `name` = \"\\\\\\\"\";\n" +
26+
"SELECT * FROM `test` WHERE `name` = ';';\n" +
27+
"SELECT * FROM `test` WHERE `name` = '\\'';\n" +
28+
"SELECT * FROM `test` WHERE `name` = '\\\\';\n" +
29+
"SELECT * FROM `test` WHERE `name` = '\\\\\\'';";
30+
private static final String SQL_INPUT_PIPE_DELIM = "CREATE TABLE IF NOT EXISTS `test`\n" +
31+
"(\n" +
32+
" `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,\n" +
33+
" `name` VARCHAR(32)\n" +
34+
") DEFAULT CHARACTER SET utf8|\n" +
35+
"\n" +
36+
"SELECT * FROM `test` WHERE `id` = 10|\n" +
37+
"SELECT * FROM `test` WHERE `name` = \"|\"|\n" +
38+
"SELECT * FROM `test` WHERE `name` = \"\\\"\"|\n" +
39+
"SELECT * FROM `test` WHERE `name` = \"\\\\\"|\n" +
40+
"SELECT * FROM `test` WHERE `name` = \"\\\\\\\"\"|\n" +
41+
"SELECT * FROM `test` WHERE `name` = '|'|\n" +
42+
"SELECT * FROM `test` WHERE `name` = '\\''|\n" +
43+
"SELECT * FROM `test` WHERE `name` = '\\\\'|\n" +
44+
"SELECT * FROM `test` WHERE `name` = '\\\\\\''|";
45+
public static final List<String> QUERIES = Arrays.asList(
46+
"CREATE TABLE IF NOT EXISTS `test` ( " +
47+
" `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, " +
48+
" `name` VARCHAR(32) ) " +
49+
"DEFAULT CHARACTER SET utf8",
50+
"SELECT * FROM `test` WHERE `id` = 10",
51+
"SELECT * FROM `test` WHERE `name` = \";\"",
52+
"SELECT * FROM `test` WHERE `name` = \"\\\"\"",
53+
"SELECT * FROM `test` WHERE `name` = \"\\\\\"",
54+
"SELECT * FROM `test` WHERE `name` = \"\\\\\\\"\"",
55+
"SELECT * FROM `test` WHERE `name` = ';'",
56+
"SELECT * FROM `test` WHERE `name` = '\\''",
57+
"SELECT * FROM `test` WHERE `name` = '\\\\'",
58+
"SELECT * FROM `test` WHERE `name` = '\\\\\\''"
59+
);
60+
61+
62+
@Test
63+
void readQueriesReadsAllQueries() throws IOException {
64+
QueryReader reader = new QueryReader(
65+
new StringReader(SQL_INPUT), ';'
66+
);
67+
assertThat(reader.readQueries(), is(QUERIES));
68+
}
69+
70+
@Test
71+
void readQueriesUsesDelimiter() throws IOException {
72+
QueryReader reader = new QueryReader(
73+
new StringReader(SQL_INPUT_PIPE_DELIM), '|'
74+
);
75+
List<String> queries = new ArrayList<>(QueryReaderTest.QUERIES);
76+
queries.replaceAll(s -> s.replace(';', '|'));
77+
assertThat(reader.readQueries(), is(queries));
78+
}
79+
}

0 commit comments

Comments
 (0)