初出 JAVA PRESS Vol.26

Preferences API 入門

知っておくと便利なアプリケーション設定 API

はじめに

アプリケーションを開発していると、ちょっとしたデータを設定したいことが結構ありませんか。例えば、フレームの位置と大きさとか、ゲームのハイスコアなどです。このようなデータをプリファレンスと呼びます。

普通はプリファレンスを扱うには設定ファイルを使うことが多いと思います。しかし、わざわざ設定ファイルを用意して、それを読み書きするようなコードを書くのも面倒です。また、アプリケーションを jar ファイルだけで配布したいときには、設定ファイルをどこに置くかという問題もあります。

こんなときに役立つのが Java2 Standard Edition, v1.4.0 で導入された Preferences API です。Preferences API はこのようなプリファレンスを管理するための API で、自前で設定ファイルを用意することや JNDI などのディレクトリサービスなどを使用することなく使うことができます。

簡単な使い方

まずは単純にプリファレンスデータの読み込み、書き込みを行ってみましょう。PrefsTest1.java は起動したときに引数があればそれをプリファレンスとして記録し、引数がなければ読み込んで表示します。

オブジェクトの生成

Preferences API のメインとなるクラスは java.util.prefs.Preferences です。このクラスでプリファレンスの読み込み、書き込みなどほとんどのPreferences API で提供している機能をまかなっています。Preferences クラスをインスタンス化するには何種類か方法がありますが、ここでは Preferences クラスの userNodeForPackage メソッドを使用しました (リスト 1-1)。userNodeForPackage メソッドの引数が Class クラスなのは、設定したいプリファレンスデータをパッケージ単位で管理するためなのですが、詳しくは後述します。

データの書き込み

さて、Preferences オブジェクトを生成できたので、データの書き込みを行ってみます。(2) の部分が書き込みを行っている部分です。書き込みには put メソッドを使用します。引数はキーと値の 2 つの文字列になります。ちょうど java.util.Map インタフェースと同じような使い方ですが、キーも値も文字列に限定されています。ここではキーに "name"、値が name 変数となっています。

次の行の flush メソッドはストリームでの flush メソッドの使い方と同じように、強制的に書き込み処理を行う場合に使用します。

データの読み込み

プリファレンスの読み込みには get メソッドを使用します (リスト 1-3)。やはり Map インタフェースと同じような使い方ですが、Map インタフェースの get メソッドと異なり引数は 2 つです。第 1 引数はキーなので Map インタフェースと同じですが、第 2 引数はプリファレンスが存在しなかった場合のデフォルト値を指定するためにあります。Map クラスはキーに相当する要素がなければ null オブジェクトを帰しますが、Preferences クラスは要素がなければ第 2 引数のデフォルト値を帰すようになっています。ここでは "no data" という文字列をデフォルトにしてみました。

それでは実行してみましょう。ここでは Windows2000 で実行した例を示します。c:\prefs ディレクトリの下にパッケージと同じになるように jp ディレクトを作り、その下に順々にパッケージに相当するディレクトリを作り、クラスファイルは prefs ディレクトリに置いてあります。実行結果は図 1 のようになりました。

なにも引数をつけないで実行すると、プリファレンスを読み込みます。しかし、1 回目の実行では、"no data" が出力されています。これは、まだなにもデータを書き込んでていないため、get メソッドの第 2 引数で指定されたデフォルト値が出力されているからです。

2 回目の実行でプリファレンスを書き込みを行っています。一度登録してしまえば、次からはこの値が読み込めます。この値は上書きするか、プリファレンスを削除するまで有効になります。

3 回目の実行では、書き込んだデータを読み込むことに成功しました。

図 1 PrefTest1 の実行結果

 

リスト 1 PrefTest1.java

package jp.ne.airnet.sakuraba.prefs;
 
import java.util.prefs.Preferences;
import java.util.prefs.BackingStoreException;
 
public class PrefsTest1 {
    private Preferences prefs;
    private static final String KEY = "name";
 
    public PrefsTest1() {
        prefs = Preferences.userNodeForPackage(this.getClass()); // (1)
    }
 
    public void save(String name) {
        try {
            System.out.println("Save the name: " + name);
            prefs.put(KEY, name);                                // (2)
            prefs.flush();                                       // (2)
        } catch (BackingStoreException ex) {
            ex.printStackTrace();
        }
    }
 
    public void load() {
        String name = prefs.get(KEY, "no data");                 // (3)
        System.out.println("Load the name: " + name);
    }
 
    public static void main(String[] args) {
        PrefsTest1 test = new PrefsTest1();
        if (args.length > 0) {
            test.save(args[0]);
        } else {
            test.load();
        }
    }
}

