SqlFunctionTokenizer.java

package com.github.mygreen.sqlmapper.metamodel.support;

/**
 * SQL関数の字句解析処理。
 *
 * @since 0.3
 * @author T.TSUCHIE
 *
 */
public class SqlFunctionTokenizer {

    /**
     * トークンの種類
     *
     */
    public enum TokenType {
        SQL,
        LEFT_VARIABLE,
        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 SqlFunctionTokenizer(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 LEFT_VARIABLE:
            parseLeftVariable();
            break;
        case BIND_VARIABLE:
            parseBindVariable();
            break;
        default:
            parseEof();
            break;
        }
        return tokenType;
    }

    /**
     * Parse the SQL.
     */
    protected void parseSql() {

        int leftVariableStartPos = sql.indexOf("$left", position);
        int bindVariableStartPos = sql.indexOf("?", position);
        int nextStartPos = getNextStartPos(leftVariableStartPos, bindVariableStartPos);

        if (nextStartPos < 0) {
            // 特定のトークンでない場合は、すべてSQL区分する。
            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 == leftVariableStartPos) {
                nextTokenType = TokenType.LEFT_VARIABLE;
                position = leftVariableStartPos + 4;

            } else if (nextStartPos == bindVariableStartPos) {
                nextTokenType = TokenType.BIND_VARIABLE;
                position = bindVariableStartPos;
            }

            if (needNext) {
                next();
            }
        }
    }

    /**
     * 次のトークンの開始位置を返します。
     *
     * @param leftVariableStartPos {@literal $left} 変数の開始位置
     * @param bindVariableStartPos {@literal ?} 変数の開始位置
     * @return 次のトークンの開始位置。特定のトークンでない場合は {@literal -1} を返します。
     */
    protected int getNextStartPos(int leftVariableStartPos, int bindVariableStartPos) {

        int nextStartPos = -1;
        if (leftVariableStartPos >= 0) {
            nextStartPos = leftVariableStartPos;
        }

        if (bindVariableStartPos >= 0
                && (nextStartPos < 0 || bindVariableStartPos < nextStartPos)) {
            nextStartPos = bindVariableStartPos;
        }
        return nextStartPos;
    }

    /**
     * {@literal $left} 変数をパースします。
     */
    protected void parseLeftVariable() {
        token = "$left";
        nextTokenType = TokenType.SQL;
        position += 1;
        tokenType = TokenType.LEFT_VARIABLE;
    }

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

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

    /**
     * バインド変数の現在までの出現回数を取得します。
     * @return バインド変数の現在までの出現回
     */
    public int getBindBariableNum() {
        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;
    }

}