Posts tagged ‘WebSocket’

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 ならばその文字列をそのまま返し、プレイン・テキストの場合、&lt: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 レルム」のエントリをご参照ください。

ここで、ご紹介した内容の内、ご参考になる部分があれば幸いです。

2013年12月11日 at 2:55 AM 2件のコメント

Java EE 7 HoL on JJUG CCC


2013 年 11 月 9 日に JJUG CCC 2013 Fallベルサール西新宿で開催されますが、13:15 – 15:05 まで「R5-1 Java EEハンズオン」を実施します。今日はそのハンズオンで実施する内容についてご紹介します。

2013年11月 11日追記:JJUG CCC で実施したハンズオンの資料を下記に公開しました。また、本プロジェクトの全ソースコードは下記より参照できます。
https://github.com/yoshioterada/JavaEE7-HoL/
HoL の資料は Step by Step で記載したためページ数が多いですが、実装コード量はとても少ないです。

本 HoL では Java EE 7 に含まれる技術だけを使ってリアルタイムの情報配信をするアプリケーションを作成します。WebSocket のサンプルというとチャットのようなアプリケーションは数多くありますが、WebSocket はアイディア次第でとてもおもしろい物を作れるだけでなく、実際にビジネスで即使えるような物も作れます。

ただ、WebSocket を本番環境で大規模に扱うためには負荷も考慮しなければなりません。基本的には Java EE 7 に準拠したアプリケーション・サーバ1台で WebSocket アプリケーションを動作させた場合、そのサーバに接続するクライアントにのみしか状況発信、共有などができないため、おのずと接続数に限界が生じます。実際には接続数ではなく、情報配信の部分がボトルネックとなり接続数を制限せざるおえない状況になるでしょう。

それでは、単純に台数を増やしてクラスタ構成を組めば良いと考えるかもしれませんがそんなに簡単にはいきません。なぜならば JSR-356 の WebSocket ではサーバ・エンドポイントに接続するクライアントの管理は自身でする事になっており、インスタンスをまたいで接続しているクライアントの情報を管理するのはとても大変だからです。
例えば、Collection に WebSocket のクライアント・エンドポイントの情報(全クライアント情報)を入れたとして、その情報をインスタンス間で交換するのはデータ量も多くナンセンスです。また、ある一つのインスタンスで受信したメッセージを他の全インスタンスに共有させるのも、インスタンスの増加の度にコードを修正しなければならなくなるので現実的ではありません。

そこでこうした WebSocket のクラスタリングを実現するために、外部のメッセージ・プロバイダ(MQ 関連製品)を使用します。メッセージ・プロバイダにメッセージをキューイングし、アプリケーション・サーバの各インスタンスがキューを監視していれば、キューにデータが入ってきた時点でそのメッセージを取り出し処理をする事ができます。

実際には、内部的には JMS の Publish-Subscriber を利用しています。Web アプリケーションで1人が JMS のトピックに対して配信したメッセージを、各インスタンス上で稼働する MDB が同一トピックをサブスクライブしており、メッセージが入ってきた事を検知した後、全 WebSocket クライアント・エンドポイントに対してメッセージを配信を行います。



また、Web アプリケーションと WebSocket 側の実装パッケージをを分ける事によって、上記の概念図に示すように、よりセキュアに情報発信側と受信側を分けて運用する事もできるようになります。だれでもが WebSocket サーバに接続する全クライアントに情報配信ができようなシステムは危険ですよね。

そこで、今回は上記のような概念図を念頭に、簡単なアプリケーションを構築し、GlassFish のクラスタリング構成を作成します。実際にクラスタ環境で WebSocket を動かしている動画を下記に示します。
※ 今回は Load Balancer や Firewall の部分は対象範囲外とします。

