Bean Validationの日本語メッセージリソースのドラフトをQiitaにアップしました

ここではお知らせのみ。Bean Validation (Hibernate Validator) の日本語メッセージリソースのドラフトを考えたので、Qiita にアップして皆さんの意見を募ることにしました。

qiita.com

みんなから意見を聞くタイプのものは Qiita の方がいいかなあと思い、初めて Qiita を利用してみました。こういうの他には何がいいのだろう?

JavaFXの非矩形ウィンドウにおけるWindowsとMacでのマウスイベントの違い

しばらく blog を書いていなかったので先日気がついた小ネタでも書きます。

かなり昔に JavaFX で xeyes のクローンを作ってみた話 を書いたことがあったのですが、最近あれをイチから作り直してみました。コードも GitHub にアップしています。 *1

github.com

以前のブログエントリからは次のようにかなり作りが変わっています。

  • 以前は Mac 上で Swing の EDT を実行すると HeadlessException が飛ぶ問題があり、仕方なく Swing を土台として作っていたのですが、現在はこの問題は解消されているので、JavaFX を土台としたものに変更しています。
    • 唯一、グローバルなマウスカーソルのアドレスを取得するのに AWT に頼っていますが、JavaFX 11 で待望の Robot クラスが追加されるので、11 になれば "Pure JavaFX" なアプリケーションにできる見込みです。
  • マウスカーソルの座標を取得するタイミングを以前は Swing の Timer で起こしていたのですが、これを JavaFXAnimationTimer を利用するように変更し、より滑らかに目玉が動くようになっています。
  • Stage のスタイルを StageStyle.TRANSPARENT に変更し、本家 xeyes のように背景を透過するようにしました。

f:id:aoe-tk:20180825210949p:plain

最後のポイントに書いたように、Stage のスタイルを StageStyle.TRANSPARENT に変更したため、ウィンドウのタイトルバーやウィンドウ枠がなくなるため、ウィンドウの移動やリサイズを自力で実装する必要があります。

その際に気が付いたことがありました。リサイズや移動が可能であることをが分かるように、アプリケーション上でのマウスカーソルの位置によってマウスカーソルの形状を変えるようにしました。ところが Windows 上で動かした場合と Mac 上で動かした場合に次のような違いが見られたのです。

  • Mac 上で動かした場合、マウスカーソルが (透明になっている) ウィンドウの端や隅に到達してもちゃんとカーソル形状が変わる。
  • Windows 上で動かした場合、「目」の上でしかマウスカーソルの形状が変更しない。
    • と言うか、マウスイベントが全て「目」の上でしか発生しない。

この動きの違いに悩んで色々調べてみたのですが、どうも JavaFX 側の考え方としては Windows 版の動きが正 のようです。Mac 版の動きについて次のようなバグチケットが作られていました。

https://bugs.openjdk.java.net/browse/JDK-8088104

まあ考えてみれば理屈は分かります。非矩形に作ったのにマウスが矩形に沿って反応するのは確かに不自然です。自分が作ったこの xeyes については Windows 版ではウィンドウの四隅からリサイズが行えないという不便な点がありますが、縦方向と横方向のリサイズは可能なので、まあこれでいっかーとそのままにしましたw

というわけで小ネタでした。

*1:完全に自分用に作ったので IntelliJ 形式のプロジェクトそのままでアップしてます。まあ特殊なライブラリは一切使っていないので、他の IDE やエディタでも普通に扱えるはずです。

Java Client Roadmap Updateによせて (後編)

というわけで先日アップした次のエントリの後編です。

aoe-tk.hatenablog.com

前回は年寄りの思い出話という感じでしたがまさかの大きな反響を頂いて驚いています。後編については JavaFX や Swing、そしてクロスプラットフォーム GUI の今後について思うところを書いていきたいと思います。

JavaFX は今後どうなる?

今回の決定で JavaFXJDK リリースから分離されることになったわけですが、逆に言うと JDK のリリースサイクルに縛られること無く開発を進められることになります。そして、私の感覚からすると、当面 JavaFX が廃れるような心配はしなくていいと見ています。

JavaFXJava EE と同様によりオープンソースコミュニティに今後の開発をゆだねることになりましたが、JavaFX のコミュニティは今でもとても盛り上がっています。OpenJFX の ML では今でも盛んに議論がされていますし、先のホワイトペーパーでも「情熱的なコミュニティ」であると賞賛しています。

実は今回の発表の少し前に OpenJFX のリーダーである Kevin Rushforth 氏から OpenJFX コミュニティの今後の方向性について問いかけがありました。

http://mail.openjdk.java.net/pipermail/openjfx-dev/2018-February/021335.html

この議論では大変な盛り上がりを見せ、より外部の人が入りやすくなるような雰囲気にしようという方向になりました。その一環として GitHub に OpenJFX のリポジトリのミラーも作られ、PR を受け付けられるようにしています (OCA へのサインは必要です) 。

github.com

このように、JDK のリリースサイクルに縛られず、より外部も参加しやすくなることで、かえって開発のスピードが上がる可能性もあると考えています。前編でも述べたように海外では結構採用事例があり、Gluon のように JavaFX をビジネスのメインにしている会社もあります *1

と言うわけで、Java の標準 GUI では無くなりましたが、優秀なライブラリとしてあり続けるだろうと思っています。前編でも述べましたが、とても開発しやすい API ですし、JavaGUI を作るときの最有力候補であることは変わらないと思っています。コミュニティとの連携も取りやすくなる方向になっていますし、業務で JavaFX を採用したところがあってもそんなに心配しなくてもいいと思います。

私自身はここ数年フロントエンドとは余り縁の無い仕事をしている事もあり、業務で JavaFX を触る機会は無かったのですが、自分の身の回りでで GUI を使った道具が必要な時は JavaFX で作っていました。今後もそのためには JavaFX を使い続けると思っています。

Swing について

紆余曲折を経て、Java のデフォルト GUI は Swing に戻った感があるのですが、前編でも述べたように OpenJDK では再び Swing に力が入りそうな雰囲気になっています。Swing に JavaFX の良いところ (FXML とかバインディングとか) が移植されるような流れにならないかなあ。

私は Swing は一定の成功を収めたと思っています。何より Swing が覇権を握っている分野があります。それは IDE です。NetBeans と JetBrains の IDE だけじゃないかと突っ込まれそうですが、特に JetBrains IDE のここ最近の躍進ぷりはすごいです。JavaScriptPythonPHPRuby、Go など実に様々な言語コミュニティで人気を博しています。

クロスプラットフォーム GUI の今後

今回の件でクロスプラットフォーム GUI というのはやっぱり難しいものだなとは思いました。でも需要があるのは確かです。ゲームエンジンの様にユースケースをより絞ったものは普及していますしね。

汎用的なクロスプラットフォーム GUI で一番成功しているのはやはり Qt でしょうか。モバイルへの進出にも成功していますし。開発言語は C++ ですが、他のプログラミング言語へのバインディング も多いです。ですが、Java バインディングである Jambi が死んでしまったんですよね...。 *2

後は有力候補としてはやはり Electron なんですかねえ。私は好きじゃないんですよ。ぶっちゃけ Chrome ブラウザそのものなので、Electron アプリを 1 つ立ち上げると (潤沢にリソースを使う) Chrome ブラウザが余計に 1 つ立ち上がるようなものなので、使う側としてそんなに好きじゃないんです。Web 開発の難しさをデスクトップ GUI アプリ開発に持ち込みますし、そんなに JavaScript + CSS で開発したいですか?と言いたい感じです。まあ古典的なスタイルの GUI 開発が好きな人間としては、WebComponents に期待しているところです。

モバイル OS 戦争に敗れ去った MS は Xamarin に力を入れていますし、最近は Flutter なんてのも登場しましたし、今後もクロスプラットフォーム GUI へのトライアルは色々な形で出てくるでしょうね、というところで雑感垂れ流しのエントリを締めくりりたいと思います。

*1:この会社は JavaFXiOSAndroid 向けアプリを開発できる、Gluon Mobile などを販売しています。

*2:3 年ほどコミットがなく、公式サイトもドメインが乗っ取られています...。

Java Client Roadmap Updateによせて (前編)

既にご存知の方も多いと思いますが、先日 Oracle から JavaFX をはじめとする、Java のクライアントテクノロジーについて今後のロードマップが発表されました。

https://blogs.oracle.com/java-platform-group/the-future-of-javafx-and-other-java-client-roadmap-updates

上記ブログエントリでは主に JavaFX の今後の扱いについて述べていますが、以下のホワイトペーパーにはそのほかに Applet や Java Web Start、そして Swing/AWT といった Java のクライアントテクノロジー全般の今後のロードマップについて記載されています。

http://www.oracle.com/technetwork/java/javase/javaclientroadmapupdate2018mar-4414431.pdf

このホワイトペーパーを読んで、まあ色々思うことがありました。Java のクライアントテクノロジーについての過去も振り返りながら私が感じたことを色々語りたくなったので、このエントリを書きました。まあ巷で言う「ポエム」ってやつですね。

長くなったので前後編に分けて書くことにしました。

ホワイトペーパーに書かれていたこと

ホワイトペーパーに記載されていた内容を簡単にまとめると次の通りです。

  • Oracle Java SE 11 からは JavaFX を同梱しないことを確定
    • これまで Oracle JDK には JavaFX を同梱、OpenJDK には同梱しないという状況だったが Oracle JDK も同様になる
    • 今後も OpenJDK には同梱されないという状況はそのまま
    • JavaFXJava のデフォルト GUI ツールキットではなくなることを意味する
  • Applet のサポートは予告通り Java SE 8 までで、Java SE 11 には含まれなくなる
  • Java Web Start についても Java SE 11 以降には含まないことを明言
    • これは今回の発表で初めて明らかになったこと
  • Swing と AWT については Java SE 11 においても開発を継続する

JavaFX については後ほど述べるとして、注目してもらいたいことは Applet、Web Start の扱いです。 これらに対する実行環境のアップデートが提供され続けるのは Java SE 8 のサポート期間の間だけ ということです。

よって、特に Applet を使った業務システムを展開しているところは Java SE 8 の無償アップデート提供期間中に対応を検討する必要があります。特にブラウザ Java Plug-in 部分はクライアント PC のセキュリティに直結するため、アップデートせずに放置するという選択肢は危険です。対応策としては次のようになります。

  • Oracleの商用サポートを契約し、継続して実行環境のアップデートを受け取る
  • アプリケーションを改修し、ブラウザ上で動かすのではなく、JRE を同梱したスタントアロンアプリケーションとしての配布に切り替える

というわけで、このロードマップ発表で Java のクライアントテクノロジーは転換点を迎えることになりました。これまでの経緯をちょっと振り返ってみようかなと思います。

Java クライアントテクノロジーの変遷について振り返り

Java 誕生時の GUI

Java は誕生時から AWT という GUI ツールキットを同梱していました。Java の主な目的の1つに Web ブラウザ上で GUI アプリケーションを動かせるようにすることがあったからです。

AWT は画面に描画する GUI 要素は WindowsMac といった実行環境のコントロールを使っていました。それに対して抽象化層を提供するものであったため、用意された GUI 要素は最大公約数的なものでした。そこで、AWT をベースに、Java2D を使用して全ての GUI 要素を自分で描画するSwing というツールキットが登場します。これは当時 Netscape 社が独自に開発したものに Sun が目を付けて共同開発したものです。

Swing はどの OS でも動く GUI を構築できるということで非常に画期的でしたが、如何せん登場時期が早すぎました (20世紀のことです) 。当時のクライアント PC のスペックでは動作が重すぎました。私は MMX Pentium 166MHz のマシンで初めて Swing を動かしたのですが、全ての動きがスローモーションで笑っちゃうしかなかったのを覚えています。

ですがこれにめげることなく継続的にアップデートを続け、JDK1.4 あたりでようやく実用的な速度が出るようになり、この頃から業務アプリケーション向けクライアント開発をはじめ、そこそこ採用されるようになってきた印象があります。私もこの時期に Swing での開発を経験しています。

Swing の発展

そして Java 5 から 6 に掛けた辺りで、当時既に普及していた Web アプリケーションに対して、当時の HTML の表現力の限界からリッチクライアントが再び見直される流れが出てきます。特に Macromedia (現在は Adobe に吸収) が Flash を業務クライアントとして推すことに熱心で、ここから Flex が誕生します。その流れを受け、Java SE 6 の頃に Sun は Swing 関連の技術に大きなてこ入れを行います。

  • Java.net にデスクトップ専用のプロジェクトができる
  • Java Plug-in の強化
  • デスクトップ環境と連携する API の追加
    • 特定のファイル・タイプに関連付けられたデフォルト・アプリケーションと対話する機能を提供する java.awt.Desktop クラス
    • デスクトップ環境のシステムトレイにアクセスし、アプリケーション独自のトレイアイコン、メニューを追加できる java.awt.SystemTray クラス
  • Swing 開発のためのアプリケーションフレームワークプロジェクトとその関連技術の開発が始まる

RIA の潮流と JavaFX の登場

この頃が Swing の全盛期だったかなあと思っています。そして大体同じ頃に別の流れが出てきました。それがコンシューマ向け Web を対象とした RIA の流れです。MicrosoftSilverlight をリリースした辺りから Flash と共に次のような特徴を備えたものとして注目され始めました。

  • HTML の限界を超えた操作性を提供
  • Web ブラウザ上でもデスクトップ上でも実行可能
  • お仕着せのコンポーネントでは無く、デザイナが GUI 要素を自由自在にデザインできる
    • この頃 iPhone がマルチタッチによる操作性と流麗なグラフィックの GUI をひっさげてデビューしたことも大きな影響を与えていたと思います

もちろん Java にも Applet や Web Start という同様の技術がありました。Swing も GUI 要素を自分で描画する仕組みになっているため、頑張ればどんな見た目にすることもできます。しかし、デザイナが扱うのには向いていないプラットフォームでした。

そこで Sun が目を付けたのが、Sun が買収した企業である SeeBeyond に所属していた Christopher Oliver 氏が開発していた From Follows Function (F3) というプロジェクトでした。 これは確か Swing を開発しやすくするための DSL だったと思います。 (さくらばさんより指摘があり、これは正しくないとのことでした。F3 は Flash + ActionScriptDSL を目指したもので、たまたま実行の土台として Swing を使っていたとのことです。) Sun はこの技術をベースに JavaFX というプロジェクトを立ち上げ、次のような特徴を備えたコンシューマ向け GUI 開発、実行環境としてデビューさせます。

最初は Swing のラッパーコンポーネントという印象もあった JavaFX ですが、Sun はこれを AWT に頼らない GUI ツールキットとして発展させます。Prism エンジンを開発して GPU を積極的に活用する新世代の Java GUI ツールキットになっていきました。この頃から Sun は Swing への投資を急速にトーンダウンさせ、先ほど挙げた Swing App Framework 関連の JSR は中止してしまいました。これは当時 Swing 開発者からの反発もかなり大きかったことを覚えています。

ともかく Sun は JavaFX をすごくアピールしていました。Sun の開催するカンファレンスでは JavaFX を使った流麗なインターフェースをよくデモしていました。ですが、最も JavaFX を浸透させたかったスマートフォンへの展開を失敗してしまいます。旧 Windows Mobile に提供したのみで、水面下で Android への採用も持ちかけていたという話を聞いたこともあるのですが、現実はそうでないことは皆さんのよく知るところです。

