2012-09-29

Rhino1.7R4向けに、単一の関数をJavaイベントリスナに変換する関数を作ってみた

Rhino1.7R4で単一の関数をWindowLisenerの実装に使うとエラーが発生する件で、 対策案としてWindowLisenerの全てのメソッドを実装する例を挙げた。
が、7つもあるメソッドのうち主に使用するのはwindowClosingくらいなので、これは面倒くさい。
そこで、過去のRhinoと似た動きをさせるための関数を作ってみた。

1-1. 単一の関数をJavaイベントリスナに変換する関数(function-as-listener.js前半):
function newListener(listenerInterface, handlerFunc) {
    function getDispatcher(methodName) {
        return function(args) {
            var actualArgs = [];
            for (var i = 0; i < arguments.length; i++) {
                actualArgs.push(arguments[i]);
            }
            actualArgs.push(methodName);
            return handlerFunc.apply(this, actualArgs);
        };
    };
    var clazz = listenerInterface.__javaObject__;
    var methods = clazz.getMethods();
    var handlerObj = {};
    for (var i = 0; i < methods.length; i++) {
        var method = methods[i];
        handlerObj[method.getName()] = getDispatcher(String(method.getName()));
    }
    return new listenerInterface(handlerObj);
}

1-2. 使用例(function-as-listener.js後半):
var gui = new java.awt.Frame("function-as-listener");
gui.addWindowListener(newListener(
    java.awt.event.WindowListener,
    function(event, methodName){
        if (methodName === "windowClosing") {
            event.getWindow().setVisible(false);
            return;
        }
        java.lang.System.out.println(methodName);
    }
));
gui.setSize(200, 100);
gui.setVisible(true);
java.lang.Thread.currentThread().sleep(1000);
gui.dispose();
java.lang.System.out.println("ok");

2. Rhino1.7R4で実行する:
>java -classpath .;js.jar org.mozilla.javascript.tools.shell.Main -w -debug function-as-listener.js
windowActivated
windowOpened
ok
windowDeactivated
windowClosed

3. jrunscript(Java1.7)で実行する:
>jrunscript function-as-listener.js
windowActivated
windowOpened
windowDeactivated
windowClosed
ok

Java1.7のRhinoスクリプトエンジンはRhino1.7相当の機能を使用できる

jrunscriptに-qオプションを付けてRhinoのバージョンを確認したところ、 Java1.6の時点ではRhino1.6だったが、Java1.7からRhino1.7と表示されるようになった。
これはJava1.7付属のRhinoでようやくletを使えるということなので、 varで宣言した変数のスコープに悩まされてきた身としては大変ありがたい。
特にfor文でループ変数のスコープを最小限にするために、 いちいち (function(){})() で囲ってやるのは手間だった。 Rhino1.7相当なら、最初から for (let i = 0; i < count; i++) と書くことができる。

1. 実行するスクリプト(let.js):
var stdout = java.lang.System.out;

{
    let n = 2;
    for (let i = 0; i < n; i++) {
        stdout.print(" " + i);
    }
    stdout.println();
    stdout.println("i is " + typeof i);
}
stdout.println("n is " + typeof n);

2. 実行する:
>jrunscript let.js
 0 1
i is undefined
n is undefined

2012-09-25

Rhino1.7R4で同梱のSwingApplication.jsを実行するとエラーが起きる

JavaScript Functions as Java Interfaces の後半に書かれている「2つ以上のメソッドを持つインターフェースが必要な箇所に、 関数を1つだけ渡す(関数の最後の引数に呼ばれたメソッド名が設定されるので、実行時に分岐できる)」 という方法を使うとエラーが発生する。
Rhino1.7R3(Java1.7付属のRhinoも含む)までは発生しない。 Rhino1.7R4でInterfaceAdapter.javaの実装が以下のように変更されたことが原因。
  • 変更前: スクリプトで実装しようとしているJavaインターフェースが複数のメソッドを持つ場合、 引数の数とデータ型が全て一致していればOK(例えば、WindowListenerやKeyListenerのようなGUIイベントリスナ)
  • 変更後: スクリプトで実装しようとしているJavaインターフェースが複数のメソッドを持つ場合、 許されるのはオーバーロードだけ(名前が同じで引数の数とデータ型だけが異なるメソッド)
