SqlTokenizer.java

/*
 * Copyright 2004-2010 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 com.github.mygreen.splate.parser;

import com.github.mygreen.splate.SqlUtils;

/**
 * SQLをトークンに分解するクラスです。.
 *
 * @version 0.2
 * @author higa
 */
public class SqlTokenizer {

    /**
     * トークンの種類
     *
     */
    public enum TokenType {
        SQL,
        COMMENT,
        ELSE,
        BIND_VARIABLE,
        EOF
    }

    /**
     * 解析対象のSQL
     */
    private String sql;

    /**
     * 現在解析しているポジション
     */
    private int position = 0;

    /**
     * トークン
     */
    private String token;

    /**
     * 現在のトークン種別
     */
    private TokenType tokenType = TokenType.SQL;

    /**
     * 次のトークン種別
     */
    private TokenType nextTokenType = TokenType.SQL;

    private int bindVariableNum = 0;

    public SqlTokenizer(String sql) {
        this.sql = sql;
    }

    /**
     * @return SQLを返します。
     */
    public String getSql() {
        return sql;
    }

    /**
     * @return 現在解析しているポジションを返します。
     */
    public int getPosition() {
        return position;
    }

	/**
     * @return トークンを返します。
     */
    public String getToken() {
        return token;
    }

    /**
     * @return 現在解析しているポジションより前のSQLを返します。
     */
    public String getBefore() {
        return sql.substring(0, position);
    }

    /**
     * @return 現在解析しているポジションより後ろのSQLを返します。
     */
    public String getAfter() {
        return sql.substring(position);
    }

    /**
     * @return 現在のトークン種別を返します。
     */
    public TokenType getTokenType() {
        return tokenType;
    }

    /**
     * @return 次のトークン種別を返します。
     */
    public TokenType getNextTokenType() {
        return nextTokenType;
    }

    /**
     * @return 次のトークンに進みます。
     */
    public TokenType next() {
        if (position >= sql.length()) {
            token = null;
            tokenType = TokenType.EOF;
            nextTokenType = TokenType.EOF;
            return tokenType;
        }
        switch (nextTokenType) {
        case SQL:
            parseSql();
            break;
        case COMMENT:
            parseComment();
            break;
        case ELSE:
            parseElse();
            break;
        case BIND_VARIABLE:
            parseBindVariable();
            break;
        default:
            parseEof();
            break;
        }
        return tokenType;
    }

    /**
     * Parse the SQL.
     */
    protected void parseSql() {
        int commentStartPos = sql.indexOf("/*", position);
        int commentStartPos2 = sql.indexOf("#*", position);
        if (0 < commentStartPos2 && commentStartPos2 < commentStartPos) {
            commentStartPos = commentStartPos2;
        }
        int lineCommentStartPos = sql.indexOf("--", position);
        int bindVariableStartPos = sql.indexOf("?", position);
        int elseCommentStartPos = -1;
        int elseCommentLength = -1;
        if (lineCommentStartPos >= 0) {
            int skipPos = skipWhitespace(lineCommentStartPos + 2);
            if (skipPos + 4 < sql.length()
                    && "ELSE".equals(sql.substring(skipPos, skipPos + 4))) {
                elseCommentStartPos = lineCommentStartPos;
                elseCommentLength = skipPos + 4 - lineCommentStartPos;
            }
        }
        int nextStartPos = getNextStartPos(commentStartPos,
                elseCommentStartPos, bindVariableStartPos);
        if (nextStartPos < 0) {
            token = sql.substring(position);
            nextTokenType = TokenType.EOF;
            position = sql.length();
            tokenType = TokenType.SQL;
        } else {
            token = sql.substring(position, nextStartPos);
            tokenType = TokenType.SQL;
            boolean needNext = nextStartPos == position;
            if (nextStartPos == commentStartPos) {
                nextTokenType = TokenType.COMMENT;
                position = commentStartPos + 2;
            } else if (nextStartPos == elseCommentStartPos) {
                nextTokenType = TokenType.ELSE;
                position = elseCommentStartPos + elseCommentLength;
            } else if (nextStartPos == bindVariableStartPos) {
                nextTokenType = TokenType.BIND_VARIABLE;
                position = bindVariableStartPos;
            }
            if (needNext) {
                next();
            }
        }
    }

    /**
     * Returns the next starting position.
     *
     * @param commentStartPos starting position of the comment
     * @param elseCommentStartPos starting position of the ELSE comment
     * @param bindVariableStartPos starting position of the bind variable
     * @return the next starting position.
     */
    protected int getNextStartPos(int commentStartPos, int elseCommentStartPos,
            int bindVariableStartPos) {

        int nextStartPos = -1;
        if (commentStartPos >= 0) {
            nextStartPos = commentStartPos;
        }
        if (elseCommentStartPos >= 0
                && (nextStartPos < 0 || elseCommentStartPos < nextStartPos)) {
            nextStartPos = elseCommentStartPos;
        }
        if (bindVariableStartPos >= 0
                && (nextStartPos < 0 || bindVariableStartPos < nextStartPos)) {
            nextStartPos = bindVariableStartPos;
        }
        return nextStartPos;
    }

    /**
     * Parse the comment.
     */
    protected void parseComment() {
        int commentEndPos = sql.indexOf("*/", position);
        int commentEndPos2 = sql.indexOf("*#", position);
        if (0 < commentEndPos2 && commentEndPos2 < commentEndPos) {
            commentEndPos = commentEndPos2;
        }
        if (commentEndPos < 0) {
            throw new SqlParseException(SqlUtils.resolveSqlPosition(sql, position),
                    String.format("Not closed comment '*/' for %s.", sql.substring(position)));
        }
        token = sql.substring(position, commentEndPos);
        nextTokenType = TokenType.SQL;
        position = commentEndPos + 2;
        tokenType = TokenType.COMMENT;
    }

    /**
     * Parse the bind variable.
     */
    protected void parseBindVariable() {
        token = nextBindVariableName();
        nextTokenType = TokenType.SQL;
        position += 1;
        tokenType = TokenType.BIND_VARIABLE;
    }

    /**
     * Parse the ELSE comment.
     */
    protected void parseElse() {
        token = null;
        nextTokenType = TokenType.SQL;
        tokenType = TokenType.ELSE;
    }

    /**
     * Parse the end of the SQL.
     */
    protected void parseEof() {
        token = null;
        tokenType = TokenType.EOF;
        nextTokenType = TokenType.EOF;
    }

    /**
     * @return the bind variable name for the position parameters.
     */
    protected String nextBindVariableName() {
        return "$" + ++bindVariableNum;
    }

    /**
     * トークンをスキップします。
     *
     * @return スキップしたトークン
     */
    public String skipToken() {
        int index = sql.length();
        char quote = position < sql.length() ? sql.charAt(position) : '\0';
        boolean quoting = quote == '\'' || quote == '(';
        if (quote == '(') {
            quote = ')';
        }
        for (int i = quoting ? position + 1 : position; i < sql.length(); ++i) {
            char c = sql.charAt(i);
            if ((Character.isWhitespace(c) || c == ',' || c == ')' || c == '(')
                    && !quoting) {
                index = i;
                break;
            } else if (c == '/' && i + 1 < sql.length()
                    && sql.charAt(i + 1) == '*') {
                index = i;
                break;
            } else if (c == '-' && i + 1 < sql.length()
                    && sql.charAt(i + 1) == '-') {
                index = i;
                break;
            } else if (quoting && quote == '\'' && c == '\''
                    && (i + 1 >= sql.length() || sql.charAt(i + 1) != '\'')) {
                index = i + 1;
                break;
            } else if (quoting && c == quote) {
                index = i + 1;
                break;
            }
        }
        token = sql.substring(position, index);
        tokenType = TokenType.SQL;
        nextTokenType = TokenType.SQL;
        position = index;
        return token;
    }

	/**
     * ホワイトスペースをスキップします。
     *
     * @return スキップしたホワイストスペース
     */
    public String skipWhitespace() {
        int index = skipWhitespace(position);
        token = sql.substring(position, index);
        position = index;
        return token;
    }

    private int skipWhitespace(int position) {
        int index = sql.length();
        for (int i = position; i < sql.length(); ++i) {
            char c = sql.charAt(i);
            if (!Character.isWhitespace(c)) {
                index = i;
                break;
            }
        }
        return index;
    }
}