/*
* Copyright 2004-2010 H2 Group. Multiple-Licensed under the H2 License,
* Version 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html).
* Initial Developer: H2 Group
*/
package org.teiid.odbc;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
/**
* This class can split SQL scripts to single SQL statements.
* Each SQL statement ends with the character ';', however it is ignored
* in comments and quotes.
*/
public class ScriptReader {
private Reader reader;
private StringBuilder builder;
private boolean endOfFile;
private boolean insideRemark;
private boolean blockRemark;
private boolean rewrite;
private int expressionStart=-1;
private int expressionEnd=-1;
/**
* Create a new SQL script reader from the given reader
*
* @param reader the reader
*/
public ScriptReader(Reader reader) {
this.reader = reader;
this.builder = new StringBuilder(1<<13);
}
public ScriptReader(String string) {
this.reader = new StringReader(string);
this.builder = new StringBuilder(string.length());
}
/**
* Close the underlying reader.
*/
public void close() throws IOException{
reader.close();
}
/**
* Read a statement from the reader. This method returns null if the end has
* been reached.
*
* @return the SQL statement or null
*/
public String readStatement() throws IOException{
if (endOfFile) {
return null;
}
while (true) {
String result = readStatementLoop();
if (result != null || endOfFile) {
return result;
}
}
}
private String readStatementLoop() throws IOException {
int c = read();
while (true) {
if (c < 0) {
endOfFile = true;
break;
} else if (c == ';') {
builder.setLength(builder.length()-1);
break;
}
switch (c) {
case '$': {
c = read();
if (c == '$' && (builder.length() < 3 || builder.charAt(builder.length() - 3) <= ' ')) {
// dollar quoted string
while (true) {
c = read();
if (c < 0) {
break;
}
if (c == '$') {
c = read();
if (c < 0) {
break;
}
if (c == '$') {
break;
}
}
}
c = read();
}
break;
}
case '\'':
//TODO: in rewrite mode could handle the E' logic here rather than in the parser
//however the parser now uses E' with like to detect pg specific handling
if (expressionEnd != builder.length() - 1) {
expressionStart = builder.length() - 1;
}
while (true) {
c = read();
if (c < 0 || c == '\'') {
break;
}
}
expressionEnd = builder.length();
c = read();
break;
case '"':
while (true) {
c = read();
if (c < 0) {
break;
}
if (c == '\"') {
break;
}
}
c = read();
break;
case '/': {
c = read();
if (c == '*') {
// block comment
startRemark(false);
while (true) {
c = read();
if (c < 0) {
break;
}
if (c == '*') {
c = read();
if (c < 0) {
break;
}
if (c == '/') {
endRemark();
break;
}
}
}
c = read();
} else if (c == '/') {
// single line comment
startRemark(false);
while (true) {
c = read();
if (c < 0) {
break;
}
if (c == '\r' || c == '\n') {
endRemark();
break;
}
}
c = read();
}
break;
}
case '-': {
c = read();
if (c == '-') {
// single line comment
startRemark(false);
while (true) {
c = read();
if (c < 0) {
break;
}
if (c == '\r' || c == '\n') {
endRemark();
break;
}
}
c = read();
}
break;
}
case ':': {
if (rewrite) {
int start = builder.length();
c = read();
if (c == ':') {
while (true) {
c = read();
if (c < 0 || !Character.isLetterOrDigit(c)) {
String type = builder.substring(start+1, builder.length() - (c<0?0:1));
builder.setLength(start-1);
if (expressionStart != -1 && expressionEnd == start -1) {
//special handling for regclass cast - it won't always work
if ("regclass".equalsIgnoreCase(type)) { //$NON-NLS-1$
builder.insert(expressionStart, "regclass("); //$NON-NLS-1$
builder.append(")"); //$NON-NLS-1$
} else if ("regproc".equalsIgnoreCase(type)) { //$NON-NLS-1$
String name = builder.substring(expressionStart);
if (name.startsWith("'\"") && name.length() > 4) { //$NON-NLS-1$
name = name.substring(2, name.length()-2);
name = '\''+ name + '\'';
}
if (name.startsWith("'")) { //$NON-NLS-1$
builder.setLength(expressionStart);
builder.append(name.toUpperCase());
}
builder.insert(expressionStart, "(SELECT oid FROM pg_proc WHERE upper(proname) = "); //$NON-NLS-1$
builder.append(")"); //$NON-NLS-1$
} else {
builder.insert(expressionStart, "cast("); //$NON-NLS-1$
builder.append(" AS ").append(type).append(")"); //$NON-NLS-1$ //$NON-NLS-2$
}
}
if (c != -1) {
builder.append((char)c);
}
break;
}
}
}
break;
}
c = read();
break;
}
case '~': {
if (rewrite) {
int start = builder.length() - 1;
boolean not = false;
if (start > 0 && builder.charAt(start - 1) == '!') {
not = true;
start -= 1;
}
c = read();
boolean like = false;
if (c == '~') {
like = true;
c = read();
}
if (c == '*') {
break; //can't handle
}
builder.setLength(start);
if (not) {
builder.append(" NOT"); //$NON-NLS-1$
}
if (like) {
builder.append(" LIKE "); //$NON-NLS-1$
} else {
builder.append(" LIKE_REGEX "); //$NON-NLS-1$
}
if (c != -1) {
builder.append((char)c);
}
}
c = read();
break;
}
default: {
c = read();
}
}
}
String result = builder.toString();
builder.setLength(0);
if (result.length() == 0) {
return null;
}
return result;
}
private void startRemark(boolean block) {
blockRemark = block;
insideRemark = true;
}
private void endRemark() {
insideRemark = false;
}
private int read() throws IOException {
int c = reader.read();
if (c != -1) {
builder.append((char)c);
}
return c;
}
/**
* Check if this is the last statement, and if the single line or block
* comment is not finished yet.
*
* @return true if the current position is inside a remark
*/
public boolean isInsideRemark() {
return insideRemark;
}
/**
* If currently inside a remark, this method tells if it is a block comment
* (true) or single line comment (false)
*
* @return true if inside a block comment
*/
public boolean isBlockRemark() {
return blockRemark;
}
public void setRewrite(boolean rewrite) {
this.rewrite = rewrite;
}
}