a-1. SwingApplication.jsを実行する:
>java -classpath .;js.jar org.mozilla.javascript.tools.shell.Main -w -debug SwingApplication.js
js: "SwingApplication.js", line 69: Cannot convert function to interface java.awt.event.WindowListener since it contains methods with different names
        at SwingApplication.js:69


b-1. 問題の実装方法を中心に記述したスクリプト(event-listener.js):
var gui = new java.awt.Frame("event-listener");
gui.addWindowListener(
    function(event, methodName) {
        if (methodName === "windowClosing") {
            event.getWindow().setVisible(false);
        }
    }
);
gui.setSize(200, 100);
gui.setVisible(true);
java.lang.Thread.currentThread().sleep(1000);
gui.dispose();
java.lang.System.out.println("ok");

b-2. Rhino1.7R4で実行する:
>java -classpath .;js.jar org.mozilla.javascript.tools.shell.Main -w -debug event-listener.js
js: "event-listener.js", line 3: Cannot convert function to interface java.awt.event.WindowListener since it contains methods with different names
        at event-listener.js:3

b-3. jrunscript(Java1.7、Rhino1.7R3 PRERELEASE相当)で実行する:
>jrunscript event-listener.js
ok

上記の動作が開発側の意図したものかどうか、New in Rhino 1.7R4に説明されていないので判断できない。
エラーの回避策としては、Rhino1.7R3を使い続けるのが簡単だが、 書き直すことが手間でないと感じるなら Implementing Java Interfaces で説明されているやり方に変更する方法も一応ある。

c-1. Javaインターフェースの全てのメソッドをスクリプトで実装する例:
var listener = new java.awt.event.WindowListener({
    windowClosing: function(event) { event.getWindow().dispose(); },
    windowClosed: function(event) { },
    windowIconified: function(event) { },
    windowOpened: function(event) { java.lang.System.out.println("opened"); },
    windowDeiconified: function(event) { },
    windowActivated: function(event) { java.lang.System.out.println("activated"); },
    windowDeactivated: function(event) { }
});

2012-09-21

importPackageを使った場合、Javaクラスは必要になってから取り込まれる

JavaパッケージをimportPackageやJavaImporterで取り込むと、 パッケージに属する全クラスが一度に取り込まれるのでは、 という勝手なイメージを抱いていた。が、実際はそのようなことはなかった。
以下のスクリプトで確認できる通り、パッケージを取り込んだ時点では、 クラスの取り込みは行われない。取り込んだパッケージに属するクラスの名前が指定された時に、 初めて対象のクラスが取り込まれる。
この仕組みの代償として、パッケージを取り込んだグローバルスコープやJavaImporterオブジェクトは、 for (var key in obj) でループした時点ではkeyに名前が渡されなかったとしても、 名前を指定してアクセスした時は値が返ってくる、という予測しにくい動きをするので、 複数のスクリプトファイルで作業している時は混乱を招くかもしれない。

1. 実行するスクリプト(java-importer.js):
var topLevel = this;
var stdout = java.lang.System.out;

// オブジェクトに定義された名前を出力する関数
function dump(obj) {
    var keys = [];
    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
            keys.push(key);
        }
    }
    stdout.println("[" + keys + "]");
}

// importPackageでパッケージを取り込む
(function() {
    importPackage(java.util);
    importClass(java.io.File);

    var list = new ArrayList();
    list.add(new File("."));
    list.add(new File("java-importer.js"));
    list.add(new File("js.jar"));
    stdout.println(list); // [., java-importer.js, js.jar]
})();
dump(topLevel); // [importer,dump,stdout,topLevel,File,ArrayList]

stdout.println("LinkedList" in topLevel); // true
stdout.println(!!topLevel.HashMap); // true
dump(topLevel); // [importer,dump,stdout,topLevel,File,ArrayList,LinkedList,HashMap]

// JavaImporterでパッケージを取り込む
var importer = JavaImporter(
    java.net,
    java.nio.charset.Charset
);
dump(importer); // [Charset]

stdout.println("URL" in importer); // true
stdout.println("" + importer.URLDecoder); // [JavaClass java.net.URLDecoder]
dump(importer); // [Charset,URL,URLDecoder]

2. 実行する:
>java -classpath .;js.jar org.mozilla.javascript.tools.shell.Main -w -debug java-importer.js
[., java-importer.js, js.jar]
[importer,dump,stdout,topLevel,File,ArrayList]
true
true
[importer,dump,stdout,topLevel,File,ArrayList,LinkedList,HashMap]
[Charset]
true
[JavaClass java.net.URLDecoder]
[Charset,URL,URLDecoder]