プリファレンスの設定

ユーザのプリファレンス

ここで、ちょっとした実験をしてみましょう。まず、先ほどの PrefsTest1 クラスを実行した後、一度ログアウトしてください。次に、もしできるなら、異なるユーザでログインしてみてください。そして、PrefTest1 クラスをもう一度実行して先ほど書き込んだプリファレンスを読み込んでみてください。

プリファレンスの読み込みはできなかったのではないでしょうか。つまり、異なるユーザで実行するとプリファレンスの読み込みが行えないのです。どうしてだと思いますか。

その答えはプリファレンスはユーザごとに設定できるためです。例えば、sakuraba と yuichi という 2 人のユーザが登録されているとします。このとき、ユーザ sakuraba のプリファレンスとユーザ yuichi のプリファレンスは異なっています。

ユーザごとにプリファレンスを設定できるので、たとえば自分の好きな位置にフレームの大きさや位置などを設定でき、その設定は自分だけに有効で他の人には無効になります。

システムのプリファレンス

しかし、ユーザごとではなくてアプリケーションに固有の設定をしたい場合はどうしましょう。たとえば、ゲームのハイスコアはユーザごとに設定するより、アプリケーションで 1 つにしたいですよね。

このような場合はアプリケーション固有の Preferences オブジェクトを用いることで、対応できます。アプリケーション固有のプリファレンスというのは、言い換えればシステムのプリファレンスということができると思います。

先ほどの PrefsTest1 クラスでは Preferences オブジェクトを userNodeForPackage メソッドを使用して取得しましたが、システムのプリファレンスには systemNodeForPackage メソッドを使用して Preferences オブジェクトを取得します。

システムプリファレンスを使用することで、アプリケーションのプリファレンスを設定できるようになります。PrefTest2 クラスは PrefTest1 クラスと異なりシステムのプリファレンスを使用してみました。(1) の部分でシステムの Preferences オブジェクトの取得を行っています。

それでは先ほどと同じように一度実行してデータを書き込んでから、異なるユーザでプリファレンスを読み込んでみましょう。ここでは、ユーザ名の確認のために set コマンドを使用しています。

まず、図 2 に示すように、ユーザ sakruaba で実行します。次に図 3 のように異なるユーザ yuichi で実行しても、ただしくプリファレンスを読み込むことができました。

図 2 システムプリファレンスの使用

図 3 異なるユーザでのシステムプリファレンスの使用

リスト 2 PrefTet2.java

package jp.ne.airnet.sakuraba.prefs;
 
import java.util.prefs.Preferences;
 
public class PrefsTest2 extends PrefsTest1 {
 
    public PrefsTest2() {
        prefs = Preferences.systemNodeForPackage(this.getClass());  // (1)
    }
 
    public static void main(String[] args) {
        PrefsTest2 test = new PrefsTest2();
        if (args.length > 0) {
            test.save(args[0]);
        } else {
            test.load();
        }
    }
}

このようにしてアプリケーション固有の設定データとユーザごとのプリファレンスを分けることができます。

この機能を使うことで、たとえばアプリケーションの設定データのデフォルト値にシステムのプリファレンスを使用し、ユーザがそれを変更したらユーザのプリファレンスに記録するなどの使い方もできると思います。

プリファレンスはどこに?

プリファレンスの格納場所

プリファレンスの読みこみ/書きこみを行うことができましたが、実際のデータはどこにあるのでしょう。それがわからないと心配でしょうがないかたもいらっしゃるのではないでしょうか。

プリファレンスはプラットフォームによって管理の方法が異なります。たとえば、UNIX ではファイル、Windows ではレジストリでプリファレンスデータを保持しています。ここでは、Windows でどのようにデータが管理されているかを説明したいと思います。

Windows ではプリファレンスをレジストリに保持するのですが、ユーザとシステムのプリファレンスでは格納する場所が異なります。ユーザプリファレンスの場合は \\HKEY_USERS\<アカウント名>\Software\JavaSoft\Prefs に格納されます。ログインしている最中は \\HEKY_USERS\<アカウント名> 以下のレジストリは \\HEKY_CURRENT_USER にコピーされるので、こちらを見ても OK です。

システムプリファレンスは \\HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft\Prefs に格納されます。HKEY_LOCAL_MACHINE はマシンに固有の設定を行うためのレジストリです。

どのように書き込まれる?

どのようにレジストリに書きこまれるか、先ほどの PrefTest1.java を使って試してみましょう。それに先立って、一度も Preferences API を使ったことない場合のレジストリを見ておきます。レジストリを見るには regedit を使用します。図 4 が Preferences API を使う前の状態です。references API を使っていない状態だと、レジストリには Prefs というキーがあるだけで、値はなにもセットされていません。

