システム開発で思うところ

Javaで主にシステム開発をしながら思うところをツラツラを綴る。主に自分向けのメモ。EE関連の情報が少なく自分自身がそういう情報があったら良いなぁということで他の人の参考になれば幸い

スクラム冬の陣2017 みんなで学ぶスクラム に行ってきました

postudy.doorkeeper.jp

これまでJava界隈のセミナー(主にJJUG関係)に行っていましたが、今回は開発手法に関する勉強会に行ってきました。

一人開発の私にアジャイルとかスクラムとか関係は無いのかもしれませんが、以前、開発プロセスの仕事をしていたこともあり、こういう情報は聞くだけでも十分面白いですね。

改めて感想の分量とお話の面白さは比例しないことを先に断った上でツラツラと書いていきたいと思います。

20分でスクラム入門 ステップ・バイ・ステップ

スクラムのイロハのお話。資料をパブリックドメインと明言していただけると社内で勉強会をしようと思う人たちにとって使いやすいので良いですね。

Scrumを使うと本当に成長できるの?

外的動機付けがクリエイティブな仕事の生産性を下げるというのは興味深かったです。クリエイティブな仕事については、芸術家に1億出すから感動する作品を作れって言っても「金額の問題じゃない、才能とインスピレーションの問題だ!」という話だと思っていたので。必ずしもそうではないんですね。まぁ、芸術家の制作物に対して「生産性」というのも変な話かもしれませんが。。
研究者にとって一番の報酬は、生活に苦労することなく一番好きな研究を死ぬまで続けられることだ、という話をどこかで聞いた記憶がありますが、つまりはそういうことなんでしょうね。それは理解できる気がします。

多様な働き方をするチームでスクラムを実践してみた

開発業務以外でのスクラム適用のお話。
業務改善手法として活用した、ということで良いのかな?以前、WEB-DBPressで知った業務フロー改善ツールのマジカの記事を思い出しつつ、話を聞きました*1
間接部門といわれる人たちの生産性や成果については定量化が難しいと言われますが、こうやって見えるようにするだけでも随分と変わるだろうなぁ、と思います。
スクラムの導入ハードルを考えたら、営利活動に直接影響のない間接部門から適用をして、会社として有効性を立証してから開発部門に展開をする、というのも良いアプローチなのかもしれないと思いました。開発部門展開前に自社にスクラムマスターになる人がいるというのもメリットの1つなように思いますね。

お悩み解決!持ってて損しない道具箱

モデリングのお話。
事前に資料を見てしまっていたこともあり、ちょっと休憩モードになってしまった。。 モデリングトレーニングは、能力ではなく技術なので、やればやるほどスキルアップするというのは実感があるので納得するところ。言いたい事は理解できるので駄目というわけではないですが、鉛筆のモデリングは製造系システムの人だったらピンとくるかもしれないけど、ちょっと私にはモデリングのためのモデリング、みたいな気がしてしっくりこなかったかな。個人的には、こちらの書籍にあるような結果に対して、その背景となる構造をモデリングする、というのが、しっくりくるかな。

楽々ERDレッスン (CodeZine BOOKS)

楽々ERDレッスン (CodeZine BOOKS)

導入に困っているあなたに贈る スクラム導入コミュニケーション術

【Scrumを使うと本当に成長できるの?】と同じく、スクラム導入にむけた障害解消ノウハウのお話(もっと良い表現が思いつかない。。)
どちらも「こんなにスクラムは良いのだから採用しましょう!」というアプローチではないことが良いと思った。ちょっとずれた表現になるとは思うけど「良い商品が売れるのではなくて、売れる商品が良い商品」という感じ(いや違うなぁ)。何かを推進したい場合は、自分の正しさのみを正義にしてもダメだよ(これもちょっと違うなぁ)。
いずれにしても、この知見は、実際にスクラムを運用する際にも活用できるメンタリティだと思います。

組織”規模毎”のアジャイル開発アプローチ事例~成功・失敗・ピボット・トライ~

スクラムマネージャーの話。 手の届く範囲のスクラムマネージャーから、全社統轄のスクラムマネージャーになった際に同じ手法のままでは駄目だったという話。全社で約50%の適用(アーリーマジョリティ)な状態になってからのサポート指針の見直しについては、数値的考察であったことも含めて、とても興味深かったです。事例としては、部門別に見てスクラムの占拠率が低いところに重点的にサポートをして定着を図るという話でした。キャズム理論の話も少しされていましたが、スクラムの適用そのものを戦略とした場合、ランチェスター戦略的アプローチも検討してみてはどうかな?と、知ったかで思ったりもしました。いずれにしても導入がゴールではなく、花と同じようにきちんと水や肥料を与えないと枯れてしまうというのは世の常だな、と思いました。企業規模も鑑みると、たぶん、スクラムマネージャーマネージメントが必要な気がします。

産業機器B2Bビジネスへのスクラム手法の有効活用

機器製造でのスクラムというのは初めて聞いたので面白かったというか意外でした。恣意的なマネージメント力とか開発部門による営業力強化みたいな抽象的な指針ではなく「スクラム」というツールの介在が、物を作る情熱と専門性へのプライドのバランスを取ったのかな?と思いました。制約があるから発想が生まれるという話を聞いたことがありますが、スクラムというツールが程よい制約・ルールになったのかもしれませんね。

エンタープライズアジャイルの可能性と実現への提言

約2年間の知見の集約と提言のお話。
ちょっと文章で表現するのは難しいので、上述のリンクから資料をみていただくのが一番よろしいかと。。 という前提を置いたうえで、スライドでポイントしておきたいところのメモです。
「うちでもアジャイル開発やってみましたアンチパターン」と、アジャイルの適用ターゲットになる「ポストERPとしての次世代手組み」の【手組み2.0の領域】の要素群のところ。
あと、経営層向けには「ROIを考えるための要素」のところ。定量的にアジャイル開発でリリースした方が収益化を考えても良いよ、という論理的な説明になっています。
アジャイル開発導入の課題」も体制など具体的な数値の話があるので目を通しておきたいところです。

客先常駐案件でスクラムを導入したスクラムマスターの話

運用保守フェーズにスクラムを適用したお話。
大規模な段階的イテレーションで無ければ、確かに運用保守フェーズにスクラムは適用しやすいように思います*2。むしろ、開発の基本基盤や実現性の担保がある程度とれている運用保守フェーズこそ、お試しアジャイル導入には有効な気がします。

ベトナムでのアジャイルオフショア開発への挑戦

現地駐在でのオフショア開発でのアジャイル適用のお話。

  • どんな人も達成感を味わうことは喜びである
  • 理解が出来ても実践できないことはある、ということを謙虚に受け止めよう
  • スクラムの効果はメンバーとの物理的な距離の近さに比例する

なんたって”DevQA” アジャイル開発とQAの合体が改善を生む

開発プロセスにかかわっていた時に、BDD駆動開発に興味を持ちました。そのプロジェクトに参画した工程がシステムテストだったということもあり、TDDというよりもBDDの方がしっくりとくる感じでした。その時「基本設計工程が終わったら、そこからテスト計画を並行して行い、開発と並行もしくは先んじてあるべき状態を検証すれば品質は良くなるのではないだろうか?」と考えていました。その解として私にはBDDがしっくり来たわけです。
ですが、私ごときが思うくらいですから、多くの知見において、それはすでに実践されていると思いましたし、そうだとして何故上手くいっていないのだろうか?とも思っていました。また設計要件が満たされることを保証すること=機能が充足している・品質が良い、というのにも疑問がありました。なぜなら、ITやST工程になって発生する「仕様漏れ」「検討漏れ」といった上流工程*3起因のバグは取り残されてしまうからです。むしろ、そういうバグは質が悪く致命的なものになるケースになりやすいです。つまり本来解消しておきたい致命的な仕様漏れは上流工程関係者だけでは解消できないという開発プロセスとして構造的な問題があると感じていました。
アジャイルがその解なのかなぁ、と思うところもありましたが、顧客の事業計画上、そういう形式での予算構成では無く、理想と現実の差を感じつつ、ウォーターフォールの範疇で出来うることを考える日々でした。
おそらく、その解に近いアプローチが、この"DevQA"なように思います。アジャイルの方がより参加者にとって適用しやすいというところはあるかもしれませんが、このアプローチは開発手法の大小に関係ないように思います。他の方のお話も十分に面白いものでしたが、このお話を聞けただけでも参加させていただいた価値があるくらい面白かったです。