最後に、
今回のハンズオンは少し応用するだけで、スポーツ・ニュースの配信サイトで行われているようなリアルタイムの試合経過速報等をとても簡単にさらに効率良く提供する事ができるようになります。またそれ以外にもお客様にリアルタイムの情報をいち早くお届けしたいニーズがあればすぐにご利用いただけるでしょう。
今回このようなアプリケーションを作成する HoL を実施しますので楽しみにしておいてください。
また HoL を元に大規模 WebSocket リアルタイム情報配信を是非ご検討ください。

HoL で必要なのは事前準備としては JDK 7 u45 以降のインストールと NetBeans 7.4 (GlassFish バンドル版)以降のインストールをしていただくだけです。それ以外は一切必要ございません。
それぞれ、下記より御入手ください。
※ Java SE 8 の HoL も両方参加される方は JDK 7,8 共にインストールが必要ですが、本 HoL では JDK 7 を使用して行います。

● http://www.oracle.com/technetwork/java/javase/downloads/index.html
● https://netbeans.org/downloads/index.html 
 (Java EE : 203 MB版 もしくは、すべて : 220 MB 版)

※ さて、これから説明、配布用の資料を作成しよう。(^_^;)

2013年11月 11日追記:JJUG CCC で実施したハンズオンの資料を下記に公開しました。

2013年10月23日 at 3:53 PM 1件のコメント

ご注意(NOTE): WebSocket with Concurrency for EE


Java EE 7 のリリースが控えておりますが、先日 Java Day Tokyo で私のセッションの中でデモした WebSocket (JSR-356) と Concurrency Utilities for EE (JSR-236) を組み合わせたデモのコードについてご紹介すると共に、実装時の注意点をご報告いたします。

下記は、実際に私が WebSocket (JSR-356) と Concurrency Utilities for EE (JSR-236) を組み合わせたコードを実装する際にハマった内容をお届けします。
コードは下記のようなコードを記載しています。

package jp.co.oracle.websocket;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
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.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import jp.co.oracle.tasks.WebSocketAIRSearchTask;
import jp.co.oracle.tasks.WebSocketHotelSearchTask;

@ServerEndpoint(value = "/asyncResult")
public class AsyncResultWebSocketEndpoint {

    private static final Logger logger = Logger.getLogger(AsyncResultWebSocketEndpoint.class.getPackage().getName());

    // Concurrency Utilities for EE の ManagedExecutorService をインジェクト
    @Resource(name = "concurrent/DefaultManagedExecutorService")
    ManagedExecutorService managedExecsvc;

    // WebSocket のコネクションがオープンした際の処理
    @OnOpen
    public void initOpen(Session session) {
        executeTasks(session);
    }

    // WebSocket クライアントからメッセージを受信した際の処理
    @OnMessage
    public void receivedMessage(String message, Session session) {
        if (!message.equals("re-execute")) {
            return;
        }
        executeTasks(session);
    }

    // 実際の処理内容
    private void executeTasks(Session session) {
        // 複数タスクの実行の際に終わった順に処理結果を取り出す
        ExecutorCompletionService<String> execCompService = new ExecutorCompletionService<>(managedExecsvc);

       // 複数タスクの登録
        List<Future<String>> futures = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
                WebSocketHotelSearchTask task = new WebSocketHotelSearchTask(i);
                futures.add(execCompService.submit(task));
        }
        try {
            // 終了したタスクの順番に処理結果を取得し
            // 処理結果を WebSocket のクライアント・エンドポイント
           // に対して処理結果を送信
            for (Future<String> results : futures) {
                String resultString = execCompService.take().get();
                session.getBasicRemote().sendText(resultString);
            }
        } catch (IOException | InterruptedException | ExecutionException ex) {
            logger.log(Level.SEVERE, null, ex);
        }
    }
}

ダミーのタスク

package jp.co.oracle.tasks;

import java.util.concurrent.Callable;
import java.util.logging.Level;
import java.util.logging.Logger;

public class WebSocketHotelSearchTask implements Callable<String> {

