Java Day Tokyo 2014 開催のご案内

本日、Java Day Tokyo 2014 のイベントの登録が正式に開始いたしました。
昨年は、Java EE 7 のローンチの直前という事もあり、Java EE のお祭りでしたが、今年の、Java Day Tokyo は日本オラクルによる Java SE 8 のローンチ・イベントとしての位置づけておりまして、Java SE 8 に関する多数のセッションをご用意しています。
また本社からも多数のエンジニアを招いております。中でも注目なのは、Stuart Marks さんですが、彼は Java SE 8 の Lambda の実装に深く携わり、Lambda が導入されるまでの経緯から、実際の内部実装まで深くしる人間です。このビデオで一番右にいる方が Stuart Marks さんです。
また、一番右側に映っているのは JavaOne のチェアマンでもあり、Java エバンジェリストのStepen Chin さんです。彼もまた今年の Java Day Tokyo に参加してくださいます。
JavaOne Rock Star
http://www.oracle.com/javaone/rock-stars/index.html
本場 JavaOne ではすばらしい発表者に JavaOne RockStar という称号が与えられます。彼らは共に RockStar の称号を頂いており、すばらしい発表が期待できます。
JavaVM から Lambda, JavaFX, Nashorn まで Java SE 8 に関連したセッションを多数ご用意いたしました。もちろん Java EE 7 も去年の正式発表を元に、今年はより現実的でHTML 5 との組み合わせや将来的な内容も含まれるセッションをご用意いたしました。
是非、今年最大の Java イベントである Java Day Tokyo 2014 にお越しください。皆様のご参加を心より楽しみに致しております。
PS.
過去の経験上、人気セッションは早々に満席になる可能性もございます。是非、お早いうちに希望のセッションをご選択いただけますよう宜しくお願いします。
【祝】Java SE 8 正式リリース
2014 年 3 月 18 日正午 (日本時間 3 月 19 日:午前 4時) 、予定通り Java の開発者待望のJava SE 8 が正式にリリースされました。当日は、多くのメディアでも取り上げられた他、Twitter 上でも非常に多くのお祝いメッセージや期待するメッセージを見受けました。
Publickey :[速報]Java 8が正式公開。ラムダ式、新しい日時API、JavaFX8など。NetBeans 8.0も登場
日経 IT Pro : 2年8カ月ぶりの Javaの新版「Java SE 8」が公開
インターネットコム:Java 8 が一般公開、ラムダ式を採用
マイナビニュース:Java 8が正式リリース – ラムダ式採用、新型導入など大幅強化
インプレス:Oracle、約3年 ぶりとなる「Java SE」のメジャーバージョン「Java SE 8」を正式公開
また、2014 年 3 月 21 日 (金)日本 Java ユーザ・グループ主催の Java SE 8 正式リリースお祝いイベント「祝☆Java 8 Launch」が開催されました。
ここで、私は Java SE 8 がリリースされるまでの歴史や概要を紹介しました。
(お祝いの意味も込めて、今私が持っている Duke 画像を全て使いました。)
その他、5 月に開催する Java のイベントについても紹介しました。今年も JavaDay Tokyo 2014を開催致します。皆様どうぞ楽しみにしてください。
5 月の Java 祭り
5 月 18 日(日) JJUG CCC Spring 2014 場所:ベルサール西新宿
5 月 22 日(木) Java Day Tokyo 2014 場所:品川プリンスホテル
5 月 23 日(金) Java SE 8 & Raspberry Pi ハンズオン 場所:オラクル青山センター
5 月 24 日(土) Java Bike Ride (サイクリング)
最後に、私が本日使用したプレゼン資料の最後に Java SE 8 の主要な機能(Lambda, Date & Time API, JavaFX) 以外の細かな機能変更を 100 ページ程にまとめました。主要機能以外で Java SE 8 でどのような機能が加わったのかをご確認されたい方はご参照頂ければ幸いです。
※ この資料は本イベントでの発表にあわせて急ピッチで作成したため、Lambda, Date & Time API, JavaFX の説明は手を抜いています(主要機能は他の発表者が詳しく説明してくださるので)。また間違いが無いように気をつけたつもりですが、間違いが含まれているかもしれません。もし間違い等を見つけた方は是非、コメント欄にコメントをお寄せいただければ幸いです。
本参考資料が決して全ての変更点ではございません。実際に Java SE 8 で追加された クラスやメソッドの一覧は、高橋 徹さん が昨年末に執筆してくださった、「Java Advent Calendar 2013 11日目 – Java SE 8の新クラス・メソッド一覧」 をご参考にいただけますし、私が調べた所では、上記以外にもオリジナル・ドキュメント自身の since 1.8 の記載漏れで上記にリストされていないクラス等もあります。
ただ、この資料が Java SE 8 で追加された新機能の把握にお役立ち頂ければ幸いです。
Happy Java SE 8 Life !!
そして、JJUG CCC, JavaDay Tokyo でまた宜しくお願いします。
Java SE 8 に Lambda 式や Stream API を導入した背景から、どのように実装すべきかの説明資料。
Java SE 8 で Charset の改良
Java SE 8 の新機能、改善点の一つに Charset の実装の改善があります。
これは charset のサイズを小さくし、エンコード、デコードのパフォーマンスを改善する事を目的としています。
実際、Java SE 7 の charset と Java SE 8 のサイズを比較してみると、下記コマンド実行例のように約 3.7 M ほどあったファイルが、3.1 M までスリム化しています。
追記 (2014 年 3 月 18 日):
山中 淳彦さん から本件に関して追加情報を頂きましたので、その内容を下記に共有いたします。
以降、山中 淳彦さんから頂いた情報:
横からすいません。私も興味があったのでチラッと調べてみました。言及されてる変更はJEP 112: Charset Implementation Improvements[1] で記述されてるもので、具体的な issue としては、JDK-6653797[2]、JDK-7183053[3]が挙げられてますね。
前者、
By re-implemen/organize JDK’s charset implementation package
sun.nio.cs to achive the following goals
(1) decrease the size of the charsets.jar by 50% (from current 3.4M to below 1.7M).
(2) ease the maintenance cost (most charset implementation to be generated during
JDK build time from text based mapping tables)
(3) improve the performance (speed up the nio encoding/decoding performance)
(4) replace the current sun.io.* converters with an “adaptor” to re-direct the
sun.io encoding/decoding to nio.charset implementation t finally remove the
burden of maintain sun.io.
によって、十年来の目標だった JDK-4948149[4] での、sun.io.* の機能の sun.nio.cs.* への移管が達成され、やっと sun.io.* が削除されたということだと思われます。
後者は、“Faster new String(bytes, cs/csn) and String.getBytes(cs/csn)”[5]
によると、Java7 で single-byte の new String(bytes, cs/csn) のパフォーマンスが改善されたので、Java8 では、同様の hack が UTF-8 charset 文字にも施され、single byte、multi-byte 文字全てで、new String(bytes, cs/csn)、String.getBytes(cs/csn) のパフォーマンスが改善されたという話のようです。
[1] http://openjdk.java.net/jeps/112
[2] https://bugs.openjdk.java.net/browse/JDK-6653797
[3] https://bugs.openjdk.java.net/browse/JDK-7183053
[4] https://bugs.openjdk.java.net/browse/JDK-4948149
[5] https://blogs.oracle.com/xuemingshen/entry/faster_new_string_bytes_cs
追記2
JDK-7183053に関する変更[1]で、test/sun/nio/cs/StrCodingBenchmarkDB.java[2] が追加されてますが、codereview request for 7183053[3] でその効果が紹介されてました。
The results of the “non -scientific” benchmark StrCodingBenchmarkDB
running on client
and server vm on my linux machine are included in docs_c (client) and
docs_s(server)
below.
http://cr.openjdk.java.net/~sherman/7183053/dbcs_c
http://cr.openjdk.java.net/~sherman/7183053/dbcs_s
よくわかってませんが、確かに速くなってるようです。
[1] http://hg.openjdk.java.net/hsx/hotspot-comp/jdk/rev/c76ad79a5a2f
[2] http://cr.openjdk.java.net/~sherman/dbcs_array/webrev/test/sun/nio/cs/StrCodingBenchmarkDB.java.html
[3] http://mail.openjdk.java.net/pipermail/core-libs-dev/2012-July/010806.html
山中 淳彦さんから頂いた情報はここまで……
> ls -l /Library/Java/JavaVirtualMachines/jdk1.7.0_51.jdk/Contents/Home/jre/lib/charsets.jar -rw-rw-r– 1 root wheel 3696271 12 19 12:13 /Library/Java/JavaVirtualMachines/jdk1.7.0_51.jdk/Contents/Home/jre/lib/charsets.jar > ls -l /Library/Java/JavaVirtualMachines/jdk1.8.0.jdk//Contents/Home/jre/lib/charsets.jar |
実際に jar を展開して比較した所、sun.io.* が全て削除されている事が分かりました。また、その他にも日本語に関連した部分も含め改良が加わっている事が分かりました。比較の内容の詳細を下記にアップしましたのでどうぞご参照ください。
jBatch(JSR-352) on Java SE 環境
先日のデブサミの発表後、jBatch (JSR-352) についてご質問を頂き、また別件でも同じ質問を頂きましたので、その内容を共有致します。
jBatch を cron 等で実行したいのだが、jBatch は Java EE 環境でしか実行できないのか?とのご質問を頂きました。
答えは、jBatch の仕様上、Java SE 環境上でも動作するように実装されております。
ただし、Java EE 環境上で実装する方がとても簡単に実装・運用ができますので個人的にはJava EE 環境上での動作をお薦めします。仮に Java SE 環境上で jBatch (jBatch の RI)を実行したい場合は下記をご参照ください。
1. 準備
Java SE 環境上で jBatch を稼働させるためには、JavaDB(Derby) が必要です。
また、jBatch の RI を使って Java SE 環境上で動作させるためには、
jBatch RI の実行に必要なライブラリ一式を下記より入手します。
https://java.net/projects/jbatch/downloads/download/jsr352-SE-RI-1.0.zip
zip を展開すると下記のファイルが含まれています。下記全ファイルを lib 配下にコピーしてください。
- derby.jar
- javax.inject.jar
- jsr352-SE-RI-javadoc.jar
- javax.batch.api.jar
- jsr352-RI-spi.jar
- jsr352-SE-RI-runtime.jar
2. Batch コンテナを実行するための設定
次に Batch コンテナを稼働させるためのプロパティの設定を行います。
META-INF ディレクトリ配下に services ディレクトリを作成して
それぞれ下記のファイルを作成してください。
src/META-INF/services/batch-config.properties
JDBC_DRIVER=org.apache.derby.jdbc.EmbeddedDriver # JDBC_URL=jdbc:derby://localhost:1527/batchdb;create=true JDBC_URL=jdbc:derby://localhost:1527/batchdb
src/META-INF/services/batch-services.properties
J2SE_MODE=true
以上で基本的には Java SE 環境上で動作させるために必要な設定は完了です。
3. 動作確認
それでは、バッチであるファイルの内容を別のファイルに書き出すサンプルを作成します。(※ 以降は Java EE 環境での実装と同じです。)
下記に、本 jBatch プロジェクトのディレクトリ構成を下記に示します。