全体を通じて

皆さんの話に共通して感じたのは、スクラムアジャイルが素晴らしいということではなく、チーム全員が参加して作り出す仕事は面白い、ということでしょうか。スクラムは、その手法としてプロセスそのものに、その考慮を生き渡らせているので有効ですよ、というところでしょうか。 PMOが流行ったときに、現場側で感じた「高みの見物の寸評者」感よりも、スクラムの方が運用プロセスにマネージャーの参加を促す仕組みが組み込まれている感じを受けました*4

さいごに

初めて参加したアジャイル勉強会でしたが、色々と得るものが多かったように思います。一人開発で何を生かすのか?というのはありますが、明日への活力的なものを共有できたというだけでも十分な効用だと思います。

*1:マジカはフローそのものの話なので、プロセスであるアジャイルとはレイヤーが違いますけどね

*2:大規模な段階的イテレーションをしている時点を運用保守フェースといって良いのか?というのは置いておいて

*3:この言い方はあまり好きではないですが

*4:誤解無く言うと正しいPMOも血の通った価値あるものです。ただ何故か知識優先というか、あたまでっかちというか、そういう印象が強かったというだけです

Gitのメモ

GitHubの使用するにあたって必要と思われる自分向けのサイトリンクおよびメモです。
本記事を都度更新します。

作法

Git がわからなくても Github を利用しよう | そんなこと覚えてない

注意事項

チーム開発においてGit初心者が踏みがちな地雷まとめ|TechRacho by BPS株式会社

● 他のメンバがcheckoutしている可能性のあるリモートブランチに対してpush -fしてはいけません
● featureブランチは一人で開発している場合、かつdevelopにmergeする前においてのみpush -fしても大丈夫

VSCodeでGit操作

(Lubuntu環境での開発時はVSCodeで完結させたいため)

VScodeだけでGit操作を完結させるのだ~~ッ!!

push -fを使う事例

Githubでアカウントを晒した事故に対する対処 #Android - Qiita

【git】git pushを取り消す - tweeeetyのぶろぐ的めも

GitHubgit gcは定期処理なので即時削除ではない
● 【私見】履歴の掃除のやり方は知っておくべきだが、本当にそれをしなければならないか考える事。汚い履歴であっても履歴そのものが悪で無ければ諦める事も大切だと思う。*1

リポジトリ管理

実験やサンプルのリポジトリを分離、自分の GitHub アカウントのリポジトリを分類・整理する : @jsakamoto

gitリポジトリを軽くしよう!|TechRacho by BPS株式会社

Gitリポジトリをメンテナンスして軽量化する #Git - Qiita

アホみたいにでかいgit repositoryを上手く扱う方法 #Git - Qiita

git repository size を削減する | DriftwoodJP

Gitでやらかした時に使える19個の奥義 #Git - Qiita

コマンドおよび作業フロー

最近のgitを使った開発フローについて - その手の平は尻もつかめるさ

2011-05-28

Gitをある程度使えるようになったら、更に覚えておくと良いかもしれないコマンドとそのオプション #Git - Qiita

Gitでやらかさないための事前予防策 #Git - Qiita

gitで差分ファイルを抽出する #Git - Qiita

GitHub初心者はForkしない方のPull Requestから入門しよう | qnyp blog

http://moznion.hatenadiary.com/

コミット履歴を綺麗にするときの`git commit --fixup`と`git rebase --autosquash` - 理系学生日記

NetBeansGitHub

https://netbeans.org/kb/docs/ide/git_ja.html

NetBeans Git操作 - ソフトウェアエンジニアリング - Torutk

開発はローカルブランチで行う*2

NetBeansMarkdown

NetBeans Markdown plugin | junichi11.com

記載内容をすぐ確認したい場合は
[オプション]→[一般]→[Webブラウザ]:埋め込みWebkitブラウザ
[オプション]→[その他]→[Markdown]:[View Html On Save]をオン
保存する都度、埋め込みブラウザの内容が更新されます。
ディスプレイが小さい場合は、埋め込みブラウザのタブを[フロート]して別ウィンドウにすると作業がやり易いです。

bitbucket

https://toruuetani.bitbucket.io/bitbucket.html

鍵の作成

ssh 複数アカウント設定 #GitHub - Qiita

BitbucketにSSH認証鍵を設定する #Git - Qiita

http://tec-shi.com/tips/648/

その他

How to Publish Maven Site Docs to BitBucket or GitHub Pages - DZone

MarkdownでSlide作成して自分のGitHub Pagesで公開する - 山pの楽しいお勉強生活

書籍

*1:アカウント情報などセキュリティに関するものは悪

*2:この発想が無いので戸惑ってしまった

Pluggable Annotation Processing API Sample

Pluggable Annotation Processing APIについて調べたり試したりしたまとめです。
今回やりたかったことは、Annotation Processorで生成したソースが確認できるまでの環境準備です。「とりあえずAnnotation Processorで簡易かつ完結したプロジェクトが欲しい」という事を満たすことが目的です。

参考サンプル

参考リンクとは別に、今回動くもののサンプルとして参考にさせていただいたサイトを先に挙げておきます。サンプルとさせていただいたAnnotation Processorそのものの意図などはこちらを確認いただければと思います。

qiita.com

GitHub

今回のソースおよび実行環境の全量です*1

GitHub - vermeer-1977-blog/annotation-processor-sample: Pluggable Annotation Processing API Sample

ポイント

今回の目的である「完結したプロジェクトを作成する」というところに絞ったポイントです。

テストについて多くのサイトではAptina Unitを話題に出していました。実際、DomaJsonPullParserでも使われています。私自身も試してみたのですがJavaのバージョン@SupportedSourceVersion(SourceVersion.RELEASE_8)で警告が出る事と今回のサンプルで上手くソース生成結果を取得できなかったので(getGeneratedSourceSourceNotGeneratedExceptionが出る)、私はCompile Testingを使うことにしました。
ただCompile TestingにはgetGeneratedSourceのようなメソッドが見当たりませんでしたので、今回のプロジェクトでは直接コンソールに結果を出力するようにしています。*2


テストとは直接関係ないのですが、生成後想定クラスSampleFactory.javaについては、NetBeansで編集して保存をすると自動でフォーマットされてしまい、こちらの意図した形式と不一致になってしまいました。拡張子をtxtにして編集をして、最後に拡張子だけをjavaに変えるという力技で対処しました。

参考リンク

Annotation Processorについて教科書的知識として必ず目を通した方が良いもの*3


実行サンプル(上述と同じ)
Annotation Processingを使ったソースコード生成プログラムを作ってみる。(Java編) - Qiita


Compile Testing

アノテーションプロセッサで生成したコードをCompile Testingを使ってテストする - 量産型エンジニアの憂鬱


その他

アノテーションプロセッサで AST 変換 - Lombok を参考にして変数の型をコンパイル時に変更 - なんとなくな Developer のメモ

さいごに

Annotation Processorを調べている中でDomaJsonPullParserのソースを少しだけ読んだりしました。正直、私には難しかったです。ですが分からないなりにAnnotation Processorの良さも分かったようにも思います。うらがみさんの「白魔術」という表現も言い得て妙だと思いました。

https://twitter.com/backpaper0/status/578776701989052416

ツール

(2017/11/17追記)

ツールを作ってみました。

ついでに、その実装説明も書いてみました。

vermeer.hatenablog.jp

vermeer.hatenablog.jp

*1:GitHubを初めて使いました。思った構成にならず、Readmeの書き方に戸惑い、何度もRepositoryを削除するという繰り返し。まだ良く分かっていませんが、とりあえずプロジェクト公開まで至れたので良しとしたいと思います。

*2:その他のメッセージも同様ですが実際の稼動時には出力しないようにしてください。

*3:といいつつ、私は途中までしか読んでいません。。なので自分向けのリンクでもあります

Netbeans で Pluggable Annotation Processing API

こちらの記事のNetBeans版のようなものです。

d.hatena.ne.jp

AnnotationProcessorが動くまでのところのチュートリアル的にやってみようと思ってやってみたところ、少し躓いたところもあったので誰かの参考になればと思い、まとめてみました。事例のベースもリンク元と同じなので詳細は上述のリンクを確認ください。本記事では動くまでの流れを中心にします。

はじめに

NetBeansのビルドではなくmavenプロジェクトで実施しています。試してはいませんがNetBeansに強依存はしていないとは思います。
実行環境はJava8です。

