目次


JSF 2 の魅力

JSF ウィザード

JSF 2 と CDI を使ってウィザードを実装する

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: JSF 2 の魅力

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:JSF 2 の魅力

このシリーズの続きに乞うご期待。

Java EE (Java™ Enterprise Edition) 6 には、JSF 2 をはじめとした強力な技術が数多く含まれています。そうした技術の 1 つ、CDI (Contexts and Dependency Injection) は、さまざまな点で、これまで長年の間、他のフレームワークで温められてきた概念を標準化しています。

この記事では、JSF 2 と CDI を組み合わせて、オンライン・クイズ用のウィザードを実装する方法を紹介します。CDI が提供する依存性注入、プロデューサー・メソッド、そして対話スコープの 3 つすべてを使用して実装するこのウィザードは、多肢選択式のオンライン・クイズに簡単に使用できます。

この記事で取り上げる話題は CDI だけではありません。他にも以下の手法を説明します。

  • Facelets テンプレートによってコードを最小限にして、最大限に再利用する
  • ウィザードを Ajax 化し、より円滑なユーザー・エクスペリエンスを実現する
  • CDI の依存性注入によってコードを単純化する
  • CDI のプロデューサー・メソッドを実装して、Bean をビューの中でシームレスに使用する
  • CDI の対話スコープを利用して複数リクエストの使用ケースを実装する

この記事に記載するサンプルの完全なソース・コードは、ダウンロードできるようになっています。ダウンロード・リンク、そしてデプロイメント手順に関するアドバイスは、「サンプル・コードの実行」という囲み記事を参照してください。

クイズ・ウィザード

図 1 に実行中のクイズ・ウィザードを示します。

図 1. クイズ・ウィザード
クイズ・ウィザード
クイズ・ウィザード

このサンプル・アプリケーションで最初に表示されるのは、ウィザードを開始するリンク、<h:commandLink value="#{msgs.startWizard}" action="#{wizard.start}"/> だけです。リンクのテキスト「Start the wizard (ウィザードを開始する)」はプロパティー・ファイルから取得されます。このテキストの部分を表すのが、リンクの値に含まれる msgs.startWizard という表現です。国際化は 2004年頃の JSF 101 で対応しているので、ここで詳細を説明して皆さんをうんざりさせることはしません。アプリケーションは、全体がローカライズされているため、すべての文字列は messages.properties ファイルから取得されるとだけ言っておけば十分でしょう。

Start the wizard (ウィザードを開始する)」リンクからクイズ・ウィザード・ページにアクセスすると、質問が一度に 1 つずつ提示されます (図 1 の下の 2 つの画面を参照)。ウィザードが表示するボタンを有効化するかどうかを制御するために使用しているのは、単純な Ajax とサーバー・サイドの Bean です。制御の仕組みについては、この記事の「Ajax」のセクションで説明します。

図 2 に、最後の質問を提示するページと、ユーザーの回答をまとめたページを示します。「Finish (終了)」ボタンは、最後の質問のときにだけ有効になります。このボタンをクリックすると、回答をまとめたページが表示されます。

図 2. ユーザーの回答をまとめたページ
ユーザーの回答をまとめた、ウィザードのページ
ユーザーの回答をまとめた、ウィザードのページ

クイズ・ウィザードが機能する仕組みは以上のとおりです。ここからは、このウィザードの実装方法を説明していきます。

クイズ・アプリケーション

図 3 にクイズ・アプリケーションを構成するファイルを示します。

図 3. アプリケーションのファイル
アプリケーションのディレクトリー

私はこのクイズ・ウィザードを、JSF 2 のテンプレート (/templates/wizardTemplate.xhtml) を使って実装しました。このテンプレートは、ウィザードのビュー (/quizWizard/wizard.xhtml) が使用します。

テンプレートとビューに加え、ウィザードの各構成部分には Faceletsを使用しています (すべて、quizWizard ディレクトリーに置かれています)。

  • 見出し (/quizWizard/heading.xhtml)
  • 質問 (/quizWizard/question.xhtml)
  • ラジオ・ボタン (quizWizard/choices.xhtml)
  • 「Next (次へ)」、「Previous (戻る)」、および「Finish (終了)」ボタン (quizWizard/controls.xhtml)

index.xhtml は、アプリケーションを開始して「Start the wizard (ウィザードを開始する)」リンクを表示する Facelets です。done.xhtml Facelets は、質問と回答をまとめたページを表示します。

クライアントについての説明は以上です。サーバー・サイドでは、アプリケーションは 3 つの Bean を使用します。次のセクションではまず、そのうちの 2 つについて説明します。

アプリケーションの質問用 Bean

Question Bean (リスト 1 を参照) は、実際には質問、回答の選択肢一式、そして回答で構成されます。

リスト 1. Question Bean
package com.clarity;

import java.io.Serializable;

public class Question implements Serializable {
  private static final long serialVersionUID = 1284490087332362658L;

  private String question, answer;
  private String[] choices;
  private boolean answered = false; // next button is enabled when answered is true
  
  public Question(String question, String[] choices) {
    this.question = question;
    this.choices = choices;
  }

  public void setAnswer(String answer) {
    this.answer = answer;
    answered = true;
  }

  public String getAnswer()    { return answer;   }
  public String getQuestion()  { return question; }
  public String[] getChoices() { return choices;  }
  public boolean isAnswered()  { return answered; }

  public void setAnswered(boolean answered) { this.answered = answered; }  
}

このアプリケーションはまた、Questions クラス (リスト 2 を参照) に質問の配列を保持します。

リスト 2. Questions Bean
package com.clarity;

import java.io.Serializable;

import com.corejsf.util.Messages;

public class Questions implements Serializable {
  private static final long serialVersionUID = -7148843668107920897L;

  private String question;
  private Question[] questions = {      
    new Question(
       Messages.getString("com.clarity.messages", "expandQuestion", null),
       new String[] { 
         Messages.getString("com.clarity.messages", "hydrogen", null),
         Messages.getString("com.clarity.messages", "helium", null),
         Messages.getString("com.clarity.messages", "water", null),
         Messages.getString("com.clarity.messages", "asphalt", null)
       }),
       
   new Question(
       Messages.getString("com.clarity.messages", "waterSGQuestion", null),
       new String[] { 
         Messages.getString("com.clarity.messages", "onedotoh", null),
         Messages.getString("com.clarity.messages", "twodotoh", null),
         Messages.getString("com.clarity.messages", "onehundred", null),
         Messages.getString("com.clarity.messages", "onethousand", null)
       }),
       
   new Question(
       Messages.getString("com.clarity.messages", "numThermoLawsQuestion", null),
       new String[] { 
         Messages.getString("com.clarity.messages", "one", null),
         Messages.getString("com.clarity.messages", "three", null),
         Messages.getString("com.clarity.messages", "five", null),
         Messages.getString("com.clarity.messages", "ten", null)
       }),
       
   new Question(
       Messages.getString("com.clarity.messages", "closestSunQuestion", null),
       new String[] { 
         Messages.getString("com.clarity.messages", "venus", null),
         Messages.getString("com.clarity.messages", "mercury", null),
         Messages.getString("com.clarity.messages", "mars", null),
         Messages.getString("com.clarity.messages", "earth", null)
       })         
  };
  
  public int size()                        { return questions.length; }
  public String getQuestion()              { return question; }
  public void setQuestion(String question) { this.question = question; }
  public Question[] getQuestions()         { return questions; }
}

リスト 1リスト 2 は何の変哲もない内容で、単にサーバー上の質問のリストを提示しているだけです。ただし、ヘルパー・メソッドを使用してプログラムによって文字列をリソース・バンドルから抽出しているという点は注目に値します。このヘルパー・メソッドがどのように機能するかについては、記事のコードをダウンロードして確認してください。また、『Core JavaServer Faces』(「参考文献」を参照) でもヘルパー・メソッドについて説明しています。

アプリケーションの 3 つの Bean のうち、2 つに関する内容はこれだけですが、残りの Wizard Bean はウィザードのコントローラーとして機能します。このアプリケーションで興味深い Java コードがあるのは、唯一、この Bean の中だけです。Wizard Bean については、「CDI: 依存性注入と対話」のセクションで説明します。

アプリケーションを構成するファイルと質問用 Bean について大体の感覚がつかめたところで、次はウィザードのビューを実装する方法に話題を移します。

テンプレートとビュー

ほとんどのウィザードは、図 4 に示す共通の構造を持っていると考えて問題ありません。

図 4. ウィザードの構造
クイズ・ウィザードのボタン
クイズ・ウィザードのボタン

リスト 3 に、この構造をカプセル化するテンプレートを記載します。