ここで、PrefsTest1 を実行します。すると、図 5 のようにレジストリが次のように変化します。Prefs 以下にパッケージに対応したキーが作成されています。そしてパッケージに対応するところに name というキーが作成され、その値として sakuraba 登録されています。

Registry before using Prefs API

図 4 Preferences API を未使用時のレジストリ

Registry after using Prefs API

図 5 Preferences API を使った後のレジストリ

システムプリファレンスの場合でも同じように登録が行われます。

ここで注目していただきたいのが、Preferences API ではプリファレンスをパッケージを基にして管理しているということです。プログラム中では単にキーと値しか使っていないのですが、実際にはそれにパッケージ名を付加して、レジストリにツリー状に登録を行っているのです。

こうすることで、同じキーを使用しても、パッケージが異なれば区別して使うことができます。

このパッケージツリーを作るために Preferences オブジェクトを生成する userNodeForPackage メソッドや systemNodeForPackage メソッドでは引数に Class クラスを使用していたのです。

ほかにもある Preferences の取得方法

アプリケーションの中でプリファレンスがツリーのどこにデータを登録しているかを知るには Preferences クラスの absolutePath メソッドを、またuser か system かを知るには isUserNode メソッドを使います。簡単なサンプル PrefsTest3 クラス(リスト 3)で試してみましょう。

実行結果は図 6のようになります。次に、ソースの中の userNodeForPackage メソッドを systemNodeForPackage メソッドに代えて実行してみてください。ちゃんと出力には反映されていると思います。

ところで、今までは Preferences オブジェクトの取得には userNodeForPackage メソッドと systemNodeForPackage メソッドを使いましたが、これらのメソッドを使う以外にも Preferences を取得する方法があります。

それは Preferences クラスの userRoot メソッドもしくは systemRoot メソッドと node メソッドを組みあわせる方法です。userRoot メソッドはユーザノードのルート、レジストリであれば Prefs に相当するプリファレンスが取得できます。systemRoot メソッドは同様にシステムノードのルートを取得できます。プリファレンスを取得した後に、node メソッドを使うことで、現在のノードの子プリファレンスを取得することができます。

PrefsTest4 クラスはルート以下のすべてのノードを書きだすクラスです。PrefTest4 クラスはプリファレンスのツリーをたどるために再帰的に子プリファレンスを取得しています。

まず、(1) の部分で userRoot メソッドを使用して、ユーザのルートプリファレンスを取得します。現在のプリファレンスの子プリファレンスを調べるには (2) の部分で使用しているように childrenNames メソッドを使います。

パッケージに対応するノードを取得するには node メソッドを使用します。node メソッドの引数は文字列で、取得するノードを記述します。このとき、パッケージの区切り文字には "/" (スラッシュ) を使います。例えばルートは / だけで、jp.ne.airnet.sakuraba.prefs パッケージであれば /jp/ne/airnet/sakuraba/prefs になります。

nodeメソッドの引数は「/」で始まれば絶対パス、「/」でなければ相対パスになります。childrenNameメソッドの戻り値は相対パスになるので、それをnodeメソッドの引数にして子プリファレンスを取得するようにしました(リスト4-3)。

実行した結果は図 7 のようになりました。ノードがツリーで出力されているのが確認できると思います。

図 6 PrefsTest3 の実行例

図 7 PrefsTest4 の実行例

リスト 3 PrefsTet3.java

package jp.ne.airnet.sakuraba.prefs;
 
import java.util.prefs.Preferences;
 
public class PrefsTest3 {
    Preferences prefs;
 
    public PrefsTest3(){
        prefs = Preferences.userNodeForPackage(this.getClass());
//        prefs = Preferences.systemNodeForPackage(this.getClass());
 
        System.out.print("Prefs Node is : ");
        // ユーザかシステムかを調べる
        if(prefs.isUserNode()){
            System.out.println("User");
        }else{
            System.out.println("System");
        }

        // プリファレンスの場所を調べる
        System.out.println("Prefs Node Name : " + prefs.absolutePath());
    }
 
    public static void main(String[] args){
        new PrefsTest3();
    }
}

 

リスト 4 PrefsTet4.java

package jp.ne.airnet.sakuraba.prefs;
 
import java.util.prefs.Preferences;
import java.util.prefs.BackingStoreException;
 
public class PrefsTest4 {
    private Preferences prefs;
 
    public PrefsTest4(){
        prefs = Preferences.userRoot();                  // (1)
    }
 
