プログラムdeタマゴ

nodamushiの著作物は、文章、画像、プログラムにかかわらず全てUnlicenseです

JUnit4でJMockitを使いつつ@Theoryを使う方法

 Javaのテストをするときに、JMockitMockitoなどのモックライブラリを使うことがあると思います。

 Mockitoはちょっとしたことをしたいときに便利なのですが、実デバイスのシミュレータなど、モックにかなり複雑な処理をさせたい場合は、JMockitの方が便利という印象です。


 で、このJMockitなのですが、JUnit4.5以上と共存させるには、@RunWith(JMockit.class)と指定する必要があります。これをしない場合は、ライブラリの読み込み順序の指定しないといけないので、めんどうくちゃい。@RunWith(JMockit.class)を使いたい。

 しかし、@Theoryを使うには@RunWith(Theories.class)とする必要があります。つまり、単純には共存できなくてどーしよ、となりました。

 え、JUnit5を使えと?いや、うん………、そりゃまそうなんだけどさぁ。キゾンコードノイコウメンドウクサイジャンカー.

Theoriesランナーを拡張する


 JMockitランナーって何やってるのさ、と覗いてみたら、なんと、BlockJUnit4ClassRunnerを拡張してstatic { Startup.initializeIfPossible(); }としてるだけだった。

jmockit1/JMockit.java at master · jmockit/jmockit1 · GitHub

 あれ?これってもしかして、Theoriesを拡張して、static { Startup.initializeIfPossible(); }するだけのランナー作ったらいけるんちゃう?とやってみたら、行けてしまった

package my.mockit;

import org.junit.runners.*;
import org.junit.runners.model.*;
import org.junit.experimental.theories.*;

import mockit.internal.startup.*;

public class TheoriesWithJMockit extends Theories
{
   static { Startup.initializeIfPossible(); }

   public TheoriesWithJMockit(Class<?> testClass) throws InitializationError
   {
      super(testClass);
   }
}

 これでクラスパスの順序とか気にせずに、JMockitでモックしまくりながら、Theoriesでクルクル回せるぜ!

 まだまだJUnit4も使えますなぁ。

 私がJUnit5に移行するのはいつになるのですかなぁ。
 とりあえず、Theories相当の拡張機能とかあれば移行するかなぁ。クラスで引数を選択してくれるのが好きなんだよな。
 TestFactoryも慣れれば行けるのか?

Eclipseプラグイン開発: 拡張ポイントの定義

Eclipse プラグイン開発 目次



 Eclipseプラグイン開発でほぼ確実に避けて通れないのが拡張ポイント。拡張ポイントはなんだかよく分からんけど、裏でEclipseが上手いこと処理して、何か素晴らしぃことをしてくれる仕組み………なんてふつくしぃ世界はなく、泥臭い処理がEclipseプラグイン開発者の手によって走らされております。

 拡張ポイントとは何なのか?それを理解するには、まず自分で一回作ってみるのが、何よりも一番手っ取り早くて確実です。他人が用意した拡張ポイントを使う前に、実装してみることをお勧めします。




拡張ポイントとは

 XMLとXML Schemaである。以上。解散。


 一切の冗談抜きにこれだけです。XMLは説明不要でしょう。HTMLによく似た奴です。XML SchemaはXMLに書ける要素(HTMLならdivとかaとかimgとか)を定義するファイルです。

 XMLとXML Schemaを手で書くのは流石にしんどいので、手打ちせずにGUIで設定できるエディタが提供されてるだけです。後は、Eclipse起動時にXMLを勝手に読み込んで、DOM(みたいなもの)にしてくれるぐらいです。

 じゃぁ、どうして拡張ポイントを他のプラグイン開発者が利用しただけで、色々な機能を拡張できるのでしょうか?それは当然、拡張ポイントを定義した人はシコシコXML解析するプログラムを作って、シコシコ解析した情報を自分のプラグインにゴリゴリ統合しているからです。

 決して謎の技術ではなく、裏でそういう血と涙と汗が流れている技術なのです。


 

拡張ポイントを定義する

 拡張ポイントをシコシコ解析する前に、シコシコ解析されるXMLを書くための定義を書かなくてはなりません。

 ここでは、例として「本」を定義する拡張ポイント「sample.core.books」を作ってみましょう。

 拡張ポイント(Extension Point)タブの追加から新規拡張ポイントを定義できます。

日本語化した場合:
f:id:nodamushi:20170414005600p:plain

英語の場合:
f:id:nodamushi:20181007014848p:plain
 

 今回の例では、次のような情報を持たせようと思います。

  1. 本はタイトルを持つ
  2. 本はユニークなISBNを持つ
  3. 本はカテゴリを持つ
  4. 本は一人以上の複数の著者からなる
  5. 本は1P以上のページからなる
  6. 本は複数の他の本への参照を持つ

 

要素の定義をする

 HTMLでは、<body><a><span><table><img>など、多くの要素を利用することが出来ます。HTMLの仕様によってこれらがHTMLの要素と定義されているからです。

 同じように、拡張ポイントで使用することができる要素を定義していきましょう。

 新規に拡張ポイントを定義すると、最初はextension要素しか定義されていません。このextension要素は、HTMLで言えば<html>要素と同じで、拡張ポイントのルート要素です。削除はできません。

 新規要素ボタンを押して、要素bookを作ってみましょう。

f:id:nodamushi:20170414005629p:plain