リスト 3. ウィザードのテンプレート (templates/wizardTemplate.xhtml)
<!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://java.sun.com/jsf/html"
    xmlns:ui="http://java.sun.com/jsf/facelets">

  <h:head>
    <title>
      <ui:insert name="windowTitle">
        #{msgs.windowTitle}
      </ui:insert>
    </title>
  </h:head>
  
  <h:body>  
    <h:outputStylesheet library="css" name="styles.css" target="head"/>       
    
    <ui:insert name="heading"/>
          
    <div class="wizardPanel">
    
      <div class="subheading">
        <ui:insert name="subheading"/>
      </div>
      
       <div class="work">
         <ui:insert name="work"/>
       </div>
       
      <div class="controls">
        <ui:insert name="controls"/>
      </div>
      
    </div>      
        
  </h:body>
</html>

リスト 4 に、クイズ・ウィザードに特有の実装を記載します。

リスト 4. ウィザードの Facelets (quizWizard/wizard.xhtml)
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
     xmlns:h="http://java.sun.com/jsf/html"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    template="/templates/wizardTemplate.xhtml">

  <ui:define name="heading">
    <ui:include src="heading.xhtml"/>
  </ui:define> 
  
  <ui:define name="subheading">
    <ui:include src="question.xhtml"/>
  </ui:define>
  
  <ui:define name="work">
    <ui:include src="choices.xhtml"/>
  </ui:define>
   
  <ui:define name="controls">
    <ui:include src="controls.xhtml"/>
  </ui:define> 

</ui:composition>

テンプレートはかなり単純で、ビューが定義するページのセクションを挿入するだけです。このアプリケーションの場合、リスト 3 のテンプレートはリスト 4 のビューが定義する headingsubheadingwork、および controls セクションを挿入します。さまざまなビューに共通する機能をテンプレートにカプセル化することで、新しいビュー (このアプリケーションの場合は、新しいタイプのウィザード) を作成しやすくなります。

リスト 5 に、クイズ・ウィザードの heading セクションを記載します。

リスト 5. heading (/quizWizard/heading.xhtml)
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
     xmlns:h="http://java.sun.com/jsf/html"
    xmlns:ui="http://java.sun.com/jsf/facelets">

      <div class="heading">
        #{msgs.quizTitle}
      </div>

</ui:composition>

リスト 6 は、subheading セクションです。

リスト 6. subheading (quizWizard/question.xhtml)
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<ui:composition xmlns="http://www.w3.or g/1999/xhtml"
     xmlns:f="http://java.sun.com/jsf/core"
     xmlns:h="http://java.sun.com/jsf/html"
    xmlns:ui="http://java.sun.com/jsf/facelets">

  <h:panelGrid columns="1" id="question">
#{wizard.cursor+1}. #{questions[wizard.cursor].question}?
  </h:panelGrid>
    
</ui:composition>

リスト 5heading はクイズのタイトル (上記の例では Science Quiz) を表示し、リスト 6subheading が質問を提示します。リスト 6 で参照されている wizard.cursor は、現在の質問を指すカーソル (または、索引でも構いません) です。このカーソルはゼロを基準とします。したがって、#{wizard.cursor+1} が質問番号を表示し、#{questions[wizard.cursor].question} が質問の内容となります。

これで、サーバー・サイドの Bean やテンプレート機能などの事前準備は整いました。ここからが、いよいよ興味深い内容です。以降のセクションでは、ウィザードの Ajax を実装する方法、そしてウィザードが CDI を使用する方法を説明します。まずは Ajax から取り上げます。

Ajax

クイズ・ウィザードでのユーザー操作はすべて Ajax 呼び出しという結果となり、呼び出しに対するレスポンスが返ってきた時点で、ページの該当するセクションだけがレンダリングされます。この Ajax 呼び出しの役割は唯一、ウィザードのボタンの有効化状態を制御することだけです。図 5 に、最初の質問と 2 番目の質問のそれぞれでウィザードのボタンが有効になる様子を示します。

図 5. クイズ・ウィザードのボタン
クイズ・ウィザードのボタン
クイズ・ウィザードのボタン

ウィザードの Ajax は、2 つの Facelets ファイルに簡潔にカプセル化されています。リスト 7 にそのうちの 1 つ、choices.xhtml を記載します。

リスト 7. choices.xhtml
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
     xmlns:f="http://java.sun.com/jsf/core"
     xmlns:h="http://java.sun.com/jsf/html"
    xmlns:ui="http://java.sun.com/jsf/facelets">
       
  <h:form id="choices">
    <h:panelGrid columns="2">  
        <h:selectOneRadio value="#{questions[wizard.cursor].answer}"
                         layout="pageDirection">
          <f:selectItems value="#{questions[wizard.cursor].choices}"/>
<f:ajax render=":buttons"/>
        </h:selectOneRadio>
     </h:panelGrid>
  </h:form> 
    
</ui:composition>

ユーザーがラジオ・ボタンを選択すると、JSF がサーバーに対して Ajax 呼び出しを行い、ラジオ・ボタンの選択項目 (質問に対する回答) を Backing Bean のプロパティーに記録します。Ajax 呼び出しに対するレスポンスが返ってくると、JSF がウィザードのボタンを更新します。

リスト 8 に controls.xhtml を記載します。

リスト 8. controls.xhtml
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
     xmlns:f="http://java.sun.com/jsf/core"
     xmlns:h="http://java.sun.com/jsf/html"
    xmlns:ui="http://java.sun.com/jsf/facelets">

    <h:form id="buttons">
   
        <h:panelGrid columns="4" styleClass="wizardControls">
<f:ajax render=":question :choices buttons">

            <h:commandButton id="next" 
                        styleClass="wizardButton"
                             value="#{msgs.nextButtonText}" 
                          disabled="#{not wizard.nextButtonEnabled}"/> 
                    actionListener="#{wizard.nextButtonClicked}"/>
      
            <h:commandButton id="previous"
                        styleClass="wizardButton"
                             value="#{msgs.previousButtonText}" 
                          disabled="#{not wizard.previousButtonEnabled}"
                    actionListener="#{wizard.previousButtonClicked}"/>
</f:ajax>
                                                              
        <h:commandButton id="finish"
                    styleClass="wizardButton"
                         value="#{msgs.finishButtonText}" 
                      disabled="#{not wizard.finishButtonEnabled}"
                        action="#{wizard.end}"/>
                                                                      
        </h:panelGrid>
        
    </h:form>
</ui:composition>

ユーザーが「Next (次へ)」ボタンまたは「Previous (戻る)」ボタンをクリックした場合にも JSF はサーバーに対して Ajax 呼び出しを行い、呼び出しに対するレスポンスが返ってきた時点で、質問、質問の選択肢 (ラジオ・ボタン)、そしてボタン自体を更新します。

「Finish (終了)」ボタンは Ajax ボタンではありません。このボタンをクリックした場合には、「done」ページにナビゲートするからです。

リスト 7リスト 8 では wizard Bean を何度も参照していることに注目してください。この Bean が、クイズ・ウィザードの事実上のコントローラーとなっている Bean です。この Bean について説明して、今回の記事を締めくくりたいと思います。

CDI: 依存性注入と対話

CDI はいわば、強化された JSF Managed Bean といえます。Java EE 6 のコンポーネントとしての CDI は、長年の間に Spring で温められてきた依存性注入やインターセプターなどといった概念のさまざまな点を標準化しています。実際、CDI と Spring 3 とでは、数多くの類似する機能を共有しています。

CDI を使用することで、関心対象を「疎結合」によるものと「強い型付け」によるものとで分離することができます。こうすることより、CDI はオブジェクトのインスタンス化やそのライフタイムの管理といった Java プログラミングでの決まり切った細かい作業からの解放をほぼ完全に実現します。

JSF の観点から見ると、CDI が持つとりわけ魅力的な機能は対話スコープです。Seam によって開発された対話スコープは、プログラムによってライフタイムが管理されたスコープであり、リクエストとセッションの間での厳格なオール・オア・ナッシングの選択から解放させてくれます。

クイズ・ウィザードが CDI を使用しているのは、Wizard Bean の中だけです (リスト 9 を参照)。

リスト 9. Wizard Bean
package com.clarity;

import java.io.Serializable;

import javax.enterprise.context.Conversation;
import javax.enterprise.context.ConversationScoped;
import javax.enterprise.inject.Produces;
import javax.faces.event.ActionEvent;
import javax.inject.Inject;
import javax.inject.Named;

@Named()
@ConversationScoped()
public class Wizard implements Serializable {
  private static final long serialVersionUID = 1L;
  private Questions questions = new Questions();
  private int cursor = 0;
  
@Inject
  private Conversation conversation;@Produces @Named
  public Question[] getQuestions() {
    return questions.getQuestions();
  }
  
  public void nextButtonClicked(ActionEvent e) {
    incrementCursor();
  }

  public void previousButtonClicked(ActionEvent e) {
    decrementCursor();
  }
    
  public void incrementCursor() { ++cursor; }
  public void decrementCursor() { --cursor; }
  public int  getCursor()       { return cursor; }
  public void resetCursor()     { cursor = 0; }

