Eclipse およびそのプラグインは JVM 上で動きます。従って、JVM 上で動く言語であれば、それを使ってプラグインを作成し、Eclipse を拡張することができます。たとえば 2012年 3月時点では、Closure や Scala などで、Eclipse のプラグインを開発可能です。まず Java で書かれたプラグインのソースコードを Scala で記述し直すとどのようになるのか見てみましょう。
たとえば、Eclipse で利用される仕組みとして、IAdaptable というものがあります。これはあるクラスが直接用途に沿ったインタフェースを持っていなくとも、適切なインタフェースを持ったインスタンスを返す必要がある場合に有効な仕組みです。Eclipse.org には Eclipse 上のリソースを削除する DeleteResourceAction を例に解説したドキュメントがあり、Eclipse.org の Wiki である Eclipsepedia で “IAdaptable” を検索すると見つかります。
この IAdaptable インタフェースのメソッド getAdapter を Java で記述すると、リスト 1 のように記述できるでしょう。
リスト 1. getAdapter
@Override
public Object getAdapter(Class adapter) {
if (adapter == IFile.class) {
// do something
} else if (adapter == IResource.class) {
// do something
}
// do something...
return null;
}
|
これを Scala で記述しなおすと、たとえば次のリスト 2 のように書くことができます
リスト 2. getAdapter
def getAdapter(adapter: Class[_]): Object = {
adapter match {
case cls if cls == classOf[IFile] => // do something
case cls if cls == classOf[IResource] => // do something
case _ => null
}
// do something...
null
}
|
また一般的に、validation の結果を、リターンコードとメッセージの二つで取得したい場合、通常の Java ではコンテナクラスを用意する方法が考えられます。 (リスト 3)
リスト 3. コンテナクラス
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.swt.widgets.Shell;
public class SampleClass {
private class ResultContainer {
private boolean success;
private String message;
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
private ResultContainer validate(String str) {
ResultContainer result = new ResultContainer();
if (str == null) {
result.setSuccess(false);
result.setMessage("null string");
} else if (str.equals("")) {
result.setSuccess(false);
result.setMessage("empty string");
} else if (str.contains("@")) {
result.setSuccess(false);
result.setMessage("invalid charactor");
} else {
result.setSuccess(true);
result.setMessage("");
}
return result;
}
private void showResult(ResultContainer result, Shell parent) {
if (result == null) {
MessageDialog.openError(parent, "validation", "unknown error");
} else if (result.isSuccess()) {
MessageDialog.openInformation(parent, "validation", "valid");
} else if (!result.isSuccess()) {
MessageDialog.openError(parent, "validation", result.getMessage());
}
}
}
|
これに対して、Scala であれば、タプルをそのインタフェースに利用することで、インナークラスを用意しなくともリターンコードとメッセージの二つを渡すことが可能です。 (リスト 4)
リスト 4. タプル
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.swt.widgets.Shell;
class SampleClass {
def validate(str: String): (Boolean, String) = {
str match {
case "" => (false, "empty string")
case null => (false, "null string")
case x if x.contains("@") => (false, "invalid charactor")
case _ => (true, "")
}
}
def showResult(t: (Boolean, String), parent : Shell): Unit = {
t match {
case (true, _) => MessageDialog.openInformation(parent, "validation", "valid")
case (false, mes) => MessageDialog.openError(parent, "validation", mes)
case _ => MessageDialog.openError(parent, "validation", "unknown")
}
}
}
|
Scala を利用することで、Eclipse のプラグインをよりシンプルなコードで記述できることがおわかりいただけたと思います。それではこれから実際に開発環境をセットアップして Scala でプラグインを作ってみましょう。
Eclipse 環境で Scala を開発するには、Scala IDE プラグインが有用です。Scala IDE のサイトによると、バージョン 2.0 では以下のような機能を有しており、Scala 開発において有効な手段となることでしょう。
- Scala と Java のコードがミックスされたプロジェクトを扱うことができます
- Scala のクラスやオブジェクトの生成ウィザード
- コード補間
- 変数やメソッドの定義場所へのジャンプ
- コードフォーマット
- リファクタリング
- デバッガ
…
Eclipse のプラグインが JVM 上で動くという事は前述した通りです。このため JVM で動く言語でプラグインを記述することが可能で、実際にこの Scala 開発効率化のための Scala IDE のプラグイン自体もまた、Scala で記述されています。
次節から簡単なプラグインの作成例を通して、Scala による Eclipse のプラグイン作成の流れを見ていきましょう。作成するプラグインは、Eclipse のリソースに対するアクションを追加し、ワークスペース内のプロジェクトのリソースを右クリックしてメニューからアクションを起動すると、選択したリソースの名前をメッセージダイアログで表示するというものです。
Eclipse 上で Scala のプログラムを作成するのには前述の Scala IDE を使うのが効率的です。Scala IDE は、Scala IDE のサイトからダウンロード可能です。2012年 2月の時点では、Scala IDE 2.0 が入手可能です。Scala IDE がサポートしているバージョンにあわせて、Eclipse を導入します。
Eclipse のサイトより PDE をダウンロードして好きな場所に展開しましょう。尚、本記事では、Eclipse 3.6.2 を使用します。
Eclipse のメニューより、「ヘルプ」->「新規ソフトウェアのインストール」を選択します。出てくるダイアログに、先ほどの Scala IDE にあるアップデートサイトの URL を入力して、インストールを行います。本記事では、2.0 を使用します。
Scala IDE を導入した Eclipse のワークスペースで、プラグインを作成していきます。まず通常の Java をベースにしたプラグイン・プロジェクトを作成して、それを元に Scala のコードをコンパイルできるようにしていきます。
新規プロジェクトより、プラグイン・プロジェクトを選択します。プラグインの名前やバージョンなどの情報を入力して「次へ」を選択します。 (図 1)
図 1. 新規プラグイン・プロジェクト
テンプレートの選択で、“Plug-in with a popup menu” を選択して、次のページへ進みます。 (図 2)
図 2. テンプレートの選択
ポップアップメニューが出てくるクラスの種類や、メニューのラベル、そしてメニューから起動されるアクションのクラスを設定して、雛形の作成は終了です。 (図 3)
図 3. ポップアップメニューの設定
パッケージ・エクスプローラーに次のような構成で、プロジェクトが生成されました。ここでは、ポップアップメニューを選択したときに呼び出されるアクションクラスは SampleAction.java として、sample.popup.actions パッケージに入っています。 (図 4)
図 4. パッケージ・エクスプローラー
この時点では、コードはすべて Java で記述されていますので、ここから徐々に Scala を受け付けるように変更していきます。
プロジェクトの設定は、プロジェクトの直下にある「.project」ファイルに記述されています。この中に、Scala のネイチャーとビルダーを足すことで、Scala のコードをビルドすることが可能になります。
まずは、メニューよりナビゲーター・ビューを開いて、「.project」ファイルに、Scala IDE の提供するビルドコマンドとネイチャーを追加します。 (リスト 5)
リスト 5. .project ファイル
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>local.test.scala</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.pde.ManifestBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.pde.SchemaBuilder</name>
<arguments>
</arguments>
</buildCommand>
<!-- add scala build command -->
<buildCommand>
<name>org.scala-ide.sdt.core.scalabuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.pde.PluginNature</nature>
<!-- add scala nature -->
<nature>org.scala-ide.sdt.core.scalanature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>
|
次は、プロジェクトのプロパティを開いて、Builders を設定します。「.project」ファイルを編集したため、ビルダーに「Scala Builder」が追加されていることを確認し、Java のビルダーのチェックをはずします。これは、Scala に先んじて Java のシンタックスチェックが走るのを防ぐためです。 (図 5)
図 5. ビルダーの設定
次に Java のビルドパスを設定します。作成された状態では、Java の標準ライブラリの他に Eclipse のプラグインに必要なライブラリが設定されています。ここに Scala のライブラリを追加します。(図6)
図 6. ライブラリの設定
これで Scala のコードを書く準備は整いました。
では、プラグインのクラスを Scala にしていきましょう。ここでは、ポップアップメニューから選んだときに呼び出されるアクションクラスを Scala 化します。先ほどの、sample.popup.actions パッケージにある、SampleAction の拡張子を「java」から「scala」に変更します。拡張子の変更は、ナビゲーター・ビューで行うことができます。
次は、Eclipse によって生成された java のアクションクラスのコードです。 (リスト 6)
リスト 6. アクションクラス
package sample.popup.actions;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IObjectActionDelegate;
import org.eclipse.ui.IWorkbenchPart;
public class SampleAction implements IObjectActionDelegate {
private Shell shell;
/**
* Constructor for Action1.
*/
public SampleAction() {
super();
}
/**
* @see IObjectActionDelegate#setActivePart(IAction, IWorkbenchPart)
*/
public void setActivePart(IAction action, IWorkbenchPart targetPart) {
shell = targetPart.getSite().getShell();
}
/**
* @see IActionDelegate#run(IAction)
*/
public void run(IAction action) {
MessageDialog.openInformation(
shell,
"Sample",
"New Action was executed.");
}
/**
* @see IActionDelegate#selectionChanged(IAction, ISelection)
*/
public void selectionChanged(IAction action, ISelection selection) {
}
}
|
これを、たとえば次のような Scala のコードに書き換えます。 (リスト 7)
リスト 7. アクションクラス
package sample.popup.actions;
import org.eclipse.jface.action.IAction
import org.eclipse.jface.dialogs.MessageDialog
import org.eclipse.jface.viewers.ISelection
import org.eclipse.swt.widgets.Shell
import org.eclipse.ui.IObjectActionDelegate
import org.eclipse.ui.IWorkbenchPart
class SampleAction extends IObjectActionDelegate {
var shell: Shell = _
/**
* Constructor for Action1.
*/
def SampleAction() = {
}
/**
* @see IObjectActionDelegate#setActivePart(IAction, IWorkbenchPart)
*/
def setActivePart(action: IAction, targetPart: IWorkbenchPart): Unit = {
shell = targetPart.getSite().getShell();
}
/**
* @see IActionDelegate#run(IAction)
*/
def run(action: IAction): Unit = {
MessageDialog.openInformation(
shell,
"Sample",
"New Action was executed.");
}
/**
* @see IActionDelegate#selectionChanged(IAction, ISelection)
*/
def selectionChanged(action: IAction, selection: ISelection): Unit = {
}
}
|
エディタ上で正しく Scala のコードとして認識され、コンパイルされます。これで Scala のコードを取り入れたプラグインのプロジェクトとして認識されました。Scala の文法があっているのにエラーマーカーが出る場合、Java のビルダーが動いている可能性があります。プロジェクトのビルドのプロパティを確認してみましょう。
プロジェクト上でのエラーがなくなったら、今度は動作を確認してみましょう。
ランタイムのために、特別な設定は不要です。通常のEclipseのランタイム環境を選択し、ワークスペース上の Scala コードを含んだプラグインをロードします。標準的な設定ではワークスペース上のプラグインはすべてロードされます。
ランタイム環境が起動したら、リソース・パースペクティブを開いて、一般的なプロジェクトを作成しましょう。パッケージ・エクスプローラー、作成されたプロジェクトのコンテキストメニューを見てみると、プラグインで追加したメニューが表示されます。 (図 7)
図 7. ポップアップメニュー
では起動してみましょう。
するとダイアログは出るのですが、メッセージはコード上の文字列と等しくありません。 (図 8)
図 8. ダイアログ
ランタイムワークベンチの「.log」ファイルを確認してみましょう。「.log」ファイルは、実行時に利用したワークスペースのルートにある「.metadata」フォルダの下に作られます。すると、「java.lang.NoClassDefFoundError: scala/ScalaObject」および「Caused by: java.lang.ClassNotFoundException: scala.ScalaObject」の記述があります。どうやら ScalaObject が見つからないようです。プラグインが ScalaObject を参照できるように、MANIFEST.MF に Scala のパッケージを追加しましょう。これは、plugin.xml から依存関係を追加することで MANIFEST.MF を変更できます。 (図 9)
図 9. 依存関係の追加
ではあらためてランタイム環境を起動して、追加したメニューを実行してみましょう。
今度はダイアログにコード上の文字列が正しく表示されました。 (図 10)
図 10. ダイアログ
Scala で書かれたアクションクラスを拡張して、表示されるダイアログに選択したリソースの名前を表示させてみます。 (リスト 8)
リスト 8. アクションクラスの拡張
package sample.popup.actions;
import org.eclipse.jface.action.IAction
import org.eclipse.jface.dialogs.MessageDialog
import org.eclipse.jface.viewers.ISelection
import org.eclipse.swt.widgets.Shell
import org.eclipse.ui.IObjectActionDelegate
import org.eclipse.ui.IWorkbenchPart
import org.eclipse.core.resources.IResource
import org.eclipse.jface.viewers.IStructuredSelection
class SampleAction extends IObjectActionDelegate {
var shell: Shell = _
var selected: Option[IResource] = None
/**
* Constructor for Action1.
*/
def SampleAction() = {
}
/**
* @see IObjectActionDelegate#setActivePart(IAction, IWorkbenchPart)
*/
def setActivePart(action: IAction, targetPart: IWorkbenchPart): Unit = {
shell = targetPart.getSite().getShell();
}
/**
* @see IActionDelegate#run(IAction)
*/
def run(action: IAction): Unit = {
var msg: String = ""
selected match {
case res: Some[IResource] =>
msg = "selected resource -> " + res.get.getName()
case _ => msg = "selection is empty"
}
MessageDialog.openInformation(
shell,
"Sample",
msg);
}
/**
* @see IActionDelegate#selectionChanged(IAction, ISelection)
*/
def selectionChanged(action: IAction, selection: ISelection): Unit = {
selected = None
selection match {
case sel: IStructuredSelection =>
sel.getFirstElement() match {
case resource: IResource => selected = Some(resource)
case _ =>
}
case _ =>
}
}
}
|
リソースを選択したときにワークベンチから呼び出される selectionChanged メソッドで、選択されたリソースを保持して、アクションが呼び出されたときに表示するダイアログに、保持したリソースの名前を追記します。
ではこのコードをランタイムで実行してみましょう。
が、大変遺憾なことに今度は何も起きません。
NoClassDefFoundError caused by ClassNotFoundException
今度もランタイムワークベンチの「.log」ファイルを見てみると、「java.lang.NoClassDefFoundError: scala/collection/mutable/StringBuilder」および「Caused by: java.lang.ClassNotFoundException: scala.collection.mutable.StringBuilder」の記述がありますが、今度は ScalaのStringBuilder のようです。StringBuilder の含まれる scala.collection.mutable のパッケージをMANIFEST.MFに追加しましょう。 (図 11)
図 11. 依存関係の追加
これで StringBuilder は参照できるようになることでしょう。ランタイムで実行してみると、今度は期待したとおりに選択したプロジェクトの名前がダイアログに追加されました。 (図 12)
図 12. ダイアログ
Eclipse のプラグインでは、参照する外部ライブラリのパッケージは暗黙には解決されません。すべて明示的に MANIFEST.MF に追加します。MANIFEST.MF に追加したパッケージがさらに別のパッケージを参照していた場合、その別のパッケージも列挙する必要があります。
ここまでで、Scala で書かれたプラグイン・プロジェクトのランタイム実行までできるようになりました。では、他の Eclipse でインストール可能なようにエクスポートしてみます。本記事では例を簡潔にするために、エクスポートしたプラグインをインストールする先の環境にはすでに Scala IDE が導入されているものとします。インストール先で Scala IDE を前提としないようにするには、Scala のライブラリをエクスポートするプラグインに含めることも可能です。
簡単にプラグインのエクスポートする方法の一つに、plugin.xml の Dependencies のタブ上部にあるプラグインのアイコンを選択するというものがあります。 (図 13)
図 13. プラグインのエクスポート
開いたダイアログで、含まれるプラグインとその出力先を設定します。本記事では、作成したプロジェクトの直下に設定しています。 (図 14)
図 14. プラグインのエクスポート
本記事ではエクスポート先にプラグイン・プロジェクトの場所を指定したので、エクスポートの完了後にプロジェクトを更新すると、plugins フォルダとその中に jar ファイルが現れます。これがエクスポートされたプラグインです。 (図 15)
図 15. エクスポートされたプラグイン
無事に作成されたようです。が、jar の中を確認してみると、Sample_1.0.0.0\sample\popup\actions\SampleAction.scala が見つかります。Scala のファイルのままコピーされてしまい、ビルドされていないので class ファイルにならずこのままでは実行できません。プラグインをエクスポートする際に、Eclipse はプロジェクトをビルドし直す Ant タスクを生成して実行します。この生成された Ant タスクは java を前提にしており、Scala のことは知りません。
プラグインのエクスポート時に生成される Ant タスクにフックをかけて、Scala のコードもコンパイルしてビルドに含めてもらいます。
Scala をビルドする Ant タスクは、Scala のコンパイラのライブラリに定義されています。これをプラグインのエクスポート時に呼び出してもらえば解決です。Eclipse では、プラグイン・プロジェクトのビルドを行う Ant タスクにカスタムコールバックを仕込むように設定することが可能です。
まずプロジェクト内に次のような xml ファイルを作成します。 (リスト 9)
リスト 9. Ant タスクのカスタムコールバック
<?xml version="1.0" encoding="UTF-8"?>
<project name="ant build custom callback for scala">
<target name="init">
<property name="scala-lib.home" location="C:/work/eclipses/helios_3.6.2/
configuration/org.eclipse.osgi/bundles/204/1/.cp/" />
<property name="scala-compiler.home" location="C:/work/eclipses/helios_3.6.2/
configuration/org.eclipse.osgi/bundles/203/1/.cp/" />
<property name="scala-lib.jar" value="${scala-lib.home}/lib/scala-library.jar" />
<path id="scala.plugin.classpath">
<path refid="@dot.classpath" />
<pathelement location="${scala-lib.jar}" />
<pathelement location="${target.folder}" />
</path>
<taskdef resource="scala/tools/ant/antlib.xml">
<classpath>
<pathelement location="${scala-compiler.home}/lib/scala-compiler.jar" />
<pathelement location="${scala-lib.jar}" />
</classpath>
</taskdef>
</target>
<target name="pre.build.jars">
</target>
<target name="pre.@dot">
</target>
<target name="post.compile.@dot" depends="init">
<scalac srcdir="${source.folder1}" destdir="${target.folder}"
classpathref="scala.plugin.classpath">
<include name="**/*.scala" />
</scalac>
<delete>
<fileset dir="${target.folder}" includes="**/*.scala" />
</delete>
</target>
<!-- compile for this bundle -->
<target name="post.@dot">
</target>
<target name="post.build.jars">
</target>
<target name="pre.clean">
</target>
<target name="post.clean">
</target>
</project>
|
まず、Scala ビルド用の Ant タスクを読み込むために、Scala のライブラリとコンパイラのパスを設定します。本記事の例にある init 内の scala-lib.home および scala-compiler.home で指定された location の値は、筆者の環境でのパスです。これらのパスは導入した Scala IDE のバンドルの場所を確認して正しいパスに置き換えてください。Scala のライブラリのパスは、プロジェクトのプロパティで設定された Java のビルドパスのリストの中にある Scala のライブラリの設定から確認することができます。同様にして、Scala のコンパイラも同様にして確認することが可能です。
また、ビルド時に Eclipse のライブラリが参照できるようにクラスパスに追加します。この Eclipse のライブラリは、Eclipse が生成する Ant タスク上で “@dot.classpath” として定義されていますので、この名前で参照します。
次に、ターゲット “post.compile.@dot” で、実際の Scala のビルドを行います。これは、Java のビルドが完了した後に呼び出されるコールバックです。この時点で、Scala のコードもまたエクスポートする jar にコピーされてしまうので、scala ファイルを削除してしまいます。
コールバックのターゲットやプロパティの詳細は、Eclipse のヘルプより「Plug-in Development Environment Guide」 -> 「Tasks」 -> 「PDE Build Advanced Topics」 -> 「Feature and Plug-in custom build steps」に記載されています。
本記事では、この内容の xml ファイルをプロジェクトの直下に「customCallbackForScala.xml」という名前で作成しました。
この xml ファイルを Ant ビルドのコールバックで使うように、「build.properties」ファイルに追記します。 (リスト 10)
リスト 10. build.properties
source.. = src/
output.. = bin/
customBuildCallbacks=customCallbackForScala.xml
bin.includes = plugin.xml,\
META-INF/,\
.
|
では、あらためてプラグインのエクスポートを行います。先ほどと同様に、出来上がった jar ファイルの中を確認してみると、Sample_1.0.0.0\sample\popup\actions\SampleAction.class になっています。無事に Scala のコードもコンパイルしてもらえたようです。これで他の Scala IDE の導入された Eclipse 上で、プラグインを使うことができるようになりました。
このコールバックの Ant は、アップデートサイトを作成した場合でも機能します。
本記事では、Scala IDE を導入した Eclipse 環境で、Scala で書かれたプラグイン・プロジェクトの作成、テスト実行、プラグインとしてのエクスポートまでの一連の流れを学びました。
主だった項目を並べますと、
- PDE 環境を持った Eclipse を用意して、Scala IDE を導入する。
- プラグイン・プロジェクトを作成して、「.project」ファイルに Scala のネイチャーとビルダーを追加する。
- プラグイン・プロジェクトのプロパティより、ビルドの設定から Java のビルダーのチェックをはずし、Java のビルドパスに Scala のライブラリを追加する。
- MANIFEST.MF に、参照する Scala のパッケージをすべて記載する。
- Scala のビルド用の Ant のカスタムコールバックを作成する。
- build.properties に作成した Ant のカスタムコールバックを設定する。
となります。本記事の内容は、他の JVM 上で動く言語で Eclipse のプラグインを作成する際にも参考になることでしょう。
- Scala のサイトより、Scala についての理解を深めることができます。
- Scala IDE より、開発環境の最新情報をチェックしてください。
- PDE のオンライン・ドキュメントより、Eclipse のプラグイン開発について理解を深めることができます。
- Eclipse オンライン・ドキュメントである Eclipsepedia より、IAdaptable やその他の Eclipse の仕組みについての理解を深めることができます。
- Eclipse のオンライン・ヘルプより、Ant のカスタムコールバックについて参照してください。
- Scala で提供されている Ant タスクについて参照してください。