英語の場合(以下英語は略):
f:id:nodamushi:20181007015015p:plain


f:id:nodamushi:20170414005640p:plain

 同様に、author、page、reference要素を宣言しておきました。

f:id:nodamushi:20170414005651p:plain

 

属性の定義をする

 各要素は属性を持つことが出来ます。HTMLで言えば、<img src="image.png" alt="画像です" id ="myimg">とあった場合、srcやalt、idが属性です。

 属性にはuseとtypeを指定することが出来ます。

 

use

 例えば、imgタグのsrc属性が無かったら画像を表示することが出来ません。HTMLは解釈がゆるいのでなくても構いませんが、出来ることならユーザーに入力を強制したいものです。その指定をuseで決めます。

use 説明
require 必須で、必ず何らかの入力を要求する。
optional 値を書いても書かなくても許される。
default 初期値が最初から入力される。

 

type

 imgタグのsrc属性はファイルの場所を示す「文字列」、alt属性は画像が読めなかったときに表示する「文字列」といった風にそれぞれ入力する内容が決まっています。typeではどういった「文字列」を入力するのかの指定をします。

type 説明
string 文字列を入力させる。制限事項を使うことで、入力値をコンボボックスで選べる
boolean trueかfalseかのどちらかだけを指定できる文字列
java 特定のクラスのサブクラス、または特定のインターフェースの実装クラスだけを指定できる文字列
resource プラグイン内のリソースのパスを入力する支援ボタンがついた文字列
identifier 拡張ポイントの特定の要素の特定の要素と同じ値を入力する支援ボタンがついた文字列

 

 

 では、book、author、page、referenceに属性を定義してみましょう。

 bookには、title属性、isbn属性、category属性を設定しました。category属性は選択制で、デフォルトはとりあえず工口本にしておきました。

f:id:nodamushi:20170414005702p:plain

 拡張ポイントを使う側のエディタでは、bookの属性入力画面がこんな感じになります。

f:id:nodamushi:20170414005709p:plain

 

 authorにはname属性と、顔写真を載せるためにimage属性を追加しました。

f:id:nodamushi:20170414005717p:plain

 同様に、authorの属性入力画面はこんな感じです。imageにリソース選択ボタンが追加されます。

f:id:nodamushi:20170414005723p:plain

 

 pageにはnumber属性と、ページの内容を表すcontents属性を追加しました。contentsはダイナミックなコンテンツかも知れないので、プログラマブルに指定できるように、sample.core.IBookPageを実装したクラスとしました。

f:id:nodamushi:20170414005730p:plain

 入力画面では、contents属性をクリックすると、IBookPageを継承するクラスを作ることが出来たり、既に存在するIBookPageクラスを選択できるボタンが追加されます。

f:id:nodamushi:20170414005742p:plain

 

 referenceには、参照したい本のISBNを指定させることにします。

f:id:nodamushi:20170414005748p:plain

 入力画面では、既に定義されているbook要素のisbmの値を選べるボタンが追加されます。

f:id:nodamushi:20170414005754p:plain



要素の子要素を定義する

<table>
  <tr><td/></tr>
  <tr><td/></tr>
</table>

 上のHTMLのように、table要素の下にはtr要素を、tr要素の下にはtd要素を配置することが出来ます。

 拡張ポイントの要素も同様に子要素を持つように指定することが可能です。

choice (選択)

 正規表現的に書けば(A|B|C)になります。各要素は個数指定できるので、大ざっぱに書くと、(A*|B*|C*)*ですかね。要素A,B,Cが何個出てきても構わない、というときにchoiceを使うとよいです。

 

sequence (シーケンス)

 正規表現的に書けば(ABC)になります。各要素は個数指定できるので、大ざっぱに書けば、(A*B*C*)*ですかね。なお、完全に正規表現ではないので、AB*C*という指定にした場合も、ABCBCという入力を受け付けます。

 例えばtitle要素は必ず一つ、description要素は0または1、その他の要素は何個でもという様な場合はsequenceを使います。



 さぁ、それでは「sample.core.books」拡張ポイントを完成させてしまいましょう。拡張ポイント直下には「book」を1つ以上できるので、choiceを使って(book)+としました。なお、(book+)でも構わないっちゃ構わない。

 次に、bookには一人以上の著者、1P以上のページ、0個以上の他の本への参照を持つので、sequenceを用いて、(author)+(page)+(reference)*としました。(下図の左)

 エディタでは、下図の右のように要素を構成できます。

f:id:nodamushi:20170414005809p:plain



他の拡張ポイント定義を利用する


 よくスキーマを見ると、「スキーマの取り込み」という項目があります。スキーマは他のスキーマをインクルードすることが出来ます。

f:id:nodamushi:20170414005818p:plain

 

 インクルードすることで、他のファイルで定義した要素をそのまま再利用することが可能になります。

 といっても、この項目をそのまま使うことはまず無いでしょう。そんなに再利用したくなるほど、すゥンばらしィ定義ファイルを自分で作るなんて、普通ないでしょうし。でも、この追加ボタンで追加できるのは、自分で定義したファイルだけなのです。


 それではあまり意味がないので、折角なので、知っておくと非常に有用な要素を取り込んでみましょう。
 ある場面では、拡張を有効化したいけど、ある場面では無効化したいという場面は多いですよね。これは「org.eclipse.core.expressions.definitions」を別途定義してもらい、definitionsに指定したIDを受け取ることで実現可能です。しかし、定義を二個に分離するのも面倒ですね。definitionsで書く内容を我々の拡張ポイントに取り込んでみましょう。(取り込んだものを、Javaで実際にどうやって使うのかは次回説明します)

 まずは、必須プラグインに「org.eclipse.core.expressions」を追加します。

 次に、スキーマのエディタで、「ソース」を選択し、XMLの生ソースを表示します。

