SqlUtils.java

package com.github.mygreen.splate;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.atomic.AtomicReference;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.NonNull;

/**
 * 2Way-SQL機能の中で提供されるユーティリティクラス。
 * <p>MirageSQL/Seaser2からの持ち込みなので、既存のユーティリティクラスとは分けて定義する。</p>
 *
 * @version 0.2
 * @author T.TSUCHIE
 *
 */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class SqlUtils {

    /**
     * 空の文字列の配列です。
     */
    public static final String[] EMPTY_STRINGS = new String[0];

    /**
     * 文字列を置き換えます。
     * 置換対象の文字列がnullの場合は、結果として {@literal null} を返します。
     *
     * @param text テキスト
     * @param fromText 置き換え対象のテキスト
     * @param toText 置き換えるテキスト
     * @return 結果
     */
    public static final String replace(final String text, final String fromText, final String toText) {

        if (text == null || fromText == null || toText == null) {
            return null;
        }
        StringBuilder buf = new StringBuilder(100);
        int pos2 = 0;
        while (true) {
            int pos = text.indexOf(fromText, pos2);
            if (pos == 0) {
                buf.append(toText);
                pos2 = fromText.length();
            } else if (pos > 0) {
                buf.append(text, pos2, pos);
                buf.append(toText);
                pos2 = pos + fromText.length();
            } else {
                buf.append(text.substring(pos2));
                break;
            }
        }
        return buf.toString();
    }

    /**
     * 文字列が空かどうか判定します。
     *
     * @param text 文字列
     * @return 文字列が {@literal null} または空文字列なら {@literal true} を返します。
     */
    public static final boolean isEmpty(final String text) {
        return text == null || text.length() == 0;
    }

    /**
     * リソースをテキストとして読み込む。
     * <p>引数で指定したストリームは自動的にクローズします。</p>
     *
     * @param in リソース
     * @param encoding エンコーディング
     * @return 読み込んだテキスト
     * @throws IOException リソースの読み込みに失敗した場合にスローされます。
     */
    public static String readStream(final InputStream in, final String encoding) throws IOException {

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try(in; out) {
            byte[] buf = new byte[1024 * 8];
            int length = 0;
            while((length = in.read(buf)) != -1){
                out.write(buf, 0, length);
            }

            return new String(out.toByteArray(), encoding);

        }
    }

    /**
     * 文字列のメッセージダイジェストを作成します。
     * @param text 計算対象の文字列
     * @return メッセージダイジェスト
     */
    public static String getMessageDigest(final String text) {

        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            md.update(text.getBytes(StandardCharsets.UTF_8));

            byte[] hash = md.digest();
            StringBuilder sb = new StringBuilder();
            for (byte b : hash) {
                sb.append(String.format("%02x", b));
            }

            return sb.toString();

        } catch (NoSuchAlgorithmException e) {
            throw new IllegalArgumentException("not such algorithm name.", e);
        }

    }

    /**
     * SQL中の位置として行、列の位置を解決します。
     * @param sql SQL
     * @param position 位置
     * @return テンプレートの位置情報
     */
    public static Position resolveSqlPosition(final @NonNull String sql, final int position) {

        // 現在の位置より前の情報を切り出し、改行を検索する。
        final String before = sql.substring(0, position);

        int row = 1;

        final String[] searchWords = {"\r\n", "\r", "\n"};

        AtomicReference<CharSequence> foundStr = new AtomicReference<>();
        int lastIndex = 0;
        int index = 0;
        while((index = indexOfAny(before, index, foundStr, searchWords)) >= 0) {
            // 改行が見つかったら行数をカウントアップする。
            row++;

            index += foundStr.get().length();

            // 現在見つかっている改行の位置を保持しておく
            lastIndex = index;
        }

        // 最後に改行が見つかった位置から、SQLの位置を引けば列がわかる
        int col = position - lastIndex;
        if(lastIndex > 0) {
            col--;
        }

        // 行の切り出し
        final String after = sql.substring(lastIndex);
        String line = after;
        if(after != null && (index = indexOfAny(after, 0, null, searchWords)) >= 0) {
            line = after.substring(0, index);
        }

        final Position result = new Position();
        result.setCol(col);
        result.setRow(row);
        result.setLine(line);

        return result;

    }

    /**
     * <p>指定した複数の文字列から、最初に出現する位置のインデックスを返します。</p>
     * <p>CommonsLangの{@code StringUtils#indexOfAny}の持ち込み。</p>
     *
     * <ul>
     *   <li>検索対象の文字が {@code null}の場合は、{@code -1} を返します。</li>
     *   <li>検索文字が {@code null} または空の配列の場合は、{@code -1} を返します。</li>
     *   <li>検索文字に 空文字({@code ""})が含まれる場合、{@code 0}を返します。</li>
     *   <li></li>
     * </ul>
     *
     * @param str 検索対象の文字列
     * @param startPos 検索開始する位置。0から始まります。
     * @param foundStr 見つかった文字列。見つかった場合には値がセットされます。{@code null}の場合は値はセットされません。
     * @param searchStrs  検索する文字列
     * @return 初めに見つかった文字列のインデクスを返します。見つからない場合は、{@code -1}を返します。
     * @since 0.2
     */
    public static int indexOfAny(final CharSequence str, final int startPos,
            final AtomicReference<CharSequence> foundStr, final CharSequence... searchStrs) {

        if (str == null || searchStrs == null || searchStrs.length == 0) {
            return INDEX_NOT_FOUND;
        }

        // String's can't have a MAX_VALUEth index.
        int ret = Integer.MAX_VALUE;

        int tmp = 0;
        for (final CharSequence search : searchStrs) {
            if (search == null) {
                continue;
            }
            tmp = indexOf(str, search, startPos);
            if (tmp == INDEX_NOT_FOUND) {
                continue;
            }

            if (tmp < ret) {
                ret = tmp;

                // 見つかった文字列を保存する
                if(foundStr != null) {
                    foundStr.set(search);
                }
            }
        }

        return ret == Integer.MAX_VALUE ? INDEX_NOT_FOUND : ret;
    }

    /**
     * 文字列の位置検索で失敗したときの値.
     * @since 0.2
     */
    private static final int INDEX_NOT_FOUND = -1;

    /**
     * {@link CharSequence}に対する {@code indexOf(CharSequence)}の実装。
     * <p>CommonsLangの{@code CharSequenceUtils#indexOf}の持ち込み。</p>
     *
     * @param cs the {@code CharSequence} to be processed
     * @param searchChar the {@code CharSequence} to be searched for
     * @param start the start index
     * @return the index where the search sequence was found
     * @since 0.2
     */
    private static int indexOf(final CharSequence cs, final CharSequence searchChar, final int start) {
        if (cs instanceof String) {
            return ((String) cs).indexOf(searchChar.toString(), start);
        } else if (cs instanceof StringBuilder) {
            return ((StringBuilder) cs).indexOf(searchChar.toString(), start);
        } else if (cs instanceof StringBuffer) {
            return ((StringBuffer) cs).indexOf(searchChar.toString(), start);
        }
        return cs.toString().indexOf(searchChar.toString(), start);
    }
}