| /* |
| * 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); |
| } |
| } |
| } |
| |
| } |