まず、メイン・メソッドから Batch の JOB: “my-batch-job” を起動します。
package com.yoshio3.main;
import java.util.Properties;
import javax.batch.operations.JobOperator;
import javax.batch.runtime.BatchRuntime;
/**
*
* @author Yoshio Terada
*/
public class StandAloneBatchMain {
public static void main(String... args) {
JobOperator job = BatchRuntime.getJobOperator();
long id = job.start("my-batch-job", new Properties());
}
}
この、”my-batch-job” の処理内容は、META-INF/batch-jobs ディレクトリ配下に、”my-batch-job.xml” として定義します。
“my-batch-job” の内容を下記に示します。プロパティを2つ input_file,output_file 定義し、それぞれ /tmp/input.txt, /tmp/output.txt を示します。また、JOB の step としてチャンク形式の step を1つ定義し、データの読み込み用(reader)、処理用(processor)、書き込み(writer)用の処理を、それぞれ、MyItemReader, MyItemProcessor, MyItemWriter に実装します。
<job id="my-batch-job"
xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="1.0">
<properties>
<property name="input_file" value="/tmp/input.txt"/>
<property name="output_file" value="/tmp/output.txt"/>
</properties>
<step id="first-step">
<chunk item-count="5">
<reader ref="com.yoshio3.chunks.MyItemReader"/>
<processor ref="com.yoshio3.chunks.MyItemProcessor"/>
<writer ref="com.yoshio3.chunks.MyItemWriter"/>
</chunk>
</step>
</job>
読み込み用の処理は、ItemReader を実装したクラスを作成します。ここでは、input_file で指定されたプロパティのファイル(/tmp/input.txt)ファイルを読み込み、1行読み込んでその値を返します。
package com.yoshio3.chunks;
import java.io.BufferedReader;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import javax.batch.api.chunk.ItemReader;
import javax.batch.runtime.context.JobContext;
import javax.inject.Inject;
public class MyItemReader implements ItemReader {
@Inject
JobContext jobCtx;
BufferedReader bufReader;
@Override
public void open(Serializable checkpoint) throws Exception {
String fileName = jobCtx.getProperties()
.getProperty("input_file");
bufReader = Files.newBufferedReader(Paths.get(fileName),Charset.forName("UTF-8"));
}
@Override
public void close() throws Exception {
bufReader.close();
}
@Override
public Object readItem() throws Exception {
String data = bufReader.readLine();
System.out.println("Reader readItem : " + data);
return data;
}
@Override
public Serializable checkpointInfo() throws Exception {
return null;
}
}
次に、読み込んだデータの加工処理部分は、ItemProsessor を実装したクラスに記述します。ここでは、読み込んだデータ(文字列)に対して、文字列を付加して返しています。
package com.yoshio3.chunks;
import javax.batch.api.chunk.ItemProcessor;
/**
*
* @author Yoshio Terada
*/
public class MyItemProcessor implements ItemProcessor {
@Override
public Object processItem(Object item) throws Exception {
String line = (String)item ;
StringBuilder sBuilder = new StringBuilder();
sBuilder.append("Processor processItem : ");
sBuilder.append(line);
String returnValue = sBuilder.toString();
System.out.println(returnValue);
return returnValue;
}
}
最後に書き出し部分を ItemWriter を実装したクラスに記述します。ここでは、oputput_file のプロパティを取得して書き出すファイル名を取得しています。次にファイルに対して取得したデータを書き出しています。
package com.yoshio3.chunks;
import java.io.BufferedWriter;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import javax.batch.api.chunk.ItemWriter;
import javax.batch.runtime.context.JobContext;
import javax.inject.Inject;
/**
*
* @author Yoshio Terada
*/
public class MyItemWriter implements ItemWriter {
@Inject
JobContext jobCtx;
String fileName;
BufferedWriter bufWriter;
@Override
public void open(Serializable checkpoint) throws Exception {
fileName = jobCtx.getProperties()
.getProperty("output_file");
bufWriter = Files.newBufferedWriter(Paths.get(fileName), Charset.forName("UTF-8"));
}
@Override
public void close() throws Exception {
bufWriter.close();
}
@Override
public void writeItems(List<Object> items) throws Exception {
for (Object obj : items) {
String data = (String) obj;
System.out.println("Writer writeItems : " + data);
bufWriter.write(data);
bufWriter.newLine();
}
}
@Override
public Serializable checkpointInfo() throws Exception {
return null;
}
}
上記を実装した後、コンパイルをしてください。
# java -classpath
lib/jsr352-ri-1.0/javax.inject.jar:
lib/jsr352-ri-1.0/derby.jar:
lib/jsr352-ri-1.0/jsr352-RI-spi.jar:
lib/jsr352-ri-1.0/javax.batch.api.jar:
lib/jsr352-ri-1.0/jsr352-SE-RI-javadoc.jar:
lib/jsr352-ri-1.0/jsr352-SE-RI-runtime.jar:
build/classes com.yoshio3.main.StandAloneBatchMain
実行すると下記のようなログを確認できます。chunk 形式ではデフォルトで 10 件まとめて読み込み&処理を実施し、
まとめて 10 件書き込むという動作を下記からも確認できるかと思います。
2 18, 2014 12:10:50 午後 com.ibm.jbatch.container.services.impl.JDBCPersistenceManagerImpl createSchema 情報: JBATCH schema does not exists. Trying to create it. 2 18, 2014 12:10:50 午後 com.ibm.jbatch.container.services.impl.JDBCPersistenceManagerImpl createIfNotExists 情報: CHECKPOINTDATA table does not exists. Trying to create it. 2 18, 2014 12:10:50 午後 com.ibm.jbatch.container.services.impl.JDBCPersistenceManagerImpl createIfNotExists 情報: JOBINSTANCEDATA table does not exists. Trying to create it. 2 18, 2014 12:10:50 午後 com.ibm.jbatch.container.services.impl.JDBCPersistenceManagerImpl createIfNotExists 情報: EXECUTIONINSTANCEDATA table does not exists. Trying to create it. 2 18, 2014 12:10:50 午後 com.ibm.jbatch.container.services.impl.JDBCPersistenceManagerImpl createIfNotExists 情報: STEPEXECUTIONINSTANCEDATA table does not exists. Trying to create it. 2 18, 2014 12:10:50 午後 com.ibm.jbatch.container.services.impl.JDBCPersistenceManagerImpl createIfNotExists 情報: JOBSTATUS table does not exists. Trying to create it. 2 18, 2014 12:10:50 午後 com.ibm.jbatch.container.services.impl.JDBCPersistenceManagerImpl createIfNotExists 情報: STEPSTATUS table does not exists. Trying to create it. Reader readItem : hogehoge1 Processor processItem : hogehoge1 Reader readItem : hogehoge2 Processor processItem : hogehoge2 Reader readItem : hogehoge3 Processor processItem : hogehoge3 Reader readItem : hogehoge4 Processor processItem : hogehoge4 Reader readItem : hogehoge5 Processor processItem : hogehoge5 Reader readItem : hogehoge6 Processor processItem : hogehoge6 Reader readItem : hogehoge7 Processor processItem : hogehoge7 Reader readItem : hogehoge8 Processor processItem : hogehoge8 Reader readItem : hogehoge9 Processor processItem : hogehoge9 Reader readItem : hogehoge10 Processor processItem : hogehoge10 Writer writeItems : Processor processItem : hogehoge1 Writer writeItems : Processor processItem : hogehoge2 Writer writeItems : Processor processItem : hogehoge3 Writer writeItems : Processor processItem : hogehoge4 Writer writeItems : Processor processItem : hogehoge5 Writer writeItems : Processor processItem : hogehoge6 Writer writeItems : Processor processItem : hogehoge7 Writer writeItems : Processor processItem : hogehoge8 Writer writeItems : Processor processItem : hogehoge9 Writer writeItems : Processor processItem : hogehoge10 Reader readItem : hogehoge11 Processor processItem : hogehoge11 Reader readItem : hogehoge12 Processor processItem : hogehoge12 Reader readItem : hogehoge13 Processor processItem : hogehoge13 Reader readItem : hogehoge14 Processor processItem : hogehoge14 Reader readItem : hogehoge15 Processor processItem : hogehoge15 Reader readItem : hogehoge16 Processor processItem : hogehoge16 Reader readItem : hogehoge17 Processor processItem : hogehoge17 Reader readItem : hogehoge18 Processor processItem : hogehoge18 Reader readItem : hogehoge19 Processor processItem : hogehoge19 Reader readItem : hogehoge20 Processor processItem : hogehoge20 Writer writeItems : Processor processItem : hogehoge11 Writer writeItems : Processor processItem : hogehoge12 Writer writeItems : Processor processItem : hogehoge13 Writer writeItems : Processor processItem : hogehoge14 Writer writeItems : Processor processItem : hogehoge15 Writer writeItems : Processor processItem : hogehoge16 Writer writeItems : Processor processItem : hogehoge17 Writer writeItems : Processor processItem : hogehoge18 Writer writeItems : Processor processItem : hogehoge19 Writer writeItems : Processor processItem : hogehoge20 Reader readItem : null
書き込む間隔を変更したい場合は、Job XML の設定を変更し<chunk item-count=”5″>を設定します。
<job id="my-batch-job"
xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="1.0">
<properties>
<property name="input_file" value="/tmp/input.txt"/>
<property name="output_file" value="/tmp/output.txt"/>
</properties>
<step id="first-step">
<chunk item-count="5">
<reader ref="com.yoshio3.chunks.MyItemReader"/>
<processor ref="com.yoshio3.chunks.MyItemProcessor"/>
<writer ref="com.yoshio3.chunks.MyItemWriter"/>
</chunk>
</step>
</job>
<chunk item-count=”5″>を設定した後、実行すると下記のような結果が得られます。
run: Reader readItem : hogehoge1 Processor processItem : hogehoge1 Reader readItem : hogehoge2 Processor processItem : hogehoge2 Reader readItem : hogehoge3 Processor processItem : hogehoge3 Reader readItem : hogehoge4 Processor processItem : hogehoge4 Reader readItem : hogehoge5 Processor processItem : hogehoge5 Writer writeItems : Processor processItem : hogehoge1 Writer writeItems : Processor processItem : hogehoge2 Writer writeItems : Processor processItem : hogehoge3 Writer writeItems : Processor processItem : hogehoge4 Writer writeItems : Processor processItem : hogehoge5 Reader readItem : hogehoge6 Processor processItem : hogehoge6 Reader readItem : hogehoge7 Processor processItem : hogehoge7 Reader readItem : hogehoge8 Processor processItem : hogehoge8 Reader readItem : hogehoge9 Processor processItem : hogehoge9 Reader readItem : hogehoge10 Processor processItem : hogehoge10 Writer writeItems : Processor processItem : hogehoge6 Writer writeItems : Processor processItem : hogehoge7 Writer writeItems : Processor processItem : hogehoge8 Writer writeItems : Processor processItem : hogehoge9 Writer writeItems : Processor processItem : hogehoge10 Reader readItem : hogehoge11 Processor processItem : hogehoge11 Reader readItem : hogehoge12 Processor processItem : hogehoge12 Reader readItem : hogehoge13 Processor processItem : hogehoge13 Reader readItem : hogehoge14 Processor processItem : hogehoge14 Reader readItem : hogehoge15 Processor processItem : hogehoge15 Writer writeItems : Processor processItem : hogehoge11 Writer writeItems : Processor processItem : hogehoge12 Writer writeItems : Processor processItem : hogehoge13 Writer writeItems : Processor processItem : hogehoge14 Writer writeItems : Processor processItem : hogehoge15 Reader readItem : hogehoge16 Processor processItem : hogehoge16 Reader readItem : hogehoge17 Processor processItem : hogehoge17 Reader readItem : hogehoge18 Processor processItem : hogehoge18 Reader readItem : hogehoge19 Processor processItem : hogehoge19 Reader readItem : hogehoge20 Processor processItem : hogehoge20 Writer writeItems : Processor processItem : hogehoge16 Writer writeItems : Processor processItem : hogehoge17 Writer writeItems : Processor processItem : hogehoge18 Writer writeItems : Processor processItem : hogehoge19 Writer writeItems : Processor processItem : hogehoge20 Reader readItem : null
以上のように、Java SE の環境でも jBatch (JSR-352) を実行する事ができます。今回は参考のため jBatch の RI を使用しましたが、各 Java EE 7 準拠のアプリケーション・サーバで必要なライブラリはそれぞれ異なるかと想定します。必要なライブラリは各アプリケーション・サーバでお調べください。
Java EE 7 の新機能紹介と Java のイベントのご紹介
2014年2月13日(木)・14日(金)に目黒雅叙園でDevelopers Summit 2014 が開催されました。
今日は関東はあいにくの天気(大雪)で、とても足元が悪い中多くの方にイベントやセッションに参加して頂きまして誠にありがとうございました。また、Developers Summit 2014の事務局の皆様に置かれましても、同様に(Javaの)大規模イベントの企画を行っている者として、数多くの難作業があった事が用意に想像ができるため、イベントの主催者の皆様全員にあつく御礼を申し上げます。
私自身は、本日 14日(金)に【14-D-6】45 new features of Java EE 7 in 45 minutes というセッションを持たせて頂き発表を行いました。私のセッションにも非常に多くの開発者の皆様にご参加いただき良いフィードバックや、難しすぎたといったフィードバックなども頂きそうしたご意見を次回につなげていきたいと思います。本日発表に使用した資料を下記に公開します。実際にプレゼン時にはアニメーション等も使用しているため、下記の静的なコンテンツと内容は若干変わりますがご参考頂ければ誠に幸いです。
今回の個人的な所管として、45 分で 45 個 (総ページ数 P 90) の Java EE 7 の新機能をご紹介する事は初めてのチャレンジだったため、時間内に終わるか発表前から心配しておりましたが、結果、なんとかそして大幅な時間超過なく終える事ができました (最後はかなり足早になってしまい申し訳ありません)。本日、ご紹介した内容を本来であれば1件づつブログなどの記事にまとめられれば良いのですが、私自身次のステップに足を向けなればならないため、直近でブログに書く事は困難かもしれません。(もちろん時間ができれば書きますが。)どうぞご理解頂ければ幸いです。
さて、その次のステップでございますが、今日のセッションの最後に申し上げましたが、5 月に今年最大の Java のお祭りイベントを企画しております。そしてこれからその準備に多くの時間を割くようになります。現時点で詳細を申し上げる事はできませんが、5月18日〜5月24日まで、Java の開発者の皆様は極力日程を空けておいていただけないでしょうか。正式な情報が決まり次第、JJUG / 日本オラクルから正式アナウンスがあるかと想定します。
2/17 追記:JJUG のイベントとオラクルのイベントは、別イベントとして、別日程で開催されます。また、JJUG のイベント JJUG CCC につきましては、Call for Papers の募集も既に開始しておりますので日程等の詳細は JJUG のサイトをご覧ください。
日本オラクルからの正式なアナウンスまでは是非お待ちいただきたいのですが、今から 5 月の出張申請、もしくは上長への申請を上げておいていただければ誠に幸いです。
今年のイベントは、Java SE 8 の正式リリース後という事もあり、昨年以上の盛り上がりをご期待頂けるかと思いますので、どうぞお楽しみにしてください。
「5月末 Java のお祭りをするどぉーーーー!!!」
CDI 1.1 : @Vetoed アノテーション of 1/45
昨日、「Java EE 7 の新機能を 45 分で 45 個ご紹介」のエントリを書きましたが、具体的にどのような内容なのか?ご興味ある方もいらっしゃるかと思いますので、セッション内容の一例をご紹介します。
CDI 1.1 : @Vetoed アノテーション
CDI 1.1 から特定のクラスだけをインジェクション対象からはずし、インジェクションできないようにする事ができるようになりました。インジェクション対象からはずすためには、該当のクラスに対して、@Vetoed アノーテションを付加します。
ご参考:CDI 1.0 までは XML で除外対象の設定を行ってました。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee">
<scan>
<exclude name="com.acme.rest.*" />
<exclude name="com.acme.faces.**">
<if-class-not-available name="javax.faces.context.FacesContext"/>
</exclude>
</scan>
</beans>
@Vetoed アノテーションの利用例として、JPA のエンティティ・クラスに対して付加する事が有効です。このアノテーションを JPA のエンティティに付加する事で、CDI のライフサイクルと JPA のライフサイクルの競合を防ぎより安全に実装することができます。下記の例では JPA のエンティティを CDI では管理できないようにし JPA の EntitiManager によってのみ管理する例を示します。
@Entity
@Vetoed
public class Person {
@Column
private String fName;
@Column
private String lName;
}
もしくは、package-info.java を作成し、該当パッケージに含まれるクラス全てをインジェクション対象から外す事も可能です。
@Vetoed package com.yoshio3.noninjectable ; import javax.enterprise.inject.Vetoed ;
このように、本セッションは Java EE 7 で追加された新機能をいち早くご理解されたい方に有用なセッションです。前提としてコンフィグレーションの内容やソースコードを交えての説明ですので Java のコードが理解できる方の参加が必須で、金魚本や過去のセミナーなどから Java EE 5/6 を触った事のある方ならばより理解がしやすいかと想定します。
Java EE 7 の新機能を 45 分で 45 個ご紹介
皆様、いつも大変お世話になっております。
この度、Developers Summit 2014 (通称:デブサミ) でJava EE 7 の新機能をご紹介するセッションを持たせていただき、登壇する事になりましたのでご案内致します。
2014/2/14(金) 16:20~17:05
「45 new features of Java EE 7 in 45 minutes」
Java EE 7 は2013年6月に正式にリリースされ、2014年以降Java EE 7 に準拠したアプリケーションが続々と登場する事が予想されます。Java EE 7 は、WebSocket,JSON, Batch, Concurrencyの新機能が追 加された他、既存のAPIにも様々な改良が施されています。エンタープライズJavaの今後、とても重要になるJava EE 7の新機能を45分で45 の新機能を紹介します。
※ このセッションは、本場 JavaOne San Francisco 2013 で人気があったセッションを日本で初めて実現する物で、コンテンツは今回、日本の開発者の皆様向けに作り直して提供予定です。また、Java EE 5,6 についてある程度勉強して頂いている方が、Java EE 7 への差分をまとめて理解をする事ができるセッションです。ソースコードを交えながらご紹介いたしますので、是非ご都合のつく方はお越しください。
Java EE 7 WebSocket Mail の実装
本エントリは、Java Advent Calendar 2013 の 20日目のエントリです。
昨日、19日目は 23 才のお誕生日を迎えた、 ひらおかゆみさん (id:yumix_h) の
「JavaMailを手軽に使うライブラリ」でした。 yumix_h さんお誕生日おめでとうございます!!
明日は、@nagaseyasuhito さんです。
本エントリは、1つ前のエントリで投稿した「JSF + WebSocket で実装した IMAP Web メール・クライアント」の続きのエントリで、WebSocket の実装部についてご紹介します。
ただし、残念ながら今回はアンチパターンとしてのご紹介になります。
ポイントは、WebSocket のサーバ・エンドポイントの実装で別スレッドを立てて監視などを行ってはいけないという点です。
追記:2014 年 5 月 9 日 : Java Mail 1.5.2 のリリースによりアンチパターンではなくなりました。詳細は本エントリ最下部をご参照
WebSocket はリアルタイム通知を行うために多くの開発者に興味を持たれています。実際、このデモでは、IMPAの受信箱(INBOX)に入ってきたメッセージをユーザにいち早く、リアルタイムでお知らせするために、WebSocket を使用して通知しています。WebSocket を使う事でこのようなアプリケーションを作成する事もできます。
実際には、下記のように実装しています。まず、クライアント側の View の実装ですが、JSF の XHTML と JavaScript でそれぞれ実装します。
JSF のページ内で <h:outputScript> を指定し WebSocket のクライアント・エンドポイントの実装 JavaScript(ws-client-endpoint.js) を読み込んでいます。次に、PrimeFaces で用意されている、「Notification Bar」を使用します。「Notification Bar」は、下記 URL のサンプルでご参照頂ける通り、動的にパネルを表示させる事ができる、JSF のコンポーネントです。
http://www.primefaces.org/showcase/ui/notificationBar.jsf
このコンポーネントを使用して、WebSocket のサーバ・エンドポイントからメッセージを受信した際に、 JavaScript から PF(‘bar’).show() を実行してこのコンポーネントを表示させます。一方で、このコンポーネントを非表示にするためには、JavaScript で PF(‘bar’).hide()を呼び出す事で非表示にできます。
実際に、WebSocket のサーバに接続するためには、「リアルタイムチェック開始」ボタンを押下して接続します。「リアルタイムチェック開始」ボタンを押下すると JavaScript の connectServerEndpoint() が呼び出され、WebSocket のサーバ・エンドポイントに接続します。
<h:head>
<title>JSF-WebSocket WebMail</title>
<f:event type="preRenderView" listener="#{messageReceiveMgdBean.onPageLoad}"/>
<h:outputScript library="javascripts" name="ws-client-endpoint.js"/>
</h:head>
<h:body>
<h:form id="form">
<p:notificationBar position="top" effect="slide" widgetVar="bar" styleClass="top" style="background-color : #F8F8FF ; width: fit-content;">
<h:panelGrid columns="2" columnClasses="column" cellpadding="0">
<h:outputText value="新着メッセージ :" style="color: red;font-size:12px;" /> <p:commandButton value="閉じる" onclick="PF('bar').hide()" type="button" style="font-size:10px;"/>
<h:outputText value="Subject :" style="font-size:10px;" /><h:outputText id="wssubject" value="" style="font-size:10px;" />
<h:outputText value="From :" style="font-size:10px;" /><h:outputText id="wsfrom" value="" style="font-size:10px;" />
<h:outputText value="メッセージ・サマリー :" style="font-size:10px;" /><h:outputText id="wssummary" value="" style="font-size:10px;" />
</h:panelGrid>
</p:notificationBar>
…中略
<input id="connect" type="button" value="リアルタイムチェック開始" style="font-size:10px;" onClick="connectServerEndpoint();"/>
<input id="close" type="button" value="リアルタイムチェック中止" style="font-size:10px;" onClick="closeServerEndpoint();"/>
JavaScript の実装は下記の通りです。まず、ページのロード時には、「リアルタイムチェック中止」のボタンを非表示にし「リアルタイムチェック開始」ボタンのみ表示させています。ボタンを押下すると「connectServerEndpoint()」を呼び出しますが、ここでは、WebSocket のサーバ・エンドポイントに接続し、「リアルタイムチェック開始」、「リアルタイムチェック中止」ボタンそれぞれの表示、非表示を切り替えます。
次に、サーバ・エンドポイントからメッセージを受信した場合、onMessage() 経由で writeToScreen() を呼び出しJSON のデータを展開した後、JSF のコンポーネントに対して値を代入し、 PF(‘bar’).show()で通知バー(<p:notificationBar>)を表示させます。
var websocket = null;
function init() {
document.getElementById("close").style.display = "none";
}
function closeServerEndpoint() {
websocket.close(4001, "Close connection from client");
document.getElementById("connect").style.display = "block";
document.getElementById("close").style.display = "none";
}
function connectServerEndpoint() {
var wsUri = "ws://localhost:8080/JSF-WebSocket-Mailer/inbox-check";
if ("WebSocket" in window) {
websocket = new WebSocket(wsUri);
} else if ("MozWebSocket" in window) {
websocket = new MozWebSocket(wsUri);
}
websocket.onopen = function(evt) {
onOpen(evt);
};
websocket.onmessage = function(evt) {
onMessage(evt);
};
websocket.onerror = function(evt) {
onError(evt);
};
websocket.onclose = function(evt) {
closeServerEndpoint();
};
document.getElementById("connect").style.display = "none";
document.getElementById("close").style.display = "block";
}
function onOpen(evt) {
;
}
function onMessage(evt) {
writeToScreen(evt.data);
}
function onError(evt) {
writeToScreen("ERROR: " + evt.data);
}
function writeToScreen(messages) {
if (window.JSON)
{
var obj = JSON.parse(messages);
var subject = obj.subject;
var from = obj.address;
var summary = obj.summary;
document.getElementById('form:wssubject').innerHTML = subject;
document.getElementById('form:wsfrom').innerHTML = from;
document.getElementById('form:wssummary').innerHTML = summary;
}
PF('bar').show();
}
window.addEventListener("load", init, false);
次に、サーバ・エンドポイント側の実装ですが、サーバ側の実装は下記の3クラスから構成されています。
InboxCheck : WebSocket のサーバ・エンドポイントの実装
MessageEncoder : MessageからJSONを生成するエンコーダ
InboxCheckRunnableTask : INBOX を監視する並列処理用タスク
まず、InboxCheck ですが、このクラスが WebSocket のサーバ・エンドポイントの重要なクラスです。このクラスでは、クライアント・エンドポイントから接続された際に、IMAP サーバへ接続するためのユーザ名、パスワードを受け取り、IMAP サーバへ接続を行っています。
※ 今回、JSF のログインページで入力された、ユーザ名、パスワードをWebSocket 側でも扱えるように Session Scope に値を代入し、取り出していますが、本来 Session Scope に代入すべきではないので、別の方法 (Flash等でできれば) でユーザ名、パスワードを渡す方法を検討すべきです。今回は簡単のため、Session で扱わせて頂きました。
IMAPサーバへ接続した後、checkNewMessage() で新着メッセージを監視します。
追記:checkNewMessage() メソッドの実装は、下記の実装ではなく、Java Mail 1.5.2 でリリースされたIdleManagerクラスを使用して実装してください。詳細は本エントリの最下部に記載しています。
具体的には、下記の実装箇所で、IMAP の INBOX Folder に対して、メッセージが追加された際の処理を実装しています。
folder.addMessageCountListener(new MessageCountAdapter() {});
この内部実装では、メッセージが追加(新着メッセージが来た)された際に、最新のメッセージを取得し、その情報を WebSocket のクライアント・エンドポイントに対して配信しています。
次に、実際にメッセージが追加された事を検知するための実装を行います。これは、IMAP の IDLE (RFC 2177) 機能を使って、フォルダの変更をリアルタイムで検知します。このリアルタイム監視は、InboxCheckRunnableTask で実装し、別のスレッドで監視を行うようにします。
package jp.co.oracle.samples.websockets;
import java.io.IOException;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Resource;
import javax.annotation.security.RolesAllowed;
import javax.enterprise.concurrent.ManagedThreadFactory;
import javax.inject.Inject;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Store;
import javax.mail.event.MessageCountAdapter;
import javax.mail.event.MessageCountEvent;
import javax.websocket.EncodeException;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnOpen;
import javax.websocket.server.ServerEndpoint;
import jp.co.oracle.samples.mgdBean.IndexLoginMgdBean;
import jp.co.oracle.samples.tasks.InboxCheckRunnableTask;
/**
*
* @author Yoshio Terada
*/
@ServerEndpoint(value = "/inbox-check",
encoders = {MessageEncoder.class})
public class InboxCheck {
private Store store;
private static final Logger logger = Logger.getLogger(InboxCheck.class.getPackage().getName());
@Resource
ManagedThreadFactory threadFactory;
InboxCheckRunnableTask invokeCheck = null;
@Inject
IndexLoginMgdBean login;
@OnOpen
public void onOpen(javax.websocket.Session session) {
try {
initStore(login.getImapServer(), login.getUsername(), login.getPassword());
checkNewMessage(session);
} catch (MessagingException mes) {
try {
logger.log(Level.SEVERE, "Exception occured on monitoring INBOX ", mes);
session.close();
} catch (IOException ex) {
logger.log(Level.SEVERE, "Failed to close session", ex);
}
}
}
@OnClose
public void onClose(javax.websocket.Session session) {
if (invokeCheck != null) {
invokeCheck.terminateRealTimeCheck();
}
}
@OnError
public void onError(Throwable t) {
logger.log(Level.SEVERE, "Error Occured", t);
}
/* 本エントリの最下部に説明した IdleManager クラスを使用して実装してください
private void checkNewMessage(final javax.websocket.Session session) throws MessagingException {
// INBOX のフォルダを対象
Folder folder = store.getFolder("INBOX");
if (!folder.isOpen()) {
folder.open(javax.mail.Folder.READ_WRITE);
}
// フォルダのメッセージ・カウント数を監視
folder.addMessageCountListener(new MessageCountAdapter() {
@Override
public void messagesAdded(MessageCountEvent e) {
Message[] msgs = e.getMessages();
Message msg = msgs[msgs.length - 1];
try {
// WebSocket のクライアント・エンドポイントに送信
session.getBasicRemote().sendObject(msg);
} catch (IOException | EncodeException ioencx) {
logger.log(Level.SEVERE, "Failed to Send Message ", ioencx);
}
}
});
// 別スレッドでメッセージの到着を監視
newInboxCheckThreadWithRetryCount(folder);
}
private void newInboxCheckThreadWithRetryCount(Folder folder) {
invokeCheck = new InboxCheckRunnableTask(folder);
Thread runTask = threadFactory.newThread(invokeCheck);
runTask.start();
}
*/
// Store の初期化(ページのロード時)
private void initStore(String imapServer, String username, String password) throws MessagingException {
Properties props = System.getProperties();
props.setProperty("mail.store.protocol", "imaps");
Session session = Session.getDefaultInstance(props, null);
javax.mail.Store initStore = session.getStore("imaps");
initStore.connect(imapServer, username, password);
this.store = initStore;
}
}
InboxCheckRunnableTask では、IDLE 機能が有効か無効かをチェックし、IDLE 機能が有効な場合、idle()で新着メッセージの到着を待ち受けます。idle() はメッセージが1通到着すると処理が終了しますので、スレッドの終了メソッドが呼び出されるか、何らかの例外が発生するまで、無限ループで繰り返し呼び出します。
一方で、IDLE 機能が無効なサーバに接続する場合は(Yahooなど)、idle() で待ち受けできませんので、自身で定期的にポーリングを行っています。
package jp.co.oracle.samples.tasks;
import com.sun.mail.imap.IMAPFolder;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.mail.Folder;
import javax.mail.MessagingException;
/**
*
* @author Yoshio Terada
*/
/* 本エントリの最後に記載した IdleManager で実装してください。
public class InboxCheckRunnableTask implements Runnable {
private final static int MAIL_CHECK_IDLE_TIME = 20000;
private Folder folder;
private static final Logger logger = Logger.getLogger(InboxCheckRunnableTask.class.getPackage().getName());
volatile boolean isRunnable = true;
public InboxCheckRunnableTask(Folder folder) {
this.folder = folder;
}
private void executeCheckForIdleDisable() throws InterruptedException, MessagingException {
Thread.sleep(MAIL_CHECK_IDLE_TIME);
int count = folder.getMessageCount();
}
public void terminateRealTimeCheck() {
isRunnable = false;
}
@Override
public void run() {
boolean idleIsAvailable = true;
while (isRunnable) {
// IMAPFolder のインスタンスで isIdleEnable が true の時実行
if (folder instanceof IMAPFolder) {
IMAPFolder ifolder = (IMAPFolder) folder;
if (idleIsAvailable) {
try {
ifolder.idle();
} catch (javax.mail.FolderClosedException fce) {
logger.log(Level.SEVERE, "IMAP Folder closed:", fce);
isRunnable = false;
} catch (MessagingException ex) {
if (ex.getMessage().contains("IDLE not supported")) {
idleIsAvailable = false;
} else {
logger.log(Level.SEVERE, "IMAP Folder & Something error occured;", ex);
isRunnable = false;
}
}
} else {
try {
executeCheckForIdleDisable();
} catch (InterruptedException | MessagingException ime) {
logger.log(Level.SEVERE, "Some error occured on executeCheckForIdleDisable() : ", ime);
isRunnable = false;
}
}
} else {
logger.log(Level.SEVERE, "THis is not IMAP Folder.");
isRunnable = false;
}
}
}
*/
}
最後に、JavaMail の Message から JSON の文字列を生成するエンコーダを作成します。このエンコーダを作成する事で、Message オブジェクトから、JSON に変換してクライアント・エンドポイントに対してメッセージを送信できるようになります。ここでは、Java EE 7 の標準に含まれる JSON-P を使用して実装しています。
session.getBasicRemote().sendObject(msg);
package jp.co.oracle.samples.websockets;
import java.io.IOException;
import java.io.StringWriter;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.json.Json;
import javax.json.JsonArrayBuilder;
import javax.json.JsonObject;
import javax.json.JsonWriter;
import javax.mail.Address;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.internet.InternetAddress;
import javax.websocket.EncodeException;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;
import jp.co.oracle.samples.msgutil.MessageDumpUtil;
/**
*
* @author Yoshio Terada
*/
public class MessageEncoder implements Encoder.Text<Message> {
private final static int SUMMARY_SIZE = 80;
private static final Logger logger = Logger.getLogger(MessageEncoder.class.getPackage().getName());
@Override
public String encode(Message msg) throws EncodeException {
try {
// Address[] から JSon 配列を作成
Address[] addresses = msg.getFrom();
JsonArrayBuilder array = Json.createArrayBuilder();
for (Address adres : addresses) {
InternetAddress iAddress = (InternetAddress) adres;
array.add(iAddress.toUnicodeString());
}
// メッセージ・サマリを取得・作成
MessageDumpUtil dumpUtil = new MessageDumpUtil();
String msgSummary = dumpUtil.getText(msg);
if (!msgSummary.isEmpty() && msgSummary.length() > SUMMARY_SIZE) {
String tmp = msgSummary.replaceAll("(\r|\n)", "");
msgSummary = tmp.substring(0, SUMMARY_SIZE);
msgSummary = msgSummary + " ......";
}
//JSon オブジェクトを生成
JsonObject model = Json.createObjectBuilder()
.add("subject", msg.getSubject())
.add("address", array)
.add("summary", msgSummary)
.build();
//JSon オブジェクトから JSon 文字列を生成
StringWriter stWriter = new StringWriter();
try (JsonWriter jsonWriter = Json.createWriter(stWriter)) {
jsonWriter.writeObject(model);
}
return stWriter.toString();
} catch (MessagingException | IOException ex) {
EncodeException ee = new EncodeException(msg, "Encode failed ", ex);
logger.log(Level.SEVERE, null, ex);
throw ee;
}
}
@Override
public void init(EndpointConfig config) {
}
@Override
public void destroy() {
}
}
上記のようにして、IMAP サーバの受信箱(INBOX)に新着メッセージが来た時点で通知を行う事ができます。しかし、このようなアプリケーションを一般公開する大規模なサービスとして実装すべきではありません。元々、WebSocket をサポートするサーバ・エンジンは大量のアクセスに対し少ないスレッドで処理を行うように、NIO で実装されている事が多いかと想定します。
GlassFish (Grizzly) の場合:
Grizzlyの概要 : C10K問題に対応するGlassFish(Grizzly)
Grizzlyの概要 2 : Java New I/Oで実装されたサーバその他、Jetty でも古くからNIOに対応しています。
今回作成した私の WebSocket で通知するアプリは、監視を行うために、新しく1つのスレッドを作成し、スレッド内で変更を監視するように実装しています。つまり、IMAP の受信箱(INBOX) の監視を行うために、ユーザ毎に新しくスレッドを生成しています。監視を行う為に別のスレッドを起こして無限ループ内で特定のイベントを監視するプログラムはよくあるかと想定しますが、そのようなアプリケーションに WebSocket は向きません。仮に ManagedExecutorService に変更し、コネクション・プールを使用しても問題は同じです。スレッド・プール数の最大までスレッドが生成されると終了です。
せっかく、サーバ・エンジンが NIO で実装して大量のアクセスに対して、少数のスレッドで処理を裁くことができるように実装されていても、個々のユーザ、もしくはリクエスト毎に、アプリケーション側でスレッドを生成すると、アプリケーション・レベルでC10K 問題が発生します。具体的には、100 人サーバに接続してきた場合に、100 スレッドが生成され、1000 人接続してきた場合、1000 スレッドが必要になります。
アクセス数が限られる環境では、こうした実装もありかもしれませんが、大規模に展開するサービスでは、リアルタイムで何らかの監視を行うために、個々にスレッドを生成するような実装はやめた方がよいと思います。WebSocket はリアルタイム監視などにもでき、簡単に実装できますと私も説明をしていますが、サーバ側の実装は十分にご検討頂いた後、作成してください。
追記:2014 年 5 月 9 日
上記の実装後、Java Mail のスペックリードに上記を解決するために、NIO を使って実装できる API を新たに作成して欲しい旨要望を出しておりました。その結果、Java Mail 1.5.2 より実験的に、IdleManager クラスが導入されました。これにより上記で実装した idle() メソッドの代わりに IdleManager で実装する事で、多くのスレッドを生成しなくてもよくなりサーバ側での C10K 問題を解決できます。詳しくは下記 IdleManager の API とそこに記載されているサンプルをご覧ください。
https://javamail.java.net/nonav/docs/api/com/sun/mail/imap/IdleManager.html
実際には微調整が必要ですが下記のような実装になります。
@Resource
ManagedExecutorService es;
private void checkNewMessage(final javax.websocket.Session session) throws MessagingException, IOException {
Properties props = session.getProperties();
props.put("mail.event.scope", "session"); // or "application"
props.put("mail.event.executor", es);
//javax.mail.Session
Session mailSession = Session.getInstance(props, null);
// シングルトン EJB より IdleManager を取得
final IdleManager idleManager =
imapIdleManager.getSingleIdleManager(mailSession, es);
//final IdleManager idleManager = new IdleManager(mailSession, es);
// IMAP Server へ接続
javax.mail.Store initStore = mailSession.getStore("imaps");
initStore.connect("imap-server.yoshio3.com", "USERNAME", "PASSWORD");
Folder folder = store.getFolder("INBOX");
folder.open(Folder.READ_WRITE);
folder.addMessageCountListener(new MessageCountAdapter() {
public void messagesAdded(MessageCountEvent ev) {
Folder folder = (Folder)ev.getSource();
Message[] msgs = ev.getMessages();
System.out.println("Folder: " + folder +
" got " + msgs.length + " new messages");
// process new messages
idleManager.watch(folder); // keep watching for new messages
}
});
idleManager.watch(folder);
}
※ JJUG CCC の HoL で実施した WebSocket のハンズオンは、JMS を使用して単一のMDB でキューやトピックを監視しメッセージ配信を行っているため、上記のような問題はありません。
JSF + WebSocket で実装した IMAP Web メール・クライアント
このエントリは Java EE Advent Calendar 2013 の 11 日目のエントリです。昨日はsk44_ さんの 「JSF で日本語ファイル名のファイルダウンロード?」のご紹介 でした。
明日は @nagaseyasuhito さんです。
エントリを始める前に、昨日 12/10 は Java EE 6/GlassFish v3 が正式リリースされて丁度 4 年目にあたる日でした。2009 年のブログを確認すると 昨日の日本時間の夜 11 時頃からダウンロードできていたようです。
Happy Birth Day 4th Anniversary of Java EE 6 & GlassFish v3 !!
今回私は、JavaServer Faces (JSF) + WebSocket + Java Mail API を使用して、IMAP のメールクライアントを作成しました。本アプリケーションは、Ajax を使用していますが、Ajax 部分では一切 JavaScript を使用していません。JSF のデフォルトで用意されている Ajax ライブラリを使用し動的な画面更新を実現しています。また、今回実装したコードはコード量も比較的少なくある程度かんたんに動かす所までの実装で 3-4 日程度で実装できています。是非ご覧ください。
このアプリケーションのデモ動画はコチラ
今回作成した JSF のアプリケーションは並列処理 (Concurrency Utilities for Java EE)も利用しています。例えば長時間処理が必要な処理を実行しなければならない場合、バックエンドの処理をシーケンシャルに処理していては大量の時間を要してしまいます。これを並列処理 (Concurrency Utilities) を利用する事で描画までの時間を短縮する事もできます。
ここでご紹介する JSF(PrimeFaces) で実装したサンプル・アプリケーションを通じて Java EE 7 のテクノロジーを使ってどのような事ができるのか、どのようにして実装できるのかをご理解いただければ幸いです。
特に JSF (PrimeFaces)で ツリーやテーブルを扱う部分、さらには Ajax を実現する部分はご注目ください。
※ このアプリケーションの実装では INBOX を監視し新規メッセージを受信した際、WebSocket で通知を行なう部分も実装しています。しかし WebSocket の実装部分は次回エントリで記載する予定です。
今回作成したアプリの全ソースコードは下記の URL にアップしました。
https://github.com/yoshioterada/JSF-WebSocket-Mailer
本アプリケーションで使用する、Java EE 7 の技術を紹介します。
● JavaServer Faces 2.2 (PrimeFaces 4.0)
● Java Mail 1.5
● Contexts and Dependency Injection 1.1
● Concurrency Utilities 1.0
まず、本アプリケーションの完成予想イメージを示します。