    public void printNode(){
        System.out.println();
        
        try{
            String[] children = prefs.childrenNames();   // (2)
            printNode(prefs, children, 0);
        }catch(BackingStoreException ex){
            ex.printStackTrace();
        }
    }
        
    public void printNode(Preferences parent, String[] childrenNames, int depth){
        try{
            for(int i = 0 ; i < childrenNames.length ; i++){
                for(int j = 0 ; j < depth*2 ; j++){
                    System.out.print(" ");
                }
                System.out.println(childrenNames[i]);

                Preferences node = parent.node(childrenNames[i]);    // (3)
                printNode(node, node.childrenNames(), depth + 1);
            }
        }catch(BackingStoreException ex){
            ex.printStackTrace();
        }
    }
 
    public static void main(String[] args){
        PrefsTest4 test = new PrefsTest4();
        test.printNode();
    }
}

応用例

サンプルの概要

今までは、Preferences API の仕組みを説明するだけのサンプルアプリケーションだったので、もうちょっと具体的に Preferences API が役に立つようなサンプルを作ってみました。簡単で分かりやすい例ということで、サイコロを選んでみました。

サイコロアプリケーションの中では

の 2 つの設定にプリファレンスを使用しています。また、システムプリファレンスをデフォルト値、ユーザプリファレンスを個人の設定に使用しました。

リスト 6 にサイコロの Dice.java のソースを示しました。サイコロの主な動作は、フレームに貼りつけられたボタンが押されたらスレッドを起動して、定期的に乱数を生成し、その値に応じた画像を表示。再び、ボタンが押されたらスレッドを停止させるというものです。スレッドを停止したときの画像がサイコロの目になります。

図 8 がサイコロの実行画面になります。サイコロの画像ファイルは変更することができ、図 9 でメニューを選択し、画像ファイルを指定すると図 10 のように表示が変化します。


図 8 サイコロの実行画面


図 9 画像ファイルの変更

図 10 変更後の実行画面

プリファレンスの取得から消去まで

Dice クラスではプリファレンスを次のように扱っています。

  1. システムプリファレンスとユーザプリファレンスの取得
  2. システムプリファレンスを使用したデフォルト値の読み込み
  3. ユーザプリファレンスから個人設定の読み込み
  4. フレームが移動されたときに、フレーム位置をユーザプリファレンスに書き込み
  5. メニューから画像ファイルを指定されたときに、ファイル名をユーザプリファレンスに書き込み
  6. メニューからデフォルトの読み込みを指定されたとき、システムプリファレンスからデフォルト値を読み込み、ユーザプリファレンスを消去する

プリファンレスの取得と読み込み

1 の処理のプリファレンスの取得はリスト 5-1 で行っています。特に今までのサンプルと変わるところはなく、systemNodeForPackage メソッドと userNodeForPackage メソッドを使用しています。

次が、デフォルト値および個人設定の読み込みです。リスト 5-2 でデフォルト値の読みこみ (システムプリファレンスを使用した読み込み)、リスト 5-3 で個人設定の読み込み (ユーザプリファレンスを使用した読み込み) を行っています。

このとき、フレーム位置は整数なので、今まで使用してきた get メソッドではなく、整数値を読み込むための getInt メソッドを使用しています。

Preferences クラスでは getInt メソッド以外にも、プリミティブ型をプリファレンスとして使用できるようにするためのメソッド群が用意されています。これらメソッド群の一覧を表 1 にまとめました。

各メソッドとも get メソッド、put メソッドと引数の型が異なるだけで、使い方は変わりません。

実行

4 から後の処理が実行時の処理になります。

フレームの移動の検出には ComponentEvent クラスを使用しています。フレームが移動すると、ComponentListener インタフェースの componentMoved メソッドがコールされるので、そこでプリファレンスの書きこみを行っています (リスト 5-4)。ここでも、フレーム位置が整数値なので put メソッドではなく、putInt メソッドを使用しています。

5 と 6 の処理はメニューから選択して行います。5 の処理の画像ファイルの選択は単にダイアログから入力された文字列をユーザプリファレンスに書きこむだけです(リスト 5-6)。

6 の処理のデフォルトの読みこみを行うときは、システムプリファレンスから再度読みこみを行うだけでいいように思えます。しかし、これだけだと、アプリケーションを終了させて、再び起動したときにユーザプリファレンスが読みこまれてしまいます。そこで、ユーザプリファレンスの消去が必要になります。

プリファンレスの消去

