Содержание


JSF 2 fu

Cоздание мастера JSF

Создайте свой собственный мастер средствами JSF 2 и CDI

Comments

Серия контента:

Этот контент является частью # из серии # статей: JSF 2 fu

Следите за выходом новых статей этой серии.

Этот контент является частью серии:JSF 2 fu

Следите за выходом новых статей этой серии.

JSF 2 – одна из многочисленных мощных технологий, реализованных в Java™ Enterprise Edition (Java EE) 6. В число других таких технологий входит технология Contexts and Dependency Injection (CDI), которая в определенной степени стандартизует концепции, разработка которых велась в течение многих лет в других программных инфраструктурах.

В этой статье я расскажу вам, как с помощью JSF 2 и CDI создать мастер для онлайновых опросов. Мы воспользуемся такими свойствами CDI, как инъекции зависимостей (dependency injection), producer-методы и контекст последовательности взаимодействий (conversation scope), и создадим с их помощью мастер, который в дальнейшем можно будет использовать для реализации онлайновых опросов и викторин.

Помимо технологии CDI, в этой статье мы рассмотрим следующие вопросы:

  • Использование фейслет-шаблонов для минимизации кода и увеличения возможностей повторного использования
  • Использование в мастере технологий Ajax для удобства работы пользователей
  • Использование инъекций зависимостей CDI для упрощения кода
  • Реализация producer-методов CDI для органичного встраивания bean-компонентов в виды приложения
  • Использование контекста последовательности взаимодействий CDI для реализации приложений с множественными запросами.

Полный исходный код примеров, используемых в этой статье, доступен для скачивания. Во врезке Выполнение примеров кода вы найдете ссылку для загрузки кода и инструкции по его установке.

Мастер опроса

На рисунке 1 показан пример работающего мастера опроса:

Рисунок 1. Мастер опроса
Quiz wizard
Quiz wizard

Сначала приложение отображает всего одну ссылку. Щелчок на ссылке активизируем мастер опроса: <h:commandLink value="#{msgs.startWizard}" action="#{wizard.start}"/>. Текст ссылки (Start the wizard) подгружается из файла свойств и представлен выражением msgs.startWizard в значении ссылки. Вопросы интернационализации относятся к самым азам JSF, так что я не буду забивать вам голову всеми деталями. Достаточно сказать, что приложение полностью локализовано, так что все текстовые строки загружаются из файла messages.properties.

Ссылка Start the wizard перенаправляет пользователя на страницу мастера опроса. В свою очередь, мастер предлагает пользователю ответить на ряд вопросов (по одному вопросу в каждом окне – см. рисунок 1). Для управления кнопками мастера я использую несложный Ajax-код и bean-компонент на стороне сервера – подробнее мы рассмотрим этот вопрос в разделе Ajax.

На рисунке 2 показана страница с последним вопросом и итоговая страница с ответами пользователя. Кнопка Finish становится доступной только когда пользователь ответил на последний вопрос. Нажатие на кнопку Finish открывает итоговую страницу со сводкой вопросов и ответов.

Рисунок 2. Итоговая страница
Wizard summary
Wizard summary

Теперь, когда мы разобрались, как работает мастер опроса, посмотрим, как его реализовать.

Приложение quiz

Дерево файлов приложения показано на рисунке 3:

Рисунок 3. Файлы приложения
Application directory

Мой мастер вопросов основан на шаблоне JSF 2 (/templates/wizardTemplate.xhtml), который используется мастером видов (/quizWizard/wizard.xhtml).

Помимо шаблона и видов, у меня есть набор фейслетов — хранящихся в директории quizWizard — для каждого из компонентов мастера:

  • Заголовок (/quizWizard/heading.xhtml)
  • Вопрос (/quizWizard/question.xhtml)
  • Радиокнопки для выбора ответа (quizWizard/choices.xhtml)
  • Кнопки Next, Previous и Finish (quizWizard/controls.xhtml)

Фейслет index.xhtml активизирует приложение по ссылке Start the wizard, а фейслет done.xhtml отображает итоговую страницу с вопросами и ответами.

Весь этот код работает на стороне клиента. На стороне сервера приложение использует три bean-компонента, два из которых мы рассмотрим в следующих секциях.

Bean-компоненты приложения, отвечающие за загрузку и отображение вопросов

Bean-компонент Question, код которого показан в листинге 1, определяет вопрос, набор вариантов ответов и сам ответ.

Листинг 1. Bean-компонент Question
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; // следующая кнопка становится доступной 
  // в том случае, когда параметр answered имеет значение 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. Соответствующий bean-компонент приведен в листинге 2:

Листинг 2. Bean-компонент Questions
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, не содержит ничего выдающегося, он просто передает список вопросов, хранящихся на сервере. Единственным интересным моментом является то, что я программным способом с помощью helper-метода вытаскиваю строки из соответствующего пакета ресурсов. Посмотреть, как работает этот метод, вы можете, загрузив примеры кода. Подробное описание метода можно найти в Core JavaServer Faces (см. Ресурсы).

Это все, что я хотел рассказать о bean-компонентах приложения, за исключением компонента Wizard, который отвечает за управление мастером. Весь интересный Java-код приложения находится именно в этом компоненте. Мы подробно рассмотрим bean-компонент Wizard в разделе CDI: инъекции зависимостей и последовательности взаимодействий.

Познакомившись с иерархией файлов приложения и его bean-компонентами, перейдем к разбору реализации вида мастера.

Шаблон и представление

Структуру большинства мастеров в общем виде можно представить так, как показано на рисунке 4:

Рисунок 4. Структура мастера
Quiz wizard buttons
Quiz wizard buttons

В листинге 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. Фейслет мастера (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, заголовок, подзаголовок, рабочую область и область управляющих элементов. Инкапсулирование основных свойств вида в шаблон значительно облегчает процесс создания новых видов — в нашем случае новых типов мастеров.

В листинге 5 показана секция заголовка мастера опросов:

Листинг 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 приведен код секции подзаголовка:

Листинг 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>

Код заголовка, приведенный в листинге 5, отображает заголовок опроса — в нашем случае это Science Quiz. Код подзаголовка, указанный в листинге 6, показывает вопрос. Параметр wizard.cursor, использующийся в листинге 6, представляет собой курсор (иначе говоря, индекс), который указывает на текущий вопрос. Значения курсора начинаются с нуля, так что #{wizard.cursor+1} отображает номер вопроса, а #{questions[wizard.cursor].question} отображает собственно вопрос.

Теперь, когда мы покончили с вводной частью (разбором серверных bean-компонентов и шаблонов), перейдем к рассмотрению действительно интересных моментов, а именно, разберем, как реализация мастера использует Ajax и CDI. Начнем с Ajax.

Ajax

Все пользовательские операции в мастере опросов генерируют Ajax-вызовы, так что после обработки таких вызовов обновляются только соответствующие секции страницы. Одна из функций, реализованных с помощью Ajax-вызовов, - это управление кнопками мастера. На рисунке 5 показано, как меняется активное состояние кнопок мастера при ответе на первый и второй вопросы:

Рисунок 5. Кнопки мастера вопросов
Quiz wizard buttons
Quiz wizard buttons

Ajax-код мастера заключен в двух фейслет-файлах. В листинге 7 показан код файла choices.xhtml:

Листинг 7. Выбор ответа
<!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. При возращении вызова JSF обновляет управляющие кнопки мастера.

В листинге 8 приведен код файла controls.xhtml:

Листинг 8. Управляющие кнопки
<!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-обращение к серверу. По возвращении вызова JSF обновляет вопрос и радиокнопки выбора, а также меняет управляющие кнопки.

Кнопка Finish не использует Ajax-вызовов, поскольку щелчок на этой кнопке перенаправляет пользователя на итоговую страницу.

Обращаю ваше внимание на то, что код, приведенный в листингах 7 и 8, содержит множество ссылок на bean-компонент wizard. Этот bean-компонент фактически управляет мастером опросов.

CDI: инъекции зависимостей и последовательности взаимодействий

Образно говоря, CDI можно охарактеризовать как «накачанные» JSF-управляемые bean-компоненты. В качестве компонента Java EE 6 CDI во многом представляет собой стандартизацию концепций, которые в течение ряда лет разрабатывались в рамках инфраструктуры Spring (в частности, инъекции зависимостей (dependency injection) и перехватчики (interceptor)). Фактически CDI и Spring 3 обладают множеством сходных функций.

Благодаря свойствам, которые в CDI называются слабое связывание (loose coupling) и строгая типизация (strong typing), вы получаете возможность эффективно разделять задачи и избавляетесь от необходимости писать рутинный Java-код для создания сущностей объектов и управления их жизненным циклом.

С точки зрения JSF одной из наиболее привлекательных функциональностей CDI является контекст последовательности взаимодействий (conversation scope). Впервые эта функциональность была реализована в инфраструктуре Seam. Она представляет собой контекст использования компонента, жизненный цикл которого управляется программным образом. Применение контекста последовательности взаимодействий позволяет разработчикам избежать взаимоисключающего выбора между запросом и сеансом.

Код мастера, использующий функции CDI, заключен в bean-компоненте Wizard (листинг 9):

Листинг 9. Bean-компонент Wizard
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. Для начала, как я уже упоминал ранее, bean-компонент Wizard определяет методы, которые управляют доступностью кнопок мастера. Кроме того, компонент определяет методы, которые JSF вызывает при нажатии пользователем кнопок Next и Previous. В зависимости от того, какая кнопка нажата, эти методы либо открывают страницу со следующим вопросом, либо возвращают пользователя на предыдущую страницу.

Однако вполне очевидно, что самая главная особенность bean-компонента Wizard состоит в использовании CDI. Сначала, как в примерах других статей этой серии, я использую CDI-версию аннотации @Named вместо аннотации @ManagedBean (CDI-реализация аннотации @Named определяется стандартом JSR 330, Dependency Injection for Java). Обе упомянутые аннотации создают bean-компонент со своим контекстом использования (scope), доступ к которому осуществляется средствами JSF EL. Однако возможности CDI-управляемого bean-компонента значительно шире, так что если вы работаете с сервером приложений, совместимым с Java EE 6, я настоятельно советую вам использовать аннотацию @Named вместо @ManagedBean.

При внимательном изучении листинга 6 и листинга 7 вы, безусловно, обратите внимание на то, что я с помощью языка выражений JSF EL обращаюсь к bean-компоненту questions. Далее, как вы помните, в листинге 2 была приведена реализация класса Questions. Однако аннотации @Named вы в листинге 2 не найдете. В обычных условиях отсутствие аннотации вызовет сообщение об ошибке, однако в нашем случае создание bean-компонента questions обеспечивается методом Wizard.getQuestions(). Этот метод объявляется аннотацией @Produces, а это, в свою очередь, означает, что при упоминании соответствующего bean-компонента в выражении JSF EL JSF вызовет метод Wizard.getQuestions() для получения нового экземпляра компонента Questions.

Следующий интересный момент – использование в компоненте Wizard контекста последовательности взаимодействий. Ссылка Start the wizard на приветственной странице мастера опросов привязана к методу start() bean-компонента Wizard. Метод start(), в свою очередь, обращается к методу begin() для создания последовательности взаимодействий пользователя с сервером (conversation). begin() преобразует текущий запрос (который может рассматриваться как последовательность взаимодействий, состоящая из одного шага) в продолжительную последовательность взаимодействий, которая будет активна до тех пор, пока не будет вызван метод end() или не истечет установленное время таймаута. Поскольку я определил Conversation scope для компонента Wizard, его жизненный цикл заканчивается вместе с окончанием последовательности взаимодействий (conversation).

Безусловно, вы можете отказаться от использования контекста последовательности взаимодействий и реализовать свой собственный аналог последовательности взаимодействий в рамках пользовательского сеанса. Собственно говоря, до появления контекста последовательности взаимодействий именно так и поступало большинство разработчиков в тех случаях, когда в приложении требовалось управление взаимодействием между пользователем и сервером с использованием нескольких запросов. Применение CDI позволяет избавиться от ручного хранения и учета промежуточных состояний и значений.

Наконец, обратите внимание на использование CDI-инъекций для добавления последовательности взаимодействий в управляемый bean-компонент. Благодаря таким инъекциям я могу начинать и останавливать последовательность взаимодействий программным способом. Инъекция ресурсов позволяет мне сосредоточиться на решении конкретных задач и не тратить время на банальный код для создания этих ресурсов и управления их жизненным циклом.

Заключение

В этой статье я коснулся сразу нескольких тем — реализация мастера с использованием Ajax, использование шаблонов, инъекции зависимостей, контекст последовательности взаимодействий, — и при этом код нашего приложения остался на удивление коротким. Таким образом, использование JSF 2 и CDI позволяет вам с минимальными затратами создавать надежно работающие веб-приложения с гибкими возможностями многократного использования.

До конца лета публикация статей серии JSF 2 fu будет приостановлена, но осенью я вернусь с новыми примерами, и вы сможете продолжить оттачивать свои приемы и технику использования JSF.


Ресурсы для скачивания


Похожие темы


Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=765504
ArticleTitle=JSF 2 fu: Cоздание мастера JSF
publish-date=10142011