2012-09-20

Javaが提供する長いクラス名(FQCN)の代わりにJavaScript流のエイリアスを使う

Rhinoから利用するJavaクラスやJavaパッケージの名前が事前に分かっている場合、 ローカル変数をエイリアスにすることで短く書けるようになる。
特にこの方法は、エイリアスの変数名を工夫することで、 JavaScriptの組み込みオブジェクトと同じ名前のJavaクラスでも取り込めるメリットがある。
(importPackageやimportClassは便利だが、グローバルスコープに直接Javaクラス名を書き込むので、 例えばjava.lang.Stringを取り込むと、同じStringという名前を持つJavaScriptの組み込みオブジェクトが グローバルスコープから見えなくなってしまう)

1. 実行するスクリプト(java-class-name-alias.js):
var stdout = java.lang.System.out;

(function() {
    var JString = java.lang.String;
    var JMath = java.lang.Math;
    stdout.println(JString.format("%.0f, %.0f", JMath.PI, JMath.max(3.2, 6.4)));
})();

// スコープ外に影響を与えない
stdout.println("JString is " + typeof JString);
stdout.println("JMath is " + typeof JMath);

2. 実行する:
>java -classpath .;js.jar org.mozilla.javascript.tools.shell.Main -w -debug java-class-name-alias.js
3, 6
JString is undefined
JMath is undefined

JavaAdapterでJavaクラスを拡張する

スクリプトでJavaクラスを拡張したり複数のJavaインターフェースを実装したい場合は、 RhinoのJavaAdapterを利用できる。
ただし、JavaAdapterは実行時にJavaクラスを生成する仕組みなので、 環境によってはセキュリティや配布ファイルサイズを考慮して削除されている。
Javaランタイムに付属のRhinoは、上記理由でJavaAdapterが除かれているため、以下のスクリプトは実行できない。

1. 実行するスクリプト(java-adapter.js):
var stdout = java.lang.System.out;

var filter = new JavaAdapter(
    java.io.FilenameFilter,
    {
        accept: function(dir, name) {
            if (name.endsWith(".js")) {
                return true;
            }
            return false;
        }
    }
);

var dir = new java.io.File(".");
var fileNames = dir.list(filter);
stdout.println(java.util.Arrays.toString(fileNames)); // [java-adapter.js]

2. 実行する:
>java -classpath .;js.jar org.mozilla.javascript.tools.shell.Main -w -debug java-adapter.js
[java-adapter.js]

2012-09-19

Javaの可変長引数メソッドを呼び出す

Rhino1.6R6から、java.lang.System.printf()やjava.lang.String.format()を 自然な書き方で利用できるようになった。
ちなみに、jrunscriptの組み込み関数printfは、古いRhinoでも動くように 可変長引数ではなくオブジェクト配列を使用して実装されている。 (内部でargumentsの第2引数以降をjava.lang.Objectの配列にコピーしてから、 java.lang.System.printf()を呼び出す)

1. 実行するスクリプト(varargs.js):
java.lang.System.out.printf("%.1f, %.1f\n", [1.2, 2.4]);
java.lang.System.out.printf("%.1f, %.1f\n", 3.6, 4.8); // 可変長引数の代わりに配列も使える

2-1. Rhino1.7で実行する:
>java -classpath .;js.jar org.mozilla.javascript.tools.shell.Main -w -debug varargs.js
1.2, 2.4
3.6, 4.8

2-2. jrunscript(Java1.7)で実行すると、結果はRhino1.7と同じ:
>jrunscript varargs.js
1.2, 2.4
3.6, 4.8

条件付きcatchで必要な例外だけ処理する

Rhinoで通常のcatchを使用すると、 ファイルがなかったことを知らせるFileNotFoundExceptionだろうと、 スクリプトの記述ミスで起きたNullPointerExceptionだろうと、 何でもcatchしてしまう。
例外をcatchした後でif文を使って内容をチェックし、 目的外の例外オブジェクトだったら再びthrowする方法もあるが、 条件付きcatchを使用することで、もっと簡潔に記述できる。

