torutkのブログ

ソフトウェア・エンジニアのブログ

JavaFXで作ったカレンダーに祝日の設定ファイルを読み込み反映させる

デスクトップにカレンダーを表示するプログラムをJavaFXで作成しています。
https://github.com/torutk/calendar

Ver. 0.1.8では、日付の色を、平日は黒、土曜日は青、日曜日は赤としていました。が、やはり祝日も識別したいなと思いました。

日本の祝日は、日付固定の祝日(元旦の1月1日、建国記念日の2月11日など)、月の何番目かの週の月曜日となる祝日(成人の日の1月第2月曜日、海の日の7月第3月曜日)、国立天文台の暦計算に基づき閣議決定で定まる祝日(春分の日秋分の日)、日曜日と祝日が重なったときの振り替え休日(基本は翌月曜日だが祝日が連ちゃんするときはさらにずれる)、祝日と祝日に1日だけ平日が挟まれた時の国民の休日などがあり、事前にロジックで計算しきることが困難に思えました。

そこで、祝日を予め設定ファイルに記載し、その設定ファイルから祝日情報を読み込む機能を追加することにしました。

設定ファイルには、祝日とする年月日を1行1件で記載することとし、年月日はjava.time.LocalDateクラスがパースできる形式(ISO-8601)で記述します。

設定ファイルは、コマンドラインオプションでそのパスを指定し、設定ファイルが存在しない場合はプログラムの一部(JARファイルの中に組み込むファイル)として提供するデフォルトの設定ファイルを使用することとします。

コマンドラインオプションでパスを指定

JavaFXでは、プログラム起動時にコマンドラインオプション等で渡される情報をパラメータAPIとして定義しています。詳しくは次のWikiページに記載しています。
JavaFXとパラメータの取得 - ソフトウェアエンジニアリング - Torutk

パラメータAPIは、コマンドラインオプション、Java Web Startのパラメータ(HTML)、ネイティブバンドルのコンフィグレーションファイルなどに設定したオプションを共通のAPIで読み取ります。
オプションには、名前付き、名前なしの2種類があり、前者は--aaa=bbbの形式、後者は--cccの形式です。

--holiday=conf/holidays.txt

のようにコマンドラインで指定するとします。
javafx.application.Application派生クラスのstartメソッド内などで次のAPIで指定した内容(例では "conf/holidays.txt")を取得します。

    Map<String, String> params = getParameters().getNamed();
    String confPathText = params.getOrDefault("holiday", "holidays.conf");

ApplicationクラスのgetParametersメソッドで、起動時オプション情報を取得します。今回は名前付きオプションで指定するので、さらにgetNamedメソッドで名前付きオプション情報をMap型で取得します。このMapには、名前付きオプションの指定が--aaa=bbbであれば、aaaをキーにbbbを値にしたデータが格納されています。あとはキーを指定して値を取り出します。

Java SE 8ではMapインタフェースにgetOrDefaultメソッドが追加され、キーに指定した値が格納されていないときに、nullではなく第2引数に指定した値を返すことができます。

パスで指定したファイルのテキスト情報を読み出す

Java SE 7から搭載されたNIO2 APIを使ってファイルを読み出します。
java.nio.fileパッケージのPathsクラス、Pathクラス、Filesクラスを使います。

Path filePath = Paths.get(confPathText);

今回は、ファイルシステムの固有機能は使わないので、簡易にPathインスタンスを生成するPathsクラスのgetメソッドを使います。ファイルへのパスを表現する文字列を引数としてPaths.getメソッドを呼ぶと、ファイルシステム上のパスを表現するPathインスタンスが生成されます。引数に指定する文字列は、相対パスでも絶対パスでも指定可能です。

if (Files.exists(filePath)) {

ファイルが実在するかどうかをjava.nio.file.Filesクラスのexistsメソッドで判定します。

List<LocalDate> holidays = null;
try (Stream<String> lines = Files.lines(filePath)) {      // (1)
    holidays = lines.map(s -> LocalDate.parse(s))         // (2)
                    .collect(toList());                   // (3)
} catch (IOException ex) {
    logger.config(() -> String.format(
        "In holidays configuration, file %s cannot be read because of %s",
        filePath, ex.getLocalizedMessage()
    ));
}

ファイルの内容を1行1行取り出して処理をしていく場合、Java SE 8から搭載されたStream APIを使うと簡潔に記述できます。ファイルから読み込むデータをString型のStreamとして生成し(1)、Stream APIのmapでString型からLocalDate型へ変換し(2)、その結果をListへ出力します(3)。

Files.lineはファイルをオープンするので、使用後にクローズが必要です。try-with-resource構文と一緒に使うようにします。

空行があるとエラーになってしまう

設定ファイルに空行があると、(1)のファイルから読み込んだデータ(Stream)に、空文字列が入りますが、これは(2)のmapでLocalDate.parse(s)のsに空文字が入りエラー(例外発生)となります。
そこで、あらかじめ空文字列を除外するフィルターを入れます(4)。

try (Stream<String> lines = Files.lines(filePath)) {      // (1)
    holidays = lines.filter(s -> !s.isEmpty())            // (4)
                    .map(s -> LocalDate.parse(s))         // (2)
                    .collect(toList());                   // (3)

クラスパス上からファイルを読み込む

JARファイルに同梱した設定ファイルを読み込む場合は、通常のPathでは指定できないので、ClassクラスのgetResourceAsStreamメソッドを利用します*1

List<LocalDate> holidays = null;
try (BufferedReader reader = new BufferedReader(new InputStreamReader(
        this.getClass().getResourceAsStream("/holiday.conf")))
) {
    holidays = reader.lines.filter(s -> !s.isEmpty())     
                    .map(s -> LocalDate.parse(s))         
                    .collect(toList());                   
} catch (IOException ex) {
    logger.warning(() -> String.format(
        "In holidays configuration, resource %s cannot be read because of %s",
        "/holiday.conf", ex.getLocalizedMessage()));
}

クラスパス上に存在するファイルを読み込む場合は、ClassインスタンスのgetResourceAsStreamメソッドでファイルパスを指定し、InputStreamインスタンスを取得します。
Streamの形にするには、BufferedReaderインスタンスを生成します。InputStreamインスタンスをInputStreamReaderインスタンスで包み、さらにそれをBufferedReaderインスタンスで包みます。

ファイルをオープンする処理なので、使用後にクローズが必要です。try-with-resource構文と一緒に使うようにします。

*1:JARファイルを特定し、JARファイル内のパスが特定できれば、ZIPファイルシステムのPathとして扱うことができるかもしれないと思いますが、未確認です。