annotationの後ろあたりで良いので、次のincludeを追加して下さい。

<include schemaLocation=
  "schema://org.eclipse.core.expressions/schema/expressionLanguage.exsd"/>


 全体としてはこんな感じの場所です。schema直下だったらどこでも良いと思うけど。

f:id:nodamushi:20170414005827p:plain


 では、定義の画面に戻ってみましょう。bookのシーケンスを右クリックして、新規を選択すると、book,page,author,reference以外のadaptなど今回定義していない要素がずらっと並びます。

f:id:nodamushi:20170414005835p:plain


 この中からenablememtを選んで追加して下さい。


 さて、これで自分独自の拡張ポイントを定義することに成功しました。しかし、まだ定義しただけです。これを自分で解析する機構を作らなくてはなりません。

 というわけで、次回に続く。

Eclipseプラグイン開発: 非UIプラグインのテスト

Eclipse プラグイン開発 目次


 

 Eclipse開発で1番………いや、2番ぐらい?いや、1番かな………?まぁ、それぐらい困るのがどうやってJUnitテストすれば良いのかわかりにくいこと。Eclipseのプラットフォーム(OSGi)が絡んでいなければ、個別にテストできるけど、他のプラグインを利用しているなどプラットフォームが絡んでくると、テストが出来ないと言うことになる。

 ただでさえEclipse開発なんて手探りなのに、テストのやり方が分からなくて、出来る範囲だけでやってたら、大量の未テスト項目が出来てしまって呆然としております。そんなことにならないように、皆さんは最初からテストの作り方を知っておきましょう。

 ただ、調べるといろんなやり方があるというか、私も何が正しいのか分かっていないので、現状のnodamushiのやり方を紹介するだけです。

 ちなみに、これ書いてる今もGUI操作を伴うテスト方法とか分かってないので、誰か教えて下さい。

 

Eclipseプラグインをテストするプロジェクトを別に作る

 Mavenとかでプロジェクトを作ると「src/main」と「src/test」のように一つのプロジェクトの中にテストを配置するように作られますが、Eclipseプラグインテストをする際はこれでは不都合があります。
 それは拡張ポイントのテストが出来ないことです。1つのEclipseプラグインに複数のplugin.xmlを配置することが出来ないからです。


 従って、Eclipseプラグインのテストは、本体とは別にもう一つプラグインを作る必要があります。

 ここでは例として次のようなプロジェクトのテストをしてみたいと思います。

f:id:nodamushi:20170407011351p:plain

 

 NodamushiImplは以下のような実装になっています。getPluginIDメソッドで無駄にBundleをActivatorから取得しており、バンドルが起動していないとヌルポになってしまいます。

package nodamushi.internal.core;

import nodamushi.core.INodamushi;
import nodamushi.core.NodamushiPlugin;

public class NodamushiImpl implements INodamushi{
  @Override public String getName(){
    return "nodamuchi";
  }
  @Override public String getPluginID(){
    return NodamushiPlugin.getDefault().getBundle().getSymbolicName();
  }
}

 nodamushi.coreは公開パッケージですが、nodamushi.internal.coreは他に公開したくありません。しかし、テストを別プロジェクトにしてしまう以上、これを公開しなければテスト用プロジェクトからnodamushi.internal.coreが見えません

 これを回避するために、テストプロジェクトに対して、nodamushi.internal.coreを限定的に公開すると言うことが出来ます。

f:id:nodamushi:20170407011515p:plain

 

 しかし、テスト用に作るプラグインプロジェクトは一般公開するつもりのないものです。そんなものの情報をnodamushi.coreに残しておくというのはどうも気持ちが悪いです。


 そこで、テスト用プラグインプロジェクトをnodamushi.coreプラグインのフラグメント プロジェクトnodamushi.core.testとして作ってしまいます。こうすれば、nodamushi.coreがnodamushi.core.testの存在を一切知らなくても、nodamushi.core.testはnodamushi.coreの全パッケージにアクセスすることが可能になります。


 

テストケースクラスとスイートクラスを作る

 テストケース・スイートクラスはいつもと同じようにJUnitの新規ウィザードから作成して下さい。

f:id:nodamushi:20170407011534p:plain

 

 作成しようとすると、いつものJava開発と違う画面がポップアップします。これはそのままOKを押して下さい。自動的に依存関係にJUnitを追加してくれます。


f:id:nodamushi:20170407011548p:plain

 
 

 以下の図のようにテストケースとテストスイートを作りました。
f:id:nodamushi:20170407011603p:plain

 


 中身はこんな感じです。

NodamushiImplTest.java(テストケース)

package nodamushi.internal.core;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.*;

import org.junit.Test;

public class NodamushiImplTest{

  @Test public void getNameTest(){
    NodamushiImpl n = new NodamushiImpl();
    assertThat(n.getName(), is("nodamushi"));
  }

  @Test public void getPluginIDTest(){
    NodamushiImpl n = new NodamushiImpl();
    assertThat(n.getPluginID(), is("nodamushi.core"));
  }
}

AllTest.java(スイート)

