Wednesday, March 25, 2026

OmniFaces 5.2 released!

OmniFaces 5.2 has been released! Relatively a lot of things have been added in barely 2 weeks (5.1 was kind of forcibly released 2 weeks ago because it had an important bugfix). Three new components, a new push transport, and a handful of improvements and fixes.

<dependency>
    <groupId>org.omnifaces</groupId>
    <artifactId>omnifaces</artifactId>
    <version>5.2</version>
</dependency>

You can find the complete list of additions, changes and fixes at What's new in OmniFaces 5.2? in the showcase.

New: <o:sse>

Have you ever used <o:socket> for server-to-client push and then discovered that corporate proxies or firewalls block WebSocket connections?

The new <o:sse> component provides an alternative push transport based on Server-Sent Events. It uses plain HTTP, works through any proxy or CDN, has built-in reconnect, and benefits from HTTP/2 multiplexing. No additional dependencies needed. Just async servlet support which is already available since Servlet 3.0 in 2009.

The client side looks familiar:

<o:sse channel="liveUpdates" onmessage="handleUpdate" />

The server side uses the same PushContext interface as <o:socket>, but with a new type attribute on @Push:

@Inject @Push(type = SSE)
private PushContext liveUpdates;

public void sendUpdate() {
    liveUpdates.send("Hello from SSE!");
}

Both <o:sse> and <o:socket> provide one-way (server to client) push. The key difference is the transport. SSE runs over plain HTTP so it works through HTTP infrastructure that may block WebSocket. WebSocket on the other hand is not affected by the browser's per-origin connection limit when the server does not support HTTP/2.

When the server does not support HTTP/2, SSE is the worse choice because browsers hard-limit concurrent HTTP/1.1 connections per origin (Chrome has a limit of 6), and multiple SSE channels across tabs may exhaust this limit and queue further HTTP requests. So the <f:ajax> will simply stop working when the connection limit is hit by solely SSE connections. So in case you wish to use <o:sse>, you need to make absolutely sure that your server (and proxy!) supports HTTP/2.

This is a big candidate to end up as new <f:sse> component in a future Jakarta Faces version.

New: <o:notification>

What if you could send browser notifications from the server side as easy as sending a push message? The new <o:notification> component basically extends <o:sse> with the Web Notifications API integration (which was only recently finished, at 16 March 2026). It opens an SSE connection and shows incoming push messages as browser notifications, even when the user is in another tab.

It requires the PWAResourceHandler for the service worker, and a user gesture to request permission:

<h:head>
    ...
    <link rel="manifest" href="#{resource['omnifaces:manifest.webmanifest']}" /> <!-- Activates PWAResourceHandler -->
</h:head>
<h:body>
    <button type="button" onclick="OmniFaces.Notification.requestPermission()">Enable Notifications</button>
    ...
    <o:notification channel="notifications" />
</h:body>

From the server side, inject a NOTIFICATION-typed push context and send a Notification.Message instance:

@Inject @Push(type = NOTIFICATION)
private PushContext notifications;

public void sendNotification() {
    notifications.send(Notification.createNotificationMessage("System maintenance", "The system will undergo maintenance at 22:00 UTC."));
}

You can optionally add a URL so that clicking the notification navigates to it. User-targeted notifications are supported via the user attribute.

public void sendOrderShippedNotification(@Observes OrderShippedEvent event) {
    var userId = event.getUserId();
    var orderId = event.getOrderId();
    notifications.send(Notification.createNotificationMessage("Order shipped", "Your order #" orderId + " has been shipped.", "/orders/" + orderId), userId);
}

Stacking, silent mode, and requireInteraction can be configured via boolean attributes on <o:notification>.

New: <o:scriptErrorHandler>

Ever wondered how many JavaScript errors your users are silently swallowing and therefore you're unaware which bugs they're actually facing? The new <o:scriptErrorHandler> catches uncaught JavaScript errors and unhandled promise rejections on the client and sends them to the server via navigator.sendBeacon(), where they are fired as CDI events. No additional endpoint boilerplate needed. The servlet is auto-registered when at least one CDI observer on ScriptError is present.