そして Oracle による Sun 買収というイベントが起きます。

Oracle の買収と JavaFX の方向転換、発展

Oracle の買収で JavaFX がどうなるか不安視されましたが、JavaFX は引き続き盛り上げていくことを宣言します。ですが、OracleJavaFX をコンシューマ向け開発では無く、あくまで業務向け開発のツールにすることを重視しました。...とは Oracle ははっきり言っていませんが、その後の動きや Oracle のビジネス領域から考えれば明白でした。

JavaFX 2.0 において、JavaFX Script を廃止することが決まりました。そして、Java + FXML で開発するというスタイルに変わります。当然のことながらこれは大騒ぎになりました。 *1

ですが、結果としてこれは JavaFX にはプラスとなりました。本来の Java 開発者層が JavaFX に関心を持ち、注目度がぐっと上がりました (そのかわり Web デザイナ層は離れていきましたが...) 。古くささが見えてきた Swing と比べ、次のような点が優れていました。

  • FXML による GUI 構造の分離
  • プロパティとバインディング API によるリアクティブなスタイルでのビュー更新
    • これは遅延実行の仕組みを備えるなど、他のプラットフォームと比べても優れていました
  • Lambda Ready

私が JavaFX を本格的に触りだしたのもやはりこの時期でした。Flex による RIA 開発経験をしたこともあり、ユーザーに様々な UX を提供可能な RIA の可能性に目を向けていたところ、Java で開発可能な RIA プラットフォームが登場したので注目をしたのです。Flex と比べて優れているところも多いと評価していました。

2013 年の Java Day Tokyo の Java the Night に登壇したとき はこんなツールをデモして、JavaFX の魅力をアピールしたりしてました。

f:id:aoe-tk:20130519005331p:plain

Oracle は積極的に JavaFX の開発を進め、JavaFX 8 までは盛んに機能追加も行い、今後 Java の標準 GUIJavaFX になると宣言しました。JavaOne で iOSAndroidバイスでの稼働をデモしたりと、Oracle は本気だと私もすごく期待していました。

主に欧州を中心に業務系システムやトレーディングツールなどでの採用例が出てくるようになります。NASA など科学技術研究の現場での採用も多かったように思います。 Eclipse GEF も最新版では JavaFX ベースになっています。

そして Oracle の方向転換

ですが、JavaFX 8 のリリースがピークでした。JavaFX 8 リリースの後辺りから次のように徐々に暗雲が垂れ込んできました

  • iOSAndroid への搭載の話はトーンダウンし、OpenJFX でソースだけ公開してお茶を濁す
    • 肝心の JVM 部分の公開がなかった
  • Raspberry Pi での実行にも積極的になったかと思えば、1 年後にはトーンダウン
    • 最初は Oracle Java SE Embedded に JavaFX を同梱していましたが、途中からやめてしまいました
  • Scene Builder も 2.0 リリースを最後にメンテナンスモードに

Java 9 では Jigsaw 対応で手一杯で新機能の追加はほとんどなく、明らかにリソースを絞っているように見えました。そして今回の発表につながります。

ホワイトペーパーを見る限り、OracleJava のクライアントテクノロジーに対する興味がほとんど無いことが分かります。現在は Web ファースト、モバイルファーストの流れであると結論付けています。まあ Oracle の立ち位置的にここまで見てくれたのが奇跡的ですらあったのかも知れませんが。

なお OpenJDK コミュニティは AWT、Swing、Java2D の強化に積極的で、そういう意味では様々な紆余曲折を経て Java 標準 GUI の座は再び Swing に戻ってきたのかも知れません。

と言うわけで Java のクライアントテクノロジーの歴史を振り返ったところで前編終了。長くなりすぎて疲れましたw 後編では JavaFX 、そしてクロスプラットフォーム GUI の今後について思うところを書こうかなと思っています。