新規プロジェクト(Processor提供側)を作る

mavenプロジェクトを作成する

f:id:vermeer-1977:20170102233832p:plain f:id:vermeer-1977:20170102233833p:plain

提供側ソース

package com.mycompany.annotationprocessor;

import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic.Kind;

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("java.lang.SuppressWarnings")
public class SuppressWarningsProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations,
                           RoundEnvironment roundEnv) {
        Messager messager = processingEnv.getMessager();
        for (TypeElement annotation : annotations) {
            for (Element element : roundEnv
                    .getElementsAnnotatedWith(annotation)) {
                messager.printMessage(Kind.WARNING, "@SuppressWarningsしちゃだめ",
                                      element);
            }
        }
        return true;
    }
}

サービスプロバイダーコンフィギュレーションファイルの保存先作成

このフォルダの作成自体がポイントです。*1

f:id:vermeer-1977:20170102233835p:plain f:id:vermeer-1977:20170102233836p:plain f:id:vermeer-1977:20170102233837p:plain f:id:vermeer-1977:20170102233838p:plain

サービスプロバイダーコンフィギュレーションファイルを作成

f:id:vermeer-1977:20170102233839p:plain f:id:vermeer-1977:20170102233840p:plain ファイル名:javax.annotation.processing.Processorを作成し、AnnotationProcessorを登録する

com.mycompany.annotationprocessor.SuppressWarningsProcessor

今回は1つですが複数のAnnotationProcessorがある場合 改行区切りで登録できます。

最終的なフォルダ構成

├─src
│  └─main
│      ├─java
│      │  └─com
│      │      └─mycompany
│      │          └─annotationprocessor
│      │                  SuppressWarningsProcessor.java
│      │                  
│      └─resources
│          └─META-INF
│              └─services
│                      javax.annotation.processing.Processor

pom.xmlの編集

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.mycompany</groupId>
    <artifactId>AnnotationProcessor</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.6.0</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <!-- Disable annotation processing for ourselves. -->
                    <compilerArgument>-proc:none</compilerArgument>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

ポイントはコンパイルオプションのところ-proc:noneです。またpom.xmlgroupIdは次の利用側で参照します。ただし、今回は「同一プロジェクトグループ」という形で参照します。

新規プロジェクト(Processor利用側)を作る

mavenプロジェクトを作成する

プロジェクト名samplesで作成。作成手順は上述の通りなので省略。
既存のmavenプロジェクトがあれば新規作成は不要です。

利用側プログラム

package com.mycompany.samples;

import java.util.ArrayList;
import java.util.List;

@SuppressWarnings("unchecked")
public class Sample {

    void hoge() {
        List list = new ArrayList<>();
        list.add("");
        System.out.println(list.size());
    }
}

pom.xmlの編集

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.mycompany</groupId>
    <artifactId>samples</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.6.0</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <showDeprecation>true</showDeprecation>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <!-- for my group annotation processor start -->
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>AnnotationProcessor</artifactId>
            <version>${project.version}</version>
            <type>jar</type>
        </dependency>
        <!-- for my group annotation processor end -->
    </dependencies>

</project>

今回はgroupIdは、提供側と同じなので${project.groupId}と指定しています。

適用された結果

f:id:vermeer-1977:20170102233841p:plain

警告を見ると適用されていることが分かります。

さいごに

他に参考にさせていただいた情報だともっと面倒な印象でしたが、結果としてmavenの設定だけで大きな手間も無く動くところまで確認できて良かったです。

今回の記事とは関係ないですがmavenプロジェクトを個別に作成をして依存を定義するというのは、自分のシステムでレイヤー間の依存関係を明確にするために使っています。
プロジェクトで分割しておくことで、うっかりController層のクラスを誤ってService層で参照しないように強制させることが出来ます。*2

*1:私が躓いたところです。当初、場所が正しくなく参照されませんでした。

*2:Java9のJigsawがリリースされるまでの工夫といったところでしょうか

EnumのInterfaceに共通処理を実装する

以前の記事で、Interfaceとユーティリティのペアを説明しました。
Enumに状態を保持して、ユーティリティで振舞いを実装する、というやり方です。

vermeer.hatenablog.jp

ロジックの冗長さを避けようと思ったわけですがオブジェクト指向ってそうじゃないんだよね、とも思います。オブジェクト指向至上主義ではありませんが、ユーティリティを使うやり方は、気を抜くとユーティリティまみれになり兼ねないです*1

というわけで、前回の実装をベースに、Interfaceで出来そうな範囲の実装をしてユーティリティクラスを使わない実装を考えてみたいと思います。

実装:staticメソッドを追加する

Interface

package com.mycompany.samples.enumclass.test;

import lombok.NonNull;

/**
 * propertiesの仕様を前提として拡張をしたEnumクラスを共通操作するためのInterface
 *
 * @author Yamashita
 */
public interface EnumCodePropertyInterface {

    public Object getCode();

    public String getProperty();

    /**
     * properties用に拡張したEnumのcode値から取得した拡張Enumを生成する
     *
     * @param <E> EnumCodePropertyInterfaceで拡張したEnumClass
     * @param enumType 生成対象となるEnumClassの型となるClass
     * @param code テーブルや定数として指定しているcode値
     * @return codeに一致するEnumクラス
     */
    public static <E extends Enum<E> & EnumCodePropertyInterface> E codeOf(Class<E> enumType, @NonNull Object code) {
        for (E type : enumType.getEnumConstants()) {
            if (type.getCode().equals(code)) {
                return type;
            }
        }
        throw new IllegalArgumentException("enum class has not code : " + code.toString());
    }

    /**
     * properties用に拡張したEnumのpropertyKey値から取得した拡張Enumを生成する
     *
     * @param <E> EnumCodePropertyInterfaceで拡張したEnumClass
     * @param enumType 生成対象となるEnumClassの型となるClass
     * @param property propertiesの検索に使用するpropertyKey
     * @return propertyKeyに一致するEnumクラス
     */
    public static <E extends Enum<E> & EnumCodePropertyInterface> E propertyOf(Class<E> enumType, @NonNull String property) {
        for (E type : enumType.getEnumConstants()) {
            if (type.getProperty().equals(property)) {
                return type;
            }
        }
        throw new IllegalArgumentException("enum class has not propertyKey : " + property);
    }

}

EnumClass

package com.mycompany.samples.enumclass.test;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NonNull;

@AllArgsConstructor @Getter
public enum Gender implements EnumCodePropertyInterface {
    MALE(1, "男"),
    FEMALE(2, "女");

    private final Integer code;
    private final String property;

    public static Gender codeOf(@NonNull Object code) {
        return EnumCodePropertyInterface.codeOf(Gender.class, code);
    }

    public static Gender propertyOf(@NonNull String property) {
        return EnumCodePropertyInterface.propertyOf(Gender.class, property);
    }
}

使用例

    public void exec() {
        Gender gender = Gender.codeOf(Gender.FEMALE.getCode());
        System.out.println("gender.getCode() = " + gender.getCode());
        System.out.println("gender.getProperty() = " + gender.getProperty());
    }

さいごに

staticによる実装なので、結局のところユーティリティクラスを内包しただけとも言えるかもしれません*2。メリットは使用するEnumClassに仕様が凝集していて使用時の実装もすっきりと読みやすくはなっているように思います。なによりもEnumvalueOfと並列な感じで個人的には分かりやすいように思います。デメリットは、Enumを定義する際に儀式的にメソッドを実装しないといけないというところです。*3

後日追記

自分なりにAnnotationProcessorによる実験的な実装も検討してみましたが素直にユーティリティを使うやり方がシンプルで良さそうだ、という結論に至りました*4

*1:必然性があれば、ユーティリティは悪ではないですが、やり易いからという理由でユーティリティを多用するのは、基本的に良くないと思っています

*2:おそらく実際そうです

*3:冗長さを解消するためにはPluggable Annotation Processing APIが解な気がします

*4:lombokの拡張が自分のイメージに一番近いのですが、そこまででも無いかな、と

JSFでResourceBundleを適用する

前回は、全部入りのResourceBundleを作ってみました。

vermeer.hatenablog.jp

今回は、そのResourceBundleをJSFで使えるようにするためのパーツとサンプル実装を作りました。ほとんどは、参考リンクのままです。

ResourceBundleのファクトリークラス

/*
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 *  Copyright © 2016 Yamashita
 */