プリファレンスの消去を行っているのが Dice クラスの removeUserPreferences メソッドです(リスト 5-6)。実際にプリファレンスを消去するには Preferences クラスの remove メソッドを使用します。ここで注意しなくては行けないのが、消去したいプリファレンスが存在するかどうかです。 removeUserPreferences メソッドではプリファレンスの確認のための if 文が存在していますが、これをとってしまってリスト 5 のように単に remove メソッドをコールするだけにしてみたらどうなるでしょうか。

まだユーザプリファレンスを設定していない状態で「デフォルトの読みこみ」を行ってみた結果が図 11 です。図 11 ではコンソールにはなにやらエラーらしきものが出力されました。しかし、例外が発生したというわけではないようです。また、この後の動作がおかしくなるということもありませんでした。

実をいうと、このメッセージは J2SE v1.4.0 から導入された Logging API によるものです。例外ではないとすると、Preferences API と同じように J2SE v1.4.0 で導入された Assertion が発生しているのかと思い、java のオプションでシステムの Assertion を有効にする -esa オプションを付加して実行してみましたが、Assertion は発生していないようでした。

とはいうものの、このようなログが出力されるのもいやなので、プリファレンスを消去する場合にはプリファレンスがあるかどうかを確認したほうがいいようです。

図 11 「デフォルトの読みこみ」の実行

表 1 ユーティリティメソッド群

メソッド名 使用できる型
getBoolean(String key, boolean default)
putBoolean(String key, boolean value)
boolean
getByteArray(String key, byte[] default)
putByteArray(String key, byte[] value)
byte[]
getInt(String key, int default)
putInt(String key, int value)
int
getLong(String key, long default)
putLong(String key, long value)
long
getFloat(String key, float default)
putFloat(String key, float value)
float
getDouble(String key, double default)
putDouble(String key, double value)
daouble

 

リスト 5 Dice.java

package jp.ne.airnet.sakuraba.prefs.dice;
 
import java.awt.Component;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.prefs.Preferences;
import java.util.prefs.BackingStoreException;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
 
public class Dice implements Runnable {
    protected Preferences systemPrefs;
    protected Preferences userPrefs;
 
    // プリファレンスのキー
    private static final String FILEBASE = "filebase";
    private static final String LOCATION_X = "x";
    private static final String LOCATION_Y = "y";
 
    // プリファレンスのデフォルト値
    private static final String DEFAULT_FILEBASE = "dice";
    private static final int DEFAULT_X = 100;
    private static final int DEFAULT_Y = 100;
 
    private static final int NO_DEF = -1;
    private static final int MAX_NUMBER = 6;
 
    // 画像ファイル名
    private String filebase;
 
    // フレーム位置
    private int locationX;
    private int locationY;
 
    protected JFrame frame;
    private JButton button;
    protected JMenu menu;
 
    // イメージを保持するためのリスト
    private List skins;
 
    private Thread thread;
 
    public Dice() {
        // プリファレンスの読みこみ
        loadPreferences();
         
        // フレームの初期設定
        initFrame();
 
        // メニューの初期設定
        initMenu();
 
        // イメージの取りこみ
        skins = new ArrayList();
        setupImages();
 
        // ボタンの初期設定
        initButton();
 
        frame.getContentPane().add(button);
        frame.pack();
        frame.setVisible(true);
    }
 
    // フレームの初期設定
    private void initFrame() {
        frame = new JFrame("Dice");
        setupLocation();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        
        // フレームが移動したときの処理
        frame.addComponentListener(new ComponentAdapter() {
            public void componentMoved(ComponentEvent event) {
                Component comp = event.getComponent();
                // フレーム位置の書きこみ
                saveLocationPreferences(comp.getLocation());
            }
        });
    }
 