    private static final Logger logger = Logger.getLogger(WebSocketHotelSearchTask.class.getPackage().getName());
    private int counter;

    public WebSocketHotelSearchTask(int counter) {
        this.counter = counter;
    }

    // タスクの処理内容によっては時間のかかるタスクもあるため
 // 半分のタスクをわざと 4 秒待たせ、タスクの登録順にタスクが
    // 完了しないように作成
    @Override
    public String call() {
        String result = "";
        if (counter % 2 == 1) {
                Thread.sleep(4000);
         }
            result = "ホテル検索タスク完了 : 終了タスクの ID" + counter;;
        } catch (InterruptedException ex) {
            logger.log(Level.SEVERE, null, ex);
        }
        return result;
    }
}

今回、なぜ下記のようにタスクの一括登録を行った後に、タスクの処理が終わった順に WebSocket のクライアント・エンドポイントに対してメッセージ配信を行うコードを実装したかというと(つまり、タスクの処理コード中から WebSocket のクライアントに対してメッセージ配信をしていない)、WebSocket 側のスレッドの制限があったためです。

       // 複数タスクの登録
        List<Future<String>> futures = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
                WebSocketHotelSearchTask task = new WebSocketHotelSearchTask(i);
                futures.add(execCompService.submit(task));
        }
        try {
            // 終了したタスクの順番に処理結果を取得し
            // 処理結果を WebSocket のクライアント・エンドポイント
           // に対して処理結果を送信
            for (Future<String> results : futures) {
                String resultString = execCompService.take().get();
                session.getBasicRemote().sendText(resultString);
            }

当初、下記のように Runnable (Callable の call() 中で実装も可)のインタフェースを実装したタスクを作成し、タスクの処理の中で WebSocket のエンドポイントに対してメッセージを配信するコードを記載しました。例えば、下記のような感じです。

public class SomeMyTask implements Runnable{
   Session session;
   public SomeMyTask(Session session){
       this.session = session;
   }

    @Override
     void run(){
           // 何らかの処理を実施
           // タスクの処理の最後に、WebSocket のクライアント・エンドポイント
           // に対してメッセージを配信
           session.getBasicRemote().sendText(resultString);
     }
}

そして、下記のコードを書いてタスクを実行しました。

    for( int  i = 0 ; i < 10 ; i++ ){
        SomeMyTask task = new SomeMyTask(session);
        managedExecsvc.submit(task);
    }

すると下記の例外が発生しました。

例外の出力内容:

java.lang.IllegalStateException: HeapBuffer has already been disposed
at org.glassfish.grizzly.memory.HeapBuffer.checkDispose(HeapBuffer.java:802)
at org.glassfish.grizzly.memory.HeapBuffer.position(HeapBuffer.java:188)
at org.glassfish.grizzly.nio.transport.TCPNIOAsyncQueueWriter.fillByteBuffer(TCPNIOAsyncQueueWriter.java:194)
at org.glassfish.grizzly.nio.transport.TCPNIOAsyncQueueWriter.writeComposite0(TCPNIOAsyncQueueWriter.java:151)
at org.glassfish.grizzly.nio.transport.TCPNIOAsyncQueueWriter.write0(TCPNIOAsyncQueueWriter.java:80)
at org.glassfish.grizzly.nio.AbstractNIOAsyncQueueWriter.processAsync(AbstractNIOAsyncQueueWriter.java:458)
at org.glassfish.grizzly.filterchain.DefaultFilterChain.process(DefaultFilterChain.java:110)
at org.glassfish.grizzly.ProcessorExecutor.execute(ProcessorExecutor.java:77)
at org.glassfish.grizzly.nio.transport.TCPNIOTransport.fireIOEvent(TCPNIOTransport.java:838)
at org.glassfish.grizzly.strategies.AbstractIOStrategy.fireIOEvent(AbstractIOStrategy.java:113)
at org.glassfish.grizzly.strategies.WorkerThreadIOStrategy.run0(WorkerThreadIOStrategy.java:115)
at org.glassfish.grizzly.strategies.WorkerThreadIOStrategy.access$100(WorkerThreadIOStrategy.java:55)
at org.glassfish.grizzly.strategies.WorkerThreadIOStrategy$WorkerThreadRunnable.run(WorkerThreadIOStrategy.java:135)
at org.glassfish.grizzly.threadpool.AbstractThreadPool$Worker.doWork(AbstractThreadPool.java:564)
at org.glassfish.grizzly.threadpool.AbstractThreadPool$Worker.run(AbstractThreadPool.java:544)
at java.lang.Thread.run(Thread.java:722)

なぜだろうと、Grizzly のソースコードを追ってみると、既にヒープが開放されてしまっているようです。

800     protected final void  [More ...] checkDispose() {
801         if (heap == null) {
802       throw new IllegalStateException(
803                     "HeapBuffer has already been disposed",
804                     disposeStackTrace);
805         }
806     }

当初バグかとも思ったのですが念のため、WebSocket (JSR-356) 仕様のスレッド関連部分をチェックしてみました。すると下記の 5.1 に WebSocket に関するスレッドの注意書きが記載されておりました。

5.1 Threading Considerations
Implementations of the WebSocket API may employ a variety of threading strategies in order to provide a scalable implementation. The specification aims to allow a range of strategies. However, the implementation must fulfill certain threading requirements in order to provide the developer a consistent threading environment for their applications.

Unless backed by a Java EE component with a different lifecycle (See Chapter 7), the container must use a unique instance of the endpoint per peer. [WSC-5.1-1]

In all cases, the implementation must not invoke an endpoint instance with more than one thread per peer at a time. [WSC-5.1-2]
(※ まさにここに記載されており、同時にピア毎に1つ以上のスレッドからエンドポイントのインスタンスを呼び出してはならない。)
と記載されておりました。

The implementation may not invoke the close method on an endpoint until after the open method has completed. [WSC-5.1-3]

This guarantees that a websocket endpoint instance is never called by more than one container thread at a time per peer. [WSC-5.1-4]

If the implementation decides to process an incoming message in parts, it must ensure that the corresponding onMessage() calls are called sequentially, and do not interleave either with parts of the same message or with other messages [WSC-5.1.5].

つまり、タスクの実行自身は複数のスレッドで実行できるのですが、WebSocket のクライアント・エンドポイントへのメッセージ送信は1箇所にまとめて実装しなければならない事に気付き上記のようなコードを書いています。

個人的には、マルチスレッドから WebSocket のクライアント・エンドポイントにメッセージ送信ができるようになるとより便利になるのではないかと思いますが、皆様如何でしょう? もちろん、既に仕様は FIX して Java EE 7 のリリース時点では無理ですし、Grizzly 等サーバ側の実装も今のままだと難しい部分があるかもしれません。しかし、皆様で声を上げていけば、時期 Java EE 8 の WebSocket 1.1 当たりで、マルチスレッドからのメッセージ送信もできるかも?!しれないので、賛同して頂ける方がいらっしゃったら、まとめて報告したいなと思っております。
(仕様上ダメって断られる可能性ももちろんありますが。(^_^;))

でも、昔と違って今の Java はこういった事がスペック・リードやエキスパート・グループメンバーに伝えやすい環境なんですよ!!
JJUG として Adopt-A-JSR プログラムに参加し、日本から改善要望なども出していけるといいですね。

2013年5月23日 at 4:34 PM


Java Champion & Evangelist

ご注意

このエントリは個人の見解であり、所属する会社の公式見解ではありません

カレンダー

2017年2月
« 10月    
 12345
6789101112
13141516171819
20212223242526
2728  

カテゴリー

Twitter

clustermap

ブログ統計情報

  • 921,972 hits

Feeds