package com.mycompany.samplejsf.infrastructure.jsf.uicomponet;

import com.mycompany.samplejsf.infrastructure.resourse.CustomControl;
import java.util.Locale;
import java.util.ResourceBundle;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

/**
 * JSFで参照するResourceBundleを生成するFactoryクラス.<br>
 * 本クラスで生成したクラスを{@literal faces-config.xml}に設定してJSFから参照する.<br>
 *
 * @author Yamashita
 */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class JsfResourceBundle {

    /**
     * ResourceBundleを生成する.<br>
     * 取得方法の制御については、CustomControlで指定をする.<br>
     *
     * @see com.mycompany.samplejsf.infrastructure.resourse.CustomControl
     *
     * @param bundleBaseName 取得対象のResourceBundleのBaseName(AAAA.propertiesのAAAAの部分)
     * @param customControl 必要情報を設定したCustomControl
     * @return 生成したResourceBundle
     */
    public static ResourceBundle getBundle(String bundleBaseName, CustomControl customControl) {
        Locale locale = JsfContextUtil.getLocale();
        return ResourceBundle.getBundle(bundleBaseName, locale, customControl);
    }

    /**
     * ResourceBundleを生成する.<br>
     * Controlを未指定の場合は、文字コードはデフォルト(ascii)、取得対象はpropertiesファイルのみとしてResourceBundleを生成する.<br>
     * 取得対象をpropertiesのみとするのは、検索対象を絞る事で性能改善を図る.
     *
     * @param bundleBaseName 取得対象のResourceBundleのBaseName(AAAA.propertiesのAAAAの部分)
     * @return 生成したResourceBundle
     */
    public static ResourceBundle getBundle(String bundleBaseName) {
        CustomControl customControl = CustomControl.builder()
                .formats(CustomControl.FORMAT_PROPERTIES)
                .build();
        return JsfResourceBundle.getBundle(bundleBaseName, customControl);
    }
}

ResoureceBundleを生成する

/*
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 *  Copyright © 2016 Yamashita
 */
package com.mycompany.samplejsf.domain.resource;

import com.mycompany.samplejsf.infrastructure.jsf.uicomponet.JsfResourceBundle;
import java.util.Enumeration;
import java.util.ResourceBundle;

/**
 * 表示ラベルResourceBundle
 *
 * @author Yamashita
 */
public class LabelResourceBundle extends ResourceBundle {

    public LabelResourceBundle() {
        ResourceBundle bundle = JsfResourceBundle.getBundle("label");
        super.setParent(bundle);
    }

    @Override
    protected Object handleGetObject(String key) {
        return super.parent.getObject(key);
    }

    @Override
    public Enumeration<String> getKeys() {
        return super.parent.getKeys();
    }
}

propertiesファイルのbaseNameであるlabelを指定します。今回は、デフォルト指定のためCustomControlを使用していませんが、先日作成したCustomControlを引数として使用することで文字コードや参照リソースの絞り込みなどができます。

properties

label.properties

hello=こんにちは
hello.hi=こんにちは、こんちは

message.properties

noselect=選択無し
gender_male=男
gender_female=女

faces-config.xmlを設定する

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

<!-- =========== FULL CONFIGURATION FILE ================================== -->

<faces-config version="2.0"
              xmlns="http://java.sun.com/xml/ns/javaee"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd">

    <application>
        <resource-bundle>
            <base-name>
                message
            </base-name>
            <var>msg</var>
        </resource-bundle>
        <resource-bundle>
            <base-name>
                com.mycompany.samplejsf.domain.resource.LabelResourceBundle
            </base-name>
            <var>label</var>
        </resource-bundle>
    </application>
</faces-config>

xhtml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html">
    <head>
        <title>properties label</title>
        <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    </head>
    <body>
        <h:form>
            <div>
                <p>#{label.hello}</p>
                <p>#{label['hello.hi']}</p>
                <p>#{msg['gender_male']}</p>
            </div>
        </h:form>
    </body>
</html>

出来るは出来たのですが、msgNetBeans*1で入力補完がされるのですが、独自に拡張したResourceBundleで参照しているlabelについては入力補完がされませんでした。タイプセーフになることを期待したのですが、ちょっと残念です。

実行結果

f:id:vermeer-1977:20161231163714p:plain

参考リンク

http://noknow.info/it/2015/javaee7_jsf2_2_use_resourcebundle_of_utf8file_ja.html

Code

2018/4/17 追加

Bitbucket

*1:Product Version: NetBeans IDE 8.1(Build 201510222201)

全部入りなResourceBundleを考える

恥ずかしながら、これまで小さくローカル環境のシステムなので国際化も文字コードも考慮することがありませんでした。 これまで参画したプロジェクトではフレームワークなりで準備されたものを使っていたので、あまり意識していませんでした。

調べて見ると色々なことが分かりました。そして調べていくと良くあることなのですが「単純にやるならこれで十分。だけど本来ならこういうことも考えておきたい」という衝動です。
それだけでなく「全部入りで汎用なクラスがあったら後々便利かも!」という更なる衝動です。 YAGNI*1ということも考えるのですが、必要になったときに再び調べ直すのも面倒と思ったりしつつ、悶々としながら落としどころを勉強するくらいなら、いっそ自分なりの「これなら大体の事は大丈夫だろうクラス」を作ることにしました。

ということで、自分なりの全部入りResourseBundleというのを整理してみました。多分、これで当分はResourseBundleのことを自分としては忘れて良くなると思います。というか、そうなりたいです。

ちなみに、文字コードですが、自動で判別するような仕組みも考えようと思ったのですが、こちらについては良い案がありませんでした。個人的に色々と実験をしましたが、万能薬とまではいかないことと、自分が準備するファイルなのに毎回判定をするのは妥当なのかな?と考えた結果、文字コードは判別ではなく指定するやり方にしました。ということで、こちらはYAGNIと判断しました。

前置きが長くなりました。それ以上に長いコードと実行結果の貼り付けが以下に続きます。

実装

ResourceBundleのControlの拡張

/*
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 *  Copyright © 2016 Yamashita,
 */
package com.mycompany.samples.resourse;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.FileSystemNotFoundException;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import java.util.ResourceBundle.Control;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.Singular;

/**
 * 通常のCotrolに加えて、文字コードを指定してResourseを取得する拡張をしたResourceBundle.Controlクラスを生成するBuilder<br>
 * 冗長的ではあるが拡張メソッドを把握するため、およびその用途メモを記すために、拡張対象メソッドを全てOverrideする.<br>
 *
 * @author Yamashita,
 */
@Builder
public class CustomControl extends Control {

    /**
     * formatに使用するXML用の定数
     */
    public static final List<String> FORMAT_XML = Collections.unmodifiableList(Arrays.asList("xml"));

    /* propertiesファイルの読み込み時のエンコードに使用する文字コード(例:UTF-8,SJIS etc) */
    private final String charCode;

    /* ResourceBundleの有効期限(0またはキャッシュ格納時刻からの正のミリ秒オフセット)、有効期限制御を無効にする場合はTTL_NO_EXPIRATION_CONTROL、キャッシュを無効にする場合はTTL_DONT_CACHE  */
    private final Long timeToLive;

    /* 読み込み対象とするformat */
    @Singular
    private final List<String> formats;

    /* リソースを取得する優先度のLocaleペアのリスト */
    @Singular
    private final List<TargetCandidateLocalePair> targetCandidateLocalePairs;

    /* fallbackが再帰的に呼び込まれている状態 = 無限ループ */
    @Getter(AccessLevel.PRIVATE)
    private boolean isFallBackInfiniteLoop = false;