    // メニューの初期設定
    private void initMenu() {
        JMenuBar menuBar = new JMenuBar();
        menu = new JMenu("File");
        menuBar.add(menu);
 
        JMenuItem item = new JMenuItem("Load Images...");
        item.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                stop();
 
                // 画像ファイルの再設定
                updateFilebase();
                setupImages();
                frame.pack();
            }
        });
        menu.add(item);
         
        item = new JMenuItem("Load Default");
        item.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                stop();
 
                // システムプロパティの読みこみ
                loadSystemPreferences();
                setupLocation();
                setupImages();
                frame.pack();
 
                // ユーザプロパティの削除
                removeUserPreferences();
            }
        });
        menu.add(item);
 
        item = new JMenuItem("Exit");
        item.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                System.exit(1);
            }
        });
        menu.add(item);

        frame.setJMenuBar(menuBar);
    }
 
    // ボタンの初期設定
    private void initButton() {
        button = new JButton((ImageIcon)skins.get(0));
        button.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                processMouseEvent();
            }
        });
    }
 
    // イメージの設定
    protected void setupImages() {
        skins.clear();
        for (int i = 1 ; i <= MAX_NUMBER ; i++) {
            ImageIcon icon = new ImageIcon(filebase + i + ".jpg");
            skins.add(icon);
        }
 
        if (button != null) {
            button.setIcon((ImageIcon)skins.get(0));
        }
    }
 
    // フレーム位置の設定
    protected void setupLocation() {
        frame.setLocation(locationX, locationY);
    }
 
    // プリファレンスの読みこみ
    private void loadPreferences() {
        // システムプリファレンス
        systemPrefs = Preferences.systemNodeForPackage(this.getClass()); // (1)
        loadSystemPreferences();
 
        // ユーザプリファレンス
        userPrefs = Preferences.userNodeForPackage(this.getClass());     // (1)
        loadUserPreferences();
    }
 
    // システムプリファレンスの読みこみ
    private void loadSystemPreferences() {
        locationX = systemPrefs.getInt(LOCATION_X, DEFAULT_X);          // (2)
        locationY = systemPrefs.getInt(LOCATION_Y, DEFAULT_Y);          // (2)
 
        filebase = systemPrefs.get(FILEBASE, DEFAULT_FILEBASE);         // (2)
    }
 
    // ユーザプリファレンスの読みこみ
    protected void loadUserPreferences() {
        int tmpX = userPrefs.getInt(LOCATION_X, NO_DEF);                // (3);
        if (tmpX != NO_DEF) {
            locationX = tmpX;
        }
 
        int tmpY = userPrefs.getInt(LOCATION_Y, NO_DEF);                // (3)
        if (tmpY != NO_DEF) {
            locationY = tmpY;
        }
 
        String tmpFileBase = userPrefs.get(FILEBASE, DEFAULT_FILEBASE); // (3)
        if (!filebase.equals(tmpFileBase)) {
            filebase = tmpFileBase;
        }
    }
 
    // ウィンドウ位置の保存
    private void saveLocationPreferences(Point location) {
        userPrefs.putInt(LOCATION_X, location.x);                      // (4)
        userPrefs.putInt(LOCATION_Y, location.y);                      // (4)
    }
 
    // 画像ファイル設定の保存
    private void saveFilebasePreferences() {
        userPrefs.put(FILEBASE, filebase);                             // (5)
    }
        
    // ユーザプリファレンスの削除
    // プリファレンスが存在するかを確かめてから、
    // 削除を行う
    private void removeUserPreferences() {                             // (6)
        if (userPrefs.getInt(LOCATION_X, NO_DEF) != NO_DEF) {          // (6)
            userPrefs.remove(LOCATION_X);                              // (6)
        }                                                              // (6)
 
        if (userPrefs.getInt(LOCATION_Y, NO_DEF) != NO_DEF) {          // (6)
            userPrefs.remove(LOCATION_Y);                              // (6)
        }                                                              // (6)
 
        if (!userPrefs.get(FILEBASE, "").equals("")) {                 // (6)
            userPrefs.remove(FILEBASE);                                // (6)
        }                                                              // (6)
    }
 
    // 画像ファイルの更新
    private void updateFilebase() {
        String tmpFilebase = JOptionPane.showInputDialog(frame,
                         "画像ファイル名を入力してください\n画像ファイルは [ファイル名][1〜"
                         + MAX_NUMBER + "].jpg になります");
        if (tmpFilebase != null) {
            int i = 1;
            for (; i <= MAX_NUMBER ; i++) {
                if (!new File(tmpFilebase + i + ".jpg").exists()) {
                    break;
                }
            }
                 
            if (i == MAX_NUMBER + 1){
                filebase = tmpFilebase;
                saveFilebasePreferences();
            } else {
                JOptionPane.showMessageDialog(frame, tmpFilebase + i + ".jpg がありません",
                                              "Warning", JOptionPane.WARNING_MESSAGE);
                updateFilebase();
            }
        }
    }
 
    // MouseEvent の処理
    // スレッドが動作しているときは停止、
    // スレッドが停止しているときは開始させる
    private void processMouseEvent() {
        if (thread == null) {
            start();
        } else {
            stop();
        }
    }
 
    // スレッドの開始
    private void start() {
        thread = new Thread(this);
        thread.start();
    }
 
    // スレッドの停止
    protected void stop() {
        if (thread != null) {
            thread.interrupt();
            thread = null;
        }
    }
 
    public void run() {
        Thread currentThread = Thread.currentThread();
        Random rand = new Random();
 
        while (thread == currentThread) {
            // 乱数でサイコロの目を選択して、表示を行う
            int value = rand.nextInt(MAX_NUMBER);
            button.setIcon((ImageIcon)skins.get(value));
 
            try {
                Thread.sleep(100L);
            } catch (InterruptedException ex) {
                return;
            }
        }
    }
 
    public static void main(String[] args) {
        new Dice();
    }
}

 