1. 実行するスクリプト(catch-if.js):
var stdout = java.lang.System.out;
var fileName = "<no file>";
try {
    new java.io.FileInputStream(fileName); // FileNotFoundException
} catch (e if e.javaException instanceof java.io.FileNotFoundException) {
    stdout.println("file not found");
}
try {
    new java.lang.Class.forName(null); // NullPointerException
} catch (e if e.javaException instanceof java.lang.ClassNotFoundException) {
    stdout.println("class not found");
} catch (e) {
    stdout.println(e);
}

2-1. Rhino1.7で実行する:
>java -classpath .;js.jar org.mozilla.javascript.tools.shell.Main -w -debug catch-if.js
file not found
JavaException: java.lang.NullPointerException: null

2-2. jrunscript(Java1.7)で実行すると、結果はRhino1.7と同じ:
>jrunscript catch-if.js
file not found
JavaException: java.lang.NullPointerException: null

Rhinoでcatchした例外の種類を調べる

"catch (e)"した場合、スローされたオブジェクトがそのままeに設定されると思っていたのだが、 もう少し複雑だった。
  1. Javaで実装されたメソッドが例外をスローした場合、e.javaExceptionにJavaの例外オブジェクトが入っている
  2. スクリプトエラーの場合、e.rhinoExceptionにRhinoの例外オブジェクトが入っている
  3. スクリプトで明示的にthrowを実行した場合、eにスローされたオブジェクトそのものが入っている
1. 実行するスクリプト(exception-type.js):
var stdout = java.lang.System.out;
function catchAndDumpException(func) {
    try {
        func();
    } catch (e) {
        if (e.javaException) {
            stdout.println("e.javaException=" + e.javaException);
        } else if (e.rhinoException) {
            stdout.println("e.rhinoException=" + e.rhinoException);
        } else {
            stdout.println("e=" + e);
        }
    }
}
catchAndDumpException(function() {
    java.lang.Class.forName("UNKNOWN-CLASS"); // ClassNotFoundException
});
catchAndDumpException(function() {
    undefined.name; // EcmaError
});
catchAndDumpException(function() {
    throw new java.lang.IllegalArgumentException("Message");
});
catchAndDumpException(function() {
    throw "Error";
});