    /**
     * 新しいResourceBundleを生成する.<br>
     * propertiesは、charCodeで指定した文字コードでエンコードしながら読み込む.<br>
     *
     * @see java.util.ResourceBundle.Control#newBundle(java.lang.String, java.util.Locale,
     * java.lang.String,java.lang.ClassLoader, boolean)
     *
     * @return 生成したResourceBundle
     * @throws java.lang.IllegalAccessException 配列以外のインスタンス作成、フィールドの設定または取得、メソッドの呼び出しを試みた場合の例外
     * @throws java.lang.InstantiationException 指定されたクラスオブジェクトのインスタンスを生成できない場合の例外
     * @throws java.io.IOException resourceファイルの取得時に発生時の例外
     */
    @Override
    public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload)
            throws IllegalAccessException, InstantiationException, IOException {

        this.isFallBackInfiniteLoop = false;
        if (CustomControl.FORMAT_CLASS.contains(format)) {
            return super.newBundle(baseName, locale, format, loader, reload);
        }

        if (CustomControl.FORMAT_PROPERTIES.contains(format)) {
            return this.newBundleProperties(baseName, locale, format, loader, reload);
        }

        if (CustomControl.FORMAT_XML.contains(format)) {
            return this.newBundleXML(baseName, locale, format, loader, reload);
        }

        throw new IllegalArgumentException("unknown format: " + format);
    }

    /**
     * propertiesファイルの読み込みResourceBundleを生成する.
     *
     * @param baseName
     * @param locale
     * @param format
     * @param loader
     * @param reload
     * @return 生成したResourceBundle
     * @throws IllegalAccessException
     * @throws InstantiationException
     * @throws IOException
     */
    private ResourceBundle newBundleProperties(String baseName, Locale locale, String format, ClassLoader loader, boolean reload)
            throws IllegalAccessException, InstantiationException, IOException {

        String bundleName = toBundleName(baseName, locale);
        ResourceBundle bundle = null;
        final String resourceName = (bundleName.contains("://"))
                                    ? null
                                    : toResourceName(bundleName, "properties");
        if (resourceName == null) {
            return bundle;
        }
        final ClassLoader classLoader = loader;
        final boolean reloadFlag = reload;
        InputStream stream = null;
        try {
            stream = AccessController.doPrivileged((PrivilegedExceptionAction<InputStream>) () -> {
                InputStream is = null;
                if (reloadFlag) {
                    URL url = classLoader.getResource(resourceName);
                    if (url != null) {
                        URLConnection connection = url.openConnection();
                        if (connection != null) {
                            // Disable caches to get fresh data for reloading.
                            connection.setUseCaches(false);
                            is = connection.getInputStream();
                        }
                    }
                } else {
                    is = classLoader.getResourceAsStream(resourceName);
                }
                return is;
            });
        } catch (PrivilegedActionException e) {
            throw (IOException) e.getException();
        }
        if (stream != null) {
            try {
                if (this.charCode != null) {
                    bundle = new PropertyResourceBundle(new InputStreamReader(stream, this.charCode));
                } else {
                    bundle = new PropertyResourceBundle(stream);
                }
            } finally {
                stream.close();
            }
        }
        return bundle;
    }

    /**
     * XMLファイルの読み込みResourceBundleを生成する.
     *
     * @param baseName
     * @param locale
     * @param format
     * @param loader
     * @param reload
     * @return 生成したResourceBundle
     * @throws IllegalAccessException
     * @throws InstantiationException
     * @throws IOException
     */
    private ResourceBundle newBundleXML(String baseName, Locale locale, String format, ClassLoader loader, boolean reload)
            throws IllegalAccessException, InstantiationException, IOException {

        ResourceBundle bundle = null;

        String bundleName = toBundleName(baseName, locale);
        final String resourceName = (bundleName.contains("://"))
                                    ? null
                                    : toResourceName(bundleName, "xml");

        InputStream stream = null;
        if (reload) {
            URL url = loader.getResource(resourceName);
            if (url != null) {
                URLConnection connection = url.openConnection();
                if (connection != null) {
                    // Disable caches to get fresh data for reloading.
                    connection.setUseCaches(false);
                    stream = connection.getInputStream();
                }
            }
        } else {
            stream = loader.getResourceAsStream(resourceName);
        }
        if (stream != null) {
            try (BufferedInputStream bis = new BufferedInputStream(stream)) {
                bundle = new XMLResourceBundle(bis);
            }
        }
        return bundle;
    }

    /**
     * リソースを取得する優先度を指定する.<br>
     *
     * 直接拡張をする場合の実装例(LocaleがJapaneseの時、なぜか英語を優先したい例).<br>
     * <pre>
     * {@code
     * if (locale.equals(Locale.JAPAN)) {
     *       return Arrays.asList(Locale.ENGLISH,
     *       locale,
     *       Locale.JAPANESE,
     *       Locale.ROOT);
     * } else {
     *       return super.getCandidateLocales(baseName, locale);
     * }
     * }
     * </pre>
     *
     * @see java.util.ResourceBundle.Control#getCandidateLocales(java.lang.String, java.util.Locale)
     *
     * @return 優先度順のロケールリスト
     */
    @Override
    public List<Locale> getCandidateLocales(String baseName, Locale locale) {

        Optional<List<Locale>> candidateLocales = targetCandidateLocalePairs.stream()
                .filter(pair -> pair.getTargetLocale().equals(locale))
                .map(TargetCandidateLocalePair::getCandidateLocales)
                .findAny();

        if (candidateLocales.isPresent() == false) {
            return super.getCandidateLocales(baseName, locale);
        }

        List<Locale> localeSetedCandidateLocales = new ArrayList<>();
        candidateLocales.get().stream()
                .forEachOrdered(candidateLocale -> {
                    localeSetedCandidateLocales.add(candidateLocale == null ? locale : candidateLocale);
                });
        return localeSetedCandidateLocales;
    }

    /**
     * デフォルトリソースを取得する.<br>
     * リソースバンドルの検索時、指定したロケールに対応したリソースバンドルが存在しない場合、デフォルトリソースではなく、デフォルトロケールに対応したリソースバンドルを検索してしまう.<br>
     * 本対応をしないと意図したリソースではないデフォルトロケールを取得してしまい国際化対応が正しく行われない.<br>
     *
     * @see java.util.ResourceBundle.Control#getFallbackLocale(java.lang.String, java.util.Locale)
     * @return デフォルトリソースのロケール
     */
    @Override
    public Locale getFallbackLocale(String baseName, Locale locale) {
        if (this.isFallBackInfiniteLoop) {
            throw new MissingResourceException("you set baseName is  [" + baseName + "]. fallback locale, but does not exist baseName resource file. check ResourceBundle.getBundle param 'baseName' and resource file name.", baseName, "");
        }
        this.isFallBackInfiniteLoop = true;
        return Locale.ROOT;
    }

    /**
     * ResourceBundleとして読み込み対象とする分類を返却する.<br>
     * デフォルトの優先度は高い順に、{@literal class > properties > xml}.<br>
     * 特定のフォーマットを指定した場合は、そちらを採用する.<br>
     * baseNameが同じで拡張子が異なるファイルが存在する場合、フォーマットを指定することで正しく処理できるようになる.<br>
     *
     * @see java.util.ResourceBundle.Control#getFormats(java.lang.String)
     * @return ResourceBundleとして読み込み対象とする分類リスト
     */
    @Override
    public List<String> getFormats(String baseName) {
        return this.formats.isEmpty()
               ? Collections.unmodifiableList(Arrays.asList("java.class",
                                                            "java.properties",
                                                            FORMAT_XML.get(0)))
               : Collections.unmodifiableList(this.formats);
    }

    /**
     * キャッシュ内のロード済みバンドルの有効期限を取得する.<br>
     * キャッシュ内のロード済みバンドルに有効期限を設ける場合はその時間(0またはキャッシュ格納時刻からの正のミリ秒オフセット)、有効期限制御を無効にする場合はTTL_NO_EXPIRATION_CONTROL、キャッシュを無効にする場合はTTL_DONT_CACHE。
     * <br>
     *
     * @see java.util.ResourceBundle.Control#getTimeToLive(java.lang.String, java.util.Locale)
     * @return バンドルの有効期限
     */
    @Override
    public long getTimeToLive(String baseName, Locale locale) {
        return this.timeToLive == null
               ? super.getTimeToLive(baseName, locale)
               : this.timeToLive;
    }

    /**
     * キャッシュ再ロード判定.<br>
     * キャッシュ内で有効期限の切れたbundleを再ロードする必要があるかどうかを、loadTimeに指定されたロード時刻やその他のいくつかの条件に基づいて判定する(継承元クラスのコメント抜粋).<br>
     * (拡張仕様が無いので継承元の操作をそのまま行う)<br>
     *
     * @see java.util.ResourceBundle.Control#needsReload(java.lang.String, java.util.Locale, java.lang.String,
     * java.lang.ClassLoader, java.util.ResourceBundle, long)
     * @return キャッシュ内で有効期限の切れたbundleを再ロード要否
     */
    @Override
    public boolean needsReload(String baseName, Locale locale, String format, ClassLoader loader, ResourceBundle bundle, long loadTime) {
        // ここを実装
        return super.needsReload(baseName, locale, format, loader, bundle, loadTime);
    }

    /**
     * BaseNameとlocaleの組み合わせで取得対象となるプロパティファイル名を編集する.<br>
     * (拡張仕様が無いので継承元の操作をそのまま行う)<br>
     *
     * @see java.util.ResourceBundle.Control#toBundleName(java.lang.String, java.util.Locale)
     * @return 取得対象のプロパティファイル名
     */
    @Override
    public String toBundleName(String baseName, Locale locale) {
        // ここを実装
        return super.toBundleName(baseName, locale);
    }

}
/*
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 *  Copyright © 2016 Yamashita,
 */
