jQuery on Rhino の続き(知らぬは我が身ばかりなり)

昨日修正してみたenv-jsは、実は現在では thatcher/env-js の方がメインらしい。メーリングリストなどもありとても活発に開発されているようだ。

Resig氏のリポジトリだから本家だろうとそこで探索を打ち切ってしまったのが間違いだったようで……。ということでオレオレ修正は完全に無駄骨に終わった(DocumentFragmentのnodeTypeが11だ、とかどーでもいい知識以外は)。

jQuery on Rhino

唐突にサーバサイドJavaScriptがやってみたくなった。Mayaaとかがやっているように、Rhinoを使って、JavaでつくったWebアプリのビュー部分でJavaScriptを使うという計画。

JavaScriptでHTMLをいじるならjQueryしかないと個人的に思っているので、まず jQuery on Rhino を試してみようと考える。ちょっと検索してみると Bringing the Browser to the Server というjQueryの作者自身の記事があった。記事は2007年と古いけど、もう少し新しいバージョンがGitHubjeresig/env-js にあるようなので、こちらで試してみる。

まずは、簡単な要素の操作。上のリポジトリにある test/jquery.js (jQuery 1.2.6) ではきちんと動く。(defaultValue が出るのはご愛敬)

C:\tmp>java -jar rhino\js.jar
Rhino 1.7 release 1 2008 03 06
js> load('env-js/src/env.js')
js> window.location = 'index.html'
index.html
js> load('env-js/test/jquery.js')
js> $('p').text()
testです。
js> $('p').text('変更しました')
[object Object]
js> document.innerHTML
<HTML defaultValue=''>
<BODY defaultValue=''>
<P defaultValue=''>変更しました</P>
</BODY>
</HTML>

でも、やっぱり最新版だろ! とjQueryを1.3.2に更新すると、動かない。

C:\tmp>java -jar rhino\js.jar
Rhino 1.7 release 1 2008 03 06
js> load('env-js/src/env.js')
js> window.location = 'index.html'
index.html
js> load('jquery.js')
js: "jquery.js", line 2167: uncaught JavaScript runtime exception: TypeError: Cannot
 find function createComment in object Document.
        at jquery.js:2167
        at jquery.js:2161
        at jquery.js:1419
        at jquery.js:12
        at <stdin>:4

ということで、とりあえず完全かどうか知らないが text() だけは動くように env.js を修正してみる。変更点は、

  • DOMDocument に createComment, createDocumentFragment がないので追加。
  • DOMDocumentFragment のプロトタイプが無かったので追加。
  • childNodes, firstChild, lastChild, appendChild, insertBefore はelementに固有ではないはずなので DOMElement から DOMNode に移動。

という感じ。

--- C:/tmp/env-js/src/env.js	Sun Oct 12 08:26:22 2008
+++ C:/tmp/env-js/src/my-env.js	Mon Feb 23 00:03:54 2009
@@ -150,6 +150,12 @@
 			return makeNode( this._dom.createTextNode(
 				text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")) );
 		},
+		createComment: function(data){
+			return makeNode( this._dom.createComment(data) );
+		},
+		createDocumentFragment: function(){
+			return makeNode( this._dom.createDocumentFragment() );
+		},
 		createElement: function(name){
 			return makeNode( this._dom.createElement(name.toLowerCase()) );
 		},
@@ -306,9 +312,59 @@
 		},
 		get outerHTML(){
 			return this.nodeValue;
+		},
+		get childNodes(){
+			return new DOMNodeList( this._dom.getChildNodes() );
+		},
+		get firstChild(){
+			return makeNode( this._dom.getFirstChild() );
+		},
+		get lastChild(){
+			return makeNode( this._dom.getLastChild() );
+		},
+		appendChild: function(node){
+			return makeNode( this._dom.appendChild( node._dom ) );
+		},
+		insertBefore: function(node,before){
+			this._dom.insertBefore( node._dom, before ? before._dom : before );
+			
+			execScripts( node );
+			
+			function execScripts( node ) {
+				if ( node.nodeName == "SCRIPT" ) {
+					if ( !node.getAttribute("src") ) {
+						with (window) {
+							eval( node.textContent );
+						}
+					}
+				} else {
+					var scripts = node.getElementsByTagName("script");
+					for ( var i = 0; i < scripts.length; i++ ) {
+						execScripts( node );
+					}
+				}
+			}
+		},
+		removeChild: function(node){
+			return makeNode( this._dom.removeChild( node._dom ) );
 		}
 	};
 