2-1. Rhino1.7で実行する:
>java -classpath .;js.jar org.mozilla.javascript.tools.shell.Main -w -debug exception-type.js
e.javaException=java.lang.ClassNotFoundException: UNKNOWN-CLASS
e.rhinoException=org.mozilla.javascript.EcmaError: TypeError: Cannot read property "name" from undefined (exception-type.js#22)
e=java.lang.IllegalArgumentException: Message
e=Error

2-2. jrunscript(Java1.7)で実行すると、結果はRhino1.7とほぼ同じ:
>jrunscript exception-type.js
e.javaException=java.lang.ClassNotFoundException: UNKNOWN-CLASS
e.rhinoException=sun.org.mozilla.javascript.internal.EcmaError: TypeError: Cannot read property "name" from undefined (exception-type.js#22)
e=java.lang.IllegalArgumentException: Message
e=Error

Rhinoでprintfやformatに整数フォーマット(%d)を使うとエラー

以下の記述はIllegalFormatConversionExceptionを引き起こす(大量のスタックトレースが出力されるので注意)。

a-1. エラーを引き起こすスクリプト:
java.lang.String.format("%d", [10]); // error
java.lang.System.out.printf("%d", [10]); // error

a-2. エラーメッセージ(抜粋):
org.mozilla.javascript.WrappedException: Wrapped java.util.IllegalFormatConversionException: d != java.lang.Double

数値(Number)をパラメータ配列に直接入れた結果、 整数フォーマット(%d)に対してjava.lang.Doubleが渡されてしまったようだ。
対策として、数値をjava.lang.Integer.valueOf()メソッドでjava.lang.Integerに変換してから パラメータ配列に入れることにする。

b-1. 実行するスクリプト(printf-format.js):
var stdout = java.lang.System.out;
var n = 10; // Number
try {
    stdout.println(java.lang.String.format("%d", [n])); // error
} catch (e) {
    stdout.println(e.message);
}
try {
    stdout.printf("%d\n", [n]); // error
} catch (e) {
    stdout.println(e.message);
}
stdout.println(java.lang.String.format("%d", [java.lang.Integer.valueOf(n)]));
stdout.printf("%d\n", [java.lang.Integer.valueOf(n)]);

b-2-1. Rhino1.7で実行する:
>java -classpath .;js.jar org.mozilla.javascript.tools.shell.Main -w -debug printf-format.js
java.util.IllegalFormatConversionException: d != java.lang.Double
java.util.IllegalFormatConversionException: d != java.lang.Double
10
10

b-2-2. jrunscript(Java1.7)で実行する:
>jrunscript printf-format.js
java.util.IllegalFormatConversionException: d != java.lang.Double
java.util.IllegalFormatConversionException: d != java.lang.Double
10
10

2012-09-17

最近のRhinoはjava.lang.System.inと書ける

Rhino1.7R4をダウンロードしてみたところ、 "in"というプロパティ名をドット記法で書けるようになっていた。
Rhino1.6R7やjrunscript(Java1.7)で同じ書き方をするとエラーになるので、 もし複数の環境に対応したスクリプトを書こうとしている場合は注意が必要だ。

1. 実行するスクリプト(system-in.js):
print(java.lang.System["in"]);
print(java.lang.System.in);

2-1. Rhino1.7R4で実行すると、プロパティ名がinでもエラーにならない:
>java -classpath .;js.jar org.mozilla.javascript.tools.shell.Main -w -debug system-in.js
java.io.BufferedInputStream@110b205
java.io.BufferedInputStream@110b205

2-2. Rhino1.6R7で実行した場合、プロパティ名inをドット記法で書いている箇所がエラーになる:
>java -classpath .;js.jar org.mozilla.javascript.tools.shell.Main -w -debug system-in.js
js: "system-in.js", line 2: missing name after . operator
js: print(java.lang.System.in);
js: .........................^
js: "system-in.js", line 1: Compilation produced 1 syntax errors.

2-3. jrunscript(Java1.7)で試した場合、Rhino1.6R7と同様のエラーが発生する:
>jrunscript system-in.js
script error in file system-in.js : sun.org.mozilla.javascript.internal.EvaluatorException: missing name after . operator (system-in.js#2) in system-in.js at line number 2

Rhino起動時の-classpathオプションに"."を入れ忘れてエラー

書き捨てスクリプトからカレントディレクトリのJavaクラスを呼び出そうとして、 うっかり-classpathにjs.jarだけ指定していたためにエラーになったことが何度かあった。
それ以来、Rhino起動時は、まず"."と"js.jar"をクラスパスに含めるようにしている。

1. Javaソースファイル(fruits/Fruit.java):
package fruits;
public class Fruit {
    public String name;
}

2. Javaクラスファイルを作成する(fruits/Fruit.class):
>javac fruits/Fruit.java

3. スクリプトファイル(fruit.js):
print(Packages.fruits.Fruit);
var fruit = new Packages.fruits.Fruit();
fruit.name = "banana";
print("fruit=" + fruit.name);

4-1. -classpathオプションに"."を入れ忘れた場合、指定されたJavaクラスが見つからないのでエラー:
>java -classpath js.jar org.mozilla.javascript.tools.shell.Main -w -debug fruit.js
[JavaPackage fruits.Fruit]
js: "fruit.js", line 2: uncaught JavaScript runtime exception: TypeError: [JavaPackage fruits.Fruit] is not a function, it is object.
        at fruit.js:2

4-2. -classpathオプションに"."を含めると、期待通りJavaクラスを使える:
>java -classpath .;js.jar org.mozilla.javascript.tools.shell.Main -w -debug fruit.js
[JavaClass fruits.Fruit]
fruit=banana

デバッグ向けの設定でRhino Shellを使う

-wオプションと-debugオプションを指定すると、 スクリプトに問題があった時に詳しいメッセージが出力されるので、よく使っている。

1. 実行するスクリプト(warning-and-error.js):
eval({}); // warning
a; // error

2-1. オプションなしの場合、警告メッセージやエラー発生位置が出力されない:
>java -classpath .;js.jar org.mozilla.javascript.tools.shell.Main warning-and-error.js
js: uncaught JavaScript runtime exception: ReferenceError: "a" is not defined.

2-2. -wオプションと-debugオプションを指定した場合、警告メッセージとエラー発生位置が出力される:
>java -classpath .;js.jar org.mozilla.javascript.tools.shell.Main -w -debug warning-and-error.js
js: warning: "warning-and-error.js", line 1: Calling eval() with anything other than a primitive string value will simply return the value. Is this what you intended?
js: "warning-and-error.js", line 2: uncaught JavaScript runtime exception: ReferenceError: "a" is not defined.
        at warning-and-error.js:2