Just put it in the head before any other <h:outputScript> components:

<h:head>
    <o:scriptErrorHandler />
    ...
</h:head>

And observe the events in any (typically application scoped) CDI bean:

@ApplicationScoped
public class ScriptErrorObserver {

    private static final Logger logger = Logger.getLogger(ScriptErrorObserver.class.getName());

    public void onScriptError(@Observes ScriptError error) {
        logger.warning(error.toString());
    }
}

The ScriptError event provides the page URL, error message, error name, stack trace, source URL, line/column number, remote address, user agent, and user principal. Client-side deduplication prevents flooding the server with repeated errors. You can customize default deduplication via maxRecentErrors and errorExpiry attributes which default to 100 errors and 1 minute respectively.

omnifaces.taglib.xml migrated to Vdlgen

The hand-maintained omnifaces.taglib.xml file, which had grown to over 8,000 lines of basically copypasted javadoc blocks, has been completely replaced by Vdlgen. This is a new OmniFaces project: a Java annotation processor that generates the .taglib.xml file from annotations on the source code during compilation. The taglib can never anymore drift from the actual component, converter, validator or function implementation.

Components annotated with @FacesComponent will automatically have their class javadoc copied as tag description and all attribute setters will automatically have their method javadoc copied as attribute descriptions. You can optionally add Vdlgen-provided @FacesAttribute(required = true) to the setter method in order to mark it as a required attribute.

Tag handlers, which don't have annotation support out the box by Faces API, need explicit metadata annotations like this:

/**
 * Tag description.
 */
@FacesTagHandler(namespace = OmniFaces.OMNIFACES_NAMESPACE)
public class ExampleTagHandler extends TagHandler {

    /** Tag attribute description. */
    @FacesAttribute(required = true)
    private final TagAttribute type;

    /** Tag attribute description. */
    @FacesAttribute(name = "var", description = "Tag attribute description which overrides javadoc")
    private final String varValue;
}

There are also @FacesComponentConfig, @FacesConverterTag, @FacesValidatorTag, @FacesFunctions, and @FacesFunction annotations. They cover all cases that the .taglib.xml supports. Jakarta Faces own @FacesComponent and @FacesConverter and @FacesValidator annotations already by default recognized; Vdlgen just extends them with the missing metadata.

Ultimately the .taglib.xml file will be read by Vdldoc to generate the VDL documentation (like this), so you only have to write those descriptions in one place. If you maintain your own component library, one should wonder whether this makes your life easier too :)

Changes

The <o:socket> web socket endpoint URL pattern has changed from /omnifaces.push/* to /omnifaces.socket/* because "push" is now not anymore exclusively for web sockets. If you have any web.xml security constraints on the old URL pattern, you need to update them.

The @Push annotation got a new type attribute which defaults to SOCKET. The existing @Inject @Push PushContext injection points continue to work unchanged.

The <o:socket> endpoint is now auto-registered when at least one @Inject @Push PushContext appears in the source code. You no longer need to configure the org.omnifaces.SOCKET_ENDPOINT_ENABLED context parameter for this.

SocketPushContextProducer has been deprecated and replaced by PushContextProducer.

Fixes

CombinedResourceHandler: failed to strip "use strict" from combined scripts when the directive was not on its own line. (#921)

<o:socket>: threw IllegalArgumentException in Hacks#removeViewState() when session was already expired at same moment. (#937)

All these fixes are also available in 4.7.3 / 3.14.14.

AI assisted development

Large parts of this release were developed with the help of Claude Code. It was used as a pair programming partner for prototyping new components, writing unit tests, and backporting fixes across branches. All generated code was reviewed, tested, and adjusted by hand before committing. AI didn't design the features, it accelerated the implementation of decisions already made, reducing the estimated development time by more than half.

No comments: