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