プログラムdeタマゴ

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

直前の英字文字列をIMEで再変換するAIUEO Eclipse Plugin作った

 いよぅ!IME圏でEclipseを使っているみんな!nodamushiからのナイスクリームなプラグインをプレゼントだ!

 ………すいませんでした。現在深夜………というか明朝の5時、妙なテンションになりました。気がついたら徹夜だよ。


 さて、前回の記事でAtokの入力済みのローマ字を再変換する機能について触れました。
 ポップアップも消せるようになりましたし、とてもいい機能なんですが、最悪の欠点が2つあります。

  1. 本当に直前に入力した文字しか変換できない
  2. 変換確定後IMEがONになっている

 最初のはまだしも、2番目がプログラミングをする上でこの上なくウザい。画面割りたくなる。

 プログラミングで日本語(笑 とか言ってる意識高い(笑 人は放っておいて、基本的には日本語って以下の状況で使うと思います。

  1. コメント
  2. JavaDocなどのドキュメント
  3. テストメソッド名

 コメント書いてるときはまだいいんですが、JavaDocを書いてるときは@paramとか{@link ****}とか出てきて英語と日本語のチャンポンなのでIME切り替えが面倒くさい。テストメソッド名も、その後必ず()を書くわけで、IMEの切り替えが面倒くさい。

 というわけで、そんな不満を解消するためにAIUEOという名前のプラグイン作りました。名前が酷い。

 もし、既に似たようなのが存在してたら泣く。
 

インストール

利用可のなのはWindowsだけです。

Eclipseの新規ソフトウェアのインストールで「https://nodamushi.github.io/AIUEO」からインストール。

手動インストールが良い人はReleases · nodamushi/AIUEO · GitHubからzipをダウンロードし、dropinに展開する。


使ってはダメな人

 英数字、日本語の入力切り替えをIMEのON/OFF(半角/全角キー)ではなく、無変換・変換キーで行う人はインストールしないでください。仕組み上、対応できません。

機能と仕組み

あいうえおkakikukeko|sasisuseso

 oとsの間の|はカーソル位置だと思ってください。

1. カーソル位置から前方(もしくは後方)に区切り文字を探し、書き換え範囲を見つける。(設定で区切り文字は変えられます)
  1. oを読み取る。(区切り文字ではない) 
  2. kを読み取る。(区切り文字ではない)
    ………
  3. おを読み取る。区切り文字なので、その一つ前のkまでを 書き換え範囲とする
2. kakikukekoを削除する
3. IMEをオンにする
4. kakikukekoをキーボード入力する
5. スペースを入力する
6. IMEの変換が終了するまでイベント待機
7. IMEの変換終了イベントを受け取ると、IMEをoffにする

 つまり、完全にアナログなやり方を模倣してるだけです。もっといいやり方あるんだろうけど、いんだよ、出来りゃ。

使い方

 キーバインドはデフォルトではCtrl+;に「ローマ字を再変換する(前方)」とCtrl+Shift+;「ローマ字を再変換する(後方)」を割り当てています。


f:id:nodamushi:20170723052222p:plain

 上↑の画像のような状態でCtrl+;を押すと、下↓の様にIMEで変換できます。

f:id:nodamushi:20170723052322p:plain

 変換完了後はIMEがOFFになります。

f:id:nodamushi:20170723052616p:plain

 「そー言えばこれは変換に失敗します」と打ちたかったのですが、初期設定だと、「そー」が変換されません。

f:id:nodamushi:20170723052649p:plain

 これは、-が区切り文字と見なされているからです。-も変換したい場合はこれを消してください。

f:id:nodamushi:20170723052755p:plain

f:id:nodamushi:20170723052908p:plain

JISキー以外のキーボードの方

 何とか根性でJIS以外のキーボード設定を作って下さい。なんとなく読めば分かると思う。
 ただ、一応くっつけたけど、nodamushiは全くテストしてないので(オイ 動くかは知らない。

ATOKの直前の入力を日本語にするをポップアップさせない方法

 ATOKの直前の入力を日本語にするっていう機能あんまり使わないけど、プログラミングで半角入力のまま日本語入力とか微妙に便利だったりします。
(とある理由で、私にはもう必要の無い機能になったのですが)

 でも、この機能を使っていると、ポップアップがこの上なくウザい。
f:id:nodamushi:20170717141603j:plain

 これを非表示にする方法を調べたのだけども、ググっても出てこなかったのだけど、何故かヘルプの方で検索すると出てきたので、ここでもメモ。

 "日本語入力オフのまま入力した文字を読みにする"というヘルプページに"●こんなときには"という項目に書いてあるのだが

読みへの復帰のキー操作を提示するツールチップを表示したくないときは、以下の手順で設定をオフにします。
1. ATOK プロパティを起動します。
2. [入力・変換]シートの[入力補助-特殊]を選択します。
3. [設定一覧]の[日本語入力オンへの切り替えを通知する]をオフにします。
4. [OK]をクリックします。

 とのこと。
f:id:nodamushi:20170717141845j:plain

 これで確かに出なくなりました。は~、ストレス元が一つ減った。

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

 

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

  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: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"));
    }
  }

 


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

 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);