*1:当時のさくらばさんの怒り様はまあすごかったです (^^;;

Asakusa Direct I/O formatted textの紹介

はじめに

このエントリは Asakusa Framework Advent Calendar 2017 の20日目のエントリです。実は 2 年ぶりくらいにお仕事で Asakusa Framework を使った開発をしているので、今年は参加することにしました。

Asakusa Framework は 6 年前の初期リリースから継続して機能の追加を積極的に行っています。先ほど 2 年ぶりに Asakusa で開発していると書きましたが、その間にも色々進化していて、確実に便利になっていると感じました。

今回はその中の Direct I/O formatted text について紹介したいと思います。これはバージョン 0.9.1 になって追加された、かなり新しい機能です。

Direct I/O は HDFSAWS S3、GCS といった分散ファイルシステム上に保存されたファイルを透過的に読み書きする機能ですが、これまで次のようなファイルの種類に応じてデータモデルへのマッピングを行う機能が用意されていました。

  • Direct I/O CSV
  • Direct I/O TSV
  • Direct I/O line

DirectI/O formatted text はこれまでファイルの種類に応じて別々の API を用いていたところに統一的な機能を提供し、さらに機能追加も行われた強力なライブラリになっています。次のような強みがあります。

  • より多様なデータ形式に対応できる
  • 不整合データに対して柔軟な操作設定ができる
  • 読み込み時にファイルについてのメタ情報を入れられる

詳細についてはドキュメントをじっくり読んでもらうとして、ここでは個人的に嬉しかった点を挙げていきたいと思います。

嬉しかった点その1

「きちんとしていないデータに対してかなり無理が利く」という点です。

例えば、複数のシステム間で連携をするとします。各システムにサービスインターフェースが用意されていればいいですが、そうも行かず、いわゆるファイル連携をすることも多いと思います。

このファイル連携、とにかく泥臭い対応が必要になることが多いですよね。連携先の各システムに「CSV ファイルで連携しましょう」って声を掛けても次のように綺麗なデータが来るとは限らないことが多かったりしますよね。

  • 改行コード、エンコーディングがばらばら
  • 同じデータに対応したものなのにヘッダがあったりなかったり
  • 固定長ファイルを単純に CSV 化したのか、各項目が空白埋めでご丁寧に桁を揃えられているとか
  • 行によってカラム数が異なる CSV 的なものが出てくるとか
    • 本体のデータ部分以外に見出しや合計行なんかも 1 つのファイルに混じっている例を見たことがあります...

Direct I/O formatted text はかなり柔軟な読み込みオプションがあり、このような汚いファイルに対してもかなり無理が利くようになっています。

Direct I/O formatted text を使うためには DMDL のモデル定義に @directio.text.tabular (CSV以外の形式のファイルを扱う場合に使い、区切り文字を指定する) もしくは @directio.text.csv 属性を指定します。

@directio.text.csv(
    charset = "Windows-31J",
    header = skip,
    trim_input = true,
    true_format = "1",
    false_format = "0",
    date_format = "yyyy/MM/dd",
    datetime_format = "yyyy/MM/dd HH:mm:ss",
    on_more_input = report,
    on_less_input = ignore
)
data_model = {

};

属性に指定しているオプションに注目してください。 charset 指定があるのは当然のこととして、どの値を真偽値としてマッピングするか日付時刻のフォーマットも指定可能です。

注目してほしいのは header 指定で、読み込みや書き込みにおいてのヘッダ指定を次のようにかなり柔軟に指定可能です。

指定値 内容
nothing 何もしない (ヘッダ行が無いものとして読み込み、ヘッダを出力しない)
force ヘッダが存在しているものとして読み込み (1行スキップする) 、ヘッダを出力する
skip 読み込み時にヘッダが あれば スキップし、ヘッダは出力しない
auto 読み込み時にヘッダが あれば スキップし、ヘッダを出力する

skipauto といった、ヘッダのあるなしをよしなに扱ってくれます。こんな気配りができるライブラリは割と珍しいと思いませんか。

trim_input オプションは先頭や末尾に空白文字が入っていると除去してくれます。これでわざわざ @Update 演算子を使わなくてもいいですね。

on_more_inputon_less_input なんてオプションもあります。前者は読み込み時にレコードに余計なフィールドがあった場合の動作を、後者はレコードのフィールドが不足している場合の動作を指定できるようになっており、少々変なデータでも対処できるようになっています。この手のライブラリは普通例外を飛ばして終わりということが多いですよね。

on_more_input の場合の指定内容は次の通りです。

指定値 内容
error エラーログを出して異常終了する
report 警告ログを出力した上で無視する
ignore 単に余剰フィールドを無視する

on_less_input の場合の指定内容は次の通りです。

指定値 内容
error エラーログを出して異常終了する
report 警告ログを出力した上で不足フィールドに NULL を入れる
ignore 不足フィールドに NULL を入れる

これで行によってカラム数が異なる CSV 的なもへの対処とかもできそうですよね。

嬉しかった点その2

「読み込み時にファイルのメタ情報をデータモデルに追加できる」という点です。

データフィールド属性に次のような読み込んだファイルに関する情報を埋め込むためのものが用意されています。

属性 内容
@directio.text.file_name 属性を指定されたフィールドにファイルパスを設定する
@directio.text.line_number 属性を指定されたフィールドに当該データのファイルの行番号を設定する (物理的な行番号であるため、途中で改行の入っているレコードが存在した場合、次のレコードは番号がスキップすることになる)
@directio.text.record_number 属性を指定されたフィールドに当該データのファイルのレコード番号を設定する (こちらは論理的な行番号)

でファイルのデータチェックで、ファイル名や行番号の情報も出力したいような場合でも Asakusa 上で実行できるようになりますね。

終わりに

とりあえずはこんなところです。他にも多彩な機能が用意されているので、詳しくはドキュメントを読んでください。

Asakusa Framework は開発の現場で遭遇する「これがあったらいいな」的な機能を結構貪欲に取り込み続けています。取り込んで欲しい機能があったら MLGitHub (日本語でも OK ですよ) などで積極的に提案してみることをお勧めします。

Bean ValidationのJavaFX対応

このエントリは JavaFX Advent Calendar 2017 の 19 日目のエントリです。前日は id:planet-az さんによる「 簡単なミュージックプレーヤーをさらにいじってみた 」でした。

はじめに

今回取り上げるのは、 JSR 380 Bean Validation 2.0JavaFX 対応についてです。そうです、Java EE に含まれる Bean Validation が JavaFX に対応したのです! まさか EE の仕様が JavaFX のことを考慮するとは思わなかったので、これは非常に驚きました。

具体的には JavaFX のプロパティ API に対して Bean Validation が対応しました。このエントリではその使い方について紹介したいと思います。

Bean Validation 2.0 について

Bean Validation 2.0 は Java EE 8 に含まれる仕様です。以下の Web サイトに Specification がまとめられています。

http://beanvalidation.org/2.0/spec/

新機能の 1 つとして次のようなものが挙げられています。

  • Support for validating container elements by annotating type arguments of parameterized types, e.g. List<@Positive Integer> positiveNumbers (see Container element constraints); this also includes:
    • More flexible cascaded validation of collection types; e.g. values and keys of maps can be validated now: Map<@Valid CustomerType, @Valid Customer> customersByType
    • Support for java.util.Optional
    • Support for the property types declared by JavaFX
    • Support for custom container types by plugging in additional value extractors (see Value extractor definition)

新たにコンテナ要素のバリデーションに対応しているとあります。コレクションや Optional オブジェクトなどの中身に対してバリデーションを掛けられるようになったのです。そしてこの対応の一環として、 JavaFX のプロパティもデフォルトで対応するようになったのです。 *1

JavaFX アプリケーションでの Bean Validation の使い方

それでは早速使ってみます。折角 JavaFX を使っているので、バインドを活用してみることにします。

作成するサンプルアプリケーション

作ってみるのは次のようなユーザ登録フォームを想定したものです。

f:id:aoe-tk:20171218231023p:plain

テキストフィールドに文字を打ち込むたびにバリデーションを行い、正しい文字列が入力されたタイミングでエラーメッセージが消えるようになります。E-mail、パスワードどちらも OK になれば登録ボタンがクリック可能になります。

f:id:aoe-tk:20171218231509g:plain

E-mail 欄のバリデーションには Bean Validation 2.0 で新たに入った @Email を使ってみることにします。

依存性の設定

まず、JavaFX アプリケーションで Bean Validation を使えるようにします。参照実装である Hibernate Validator を使います。Maven だと 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>

<%-- Snip --%>

    <dependencies>
        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>6.0.4.Final</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish</groupId>
            <artifactId>javax.el</artifactId>
            <version>3.0.0</version>
        </dependency>
    </dependencies>

<%-- Snip --%>

</project>

Model、FXML の実装

Model のコードは次のようになります。

package aoetk.sample.beanvalidation;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;

public class Model {
    @NotEmpty
    @Email
    private StringProperty email = new SimpleStringProperty("");

    @Size(min = 8)
    private StringProperty password = new SimpleStringProperty("");

    public String getEmail() {
        return email.get();
    }

    public StringProperty emailProperty() {
        return email;
    }

    public void setEmail(String email) {
        this.email.set(email);
    }

    public String getPassword() {
        return password.get();
    }

    public StringProperty passwordProperty() {
        return password;
    }

    public void setPassword(String password) {
        this.password.set(password);
    }
}

フィールドに着目してください。このように JavaFX プロパティに対して直接 Bean Validation のアノテーションを指定可能になっています。

次に View としての FXML のコードを示します。

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

<%-- import略 --%>
<GridPane hgap="10.0" vgap="10.0" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml/1" fx:controller="aoetk.sample.beanvalidation.Controller">
   <rowConstraints>
      <RowConstraints minHeight="10.0" />
      <RowConstraints minHeight="10.0" />
      <RowConstraints minHeight="10.0" />
   </rowConstraints>
   <columnConstraints>
      <ColumnConstraints hgrow="NEVER" minWidth="10.0" />
      <ColumnConstraints hgrow="ALWAYS" minWidth="10.0" />
   </columnConstraints>
   <children>
      <Label text="E-mail" />
      <Label text="Password" GridPane.rowIndex="1" />
      <Button fx:id="registerButton" mnemonicParsing="false" text="Register" GridPane.columnSpan="2" GridPane.halignment="CENTER" GridPane.rowIndex="2" />
      <VBox GridPane.columnIndex="1">
         <children>
            <TextField fx:id="emailField" prefHeight="27.0" prefWidth="350.0" />
            <Label fx:id="emailErrorLabel" text="Label" textFill="red" />
         </children>
      </VBox>
      <VBox GridPane.columnIndex="1" GridPane.rowIndex="1">
         <children>
            <PasswordField fx:id="passwordField" />
            <Label fx:id="passwordErrorLabel" text="Label" textFill="red" />
         </children>
      </VBox>
   </children>
   <padding>
      <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
   </padding>
</GridPane>

Scene Builder で見ないとちょっと分かりにくいかも知れませんね。入力フィールドに加え、エラーメッセージについても fx:id 属性を与えて Controller 側でアクセスできるようにしている点に注目してください。

Controller でのバインド、バリデーション実装

そして、この Model と View を Controller でバインドし、さらにフィールドに対して入力が発生した場合にバリデーションが実行されるようにします。

// (import略)
public class Controller implements Initializable {
    @FXML
    Label emailErrorLabel;

    @FXML
    Label passwordErrorLabel;

    @FXML
    Button registerButton;

    @FXML
    TextField emailField;

    @FXML
    PasswordField passwordField;

    private StringProperty emailMessage = new SimpleStringProperty("");

    private StringProperty passwordMessage = new SimpleStringProperty("");

    private Model model = new Model();

    private Validator validator;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        final ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
        validator = validatorFactory.getValidator();
        validateModel();

        emailErrorLabel.textProperty().bind(emailMessage);
        passwordErrorLabel.textProperty().bind(passwordMessage);
        registerButton.disableProperty().bind(emailMessage.isNotEmpty().and(passwordMessage.isNotEmpty()));
        emailErrorLabel.managedProperty().bind(emailMessage.isNotEmpty());
        passwordErrorLabel.managedProperty().bind(passwordMessage.isNotEmpty());
        model.emailProperty().bind(emailField.textProperty());
        model.passwordProperty().bind(passwordField.textProperty());
        emailField.textProperty().addListener(observable -> validateModel());
        passwordField.textProperty().addListener(observable -> validateModel());
    }

    private void validateModel() {
        final Set<ConstraintViolation<Model>> violations = validator.validate(model);
        emailMessage.set("");
        passwordMessage.set("");
        for (ConstraintViolation<Model> violation : violations) {
            if (violation.getPropertyPath().toString().equals("email")) {
                emailMessage.set(violation.getMessage());
            } else if (violation.getPropertyPath().toString().equals("password")) {
                passwordMessage.set(violation.getMessage());
            }
        }
    }
}

