Wednesday, October 2, 2019 / JavaScript, Rhino, Groovy

Rhino で Java 側でつくった配列クラスを使う

カスタムオブジェクトを Java側でつくって JavaScript であれこれしたい場合で、 そのカスタムオブジェクトが配列的なクラスの場合 list[0] のようにアクセスしたときに意図した値を返す方法.

結論としては、ScriptableObject を拡張してつくったサブクラスで Object get(int index, Scriptable start) メソッドをオーバーライドすればOK.

MyArrayList という配列オブジェクトを準備

テストのため、このクラスはどの配列の要素にアクセスしても hello という文字列しか返さないようにしてみます. (例によりコードは groovy です.)

class MyArrayList extends ScriptableObject {
    @Override
    String getClassName() { return 'MyArrayList' }

    @Override
    Object get(int index, Scriptable start){
        return 'hello'
    }
}

myArrayList[0] などを JavaScript 側で実行したときに get メソッドが呼ばれます. このクラスは常に hello 文字列を返すだけの実装になっています.

このJavaScript を実行してみると hello 文字列がかえるはずです.

def script0 = 'new MyArrayList()[999]'

helloMyArrayList.groovy

@Grab(group='org.mozilla', module='rhino', version='1.7.11')
import org.mozilla.javascript.Context
import org.mozilla.javascript.ScriptableObject
import org.mozilla.javascript.Scriptable

class MyArrayList extends ScriptableObject {
    @Override
    String getClassName() { return 'MyArrayList' }

    @Override
    Object get(int index, Scriptable start){
        return 'hello'
    }
}

def script0 = 'new MyArrayList()[999]'

def cx = Context.enter()
def scope = cx.initStandardObjects()
ScriptableObject.defineClass(scope, MyArrayList.class)
def result0 = cx.evaluateString(scope, script0, "<cmd>", 1, null)
println result0

実行してみると

$ groovy helloMyArrayList.groovy
hello

普通の配列として機能するようにアップグレード

MyArrayList クラスを修正して普通の配列として振る舞うようにします。 具体的には:

  • push して要素を追加できる
  • length プロパティがある
  • インデックスの範囲が格納されている要素数を超えたら undefined を返すようにする

修正版 MyArrayList

class MyArrayList extends ScriptableObject {
    private def list = new java.util.ArrayList()

    @Override
    String getClassName() { return 'MyArrayList' }

    @Override
    Object get(int index, Scriptable start){
        if(0<=index && index<list.size()){
            return list.get(index)
        }

        return super.get(index, start)
    }

    void jsConstructor() {}

    int jsGet_length(){
        return list.size()
    }

    void jsFunction_push(Object value){
        list.add(value)
    }
}

get メソッドでは、index を範囲チェックした上で、要素数以内ならば、該当要素を返し、そうでなければ、親クラスに処理を任せています.

def script0 = '''\
  var list = new MyArrayList();
  list.push('pika');
  list[0];'''

def cx = Context.enter()
def scope = cx.initStandardObjects()
ScriptableObject.defineClass(scope, MyArrayList.class)
def result0 = cx.evaluateString(scope, script0, "<cmd>", 1, null)
println result0

実行すると意図通り pika が返ります.

まとめ

これで JavaScript の配列のように振る舞うカスタムオブジェクトをJava側で用意することができました.