本アプリケーションは、ログイン画面の「IMAP Server 名」で指定した IMAP サーバに対して、「ログイン名」、「パスワード」を入力しIMAPサーバとの認証を行い、認証に成功した場合、下記の画面が表示されます。

上記の画面は、主に3つのコンポーネントから構成されています。
● フォルダ一覧表示部(画面左部)
● フォルダ内のサブジェクト一覧表示部(画面右上部)
● メッセージ表示部(画面右下部)
フォルダ一覧表示部(画面左部)
画面左側に IMAP サーバ上で作成されているフォルダの一覧を表示しています。

フォルダに子のフォルダが存在する場合、「▶」のマークが表示され、展開する事によって子のフォルダ一覧を取得できます。「▶」を押下すると Ajax でサーバに問い合わせを行い、子のフォルダを一覧を取得します。1度子フォルダを取得した後は「▶」を押下しても Ajax 通信を行なわず、開いたり閉じたりできるようになります。
特定のフォルダを選択すると、選択したフォルダ内に存在するメッセージを Ajax で行い、取得後「フォルダ内のサブジェクト一覧表示部」と「メッセージ表示部」を更新します。
また、「受信数:」のフィールドにデフォルトで「10」と表示されています。ここで扱う数字は、画面右部の「フォルダ内のサブジェクト一覧表示部」で扱うメッセージの件数を変更できます。デフォルトでは、テーブル内で表示されているメッセージは5件です。テーブル下部に存在するボタン「2」を押下する事で次の5件を表示できるようになります。
本エントリでは詳細は説明しませんが、「リアルタイム・チェック開始」、「リアルタイム・チェック中止」ボタンを押下する事で、それぞれ WebSocket 通信の開始、中止を行なうことが、INBOX (受信箱)にメッセージが届くと WebSocket でリアルタイムに通知を受け取ることができるようになります。
フォルダ内のサブジェクト一覧表示部(画面右上部)
画面右上部では、アプリケーション起動直後はデフォルトで INBOX(受信箱)に存在するメッセージの内、最新5 件のメッセージの「サブジェクト」、「送信者アドレス」、「送信日付」、「サイズ」を取得し表示しています。