リスト 6 プリファレンスの存在確認を行わない removeUserPreferences メソッド

    private void removeUserPreferences() {
        userPrefs.remove(LOCATION_X);
        userPrefs.remove(LOCATION_Y);
        userPrefs.remove(FILEBASE);
    }

プリファレンスのインポート/エクスポート

あるコンピュータで使っていたアプリケーションの設定を他のコンピュータに持っていきたくなることや、ユーザの設定を他のユーザに移したいということはよくあることではないでしょうか。設定ファイルを使用している場合は、そのファイルをコピーすればいいのですが、プリファレンスを使っている場合にはどうすればいいでしょうか。

そんなときのために、Preferences クラスにはプリファレンスをファイルにエクスポートしたり、ファイルをインポートするためのメソッドが用意されています。これを使えば、プリファレンスを他に持っていくことも可能です。

ファイルへのエクスポートには exportNode メソッドか exportSubtree メソッドを使用します。引数はどちらも OutputStream クラスです。この 2 つのメソッドの違いは現在のノードの下位のノードをエクスポートするかどうかです。exportNode メソッドは現在のノード、exportSubtree メソッドは下位のノードも含めてエクスポートします。

ファイルをインポートするには importPreferences メソッドを使用します。引数は InputStream クラスです。

先ほどのサイコロアプリケーションにプリファレンスのインポートとエクスポートを付け加えてみたのが Dice2 クラスです (リスト 7)。

Dice2 クラスはプリファレンスをファイルにエクスポートするための exportUserPreferences メソッドと、選択したファイルからプリファレンスをインポートする importUserPreferences メソッドを追加しました。それぞれのメソッドはメニューのイベント処理からコールされるようになっています。

まず、プリファレンスのエクスポートです。エクスポートするファイルの選択には JFileChooser クラスを使用しました。JFileChooser で選択したファイルのストリームを生成しているのがリスト9-1 の部分です。ストリームに対してプリファレンスをエクスポートしているのが exportNode メソッドです (リスト 9-2)。

プリファレンスをエクスポートしたファイルのサンプルをリスト 8 に示します。エクスポートしたプリファレンスのファイル形式は XML となります (図 12)。この XML ドキュメントの DTD は Preferences クラスの JavaDoc に示されています。

パッケージ名に対応するのが node タグになります。node タグには map タグが含まれており、その中に複数の entry タグがあるという構造になっています。1 つ 1 つのプリファレンスは entry タグで表されます。entry タグには属性として key と value があり、これがプリファレンスのキーと値に相当します。

次に、ファイルのインポートです。ファイルの選択はエクスポートと同様に JFileChooser クラスを使用しました。ファイルからストリームの生成がリスト 10 の (1) の部分、ストリームに対してインポートを行っているのが (2) の部分になります。インポートしたプリファレンスを読み込んでプロパティに代入し、設定しているのが (3) の部分です。

このようにプリファレンスをファイルにエクスポート/インポートすることはとても簡単にできます。ここではユーザプリファレンスの例を示しましたが、システムプリファレンスのエクスポート/インポートも同様に行うことが可能です。また、エクスポート/インポートするのはストリームに対してなので、ファイルに限らず、ソケット通信などにも使用することができます。

図12 設定のエクスポート

 

リスト 7 Dice2.java

package jp.ne.airnet.sakuraba.prefs.dice;
 
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.prefs.Preferences;
import java.util.prefs.BackingStoreException;
import java.util.prefs.InvalidPreferencesFormatException;
import javax.swing.JFileChooser;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
 
public class Dice2 extends Dice {
 
    public Dice2() {
        super();
        addMenu();
    }
 
