設定を保存する。

ほとんどのプラグインには、必ず作りこまなければならない機能がある。


その一つが、設定を保存する機能だ。実は、これが結構な曲者だったりするんだよねぇ…。


理由の一つは、eclipseのバージョンが上がるたびにAPIが改善されていっているからだ。
現時点でのAPIの実装を見る限り、まだまだ改善の余地がある。
よって、今回のエントリは、3.1及び3.2でしか通用しない知識になる可能性が高い。
でも、知ってるかそうでないかで、手間が全然違うので、覚えておくと良い感じ。


設定を保存しておくには、大雑把に言って、3種類のAPIが用意されている、
と言うか、僕が、それ以外の方法を知らない。もしかしたら、第四の方法があるかも。

嫌がらせにしか見えない。IDialogSettings

我流でプラグインを実装すると、多分一番最初に使う。と言うか自分が使ったAPIがコレ。
何故なら、プラグインプロジェクトのウィザードが作ってくれるAbstractUIPluginが持っていて一見すると使い易そうだからだ。


さて、間違い探し。5分以内にこのAPIが嘘臭い事が分ったら拍手が貰える。
余り分り易さを考慮せず、定義されている順に並べるとこんな感じ。
出来れば少し考えてみて欲しい。

public interface IDialogSettings {
    public IDialogSettings addNewSection(String name);
    public void addSection(IDialogSettings section);
    public String get(String key);
    public String[] getArray(String key);
    public boolean getBoolean(String key);
    public double getDouble(String key) throws NumberFormatException;
    public float getFloat(String key) throws NumberFormatException;
    public int getInt(String key) throws NumberFormatException;
    public long getLong(String key) throws NumberFormatException;
    public String getName();
    public IDialogSettings getSection(String sectionName);
    public IDialogSettings[] getSections();
    public void load(Reader reader) throws IOException;
    public void load(String fileName) throws IOException;
    public void put(String key, String[] value);
    public void put(String key, double value);
    public void put(String key, float value);
    public void put(String key, int value);
    public void put(String key, long value);
    public void put(String key, String value);
    public void put(String key, boolean value);
    public void save(Writer writer) throws IOException;
    public void save(String fileName) throws IOException;
}

基本的には、XMLとしてシリアライズされる為、DOM的な再起構造をしたAPIになっている。
load、saveメソッドもそれぞれ2種類あり、それなりに必要なモノは全てそろっている様な錯覚を覚えるかもしれない。と言うか少なくとも、僕は錯覚した。


よくよく見ると気付くのだけど、newSectionと言う追加の為のAPIはあるのに、
removeSectionやdeleteSectionと言った削除の為のAPIが存在しないのだ。


実装クラスのDialogSettingsと言うクラスの内部表現としてはMapが使われているので、
削除のオペレーションは実現出来る筈なのだけど、何故か定義されていない。
どういう事なのか、いまだに謎。


結局の所、loadしたインスタンスは、設定を読み込んだら、そのまま捨てて、
新しいインスタンスを作って、saveする事によって、
まぁ、差分があれば削除された事にナルヨネって事だと、理解しているのだけど。

なるほど便利、でもモノ足りない。ScopedPreferenceStore

こいつは、存在を発見するまで大いに時間がかかったAPIです。
僕と同じ轍を踏まない様に、これからプラグインを作る人は使い方を是非覚えて下さい。
但し、eclipse3.1で追加されたAPIですが、持ちうるデータ構造がダサい為、
将来に渡って現状のままとはとても思えないケド。


まずは、インスタンスを作るコードを見るのがヨロシイかと。

new ScopedPreferenceStore(new ProjectScope(project),Constants.ID_PLUGIN);

こんだけ。知ってしまえば、何のこたぁ無い。超簡単。


コンストラクタは、2種類の引数を要求するます。

  • IScopeContext
  • String

まずは、説明の簡単な後者から。
要は、eclipseにインストールされたプラグインの中で一意になる様なIDを文字列で渡せば良いだけ。
詰まる所、PluginIDを渡せって事です。Doltengなら、「org.seasar.dolteng.eclipse」。


最初に引き渡すIScopeContextがちょっとだけ難しい。
こいつの実装クラスとしては、幾つかあるます。
それぞれ、設定内容をどこに保存するかを意味しているます。
影響範囲の広い順に並べるとこんな感じ。

  • ConfigurationScope
  • DefaultScope
    • 基本的には、保存されない。位置づけがちょっと特殊で、Scopeを越えてデフォルト値を格納する為に使われる。使い方は後述。
  • InstanceScope
    • ワークスペース単位に作成される.metadata/.plugins/org.eclipse.core.runtime/.settingsと言うディレクトリの内に、引数で渡したIDと同じディレクトファイルを作ってそこに設定を保存する。ワークスペース単位なのは、正しいのだけど、名前の意味がチト理解し難い上に、実装が鼻血吹くほど複雑。でも、まぁ、説明してしまえば簡単。
  • ProjectScope
    • 指定したプロジェクト毎に、.pref.settingsと言うディレクトリを作りその中に指定したID名の設定ファイルを保存する。eclipse3.1になってから、みんな何となくディレクトリの存在には気付いてた筈。


ScopedPreferenceStoreが実装しているinterfaceは二つあるのだけど、
知らなきゃいけないAPIは、IPreferenceStoreの方だけ。
他にもイッパイあるけど、大体こんなメソッドが定義されている。

public void setValue(String name, String value);
public void setDefault(String name, String defaultObject);
public String getDefaultString(String name);
public String getString(String name);

