/**
* (created at 2011-3-11)
*/
package com.alibaba.druid.sql.parser;
import java.sql.SQLSyntaxErrorException;
/**
* support MySQL 5.5 token
*
* @author QIU Shuo
* @author shaojin.wensj
*/
public class SQLLexer {
protected final char[] buf;
/** always be {@link #buf}.length - 1 */
protected final int eofIndex;
/** current index of {@link #buf} */
protected int curIndex = -1;
/** always be {@link #buf}[{@link #curIndex}] */
protected char ch;
/** current token, set by {@link #nextToken()} */
protected int tokenPos = 0;
protected Token token;
/** A character buffer for literals. */
protected final static ThreadLocal sbufRef = new ThreadLocal();
protected char[] sbuf;
protected String stringVal;
protected SymbolTable symbolTable = new SymbolTable();
public SQLLexer(char[] sql) {
super();
if ((this.sbuf = sbufRef.get()) == null) {
this.sbuf = new char[1024];
sbufRef.set(this.sbuf);
}
if (CharTypes.isWhitespace(sql[sql.length - 1])) {
this.buf = sql;
} else {
this.buf = new char[sql.length + 1];
System.arraycopy(sql, 0, this.buf, 0, sql.length);
}
this.eofIndex = this.buf.length - 1;
this.buf[this.eofIndex] = LayoutCharacters.EOI;
scanChar();
}
public SQLLexer(String sql) {
this(sql.toCharArray());
}
protected final Token token() {
return token;
}
protected final char scanChar() {
return ch = buf[++curIndex];
}
/**
* @param skip if 1, then equals to {@link #scanChar()}
*/
protected final char scanChar(int skip) {
return ch = buf[curIndex += skip];
}
protected final boolean hasChars(int howMany) {
return curIndex + howMany <= eofIndex;
}
protected final boolean eof() {
return curIndex >= eofIndex;
}
public void nextToken() throws SQLSyntaxErrorException {
skipSeparator();
tokenPos = curIndex;
switch (ch) {
case '0':
switch (buf[curIndex + 1]) {
case 'x':
scanChar(2);
scanHexaDecimal(false);
return;
case 'b':
scanChar(2);
scanBitField(false);
return;
}
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
scanNumber();
return;
case '.':
if (CharTypes.isDigit(buf[curIndex + 1])) {
scanNumber();
} else {
scanChar();
token = Token.DOT;
}
return;
case '\'':
case '"':
scanString();
return;
case 'n':
case 'N':
if (buf[curIndex + 1] == '\'') {
scanChar();
scanString();
token = Token.LITERAL_NCHARS;
return;
}
scanIdentifier();
return;
case 'x':
case 'X':
if (buf[curIndex + 1] == '\'') {
scanChar(2);
scanHexaDecimal(true);
return;
}
scanIdentifier();
return;
case 'b':
case 'B':
if (buf[curIndex + 1] == '\'') {
scanChar(2);
scanBitField(true);
return;
}
scanIdentifier();
return;
case '@':
if (buf[curIndex + 1] == '@') {
scanSystemVariable();
return;
}
scanUserVariable();
return;
case '?':
scanChar();
token = Token.QUES;
return;
case '(':
scanChar();
token = Token.LPAREN;
return;
case ')':
scanChar();
token = Token.RPAREN;
return;
case '[':
scanChar();
token = Token.LBRACKET;
return;
case ']':
scanChar();
token = Token.RBRACKET;
return;
case '{':
scanChar();
token = Token.LBRACE;
return;
case '}':
scanChar();
token = Token.RBRACE;
return;
case ',':
scanChar();
token = Token.COMMA;
return;
case ';':
scanChar();
token = Token.SEMI;
return;
case ':':
if (buf[curIndex + 1] == '=') {
scanChar(2);
token = Token.COLONEQ;
return;
}
scanChar();
token = Token.COLON;
return;
case '=':
scanChar();
token = Token.EQ;
return;
case '~':
scanChar();
token = Token.TILDE;
return;
case '*':
scanChar();
token = Token.STAR;
return;
case '-':
scanChar();
token = Token.SUB;
return;
case '+':
scanChar();
token = Token.PLUS;
return;
case '^':
scanChar();
token = Token.CARET;
return;
case '/':
scanChar();
token = Token.SLASH;
return;
case '%':
scanChar();
token = Token.PERCENT;
return;
case '&':
if (buf[curIndex + 1] == '&') {
scanChar(2);
token = Token.AMPAMP;
return;
}
scanChar();
token = Token.AMP;
return;
case '|':
if (buf[curIndex + 1] == '|') {
scanChar(2);
token = Token.BARBAR;
return;
}
scanChar();
token = Token.BAR;
return;
case '!':
if (buf[curIndex + 1] == '=') {
scanChar(2);
token = Token.BANGEQ;
return;
}
scanChar();
token = Token.BANG;
return;
case '>':
switch (buf[curIndex + 1]) {
case '=':
scanChar(2);
token = Token.GTEQ;
return;
case '>':
scanChar(2);
token = Token.GTGT;
return;
default:
scanChar();
token = Token.GT;
return;
}
case '<':
switch (buf[curIndex + 1]) {
case '=':
if (buf[curIndex + 2] == '>') {
scanChar(3);
token = Token.LTEQGT;
return;
}
scanChar(2);
token = Token.LTEQ;
return;
case '>':
scanChar(2);
token = Token.LTGT;
return;
case '<':
scanChar(2);
token = Token.LTLT;
return;
default:
scanChar();
token = Token.LT;
return;
}
case '`':
scanIdentifierWithAccent();
return;
default:
if (CharTypes.isIdentifierChar(ch)) {
scanIdentifier();
} else if (eof()) {
token = Token.EOF;
tokenPos = curIndex = eofIndex;
} else {
throw err("unsupported character: " + ch);
}
return;
}
}
protected int offsetCache;
protected int sizeCache;
/**
* first @ is included
*/
protected void scanUserVariable() throws SQLSyntaxErrorException {
if (ch != '@') throw err("first char must be @");
offsetCache = curIndex;
sizeCache = 1;
int hash = ch;
for (; CharTypes.isIdentifierChar(scanChar()); ++sizeCache) {
hash = 31 * hash + ch;
}
stringVal = symbolTable.addSymbol(buf, offsetCache, sizeCache, hash);
token = Token.USR_VAR;
}
/**
* first @@ is included
*/
protected void scanSystemVariable() throws SQLSyntaxErrorException {
if (ch != '@' || buf[curIndex + 1] != '@') throw err("first char must be @@");
offsetCache = curIndex;
sizeCache = 2;
int hash = 31 * ch + ch;
for (scanChar(); CharTypes.isIdentifierChar(scanChar()); ++sizeCache) {
hash = 31 * hash + ch;
}
stringVal = symbolTable.addSymbol(buf, offsetCache, sizeCache, hash);
token = Token.SYS_VAR;
}
protected void scanString() throws SQLSyntaxErrorException {
boolean dq = false;
if (ch == '\'') {
} else if (ch == '"') {
dq = true;
} else {
throw err("first char must be \" or '");
}
offsetCache = curIndex;
int size = 1;
sbuf[0] = '\'';
if (dq) {
loop: while (true) {
switch (scanChar()) {
case '\'':
putChar('\\', size++);
putChar('\'', size++);
break;
case '\\':
putChar('\\', size++);
putChar(scanChar(), size++);
continue;
case '"':
if (buf[curIndex + 1] == '"') {
putChar('"', size++);
scanChar();
continue;
}
putChar('\'', size++);
scanChar();
break loop;
default:
if (eof()) {
throw err("unclosed string");
}
putChar(ch, size++);
continue;
}
}
} else {
loop: while (true) {
switch (scanChar()) {
case '\\':
putChar('\\', size++);
putChar(scanChar(), size++);
continue;
case '\'':
if (buf[curIndex + 1] == '\'') {
putChar('\\', size++);
putChar(scanChar(), size++);
continue;
}
putChar('\'', size++);
scanChar();
break loop;
default:
if (eof()) {
throw err("unclosed string");
}
putChar(ch, size++);
continue;
}
}
}
sizeCache = curIndex - offsetCache;
stringVal = new String(sbuf, 0, size);
token = Token.LITERAL_CHARS;
}
/**
* @param quoteMode if false: first 0x has been skipped; if
* true: first x' has been skipped
*/
protected void scanHexaDecimal(boolean quoteMode) throws SQLSyntaxErrorException {
offsetCache = curIndex;
for (; CharTypes.isHex(ch); scanChar());
if ((sizeCache = curIndex - offsetCache) <= 0) {
throw err("expect at least one hexdigit");
}
if (quoteMode) {
if (ch != '\'') {
throw err("invalid char for hex: " + ch);
}
} else if (CharTypes.isIdentifierChar(ch)) {
ch = buf[curIndex = offsetCache -= 2];
scanIdentifierFromNumber(offsetCache, sizeCache + 2);
return;
}
token = Token.LITERAL_HEX;
}
/**
* @param quoteMode if false: first 0b has been skipped; if
* true: first b' has been skipped
*/
protected void scanBitField(boolean quoteMode) throws SQLSyntaxErrorException {
offsetCache = curIndex;
for (; ch == '0' || ch == '1'; scanChar());
if ((sizeCache = curIndex - offsetCache) <= 0) {
throw err("expect at least one bit");
}
if (quoteMode) {
if (ch != '\'') {
throw err("invalid char for bit: " + ch);
}
} else if (CharTypes.isIdentifierChar(ch)) {
ch = buf[curIndex = offsetCache -= 2];
scanIdentifierFromNumber(offsetCache, sizeCache + 2);
return;
}
token = Token.LITERAL_BIT;
}
/**
* if first char is ., token may be {@link Token#DOT} if
* invalid char is presented after .
*/
protected void scanNumber() throws SQLSyntaxErrorException {
offsetCache = curIndex;
sizeCache = 1;
final boolean fstDot = ch == '.';
boolean dot = fstDot;
boolean sign = false;
int state = fstDot ? 1 : 0;
for (; scanChar() != LayoutCharacters.EOI; ++sizeCache) {
switch (state) {
case 0:
if (CharTypes.isDigit(ch)) {
} else if (ch == '.') {
dot = true;
state = 1;
} else if (ch == 'e' || ch == 'E') {
state = 3;
} else if (CharTypes.isIdentifierChar(ch)) {
scanIdentifierFromNumber(offsetCache, sizeCache);
return;
} else {
token = Token.LITERAL_NUM_PURE_DIGIT;
return;
}
break;
case 1:
if (CharTypes.isDigit(ch)) {
state = 2;
} else if (ch == 'e' || ch == 'E') {
state = 3;
} else if (CharTypes.isIdentifierChar(ch)) {
if (fstDot) {
sizeCache = 1;
ch = buf[curIndex = offsetCache + 1];
token = Token.DOT;
return;
} else {
throw err("invalid char after '.': " + ch);
}
} else {
token = Token.LITERAL_NUM_MIX_DIGIT;
return;
}
break;
case 2:
if (CharTypes.isDigit(ch)) {
} else if (ch == 'e' || ch == 'E') {
state = 3;
} else if (CharTypes.isIdentifierChar(ch)) {
if (fstDot) {
sizeCache = 1;
ch = buf[curIndex = offsetCache + 1];
token = Token.DOT;
return;
} else {
throw err("invalid char after '.' for as part of number: " + ch);
}
} else {
token = Token.LITERAL_NUM_MIX_DIGIT;
return;
}
break;
case 3:
if (CharTypes.isDigit(ch)) {
state = 5;
} else if (ch == '+' || ch == '-') {
sign = true;
state = 4;
} else if (CharTypes.isIdentifierChar(ch)) {
if (fstDot) {
sizeCache = 1;
ch = buf[curIndex = offsetCache + 1];
token = Token.DOT;
return;
} else if (!dot) {
scanIdentifierFromNumber(offsetCache, sizeCache);
return;
} else {
throw err("invalid char after '.' and 'e' for as part of number: " + ch);
}
} else if (!dot) {
token = Token.IDENTIFIER;
stringVal = symbolTable.addSymbol(buf, offsetCache, sizeCache);
return;
} else {
throw err("invalid char after '.' and 'e' for as part of number: " + ch);
}
break;
case 4:
if (CharTypes.isDigit(ch)) {
state = 5;
break;
} else if (fstDot) {
sizeCache = 1;
ch = buf[curIndex = offsetCache + 1];
token = Token.DOT;
} else if (!dot) {
ch = buf[--curIndex];
--sizeCache;
token = Token.IDENTIFIER;
stringVal = symbolTable.addSymbol(buf, offsetCache, sizeCache);
} else {
throw err("expect digit char after SIGN for 'e': " + ch);
}
return;
case 5:
if (CharTypes.isDigit(ch)) {
break;
} else if (CharTypes.isIdentifierChar(ch)) {
if (fstDot) {
sizeCache = 1;
ch = buf[curIndex = offsetCache + 1];
token = Token.DOT;
} else if (!dot) {
if (sign) {
sizeCache = 1;
ch = buf[curIndex = offsetCache];
scanIdentifierFromNumber(curIndex, 0);
} else {
scanIdentifierFromNumber(offsetCache, sizeCache);
}
} else {
throw err("expect digit char after SIGN for 'e': " + ch);
}
} else {
token = Token.LITERAL_NUM_MIX_DIGIT;
}
return;
}
}
token = state == 0 ? Token.LITERAL_NUM_PURE_DIGIT : Token.LITERAL_NUM_MIX_DIGIT;
}
/**
* @param initSize how many char has already been consumed
*/
private void scanIdentifierFromNumber(int initOffset, int initSize) throws SQLSyntaxErrorException {
offsetCache = initOffset;
sizeCache = initSize;
for (; CharTypes.isIdentifierChar(ch); ++sizeCache) {
scanChar();
}
stringVal = symbolTable.addSymbol(buf, offsetCache, sizeCache);
token = Token.IDENTIFIER;
}
/**
* id is NOT included in `.
*/
protected void scanIdentifier() throws SQLSyntaxErrorException {
scanIdentifierFromNumber(curIndex, 0);
}
/**
* id is included in `. first ` is included
*/
protected void scanIdentifierWithAccent() throws SQLSyntaxErrorException {
offsetCache = curIndex;
for (; scanChar() != LayoutCharacters.EOI;) {
if (ch == '`') {
if (scanChar() != '`') {
break;
}
}
}
stringVal = symbolTable.addSymbol(buf, offsetCache, sizeCache = curIndex - offsetCache);
token = Token.IDENTIFIER;
}
/**
* Append a character to sbuf.
*/
protected final void putChar(char ch, int index) {
if (index >= sbuf.length) {
char[] newsbuf = new char[sbuf.length * 2];
System.arraycopy(sbuf, 0, newsbuf, 0, sbuf.length);
sbuf = newsbuf;
}
sbuf[index] = ch;
}
/**
* skip whitespace and comment
*/
protected void skipSeparator() {
while (!eof()) {
while (CharTypes.isWhitespace(ch)) { // imply: ch!=eof
scanChar();
continue;
}
switch (ch) {
case '#': // MySQL specified
while (!eof()) {
if (scanChar() == '\n') {
scanChar();
continue;
}
}
continue;
case '/':
if (hasChars(2) && '*' == buf[curIndex + 1] && '!' != buf[curIndex + 2]) {
scanChar(2);
for (int state = 0; !eof(); scanChar()) {
if (state == 0) {
if ('*' == ch) {
state = 1;
}
} else {
if ('*' != ch) {
state = 0;
} else if ('/' == ch) {
scanChar();
continue;
}
}
}
continue;
}
return;
case '-':
if (hasChars(3) && '-' == buf[curIndex + 1] && CharTypes.isWhitespace(buf[curIndex + 2])) {
scanChar(3);
for (; !eof(); scanChar()) {
if ('\n' == ch) {
scanChar();
continue;
}
}
}
return;
}
return;
}
}
/**
* always throw SQLSyntaxErrorException
*/
protected SQLSyntaxErrorException err(String msg) throws SQLSyntaxErrorException {
String errMsg = msg + ". " + toString();
throw new SQLSyntaxErrorException(errMsg);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName()).append('@').append(hashCode()).append('{');
String sqlLeft = new String(buf, curIndex, buf.length - curIndex);
sb.append("curIndex=")
.append(curIndex)
.append(", ch=")
.append(ch)
.append(", token=")
.append(token)
.append(", sqlLeft=")
.append(sqlLeft)
.append(", sql=")
.append(buf);
sb.append('}');
return sb.toString();
}
//
// public static void main(String[] args) {
// char[] dd = new char[333];
// for (int i = 0; i < dd.length; ++i) {
// dd[i] = 'd';
// }
// char t = 'c';
// char t1 = 'd';
// long start = System.currentTimeMillis();
// long end = System.currentTimeMillis();
// for (int i = 0; i < 10; ++i) {
// int r = i % dd.length;
// t = dd[r];
// }
//
// int loop = 500000000;
// start = System.currentTimeMillis();
// for (int i = 0; i < loop; ++i) {
// int r = i % dd.length;
// t = t1;
// }
// end = System.currentTimeMillis();
// System.out.println((end - start) * 1.0d / (loop / 1000000));
// }
}