  public boolean getNextButtonEnabled() {
    return cursor != questions.size() - 1 &
    (questions.getQuestions())[cursor].isAnswered();
  }
  
  public boolean getPreviousButtonEnabled() {
    return cursor > 0;
  }
  
  public boolean getFinishButtonEnabled() {
    return cursor == questions.size() - 1 &
    (questions.getQuestions())[cursor].isAnswered();
  }
  
  public String start() {
conversation.begin();
    return "quizWizard/wizard";
  }
  
  public String end() {
conversation.end();
    return "/done";
  }
  
  private void setCurrentQuestionUnanswered() {
    Question currentQuestion = (questions.getQuestions())[cursor];
    currentQuestion.setAnswered(false);    
  }
}

クイズ・ウィザード・アプリケーションで興味深いコードのほぼすべては、上記のリスト 9 に含まれています。まず、この Wizard Bean には前のセクションで説明したように、ウィザードのボタンの有効化状態を制御するメソッドがあります。また、ユーザーが「Next (次へ)」ボタンまたは「Previous (戻る)」ボタンをクリックすると JSF によって呼び出されるメソッドもあります。この 2 つのメソッドにより、次の質問に進む操作、前の質問に戻る操作がそれぞれ行われます。

一方、表向きで Wizard Bean に関して最も興味深いところは、その CDI の使用方法です。まず、この連載で一貫してそうしてきたように、ここでは CDI の @ManagedBean アノテーションの実装ではなく、(JSR 330, Dependency Injection for Java で実際に定義されている) @Named アノテーションの実装を使用しています。どちらのアノテーションも、JSF 式言語からアクセス可能なスコープ Bean を作成しますが、CDI の Managed Bean のほうが遥かに高度な機能を持っています。したがって、Java EE 6 対応のサーバーを使用している場合には @ManagedBean よりも @Named を優先して使用してください。

リスト 6リスト 7 をよく見てみると、questions という名前の Bean に JSF 式言語でアクセスしていることがわかります。覚えているかもしれませんが、リスト 2 でも Questions クラスを実装しました。けれどもリスト 2@Named アノテーションは見当たりません。通常は、アノテーションがないとエラーが発生しますが、この場合の questions Bean は別の場所で生成されて提供されます。その場所とは、Wizard.getQuestions() メソッドです。このメソッドには @Produces アノテーションが付けられています。つまり、式言語で Questions Bean が参照されると、JSF は Wizard.getQuestions() メソッドを呼び出してこの Questions Bean を取得することを意味します。

続いて、Wizard Bean が対話スコープを使用している部分があります。アプリケーションのウェルカム・ページにある「Start the wizard (ウィザードを開始する)」リンクは、Wizard Beanの start() メソッドに接続されています。このメソッドは、conversationbegin() メソッドを呼び出すことによって対話を開始し、現行のリクエスト (実際には、1 つのリクエストの間だけ存続する対話) を長時間実行される対話にプロモートします。この対話は、タイムアウトになるか、あるいは conversationend() メソッドが呼び出されるまでは終了しません。Wizard には Conversation スコープを指定したので、そのライフタイムは対話が終了するまでです。

もちろん、対話スコープを使わずに、ユーザーのセッション内で独自の擬似対話スコープを実装するという方法もあります。実のところ、対話スコープが開発される前は、多くの開発者がアプリケーションで複数リクエストの使用ケースの状態を維持するために、この方法を使っていました。この手動による処理を、CDI が引き受けているというわけです。

最後に注目してもらいたいのは、私は CDI 注入を使用して対話を Managed Bean に注入しているため、プログラムによって対話を開始、終了できるという点です。リソース注入により、オブジェクトを作成してそのライフタイムを管理するという、いつもの細かい作業ではなく、オブジェクトを使った操作に専念することができます。

まとめ

この記事では Ajax ウィザード、テンプレート、依存性注入、対話スコープといった多岐にわたる内容を、驚くほどわずかなコードで処理しました。JSF 2 と CDI を利用することによって、堅牢で再利用可能な Web アプリケーションを最小限の作業で実装することができ、最大限の柔軟性と再利用性が実現されます。

連載「JSF 2 の魅力」は、この夏の間はお休みに入ります。秋になったら、JSF のスキル上達に役立つ盛りだくさんの内容で連載を再開する予定です。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology, Web development
ArticleID=503048
ArticleTitle=JSF 2 の魅力: JSF ウィザード
publish-date=07062010