Skip to content

omnifaces/optimusfaces

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

463 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Maven Javadoc Tests License

OptimusFaces

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.

Installation

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

Basic Usage

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.

example of op:dataTable

Relationships

<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.

Non-lazy model

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();
}

Providing criteria in the backend

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();
}

Providing criteria in the frontend

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.

<op:dataTable> attributes

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>

<op:column> attributes

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>

Ajax events

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>

Known Issues

  • OpenJPA generates broken nested correlated subqueries for @OneToMany in 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 @ElementCollection in count subquery context when a LIKE-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.

Integration Tests

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.

About

Utility library for OmniFaces + PrimeFaces combined

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors