はじめに
Java Specification Request (JSR) 223は、Javaプラットフォームとスクリプト言語を連係させるための一連のAPIと関連フレームワークを定義します。このAPIは、Java SE 6に標準装備されている標準ライブラリの一部であるため、SE6 JVM上でアプリケーションを実行する場合にはスクリプトサポートを無償で受けられます。これは、Eclipseプラットフォーム上で構築されたアプリケーションにもあてはまります。
JSR-223は、スクリプト言語とJavaプラットフォームとの間で行われるさまざまな対話を定義します。たとえば次のような対話があります。
- インタプリタ型スクリプトをJavaアプリケーションに埋め込む
- Javaオブジェクトをスクリプトコンテキスト内から変更およびコントロールする
- Java言語を使用してスクリプトインタプリタを書き、公開する
この記事では、Eclipseプラットフォームと、このプラットフォーム上で構築されるアプリケーションを、これらの新しいJava SE 6機能とスクリプト言語のメリットを利用して拡張する方法を紹介します。Eclipseプラットフォームをスクリプトで拡張する方法を覚えると、以下のことが可能になります。
- 統合開発環境内でよく行う繰り返しの操作を自動化する
- ユーザーインターフェイスとコントロールロジックの両方をその場で変更することにより、ユーザーインターフェイスのプロトタイプを短時間で作成する
- アプリケーションユーザーが一般的な環境設定の域を超えてアプリケーションをカスタマイズできるようにする。たとえばユーザーがお気に入りのスクリプトまたはドメイン固有言語(DSL)にロジックを追加できるようにする
これらの利点の多くは、スクリプト言語の性質に由来しています。スクリプト言語は、一般的には動的に型付けされ、場合によっては特定の問題分野に固有のものであることもあります。また、通常はコンパイラ型言語よりも読み書きが簡単で、JavaやCなどの古い言語のように「記述→コンパイル→実行」といったサイクルに縛られません。
前提条件
この記事は、JSR-223に関する基本的な知識のみがあれば理解できます。実際のところ、理解しておく必要があるのは次のコードだけです。
Map vars =
getScriptVariables(); // fictional method
String scriptBody = getScriptBody(); // fictional method
ScriptEngineManager sem = new ScriptEngineManager();
ScriptEngine engine = sem.getEngineByExtension("js");
for (String key : vars.keySet())
engine.put(key, vars.get(key));
engine.eval(scriptBody);
// ScriptException handling code omitted
このコードを実行すると、基本的には次の3つの処理が行われます。
- javax.script.ScriptEngineManagerを作成する(サポートされている拡張で検索)
このjavax.script.ScriptEngineManagerは、スクリプトエンジンを見つけてインスタンス化します。スクリプトエンジンとは、スクリプトを評価して、効率よく実行するコンポーネントのことです。
ScriptEngine.put(String,Object)メソッドで、一連のスクリプト変数を設定する
これにより、Javaオブジェクトとスクリプト環境間のバインディングが定義され、このようなオブジェクトをスクリプトで処理できるようになります。一般的には、Java環境をスクリプトでコントロールできます。
- 指定されたスクリプトを、
eval(String script)メソッドで評価する
JVMは、Service Providerメカニズムを使って使用可能なエンジンを探します。このとき、アプリケーションで使用できるjarファイルのMETA-INF/サービスディレクトリをスキャンして、特定の構成ファイルを検索します。スクリプトエンジンをアプリケーションで使用できるようにするには、そのスクリプトエンジンを含む、正しく構成されたjarファイルをクラスパスに追加します。この記事では、dev.java.netのスクリプトプロジェクトで提供されているスクリプトエンジンのいくつか、たとえばRubyやGroovyのエンジンを使用します。
今回のサンプルではスクリプトAPIの使用をかなり抑えていますが、チュートリアルとしての目的は十分に果たしているのではないかと思います。詳細については、最後に紹介する参考資料を参照してください。
スクリプトプラグインとそのフラグメント
この記事のサンプルコードでは、スクリプト言語とEclipse(補足説明1「EclipseのRich Client Platform」を参照。補足説明はこの記事の最後にあります)を、com.devx.scriptingプラグイン(補足説明2「Eclipseプラグインのしくみ」を参照)を介して接続しています。これにより、プラットフォームの他の部分が、スクリプトリソースと一連のプラグインフラグメントに共通アクセスできるようになります。図1で示すように、これは、アプリケーションがサポートするすべてのスクリプト言語に対応します。
図1 com.devx.scriptingプラグインアーキテクチャ
すべてのフラグメントは指定のインタプリタ(Ruby、JavaScript、AppleScriptなど)とそのJSR-223エンジンを提供しており、そのインタプリタをjavax.scriptスクリプトAPIを通じて公開します。インタプリタとJSR-223ラッパーは、セットにすることも、個別に開発して提供することもできます。
このプラグインセットアップにはさまざまなメリットがあり、たとえば次のようなことが考えられます。
- 分散フラグメントの数を制限し、プラットフォームに追加する言語を明確にコントロールできる。
- ユーザーが必要なスクリプト言語のみを更新サイトから選択できるため、アプリケーションのインストールに伴う自分の作業を軽減できる。
プラグインは次のようなIScriptインターフェイスを定義します。
public interface IScript {
// @return the URI which points to the script code.
public String getURI();
// @return the script extension
public String getExtension();
// @return a Reader which points to the script code
public Reader getReader() throws IOException;
// @return the script unique id
public String getId();
// @return the namespace (plug-in id) of the script
public String getNamespace();
// @return run the script in a modal context?
public boolean isModal();
}
スクリプトプラグインはcom.devx.scripting.ScriptSupportクラスを公開し、Eclipseプラットフォームで発生するスクリプト関連の一般的なニーズに対応するためにパブリックメソッドを定義します。一般的なニーズとしては、たとえば、進捗状況のモニタ(Eclipseでソースをコンパイルしているときに表示されるモニタなど)に照らしてスクリプトを実行する、ScriptEngineManagerに対してクエリを実行してサポート言語の一覧を取得する、などの処理が考えられます。次のコードは、クラスのパブリックインターフェイスの一部を示しています(実装についてはダウンロードサンプル中のソースコードを参照)。
public void runScript(final IScript script,
Map params) throws ScriptException;
public List getSupportedLanguages();
外部スクリプトの実行
これらの基本エレメントだけで、既にEclipseアプリケーション内でカスタムスクリプトを実行する手段を実現できます。たとえば、ユーザーがファイルシステムからスクリプトを選択し、プラットフォーム内で実行するためのEclipseアクションを提供できます。図2と図3は、これを実装した場合の画面例です。
図3 サポートされているすべての種類のスクリプトを選択できるファイルセレクタ
「Run Script」アクションは、ファイルセレクタを表示します。このファイルセレクタは、com.devx.scripting.ScriptSupportクラスに対してクエリを実行することで、使用可能なスクリプト言語のみをフィルタを使って抽出します。クエリを受け取ったcom.devx.scripting.ScriptSupportクラスは、javax.script.ScriptEngineManagerに対してサポート言語を要求します。最後に、javax.script.ScriptEngineManagerが、Service Providerメカニズムを使ってプラグインクラスパスとそのフラグメントをスキャンします。
図のような結果を得るためには、org.eclipse.ui.actionSets拡張ポイントに対して拡張を定義する必要があります。この拡張ポイントは、リスト1のcom.devx.scripting.actions.RunScriptActionクラスによって実装されている、アプリケーションに対する追加メニューアクションを提供します。
必要な作業はこれだけです。これで、アプリケーション内でRuby、Groovyなどのスクリプトを実行して、それぞれのスクリプトの性能や特性を利用することができます。たとえば、ワークスペースの一部で一括変更を行うスクリプトを作成したり、ビルドおよび配備プロセスの一部をスクリプト言語で書いて、必要に応じて開発者がプラットフォームから呼び出せるようにしたりできます。
リスト1 RunScriptActionクラス
public class RunScriptAction
implements IWorkbenchWindowActionDelegate {
// unneeded methods
public void dispose() {}
public void init(IWorkbenchWindow window) {}
public void selectionChanged(IAction action,
ISelection selection) {}
public void run(IAction action) {
FileDialog dlg = new FileDialog(
PlatformUI.getWorkbench().getActiveWorkbenchWindow().
getShell(), SWT.OPEN);
List filterNames = new ArrayList();
List filterExtensions = new ArrayList();
ScriptSupport support = new ScriptSupport();
for (ScriptSupport.Language l :
support.getSupportedLanguages()) {
filterNames.add(
l.getName() + " (*." + l.getExtension().get(0) + ")");
filterExtensions.add("*." +l.getExtension().get(0));
}
filterNames.add("All files (*.*)");
filterExtensions.add("*.*");
dlg.setFilterNames(
filterNames.toArray(new String[filterNames.size()]));
dlg.setFilterExtensions(
filterExtensions.toArray(
new String[filterExtensions.size()]));
String f = dlg.open();
if (f != null) {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(f));
support.runScript(br, getExtension(f),null,true );
}
catch(IOException ioe) {
// exception handling
}
catch(ScriptException se) {
// exception handling
}
finally {
if (br != null)
try { br.close(); }
catch(Exception ex) {}
}
}
}
private String getExtension(String f) {
if (f.lastIndexOf(’.’) != -1) {
return f.substring(f.lastIndexOf(’.’)+1);
}
else
return f;
}
}
プラットフォームでスクリプトコントリビューションを活用する
これで、次の段階に進む準備ができました。今度は、スクリプト言語を使用してEclipseプラットフォームを直接操作して変更する方法、そしてスクリプトを使ってプラットフォームにコントリビューションを追加する方法を見ていきましょう。まずは、スクリプトコントリビューションとプラットフォームとの間を結ぶバインディングレイヤーの定義が必要です。このレイヤーには、以下のものが含まれます。
- 標準のビューコントリビューションの代わりとなる、標準のEclipse拡張ポイントを模倣する拡張ポイント(scriptedViewなど)。スクリプトから返されるEclipseビューを提供するために必要。
- org.eclipse.ui.part.ViewPartなどの標準Eclipseインターフェイスを実装し、メソッド呼び出しを基幹スクリプトに委任するプロキシクラス。
- org.eclipse.ui.startup拡張ポイントへの拡張。スタートアップ時にスクリプトコントリビューションとプラットフォームとの間のすべてのプラミングとバインディングを行う。
JavaScript実装に支えられているEclipseビューの例を見てみます。図4は、この例のサイクル全体を表しています。これを見ると、構成/スタートアップ時に実行されるアクションと、実行時に実行されるアクションの違いがわかります(Eclipseアクションセットなど、他の種類のコントリビューションについては、サンプルコードを参照してください)。
図4 スクリプトコントリビューションの処理サイクル
まず、次のコードを使用して、スクリプトコントリビューションを定義します。
<plugin>
<extension
point="com.devx.scripting.scriptedContribution">
<scriptedView
allowMultiple="false"
id="com.devx.scripting.jsCalculator
name="JavaScript Calculator">
<script
extension="js"
id="com.devx.scripting.jsCalculator.script"
uri="scripts/jsCalculator.js">
script>
scriptedView>
extension>
plugin>
この拡張は標準のorg.eclipse.ui.viewsと非常によく似ています。唯一異なるのは追加のエレメントです。これは基幹スクリプトを定義します(uri属性は、プラグイン内のリソースまたは外部リソースを指定できます。プラグイン内のリソースの場合は関連URIを、外部リソースの場合はfile://スキームを使用します)。アプリケーションの起動時に、org.eclipse.ui.startup拡張ポイントとして登録されているcom.devx.scripting.ScriptingStartupクラスは、使用可能なスクリプトコントリビューションをスキャンし、これらのコントリビューションをIExtensionRegistry.addContribution()メソッドで動的にプラットフォームに追加します。なお、動的コントリビューションには特別な許可が必要です。この許可は、アプリケーション起動時に-Declipse.registry.nulltoken=trueコマンドラインを追加することで付与します。
ScriptingStartupクラスは、スクリプトコントリビューションと、スクリプトのプロキシとして動作するコントリビューションとの間で変換を行います。リスト2は、変換プロセスに関連するgetContribution()メソッドです。
おわかりのように、このコードはスクリプトコントリビューションをコピーして、それを標準のorg.eclipse.ui.viewsに変換するだけです。ビュー実装クラスであるcom.devx.scripting.view.ScriptProxyViewは、Eclipseプラットフォームによるビューへの呼び出しを基幹スクリプトに委任します。たとえば、次のコードは、スクリプトへのレンダリングプロセスの委任を示しており、これは、EclipseがcreatePartControl(Composite parent)を呼び出してビューを描画するときに実行されます。
scriptParent = new Composite(parent,SWT.NONE);
scriptParent.setLayoutData(new
GridData(GridData.FILL_BOTH));
scriptParent.setLayout(new FillLayout(SWT.HORIZONTAL));
// this call returns the script associated with this view
IScript script = getScript();
Map params = new HashMap();
params.put("parent", scriptParent);
new ScriptSupport().runScript(script, params);
これにより、スクリプトに対する変更がすぐに反映されるため(ビューを閉じて開くだけで確認できます)、開発者はユーザーインターフェイスのプロトタイプを短時間で作成できるようになります。
JavaScriptを使った計算機のサンプル
これで、スクリプト言語のメリットを利用して、従来のJavaコードでは実現に手間がかかっていた機能も実現できるようになります。たとえば、単純な計算機なども実現可能です。JavaScriptとそのeval()関数を使用すれば、ユーザーからサブミットされた数式を評価するためのコードを書かずにすみます。図5に、実際の表示例を示します。リスト3はそれを作成するJavaScriptコードです。
リフレッシュボタンを押すとスクリプトが再度読み込まれ、変更が即座にUIに反映されます。
スクリプト言語は、他にも多数のシナリオでEclipse内でインターフェイスを描画するのに役立ちます。たとえば、Groovyを使用してSWTインターフェイスを作成したり、このRubyライブラリを使用してRubyでSWTインターフェイスを作成することで、ビルダパラダイムを利用できます。
リスト2 ScriptingStartupから呼び出される^^getContribution()^^メソッド
public String getContribution(IConfigurationElement el) {
StringBuilder sb = new StringBuilder();
sb.append("");
sb.append("");
sb.append(");
appendAttribute(sb, el, "allowMultiple");
appendAttribute(sb, el, "category");
appendAttribute(sb, el, "name");
appendAttribute(sb, el, "icon");
appendAttribute(sb, el, "fastViewWidthRatio");
// Id attribute
sb.append(" id="").
append(getContributionId(el)).append("" ");
// Class attribute
sb.append(" class=""
sb.appnend("com.devx.scripting.view.ScriptProxyView"");
sb.append("/>");
sb.append("");
sb.append("");
return sb.toString();
}
public String getContributionId(IConfigurationElement el) {
return el.getAttribute("id") + ".autoView";
}
private void appendAttribute(
StringBuilder sb,
IConfigurationElement el,
String attribute) {
if (el.getAttribute(attribute) != null)
sb.append(" ").
append(attribute).
append("="").
append(el.getAttribute(attribute)).
append("" ");
}
リスト3 JavaScript計算機
importPackage(org.eclipse.ui);
importPackage(org.eclipse.swt);
importPackage(org.eclipse.swt.widgets);
importPackage(org.eclipse.swt.events);
importPackage(org.eclipse.swt.layout);
c = new Composite(parent,SWT.NONE);
c.setLayout(new GridLayout(1,true));
t = new Text(c,SWT.BORDER);
t.layoutData = new GridData(GridData.FILL_HORIZONTAL);
t.editable = false;
buttonArea = new Composite(c,SWT.NONE);
buttonArea.setLayout(new GridLayout(4,true));
buttonArea.setLayoutData(new GridData(GridData.FILL_BOTH));
var labels = new Array(
"Clr","Bck","(",")",
"7","8","9","/",
"4","5","6","*",
"1","2","3","-",
"0",".","=","+");
keylistener = {
widgetSelected: function(event) {
t.text += event.widget.text } ,
widgetDefaultSelected: function(event) { }
}
evallistener = {
widgetSelected: function(event) {
t.text = eval("res = " + t.text) } ,
widgetDefaultSelected: function(event) { }
}
clearlistener = {
widgetSelected: function(event) { t.text = "" } ,
widgetDefaultSelected: function(event) { }
}
backlistener = {
widgetSelected: function(event) {
s = "" + t.text; // conversion to javascript string
t.text = s.substring(0,s.length-1);
},
widgetDefaultSelected: function(event) { }
}
for (i = 0; i < labels.length; i++ ) {
b = new Button(buttonArea, SWT.BORDER);
b.layoutData = new GridData(GridData.FILL_BOTH);
b.text = labels[i];
switch(labels[i]) {
case "Clr" :
b.addSelectionListener(
new SelectionListener(this.clearlistener));
break;
case "Bck" :
b.addSelectionListener(
new SelectionListener(this.backlistener));
break;
case "=" :
b.addSelectionListener(
new SelectionListener(this.evallistener));
break;
default:
b.addSelectionListener(
new SelectionListener(this.keylistener));
}
}
Eclipseとスクリプト言語との完全統合に向けて
以上の説明で、Javaプラットフォームが提供する新しいスクリプト機能とEclipseとの統合がいかに簡単であるかという点についてはおわかりいただけたのではないでしょうか。これで、スクリプトエンジンをEclipseプラグインアーキテクチャに組み込んで、Eclipse環境からスクリプトを実行できます。また、Eclipseプラットフォームのまさに核となる部分でスクリプト言語を使用して、ビューやアクションセットなど、標準の拡張ポイントへの拡張を提供する方法も説明しました。
ここで説明した以外にも、Eclipse MonkeyやEclipseShellなどのプロジェクトが、Eclipseプラットフォームに対してスクリプトサポートを提供しています。これを書いている時点では、この記事の一番の目的であるJSR-223をサポートしていなかったため触れていませんが、両方とも興味深い内容なので、その一部をサンプルコードで使用しました(Monkey Domなど。Eclipse拡張を使ってカスタムオブジェクトをスクリプトに提供しています)。少し時間を割いて見てみてもいいでしょう。
補足説明1 EclipseのRich Client Platform
EclipseのRich Client Platform(RCP)は、アプリケーション開発のための完全に整備された基盤を提供するという目的で選び出されたEclipseプラットフォームのコアコンポーネントの集まりです。RCPを使用すると、設定管理、更新管理、GUIエレメントなど、すべてのアプリケーションに共通の機能を手動でプログラムする必要がなくなり、アプリケーション固有のロジックに集中できます。
補足説明2 Eclipseプラグインのしくみ
Eclipseプラットフォームのプラグインは、このプラットフォームに機能を追加するためのもので、プロプライエタリなプラグインとユーザー提供のプラグインの両方があり、メニュー、ツールバー、ビュー、エディタ、ウィザード、基幹サービスなど、あらゆる形式のプラグインが使われています。プラグインは、他のプラグインに対して拡張を宣言することで、そのプラグインに依存できます。また、他のプラグインに対して拡張ポイントを公開することもできます。この観点から見ると、Eclipseというプラットフォームは、それ全体が一連の相互接続された通信プラグインにすぎません。具体的に説明すれば、プラグインは、「plugin.xml」記述子ファイルを使用して依存と拡張ポイントを宣言しているただのjarファイルです。プラグインには、ゼロ以上のフラグメントを含めることができます。プラグインフラグメントは、そのターゲットプラグインに対して追加機能を提供します。実行時には、これらのフラグメントがターゲットプラグインにマージされます。フラグメントを使用すると、プラグインの一部をオプションにすることができ、プラグインを部分的にインストールしたり、アンインストールしたり、更新したりすることが可能になります。
参考資料
2003年からJ2EE開発者としてイタリアの金融サービス会社に勤務。ソフトウェアアーキテクトとして従来の銀行システムのWebフロントエンドの設計に従事。物理学修士。SCJP(Sun Certified Java Programmer)資格を持つ。