    // メニューの追加
    //     追加メニュー    設定をファイルから読み込む
    //                     設定をファイルに書き込む
    private void addMenu() {
        JMenuItem item = new JMenuItem("Import Configuration...");
        item.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                stop();
                importUserPreferences();
            }
        });
        menu.add(item, 2);
 
        item = new JMenuItem("Export Configuration...");
        item.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                stop();
                exportUserPreferences();
            }
        });
        menu.add(item, 3);
    }
 
    // ファイルからプリファレンスを読み込む
    private void importUserPreferences() {
        FileInputStream stream = null;
 
        try {
            JFileChooser chooser = new JFileChooser();
            chooser.setFileFilter(new XMLFileFilter());
            chooser.setCurrentDirectory(new File("."));
 
            if (chooser.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION) {
                stream = new FileInputStream(chooser.getSelectedFile());
                userPrefs.importPreferences(stream);
                 
                loadUserPreferences();
                setupLocation();
                setupImages();
            }
        } catch (FileNotFoundException ex) {
            warnFileImporting();
        } catch (IOException ex) {
            warnFileImporting();
        } catch (InvalidPreferencesFormatException ex) {
            warnFileImporting();
        } finally {
            if (stream != null) {
                try {
                    stream.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
 
    // ファイルにプリファレンスを書き込む
    private void exportUserPreferences() {
        FileOutputStream stream = null;
 
        try{
            JFileChooser chooser = new JFileChooser();
            chooser.setFileFilter(new XMLFileFilter());
            chooser.setCurrentDirectory(new File("."));
 
            if (chooser.showSaveDialog(frame) == JFileChooser.APPROVE_OPTION) {
                stream = new FileOutputStream(chooser.getSelectedFile());
                userPrefs.exportNode(stream);
            }
        } catch (FileNotFoundException ex){
            warnFileExporting();
        } catch (IOException ex) {
            warnFileExporting();
        } catch (BackingStoreException ex) {
            warnFileExporting();
        } finally {
            if (stream != null) {
                try {
                    stream.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
 
    private void warnFileImporting() {
        JOptionPane.showMessageDialog(frame, "インポートに失敗しました",
                                      "Warning", JOptionPane.WARNING_MESSAGE);
    }
 
    private void warnFileExporting() {
        JOptionPane.showMessageDialog(frame, "エクスポートに失敗しました",
                                      "Warning", JOptionPane.WARNING_MESSAGE);
    }
 
    public static void main(String[] args) {
        new Dice2();
    }
}

 

リスト 8 エクスポートしたファイルサンプル

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE preferences SYSTEM 'http://java.sun.com/dtd/preferences.dtd'>

<preferences EXTERNAL_XML_VERSION="1.0">
  <root type="user">
    <map />
    <node name="jp">
      <map />
      <node name="ne">
        <map />
        <node name="airnet">
          <map />
          <node name="sakuraba">
            <map />
            <node name="prefs">
              <map />
              <node name="dice">
                <map>
                  <entry key="x" value="100" />
                  <entry key="y" value="100" />
                  <entry key="filebase" value="number" />
                </map>
              </node>
            </node>
          </node>
        </node>
      </node>
    </node>
  </root>
</preferences>

 

図 13 設定のインポート

 

リスト 9 exportUserPreferences メソッド

    private void exportUserPreferences() {
        FileOutputStream stream = null;
 
        try{
            JFileChooser chooser = new JFileChooser();
            chooser.setFileFilter(new XMLFileFilter());
            chooser.setCurrentDirectory(new File("."));
 
            if (chooser.showSaveDialog(frame) == JFileChooser.APPROVE_OPTION) {
                stream = new FileOutputStream(chooser.getSelectedFile());           // (1)
                userPrefs.exportNode(stream);                                       // (2)
            }
        } catch (FileNotFoundException ex){
            warnFileExporting();
        } catch (IOException ex) {
            warnFileExporting();
        } catch (BackingStoreException ex) {
            warnFileExporting();
        } finally {
            if (stream != null) {
                try {
                    stream.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }

 

リスト 10 importUserPreferences メソッド

    private void importUserPreferences() {
        FileInputStream stream = null;
 
        try {
            JFileChooser chooser = new JFileChooser();
            chooser.setFileFilter(new XMLFileFilter());
            chooser.setCurrentDirectory(new File("."));
 
            if (chooser.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION) {
                stream = new FileInputStream(chooser.getSelectedFile());         // (1)
                userPrefs.importPreferences(stream);                             // (2)
                
                loadUserPreferences();                                           // (3)
                setupLocation();                                                 // (3)
                setupImages();                                                   // (3)
            }
        } catch (FileNotFoundException ex) {
            warnFileImporting();
        } catch (IOException ex) {
            warnFileImporting();
        } catch (InvalidPreferencesFormatException ex) {
            warnFileImporting();
        } finally {
            if (stream != null) {
                try {
                    stream.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }

おわりに

J2SE v1.4.0 ではいろいろな機能が新たに追加されました。Preferences API もその中の 1 つですが、New I/O や Logging API に比べると比較的地味ではあります。しかし、いままで面倒であったアプリケーションの設定を簡単に行える Preferences API は、もっと使われてもいい API であると思います。ぜひとも活用してみてください。

(2002.09)