package nodamushi.core;

import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;

import nodamushi.internal.core.NodamushiImplTest;

@RunWith(Suite.class)
@SuiteClasses({NodamushiImplTest.class})
public class AllTests{}

テストを実行する

 では実際にテストを実行してみましょう。
 メニュー→実行→実行構成を選択して下さい。
f:id:nodamushi:20170407011724p:plain

 ウィンドウが開いたら、JUnitプラグインの項目で、新しい実行構成を新規作成します。スイートクラスを開いたままこの処理を実行すると、勝手に内容が入力されます。(入力されなかったら頑張って入力して下さい)
 

 
f:id:nodamushi:20170407011756p:plain

 


 メインタブに移動し、「アプリケーションの実行」をヘッドレス・モードにします。このヘッドレス・モードはWorkbench(GUI)を起動しないので、普通にEclipseの起動をしてテストをするよりも高速に実行が出来ます。(Workbenchがテストに必要な場合はorg.eclipse.ui.ide.workbenchを選択します)

 

f:id:nodamushi:20170407011805p:plain

 

 次に引数のタブに移動します。私のようにMargeDocで日本語化している雑魚は、VM引数でMergeDocの設定を引き継いでしまうので、削除して下さい。以上の設定が完了したら、実行を押します。

 

f:id:nodamushi:20170407011822p:plain


 


 あら、どうやらnodamushiをタイポしてnodamuchiになっていたみたいです。
 てへぺろ☆(・ω<)
f:id:nodamushi:20170407011839p:plain

 

 なお、一度作成したら、Quick JUnitを導入している場合、スイートクラス内にカーソルがある場合、Ctrl+-で同じ内容を起動できます。私は「Alt+R,T,1」(ヒストリーの通常起動)か「Alt+R,H,1」(ヒストリーのデバッグ起動)を使うことが多いですが。

 これで基本的にはGUI操作やGUIリソースを必要としないものにかんしてはテストが出来ます。

Eclipseプラグイン開発: 非同期実行

Eclipse プラグイン開発 目次


 Eclipseでの非同期実行に関してはorg.eclipse.core.runtime.jobs.Jobというクラスを使います。

 

Jobの作成とタイプ


 Jobの作成は、Jobを継承して実行処理を書くか、Job.createSystemかJob.createメソッドで作成します。個人的には後者がお勧めです。

 Jobは以下の3種類の実行方法があります。GUIにどのように表示したいかによって、どれを使うかを決めて下さい。

種類 作り方 説明
システムJob createSystemで作るか、setSystem(true) UIに表示されないバックグラウンドJob
ユーザーJob setUser(tru) ダイアログが表示されるJob
通常Job UserにもSystemにもしない Progressビューには表示されるが、ダイアログは出ないJob


 10秒待機するだけのJobを実行するだけのサンプルです。

Job systemJob = Job.createSystem(makeTask("System Job"));

Job defaultJob = Job.create("Default Job!",makeTask("Default Job"));

Job userJob = Job.create("User Job!",makeTask("User Job"));
userJob.setUser(true);

systemJob.schedule();
defaultJob.schedule();
userJob.schedule();

//-----------------------------------------------

//10秒待つだけの処理を作る
private ICoreRunnable makeTask(String jobName){
  return monitor->{
    try {
      monitor.beginTask(jobName, 10);
      int i=0;
      while(i!=10){
        Thread.sleep(1000);
        monitor.worked(i);
        i++;
        if(monitor.isCanceled()){
          break;
        }
      }
    } catch (InterruptedException e) {
    }
    monitor.done();
  };
}

 実行するとUser Job!のダイアログが出ます。また、ProgressにDefault Job!とUser Job!が表示されています。
f:id:nodamushi:20170406020429p:plain

Jobの実行ルール

 あるJobが実行している間は他のJobをスタートしたくない場合があります。Jobはそういったルールを指定することが可能です。
 IResourceはこのルールを実装していて、親子関係にあるIResourceをルールに持つJobが実行している間は、実行できなくすることが可能です。

IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();

IProject aProj = root.getProject("A");//  /A/
IFile file = aProj.getFile("File.txt");// /A/File.txt
IProject bProj = root.getProject("B");//  /B/

Job aJob = Job.create("A Project Job", makeTask("A Project Job"));
Job fileJob = Job.create("File.txt Job", makeTask("File.txt Job"));
Job bJob = Job.create("B Project Job", makeTask("B Project Job"));

aJob.setRule(aProj);
fileJob.setRule(file);
bJob.setRule(bProj);


aJob.schedule();
fileJob.schedule();
bJob.schedule();

f:id:nodamushi:20170406020430p:plain
 

 A Project JobとB Project Jobは親子関係似ないので、同時に実行されています。一方、File.txt JobはA Project Jobと親子関係なので、A Project Jobが完了するまで待機状態になります。



 なお、実行順序を逆にすると、今度はA Project JobがFile.txt Jobが完了するまで待機します。

fileJob.schedule();
aJob.schedule();

f:id:nodamushi:20170406020431p:plain


Ruleを自作する

 ルールはISchedulingRuleを実装することで、自分で定義することも可能です。

メソッド 説明
contains 引数が子であるかどうか。自分自身も子であると見なす
isConflicting 衝突関係にあるかどうか。衝突していない場合は実行可能。

 

 ファイルパスなどの単一親の木構造ルールではなく、複数の親を持つルールを定義してみましょう。