public void setValue(String name, int value);
public void setDefault(String name, int value);
public int getDefaultInt(String name);
public int getInt(String name);
・・・
後は、他のプリミティブ型毎に同じセットの繰り返し。

保存されるのは、propertiesファイルと同じで、平らなデータ構造しか定義出来ない様になっていて、
IDialogSettingsの様に、入れ子構造の設定を保持する事は出来ません。
ちょっとしたトリックを使えば、入れ子構造っぽい事は出来るけどね…。
それは、まぁ宿題って事で。
JDTなんかは、力技で、スゴイ量のスゴイ構造の設定を保持してるケド。


使い方は、setValueで値を設定して、getStringやらgetIntやらで値を取り出すだけ。
但し、get系は、少し賢い動きをしててsetValueされていなければ、setDefaultで設定された値が返ってくるます。


ここで、前出のDefaultScopeが効いてくるのだけど、
setDefaultは、それを行う為に特別なAPIが用意されているます。
それが、PreferenceInitializerです。
plugin.xmlに記述されるextension pointは、こんな感じ。

   <extension
         point="org.eclipse.core.runtime.preferences">
      <initializer class="org.seasar.dblauncher.preferences.PreferenceInitializer"/>
   </extension>

実装コードは、こんな感じ。

public class PreferenceInitializer extends AbstractPreferenceInitializer {
    public PreferenceInitializer() {
        super();
    }
    public void initializeDefaultPreferences() {
        IEclipsePreferences pref = new DefaultScope()
                .getNode(Constants.ID_PLUGIN);
        pref.put(Constants.PREF_DB_PORTNO, String
                .valueOf(TcpServer.DEFAULT_PORT));
        pref.put(Constants.PREF_WEB_PORTNO, String
                .valueOf(org.h2.engine.Constants.DEFAULT_HTTP_PORT));
        pref.putBoolean(Constants.PREF_IS_DEBUG, false);
        pref.put(Constants.PREF_USER, "sa");
    }

ScopedPreferenceStoreの引数にDefaultScopeを渡していると、
分り易いのかもしれなけれど、DbLauncherでは、何故かこんな風になってるし。
IPreferenceStoreって言ってた直後に、IEclipsePreferencesなんて出てきちゃって、
混乱しかねない勢いのコードだけど、書いてる時には、神が降りてきてたと思われ。
結果的には同じ事になるので、ほったらかし。
何故、同じ事になるのかは、面倒なので説明しない方向で。
普通に設定を保持する時に、これと同じ様なコーディングスタイルにならないのには、それなりに理由があるのだけど、それも面倒なので説明しません。


所で、ここまで来て気になっている事があるかもしれない。
設定を保存するのは、どうすんだ?と。
結論だけ言うと、ScopedPreferenceStoreは、インスタンス作ってほったらかしておけば、
テキトーに保存されるます。
saveメソッドはありますが、ぶっちゃけると、呼ばない方が無難です。
Fileを開いてデータを読んだり、設定された値を保存したりってのは、賢く賢くやってくれます。
内部的には、かなり複雑なクラス構造と処理構造をしていて説明してもシンドイだけなので、説明しません。
ヒントは、最後に出てきたIEclipsePreferences。
どうしても気になるのであれば、自前でコード読んでクダサイな。

ちょっとだけなら、これで良いんじゃね?IResource#setPersistentProperty

とまぁ、設定を保持する為の本筋を説明したのだけど、
設定項目が少ない時にはもっと簡単な方法がある。
それが、IResource#setPersistentPropertyを呼んで、設定を保持し、
IResource#getPersistentPropertyを呼んで設定を取り出すって方法。


メソッドの定義はこんな感じ。

public void setPersistentProperty(QualifiedName key, String value) throws CoreException;

一つ目の引数は、まぁ、String+Stringで当該リソースに設定されるキーの中で一意になる様に文字列を決めて作るインスタンス
テキトーにプラグインのID辺りを渡せば簡単にインスタンスを作れる。


今日は、IResourceについて詳しく説明しないけど、
簡単に説明するとIResourceはeclipseが管理しているリソースを表すインターフェース。
例えば、サブインターフェースには、

  • IContainer
  • IWorkspace
  • IProject
  • IFile

なんてのがある。それぞれは、まぁ、良い感じに名は体を現しているので、
説明しなくても想像がつくと思う。
setPersistentPropertyを呼び出す事で、各リソースに紐付く形で値を保持する事が出来る。
つまりは、ワークスペース単位の設定なら、IWorkspaceのインスタンスを取って*1setPersistentProperty。
プロジェクト単位の設定なら、IProjectのインスタンスを取ってsetPersistentPropertyってすれば良い訳。


まぁ、メソッドのシグネチャを見て分る通り、Stringしか保持出来ないし、
デフォルト値を設定したりする事も出来ないから、使い勝手が凄く良いわけでは無いけど、
設定項目が1つか2つしか無いうちは、ゴリゴリとコードを書くのもアフォらしいし。
簡単なやり方もあるって事で。
これまた、今日は説明しないけど、setPersistentPropertyで設定された値は、
プラグインの静的な構造を定義する筈の、plugin.xmlから参照出来るもんで、
事と次第によっては、良い感じに使える事もあるんだよねぇ。
それについては、また今度。


設定の保存周りは、何故か良い感じにまとまったドキュメントがどこにも無いので、
今日は真面目に書いたら、エライ時間になってもうた。

*1:取り方は明日以降に説明するます。