+	// DOM Document Fragment
+
+	window.DOMDocumentFragment = function(fragment){
+	  this._dom = fragment;
+	};
+
+	DOMDocumentFragment.prototype = extend(new DOMNode(), {
+		get nodeType(){
+			return 11;
+		},
+		get nodeName() {
+			return "#document-fragment";
+		},
+	});
+
 	window.DOMComment = function(node){
 		this._dom = node;
 	};
@@ -514,40 +570,6 @@
 		removeAttribute: function(name){
 			this._dom.removeAttribute(name);
 		},
-		
-		get childNodes(){
-			return new DOMNodeList( this._dom.getChildNodes() );
-		},
-		get firstChild(){
-			return makeNode( this._dom.getFirstChild() );
-		},
-		get lastChild(){
-			return makeNode( this._dom.getLastChild() );
-		},
-		appendChild: function(node){
-			this._dom.appendChild( node._dom );
-		},
-		insertBefore: function(node,before){
-			this._dom.insertBefore( node._dom, before ? before._dom : before );
-			
-			execScripts( node );
-			
-			function execScripts( node ) {
-				if ( node.nodeName == "SCRIPT" ) {
-					if ( !node.getAttribute("src") ) {
-						eval.call( window, node.textContent );
-					}
-				} else {
-					var scripts = node.getElementsByTagName("script");
-					for ( var i = 0; i < scripts.length; i++ ) {
-						execScripts( node );
-					}
-				}
-			}
-		},
-		removeChild: function(node){
-			this._dom.removeChild( node._dom );
-		},
 
 		getElementsByTagName: DOMDocument.prototype.getElementsByTagName,
 		
@@ -620,6 +642,8 @@
 			if ( !obj_nodes.containsKey( node ) )
 				obj_nodes.put( node, node.getNodeType() == 1?
 					new DOMElement( node ) :
+					node.getNodeType() == 11 ?
+					new DOMDocumentFragment( node ) :
 					node.getNodeType() == 8 ?
 					new DOMComment( node ) :
 					new DOMNode( node ) );

超手抜きの、Javaへの組み込み。本当はjsファイルはプリコンパイルしたほうがWebアプリとかでは使い回せるからいいっぽい。

import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.Scriptable;

(略)
  ContextFactory factory = new ContextFactory();
  Context cx = factory.enterContext();
  try {
    Scriptable scope = cx.initStandardObjects();
    cx.evaluateReader(scope, new FileReader("/tmp/env-js/src/my-env.js"), "env.js", 1, null);
    cx.evaluateString(scope, "window.location = 'file:///C:/tmp/index.html';", "<load>", 1, null);
    cx.evaluateReader(scope, new FileReader("/tmp/jquery.js"), "jquery.js", 1, null);
    cx.evaluateString(scope, "$('p').text('変更されました');", "<test>", 1, null);
    Object result = cx.evaluateString(scope, "document.innerHTML;", "<output>", 1, null);
    System.out.println(result.toString());
  } finally {
    Context.exit();
  }

そういえば、現在のRhinoでは上のように ContextFactory#enterContext() を使うのが作法らしいんだけど、公式のチュートリアル では未だに Context.enter() のまま。さらにRhino自体、2008年3月からなんと1年近くも更新されていない!!

これってRhinoもう終わりフラグですか。

るびまゴルフ

http://jp.rubyist.net/magazine/?0025-RubiMaGolf
C# ばかりだったのでリハビリのためにチャレンジ。

Q1 (17文字)
puts $<.to_a.uniq
Q2 (23文字)
puts $<.read.split.uniq

厳密には STDIN のほうがいいのかな?

今回知ったこと

  • puts は配列を渡すと個々の要素を改行つきで出力してくれる。ただし改行で終わっている要素にさらに改行を追加することはない。
  • IO は Enumerable をインクルードしている。なので何も考えず文字列配列に近い操作が可能。 STDIN.grep(/pattern/) とか STDIN.map(&:upcase) (>=1.8.7)とか。

JScriptによるWMIの利用

Windowsで仕事用の(つまり、Rubyとかを勝手にインストールできないときの)スクリプトを書くときは大抵JScriptで書いているんだけど、世の中はVBScriptが主流らしく、サンプルコードとかが手に入りづらかったりする。最近もレジストリ操作をしようとして悩んでしまった。

たとえばWMIのレジストリ操作機能を使って、HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control キー配下の WaitToKillServiceTimeout の値を読みたいとする*1VBScriptなら、サンプルはいくらでも転がっているし、MSDNでもすぐ見つかる。では、JScriptではどうか?

レジストリを操作するための StdRegProv インスタンスは以下のように取得できる。