package com.mycompany.samples.resourse;

import java.util.List;
import java.util.Locale;
import lombok.Builder;
import lombok.Getter;
import lombok.Singular;

/**
 * CustomControlで使用するCandidateLocaleのペア.<br>
 * candidateLocaleにnullを指定した場合、{@link com.mycompany.samples.resourse.CustomControl#getCandidateLocales(java.lang.String, java.util.Locale)
 * }getCandidateLocalesで候補となるLocaleを取得する際、引数として指定したlocaleを処理対象として置き換えて使用する.
 *
 * @author Yamashita,
 */
@Builder @Getter
public class TargetCandidateLocalePair {

    private final Locale targetLocale;

    @Singular
    private final List<Locale> candidateLocales;

}
/*
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 *  Copyright © 2016 Yamashita,
 */
package com.mycompany.samples.resourse;

import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.Properties;
import java.util.ResourceBundle;

/**
 * XML形式のResourceBundleクラス.<br>
 *
 * XMLのフォーマット形式(http://java.sun.com/dtd/properties.dtd)
 *
 * <pre>
 * {@code
 * <!--
 * Copyright 2006 Sun Microsystems, Inc.  All rights reserved.
 * -->
 *
 * <!-- DTD for properties -->
 *
 * <!ELEMENT properties ( comment?, entry* ) >
 *
 * <!ATTLIST properties version CDATA #FIXED "1.0">
 *
 * <!ELEMENT comment (#PCDATA) >
 *
 * <!ELEMENT entry (#PCDATA) >
 *
 * <!ATTLIST entry key CDATA #REQUIRED>
 *
 * }
 * </pre>
 *
 * @author Yamashita,
 */
public class XMLResourceBundle extends ResourceBundle {

    private final Properties properties;

    /**
     * XML形式のResourceBundleのコンストラクタ<br>
     * Propertiesクラスを使用してXMLファイルを読み込む
     *
     * @param stream プロパティファイルのInputStream
     * @throws IOException InputStreamの入出力時に発生した例外
     */
    public XMLResourceBundle(InputStream stream) throws IOException {
        properties = new Properties();
        properties.loadFromXML(stream);
    }

    @Override
    public Object handleGetObject(String key) {
        if (key == null) {
            throw new NullPointerException();
        }
        return properties.get(key);
    }

    @Override
    public Enumeration<String> getKeys() {
        return (Enumeration<String>) properties.propertyNames();
    }
}

ポイントになりそうなところは、コメントに極力書いたつもりです。
大きいところは、fallback制御のところと、文字コード指定のところでしょうか。参考にさせていただいた資料を元についでにXMLも処理できるようにしました。

実行クラス(テストケースもどき)

/*
 *  Copyright © 2016 Yamashita,Takahiro
 */
package com.mycompany.samples.resourse.runner;

import com.mycompany.samples.resourse.CustomControl;
import com.mycompany.samples.resourse.TargetCandidateLocalePair;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.Locale;
import java.util.ResourceBundle;

/**
 *
 * @author Yamashita,Takahiro
 */
public class ResourseTest {

    public void execute() {
        this.useAsciiProperties();
        this.useUTF8PropertiesNoSetLocaleIsDefaultLocale();
        this.useUTF8PropertiesNoExsistLocaleIsDefaultResource();
        this.useSJISXML();
        this.useSJISSameNamePropertiesXMLnotSetFormat();
        this.useSJISSameNamePropertiesXMLsetSimpleFormat();
        this.useSJISSameNamePropertiesXMLsetMultiFormat();
        this.useUTF8candidateLocalesMatchTargetLocale();
        this.useUTF8candidateLocalesNotMatchTargetLocale();
        this.useUTF8candidateLocalesNotExistTargetLocale();
        this.useUTF8candidateLocalesExistsNullTargetLocale();
        this.useUTF8candidateLocalesExistsTargetLocaleButNotSetLocale();
        this.useNoCache();
        this.useUTF8Cache1();
        this.useUTF8Cache2();
        this.useUTF8SleepTimeLtInterval();
        this.useUTF8IntervalLtSleepTime();

    }

    private void printLog(ResourceBundle bundle) {
        Enumeration<String> enums = bundle.getKeys();
        while (enums.hasMoreElements()) {
            String key = enums.nextElement();
            System.out.println("key= " + key + ":value= " + bundle.getString(key));
            System.out.println();
        }
    }

    /**
     * 標準のASCIIコードのpropertiesを参照する
     */
    private void useAsciiProperties() {
        CustomControl control = CustomControl.builder().build();
        ResourceBundle bundle = ResourceBundle.getBundle("message", control);
        System.out.println("useAsciiProperties select message.properties");
        this.printLog(bundle);
    }

    /**
     * 文字コードにUTF8を指定。ロケールの指定をしない場合は、デフォルトロケール(ロケールがja_JP)のpropertiesを参照する
     */
    private void useUTF8PropertiesNoSetLocaleIsDefaultLocale() {
        CustomControl control = CustomControl.builder().charCode(StandardCharsets.UTF_8.toString()).build();
        ResourceBundle bundle = ResourceBundle.getBundle("utf8", control);
        System.out.println("useUTF8PropertiesNoSetLocaleIsDefaultLocale is default locale. select utf8_ja_JP.properties");
        this.printLog(bundle);
    }

    /**
     * 文字コードにUTF8を指定。指定ロケールのpropertiesが存在しない場合は、デフォルトリソース(ロケール無)のpropertiesを参照する
     */
    private void useUTF8PropertiesNoExsistLocaleIsDefaultResource() {
        CustomControl control = CustomControl.builder()
                .charCode(StandardCharsets.UTF_8.toString()).build();
        ResourceBundle bundle = ResourceBundle.getBundle("utf8", Locale.CHINESE, control);
        System.out.println("useUTF8PropertiesNoExsistLocaleIsDefaultResource is default recourse. select nolabel utf8.properties");
        this.printLog(bundle);
    }

    /**
     * xmlのリソースを参照する.XMLの場合は文字コードの指定は不要
     */
    private void useSJISXML() {
        //XML no need charCode set (set ignore)
        CustomControl control = CustomControl.builder().build();
        ResourceBundle bundle = ResourceBundle.getBundle("SJIS", control);
        System.out.println("useSJISXML is default recourse. select nolabel SJIS.xml");
        this.printLog(bundle);
    }

    /**
     * xmlとpropertiesに同名のbaseNameの資産が存在する場合は、getFormats()の優先度(Class-Properties-XML)に従ってリソースを参照する.<br>
     * 本ケースはpropertiesが参照されるが、文字コードの指定が無いため文字化けが発生する
     */
    private void useSJISSameNamePropertiesXMLnotSetFormat() {
        CustomControl control = CustomControl.builder().build();
        ResourceBundle bundle = ResourceBundle.getBundle("SJIS-SAME", control);
        System.out.println("useSJISSameNamePropertiesXMLnotSetFormat prioritize properties. select SJIS-SAME.properties garbled characters (T^T)");
        this.printLog(bundle);
    }

    /**
     * xmlとpropertiesに同名のbaseNameの資産が存在するが、formatsにXMLを指定しているためXMLを参照し、文字化けも発生しない
     */
    private void useSJISSameNamePropertiesXMLsetSimpleFormat() {
        CustomControl control = CustomControl.builder().formats(CustomControl.FORMAT_XML).build();
        ResourceBundle bundle = ResourceBundle.getBundle("SJIS-SAME2", control);
        System.out.println("useSJISSameNamePropertiesXMLsetSingleFormat select xml. select SJIS-SAME2.xml no garbled characters");
        this.printLog(bundle);
    }

