Showing posts with label PrimeFaces. Show all posts
Showing posts with label PrimeFaces. Show all posts

Monday, March 2, 2026

OmniPersistence and OptimusFaces finally reach 1.0

After roughly ten years of 0.x releases, both OmniPersistence and OptimusFaces have finally reached 1.0. Both have been in active use in several production apps since 2015 (primarily with Hibernate). This post gives an refreshing overview of what they do and what went into the 1.0 releases.

OmniPersistence 1.0

OmniPersistence reduces boilerplate in the Jakarta Persistence layer. It provides a rich base service class, declarative soft-delete, field-level auditing, structured pagination with typed search criteria, and provider and database detection. It works with Hibernate, EclipseLink and OpenJPA on any Jakarta EE compatible runtime.

<dependency>
    <groupId>org.omnifaces</groupId>
    <artifactId>omnipersistence</artifactId>
    <version>1.0</version>
</dependency>

It requires a minimum of Java 17 and Jakarta EE 10.

Entity model hierarchy

OmniPersistence ships with six base entity classes. Just pick the one that covers what your entity needs:

All four identity methods equals, hashCode, compareTo and toString default to the database ID. In case you want to base them on a business key instead, simply override identityGetters() once and all four are consistent:

@Entity
public class Phone extends GeneratedIdEntity<Long> {

    private Type type;
    private String number;
    private Person owner;

    @Override
    protected Stream<Function<Phone, Object>> identityGetters() {
        return Stream.of(Phone::getType, Phone::getNumber);
    }
}

BaseEntityService

Extend BaseEntityService<I, E> to get a full CRUD service for your entity. It works as an EJB or a CDI bean:

@ApplicationScoped // Or @Stateless if you're still on EJB
public class PersonService extends BaseEntityService<Long, Person> {
    // The @PersistenceContext is already injected. Nothing else needed.
}

Note: if you're still on EJBs, it might be wise to migrate to CDI, as development on the EJB spec has basically stalled and EJB is in long term going to be discommended in favor of CDI.

The inherited BaseEntityService API offers a lot of helpful shortcut methods for lookups, CRUD and JPQL:

// READ
Optional<Person> found = personService.findById(id);
Person person = personService.getById(id); // or null
List<Person> all = personService.list();
List<Person> subset = personService.list("WHERE e.verified = true");

// WRITE
Long id = personService.persist(newPerson);
Person updated = personService.update(person);
Person saved = personService.save(person); // persist-or-update
personService.delete(person);

Short JPQL fragments are auto-expanded to SELECT e FROM EntityName e <fragment>, so the alias e is always predefined.

Pagination with Page and search criteria

Page is an immutable value object that bundles offset, limit, ordering and search criteria. Pass it to getPage() and get back a PartialResultList that also carries the total count when requested:

Map<String, Object> criteria = new LinkedHashMap<>();
criteria.put("lastName", Like.contains("smith"));
criteria.put("age", Between.range(18, 65));
criteria.put("status", Not.value("BANNED"));
criteria.put("role", Role.ADMIN);

Page page = Page.with()
    .range(0, 10)
    .orderBy("lastName", true)
    .allMatch(criteria)
    .build();

PartialResultList<Person> result = personService.getPage(page, true); // true = include count as well
List<Person> list = result;
int count = result.getEstimatedTotalNumberOfResults(); // or -1 if count was not included

The available search criteria wrappers are:

  • Like - case-insensitive pattern matching (startsWith, endsWith, contains)
  • Between - range queries
  • Order - comparison operators (<, >, <=, >=)
  • Not - negation wrapper around any value or criteria
  • Bool, Numeric, Enumerated, IgnoreCase - specialised type handling
  • A plain value produces an exact equality predicate

Conditions can be grouped with allMatch() (AND) or anyMatch() (OR) and mixed freely.

In case you need stable performance on large datasets without SQL OFFSET, cursor-based (keyset) pagination is also available.

Page firstPage = Page.with().range(0, 10).build();
PartialResultList<Person> firstList = personService.getPage(firstPage, true);
Page secondPage = Page.with().range(firstList.getLast(), firstPage.getLimit(), false).build(); // false = not in reversed direction
PartialResultList<Person> secondList = personService.getPage(secondPage, true);

With large offsets this is much faster than e.g. Page hundredthPage = Page.with().range(990, 10).build();.

Soft delete and auditing

Mark a boolean field with @SoftDeletable and BaseEntityService will automatically exclude soft-deleted rows from all read methods and expose dedicated softDelete, softUndelete and listSoftDeleted methods:

@Entity
public class Comment extends GeneratedIdEntity<Long> {

    private String text;

    @SoftDeletable
    private boolean deleted;
}
commentService.softDelete(comment);
commentService.softUndelete(comment);
List<Comment> gone = commentService.listSoftDeleted();

Field-level change auditing fires a CDI event for every @Audit-annotated field that is modified during a transaction:

@Entity
@EntityListeners(AuditListener.class)
public class Config extends GeneratedIdEntity<Long> {

    private String key;

    @Audit
    private String value;
}
public void onAuditedChange(@Observes AuditedChange change) {
    log.info("{}.{}: {} -> {}",
        change.getEntityName(), change.getPropertyName(),
        change.getOldValue(), change.getNewValue());
}

Provider and database detection

OmniPersistence detects the active Jakarta Persistence provider and the underlying database at startup and makes them available via getProvider() and getDatabase() on BaseEntityService. This is used internally to emit provider-correct SQL, for example because string casting syntax and @ElementCollection pagination count subquery SQL syntax differs across providers and databases. In case you need it in your own service code, you can simply call them directly:

public void someMethod() {
    if (getProvider() == Provider.HIBERNATE) {
        // ...
    }

    if (getDatabase() == Database.POSTGRESQL) {
        // ...
    }
}

Supported databases as of 1.0: H2, MySQL, PostgreSQL, SQL Server and DB2.

What changed for OmniPersistence 1.0

The most impactful change is the decoupling from EJB. The initial 0.x versions required the service class to be a @Stateless EJB. It wasn't possible to make them an @ApplicationScoped CDI bean because some internals related to auditing (preupdate/postupdate) which rely on presence of EJB's SessionContext (in order to access the currently used EntityManager which could have been customized by the custom BaseEntityService implementation) would break. These internals have been improved in OmniPersistence 1.0 and you can finally freely choose between EJB or CDI.

The most visible API change is that Consumer<Map<String, Object>> parameters were replaced by direct Map<String, Object> parameters. The indirection was introduced to avoid the verbosity of new HashMap() at every call site. With Java 9's Map.of(), there is no longer a reason for it.

Other notable changes: explicit SQL Server and DB2 support was added to Database enum, compatibility with current versions of Hibernate, EclipseLink and OpenJPA was improved (in particular around @OneToMany and @ElementCollection pagination and filtering), and a comprehensive set of missing Javadocs and unit/integration tests was filled in.

OptimusFaces 1.0

OptimusFaces combines OmniFaces and PrimeFaces with OmniPersistence. The goal is to make it a breeze to create lazy-loaded, searchable, sortable and filterable <p:dataTable> based on a Jakarta Persistence model and a generic entity service.

<dependency>
    <groupId>org.omnifaces</groupId>
    <artifactId>optimusfaces</artifactId>
    <version>1.0</version>
</dependency>

Requires a minimum of Java 17, Jakarta EE 10 Web Profile, OmniFaces 4.0, PrimeFaces 15.0.0 and OmniPersistence 1.0.

PagedDataModel

PagedDataModel is the backing model for <op:dataTable>. It extends from PrimeFaces LazyDataModel. You create one in a CDI bean via the fluent builder:

@Named
@ViewScoped
public class Persons implements Serializable {

    private PagedDataModel<Person> model;

    @Inject
    private PersonService personService;

    @PostConstruct
    public void init() {
        model = PagedDataModel.lazy(personService)
            .criteria(this::getCriteria)
            .build();
    }

    private Map<Getter<Person>, Object> getCriteria() {
        Map<Getter<Person>, Object> criteria = new LinkedHashMap<>();
        criteria.put(Person::isActive, true);
        return criteria;
    }

    public PagedDataModel<Person> getModel() {
        return model;
    }
}

The criteria supplier is re-evaluated on every table interaction triggering LazyDataModel#load(), so backing bean fields can drive it dynamically from custom input fields outside the table (even though the <op:dataTable> already offers built-in filtering via searchable="true"). Here's an example:

private Map<Getter<Person>, Object> getCriteria() {
    Map<Getter<Person>, Object> criteria = new LinkedHashMap<>();

    if (searchName != null && !searchName.isBlank()) {
        criteria.put(Person::getName, Like.startsWith(searchName));
    }

    if (searchStatus != null) {
        criteria.put(Person::getStatus, searchStatus);
    }

    if (searchCreatedAfter != null) {
        criteria.put(Person::getCreated, Order.greaterThanOrEqualTo(searchCreatedAfter));
    }

    return criteria;
}