initialize() メソッドの最初で Validator オブジェクトを取得している点については特に説明不要ですね。その後すぐに validateMode() メソッドをコールして初期状態でのバリデーションを実行しています。

エラーメッセージは Validator によるバリデーションの結果出力されたメッセージをそのまま使うようにしました。メッセージを格納する StringProperty オブジェクトを Controller のフィールドとして持たせ、それをエラーメッセージの Label とバインドさせています。

mailErrorLabel.textProperty().bind(emailMessage);
PasswordErrorLabel.textProperty().bind(passwordMessage);

また、このメッセージが空文字の場合はボタンをクリック可能にし、エラーメッセージラベルもシーングラフから取り除くようにしました。これも次のようにバインドを活用しています。
(managed プロパティを false 設定すると一時的にシーングラフから取り除かれます)

registerButton.disableProperty().bind(emailMessage.isNotEmpty().and(passwordMessage.isNotEmpty()));
emailErrorLabel.managedProperty().bind(emailMessage.isNotEmpty());
passwordErrorLabel.managedProperty().bind(passwordMessage.isNotEmpty());

そして、テキストボックスやパスワードフィールドの入力内容を Model のプロパティに反映させるためにバインドします。

model.emailProperty().bind(emailField.textProperty());
model.passwordProperty().bind(passwordField.textProperty());