また、デフォルトでフォルダに存在する最新のメッセージが選択された状態になります。また、「サブジェクト」、「日付」、メッセージの「サイズ」に応じてソートができるようになっていますので、各項目でソートをしたい場合、各項目の上下の ↑↓ 部分を押下することでソートができます。
また、サーバ側ではデフォルトで受信数 10 件を管理していますが、1 画面中では、5 件のメッセージを表示できます。テーブル下部に存在する「2」のボタンを押下する事で次の5件を取得できます。このデフォルトの受信数を変更したい場合は、「フォルダ一覧表示部(画面左部)」の下に存在する「受信数」のフィールドの数字を変更し「適用」ボタンを押下する事で受信数を変更でき、参照可能な件数が代わります。例えば、「受信数」を 20 に変更した場合、テーブル下部に「1」、「2」、「3」、「4」のボタンが追加されます。また、テーブル内に存在する、メッセージ(サブジェクト等が表示されている)を選択すると対応するメッセージを Ajax で取得し「メッセージ表示部」に対象のメッセージを表示します。
メッセージ表示部
最後に、右画面の下部を下記に示します。デフォルトで INBOX(受信箱)に存在する最新のメッセージを表示していますが、「フォルダ内のサブジェクト一覧表示部(画面右上部)」で特定のメッセージをマウスでクリックし選択すると、対応するメッセージがここで表示されます。

