RhinoでJavaAPIを使う
ついに、私の使っているほぼすべての環境がJava1.6以上を使えるようになりました。
それと合わせて、Java SE 6で導入された"JSR-223 Scripting for the Java Platform"の機能を使う機会があったので、少しまとめてみました。
ちょっとした処理を書くのにJavaだと面倒。これまでは、主に某プロダクトに付属しているPerl5をスクリプト環境のメインとしておりました。副にMinGWのsh。
ある程度はPerlでも十分ですが、CPANも使えないし、ちょっと特殊なことをやろうとするとつまずいてしまいます。
いちばん欲しかったのは、APIだけJavaのを使って、コードはスクリプト言語で書ける環境です。
Java6に標準搭載されているJavaScript処理系のRhino(mozillaのサイト)は、まさにそれができてしまいます。それなりに軽いし、スクリプト環境としては申し分ありません。
それに加えて、最近またJavaScriptを触る機会が多かったので、うってつけでした。
以下のページを参考にさせていただきました。
JavaAPIの操作を中心に試していきます。
実行環境は、JDK1.6.0_29,Windows7Home(いずれも64bit版)です。上のリンクにあるjrunscript*1を使って、.jsファイルのプログラムを実行できます。
lsコマンドのようなプログラム"listfiles.js"を作ってみました。
- listfiles.js
// for Java ScriptEngine(Rhino) importPackage(java.lang) importPackage(java.io) importPackage(java.util) var JString = java.lang.String var toLong = Long.valueOf function printInfo(file) { System.out.printf("%s%s%s%s------ %8d %s %s%n", [file.isDirectory() ? "d" : "-", file.canRead() ? "r" : " ", file.canWrite() ? "w" : " ", file.canExecute() ? "x" : " ", toLong(file.length()), JString.format("%1$tF %1$tT", [toLong(file.lastModified())]), file.name]) } var paths = (arguments.length == 0) ? ((new File(".")).list() || []) : arguments for (var i = 0; i < paths.length; i++) printInfo(new File(paths[i]))
- 実行結果(Cygwin)
$ jrunscript -f listfiles.js drwx------ 4096 2012-06-15 19:53:43 test -rwx------ 2276 2012-06-11 23:37:20 A.java -rwx------ 1141 2012-06-15 19:53:43 listfiles.js -rwx------ 1818 2012-06-15 19:53:42 predef.js -rwx------ 1087 2012-06-15 19:53:42 swing.js -rwx------ 935 2012-06-15 19:53:43 unittest.js $ jrunscript -f listfiles.js *.js -rwx------ 1141 2012-06-15 19:53:43 listfiles.js -rwx------ 1818 2012-06-15 19:53:42 predef.js -rwx------ 1087 2012-06-15 19:53:42 swing.js -rwx------ 935 2012-06-15 19:53:43 unittest.js $
文末のセミコロンは意図的に無しにしています。
importPackageを使うと、JavaAPIのパッケージ単位のインポートができます。クラス単位のインポートはimportClassを使います。
StringクラスはJavaScriptの組み込み型Stringに隠されてしまうので、JScriptという別名にしています。
JavaAPIのうち、可変長引数のメソッドはArrayで渡せます。このとき注意しなければならないのが、整数型の値です。整数型の値は、そのまま渡すとJavaScriptの数値型とオートボクシングの組み合わせにより、メソッドにはDoubleとして渡されてしまいます。ここでは明示的にLong型に変換することで回避しています。
コマンドライン引数は、Array型の変数argumentsに格納されています。また、JavaAPIの戻り値の配列は、Array型に変換されます。
毎回、ユーティリティ関数をコピーしてファイル内に書くのはよろしくありません。初期処理とメイン処理を分けておいて、続けて実行してみます。
- predef.js
// for Java ScriptEngine(Rhino) importPackage(java.io) importPackage(java.util) var JString = java.lang.String function foreach(a, f) { if (a instanceof Collection) { var it = a.iterator() while (it.hasNext()) f(it.next()) } else { var i = 0 while (i < a.length) f(a[i++]) } } function grep(a, f) { var aa = new Array() foreach(a, function(o) { if (f(o)) aa.push(o) }) return aa } function map(a, f) { var aa = new Array() foreach(a, function(o) { aa.push(f(o)) }) return aa } function printf(fmt, args) { java.lang.System.out.printf(fmt, args) } function format(fmt, args) { return JString.format(fmt, args) }
- main.js
// for Java ScriptEngine(Rhino) var i = 0 foreach(arguments, function(s) { printf("argument[%d]: %s%n", [java.lang.Integer.valueOf(i++), s]) }) var numbers = grep(arguments, function(s) { return s.match(/^\d+$/) || s.match(/^\d+\.\d+$/) }) println("grep-numbers: " + numbers.join(", ")) var words = grep(arguments, function(s) { return s.match(/^\w+$/) && s.match(/^[^\d\.]+$/) }) println("grep-words: " + words.join(", ")) var ucs = map(arguments, function(s) { return s.toUpperCase() }) println("map(upper-case): " + ucs.join(", ")) Arrays.sort(arguments) println("sort: " + arguments.join(", "))
- 実行結果(Cygwin)
$ jrunscript -f predef.js -f main.js mustang 7 tiger 6 5.0 dolphin argument[0]: mustang argument[1]: 7 argument[2]: tiger argument[3]: 6 argument[4]: 5.0 argument[5]: dolphin grep-numbers: 7, 6, 5.0 grep-words: mustang, tiger, dolphin map(upper-case): MUSTANG, 7, TIGER, 6, 5.0, DOLPHIN sort: 5.0, 6, 7, dolphin, mustang, tiger $
foreach関数,grep関数,map関数他を作って、predef.jsに置きます。jrunscriptの-fオプションを続けて書けば、連続実行されます。"alias myjs='jrunscript -f predef.js -f'"みたいにしておくと、使いやすいのではないでしょうか。
Array型はjava.util.Arrays.sortでソートできます。
続いて、SwingのGUIプログラムを書いてみます。
- swing.js
// for Java ScriptEngine(Rhino) importPackage(java.awt) importPackage(java.awt.event) importPackage(javax.swing) with ( new JFrame() ) { title = "Java ScriptEngine Test" defaultCloseOperation = DISPOSE_ON_CLOSE setSize(400, 200) setLocationRelativeTo(null) setLayout(new FlowLayout()) add(function() { var b = new JButton(" GO ") with ( b ) { addActionListener(function(evt) { JOptionPane.showMessageDialog(null, "button clicked") }) addKeyListener(new KeyListener() { keyPressed: function(evt) { println("key pressed: " + evt.getKeyChar()); }, keyTyped: function(evt) {}, keyReleased: function(evt) {} }) } return b }()) visible = true }
- 実行結果(Cygwin)
$ jrunscript -f swing.js -f - js>
- 実行結果(GUI)
起動時の注意ですが、普通に実行すると、System.exit(0)で即時終了してしまいます。よって、最後にインタラクティブモードをつけて終了を阻止します。
ここでのポイントは以下のとおりです。
- JFrameのtitle,defaultCloseOperation,visibleのように、publicのgetter,setterがあるフィールドは、プロパティ的にアクセスできます。
- interfaceの無名クラスは作れますが、abstract-classの無名クラスは作れません。(KeyListenerでは作れるが、KeyAdapterでは作れない)
- ActionListenerのように、実装すべきメソッドが1つしかない場合は、名称を省略できます。そうでない場合はKeyListenerのようにします。
JUnitも使えます。
- unittest.js
// for Java ScriptEngine(Rhino) importPackage( Packages.junit.framework ) var JString = java.lang.String function assert(expected, script) { var msg = JString.format("testcode=[%s]", [script]) var assertEquals = Assert["assertEquals(java.lang.String,java.lang.String,java.lang.String)"] assertEquals( msg, expected, eval(script) ) } assert('cde', 'JString.valueOf("abcde").substring(2, 4)')
- 実行結果(Cygwin)
$ jrunscript -cp junit.jar -f unittest.js Exception in thread "main" junit.framework.ComparisonFailure: testcode=[JString.valueOf("abcde").substring(2, 4)] expected:<cd[e]> but was:<cd[]> at junit.framework.Assert.assertEquals(Assert.java:81) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at sun.reflect.misc.Trampoline.invoke(MethodUtil.java:37) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at sun.reflect.misc.MethodUtil.invoke(MethodUtil.java:244) at sun.org.mozilla.javascript.internal.MemberBox.invoke(MemberBox.java:132) at sun.org.mozilla.javascript.internal.NativeJavaMethod.call(NativeJavaMethod.java:190) at sun.org.mozilla.javascript.internal.Interpreter.interpretLoop(Interpreter.java:3073) at sun.org.mozilla.javascript.internal.Interpreter.interpret(Interpreter.java:2239) at sun.org.mozilla.javascript.internal.InterpretedFunction.call(InterpretedFunction.java:138) at sun.org.mozilla.javascript.internal.ContextFactory.doTopCall(ContextFactory.java:323) at com.sun.script.javascript.RhinoScriptEngine$1.superDoTopCall(RhinoScriptEngine.java:92) at com.sun.script.javascript.RhinoScriptEngine$1.doTopCall(RhinoScriptEngine.java:85) at sun.org.mozilla.javascript.internal.ScriptRuntime.doTopCall(ScriptRuntime.java:2747) at sun.org.mozilla.javascript.internal.InterpretedFunction.exec(InterpretedFunction.java:149) at sun.org.mozilla.javascript.internal.Context.evaluateReader(Context.java:1169) at com.sun.script.javascript.RhinoScriptEngine.eval(RhinoScriptEngine.java:149) at javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:232) at com.sun.tools.script.shell.Main.evaluateReader(Main.java:314) at com.sun.tools.script.shell.Main.evaluateStream(Main.java:350) at com.sun.tools.script.shell.Main.processSource(Main.java:267) at com.sun.tools.script.shell.Main.access$100(Main.java:19) at com.sun.tools.script.shell.Main$2.run(Main.java:182) at com.sun.tools.script.shell.Main.main(Main.java:30) $
junit.jarをCLASSPATHに設定する必要があります*2。
パッケージがjava,javaxで始まらないものは、識別子がパッケージ名として認識されないようなので、そういった場合は、先頭に"Packages."をつけるとパッケージとして扱われます。
オーバーロードの解決は自動的に行われますが、「曖昧」になる場合は、この例のassertEqualsのように明示的に選択して呼び出すことができます。
最後におまけ。API経由で実行する場合は、このようになります。
ScriptEngineManager factory = new ScriptEngineManager(); ScriptEngine engine = factory.getEngineByName("JavaScript"); try { engine.put("name", "argius"); engine.eval("println(name)"); // "argius" try { Reader r = new FileReader(scriptFile); try { engine.eval(r); } finally { r.close(); } } catch (IOException ex) { throw new RuntimeException(ex); } } catch (ScriptException ex) { throw new RuntimeException(ex); }