var oLocator = new ActiveXObject("WbemScripting.SWbemLocator");
var oService = oLocator.ConnectServer(".", "root\\default");
var oRegProv = oService.Get("StdRegProv");

で、StdRegProvクラスのドキュメント を見ると、GetStringValue という REG_SZ 型の値を取り出すためのメソッドがあるのがわかる。これを呼べばいいだろう。…と思いきや、このメソッドの定義は以下のようになっているのだった。

uint32 GetStringValue(
  [in]  uint32 hDefKey = 2147483650,
  [in]  string sSubKeyName,
  [in]  string sValueName,
 [out]  string sValue
);

見ての通り返値は数値で、メソッドが成功したかどうかを示す。肝心のレジストリの値は sValue というoutパラメータ(参照渡し)で返されるらしい。でも、JScript/JavaScriptにそんな機能あったっけ? JScriptのリファレンス を眺めてもそんな呼び出し方法はないし、適当に

var sValue;
oRegProv.GetStringValue(0x80000002, "SYSTEM\\CurrentControlSet\\Control",
  "WaitToKillServiceTimeout", out sValue);

とやっても当然だめ。outがなくてもだめ。「JScript 参照渡し」とか「JScript "call by reference"」で検索してもなかなか解決策は見つからず(探し方が悪いだけかも)。結局、Writing WMI Scripts in JScript という有益なページを見つけるまで、小一時間くらいかかってしまった…。

正解はこちら。なんと直接は呼び出せず、リフレクションのようなものを使わないといけないらしい。

var oMethod = oRegProv.Methods_.Item("GetStringValue");

var oInParam = oMethod.InParameters.SpawnInstance_();
oInParam.hDefKey = 0x80000002; // HKEY_LOCAL_MACHINE
oInParam.sSubKeyName = "SYSTEM\\CurrentControlSet\\Control";
oInParam.sValueName = "WaitToKillServiceTimeout";

var oOutParam = oRegProv.ExecMethod_(oMethod.Name, oInParam);

if (oOutParam.ReturnValue == 0) {
  WScript.Echo(oOutParam.sValue);
} else {
  WScript.Echo("Error! return value = " + oOutParam.ReturnValue);
}

sValue というプロパティ名は GetStringValue の場合で、たとえば GetDWORDValue なら uValue という名前になったりする*2

WMIやJScriptについては体系立てて勉強したことがないので、上の方法はこの世界では常識なのかもしれないけど、この情報量の少なさを体感してしまうとやっぱりVBScriptに乗り換えたほうがいいんだろうか……とちょっと思った。

*1:これだけならWshShellのRegReadメソッドを使った方が早い。本当はもっと別のことをやろうとした。

*2:入力パラメータ、出力パラメータとも、メソッドのリファレンスに載っている仮引数名と同じ?

RubyOSA で iTunes を操作

iTunesへの楽曲登録やトラック情報の入力を自動化したいと思って検索してみたら、Macだと RubyOSA というRubyのライブラリでiTunesを操作できるらしいので試してみた。環境は Tiger + Ruby 1.8.7

インストール

libxml-ruby が新しいと BUS Error とか言われて何もできないので、先に古いのを手動で入れる。

$ sudo gem install -v 0.3.8.4 libxml-ruby
$ sudo gem install rubyosa

私の場合はlibxml-rubyに依存しているのが無かったのでこれで良かったけど、新しいバージョンを入れる必要のある場合は この辺 の手順でRubyOSAのソースを書き換えないとだめかも。

初期化

基本になるのが OSA::ITunes::Application クラスで、これを作って使う。ただ、あらかじめ iTune が起動していないと何もできず、さらに run というiTunesを起動するらしきメソッド自体が「あらかじめiTunesが起動していないと呼び出せない」という意味不明な制限があるみたいなので、手抜きな回避コードを入れてます……。

require 'rubygems'
require 'rbosa'

OSA.utf8_strings = true

app = OSA.app('iTunes')
app.run rescue system 'open -a iTunes'

utf_strings は文字列をUTF8でエンコードするかどうかのフラグで、日本語の楽曲名などを扱うならたぶん必須。ほかにもいくつか設定があるらしい。

ソースの取得

ソースというのは「ライブラリ」「iPod」「音楽CD」みたいなデータ元で、Application からはまずこれを取得する。

# 名前の列挙
app.sources.each {|s| puts s.name}

# 「ライブラリ」を取得
library = app.sources.find {|s| s.kind == OSA::ITunes::ESRC::LIBRARY}

ESRC はソースの種類。

プレイリストの取得

ソースの中にさらに複数のプレイリストがある、という階層構造になっている模様。

