/*
* Copyright 2004-2015 the Seasar Foundation and the Others.
*
* 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.seasar.extension.jdbc.it.util;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import org.seasar.extension.jdbc.it.util.Dialect.SqlBlockContext;
import org.seasar.extension.jdbc.it.util.SqlFileTokenizer.TokenType;
import org.seasar.framework.exception.IORuntimeException;
import org.seasar.framework.log.Logger;
/**
* SQLファイルのリーダです。
*
* @author taedium
*/
public class SqlFileReader {
/** ロガー */
protected static Logger logger = Logger.getLogger(SqlFileReader.class);
/** SQLファイル */
protected File sqlFile;
/** SQLファイルのエンコーディング */
protected String sqlFileEncoding;
/** トークナイザ */
protected SqlFileTokenizer tokenizer;
/** 方言 */
protected Dialect dialect;
/** リーダ */
protected BufferedReader reader;
/** 行番号のカウント */
protected int lineCount;
/** 処理対象のSQLの先頭の行番号 */
protected int lineNumber;
/** ファイルの終端に達した場合{@code true} */
protected boolean endOfFile;
/** 行の終端に達した場合{@code true} */
protected boolean endOfLine = true;
/**
* インスタンスを構築します。
*
* @param sqlFile
* SQLファイル
* @param sqlFileEncoding
* SQLファイルのエンコーディング
* @param tokenizer
* トークナイザ
* @param dialect
* 方言
*/
public SqlFileReader(File sqlFile, String sqlFileEncoding,
SqlFileTokenizer tokenizer, Dialect dialect) {
if (sqlFile == null) {
throw new NullPointerException("sqlFile");
}
if (sqlFileEncoding == null) {
throw new NullPointerException("sqlFileEncoding");
}
if (tokenizer == null) {
throw new NullPointerException("tokenizer");
}
if (dialect == null) {
throw new NullPointerException("dialect");
}
this.sqlFile = sqlFile;
this.sqlFileEncoding = sqlFileEncoding;
this.tokenizer = tokenizer;
this.dialect = dialect;
}
/**
* SQLステートメントもしくはSQLブロックを読み取ります。
*
* @return ファイルの終端に達していなければSQL、ファイルの終端に達していれば{@code null}
*/
public String readSql() {
if (endOfFile) {
return null;
}
try {
if (reader == null) {
reader = createBufferedReader();
}
SqlBuilder builder = new SqlBuilder();
readLineLoop: for (;;) {
if (endOfLine) {
lineCount++;
tokenizer.addLine(reader.readLine());
builder.notifyLineChanged();
}
for (;;) {
builder.build(tokenizer.nextToken(), tokenizer.getToken());
if (builder.isTokenRequired()) {
continue;
} else if (builder.isLineRequired()) {
continue readLineLoop;
} else if (builder.isCompleted()) {
return builder.getSql();
}
throw new IllegalStateException("builder");
}
}
} catch (IOException e) {
throw new IORuntimeException(e);
}
}
/**
* 処理対象のSQLの先頭の行番号を返します。
* <p>
* 行番号は1から始まります。
* </p>
*
* @return 行番号
*/
public int getLineNumber() {
return lineNumber;
}
/**
* クローズします。
*/
public void close() {
try {
if (reader != null) {
reader.close();
}
} catch (IOException ignore) {
logger.log(ignore);
}
}
/**
* {@link #sqlFile}に対する{@link BufferedReader}を作成します。
*
* @return {@link BufferedReader}
* @throws IOException
*/
protected BufferedReader createBufferedReader() throws IOException {
InputStream is = new FileInputStream(sqlFile);
return new BufferedReader(new InputStreamReader(is, sqlFileEncoding));
}
/**
* SQLのビルダです。
*
* @author taedium
*/
protected class SqlBuilder {
/** 次のトークンが必要な場合{@code true} */
protected boolean tokenRequired;
/** 次の行が必要な場合{@code true} */
protected boolean lineRequired;
/** SQLの組み立てが完了した場合{@code true} */
protected boolean completed;
/** SQLの文字列を保持するバッファ */
protected StringBuilder buf = new StringBuilder(300);
/** SQLのキーワードを管理するリスト */
protected List<String> wordList = new ArrayList<String>();
/** SQLブロックのコンテキスト */
protected SqlBlockContext sqlBlockContext;
/** 行が変更された場合{@code true} */
protected boolean lineChanged;
/**
* インスタンスを構築します
*/
protected SqlBuilder() {
sqlBlockContext = dialect.createSqlBlockContext();
}
/**
* SQL文を組み立てます。
*
* @param tokenType
* トークンのタイプ
* @param token
* トークン
*/
protected void build(TokenType tokenType, String token) {
reset();
if (buf.length() == 0) {
lineNumber = lineCount;
}
switch (tokenType) {
case WORD:
appendWord(token);
case QUOTE:
case OTHER:
appendToken(token);
requireToken();
break;
case END_OF_LINE:
endOfLine = true;
requireLine();
break;
case STATEMENT_DELIMITER:
if (isInSqlBlock()) {
appendToken(token);
requireToken();
} else {
complete();
}
break;
case BLOCK_DELIMITER:
if (isSqlEmpty()) {
requireToken();
} else {
complete();
}
break;
case END_OF_FILE:
endOfFile = true;
complete();
break;
default:
requireToken();
break;
}
}
/**
* リセットします。
*/
protected void reset() {
endOfLine = false;
requireToken();
}
/**
* 次のトークンが必要な場合{@code true}を返します。
*
* @return 次のトークンが必要な場合{@code true}
*/
protected boolean isTokenRequired() {
return tokenRequired;
}
/**
* 次のトークンを要求します。
*
*/
protected void requireToken() {
tokenRequired = true;
lineRequired = false;
completed = false;
}
/**
* 次の行が必要な場合{@code true}を返します。
*
* @return 次の行が必要な場合{@code true}
*/
protected boolean isLineRequired() {
return lineRequired;
}
/**
* 次の行を要求します。
*
*/
protected void requireLine() {
lineRequired = true;
tokenRequired = false;
completed = false;
}
/**
* SQLの組み立てが完了した場合{@code true}を返します。
*
* @return SQLの組み立てが完了した場合{@code true}
*/
protected boolean isCompleted() {
return completed;
}
/**
* SQLの組み立てを完了します。
*
*/
protected void complete() {
completed = true;
tokenRequired = false;
lineRequired = false;
}
/**
* 単語を追加します。
*
* @param word
* 単語
*/
protected void appendWord(String word) {
sqlBlockContext.addKeyword(word);
}
/**
* トークンを追加します。
*
* @param token
* トークン
*/
protected void appendToken(String token) {
appendWhitespaceIfNecessary();
buf.append(token);
}
/**
* 必要ならば空白を追加します。
*/
protected void appendWhitespaceIfNecessary() {
if (!lineChanged) {
return;
}
if (buf.length() > 0) {
char lastChar = buf.charAt(buf.length() - 1);
if (!Character.isWhitespace(lastChar)) {
buf.append(' ');
}
}
lineChanged = false;
}
/**
* 行が変更されたことを通知します。
*/
protected void notifyLineChanged() {
lineChanged = true;
}
/**
* SQLブロックの内側を組み立てている場合{@code true}を返します。
*
* @return SQLブロックの内側を組み立てている場合{@code true}
*/
protected boolean isInSqlBlock() {
return sqlBlockContext.isInSqlBlock();
}
/**
* SQLが空の場合{@code true}を返します。
*
* @return SQLが空の場合{@code true}
*/
protected boolean isSqlEmpty() {
return buf.toString().trim().length() == 0;
}
/**
* SQLを返します。
*
* @return
*/
protected String getSql() {
if (!completed) {
throw new IllegalStateException("completed");
}
String sql = buf.toString().trim();
return endOfFile && sql.length() == 0 ? null : sql;
}
}
}