public class MyRule implements ISchedulingRule{
  private MyRule[] parents;

  public MyRule(MyRule... parents){
    this.parents = parents!=null?parents:new MyRule[0];
  }

  @Override public boolean contains(ISchedulingRule rule){
    //※自分は必ずtrueにしなくてはならない。
    if(rule == this)return true;

    if(rule instanceof MyRule){
      for(MyRule r:((MyRule)rule).parents){
        if(contains(r))return true;
      }
    }
    return false;
  }

  @Override public boolean isConflicting(ISchedulingRule rule){
    if(rule instanceof MyRule){
      MyRule other = (MyRule)rule;
      //※衝突ルールは双方向
      //  (子が実行していても親が実行していても衝突)
      return contains(other) || other.contains(this);
    }
    return false;
  }
}

 


 以下のようなテスト実行をしてみました。Job Bを遅延実行しています

MyRule a = new MyRule();
MyRule b = new MyRule();
MyRule ab = new MyRule(a,b);

Job aJob = Job.create("Job A", makeTask("A"));
Job bJob = Job.create("Job B", makeTask("B"));
Job abJob = Job.create("Job [A/B]", makeTask("A/B"));

aJob.setRule(a);
bJob.setRule(b);
abJob.setRule(ab);

aJob.schedule();
bJob.schedule(1000);
abJob.schedule(2000);

f:id:nodamushi:20170406020432p:plain
 

 A,Bの両方が完了するまで、Job [A/B]が待機していることが確認できます。


Workspaceでアトミックな処理をするJobを作る

 2005-04-12 - NetPenguinの日記によると、リソースを処理する作業は全てのプラグインにまたがって単一にしないと、失敗する場合があるそうです。(知らなかったー)そういった場合は、WorkspaceJobクラスを利用することでその目的を達成できます。

 なお、Jobのように非同期に実行したくない場合は、ResourcePlugin.getWorkspace().runを使うことも出来ます。

Jobをグループ化する

 複数のJobを走らせるとき、全てのJobが完了するまで待機したい、全てのJobを一気にcancelしたい場合などがあります。JobGroupでグループを設定することで、これらの目的を実装できます。

int maxThread = 2;//動作させる最大スレッド数
int seed = 1;//基本的に1にしておけば良い。
JobGroup group = new JobGroup("ab group", maxThread, seed);
aJob.setJobGroup(group);
bJob.setJobGroup(group);
abJob.setJobGroup(group);

aJob.schedule(); bJob.schedule(1000); abJob.schedule(2000);


try {
  //全部のJobが終わるまで待機
  //0にするとタイムアウトしない
  group.join(0, null);
} catch (OperationCanceledException|InterruptedException e) {
  e.printStackTrace();
}

 

 seedはGroupが管理する「初期」サイズで、初期値より多いJobをグループ化しても問題ありません。むしろ、初期値が大きすぎる方が問題で、初期サイズのJobが全て完了しない限りjoinで待機し続けることになります。
 後から他のスレッドでJobを遅延定義すると言った場合でも無い限りは、seedは1にしておけば良いでしょう。

int seed = 4;// Jobの数(3個)より大きい

//-------------------------------

group.join(0, null);//永遠にjoinしない

Eclipseプラグイン開発: バンドルリソース関連

Eclipse プラグイン開発 目次



 IPath,IResourceなどで、Eclipseプラットフォームが管理するリソースにアクセスが出来ます。

 しかし、プラグインがアクセスするのは何もユーザーが準備するファイルだけではありません。プラグイン自身が始めから持っているファイル達も利用する場面があります。今回の話題はそういったファイルの扱い方です。

バンドルリソースの場所

 プラグイン開発をし、実際に一般に使えるようにした場合、プラグイン用に用意したリソースが配置される場所は2通りあります。

  • 配布jarの中に入ったまま
  • インストール時に自動的に展開される


 インストール後、展開されるかどうかはfeatureの設定によりますが、基本的には後者は「exe」などの実行ファイルや、外部ライブラリの「jar」等を格納するために使い、画像などのファイルはjarの中に入れたままにすることが多いようです。


 展開されたリソースはjava.io.Fileやjava.nio.Pathで処理可能ですが、jarに同梱されているリソースはFileやPathでは処理できません。どちらのインストール方法でも対応できるように基本的にはバンドルのリソースはURLで操作することになります。


バンドルリソースの取得

 では実際にプラグインに梱包されたリソースを読み出してみましょう。例として次の様な内容のプラグインを作成しました。

f:id:nodamushi:20170405023654p:plain

 mytest.Activatorはプラグインを作ったら出てくるコードそのままです。ブログ掲載のためにコメントと空白を消しただけで、特にプログラムを弄ってはいません。

package mytest;

import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;