    /**
     * xmlとpropertiesに同名のbaseNameの資産が存在する.<br>
     * しかし、formatsの優先度に従ってpropertiesリソースを参照し、かつ文字コード指定に従って変換する
     */
    private void useSJISSameNamePropertiesXMLsetMultiFormat() {
        CustomControl control = CustomControl.builder()
                .charCode("SJIS")
                .formats(CustomControl.FORMAT_DEFAULT)
                .formats(CustomControl.FORMAT_XML)
                .build();
        ResourceBundle bundle = ResourceBundle.getBundle("SJIS-SAME3", control);
        System.out.println("useSJISSameNamePropertiesXMLsetMultiFormat select properties. select SJIS-SAME3.properties no garbled characters");
        this.printLog(bundle);
    }

    /**
     * ロケールがJAPANESEの時の候補Localeの第一優先がUS(日本語じゃなくて、英語のメッセージが出力される
     */
    private void useUTF8candidateLocalesMatchTargetLocale() {
        CustomControl control = CustomControl.builder()
                .charCode("UTF-8")
                .targetCandidateLocalePair(
                        TargetCandidateLocalePair.builder()
                        .targetLocale(Locale.JAPANESE)
                        .candidateLocale(Locale.US)
                        .candidateLocale(Locale.JAPAN)
                        .candidateLocale(Locale.ROOT)
                        .build())
                .build();

        ResourceBundle bundle = ResourceBundle.getBundle("utf8", Locale.JAPANESE, control);
        System.out.println("useUTF8candidateLocalesMatchTargetLocale select properties. Locale US");
        this.printLog(bundle);
    }

    /**
     * 優先度指定をしたロケール以外の場合は、対象ロケール本来の優先度に従って参照する.<br>
     * 本ケースの場合、USを指定しているので、優先度指定は無視される
     */
    private void useUTF8candidateLocalesNotMatchTargetLocale() {
        CustomControl control = CustomControl.builder()
                .charCode("UTF-8")
                .targetCandidateLocalePair(
                        TargetCandidateLocalePair.builder()
                        .targetLocale(Locale.JAPANESE)
                        .candidateLocale(Locale.JAPAN)
                        .candidateLocale(Locale.ENGLISH)
                        .candidateLocale(Locale.US)
                        .candidateLocale(Locale.ROOT)
                        .build())
                .build();

        ResourceBundle bundle = ResourceBundle.getBundle("utf8", Locale.US, control);
        System.out.println("useUTF8candidateLocalesNotMatchTargetLocale select properties. Locale US");
        this.printLog(bundle);
    }

    /**
     * 優先度指定をしたロケール以外の場合は、対象ロケール本来の優先度に従って参照する.<br>
     * 本ケースの場合、CHINAを指定しているので、優先度も無視されるし、デフォルトリソースが使用される
     */
    private void useUTF8candidateLocalesNotExistTargetLocale() {
        CustomControl control = CustomControl.builder()
                .charCode("UTF-8")
                .targetCandidateLocalePair(
                        TargetCandidateLocalePair.builder()
                        .targetLocale(Locale.JAPANESE)
                        .candidateLocale(Locale.JAPAN)
                        .candidateLocale(Locale.ROOT)
                        .build())
                .build();

        ResourceBundle bundle = ResourceBundle.getBundle("utf8", Locale.CHINA, control);
        System.out.println("useUTF8candidateLocalesNotExistTargetLocale select properties. Locale default");
        this.printLog(bundle);
    }

    /**
     * 優先度指定をしたロケールにNullを指定した場合は、パラメータで指定したロケールに置き換えて参照する.<br>
     * 本ケースの場合、優先度の順で判断した場合、一番始めヒットするのはJAPANESEのリソースであり、それが参照される.
     */
    private void useUTF8candidateLocalesExistsNullTargetLocale() {
        CustomControl control = CustomControl.builder()
                .charCode("UTF-8")
                .targetCandidateLocalePair(
                        TargetCandidateLocalePair.builder()
                        .targetLocale(Locale.JAPAN)
                        .candidateLocale(Locale.FRANCE)
                        .candidateLocale(null)
                        .build())
                .build();

        ResourceBundle bundle = ResourceBundle.getBundle("utf8", Locale.JAPAN, control);
        System.out.println("useUTF8candidateLocalesExistsNullTargetLocale select properties. Locale null = JAPAN");
        this.printLog(bundle);
    }

    /**
     * ロケールの指定をしない場合は、デフォルトロケール(ロケールがja_JP)のpropertiesを参照して、優先度指定は無視する.<br>
     * ※デフォルトロケールで優先度を参照もしないし、デフォルトリソースを参照することもしない.
     */
    private void useUTF8candidateLocalesExistsTargetLocaleButNotSetLocale() {
        CustomControl control = CustomControl.builder()
                .charCode("UTF-8")
                .targetCandidateLocalePair(
                        TargetCandidateLocalePair.builder()
                        .targetLocale(Locale.JAPANESE)
                        .candidateLocale(Locale.ROOT)
                        .build())
                .targetCandidateLocalePair(
                        TargetCandidateLocalePair.builder()
                        .targetLocale(Locale.JAPAN)
                        .candidateLocale(Locale.ROOT)
                        .build())
                .build();

        ResourceBundle bundle = ResourceBundle.getBundle("utf8", control);
        System.out.println("useUTF8candidateLocalesExistsTargetLocaleButNotSetLocale select properties. Locale ja_JP");
        this.printLog(bundle);
    }

    /**
     * データをキャッシュしない(CustomControl.TTL_DONT_CACHE).<br>
     * Controlの文字コードを変更すると"utf8NoCache"で同名のResourceBundleも参照文字コードも書き換わる
     */
    private void useNoCache() {
        CustomControl control = CustomControl.builder().timeToLive(CustomControl.TTL_DONT_CACHE).build();
        ResourceBundle bundle = ResourceBundle.getBundle("utf8NoCache", control);
        System.out.println("useNoCache before properties garbled characters. because setting charCode is default(ASCII)");
        this.printLog(bundle);

        control = CustomControl.builder().charCode("UTF-8").timeToLive(CustomControl.TTL_DONT_CACHE).build();
        bundle = ResourceBundle.getBundle("utf8NoCache", control);
        System.out.println("useNoCache after properties. no garbled characters. because setting charCode is UTF-8");
        this.printLog(bundle);
    }

    /**
     * データをキャッシュする(CustomControl.TTL_NO_EXPIRATION_CONTROL).<br>
     * Controlの文字コードを変更しても"utf8Cache1"で同名のキャッシュが書き換わらないので文字が化けない
     */
    private void useUTF8Cache1() {
        CustomControl control = CustomControl.builder().charCode("UTF-8").timeToLive(CustomControl.TTL_NO_EXPIRATION_CONTROL).build();
        ResourceBundle bundle = ResourceBundle.getBundle("utf8Cache1", control);
        System.out.println("useNoCache before properties no garbled characters. because setting charCode is UTF-8");
        this.printLog(bundle);

        control = CustomControl.builder().charCode("SJIS").timeToLive(CustomControl.TTL_NO_EXPIRATION_CONTROL).build();
        bundle = ResourceBundle.getBundle("utf8Cache1", control);
        System.out.println("useNoCache after properties. no garbled characters. because use cash. setting charCode is UTF-8");
        this.printLog(bundle);
    }

    /**
     * データをキャッシュする(CustomControl.TTL_NO_EXPIRATION_CONTROL).<br>
     * Controlの文字コードを変更しても"utf8Cache1"で同名のキャッシュが書き換わらないので文字が化けない
     */
    private void useUTF8Cache2() {
        CustomControl control = CustomControl.builder().timeToLive(CustomControl.TTL_NO_EXPIRATION_CONTROL).build();
        ResourceBundle bundle = ResourceBundle.getBundle("utf8Cache2", control);
        System.out.println("useCache2 before properties garbled characters. because setting charCode is default(ASCII)");
        this.printLog(bundle);

        control = CustomControl.builder().charCode("UTF-8").timeToLive(CustomControl.TTL_NO_EXPIRATION_CONTROL).build();
        bundle = ResourceBundle.getBundle("utf8Cache2", control);
        System.out.println("useCache2 after properties. garbled characters. because use cash. setting charCode is default(ASCII)");
        this.printLog(bundle);
    }