In case you already have a small list in memory, PagedDataModel.nonLazy(list) will apply sorting and filtering in-memory instead.

<op:dataTable> and <op:column>

Wire the model to the view with <op:dataTable> and declare columns with <op:column field="...">. Column ids, headers, sorting and filtering are all derived automatically from merely the field name:

<op:dataTable id="persons" value="#{persons.model}"
    searchable="true" exportable="true" selectable="true">

    <op:column field="firstName" />
    <op:column field="lastName" />
    <op:column field="dateOfBirth">
        <f:convertDateTime type="localDate" pattern="yyyy-MM-dd" />
    </op:column>
    <op:column field="address.city" />
    <op:column field="phones.number" />
    <op:column field="groups" filterMode="contains" />

</op:dataTable>

searchable="true" adds a global filter bar above the table. exportable="true" adds a column toggler and a CSV/PDF/XLSX export button. selectable="true" adds checkboxes and selected rows are available as model.getSelection().

The field attribute on <op:column> understands dot-notation across Jakarta Persistence relationship types. Thanks to the NestedBaseEntityELResolver address.city navigates a @ManyToOne, phones.number renders each element of a @OneToMany collection on a separate line, and groups on an @ElementCollection renders each element inline. Sorting and filtering on database side works for @ManyToOne and @OneToOne. For collections it depends on the Jakarta Persistence provider.

In case the generated cell output is not sufficient, you can fully customise it:

<op:column field="lastName">
    <ui:define name="cell">
        <h:link value="#{item.firstName} #{item.lastName}" outcome="person">
            <f:param name="id" value="#{item.id}" />
        </h:link>
    </ui:define>
</op:column>

Bookmarkability and stateless support

In plain PrimeFaces, a lazy <p:dataTable> requires a @ViewScoped backing bean. The reason is that the model instance needs to survive across postbacks, including its wrapped data. @ViewScoped achieves this by keeping the bean instance alive in the view map between requests, tracked by identifiers in the Jakarta Faces View State. When the bean is @RequestScoped the model is brand new on every request, its wrapped data is null at the point decode runs, and pagination, sorting and selection postbacks silently fail.

This also means that stateless Jakarta Faces views (<f:view transient="true">), which disable Jakarta Faces state saving entirely on a per-view basis, and therefore inherently break @ViewScoped beans and require @RequestScoped beans, are simply not an option when you need a paginable/sortable/filterable/selectable lazy PrimeFaces data table.

OptimusFaces solves this differently. After every table interaction, LazyPagedDataModel#updateQueryStringIfNecessary() collects the current table state and emits a JavaScript callback toOptimusFaces.Util.updateQueryString(). That function calls window.history.replaceState() to update the browser URL without a page reload and loops over all JSF forms to update the action URL. The URL now always reflects the exact current table state: page number, sort column, sort direction, active column filters and row selection.

On the next request, whether it is a postback, a browser refresh or a bookmarked URL, ExtendedDataTable takes over. It overrides preDecode() and detects that the lazy model has no wrapped data yet. Instead of letting decode fail, it calls LazyPagedDataModel#preloadPage(), which reads the table state back from the URL query string parameters and loads the correct page from the data store before decode proceeds. JSF state is never needed for this. The query string is the state.

The practical consequence is that <op:dataTable> works fully with @RequestScoped backing beans and stateless Jakarta Faces views, and the table is fully bookmarkable and shareable at the same time.

@Named
@RequestScoped // Works fine with both regular and stateless (transient) views.
public class Persons {

    private PagedDataModel<Person> model;

    @Inject
    private PersonService personService;

    @PostConstruct
    public void init() {
        model = PagedDataModel.lazy(personService).build();
    }

    public PagedDataModel<Person> getModel() {
        return model;
    }
}

Parameter names are derived from the table id. In case you have multiple tables on the same view, set a queryParameterPrefix on each to keep their parameters separate.

Ajax event marker classes

OptimusFaces uses predefined PFS (PrimeFaces Selectors) classes that components can use to opt in to automatic updates when specific table events fire:

  • updateOnDataTablePage - updated on every pagination change
  • updateOnDataTableSort - updated on sort
  • updateOnDataTableFilter - updated when the filter or global search changes
  • updateOnDataTableSelect - updated on row selection
<p:outputPanel styleClass="updateOnDataTableFilter">
    Found #{persons.model.rowCount} persons
</p:outputPanel>