public class Activator implements BundleActivator {
  private static BundleContext context;
  static BundleContext getContext() {return context;}
  public void start(BundleContext bundleContext) throws Exception {
    Activator.context = bundleContext;
  }
  public void stop(BundleContext bundleContext) throws Exception {
    Activator.context = null;
  }
}

 


 一番単純なリソースの取得方法は、このActivatorに渡されるBundleから直接リソースURLを取得する方法です。存在しないリソースを取得しようとした場合はnullが返ります。

  //ResourceTest.javaの中身
  Bundle bundle = mytest.Activator.getContext().getBundle();

  @Test public void getResourceTest(){
    assertThat( bundle.getEntry("/resource/alphabet.txt"),notNullValue());
    //存在しないときはnull
    assertThat( bundle.getEntry("/resource/hoge.txt"),nullValue());
  }

 


 URLを取得できてしまえば、中身を読み出すには普通にopenStreamしてしまえば良いので簡単ですね。

  //ResourceTest.javaの中身
  private BufferedReader reader(InputStream i){
    return new BufferedReader(new InputStreamReader(i));
  }

  @Test public void readResourceTest() throws IOException{
    URL entry = bundle.getEntry("/resource/alphabet.txt");
    try(BufferedReader r= reader(entry.openStream())){
      assertThat(r.readLine(),is("abcdefghijklmnopqrstuvwxyz"));
    }
  }

 


 これでも基本的には問題ないのですが、フラグメント・プロジェクトを扱うと、これではファイルが取得できないという問題があります。そういった問題も考慮し、お行儀良くURLやInputStreamを取得する場合は、FileLocatorを使います。

 とりあえず、今はこれだと困ることがある、ということだけ理解してください。詳しくはフラグメント・プロジェクトの項で解説します。

  //ResourceTest.javaの中身
  Path path = new Path("/resource/alphabet.txt");
  @Test public void getResourceTest2(){
    URL url = FileLocator.find(bundle, path, null);
    assertThat(url,notNullValue());
  }

  @Test public void readResourceTest2() throws IOException{
    try(BufferedReader r=
        reader(FileLocator.openStream(bundle,path, false))){
      assertThat(r.readLine(),is("abcdefghijklmnopqrstuvwxyz"));
    }
  }

 

URLをFileやPathに変換する際の要注意事項

 URLをjava.io.Fileやjava.nio.file.Pathに変換する場合に注意する点があります。 (以降この節ではFile,Pathと省略。EclipseのPath (org.eclipse.core.runtime.Path)ではない)

 ネイティブ実行ファイル(exeなど)をプラグインに含む場合、インストール方法を展開にします。こうすると、実行ファイルがファイルとして展開されて保存される為、「jarから取得→一時ファイルとして保存→実行」をしなくても、パスさえ取得出来れば即座に実行出来ます。この時、FileやPathとしてパスを取得すると、JavaのAPIと関連させる場合に便利です。

 一般的には、URLからURIに変換し、そのURIからFileやPathを得ます。その時にはtoURIを使わずに以下の様にしてください。

URL  url  = FileLocator.find(bundle, path, null);
URI  uri  = new URI(url.getProtocol(), null, url.getPath(), null);
File file = new File(uri);
Path path = Paths.get(uri);


 URL.toURIを使うと、URLが特定の文字を含む場合、例外が発生します。
 問題の核は、URLがRFC2396に従って適切なエスケープがされている必要があるという点です。例えば空白文字は%20、%そのものは%25などの様にエスケープする必要があります。このエスケープ処理はURLを生成するクラスが責任を持ちます。これらの詳細はURLのJavaDocを読んでください。

 Eclipseが作成するURLは、空白文字列を含む場合は空白をそのまま空白として出力します。従って、toURIをそのまま使うと、例外が発生する可能性があります。



バンドルリソースの画像ファイルの取得

 Eclipseプラグイン開発において、画像はImageのまま保持するのではなく、ImageDescriptorの形で保持しておいて、データを使い回すと言うことが多いです。

 このImageDescriptorを作るのは割と面倒なんですが、AbstractUIPluginというクラスのstaticメソッドとして便利関数が定義されているのでこれを使います。URLのときと同様に、存在しない画像ファイルの場合はnullが返ります。

  //ResourceTest.javaの中身
  @Test public void getImageTest(){
    ImageDescriptor img = AbstractUIPlugin.imageDescriptorFromPlugin(bundle.getSymbolicName(), "/resource/test.jpg");
    assertThat(img,notNullValue());

    ImageDescriptor img2 = AbstractUIPlugin.imageDescriptorFromPlugin(bundle.getSymbolicName(), "/resource/hoge.jpg");
    assertThat(img2,nullValue());
  }


 といっても、AbstractUIPlugin~~~~って毎度書くのも面倒くさいし、何のプラグインの画像読み出してるのかわかりにくいので、私はこうしています。もっと真面目に実装する場合はリソースマネージャーとか自作するみたいよ。私はしてないけど。

  //Activator.javaに追加
  /** このプラグインのID */
  public static final String PLUGIN_ID="mytest";

  /**
   * このプラグインのリソースからImageDescriptorを取得
   * @param path 画像ファイルのパス
   * @return
   */
  public static ImageDescriptor getImageDescriptor(String path){
    return AbstractUIPlugin.imageDescriptorFromPlugin(PLUGIN_ID, path);
  }

 

共通画像の取得

 Eclipseプラグインを開発する上で、大概必要になるフォルダアイコンやらを一々用意するのは面倒です。むろん、AbstractUIPlugin.imageDescriptorFromPluginで他のプラグインの持っている画像を取得することは出来ることは出来ますが、画像ファイルのパスが分からんし、調べたとしても(調べるプラグインがあります)、そのパスがずっと有効かどうかなんて分かりません。

 そんな開発者のために、一般によく用いられるアイコンに関しては、取得出来るようにしてくれてある場合があります。

ISharedImages shared = PlatformUI.getWorkbench().getSharedImages();
//ファイルアイコン
shared.getImageDescriptor(ISharedImages.IMG_OBJ_FILE);