本アプリケーションの実装方法の詳細
この JSF アプリケーションの主要な機能は View の実装として、JSF のFacelets を使用し「folders-show.xhtml」ファイルに画面デザインを実装しています。また、この画面のバックエンドの処理を行なったり、画面上に存在する各種コンポーネントとのバインディングを行なうために、JSF 管理対象 Bean を「MessageReceiveMgdBean.java」として実装しています。つまりこの2つのファイルを確認する事で、本アプリケーションの詳細な振る舞いを把握する事ができます。
メッセージを表示するために実装した Facelets の全ソースコードを下記に示します。ある程度、複雑な画面構成になっているにも関わらず、記載しているコード量が 88行程度と、とても少ない事をご確認いただけるのではないかと思います。
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:p="http://primefaces.org/ui"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets">
<h:head>
<title>JSF-WebSocket WebMail</title>
<f:event type="preRenderView" listener="#{messageReceiveMgdBean.onPageLoad}"/>
<h:outputScript library="javascripts" name="ws-client-endpoint.js"/>
</h:head>
<h:body>
<h:form id="form">
<p:notificationBar position="top" effect="slide" widgetVar="bar" styleClass="top" style="background-color : #F8F8FF ; width: fit-content;">
<h:panelGrid columns="2" columnClasses="column" cellpadding="0">
<h:outputText value="新着メッセージ :" style="color: red;font-size:12px;" /> <p:commandButton value="閉じる" onclick="PF('bar').hide()" type="button" style="font-size:10px;"/>
<h:outputText value="Subject :" style="font-size:10px;" /><h:outputText id="wssubject" value="" style="font-size:10px;" />
<h:outputText value="From :" style="font-size:10px;" /><h:outputText id="wsfrom" value="" style="font-size:10px;" />
<h:outputText value="メッセージ・サマリー :" style="font-size:10px;" /><h:outputText id="wssummary" value="" style="font-size:10px;" />
</h:panelGrid>
</p:notificationBar>
<p:layout fullPage="true">
<p:layoutUnit position="west" size="200" header="フォルダ一覧" resizable="true" closable="true" collapsible="true" style="font-size:14px;">
<p:tree id="docTree" value="#{messageReceiveMgdBean.root}" var="doc" selectionMode="single" dynamic="true" selection="#{messageReceiveMgdBean.selectedNode}">
<p:ajax event="select" listener="#{messageReceiveMgdBean.onNodeSelect}" update=":form:mailheader :form:specifiedMsg"/>
<p:treeNode>
<h:outputText value="#{doc.name}" style="font-size:14px;"/>
</p:treeNode>
</p:tree>
<p:outputLabel value="受信数:" style="font-size:10px;"/>
<p:inputText autocomplete="false" id="numberOfMsg" value="#{messageReceiveMgdBean.numberOfMessages}" style="font-size:10px;">
</p:inputText>
<h:commandButton id="upBtn" value="適用" style="font-size:10px;">
<f:ajax event="click" render="mailheader" execute="numberOfMsg" listener="#{messageReceiveMgdBean.updateMessageCount}"/>
</h:commandButton>
<input id="connect" type="button" value="リアルタイムチェック開始" style="font-size:10px;" onClick="connectServerEndpoint();"/>
<input id="close" type="button" value="リアルタイムチェック中止" style="font-size:10px;" onClick="closeServerEndpoint();"/>
</p:layoutUnit>
<p:layoutUnit position="center">
<p:dataTable id="mailheader" var="mheader"
paginator="true"
paginatorPosition="bottom"
value="#{messageReceiveMgdBean.mailHeaderModel}"
rows="5" rowKey=" #{mheader.messageCount}"
selection="#{messageReceiveMgdBean.selectedMailHeader}"
selectionMode="single" style="width:800px;font-size:10px;" >
<p:ajax event="rowSelect" listener="#{messageReceiveMgdBean.onMessageSelect}" update=":form:specifiedMsg" global="false"/>
<p:column id="msubject" headerText="サブジェクト" style="font-size:10px;" sortBy="subject" width="50%">
#{mheader.subject}
</p:column>
<p:column id="maddress" headerText="アドレス" style="font-size:10px;" width="30%">
<ui:repeat value="#{mheader.fromAddress}" var="fromEmail">
#{fromEmail.toUnicodeString()}
</ui:repeat>
</p:column>
<p:column id="mdate" headerText="日付" style="font-size:10px;" sortBy="sendDate" width="10%">
<h:outputLabel value="#{mheader.sendDate}">
<f:convertDateTime pattern="yyyy年MM月dd日 HH:mm:ss"/>
</h:outputLabel>
</p:column>
<p:column id="msize" headerText="サイズ" style="font-size:10px;" sortBy="size" width="10%">
#{mheader.size}
</p:column>
</p:dataTable>
<p:scrollPanel style="width:800px;height:400px" mode="native">
<h:outputText id="specifiedMsg" value="#{messageReceiveMgdBean.specifiedMessage}" escape="false"/>
</p:scrollPanel>
</p:layoutUnit>
</p:layout>
</h:form>
<p:ajaxStatus onstart="PF('statusDialog').show();" onsuccess="PF('statusDialog').hide();"/>
<p:dialog modal="true" widgetVar="statusDialog" header="処理中"
draggable="false" closable="false">
<p:graphicImage value="/resources/imgs/ajaxloadingbar.gif" />
</p:dialog>
</h:body>
</html>
次に上記 Facelets のバックエンド処理を実装する、MessageReceiveMgdBean クラスを下記に示します。
package jp.co.oracle.samples.mgdBean;
import java.io.Serializable;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Resource;
import javax.enterprise.concurrent.ManagedExecutorService;
import javax.inject.Named;
import javax.faces.view.ViewScoped;
import javax.inject.Inject;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Store;
import jp.co.oracle.samples.tasks.AllFolderHandlerTask;
import jp.co.oracle.samples.beans.FolderName;
import jp.co.oracle.samples.beans.MailHeader;
import jp.co.oracle.samples.beans.MailHeaderModel;
import jp.co.oracle.samples.tasks.SpecifiedMessageHandlerTask;
import jp.co.oracle.samples.tasks.SpecifiedNodeMailHeaderHandleTask;
import org.primefaces.event.NodeSelectEvent;
import org.primefaces.event.SelectEvent;
import org.primefaces.model.TreeNode;
/**
*
* @author Yoshio Terada
*/
@Named(value = "messageReceiveMgdBean")
@ViewScoped
public class MessageReceiveMgdBean implements Serializable {
private Store store;
private TreeNode root;
private TreeNode selectedNode;
private MailHeader selectedMailHeader;
private MailHeaderModel mailHeaderModel;
private String folderFullName;
private String specifiedMessage;
private int numberOfMessages = DEFAULT_NUMBER_OF_MESSAGE;
private final static int DEFAULT_NUMBER_OF_MESSAGE = 10;
private static final Logger logger = Logger.getLogger(MessageReceiveMgdBean.class.getPackage().getName());
@Inject
IndexLoginMgdBean login;
@Resource
ManagedExecutorService execService;
/**
* コンストラクタ
*/
public MessageReceiveMgdBean() {
}
/**
* ページのロード時の処理を実装
* 並列で各タスクを実行し結果表示速度を少し改善
*/
public void onPageLoad() {
String imapServer = login.getImapServer();
String username = login.getUsername();
String password = login.getPassword();
initStore(imapServer, username, password);
if (getRoot() == null) {
//全フォルダリストの取得
Future<TreeNode> folderHandlesubmit = execService.submit(new AllFolderHandlerTask(store));
int num = getNumberOfMessages();
if (num == 0) {
num = DEFAULT_NUMBER_OF_MESSAGE;
}
// デフォルトで INBOX のメッセージの取得
folderFullName = "INBOX";
Future<MailHeaderModel> headerHandlerSubmit = execService.submit(new SpecifiedNodeMailHeaderHandleTask(store, folderFullName, num));
Future<String> messageHandlerSubmit = null;
try {
// デフォルトで INBOX の最新のメッセージ取得
messageHandlerSubmit = execService.submit(new SpecifiedMessageHandlerTask(store, folderFullName, store.getFolder(folderFullName).getMessageCount()));
} catch (MessagingException ex) {
logger.log(Level.SEVERE, null, ex);
}
try {
//左ペインのツリーの一覧を設定
root = folderHandlesubmit.get();
//右ペインのテーブルの設定
MailHeaderModel mailmodel = headerHandlerSubmit.get();
setMailHeaderModel(mailmodel);
List<MailHeader> headers = mailmodel.getAllHeader();
//デフォルトで最新のメッセージを選択された状態に設定
if (headers != null && !headers.isEmpty()) {
MailHeader latestMailHeader = headers.get(0);
selectedMailHeader = latestMailHeader;
}
if (messageHandlerSubmit != null) {
specifiedMessage = messageHandlerSubmit.get();
}
} catch (InterruptedException | ExecutionException ex) {
logger.log(Level.SEVERE, null, ex);
}
}
}
// ツリーが選択された際に呼び出されるイベント
public void onNodeSelect(NodeSelectEvent event) {
folderFullName = ((FolderName) selectedNode.getData()).getFullName();
int num = getNumberOfMessages();
if (num == 0) {
num = DEFAULT_NUMBER_OF_MESSAGE;
}
// 選択したフォルダのメールヘッダを更新
Future<MailHeaderModel> headerHandlerSubmit = execService.submit(new SpecifiedNodeMailHeaderHandleTask(store, folderFullName, num));
// 選択したフォルダの最新メッセージを取得
try {
MailHeaderModel mailmodel = headerHandlerSubmit.get();
//メールヘッダの更新
setMailHeaderModel(mailmodel);
// 最新のメッセージ取得
Future<String> messageHandlerSubmit = execService.submit(new SpecifiedMessageHandlerTask(store, folderFullName, store.getFolder(folderFullName).getMessageCount()));
specifiedMessage = messageHandlerSubmit.get();
List<MailHeader> headers = mailmodel.getAllHeader();
//デフォルトで最新のメッセージを選択された状態に設定
if (headers != null && !headers.isEmpty()) {
MailHeader latestMailHeader = headers.get(0);
selectedMailHeader = latestMailHeader;
}
} catch (MessagingException | InterruptedException | ExecutionException ex) {
logger.log(Level.SEVERE, null, ex);
}
}
// メッセージが選択された際に呼び出されるイベント
public void onMessageSelect(SelectEvent event) {
int msgCount = ((MailHeader) event.getObject()).getMessageCount();
try {
Future<String> messageHandlerSubmit = execService.submit(new SpecifiedMessageHandlerTask(store, folderFullName, msgCount));
specifiedMessage = messageHandlerSubmit.get();
} catch (InterruptedException | ExecutionException ex) {
logger.log(Level.SEVERE, null, ex);
}
}
// メッセージのカウンタが更新された際の処理
// 10 よりしたの値が入力された場合、何もしない。
public void updateMessageCount() {
String folderName = folderFullName;
if (folderName.isEmpty()) {
folderName = "INBOX";
}
int num = getNumberOfMessages();
if (num > 10) {
Future<MailHeaderModel> headerHandlerSubmit = execService.submit(new SpecifiedNodeMailHeaderHandleTask(store, folderName, num));
try {
setMailHeaderModel(headerHandlerSubmit.get());
} catch (InterruptedException | ExecutionException ex) {
logger.log(Level.SEVERE, null, ex);
}
}
}
// Store の初期化(ページのロード時)
private void initStore(String imapServer, String username, String password) {
Properties props = System.getProperties();
props.setProperty("mail.store.protocol", "imaps");
javax.mail.Store initStore;
try {
Session session = Session.getDefaultInstance(props, null);
initStore = session.getStore("imaps");
initStore.connect(imapServer, username, password);
this.store = initStore;
} catch (MessagingException ex) {
logger.log(Level.SEVERE, null, ex);
}
}
/**
* @return the selectedNode
*/
public TreeNode getSelectedNode() {
return selectedNode;
}
/**
* @param selectedNode the selectedNode to set
*/
public void setSelectedNode(TreeNode selectedNode) {
this.selectedNode = selectedNode;
}
/**
* @return the selectedMailHeader
*/
public MailHeader getSelectedMailHeader() {
return selectedMailHeader;
}
/**
* @param selectedMailHeader the selectedMailHeader to set
*/
public void setSelectedMailHeader(MailHeader selectedMailHeader) {
this.selectedMailHeader = selectedMailHeader;
}
/**
* @return the mailHeaderModel
*/
public MailHeaderModel getMailHeaderModel() {
return mailHeaderModel;
}
/**
* @param mailHeaderModel the mailHeaderModel to set
*/
public void setMailHeaderModel(MailHeaderModel mailHeaderModel) {
this.mailHeaderModel = mailHeaderModel;
}
/**
* @return the specifiedMessage
*/
public String getSpecifiedMessage() {
return specifiedMessage;
}
/**
* @param specifiedMessage the specifiedMessage to set
*/
public void setSpecifiedMessage(String specifiedMessage) {
this.specifiedMessage = specifiedMessage;
}
/**
* @return the numberOfMessages
*/
public int getNumberOfMessages() {
return numberOfMessages;
}
/**
* @param numberOfMessages the numberOfMessages to set
*/
public void setNumberOfMessages(int numberOfMessages) {
this.numberOfMessages = numberOfMessages;
}
/**
* @return the root
*/
public TreeNode getRoot() {
return root;
}
}
本アプリケーションの実装において、特筆すべき点として JSF を利用する事で Ajax がとても簡単に実装できる点です。実際、今回のアプリケーションでは WebSocket の実装部以外で一切 JavaScript を記述しておらず、JSF の標準に含まれる <f:ajax> タグを拡張した PrimeFaces の <p:ajax> タグを使用して Ajax 通信を実現しています。
それでは、上記のコードを分割して、各コンポーネントの実装について詳しくご紹介していきます。
画面描画前に実行するコードの実装
この IMAP のメッセージを表示する画面は、画面にリクエストが発生した際に、各種画面のコンポーネントを初期化し、デフォルトで表示する全データを取得した後、描画を行ないます。これを実現するために、JSF では画面の描画前に処理を実行するために <f:event> タグを利用できます。
<h:head>
<title>JSF-WebSocket WebMail</title>
<f:event type="preRenderView"
listener="#{messageReceiveMgdBean.onPageLoad}"/>
</h:head>
ここで type=”preRenderView” 属性を追加しレンダリングされる前にイベントを発生する事を指定し、実行したい処理は listener で指定します。ここでは listener で「#{messageReceiveMgdBean.onPageLoad}”」を定義し、CDI (JSF の管理対象 Bean) として実装した MessageReceiveMgdBean クラスの onPageLoad() メソッドを呼び出しています。onPageLoad()メソッドでは最初のアクセスの際にデフォルトで描画する全コンポーネント(ツリーの構築部や、テーブルの構築部、メッセージの表示部)のデータを取得し組み立てます。
この際、「ツリーの構築部」、「テーブルの構築部」、「メッセージの表示部」を構成するための処理を、それぞれ並列処理タスクとして実装しました。 仮に並列処理で実装しない場合、画面を構築するためにかかる所要時間は、「フォルダ一覧表示部(画面左部)」+「フォルダ内のサブジェクト一覧表示部」+「メッセージ表示部」の合計時間になります。この時間を少しでも軽減するために、上記をそれぞれ別のタスクとして実装して並列に取得できるように実装します。
フォルダ一覧表示部(画面左部)の実装
下記に、画面左部の実装を説明します。