# ライブラリのすべてのプレイリストを、
# プレイリスト中の曲の合計サイズで降順ソートして表示
library.playlists.sort {|a, b| b.size <=> a.size}.each {|p| puts p.name}

# ライブラリの中の「ライブラリ」プレイリストを取得
library_plist = library.library_playlists[0]

# podcast を最初から再生
podcast_plist = library.playlists.find {|p| 'Podcast' == p.name}
podcast_plist.play

iTunes自体にそんなに詳しくないので知らないが、library_playlists は必ず1つあるのかどうか不明。
あと、試していないけどプレイリストは name= メソッドで名前変更もできるので、間違って p.name = 'Podcast' とかやると悲惨なことになるかも。

楽曲の追加・編集
# 「ライブラリ」に追加
test_track = app.add('/Users/hoge/Desktop/test.mp3')

# 追加したトラックのアーティスト名とコメントを設定
test_track.artist = 'Artist Name'
test_track.comment = 'テストです。'

# 第2引数でプレイリストを指定すると、直接追加できる
# (ライブラリにも勝手に追加される)
test_plist = library.playlists.find {|p| 'テスト' == p.name}
app.add('/Users/hoge/Desktop/test2.mp3', test_plist)

まとめてインポートしたファイルにコメントを追記したりも簡単にできそう。

おわり
app.quit

安定なソートください

C#(.NET Framework) で地味に困ることのひとつが、標準のコレクションフレームワークのソートがJavaと違って安定でないこと。マージソートくらい自分で実装してみせろ!というMicrosoftの愛のムチを無視して、逃げに走る。

static void StableSort<T>(List<T> list, Comparison<T> comparison)
{
    List<KeyValuePair<Int32, T>> wrapped = new List<KeyValuePair<Int32, T>>(list.Count);
    for (Int32 i=0; i<list.Count; i++)
    {
        wrapped.Add(new KeyValuePair<Int32, T>(i, list[i]));
    }

    wrapped.Sort(delegate(KeyValuePair<Int32, T> x, KeyValuePair<Int32, T> y)
    {
        Int32 result = comparison(x.Value, y.Value);
        if (result == 0)
        {
            result = x.Key.CompareTo(y.Key);
        }
        return result;
    });

    for (Int32 i = 0; i < list.Count; i++)
    {
        list[i] = wrapped[i].Value;
    }
}

Windowsアプリケーションのテスト

そうか、Windowsアプリのウインドウとかコンポーネントも単なるクラスだから、大抵の内容なら普通にテストができるんだ…。Webアプリ脳なので何かSeleniumみたいなものが必要なのかと漠然と思い込んでた。

こんな適当なWindowsアプリケーションがあったら

using System;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Windows.Forms;

namespace WindowsApplication1
{
    // デザイナは省略
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        // "button1" という Button が押されたときのイベント
        private void button1_Click(object sender, EventArgs e)
        {
            // "textBox1" という TextBox に値を設定
            textBox1.Text = "ボタンが押されました";
        }

        // "textBox1" のテキストが変化したときのイベント
        private void textBox1_TextChanged(object sender, EventArgs e)
        {
            // "checkBox1" という CheckBox をトグル
            checkBox1.Checked = !checkBox1.Checked;
        }
    }
}

強引に内部を蹂躙。

using System;
using System.Reflection;
using System.Windows.Forms;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            WindowsApplication1.Form1 f = new WindowsApplication1.Form1();

            // コンポーネントを取得
            TextBox textBox1 = GetField(f, "textBox1") as TextBox;
            CheckBox checkBox1 = GetField(f, "checkBox1") as CheckBox;
            Console.WriteLine("最初の状態: TextBox={0}, CheckBox={1}", textBox1.Text, checkBox1.Checked);

            // ボタン押したときのイベントを起こす
            Button button1 = GetField(f, "button1") as Button;
            CallMethod(f, "button1_Click", button1, null);
            Console.WriteLine("ボタン押下後: TextBox={0}, CheckBox={1}", textBox1.Text, checkBox1.Checked);
        }

        static object CallMethod(object o, String name, params object[] args)
        {
            MethodInfo mi = o.GetType().GetMethod(name, BindingFlags.NonPublic | BindingFlags.Instance);
            return mi.Invoke(o, args);
        }

        static object GetField(object o, String name)
        {
            FieldInfo fi = o.GetType().GetField(name, BindingFlags.NonPublic | BindingFlags.Instance);
            return fi.GetValue(o);
        }
    }
}

こういうのをNUnitなりと組み合わせればテストできそうだけど、本当にこれでいいんだろうか。普通どうやるんだろう?