What changed for OptimusFaces 1.0

Sort and filter metadata are now cached inside LazyPagedDataModel to avoid repeated map lookups on every render. SQL Server and DB2 were added to the integration test matrix. A TomEE 10.1.x/OpenWebBeans issue with <c:set scope="application"> inside column.xhtml (which caused a LinkageError on the second column inclusion due to a duplicate CDI proxy class definition) was fixed by removing the explicit scope from those tags. OpenJPA's pagination misbehaviour for @OneToMany fetch joins is now worked around with a postponed-fetch strategy that issues a secondary WHERE id IN (...) query instead of relying on a single JOIN FETCH that OpenJPA would incorrectly LIMIT before aggregating rows per entity.

Testing matrix

OptimusFaces runs its integration tests against three Jakarta Persistence providers:

  • Hibernate 7 as provided by WildFly
  • EclipseLink 5 as provided by GlassFish
  • OpenJPA 4 as provided by TomEE

And five databases:

  • H2 (embedded)
  • MySQL 8
  • PostgreSQL 15
  • SQL Server 2022
  • DB2 12

Not every combination of provider and database is exercised, but the 15-environment matrix each running 31 test cases on 19 XHTML files is wide enough to shake out a number of interesting provider-specific and database-specific bugs that would otherwise only surface in production.

A word on AI assistance

Getting both libraries to finally make it to 1.0 involved a lot of work that had been pending for a long time: missing Javadocs across both codebases, additional unit and integration tests, hardening against current versions of Hibernate, EclipseLink and OpenJPA, and fixing a number of subtle EclipseLink/OpenJPA-specific bugs around @OneToMany pagination, @ElementCollection filtering and nested correlated subqueries. I simply didn't want to release a 1.0 with halfbaked documentation or subtle EclipseLink/OpenJPA-specific bugs but I as being a Hibernate user never really got the time to get there so they stayed 0.x for fairly a long time. A substantial part of pending work was finally done with help of Claude Code.

AI-assisted development has come far enough to be genuinely useful for this kind of systematic, high-context work: completing patterns across many files, catching edge cases in tricky SQL generation code, and writing correct Javadoc for APIs as never seen before. Also the GitHub READMEs were thoroughly updated by Claude based on all it knows about the projects, including a section which carefully compares OmniPersistence to Jakarta Data.

Links

Saturday, March 3, 2012

Full ajax exception handler

Whenever some business code throws an unhandled exception, due to some unexpected environmental situation (e.g. DB down), or due to session expiration (ViewExpiredException), or due to some overseen bug (fix it asap!), it usually ends up in a HTTP 500 error page or some exception-specific error page, which you can in any way customize according the standard Servlet API rules by a <error-page> in web.xml as follows:

<error-page>
    <error-code>500</error-code>
    <location>/errors/500.xhtml</location>
</error-page>
<error-page>
    <exception-type>javax.faces.application.ViewExpiredException</exception-type>
    <location>/errors/expired.xhtml</location>
</error-page>

However, the error page does not show up at all whenever the exception occurs during a JSF ajax request. In Mojarra, only when the javax.faces.PROJECT_STAGE is set to Development, a bare JavaScript alert dialogue will show up, with only the exception type and message. This may be helpful for developers and testers during development stage, but this alert does thus not show up in Production project stage. The enduser would not get any feedback if the action was successfully performed or not. This is quite frustrating. Also for the developer.

OmniFaces to the rescue

Ideally, JSF should just show the error page in its entirety. This is possible with a custom ExceptionHandler. The OmniFaces project has recently got such an exception handler, written by yours truly, the FullAjaxExceptionHandler (source code here). All you need to do is to register the FullAjaxExceptionHandlerFactory (source code here) in faces-config.xml as follows:

<factory>
    <exception-handler-factory>
        org.omnifaces.exceptionhandler.FullAjaxExceptionHandlerFactory
    </exception-handler-factory>
</factory>

This exception handler factory will register the FullAjaxExceptionHandler which will handle exceptions on ajax requests.

The exception handler will parse the web.xml to find the error page locations of the HTTP error code 500 and all exception types. You only need to make sure that those locations point each to a Facelets file. The location of the HTTP error code 500 or the exception type java.lang.Throwable is required to have at least a fallback error page if none of the specific exception types are matched.

The exception handler will set all error details in the request scope by the standard servlet error request attributes like as in a normal synchronous HTTP 500 error page response. This way the error pages are fully reuseable for both normal and ajax requests. Finally it will create a new UIViewRoot on the error page location and force a partial render of @all. Here's an extract of relevance from the source code:

// Set the necessary servlet request attributes which a bit decent error page may expect.
final HttpServletRequest request = (HttpServletRequest) context.getExternalContext().getRequest();
request.setAttribute(ATTRIBUTE_ERROR_EXCEPTION, exception);
request.setAttribute(ATTRIBUTE_ERROR_EXCEPTION_TYPE, exception.getClass());
request.setAttribute(ATTRIBUTE_ERROR_MESSAGE, exception.getMessage());
request.setAttribute(ATTRIBUTE_ERROR_REQUEST_URI, request.getRequestURI());
request.setAttribute(ATTRIBUTE_ERROR_STATUS_CODE, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);

// Force JSF to render the error page in its entirety to the ajax response.
context.setViewRoot(context.getApplication().getViewHandler().createView(context, errorPageLocation));
context.getPartialViewContext().setRenderAll(true);
context.renderResponse();

// Prevent some servlet containers from handling the error page itself afterwards. So far Tomcat/JBoss
// are known to do that. It would only result in IllegalStateException "response already committed".
Events.addAfterPhaseListener(PhaseId.RENDER_RESPONSE, new Runnable() {
    @Override
    public void run() {
        request.removeAttribute(ATTRIBUTE_ERROR_EXCEPTION);
    }
});

Note the last part. Tomcat and JBoss seem to automatically trigger the default HTTP 500 error page mechanism after JSF has done its job. It turns out that it was triggered by the presence of the javax.servlet.error.exception request attribute, regardless of if it was been set by response.sendError(). Although that would not harm, the response is namely already committed by JSF, but it would clutter your server logs with an IllegalStateException: response already committed every time when the exception handler does its job. Hence the piece of code which removes the request attribute after the render response phase.

Finally, you could show all error details in the error page the usual way as follows:

<ul>
    <li>Date/time: #{of:formatDate(now, 'yyyy-MM-dd HH:mm:ss')}</li>
    <li>HTTP user agent: #{header['user-agent']}</li>
    <li>Request URI: #{requestScope['javax.servlet.error.request_uri']}</li>
    <li>Status code: #{requestScope['javax.servlet.error.status_code']}</li>
    <li>Exception type: #{requestScope['javax.servlet.error.exception_type']}</li>
    <li>Exception message: #{requestScope['javax.servlet.error.message']}</li>
    <li>Exception stack trace: 
        <pre>#{of:printStackTrace(requestScope['javax.servlet.error.exception'])}</pre>
    </li>
</ul>

When using OmniFaces, the #{of:xxx} functions are available by the http://omnifaces.org/functions namespace. Also, when using OmniFaces the java.util.Date representing the current timestamp is implicitly available by #{now}.

Update: the FullAjaxExceptionHandler can be tried live on the new showcase site!

But it does not work with PrimeFaces actions! (update: from 3.2 on, it will!)

Indeed, PrimeFaces does not support a render/update of @all. Here's a cite of Optimus Prime himself:

PrimeFaces does not support update="@all" because update="@all" is fundamentally wrong.

I agree with him to a certain degree. In case of successful requests, it does indeed not make any sense. You would as good just send a normal/synchronous request instead of an ajax/asynchronous request. But in case of failed requests it would have been very useful. Of course, you could send a redirect instead by ExternalContext#redirect(), that would work perfectly fine, but you would lose all request attributes, including the error details. It is really not preferable to fiddle with the session scope or maybe even the flash scope to get them to show up in the error page.

Fortunately, there's a simple way to get PrimeFaces to support @all. Just add the following piece of JavaScript code to your global JavaScript file which should be loaded after PrimeFaces' own scripts (just referencing it by <h:outputScript> ought to be sufficient):

var originalPrimeFacesAjaxResponseFunction = PrimeFaces.ajax.AjaxResponse;
PrimeFaces.ajax.AjaxResponse = function(responseXML) {
  var newView = $(responseXML.documentElement).find("update[id='javax.faces.ViewRoot']").text();

  if (newView) {
    $('head').html(newView.substring(newView.indexOf("<head>") + 6, newView.indexOf("</head>")));
    $('body').html(newView.substring(newView.indexOf("<body>") + 6, newView.indexOf("</body>")));
  }
  else {
    originalPrimeFacesAjaxResponseFunction.apply(this, arguments);
  }
};

Update: the PrimeFaces support for update="@all" will be available with 3.2, great job, Optimus Prime!