テキストボックスやパスワードフィールドの textProperty をリッスンし、文字列が入力されるたびにバリデーションを発火します。

emailField.textProperty().addListener(observable -> validateModel());
passwordField.textProperty().addListener(observable -> validateModel());

最後にバリデーションを実行する validateModel() の実装を見てみます。モデルに対してバリデーションを実行し、結果として返ってきた ConstraintViolation オブジェクトのプロパティ名をチェックし、対応するプロパティ用のエラーメッセージ StringProperty オブジェクトにメッセージをセットしています。 後はバインドの仕組みで自動的にメッセージ表示/非表示やボタンのクリック可/不可が制御されます。

private void validateModel() {
    final Set<ConstraintViolation<Model>> violations = validator.validate(model);
    emailMessage.set("");
    passwordMessage.set("");
    for (ConstraintViolation<Model> violation : violations) {
        if (violation.getPropertyPath().toString().equals("email")) {
            emailMessage.set(violation.getMessage());
        } else if (violation.getPropertyPath().toString().equals("password")) {
            passwordMessage.set(violation.getMessage());
        }
    }
}

JavaFX のバインドの仕組みを活用することで、プレゼンテーションサイドのロジックがかなり宣言的になっていることが分かると思います。

このようにして先のスクリーンショットに示したような動作を実現しました。

コードの全体は GitHub にアップしています。

https://github.com/aoetk/javafx-beanvalidation-sample

まとめ

以上に示したように、Bean Validation 2.0 では JavaFX のプロパティを対象としたバリデーションが可能になりました。バインドなど JavaFX の機構を活用して、ユーザに対して応答性の良いバリデーションの仕組みを実装できるようになります。

今回はキー入力のタイミングでバリデーションを行ってみましたが、もちろん他にもフォーカスアウトで実行する、(多くの Web アプリのように) ボタンクリックのタイミングで実行する、あるいはバリデーションエラーを受け取ったときは入力をキャンセルしてそもそも入力できない文字は入れられないようにする...などなど色んな実装方法が考えられます。皆さんなりのやり方で色々試してみるといいでしょう。

明日は...まだ埋まっていないな。みんな書いて書いて!

*1:Value extractor を実装することでカスタムのコンテナに対するバリデーションを行うことも可能になります

JavaFXアプリケーションでJava 9のモジュールを使うときの注意点