//Java画像も公開されています
JavaUI.getSharedImages().getImageDescriptor(org.eclipse.jdt.ui.ISharedImages.IMG_OBJS_CLASS);

//CDTも公開しています
CDTSharedImages.getImageDescriptor(CDTSharedImages.IMG_OBJS_BUILD);

Eclipseプラグイン開発: リソースパス関連

Eclipse プラグイン開発 目次


 Javaでファイル、ディレクトリを操作する場合、java.io.Fileやjava.nio.file.Pathを用いて表現します。

 EclipseではIPathないし、IResourceを用いて表現するのですが、これもうほんと、使いにくい。
 といっても、Eclipseでファイルを扱おうと思ったら避けることは出来ませんので、理解して付き合っていくしかありません。

IPath

 IPathはJavaのAPIで言えば、java.nio.file.Pathとほぼ同等の機能です。ファイルやディレクトリなどの場所(パス)を表現します。

 Eclipseプラグイン開発において、IPathは主に以下のパスを表現します。

  • フルパス:ワークスペースをルートとする、ワークスペース空間における絶対パス表現
  • ロケーション(ローカルシステム)パス:OS固有の絶対パス表現
  • 相対パス:絶対パスではないパス表現


f:id:nodamushi:20170403014824p:plain:right

 フルパスというのは、ワークスペースをルートとする、Eclipseシステム系の絶対パスを示します。
 たとえば、右図の様なワークスペースのプロジェクト構造をしているとき、AプロジェクトのA.javaは「/A/src/A.java」と表現され、BプロジェクトのdlinkフォルダのTest.classは「/B/dlink/Test.class」と表現されます。
 ルートであるワークスペースのパスは「/」です。この辺はUnixと同じ表現ですね。@でも何でも良いから別な文字にしときゃ良かったのに。

 

 一方で、ロケーションパスというのはOSのファイルシステムにおける絶対パスを表します。
 実際にフルパス、ロケーションパスを見たければ、プログラムを書かなくても、プロパティビューから確認することが出来ます。
 右図のワークスペースは、「F:\workspace」に配置しているのですが、プロジェクトAとプロジェクトBのローカルパスを表示すると下図の様に「F:\workspace」とは全然関係ないところに配置されていることが分かります。
 従って「ワークスペースのディレクトリ(F:\workspace)」+「フルパス」としてもOS上のファイルパスを取得することが出来るとは限りません。
f:id:nodamushi:20170403015926p:plain


 なお、ロケーションパスには、生ロケーションパス(Raw Location Path)が存在しますが、これは使うことないと思います。Eclipseのフォルダリンクは、リンク先を絶対パスで指定するタイプのものと、変数で指定する仮想リンクがあるのですが、生ロケーションパスが表現するのは後者のほうです。例えば、Test.javaはこんな風に出てきます。「PARENT-1-PROJECT_LOC/java/Test.java」。普通にロケーションパスを取得すれば、変数の所は解決してくれるので、考える必要はありません。

 相対パスは、Bundle内のリソースパスだったり、特定のパスからの相対パスだったり等、使う場所や条件によって意味が変わる通常の相対パスです。

IResource

 IResourceはワークスペース空間におけるリソース(ワークスペース、プロジェクト、フォルダ、ファイル)を表現するインターフェースです。ワークスペース空間におけるリソースというのは、要するにフルパスで表現できるリソースを表します。
 JavaのAPIでいうと、パス以外の付加情報や操作機能を持っているという点でjava.io.Fileがよく似ています。

 java.io.Fileと違うのは、ルート(ワークスペース)、プロジェクト、フォルダ、ファイルと4種類のインターフェースに派生する点と、IFile(ファイルを表現するIResource)から内容を編集したときに、Eclipseのローカル履歴に変更情報を残すことが出来る点でしょう。

IResource 説明 取得方法
IWorkspaceRoot ワークスペースを表す IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
IProject プロジェクトを表す IProject project = root.getProject("B");
IFolder フォルダーを表す container.getFolder(new Path("hoge"));
IFile ファイルを表す container.getFile(new Path("hoge.txt"));


 containerはIWorkspaceRootか、IProject、IFolderのことです。IContainerのメソッドの引数として渡すIPathは、containerからの相対パスです。

 getProject,getFolder,getFileによるプロジェクト、フォルダー、ファイルの取得は、存在しないリソースも表します。この辺はもうAPI設計のミスだと思うわー。newProject,createFolderとかにして、存在する場合はエラーにすべきだと思うわー。

 IProject、IFolder、IFileに関わらず、存在する場合のみIResourceを取得したい場合は、container.findMemberメソッドを使います。
 

IPathからjava.io.Fileやjava.nio.Pathへの変換

 Eclipse内部だけで完結するならIPath,IResourceで全て処理すれば良いのだが、大概そうもいかない。普通のライブラリって、ファイル表現にFileだったりPathを使うわけで、IPathやIResourceのままだと困るのだ。

 結論を言うと、IPathやIResourceから確実にFileやPathに変換する方法はない。
 基本は「IPath→IResource→FileやPath」という変換手順を辿らなければならないが、「IPath→IResource」の段階で絶対の手順というものはないし、「IResource→FileやPath」も必ず成功するとは限らない。