    /**
     * timeToLiveが停止時間よりも大きいのでキャッシュされた"utf8IntervalCache1"を参照する
     */
    private void useUTF8SleepTimeLtInterval() {
        CustomControl control = CustomControl.builder().charCode("UTF-8").timeToLive(1000L).build();
        ResourceBundle bundle = ResourceBundle.getBundle("utf8IntervalCache1", control);
        System.out.println("useUTF8SleepTimeLtInterval before properties UTF-8");
        this.printLog(bundle);

        try {
            System.err.println("sleeptime < timeToLive");
            Thread.sleep(1 * 1);
        } catch (InterruptedException e) {
            System.err.println(e);
        }

        control = CustomControl.builder().charCode("SJIS").build();
        bundle = ResourceBundle.getBundle("utf8IntervalCache1", control);
        System.out.println("useUTF8SleepTimeLtInterval after properties UTF-8. no garbled characters. because use cashe");
        this.printLog(bundle);
    }

    /**
     * 停止時間がtimeToLiveよりも大きいので改めて"utf8IntervalCache1"を参照したときに、ファイルが更新されていたらキャッシュを参照せず、再取得する
     */
    private void useUTF8IntervalLtSleepTime() {
        CustomControl control = CustomControl.builder().charCode("UTF-8").timeToLive(1L).build();
        ResourceBundle bundle = ResourceBundle.getBundle("utf8IntervalCache2", control);
        System.out.println("useUTF8IntervalLtSleepTime before properties UTF-8");
        this.printLog(bundle);

        try {
            System.err.println("timeToLive < sleeptime");
            System.err.println("edit utf8IntervalCache2.properties while sleeping !!");
            Thread.sleep(1 * 10000L);
        } catch (InterruptedException e) {
            System.err.println(e);
        }

        control = CustomControl.builder().charCode("SJIS").build();
        bundle = ResourceBundle.getBundle("utf8IntervalCache2", control);
        System.out.println("useUTF8IntervalLtSleepTime after properties UTF-8. garbled characters. because no use cashe");
        this.printLog(bundle);
    }
}

英語は実行結果で、ざっと分かれば良いレベルの適当なやっつけです。
最後のインターバルのテストは手作業が入っています。 おおよその使い方は、この実行クラスの例を見てもらえれば分かると思います。

参照するproperties

記事上は全て読めるようにしていますがmessage.propertiesがASCIIコードであることを除き、その他のファイルはプリフィックスに記した文字コードで作成しています。

ファイル名と内容

message.properties

test=ascii(default)


SJIS.xml

<?xml version="1.0" encoding="Shift_JIS" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
  <comment />
  <entry key="sjis.xml">XML読み込み(SJIS)</entry>
</properties>


SJIS-SAME.properties

test=properties:同名XMLあり1(SJIS)


SJIS-SAME.xml

<?xml version="1.0" encoding="Shift_JIS" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
  <comment />
  <entry key="sjis1.xml">xml:同名propertiesあり1(SJIS)</entry>
</properties>


SJIS-SAME2.properties

test=properties:同名XMLあり2(SJIS)


SJIS-SAME2.xml

<?xml version="1.0" encoding="Shift_JIS" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
  <comment />
  <entry key="sjis2.xml">xml:同名propertiesあり2(SJIS)</entry>
</properties>


SJIS-SAME3.properties

test=properties:同名XMLあり3(SJIS)


SJIS-SAME3.xml

<?xml version="1.0" encoding="Shift_JIS" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
  <comment />
  <entry key="sjis3.xml">xml:同名propertiesあり3(SJIS)</entry>
</properties>


utf8.properties

test=UTF8のテスト(default)


utf8_en_US.properties

test=UTF8english


utf8_ja_JP.properties

test=UTF8のテスト(JP)


utf8Cache1.properties

test=UTF8のテスト(cache1)


utf8Cache2.properties

test=UTF8のテスト(cache2)


utf8IntervalCache1.properties

test=UTF8のテスト(IntervalCache1)


utf8IntervalCache2.properties

test=UTF8のテスト(IntervalCache2)


utf8NoCache.properties

test=UTF8のテスト(NoCache)


実行結果

--- exec-maven-plugin:1.2.1:exec (default-cli) @ samples ---
useAsciiProperties select message.properties
key= test:value= ascii(default)

useUTF8PropertiesNoSetLocaleIsDefaultLocale is default locale. select utf8_ja_JP.properties
key= test:value= UTF8のテスト(JP)

useUTF8PropertiesNoExsistLocaleIsDefaultResource is default recourse. select nolabel utf8.properties
key= test:value= UTF8のテスト(default)

useSJISXML is default recourse. select nolabel SJIS.xml
key= sjis.xml:value= XML読み込み(SJIS)

useSJISSameNamePropertiesXMLnotSetFormat prioritize properties. select SJIS-SAME.properties garbled characters (T^T)
key= test:value= properties:? ̄??XML????1(SJIS)

useSJISSameNamePropertiesXMLsetSingleFormat select xml. select SJIS-SAME2.xml no garbled characters
key= sjis2.xml:value= xml:同名propertiesあり2(SJIS)

useSJISSameNamePropertiesXMLsetMultiFormat select properties. select SJIS-SAME3.properties no garbled characters
key= test:value= properties:同名XMLあり3(SJIS)

useUTF8candidateLocalesMatchTargetLocale select properties. Locale US
key= test:value= UTF8english

useUTF8candidateLocalesNotMatchTargetLocale select properties. Locale US
key= test:value= UTF8english

useUTF8candidateLocalesNotExistTargetLocale select properties. Locale default
key= test:value= UTF8のテスト(default)

useUTF8candidateLocalesExistsNullTargetLocale select properties. Locale null = JAPAN
key= test:value= UTF8のテスト(JP)

useUTF8candidateLocalesExistsTargetLocaleButNotSetLocale select properties. Locale ja_JP
key= test:value= UTF8のテスト(JP)

useNoCache before properties garbled characters. because setting charCode is default(ASCII)
key= test:value= UTF8????????????(NoCache)

useNoCache after properties. no garbled characters. because setting charCode is UTF-8
key= test:value= UTF8のテスト(NoCache)

useNoCache before properties no garbled characters. because setting charCode is UTF-8
key= test:value= UTF8のテスト(cache1)

useNoCache after properties. no garbled characters. because use cash. setting charCode is UTF-8
key= test:value= UTF8のテスト(cache1)

useCache2 before properties garbled characters. because setting charCode is default(ASCII)
key= test:value= UTF8????????????(cache2)

useCache2 after properties. garbled characters. because use cash. setting charCode is default(ASCII)
key= test:value= UTF8????????????(cache2)

useUTF8SleepTimeLtInterval before properties UTF-8
key= test:value= UTF8のテスト(IntervalCache1)

sleeptime < timeToLive
useUTF8SleepTimeLtInterval after properties UTF-8. no garbled characters. because use cashe
key= test:value= UTF8のテスト(IntervalCache1)

useUTF8IntervalLtSleepTime before properties UTF-8
key= test:value= UTF8のテスト(IntervalCache2)

timeToLive < sleeptime
edit utf8IntervalCache2.properties while sleeping !!
useUTF8IntervalLtSleepTime after properties UTF-8. garbled characters. because no use cashe
key= test:value= UTF8縺ョ繝?繧ケ繝?(IntervalCache2edit)

最後のテストケースはスリープ中に手作業で直接ファイルを更新
更新前
f:id:vermeer-1977:20161228135327p:plain

更新後
f:id:vermeer-1977:20161228135328p:plain

参考リンク

ResourseBundle

国際化対応や、ResourseBundleそのものの詳細は「Java SE 6完全攻略」から目を通すのが一番良いと思います。

文字コード変換

考慮はしませんでしたが調査足跡という意味でリンクだけ残しておこうと思います。ひょっとしたらファイルアップロード関連のチェックで改めて検討をする可能性もあるので。

著作権

本題とは関係ないですが、外部ライブラリを使用するにあたって調べたライセンス関連のリンク。


さいごに

やってみたものの、正直な感想は、こういう拡張は作法として良いのだろうか?という思いです。*2
いずれにせよ 全部入りなResourceBundleを とりあえず作れて個人的には満足です。
今後、今回のようなボリュームのあるものを記事にするときのためにGitHubについても勉強をした方が良いのかもしれないと新たな課題も見つかりました。

追記:2017/1/9

GitHubでプロジェクトを作成しました。
GitHub - vermeer-1977-blog/resource-bundle: 全部入りのResourceBundle

*1:"You ain't gonna need it"

*2:むしろ、どなたかに、これはダメだよ、と指摘していただけると非常にありがたいです。