Utility library for OmniFaces + PrimeFaces combined.
This project basically combines best of OmniFaces and PrimeFaces with help of OmniPersistence, an utility library for Jakarta Persistence (JPA). This project should make it a breeze to create semi-dynamic lazy-loaded, searchable, sortable and filterable <p:dataTable> based on a Jakarta Persistence model and a generic entity service.
pom.xml
<dependencies>
<!-- Target Jakarta EE server. -->
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-web-api</artifactId>
<version>10.0.0</version><!-- Minimum supported version is 10.0.0 -->
<scope>provided</scope>
</dependency>
<!-- Runtime dependencies. -->
<dependency>
<groupId>org.omnifaces</groupId>
<artifactId>omnifaces</artifactId>
<version>4.0</version><!-- Minimum supported version is 4.0 -->
</dependency>
<dependency>
<groupId>org.primefaces</groupId>
<artifactId>primefaces</artifactId>
<classifier>jakarta</classifier>
<version>15.0.0</version><!-- Minimum supported version is 15.0.0 -->
</dependency>
<dependency>
<groupId>org.omnifaces</groupId>
<artifactId>optimusfaces</artifactId>
<version>1.0</version>
</dependency>
</dependencies>Minimum supported Java / OmniFaces / PrimeFaces versions
Java EE (javax) namespace:
- 0.1 - 0.12: Java 8 / OmniFaces 2.2 / PrimeFaces 5.2
- 0.13 - 0.15: Java 8 / OmniFaces 2.2 / PrimeFaces 7.0
- 0.16: Java 8 / OmniFaces 3.0 / PrimeFaces 10.0.0
Jakarta EE (jakarta) namespace:
- 0.14.J1 - 0.16.J1: Java 11 / OmniFaces 4.0 / PrimeFaces 10.0.0:jakarta
- 0.17.J1 - 0.20.J1: Java 17 / OmniFaces 4.0 / PrimeFaces 13.0.0:jakarta
- 0.21.J1 - 0.22.J1: Java 17 / OmniFaces 4.0 / PrimeFaces 15.0.0:jakarta
- 1.0: Java 17 / OmniFaces 4.0 / PrimeFaces 15.0.0:jakarta
First create your entity service extending org.omnifaces.omnipersistence.service.BaseEntityService. You don't necessarily need to add new methods, just extending it is sufficient. It's useful for other generic things too.
@Stateless
public class YourEntityService extends BaseEntityService<Long, YourEntity> {
// ...
}And make sure YourEntity extends org.omnifaces.omnipersistence.model.BaseEntity or one of its subclasses GeneratedIdEntity, TimestampedEntity, TimestampedBaseEntity, VersionedEntity or VersionedBaseEntity.
@Entity
public class YourEntity extends BaseEntity<Long> {
@Id @GeneratedValue(strategy=IDENTITY)
private Long id;
private Instant created;
private String name;
private Type type;
private boolean deleted;
// ...
}Then create a org.omnifaces.optimusfaces.model.PagedDataModel in your backing bean as below.
@Named
@ViewScoped
public class YourBackingBean implements Serializable {
private PagedDataModel<YourEntity> model;
@Inject
private YourEntityService service;
@PostConstruct
public void init() {
model = PagedDataModel.lazy(service).build();
}
public PagedDataModel<YourEntity> getModel() {
return model;
}
}Finally use <op:dataTable> to have a semi-dynamic lazy-loaded, pageable, sortable and filterable
<p:dataTable> without much hassle.
<... xmlns:op="optimusfaces">
<h:form id="yourEntitiesForm">
<op:dataTable id="yourEntitiesTable" value="#{yourBackingBean.model}">
<op:column field="id" />
<op:column field="created" />
<op:column field="name" />
<op:column field="type" />
<op:column field="deleted" />
</op:dataTable>
</h:form>NOTE: use XML namespace of http://omnifaces.org/optimusfaces when using version 0.17 or older.
The field attribute of <op:column> represents the entity property path. This will
in turn be used in id, field, headerText and filterBy attributes
of <p:column>.
Here's how it looks like with default PrimeFaces 15 UI and all. This example uses exactly the above Java and XHTML code with a Person entity with Long id, String email, Gender gender and LocalDate dateOfBirth fields.
<op:dataTable> supports @OneToOne, @ManyToOne, @OneToMany and @ElementCollection relationships. The field attribute of <op:column> takes a dot-notation path, just as you would in EL.
@OneToOne / @ManyToOne — given an Invoice with @OneToOne Order order, @ManyToOne User seller and @ManyToOne User buyer on the Order:
<op:dataTable id="invoicesTable" value="#{shop.invoices}">
<op:column field="id" />
<op:column field="seller.name" />
<op:column field="order.buyer.name" />
<op:column field="order.totalPrice" />
</op:dataTable>@OneToMany — given an Order with @OneToMany List<Product> products. Each element is automatically rendered on a separate line:
<op:dataTable id="ordersTable" value="#{shop.orders}">
<op:column field="id" />
<op:column field="buyer.name" />
<op:column field="products.name" />
<op:column field="products.price" />
</op:dataTable>@ElementCollection — given a Product with @ElementCollection List<Tag> tags. Note: sorting on @ElementCollection fields is not supported in lazy models.
<op:dataTable id="productsTable" value="#{shop.products}">
<op:column field="id" />
<op:column field="name" />
<op:column field="tags" sortable="false" />
</op:dataTable>DTO projections are also supported by providing an additional Class<DTO> resultType and a QueryBuilder mapping to the getPage() overload in BaseEntityService. See PagedDataModel javadoc for the full DTO example.
For a static in-memory list, use PagedDataModel.nonLazy(list). Unlike the lazy model, entities only need to implement Identifiable (not BaseEntity), making it convenient for DTOs.
@PostConstruct
public void init() {
List<YourEntity> list = createItSomehow();
model = PagedDataModel.nonLazy(list).build();
}To apply fixed query constraints at the service level, create a custom getPageXxx() method using the QueryBuilder overload of getPage(), then wire it up via PagedDataModel.lazy(service::getPageXxx).
@Stateless
public class YourEntityService extends BaseEntityService<Long, YourEntity> {
public PartialResultList<YourEntity> getPageOfFooType(Page page, boolean count) {
return getPage(page, count, (criteriaBuilder, criteriaQuery, root) -> {
criteriaQuery.where(criteriaBuilder.equal(root.get("type"), Type.FOO));
});
}
}@PostConstruct
public void init() {
model = PagedDataModel.lazy(service::getPageOfFooType).build();
}To drive filtering from a separate form in the same view, pass a Map<Getter<E>, Object> supplier to .criteria(). The supplier is called on every page load so changes to the backing bean fields are picked up automatically.
@PostConstruct
public void init() {
model = PagedDataModel.lazy(service).criteria(this::getCriteria).build();
}
private Map<Getter<YourEntity>, Object> getCriteria() {
Map<Getter<YourEntity>, Object> criteria = new HashMap<>();
criteria.put(YourEntity::getName, Like.startsWith(searchName));
criteria.put(YourEntity::getCreated, Order.greaterThanOrEqualTo(searchStartDate));
criteria.put(YourEntity::getType, searchTypes);
criteria.put(YourEntity::isDeleted, false);
return criteria;
}Available Criteria wrappers: Like, Not, Between, Order, Enumerated, Numeric, Bool, IgnoreCase. A null value produces IS NULL; use Not(null) for IS NOT NULL; omit the key entirely to skip the criterion.
| Attribute | Default | Description |
|---|---|---|
id |
ID of the underlying <p:dataTable> and its widgetVar. |
|
value |
The PagedDataModel instance from the backing bean. |
|
styleClass |
Additional CSS class on the <p:dataTable>. |
|
responsive |
true |
Enables PrimeFaces reflow/responsive mode. |
rendered |
true |
|
updateQueryString |
true |
Reflects paging, sorting, filtering and selection state in the browser URL as query parameters, making the table state bookmarkable. |
queryParameterPrefix |
Prefix for all query parameters; useful when multiple tables share a page to avoid parameter name collisions. | |
sortable |
true |
Enables sorting on all columns; can be overridden per column via <op:column sortable="false">. |
filterable |
true |
Enables filtering on all columns; can be overridden per column via <op:column filterable="false">. |
paginable |
true |
Enables the paginator. |
rows |
10 |
Rows per page. |
rowsPerPage |
10,25,50 |
Comma-separated rows-per-page options shown in the paginator. |
paginatorTemplate |
{CurrentPageReport} {FirstPageLink} {PreviousPageLink} {PageLinks} {NextPageLink} {LastPageLink} |
PrimeFaces paginator template. |
currentPageReportPrefix |
Showing |
|
currentPageReportTemplate |
{startRecord} - {endRecord} of {totalRecords} |
|
currentPageReportSuffix |
records |
|
searchable |
false |
Adds a global search bar to the table header that searches across all filterable columns. |
searchPlaceholder |
Search… |
|
searchButtonLabel |
Search |
|
exportable |
false |
Adds a column toggler and a CSV export split-button to the table header. |
columnTogglerButtonLabel |
Columns |
|
exportType |
csv |
PrimeFaces DataExporter type (csv, pdf, xlsx, …). |
exportButtonLabel |
CSV |
|
exportVisibleColumnsButtonLabel |
Visible Columns |
|
exportAllColumnsButtonLabel |
All Columns |
|
exportFilename |
<id>-<timestamp> |
Export file name without extension. |
exportPreProcessorMethod |
Method expression called before export, e.g. #{bean.onBeforeExport}. |
|
exportPostProcessorMethod |
Method expression called after export, e.g. #{bean.onAfterExport}. |
|
selectable |
false |
Enables multi-row selection; selected rows are available as model.selection. |
actionable |
false |
Enables the header actions toolbar without requiring searchable or exportable. |
The default <ui:insert> in <op:dataTable> passes content directly into the underlying <p:dataTable> body. Use this to attach <f:attribute> tags or <p:ajax> listeners to the underlying <p:dataTable>:
<op:dataTable id="yourEntitiesTable" value="#{yourBackingBean.model}">
<f:attribute name="draggableRows" value="true" />
<p:ajax event="rowReorder" listener="#{yourBackingBean.saveRowOrder}" />
<op:column field="id" />
...
</op:dataTable>When searchable, exportable or actionable is enabled, a <ui:insert name="actions"> slot is available inside the header toolbar for custom buttons:
<op:dataTable id="yourEntitiesTable" value="#{yourBackingBean.model}" actionable="true">
<ui:define name="actions">
<p:commandButton value="New" action="#{yourBackingBean.create}" icon="pi pi-plus" />
</ui:define>
<op:column field="id" />
...
</op:dataTable>| Attribute | Default | Description |
|---|---|---|
field |
Entity property path; supports dot-notation for nested paths, e.g. address.street. |
|
head |
field name | Column header text. |
value |
#{item[field]} |
Cell value expression; override to compute or format, e.g. value="#{item.firstName} #{item.lastName}". |
tooltip |
title attribute on the cell wrapper <span>. |
|
styleClass |
||
rendered |
true |
|
visible |
true |
Initial column visibility; user can toggle via the column toggler when exportable="true" on the table. |
responsivePriority |
0 |
PrimeFaces responsive priority; higher values are hidden sooner on narrow screens. |
width |
Column width, e.g. 100px or 10%. |
|
iterable |
auto | When true, collection values are rendered one item per line. Auto-detected from the value type. |
emptyValue |
Text rendered when the value is null or empty. |
|
sortable |
inherits | Overrides the table-level sortable setting for this column. |
sortDescending |
false |
Sets the initial sort direction to descending for this column. |
filterable |
inherits | Overrides the table-level filterable setting for this column. |
filterMode |
contains |
Filter match mode: startsWith, endsWith, contains, exact. |
filterOptions |
When set, renders a <p:selectOneMenu> dropdown filter instead of a text input. Accepts a List or Map of options. |
|
exportable |
true |
Whether this column is included in exports. |
exportValue |
same as value |
Value used during export, e.g. to strip HTML markup from CSV output. |
The value attribute overrides what is displayed in the cell while field still drives sorting and filtering. This is the right approach for e.g. an enum with a human-readable label property:
<op:column field="gender" value="#{item.gender.label}" />
<op:column field="status" value="#{item.status.label}" head="Status" />The default <ui:insert> in <op:column> is nested directly inside the cell's <h:outputText>, so converters can be attached without overriding the cell layout:
<op:column field="price">
<f:convertNumber type="currency" currencySymbol="$" />
</op:column>
<op:column field="created">
<f:convertDateTime type="localDate" pattern="yyyy-MM-dd" />
</op:column>For fully custom cell content, use <ui:define name="cell">. The exportValue attribute controls what is written to the export file independently of the custom cell rendering:
<op:column field="name">
<ui:define name="cell">
<h:link value="#{item.name}" outcome="detail">
<f:param name="id" value="#{item.id}" />
</h:link>
</ui:define>
</op:column>
<op:column field="status" exportValue="#{item.status.label}">
<ui:define name="cell">
<h:graphicImage value="/images/#{item.status}.png" title="#{item.status.label}" />
</ui:define>
</op:column>On every paging, sorting, filtering, searching and selection action an Ajax event fires. <op:dataTable> uses PrimeFaces Selectors (PFS) to auto-update any component carrying the matching style class:
| Style class | Updated on |
|---|---|
updateOnDataTablePage |
paging |
updateOnDataTableSort |
sorting |
updateOnDataTableFilter |
filtering / searching |
updateOnDataTableSelect |
row selection |
For example, to keep a summary panel in sync with the current filter state:
<p:outputPanel styleClass="updateOnDataTableFilter">
...
</p:outputPanel>- OpenJPA generates broken nested correlated subqueries for
@OneToManyin count subquery context, this has been work arounded, but the total result count when filtering might be inaccurate there. In case this is undesireable, use a DTO projection instead. - OpenJPA and Hibernate+PostgreSQL/SQLServer generate broken nested correlated subqueries for
@ElementCollectionin count subquery context when aLIKE-based filter (e.g. global filter) is involved, this has been work arounded, but the total result count may be inaccurate when the search term matches entities exclusively via the element collection field and not via any other field. In case this is undesireable, use a DTO projection instead.
The integration tests currently run on following environments:
- WildFly Preview 39.0.1 with Mojarra 4.1.5 and Hibernate 7.1.11
- GlassFish 8.0.0 with Mojarra 4.1.6 and EclipseLink 5.0.0-B13
- TomEE 10.1.4 with MyFaces 4.0.3 and OpenJPA 4.1.1
Each environment will run the IT on following databases:
- H2 2.4.240 (embedded database)
- MySQL latest 8.x (provided by GitHub Actions Ubuntu environment) with JDBC driver 9.6.0
- PostgreSQL latest 15.x (provided by GitHub Actions Ubuntu environment) with JDBC driver 42.7.10
- SQL Server latest 2022 (provided by MicroSoft's Docker image) with JDBC driver 13.2.1.jre11
- DB2 latest 12 (provided by IBM's Docker image) with JDBC driver 12.1.3.0
Effectively, there are thus 15 full test runs of each 31 test cases on 19 XHTML files.