IPath→IResource

 まずは渡されたIPathがフルパスなのか、ロケーションパスなのか、相対パスなのか考えなくてはならない。相対パスの場合は、何に対して相対なのかで話が変わるから、ここでは考えない。
 
 フルパスなのか、ロケーションパスなのかについては、Windowsの場合は楽です。「~:\」ないし「\\」から始まっていたらロケーションパスです。そのままFileやPathに変換しましょう。しかし、これがUnix系の場合だったら…どうすんだろうね。


 で、フルパスからIResourceに変換する一番簡単な方法は以下の方法だと思う。

IResource resource = ResourcesPlugin.getWorkspace().getRoot().getFile(new Path("パス"));

 なお、パスが示す先がFolderであっても、ファイルとしてアクセスするような処理をしない限り、実はgetFileで特に問題ない。

 findMemberだと、存在しないファイルのパスを解決できないので、getFileを使うと良い。

IResource→File,Path

 単純には以下のようにすれば良い。しかし、プロジェクトを閉じている場合や、リンクを解決できない場合などで、getLocationはnullが返ることがあるので、要注意

File file = resource.getLocation().toFile();
Path path = Paths.get(resource.getLocation().toOSString());

Eclipseプラグイン開発: 開発環境の準備

Eclipse プラグイン開発 目次




 え?Eclipseプラグイン開発するつもりなんですか?まだ引き返せますよ?上司にVisual Studio Codeを提案した方がいいと思うよ?

 ちゃんと私は止めましたからね?

開発に必要なツール

  • 根性と泣かない心
  • GoogleとGithub
  • Eclipse IDE for Eclipse Committers
  • EASE
  • Maven

根性と泣かない心

 分からなくても泣かない。案外数ヶ月後にもう一度やってみたら分かったりする。

GoogleとGithub

 Eclipse開発で困難を極めるのは、目的を達成するために「何をどうすれば良いのか分からない」ことが多々あると言うことです。結論が「~~Interfaceを実装すれば良い」というだけのことであっても、「~~」にたどり着くのが結構大変。いや、マジで。

 とにかく、コーディング能力よりも検索能力が試されます。従ってGoogle先生は必須です。

 そして、GithubにはEclipse本体含む、多くのプラグインが公開されています。他のプラグインがどういう実装をしているのかを調べることは、非常に有用であり、重要な手がかりになります。私の場合、Githubの検索である程度当たりを付け、実際のソースコードはEclipse上で表示する、というやり方に落ち着きました。

参考URL

 増えてくると別ページに分離するかも。

 Eclipse Documentation HelpのPlatform Plug-in Developer Guide項目とひしだま氏のEclipseプラグイン開発はブックマークしておきましょう。
 ただ、Eclipse Helpのページ、サイドバーとかJavaScriptで動いてて非常に使いにくいんで、JavaScriptをoffにして目的のページだけ表示するようにした方が良いと思います。あと、ひしだま様は神様。

※EclipsepediaのSnippetが動かなくても泣かないコト

参考書籍


Eclipse Plug-in Development: Beginner's Guide - Second Edition (English Edition)

Eclipse Plug-in Development: Beginner's Guide - Second Edition (English Edition)

 Eclipseプラグイン開発そのものよりも、どっちかっていうとMavenのtychoの使い方を知るのにとても有用だった
 

Eclipse IDE for Eclipse Committers

 Eclipse Packages | The Eclipse Foundation - home to a global community, the Eclipse IDE, Jakarta EE and over 350 open source projects...からEclipseプラグインを開発する用のEclipseを用意しましょう。むろん、すでにEclipseを使っていて、それにプラグイン開発用プラグインをインストールしても構いません。

 特に気にしない場合、最新バージョンを選べば良いですが、対象のEclipseバージョンを限定、ないし最低バージョンが指定されている場合は、そのEclipseを選びましょう。APIが変わったり増えたりします。

EASE

 Eclipse Advanced Scripting Environment | The Eclipse Foundation

 Eclipseのプラグインで、Eclipseを実行している環境上でJavaScriptなどのスクリプト言語を動かすことが出来るプラグインです。ブラウザで言う、Firebugみたいな感じで私は使っていますが、プラグイン開発に慣れた人なら、EASEを使って、プラグインにしなくても高度なことがスクリプト的に実行できるとても便利なツールです。具体的には後に載せるYoutubeの動画を見ると分かりやすいと思います。

 Eclipseのプラグインを開発しているときに、APIがどういう動作をするのか説明読んでもよく分からないってことが結構ありますが、その動作確認のために、何度もEclipseを立ち上げ直すのはかなり面倒くさいです。EASEを使うと、そういったAPIの動作をその場で確認できます。

 ただ、v0.3まではかなり動作が不安定っていうか、しょっちゅうエラー出てました。v0.4でわりと結構使えるようになりましたが、それでも何かけっこー固まる。SWTのインスタンスにDisplayのThread以外からアクセスしようとすることが多々あるのよね…

 まだ現状動作が不安定なので、開発しているEclipseとは別のEclipseか、開発中のプラグインをテストしているEclipseで動かすのが無難でしょう。
 

Maven

Maven – Welcome to Apache Maven
Apache Maven3 (3.2.5) インストール手順 (Windows) | WEB ARCH LABO

 最近はMavenも廃れてGradleかと思うのですが、残念ながらGradleでのやり方知りません(オイ

 プラグインをビルドしたり配布する場合、Eclipseの機能をそのまま使っても良いですが、Mavenを使うことをお勧めします。特に、32bit環境、64bit環境、OS等によって配布するプラグインを変えたい場合はMavenが必須になります。