このエントリは JavaFX Advent Calendar 2017 の 1 日目のエントリです。最初を飾るのは初めてです。まだ参加者が少ないので、みんな参加してね!

いよいよJava SE 9がリリースされました。やはり9の注目はJigsawことモジュールシステムですね。特にJavaFXにとってはjavapackagerを用いてパッケージングした際の配布サイズを小さくすることができるので、特に重要ですね。

aoe-tk.hatenablog.com

ですが、JavaFXアプリケーションでモジュールシステムを利用すると早速引っ掛かるポイントがあります。それはFXMLです。

次のような FXML を使った簡単なアプリケーションを見てみましょう。

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

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>

<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="300.0" prefWidth="400.0" xmlns="http://javafx.com/javafx/9" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.Controller">
   <center>
      <StackPane prefHeight="150.0" prefWidth="200.0" BorderPane.alignment="CENTER">
         <children>
            <Label text="This is Label." fx:id="label" />
         </children>
      </StackPane>
   </center>
   <bottom>
      <HBox alignment="CENTER" BorderPane.alignment="CENTER">
         <children>
            <Button alignment="CENTER" mnemonicParsing="false" text="Button" onAction="#click" />
         </children>
         <BorderPane.margin>
            <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
         </BorderPane.margin>
      </HBox>
   </bottom>
</BorderPane>
package sample;

import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Label;

public class Controller {
    @FXML
    private Label label;

    @FXML
    void click(ActionEvent event) {
        label.setText("You clicked!");
    }
}

FXMLのいいところとして、ビューXMLとペアになるControllerクラスに対して、XML側に記述したコンポーネントへのアクセスをDIを使って提供している点ですね。publicなフィールドだけでなく、package privateやprivateなフィールドとしての宣言も可能で、その場合は @FXML アノテーションを付与すると、インスタンスがインジェクトされるようになります。イベント実行メソッドも同様に @FXML アノテーションでバインドできます。

ですが、この @FXML アノテーションを使う場合は注意が必要です。次のように module-info.java を作ってみましょう。

module fxml9sample {
    requires javafx.controls;
    requires javafx.fxml;
    exports sample;
}

すると次のような例外が飛びます。 javafx.fxml モジュールに属するクラスがこのControllerクラスに対してリフレクションによるアクセスを試みるためです。

Exception in Application start method
java.lang.reflect.InvocationTargetException
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
(中略)
Caused by: javafx.fxml.LoadException: 
fxml9sample/out/production/fxml9sample/sample/sample.fxml:15

    at javafx.fxml/javafx.fxml.FXMLLoader.constructLoadException(FXMLLoader.java:2625)
    at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2603)
(中略)
Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make field private javafx.scene.control.Label sample.Controller.label accessible: module fxml9sample does not "opens sample" to module javafx.fxml
    at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:337)
    at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:281)
    at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:176)
    at java.base/java.lang.reflect.Field.setAccessible(Field.java:170)
    at javafx.fxml/javafx.fxml.FXMLLoader$ControllerAccessor.addAccessibleFields(FXMLLoader.java:3495)
    at javafx.fxml/javafx.fxml.FXMLLoader$ControllerAccessor.access$3900(FXMLLoader.java:3344)
    at javafx.fxml/javafx.fxml.FXMLLoader$ControllerAccessor$1.run(FXMLLoader.java:3460)
    at javafx.fxml/javafx.fxml.FXMLLoader$ControllerAccessor$1.run(FXMLLoader.java:3456)
    at java.base/java.security.AccessController.doPrivileged(Native Method)
    at javafx.fxml/javafx.fxml.FXMLLoader$ControllerAccessor.addAccessibleMembers(FXMLLoader.java:3455)
    at javafx.fxml/javafx.fxml.FXMLLoader$ControllerAccessor.getControllerFields(FXMLLoader.java:3394)
    at javafx.fxml/javafx.fxml.FXMLLoader.injectFields(FXMLLoader.java:1170)
    at javafx.fxml/javafx.fxml.FXMLLoader.access$1600(FXMLLoader.java:105)
    at javafx.fxml/javafx.fxml.FXMLLoader$ValueElement.processValue(FXMLLoader.java:865)
    at javafx.fxml/javafx.fxml.FXMLLoader$ValueElement.processStartElement(FXMLLoader.java:759)
    at javafx.fxml/javafx.fxml.FXMLLoader.processStartElement(FXMLLoader.java:2722)
    at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2552)
    ... 17 more
Exception running application sample.Main

これを防ぐためには、FXMLと対になるControllerクラスが属しているパッケージに対して javafx.fxml モジュールからのリフレクションによるアクセスを許可する必要があります。そのために次のように opens 文を使って javafx.fxml モジュールに対してアクセスを許可する必要があります。

module fxml9sample {
    requires javafx.controls;
    requires javafx.fxml;
    exports sample;
    opens sample to javafx.fxml; // <- これを追加
}

既存のJavaFXアプリケーションをJava 9に移行する際に意外と引っ掛かるポイントなので注意してください。

というわけで、1 日目のエントリは小ネタでした。19 日にも書く予定ですが、そちらはもう少し大きなネタにする予定です。

明日は...執筆時点でまだいない! 誰か書いてー。