
В предыдущей части статьи мы уже познакомились с тем, как выводить на страницу данные с помощью отдельных простых компонентов. Как мы убедились, для этих целей можно использовать, как компоненты самого Primefaces, так и компоненты фреймворка JSF, на котором он основан. В частности, h:outputText - пример компонента JSF, а Data List - компонент Primefaces. В моих примерах и в документации их легко различить, например, по префиксу, JSF компоненты имеют префикс h, а Primefaces компоненты - префикс p, что также отражено в декларации xhtml документа, редко кто использует нестандартные префиксы, но если это так, то это отражено в декларации.
На самом деле компонентов и JSF, и Primefaces очень много, и описать их все в рамках даже большой статьи практически невозможно. Поэтому приведу ссылки на туториал и документацию:
туториал по базовым тегам JSF - на этом же сайте можно найти множество туториалов по более сложным и композитным компонентам JSF.
документация по UI Primefaces - здесь вы найдете описание десятков компонентов Primefaces с showcase, примерами их использования с образцами кода управляемых бинов и xhtml страниц.
Однако, как я уже писал ранее, следует иметь ввиду, что применение Primefaces может приводить к неожиданным side эффектам и не исчерпывается сведениями из документации. Например, вам требуется вывести сложно взаимосвязанные данные в каком-либо специфическом виде, и вам не хватает возможностей типовых компонентов из документации JSF и Primefaces. И вот тут уже придется потрудиться самостоятельно, применив смекалку разработчика. Не сомневаюсь, что практически все проблемы можно решить, комбинируя простые (или не очень простые) стандартные компоненты в более сложные, составные конструкции. Фактически, вы можете сделать собственные композитные компоненты на базе стандартных.
Приведу пример такой разработки. Предположим, у нас есть три сущности - сотрудник Employee, квалификация сотрудника в каком-то стеке Skill и уровень его квалификации SkillGrade в этом стеке. Не будем чрезмерно углубляться в подробности и приведем только самые основные сведения о структуре используемых данных. Пусть связь между сущностями определена таблицей связи такого вида:

получаем из таблицы для какого-то конкретного сотрудника уровень владения определенной компетенцией, у меня это - нативный запрос, что связано с конкретной реализацией структуры данных. У вас это может быть совсем другая структура и другой запрос к репозиторию, например, на базе Spring Data JPA, конкретные детали здесь приведены лишь для ясности описания моего конкретного примера:
@Repository public interface SkillRepository extends JpaRepository<Skill, Integer> { Skill findByName(String name); @Query(value = "select skill_grade_id from employees_to_skills where ( skill_id = :skill_id and employee_id = :employee_id )", nativeQuery = true) Integer getSkillGradeIdByEmployeeIdAndSkillId(Integer skill_id, Integer employee_id); }
далее в сервисе получаем для данного конкретного сотрудника полную карту его компетенций с соответствующим каждой компетенции уровнем владения
public Map<Skill, SkillGrade> getSkillMap(Employee employee) { Map<Skill, SkillGrade> map = new HashMap<>(); Set<Skill> skills = employee.getSkills(); skills.forEach(skill -> { map.put( skill, skillGradeRepository.getReferenceById(skillRepository.getSkillGradeIdByEmployeeIdAndSkillId(skill.getId(), employee.getId())) ); }); return map; }
Давайте теперь выведем список всех компетенций сотрудника, указав рядом с каждой компетенцией уровень владения ею данным сотрудником в виде красивого рейтинга со звездочками. Для этого нам придется скомбинировать два компонента - Data List p:dataList и вложненный в него рейтинг p:rating. Для этого добавим на xhtml страницу такой код:
<div class="col-12 md:col-4"> <h3>Квалификационный профиль сотрудника</h3> <p:dataList value="#{employeeCardView.employeeSkillRatingView.skillList}" var="entry" styleClass="noBorders -ml-6" itemType="none"> <p> #{entry.key.name} </p> <p:rating value="#{entry.value.id}" readonly="false" stars="6"/> </p:dataList> <div class="col-12 md:col-6"> <p:button value="Редактировать" icon="pi pi-pencil" href="/employee/skills/edit/#{employeeCardView.id}"/> </div> </div>
И создадим класс управляемого бина компонента для вывода данной информации:
import jakarta.inject.Inject; import lombok.Getter; import lombok.Setter; import lombok.extern.log4j.Log4j2; import org.satel.ressatel.entity.Employee; import org.satel.ressatel.entity.Skill; import org.satel.ressatel.entity.SkillGrade; import org.satel.ressatel.service.EmployeeService; import org.satel.ressatel.service.SkillService; import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.stereotype.Component; import org.springframework.web.context.WebApplicationContext; import java.util.ArrayList; import java.util.List; import java.util.Map; @Component("employeeSkillRatingView") @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) @Getter @Setter @Log4j2 public class EmployeeSkillRatingView { private String id; private List<Map.Entry<Skill, SkillGrade>> skillList; private final EmployeeService employeeService; private final SkillService skillService; @Inject public EmployeeSkillRatingView(EmployeeService employeeService, SkillService skillService) { this.employeeService = employeeService; this.skillService = skillService; } public void onload() { Employee employee = employeeService.getByStringId(id); if (!skillService.getSkillMap(employee).isEmpty()) { skillList = new ArrayList<>(skillService.getSkillMap(employee).entrySet()); } } }
В результате получим вывод, какой нам и требовался:

Здесь вы еще видите отдельную кнопку "Редактировать". И редактирование композитных компонентов, составленных из нескольких базовых, также является интересной и не тривиальной задачей - ее мы разберем в продолжениях статьи.
Дополнительным бонусом поясню еще один тонкий момент в коде данного компонента. Обратите внимание, что класс компонента помечен аннотацией
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
Это аннотация из Spring, и означает она, что новый экземпляр бина компонента будет создаваться каждый раз при запросе компонента из новой xhtml страницы в нашем приложении. Особенно важно это, когда у вас в приложении есть списки сущностей и карточки отдельных сущностей. Например, если бы этой аннотации не было, то происходило бы следующее: вы открываете карточку сотрудника и видите данные, полученные компонентом для этой карточки. Затем закрываете карточку и открываете карточку второго сотрудника, и вместо данных для него вы увидите в ней данные компонента, полученные для первого же сотрудника. Вы не сможете получить уникальные данные компонента для каждого сотрудника отдельно. Однако, как вы уже догадались, в Jakarta EE есть собственная аннотация @RequestScoped, которая, казалось бы, призвана выполнять ту же самую функцию. Но предупреждаю сразу - в моей конфигурации приложения (то есть при интеграции Srping + JSF + Primefaces) эта аннотация не работает, один и тот же бин все равно создается в первой же карточке сотрудника и затем подставляется в остальные карточки со всеми своими данными, что нам категорически не нужно - то есть аннотация @RequestScoped не является полным аналогом соответствующей аннотации Spring, по крайней мере, при интеграции Spring и Primefaces. Вполне возможно, что в приложении на чистой Jakarta EE эта аннотация и будет работать, но не в нашем случае.
В заключение приглашаю на бесплатный вебинар от OTUS, где рассмотрим экосистему технологий Java, спектр областей, которые обслуживает Java. Поговорим о том, какие компании активно используют Java в своих IT-продуктах. Посмотрим на географию компаний и карьерных предложений. Обоснуем верный выбор Java как профессионального стека для устойчивой карьеры.