画面左部の「フォルダ一覧表示部」の部分のコードは下記です。下記は、大きく3つのコンポーネントから構成されています。
● フォルダの一覧を表示するツリー
● 受信数を変更するテキスト・フィールド
● WebSocket によるリアルタイム監視機能の 開始/中止 ボタン
<p:layoutUnit position="west" size="200" header="フォルダ一覧" resizable="true" closable="true" collapsible="true" style="font-size:14px;">
<p:tree id="docTree" value="#{messageReceiveMgdBean.root}" var="doc" selectionMode="single" dynamic="true" selection="#{messageReceiveMgdBean.selectedNode}">
<p:ajax event="select" listener="#{messageReceiveMgdBean.onNodeSelect}" update=":form:mailheader :form:specifiedMsg"/>
<p:treeNode>
<h:outputText value="#{doc.name}" style="font-size:14px;"/>
</p:treeNode>
</p:tree>
<p:outputLabel value="受信数:" style="font-size:10px;"/>
<p:inputText autocomplete="false" id="numberOfMsg" value="#{messageReceiveMgdBean.numberOfMessages}" style="font-size:10px;">
</p:inputText>
<h:commandButton id="upBtn" value="適用" style="font-size:10px;">
<f:ajax event="click" render="mailheader" execute="numberOfMsg" listener="#{messageReceiveMgdBean.updateMessageCount}"/>
</h:commandButton>
<input id="connect" type="button" value="リアルタイムチェック開始" style="font-size:10px;" onClick="connectServerEndpoint();"/>
<input id="close" type="button" value="リアルタイムチェック中止" style="font-size:10px;" onClick="closeServerEndpoint();"/>
</p:layoutUnit>
ここで、特に「フォルダ一覧表示部」のツリーは下記のコードで実現しています。
… 前略
<p:tree id="docTree" value="#{messageReceiveMgdBean.root}" var="doc" selectionMode="single" dynamic="true" selection="#{messageReceiveMgdBean.selectedNode}">
<p:ajax event="select" listener="#{messageReceiveMgdBean.onNodeSelect}" update=":form:mailheader :form:specifiedMsg"/>
<p:treeNode>
<h:outputText value="#{doc.name}" style="font-size:14px;"/>
</p:treeNode>
</p:tree>
… 後略
HTML の中でツリーを実現するために、PrimeFaces の <p:tree> タグを使用します。 また、<p:tree> には下記の属性を指定しています。
● id= コンポーネントの識別子
● value=ツリーを描画するための TreeNode オブジェクト
● var= TreeNode 内に含まれる各ノード・オブジェクト (FolderName) に対する変数
● selectionMode=選択モード
● dynamic=動的モード
● selection=選択されたノードを表すオブジェクト
次に、上記のツリーを描画するために必要な実装コード (MessageReceiveMgdBean クラス) を下記に抜粋します。
@Named(value = "messageReceiveMgdBean")
@ViewScoped
public class MessageReceiveMgdBean implements Serializable {
private TreeNode root;
private TreeNode selectedNode;
@Resource
ManagedExecutorService execService;
public void onPageLoad() {
// ログイン画面で入力されたデータの取得
String imapServer = login.getImapServer();
String username = login.getUsername();
String password = login.getPassword();
initStore(imapServer, username, password);
if (getRoot() == null) {
//全フォルダリストの取得
Future<TreeNode> folderHandlesubmit = execService.submit(new AllFolderHandlerTask(store));
try {
root = folderHandlesubmit.get();
} catch (InterruptedException | ExecutionException ex) {
logger.log(Level.SEVERE, null, ex);
}
}
// Store の初期化(ページのロード時)
private void initStore(String imapServer, String username, String password) {
Properties props = System.getProperties();
props.setProperty("mail.store.protocol", "imaps");
javax.mail.Store initStore;
try {
Session session = Session.getDefaultInstance(props, null);
initStore = session.getStore("imaps");
initStore.connect(imapServer, username, password);
this.store = initStore;
} catch (MessagingException ex) {
logger.log(Level.SEVERE, null, ex);
}
}
}
まず、initStore() メソッドを実行し IMAP サーバに接続します。接続に問題がない場合、18 行目〜26行目で並列タスクを実行し、実行結果よりツリーを取得しています。
ここで、ツリーを構成するための並列処理タスクは下記「AllFolderHandlerTask」クラスです。Callable インタフェースを実装したこのタスクは Runnable インタフェースを実装したタスクと違い、返り値 (TreeNode) を取得することができます。
タスクが ManagedExecutorService のインスタンス execService#submit() によって実行されると、AllFolderHandlerTask クラスの call() メソッドが呼び出されます。このメソッドは IMAP サーバに存在する全フォルダ一覧を再帰的に取得し、TreeNode を構築していきます。
より詳しく説明すると、TreeNode rootのインスタンスを生成し、IMAP サーバに存在するデフォルトのフォルダの一覧を取得します。そしてフォルダの一覧を root に付け加えていきます。次に取得したフォルダの中で子のフォルダを持つフォルダの場合、再帰的に子のフォルダ一覧を取得しツリーのノードに付け加えていきます。全てのフォルダ一覧を取得した後、全フォルダ一覧を含む TreeNode のオブジェクトを返します。
#getAllIMAPFolders()の再帰の実装正直いけてないですが、
#TreeNode に追加する方法上こう実装しました。
AllFolderHandlerTask#call() メソッドの処理が完了後、onPageLoad() メソッドに処理が戻り、root = folderHandlesubmit.get() で返り値を取得できます。そして取得した結果を root に代入しています。
public class AllFolderHandlerTask implements Callable<TreeNode> {
private final Store store;
private static final Logger logger = Logger.getLogger(AllFolderHandlerTask.class.getPackage().getName());
public AllFolderHandlerTask(Store store) {
this.store = store;
}
@Override
public TreeNode call() throws Exception {
TreeNode root = new DefaultTreeNode("root", null);
Folder[] folders;
if (store == null) {
return null;
}
try {
folders = store.getDefaultFolder().list();
getAllIMAPFolders(root, folders);
} catch (MessagingException ex) {
logger.log(Level.SEVERE, null, ex);
}
return root;
}
private TreeNode getAllIMAPFolders(TreeNode root, Folder[] folders) {
TreeNode child = null;
try {
for (Folder folder : folders) {
String folName = folder.getName();
String folFullName = folder.getFullName();
if (hasChildFolder(folder) == true) {
child = new DefaultTreeNode(new FolderName(folName, folFullName), root);
getAllIMAPFolders(child, folder.list());
} else {
child = new DefaultTreeNode(new FolderName(folName, folFullName), root);
}
}
} catch (MessagingException ex) {
logger.log(Level.SEVERE, null, ex);
}
return child;
}
//フォルダに子のフォルダがあるか否か
private boolean hasChildFolder(Folder folder) throws MessagingException {
boolean hasFolder = false;
if (folder.list().length > 0) {
hasFolder = true;
}
return hasFolder;
}
}
onPageLoad() メソッドに処理が戻った後、root に代入する事で、View からフォルダ一覧がツリーとして参照できるようになります。具体的には、Facelets の <p:tree> タグに記述している value=”#{messageReceiveMgdBean.root}” で参照できるようになります。
また、<p:tree> タグに追加した属性 var=”doc” によって、ツリー中に含まれる各フォルダは、変数 doc として EL 式中で扱う事ができるようになります。具体的に doc は FolderName クラスのインスタンスを表します。例えば、#{doc.name} は FolderName インスタンスの getName() メソッドを呼び出し、フォルダ名を取得することができます。
<p:treeNode>タグ中に <h:outputText> タグを記載しています。このタグはテキストを表示するためのコンポーネントですが、属性 value=”#{doc.name}” で各フォルダの名前を出力しています。
<p:tree id="docTree" value="#{messageReceiveMgdBean.root}" var="doc" selectionMode="single" dynamic="true" selection="#{messageReceiveMgdBean.selectedNode}">
<p:ajax event="select" listener="#
{messageReceiveMgdBean.onNodeSelect}"
update=":form:mailheader :form:specifiedMsg"/>
<p:treeNode>
<h:outputText value="#{doc.name}" style="font-size:14px;"/>
</p:treeNode>
</p:tree>
次に、ツリー中のフォルダが選択された際の処理を説明します。ツリー中のフォルダを選択した際、選択されたツリーのノードは、<p:tree> タグで指定した属性、selection (<p:tree selection=”#{messageReceiveMgdBean.selectedNode}”>)によって MessageReceiveMgdBeanクラスの selectedNode に代入されます。
ここで、<p:tree> タグの直下に、<p:ajax>タグを記載していますが、この<p:ajax>タグは、ツリー内で特定のフォルダが選択されると、同タグ中の listener=”#{messageReceiveMgdBean.onNodeSelect}”
を呼び出します。これは、内部的に MessageReceiveMgdBean クラスの onNodeSelect(NodeSelectEvent event)を呼び出します。
onNodeSelect()のメソッドでは、選択されたフォルダに存在する「フォルダ内のサブジェクト一覧表示部」と「メッセージ表示部」を取得する処理が行なわれます。
呼び出した結果、<p:ajax> タグの update 属性の設定に従い、 <p:ajax update=”:form:mailheader :form:specifiedMsg”/>「:form:mailheader」と「:form:specifiedMsg」で指定するコンポーネント、つまり「フォルダ内のサブジェクト一覧表示部」と「メッセージ表示部」を更新します。
「フォルダ内のサブジェクト一覧表示部(画面右上部)の実装
つづいて、「フォルダ内のサブジェクト一覧表示部」の実装を説明します。

テーブルを実現している Facelets の実装コードは下記です。
<p:dataTable id="mailheader" var="mheader"
paginator="true"
paginatorPosition="bottom"
value="#{messageReceiveMgdBean.mailHeaderModel}"
rows="5" rowKey=" #{mheader.messageCount}"
selection="#{messageReceiveMgdBean.selectedMailHeader}"
selectionMode="single" style="width:800px;font-size:10px;" >
<p:ajax event="rowSelect" listener="#{messageReceiveMgdBean.onMessageSelect}" update=":form:specifiedMsg" global="false"/>
<p:column id="msubject" headerText="サブジェクト" style="font-size:10px;" sortBy="subject" width="50%">
#{mheader.subject}
</p:column>
<p:column id="maddress" headerText="アドレス" style="font-size:10px;" width="30%">
<ui:repeat value="#{mheader.fromAddress}" var="fromEmail">
<h:outputLabel value="#{fromEmail.toUnicodeString()}"/>
</ui:repeat>
</p:column>
<p:column id="mdate" headerText="日付" style="font-size:10px;" sortBy="sendDate" width="10%">
<h:outputLabel value="#{mheader.sendDate}">
<f:convertDateTime pattern="yyyy年MM月dd日 HH:mm:ss"/>
</h:outputLabel>
</p:column>
<p:column id="msize" headerText="サイズ" style="font-size:10px;" sortBy="size" width="10%">
#{mheader.size}
</p:column>
</p:dataTable>
ちょっと補足:
web.xml の最後に下記の <context-param>を追加してください。
上記 JSF の Facelets でメッセージ送信日付を、f:convertDateTime で変換し描画しています。この変換の際デフォルトのタイムゾーンをシステムのタイムゾーンに変更します。
<web-app version="3.1" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">
… 前略
<context-param>
<param-name>javax.faces.DATETIMECONVERTER_DEFAULT_TIMEZONE_IS_SYSTEM_TIMEZONE</param-name>
<param-value>true</param-value>
</context-param>
</web-app>
画面が描画前に呼び出される処理は onPageLoad() メソッドに実装されている事は上記ですでに紹介しました。onPageLoad() メソッドの中、つまり最初のアクセスの際に、テーブルは「INBOX(受信箱)」に存在するメッセージを並列タスクとして取得し描画します。
HTML のテーブルを扱うために PrimeFaces の <p:dataTable>を使用します。
<p:dataTable id=”mailheader” var=”mheader”
paginator=”true”
paginatorPosition=”bottom”
value=”#{messageReceiveMgdBean.mailHeaderModel}”
rows=”5″ rowKey=” #{mheader.messageCount}”
selection=”#{messageReceiveMgdBean.selectedMailHeader}”
selectionMode=”single” style=”width:800px;font-size:10px;” >
<p:dataTable> タグでは下記の属性を指定しています。
● paginator : テーブル内にボタンを表示しページ移動を可能にする属性
● paginatorPosition : ページ移動用のボタンの配置場所を指定
● value: テーブル内で扱うデータ・モデル
● rows: テーブルで描画する行数
● rowKey: 行を選択した際、他の行と区別するためのキー
● selection: 選択された行のデータ
● selectionMode: 選択モード
この中で、特に重要なのが value で指定するデータ・モデルです。今回、このテーブルを扱うためのモデルとして、MailHeaderModel クラスを実装し、value にはこのクラスのインスタンスを代入します。MailHeaderModel クラスは MailHeader オブジェクト(行を表すオブジェクト)を List で管理しています。これらのクラスのインスタンスは並列処理タスク SpecifiedNodeMailHeaderHandleTask の中で生成されます。下記に MailHeaderModel、MailHeader クラスの実装をそれぞれ示します。
まず、テーブル内に表示するデータを保持する MailHeader クラスを作成します。今回テーブルには「サブジェクト」、「From:」、「送信日」、「メッセージのサイズ」の4項目を表示させますので、4つのフィールド(subject, fromAddress, sendDate, size)を定義しています。
またmessageCount はフォルダ内に存在するメッセージの内、特定のメッセージを識別するためのキーをメッセージのカウントIDとして指定します。実際には、SpecifiedNodeMailHeaderHandleTask の中でMailHeaderのインスタンスを生成する際、Java Mail API の Message#getMessageNumber() をここに代入します。
@ViewScoped
public class MailHeader {
private String subject;
private Address[] fromAddress;
private Date sendDate;
private int size;
private Integer messageCount;
public MailHeader(String subject, Address[] fromAddress, Date sendDate, int size, Integer messageCount) {
this.subject = subject;
this.fromAddress = fromAddress;
this.sendDate = sendDate;
this.size = size ;
this.messageCount = messageCount;
}
別途、Setter, Getter メソッドを実装してください。
}
次に、上記 MailHeader クラスを管理するクラスを MailHeaderModel に実装します。このクラスはインスタンスが生成された際に、コンストラクタ内で MailHeader の List を最新日付順(最新のメッセージのカウント ID 順)でソートします。そして各行を識別するためのキーとして、メッセージのカウントID を使用します。
@ViewScoped
public class MailHeaderModel extends ListDataModel<MailHeader> implements SelectableDataModel<MailHeader> {
public MailHeaderModel() {
}
public MailHeaderModel(List<MailHeader> header) {
super(header);
Collections.sort(header, new Comparator<MailHeader>() {
@Override
public int compare(MailHeader m1, MailHeader m2) {
return m2.getMessageCount() - m1.getMessageCount();
}
});
}
@Override
public Object getRowKey(MailHeader header) {
return header.getMessageCount();
}
@Override
public MailHeader getRowData(String rowKey) {
List<MailHeader> headers = (List<MailHeader>) getWrappedData();
for (MailHeader header : headers) {
if (header.getMessageCount().toString().equals(rowKey)) {
return header;
}
}
return null;
}
}
ここで、画面ロード時に呼び出される MessageReceiveMgdBean#onPageLoad() メソッドの中から、テーブルを初期化する部分のコードを下記に抜粋し示します。画面ロード時にはデフォルトで INBOX のメッセージを取得します。INBOX のフォルダを SpecifiedNodeMailHeaderHandleTask に指定し並列タスクとして実行します。
public void onPageLoad() {
// デフォルトで INBOX のメッセージの取得
folderFullName = "INBOX";
Future<MailHeaderModel> headerHandlerSubmit =
execService.submit(
new SpecifiedNodeMailHeaderHandleTask(store,
folderFullName,
num));
try {
MailHeaderModel mailmodel = headerHandlerSubmit.get();
setMailHeaderModel(mailmodel);
// テーブル中の最新のメッセージ(MailHeader)をデフォルトで
// 選択 (マウスがクリックされた) 状態にする。
List<MailHeader> headers = mailmodel.getAllHeader();
if (headers != null && !headers.isEmpty()) {
MailHeader latestMailHeader = headers.get(0);
selectedMailHeader = latestMailHeader;
}
} catch (InterruptedException | ExecutionException ex) {
logger.log(Level.SEVERE, null, ex);
}
}
execService#submit によりSpecifiedNodeMailHeaderHandleTask(store, folderFullName, num) の並列タスクを実行します。このタスクは、指定された IMAP のフォルダの中から、num で指定した数だけ最新メッセージを取得し、取得したメッセージ(MailHeader) を含む MailHeaderModel のインスタンスを返します。
並列処理タスクの処理が完了すると、onPageLoad() に処理が戻り、headerHandlerSubmit.get() でMailHeaderModel を取得できますので、取得した MailHeaderModel を setMailHeaderModel() で置き換えて更新します。更新した後、最新のメッセージを選択状態にするため、最新のメッセージを取得し、selectedMailHeader に代入しています。
public class SpecifiedNodeMailHeaderHandleTask implements Callable<MailHeaderModel> {
private final Store store;
private final String folderFullName;
private final int numberOfMessage;
private static final Logger logger = Logger.getLogger(SpecifiedNodeMailHeaderHandleTask.class.getPackage().getName());
public SpecifiedNodeMailHeaderHandleTask(Store store,String folderFullName,int numberOfMessage){
this.store = store;
this.folderFullName = folderFullName;
this.numberOfMessage = numberOfMessage;
}
@Override
public MailHeaderModel call() throws Exception {
MailHeaderModel model = null;
if (store != null) {
try {
Folder folder = store.getFolder(folderFullName);
if (!folder.isOpen()) {
folder.open(javax.mail.Folder.READ_WRITE);
}
int endMsgs = folder.getMessageCount();
int startMsgs = endMsgs - (numberOfMessage - 1);
Message[] msgs = folder.getMessages(startMsgs, endMsgs);
List<MailHeader> data = new ArrayList<>();
for (Message msg : msgs) {
MailHeader msgModel = new MailHeader(msg.getSubject(), msg.getFrom(), msg.getSentDate(), msg.getSize(), msg.getMessageNumber());
data.add(msgModel);
}
model = new MailHeaderModel(data);
} catch (MessagingException ex) {
logger.log(Level.SEVERE, null, ex);
}
}
return model;
}
}
次に、テーブル内で行が選択された場合の実装、振る舞いを紹介します。テーブル内で行を選択すると、選択した行を表す MailHeader オブジェクトが、<p:dataTable> タグの selection 属性で指定した、
<p:dataTable selection=”#{messageReceiveMgdBean.selectedMailHeader}” >
に代入されます。また、行を選択した際のキーは MailHeaderModel クラス内の getRowKey() メソッドの返り値、つまり MailHeader の getMessageCount() をキーとなります。
また、<p:dataTable> タグの直後に<p:ajax>タグを記述しています。これにより、実際に行が選択されると <p:ajax> の属性 listener で示すメソッド onMessageSelect() メソッドが呼び出されます。
// メッセージが選択された際に呼び出されるイベント
public void onMessageSelect(SelectEvent event) {
int msgCount = ((MailHeader) event.getObject()).getMessageCount();
try {
Future<String> messageHandlerSubmit = execService.submit(new SpecifiedMessageHandlerTask(store, folderFullName, msgCount));
specifiedMessage = messageHandlerSubmit.get();
} catch (InterruptedException | ExecutionException ex) {
logger.log(Level.SEVERE, null, ex);
}
}
onMessageSelect() メソッドは選択された該当のメッセージを、SpecifiedMessageHandlerTask で実装された並列タスクで取得し、取得したメッセージの文字列を specifiedMessage に代入します。
この Ajax リクエストは処理が完了後、メッセージを<p:ajax> タグで設定されている update 属性の内容に従い、”:form:specifiedMsg” つまり「メッセージの表示部」に更新します。
メッセージ表示部(画面右下部)の実装
最後にメッセージ表示部の実装部分について説明します。
![]() |
specifiedMessage は View(Facelets) の中で下記のコードで参照・表示されます。
<p:scrollPanel style="width:800px;height:400px" mode="native">
<h:outputText id="specifiedMsg" value="#{messageReceiveMgdBean.specifiedMessage}" escape="false"/>
</p:scrollPanel>
<p:scrollPalnel> タグは、スクロールが可能なパネルで長文のメッセージを表示する際に、スクロールしながら参照が可能なパネルです。このスクロール可能なパネルの中で、IMAP の特定メッセージを <h:outputText> タグ内で表示します。ここで、<h:outputText> タグ内で excape=”false” と指定していますが、これは HTML メールを参照する場合、JSF では
デフォルトで < や > 等をエスケープ&lt;、&gt;しますので、HTMLメールをそのまま表示させるために、この部分だけエスケープしないように設定しています。
実際に、指定されたメッセージのカウントID を持つメッセージを取得するための並列タスク SpecifiedMessageHandlerTask クラスを下記に示します。
public class SpecifiedMessageHandlerTask implements Callable<String> {
private final Store store;
private final String folderFullName;
private final int msgCount;
private static final Logger logger = Logger.getLogger(SpecifiedMessageHandlerTask.class.getPackage().getName());
public SpecifiedMessageHandlerTask(Store store, String folderFullName, int msgCount) {
this.store = store;
this.folderFullName = folderFullName;
this.msgCount = msgCount;
}
@Override
public String call() throws Exception {
String returnMsg ="";
if (store != null) {
Folder folder;
try {
folder = store.getFolder(folderFullName);
if (!folder.isOpen()) {
folder.open(javax.mail.Folder.READ_WRITE);
}
Message msg = folder.getMessage(msgCount);
MessageDumpUtil dumpUtil = new MessageDumpUtil();
returnMsg = dumpUtil.getText(msg);
} catch (MessagingException | IOException e) {
logger.log(Level.SEVERE, null, e);
}
}
return returnMsg;
}
}
Message を取得するためには、Java Mail の API を使用して folder.getMessage(msgCount) で取得します。ただし Message は単純なプレイン・テキストだけではなく、マルチパート、html 形式様々な形の
メッセージが存在します。そこで、メッセージのタイプに応じて表示用の文字列を取得する必要があります。
今回の実装では、Base 64 への未対応や、マルチパートファイルのダウンロードなどは実装しておらず、テキストと HTML 表示のみ対応しています。HTML ならばその文字列をそのまま返し、プレイン・テキストの場合、<:PRE>を付加してメッセージの形式をそのままの形で表示して返しています。Message の解析を行ない表示用の文字列を取得するためのユーティリティ・クラスを MessageDumpUtil として実装しました。MessageDumpUtil クラスを下記に示します。
public class MessageDumpUtil {
private boolean textIsHtml = false;
public String getText(Part p) throws
MessagingException, IOException {
if (p.isMimeType("text/*")) {
textIsHtml = p.isMimeType("text/html");
if (true == textIsHtml) {
return (String) p.getContent();
} else {
return getPreString((String) p.getContent());
}
}
if (p.isMimeType("multipart/alternative")) {
// prefer html text over plain text
Multipart mp = (Multipart) p.getContent();
String text = null;
for (int i = 0; i < mp.getCount(); i++) {
Part bp = mp.getBodyPart(i);
if (bp.getContent() instanceof BASE64DecoderStream) {
return "現在 Base 64 のコンテンツには現在未対応です。";
} else if (bp.isMimeType("text/plain")) {
if (text == null) {
text = getPreString(getText(bp));
}
} else if (bp.isMimeType("text/html")) {
String s = getText(bp);
if (s != null) {
return s;
}
} else {
return getPreString(getText(bp));
}
}
return text;
} else if (p.isMimeType("multipart/*")) {
Multipart mp = (Multipart) p.getContent();
for (int i = 0; i < mp.getCount(); i++) {
String s = getText(mp.getBodyPart(i));
if (s != null) {
return s;
}
}
}
return null;
}
private String getPreString(String data) {
StringBuilder sb = new StringBuilder();
sb.append("<PRE style=\"font-size:12px;\">");
sb.append(data);
sb.append("</PRE>");
String s = sb.toString();
return s;
}
}
このユーティリティ・クラスでは getText(Part p) メソッドが実際の解析を行なっています。解析を行なった後、HTML は HTML として、テキストはテキストとして文字列を取りだし、最後に表示用の文字列を String で返しています。ユーティリティ・クラスから文字列が返ってくると、その値をそのまま並列タスクの戻り値として返します。
並列タスクの処理が完了すると <h:outputText value=”#{messageReceiveMgdBean.specifiedMessage}” > でその文字列を描画するために、戻り値を specifiedMessage に代入します。
以上により、大まかな実装の概要説明は終了です。
Ajax 通信の際のダイアログ表示方法
最後に、長時間の Ajax 通信時にステータス・ウィンドウを表示させる方法を紹介します。

Ajax リクエストを行なう際に、上記のステータスを表示するダイアログを表示させています。これは Ajax で長時間処理を要するような処理を行なう際にとても有用です。特に今回は IMAP サーバに直接接続を行いフォルダ一覧を取得したりメッセージを取得しているため、通常の DB アクセスよりもさらに時間を要すような処理を Ajax として実装しました。仮に上記のようなダイアログを使用しない場合、本当に Ajax のリクエストが実行されているのか否かわからなくなります。そこで、このような長時間処理用に、今回 PrimeFaces の下記のタグを使用して実装しました。
<p:ajaxStatus onstart="PF('statusDialog').show();" onsuccess="PF('statusDialog').hide();"/>
<p:dialog modal="true" widgetVar="statusDialog" header="処理中"
draggable="false" closable="false">
<p:graphicImage value="/resources/imgs/ajaxloadingbar.gif" />
</p:dialog>
基本的には、上記のコードを記述する事で全ての Ajax 通信時にステータス・ウィンドウが表示されるようになります。しかし、全ての処理で上記ダイアログを表示させたくない場合もあります。そのような場合、<p:ajax> タグの属性 global を false に設定する事で、その Ajax リクエストではダイアログを非表示にする事もできます。実際、私の場合は、テーブル中で特定のメッセージを選択した際、その対象メッセージをスクロール・パネルに表示させますが、その Ajax リクエストではダイアログを非表示にしています。
<p:dataTable id="mailheader" var="mheader"
paginator="true"
paginatorPosition="bottom"
value="#{messageReceiveMgdBean.mailHeaderModel}"
rows="5" rowKey=" #{mheader.messageCount}"
selection="#{messageReceiveMgdBean.selectedMailHeader}"
selectionMode="single" style="width:800px;font-size:10px;" >
<p:ajax event="rowSelect" listener="#{messageReceiveMgdBean.onMessageSelect}" update=":form:specifiedMsg" global="false"/>
※ご注意:全てのコンポーネントで global=false が利用できるわけではないようです。
このようにして、PrimeFaces のようなリッチな JSF コンポーネントを使用する事で、標準の JSF でもある程度簡単に、シングル・ページ・アプリケーションを構築する事ができます。如何でしょうか?是非この開発生産性の高い技術をお試しください。
最後に、
本アプリケーションは短時間で実装し、あくまでも JSF, WebSocket, JavaMail のサンプル・アプリケーションとして作成しました。そのため、実務レベルで使用するためには細かい部分で実装が足りていません。
例えばログイン時に毎回 IMAP サーバに接続し全情報を取得しますので、アプリケーションの動作としてとても遅いと感じるかもしれません。それはJSF ではなく、毎回 IMAP サーバに接続しに行っているため遅くなっています。また、一般的な IMAP のメールクライアントが実装しているような、ローカル・キャッシュを実装していません。毎回直接 IMAP サーバに参照に行っています。起動時に全画面の描画を高速にするためには、ローカル・キャッシュのデータを参照するなどが必要です。また各処理を並列処理で行なっていますが、タイミングに応じて異なるメッセージが選択される可能性もあります(注意点は GitHub のソースに記述しています)。また、セッション・タイムアウトに対する実装も今回は実装していません。ログイン認証も簡易実装しています。Java EE におけるログイン認証は「たかがレルムされどレルム GlassFish で始める詳細 JDBC レルム」のエントリをご参照ください。
ここで、ご紹介した内容の内、ご参考になる部分があれば幸いです。
たかがレルムされどレルム GlassFish で始める詳細 JDBC レルム
この記事は「 GlassFish Advent Calendar 2013」 の10日目として新たに書き下ろしたものです。昨日は蓮沼さんによる「GlassFish付属のJava DBについて」でした。
幸運にも、昨日 12/09 に蓮沼さんが GlassFish に付属している Java DB について詳しくご紹介してくださったので、今日はその流れ?!というわけではありませんが、GlassFish に付属の Java DB(他のDBにも利用できます) を使用して、Java EE 標準の認証・認可方法をご紹介します。
Realm (レルム) は古くから Java 標準の認証・認可を行なうための機能として多くの環境で利用されてきましたが、やや環境構築や設定が大変という事もあって、レルムを使用せず独自の認証システムを構築される事もいらっしゃるようです。しかし、Java EE コンテナが提供する認証・認可機能を正しく理解し設定やプログラミングを行なえば、より安全で柔軟なプログラムを簡単に実現できるようになります。
また、この認証・認可の機能は Java EE に含まれるフレーム・ワーク全体に対して適用が可能なため、今回の資料では動作確認に JavaServer Faces を利用してを行ないますが、JSFに限らず、JAX-RS でも EJB でも、その他のフレームワークでも全ての Java EE フレームワークに適用できますのでとても重要です。
非常に古くからある機能のため、既にご存知の方やご適用頂いている方も多いかと思いますが、「たかがレルムされどレルム」で Java EE の中でも重要な概念の一つと考えています。最近、Java EE を始めた方や、これから Java EE を始める方に、本ブログ・エントリが有用な情報になれば誠に幸いです。Web アプリケーションに対してどこからどのように認証・認可などの設定を進めればよいか分からない方はどうぞご覧ください。
本ブログエントリは、環境構築手順をできるだけ詳細にご紹介し、これさえ見れば JDBC レルムの環境構築がある程度できるという資料になるようにエントリを書いてみました。当初はブログのエントリ中に JDBC レルムに関する説明を書こうと考えましたが、非常に長い内容になりましたので、途中でブログで記載するのはあきらめ PDF で資料を作りなおしました。PDF であればダウンロードしプリント・アウトした後にゆっくりご確認頂けるので、よりよいかと想定しています。是非、本資料を有効活用をして頂ければ幸いです。
限られた時間の中で急ピッチで作成しましたので、手順を端折って実装した所もあります(例えばユーザ登録、削除の確認画面など)。しかし、全体的な設定手順や実装方法の概要は掴めるかと思いますので、ご参考ください。
本資料でご紹介したコードは下記にアップしております。コードだけを参照されたい場合は、下記へアクセスしてください。
https://github.com/yoshioterada/JDBC-Realm-Sample
※ 下記に公開する PDF 資料中のプロジェクト名、パッケージ名は適切ではありません。プロジェクト名、パッケージ名等は適宜ご修正ください。元々、次のブログ・エントリで記載する内容とマージして一つの資料としてまとめたかったのですが、時間の関係上とボリュームの関係で、本 PDF には JDBC レルムの部分までしか記載できませんでした。
そこで、JDBC Realm のハンズオン・ラボの資料としてはプロジェクト名、パッケージ名などが適切ではない名前が記載されています。上記 GitHub ではプロジェクト名、パッケージ名をリファクタリングした後のコードをアップしておりますので、併せてご参照ください。
最後に、認証や認可用の設定やコードを行なうと、jUnit 等のテスト連携が困難になると予想される方もいらっしゃるかもしれません。
EJB を実装されている場合には jUnit 等の単体テストで、組み込み可能 EJB コンテナを利用できます。
この組み込み可能な EJB コンテナで認証・認可まで含めたテストケースを実施したい場合、GlassFish では下記のクラスを利用する事で、EJB プログラムに対するログイン認証・認可の権限チェックを行なうことができるようになります。
GlassFish v3.x の場合:
$GLASSFISH_HOME\glassfish\modules
com.sun.appserv.security.ProgrammaticLogin
GlassFish v4.0 の場合 (パッケージ名が代わりました。)
$GLASSFISH_HOME\glassfish\modules\security-ee.jar
com.sun.enterprise.security.ee.auth.login.ProgrammaticLogin
例えば、jUnit のコード中で下記のようなコードを書く事ができます。
char[] passwd = “password”.toCharArray();
ProgrammaticLogin progLogin = new ProgrammaticLogin();
progLogin.login(“admin”, passwd , “jdbc-realm”, true);
最後に、今回は DBを使用しましたが、LDAPでも、SSO を実現するような製品群でもレルムは利用可能です。また少ない設定変更でレルムの切り替えももできますので、是非レルムをご理解いただきご利用ください。



