java.util.Scannerクラスで構文解析を行ってみた。

| コメントをどうぞ

とある作業でREST APIを作成していたのですが、リクエストからいくつかの条件を受けてDBの値を検索するような仕組みが必要となりました。
ごく単純に実装するのであれば、以下の例のような感じで実装すれば良いかと思います。

例: ユーザーを名前で検索するような場合のエンドポイントとパラメータの場合
GET http://www.example.com/user?name=xxxx

・・・が。

しかし、上記の例では名前の完全一致で検索する場合はよいですが、

  • 前方一致、中間一致、後方一致についてはどうする?
  • 検索の条件値がnullや空文字列の場合は?
  • 複数の条件を ANDまたはOR条件で結合する場合は?
    などなど、アプリケーションでの要求仕様によりますがいかにも汎用性がありません。

そこでもっと汎用性を持たせたるためにGoogle AppsのAPIよろしく、

type=user and name like '%abc%' or name like '%def%'

というようなクエリ文字列をリクエストパラメータとして利用することにしました。

ただ、このような場合、送信されてきたクエリ文字列を解析し、検索対象パラメータ、演算子および検索対象値を正確に抽出する必要があります。
Javaの場合、高度な解析が必要なのであればBNFで構文を定義し、JavaCCにより解析プログラムを作成することになるのでしょうが、今回はより簡易的に実装を行うためjava.util.Scannerクラスを利用した実装例を紹介します。

java.util.Scannerクラスとは

java.util.ScannerクラスはJavaSE5から追加された正規表現を使用した構文解析が可能な、単純なテキストスキャナです。 詳しくはJavaDocを参照。

実装例1:

まず、ごく簡単なScannerの実装例。

    public static void example1(){
        String str = "Hello world";

        //デフォルトは半角空白文字が区切り文字
        Scanner s = new Scanner(str);

        // 対象文字列をデフォルトの半角空白文字で分割してそれぞれを出力
        while (s.hasNext()) {
            System.out.println(s.next());
        }
        s.close();
    }

これを実行すると、以下のように標準出力に表示されます。

Hello
world

実装例2:

では、次の例。

    public static void example2(){
        String q = "type='test' and name like '%aaa' or email='aaa@example.com'";

        //andまたはorを区切り文字として設定(正規表現による指定)
        Scanner s = new Scanner(q).useDelimiter("s*ands*|s*ors*");

        while (s.hasNext()) {
            System.out.println(s.next());
        }
        s.close();
    }

デフォルトではScannerの区切り文字は半角空白文字なのでこれを”and” または “or”に変更してみました。
これを実行すると、こんな感じの出力になります。

type='test'
name like '%aaa'
email='aaa@example.com'

うん、正しく対象のクエリ文字列が条件毎に分割されてますね。

実装例3:

ただ、Scannerクラスでは区切り文字に指定された条件が複数ある場合、何を対象に文字列が区切られたのかが分かりません。そこでちょっと小細工。

    public static void example3(){
        String q = "type='test' and name like '%aaa' or email='aaa@example.com'";
        Scanner s = new Scanner(q).useDelimiter("s*ands*|s*ors*");

        int start = -1;
        int end = -1;
        while (s.hasNext()) {
            String fragment = s.next();
            end = s.match().start();

            if(start >= 0){
                //前の検索条件に対する論理演算子を出力
                System.out.print(q.substring(start, end));
            }

            System.out.println(fragment);

            start = s.match().end();
        }
        s.close();
    }

この例ではScanner.matchメソッドから得られるMatchResultクラスから区切り文字の条件に一致した位置を読み込み、これにより元の文字列から検索条件に相当する部分文字列を抜き出しています。実行結果はこんな感じ。

type='test'
 and name like '%aaa'
 or email='aaa@example.com'

良い感じです。

実装例4:

あとは分割された各条件毎に検索対象プロパティ、演算子および検索値を解析します。
いろいろ手法は考えられるかと思いますが、今回の例ではjava.util.regexPatternクラスおよびMatcherクラスを利用します。

    public static void example4(){
        String q = "type='test' and name like '%aaa' or email='aaa@example.com'";
        Scanner s = new Scanner(q).useDelimiter("s*ands*|s*ors*");

        int start = -1;
        int end = -1;
        while (s.hasNext()) {
            String fragment = s.next();
            end = s.match().start();

            if(start >= 0){
                System.out.println(q.substring(start, end));
            }

            Pattern pattern = Pattern.compile("(.+?)(s*=s*|s*likes*)(.+)");
            Matcher m = pattern.matcher(fragment);
            if (m.find()){
              System.out.println("key: " + m.group(1));
              System.out.println("operator: " + m.group(2));
              System.out.println("value: " + m.group(3));
            }

            start = s.match().end();

            System.out.println();
        }
        s.close();
    }

実行結果はこうなります。

key: type
operator: =
value: 'test'

 and 
key: name
operator:  like 
value: '%aaa'

 or 
key: email
operator: =
value: 'aaa@example.com'

ここまで構文が解析できれば後は解析結果にあわせてDAOの実装を行えば目的は達成できますね。

ただし、先にも述べたとおりこの例のような構文解析はあくまで単純なものですので、括弧を含んだりするような複雑なクエリ文字列の解析には対応できないと思います。
また、上記例のような実装方法以外にももっと効率的な実装方法があるかもしれません。「もっといい実装方法があるよ!」という方がいらっしゃいましたら是非ご指摘願います。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

次のHTML タグと属性が使えます: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>