Friday, May 1, 2020 / JavaScript, Rhino, Groovy

改良版) Rhino を使って Java から JavaScript を実行する

Rhino を使って Java から JavaScript を実行する から、さらにあれこれ試していて多少進捗があったので、 現在までに得た見地をまとめます。

今、関心の対象にあるのは、 いわゆる Embedding Rhino と呼ばれていることで、Javaのオブジェクト (Host Objects) をつくって、それを Rhino の javascript から利用することです。 このあたり Rhino Embedding_tutorial の内容です。

なお、例によってここにあるコードはすべて Groovy です。

console.log() を使いたい

本題に入る前にデバッグ用に必要な console.log() を Rhinoでどうするか問題を解決します。

たとえば、以下のような javascript を Embedding Rhino で実行したい。

console.log('hello world!');

これを解決します。

これは、https://github.com/mozilla/rhino/blob/master/examples/RunScript2.java に例がある通りです。 Groovy でポイントだけ書き出すと:

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

def cx = Context.enter()
def scope = cx.initStandardObjects()

def jsOut = Context.javaToJS(System.out, scope)
ScriptableObject.putProperty(scope, "out", jsOut)

def script = "out.println('hello world!');" 
cx.evaluateString(scope, script, "<cmd>", 1, null)

Context.exit()

out.println ではなく console.log としたければ:

def script = '''\
  |var console = {};
  |console.log = function(msg){ out.println(msg); };
  |
  |console.log('hello world!');
  |'''.stripMargin('|')

とすればOK。

see also : Rhino で console.log() したい

本題 自前の DOM を javascript で操作したい

いわゆるDOMで Document, Pages, Page の3つのオブジェクトがあり以下のように javascript で扱いたい、とする。

こんな javascript コード を書きたい:

var page0 = new Page('page-0');
var page1 = new Page('page-1');

var pages = new Pages();
pages.push(page0);
pages.push(page1);

var document = new Document(pages);
var len = document.pages.length;
for(int i=0; i<len; i++){
    console.log( '- ' + document.pages[i].name );
}

ここで出現する Document, Pages, Page を Java 側の Host Objects として実装したい、どうすればいいかという問題。

そのためには いわゆる Host Objects 用意すればいいのだが、以下のようなルールで実装:

  • ScriptableObject を継承する
  • String getClassName() メソッドを実装する
  • 引数なしのデフォルトコンストラクが必要
  • javascript から引数付きのコンストラクタを使いたい場合は それを @JSConstructor アノテーションで定義
  • javascript からアクセスしたいプロパティは @JSGetter, @JSSetter アノテーションで定義
  • javascript から使いたい関数は @JSFunction アノテーションで定義

それでは、これらのルールに則り、Document, Pages, Page クラスを実装してみる。

Document クラス

class Document extends ScriptableObject {
    @Override
    String getClassName() { return "Document" }

    private Pages pages

    Document(){}

    @JSConstructor
    Document(Pages pages){
        this.pages=pages
    }   

    @JSGetter
    Pages pages(){
        return this.pages
    }   
}

Pages クラス

class Pages extends ScriptableObject {
    @Override
    String getClassName() { return "Pages" }

    Pages(){
    }

    private def list = new java.util.ArrayList()

    @JSGetter
    int length(){
        return list.size()
    }

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

        return super.get(index, start)
    }

    @JSFunction
    void push(Page value){
        list.add(value)
    }
}

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

Page クラス

class Page extends ScriptableObject {
    @Override
    String getClassName() { return "Page" }

    Page(){}

    private String name

    @JSConstructor
    Page(String name){
        this.name = name
    }

    @JSGetter
    String name(){
        return name
    }
}

これらのクラスを使うコード

Document, Pages, Page を ScriptableObject.defineClass するだけです。

def script = '''\
    | // 省略:ここは最初に書いた Javascritp を定義.
'''.stripMargin('|')

def cx = Context.enter()
def scope = cx.initStandardObjects()

Object jsOut = Context.javaToJS(System.out, scope)
ScriptableObject.putProperty(scope, "out", jsOut)

ScriptableObject.defineClass(scope, Page.class)
ScriptableObject.defineClass(scope, Pages.class)
ScriptableObject.defineClass(scope, Document.class)

cx.evaluateString(scope, script, "<cmd>", 1, null)

Context.exit()

see also : Rhino を使って Java から JavaScript を実行する

DOMの構築は Java側で完了させておきたい場合

javascript 側では、単に 構築ずみの document インスタンスを使いたいだけ場合、つまり javascript はこれだけのコードにしたい:

var len = document.pages.length;
for(int i=0; i<len; i++){
    console.log( '- ' + document.pages[i].name );
}

このような場合、Java側で doucment インスタンスを構築して scope に存在させておく必要がある。

ScriptableObject.defineClass(scope, Page.class)
ScriptableObject.defineClass(scope, Pages.class)
ScriptableObject.defineClass(scope, Document.class)

def page0 = cx.newObject(scope, "Page", 'page-0')
def page1 = cx.newObject(scope, "Page", 'page-1')

def pages = cx.newObject(scope, "Pages")
pages.push(page0)
pages.push(page1)

def document = cx.newObject(scope, "Document", pages)
scope.put("document", scope, document)

まとめ

Rhinoなりのお作法があってそのドキュメントがあちこちに分散していたので、ここまで来るのが大変でした。 こんな簡単な DOM ではおよそ実用的ではないので、ここから先もまた道のりは長そうですが。