argius note

プログラミング関連

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]))
$ 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(", "))
$ 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
}
$ 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)')
$ 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);
}

*1:説明にも書いてありますが、jrunscriptは実験的(experimental)なツールなので、将来のJDKからは無くなるかもしれません。

*2:Eclipseのプロジェクト上でAPI経由で呼出した場合は、プロジェクトのビルドパスが使われるので、ビルドパスにJUnitを設定していれば使えます。