blob: 7bd42c6f452c7ce3c7f6ac00fd6227989d6d0dff [file] [log] [blame]
/*
* Copyright 2006-2018 The Apache Software Foundation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.jdo.exectck;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* Utility class to load SQL files via JDBC.
*
* The class expects a comment on a single line having a connect statement
* defining url, user and password to connect to the database e.g.
* -- connect 'jdbc:derby:jdotckdb;create=true' user 'tckuser' password 'tckuser';
*/
public class SQLFileLoader {
// the reader for the sql file
private Reader reader;
// holds the next character that is just read from the reader
private int nextChar;
// holds the current SQL statement
private StringBuilder currentStmt = new StringBuilder();
// flag indicating whether a SQL string literal is processed
private boolean scanStringLiteral = false;
// the list of SQL statements
private List<String> statements = new ArrayList<>();
// the JDBC connection url
private String connect;
// the JDBC connection user
private String user;
// the JDBC connection password
private String password;
/**
* Constructor.
* Reads the SQL statements from the specified file.
* The internal state of the class is updated with the contents of the file
* and may be accessed using the getter methods (see below).
* @param filename the name of the sql to be loaded
* @throws IOException If an I/O error occurs
*/
public SQLFileLoader(String filename) throws IOException {
try (Reader r = new BufferedReader(new FileReader(filename))) {
// init reader instance variable
this.reader = r;
// initialize nextChar with the first character from the stream
this.nextChar = this.reader.read();
int currentChar = next();
// read characters from the stream
while(currentChar != -1) {
accept(currentChar);
currentChar = next();
}
this.reader = null;
}
}
/**
* Returns the list of SQL statements processed by this SQLFileLoader.
* @return list of SQL statements
*/
public List<String> getStatements() {
return this.statements;
}
/**
* Returns the JDBC connection URL.
* @return the JDBC connection URL
*/
public String getConnect() {
return this.connect;
}
/**
* Returns the JDBC connection user.
* @return the JDBC connection user.
*/
public String getUser() {
return this.user;
}
/**
* Returns the JDBC connection password.
* @return the JDBC connection password
*/
public String getPassword() {
return this.password;
}
/**
* Checks the specified character whether it terminates a SQL Statement.
* If so the current SQL statement is added to the list of processed SQL statements
* and a new SQL statement is initialized. Otherwise the method appends the specified
* character to the current SQL statement.
* @param value character to be checked
*/
private void accept(int value) {
if (value == ';' && !this.scanStringLiteral) {
// found statement end
statements.add(currentStmt.toString());
currentStmt = new StringBuilder();
} else {
currentStmt.append((char) value);
}
}
/**
* Returns the next character from the input that is not part of a comment.
* That means single line comments and multi line comments are skipped.
* The method is able to handle SQL String literals.
* @return the next non comment character
* @throws IOException If an I/O error occurs
*/
private int next() throws IOException {
int result = this.nextChar;
switch (this.nextChar) {
case '\'' :
this.scanStringLiteral = !this.scanStringLiteral;
this.nextChar = this.reader.read();
break;
case '/' :
if (!this.scanStringLiteral) {
int next = this.reader.read();
if (next == '*') {
this.nextChar = this.reader.read();
skipToEndOfMLComment();
skipWhitespace();
return next();
} else {
this.nextChar = next;
}
} else {
this.nextChar = this.reader.read();
}
break;
case '-':
if (!this.scanStringLiteral) {
int next = this.reader.read();
if (next == '-') {
this.nextChar = reader.read();
handleSingleLineComment();
skipWhitespace();
return next();
} else {
this.nextChar = next;
}
} else {
this.nextChar = this.reader.read();
}
break;
case ' ' :
case '\t' :
case '\n' :
case '\r' :
if (!this.scanStringLiteral) {
skipWhitespace();
result = ' ';
} else {
this.nextChar = this.reader.read();
}
break;
default :
this.nextChar = this.reader.read();
break;
}
return result;
}
/**
* Skips a single line comment.
* That means any character from -- to the end of the line are akipped.
* @throws IOException If an I/O error occurs
*/
private void handleSingleLineComment() throws IOException {
Optional<String> optConnect = readIdentFollowedByLiteral("connect");
optConnect.ifPresent(x -> this.connect = x);
Optional<String> optUser = readIdentFollowedByLiteral("user");
optUser.ifPresent(x -> this.user = x);
Optional<String> optPassword = readIdentFollowedByLiteral("password");
optPassword.ifPresent(x -> this.password = x);
skipToEOL();
}
/**
* Tries to read an Java identifier with the expected name followed by a SQL string literal.
* @param expected the expected Java identifier
* @return an Optional with the text of SQL String literal if present; otherwise an empty Optional
* @throws IOException If an I/O error occurs
*/
private Optional<String> readIdentFollowedByLiteral(String expected) throws IOException {
Optional<String> optIdent = readIdentifier();
if (optIdent.isPresent()) {
String ident = optIdent.get();
if (ident.equalsIgnoreCase(expected)) {
Optional<String> optLiteral = readSQLStringLiteral();
if (optLiteral.isPresent()) {
return optLiteral;
}
}
}
return Optional.empty();
}
/**
* Tries to read a Java identifier.
* @return an Optional with the text of the identifier if present; otherwise an empty Optional
* @throws IOException If an I/O error occurs
*/
private Optional<String> readIdentifier() throws IOException {
skipBlanksAndTabs();
if (Character.isJavaIdentifierStart(this.nextChar)) {
StringBuilder ident = new StringBuilder();
ident.append((char)this.nextChar);
this.nextChar = this.reader.read();
while (Character.isJavaIdentifierPart(this.nextChar)) {
ident.append((char)this.nextChar);
this.nextChar = reader.read();
}
return Optional.of(ident.toString());
}
return Optional.empty();
}
/**
* Tries to read a SQL String literal.
* @return an Optional with the text of the SQL String literal if present; otherwise an empty Optional
* @throws IOException If an I/O error occurs
*/
private Optional<String> readSQLStringLiteral() throws IOException {
skipBlanksAndTabs();
if (this.nextChar == '\'') {
StringBuilder literal = new StringBuilder();
this.nextChar = this.reader.read();
while (this.nextChar != -1) {
// possible end of SQL String literal
if (this.nextChar == '\'') {
this.nextChar = this.reader.read();
// are there two single quotes?
if (this.nextChar == '\'') {
// yes -> append one ' and continue
literal.append('\'');
} else {
// found end of literal
break;
}
} else {
literal.append((char)this.nextChar);
this.nextChar = this.reader.read();
}
}
return Optional.of(literal.toString());
}
return Optional.empty();
}
/**
* Skip any whitespace characters.
* The variable nextChar holds the first non whitespace char.
* @throws IOException If an I/O error occurs
*/
private void skipWhitespace() throws IOException {
while (Character.isWhitespace(this.nextChar)) {
this.nextChar = this.reader.read();
}
}
/**
* Skips any blank and tab characters.
* The variable nextChar holds the first non blank or non tab char.
* @throws IOException If an I/O error occurs
*/
private void skipBlanksAndTabs() throws IOException {
while (this.nextChar == ' ' || this.nextChar == '\t') {
this.nextChar = this.reader.read();
}
}
/**
* Skips to the end of the current line.
* The newline char is consumed.
* The variable nextChar holds the first char of the next line.
* @throws IOException If an I/O error occurs
*/
private void skipToEOL() throws IOException {
while (this.nextChar != -1) {
if (this.nextChar == '\n') {
break;
}
this.nextChar = this.reader.read();
}
this.nextChar = this.reader.read();
}
/**
* Skips to the end of a multi line comment.
* Any characters up to * followed by / are consumed.
* The variable nextChar holds the first char after the comment.
* @throws IOException If an I/O error occurs
*/
private void skipToEndOfMLComment() throws IOException {
while (this.nextChar != -1) {
if (this.nextChar == '*') {
this.nextChar = this.reader.read();
if (this.nextChar == '/') {
break;
}
}
this.nextChar = this.reader.read();
}
this.nextChar = this.reader.read();
}
/**
* main method For testing; prints the internal state to System.out.
* @param args command line arguments
*/
public static void main(String[] args) {
for (String arg : args) {
try {
SQLFileLoader loader = new SQLFileLoader(arg);
List<String> stmts = loader.getStatements();
System.out.println(loader.getConnect());
System.out.println(loader.getUser());
System.out.println(loader.getPassword());
stmts.forEach(System.out::println);
} catch (IOException ex) {
System.err.println(ex);
}
}
}
}