Wednesday, July 15, 2026

OmniHai grows a backbone

OmniHai 1.5 is out! After 1.1 gave the library ears, 1.2 a voice, 1.3 the ability to step outside and browse the web, and 1.4 taught it to count the cost, 1.5 gives it a backbone.

Talking to a remote AI provider means talking to something that occasionally says no. Rate limits, a provider that is briefly down, a connection that drops halfway. None of that is your fault, and none of it should reach your users as a stack trace. Until now you had to manually wrap your own retry loop around every call. OmniHai 1.5 ships two ready-to-use resilience decorators so you never have to write that loop anymore.

Retry

Wrap any service in RetryingAIService and transient failures are retried for you. It triggers on an HTTP 429 rate limit, an HTTP 503 unavailable, and transient I/O, with exponential backoff and full jitter between attempts.

AIService resilient = new RetryingAIService(service); // 3 attempts, sensible defaults

That is the whole change. The wrapped service keeps its exact API, so every caller downstream stays the same. Need other numbers? Reach for the builder.

AIService tuned = RetryingAIService.newBuilder(service)
    .maxAttempts(5)
    .initialBackoff(Duration.ofSeconds(1))
    .maxBackoff(Duration.ofSeconds(20))
    .maxDuration(Duration.ofMinutes(1))
    .build();

It never retries a deterministic error. A bad request or an authentication failure would fail the same way on the second attempt, so retrying it only wastes time and tokens. You can override the condition with retryOn(...) when your case is different.

Failover

Retrying the same provider helps when the provider is merely busy. It does not help when the provider is down. For that there is FailoverAIService, which tries a primary service and then falls back to alternates in order, on those same transient failures.

@Inject @AI(apiKey = "#{keys.openai}")
private AIService gpt;

@Inject @AI(provider = ANTHROPIC, apiKey = "#{keys.anthropic}")
private AIService claude;

AIService resilient = new FailoverAIService(gpt, claude);
String response = resilient.chat("Explain the Jakarta EE security model.");

When OpenAI is rate limiting you, the call quietly lands on Anthropic instead. Your code asked one question and got one answer; which provider answered it is an operational detail, not an application concern.

Composing them

Both are pure decorators built on InterceptingAIServiceWrapper, so they wrap the entire service surface: chat, image, audio, moderation, synchronous and asynchronous alike. And because they are decorators, they compose. Retry each provider a few times before giving up on it and failing over to the next.

AIService resilient = new FailoverAIService(
    new RetryingAIService(gpt),
    new RetryingAIService(claude));

No CDI magic, no configuration file, no framework to buy into. Just constructors that take an AIService and return an AIService. Stack them in whatever order your situation asks for.

Streaming and partial results

Retrying a plain chat call is easy; you just call it again. Retrying a streaming call is not, because the first attempt may already have handed a dozen tokens to your consumer. A blind second attempt would replay the stream from the start and leave the consumer with a duplicated prefix. Rather than corrupt your output in silence, the decorator throws an AIStreamAbortedException, with the original failure as its cause. This exception is terminal and is never retried nor failed over.

If you do want a partially consumed stream re-attempted, hand in a ResettableConsumer as your token consumer. It carries a second handler next to the token handler, invoked right before each new attempt, so it can discard what it accumulated and let the retry start from a clean slate.

var response = new StringBuilder();
AIService resilient = new RetryingAIService(service);

resilient.chatStream("Explain the Jakarta EE security model.", ResettableConsumer.of(
    token -> response.append(token),           // append every token as it streams in
    (cause, attempt) -> response.setLength(0)  // a retry is starting; drop the partial output
));

The token handler is your normal streaming consumer. The reset handler receives the failure that triggered the re-attempt and the number of the attempt about to start, and clears the buffer so the fresh stream does not stack on top of the old one.

Refreshed default models

As always the per-provider default models have moved forward to the current generation.

ProviderDefault model
OpenAIgpt-5.6-terra
Anthropicclaude-sonnet-5
Google AIgemini-3.5-flash
xAIgrok-4.5
Mistral AImistral-medium-3-5
Meta AImuse-spark-1.1
Azure OpenAIgpt-5.5

Meta moved the most. It retired the old Llama endpoint and now serves Muse Spark through the OpenAI-compatible Meta Model API at https://api.meta.ai/v1. OmniHai follows: the default is now muse-spark-1.1, and the provider rides the shared OpenAI text handler instead of its own. If you pinned the old Llama-4-Maverick-17B-128E-Instruct-FP8 model or the api.llama.com endpoint, update those.

This generation also changed the rules. The newest Claude and Fable models (Opus 4.7 and up, Sonnet 5 and up, Fable 5 and up) dropped the classic sampling knobs; send them a temperature or a legacy thinking budget and they answer with an HTTP 400. OmniHai now knows this through supportsSamplingParameters() and simply omits those fields for such models, steering them with the ReasoningEffort from 1.4 instead. Older models keep the legacy behavior. You set your options the same way you always did; OmniHai sends whatever the target model still accepts.

Getting 1.5

Non-Maven users: download the OmniHai 1.5 JAR and drop it in /WEB-INF/lib the usual way, replacing the older version if any. Maven users: update the version.

<dependency>
    <groupId>org.omnifaces</groupId>
    <artifactId>omnihai</artifactId>
    <version>1.5</version>
</dependency>

OmniHai still needs only Java 17 and Jakarta EE 10 or MicroProfile 7, with JSON-P required and CDI, EL and MP Config optional. No new dependencies; the resilience decorators are plain Java.

Give it a try

Wrap your existing service in a RetryingAIService, or chain a couple of providers behind a FailoverAIService, and watch the transient failures stop reaching your users. As always, feedback and contributions are welcome on GitHub. If you run into anything, open an issue. Pull requests are welcome too. :)

Tuesday, July 14, 2026

Making Mojarra fast: a per-phase performance review

Mojarra 4.1.9 was measurably slower than MyFaces 4.1.3. That was the whole of issue #5753: perform a performance review of the Jakarta Faces reference implementation and close or even surpass the gap where possible. This post walks through how the review was done and what came out of it, broken down per lifecycle phase. The story throughout is 4.1.10 against the 4.1.9 release; MyFaces is the yardstick it chases. Each phase where MyFaces ran faster marked a lever to find, and a few times the fix was simply to adopt what MyFaces already did, and for the rest AI figured out it by itself. The short version: Mojarra 4.1.10 is 64% (almost 3 times) faster than the 4.1.9 release, and along the way overtook the current MyFaces 4.1.4-SNAPSHOT development build (a72f081e2) by 18%, faster on every one of six tested servers.

None of this is one clever trick. It is a few dozen small, individually-boring levers, each measured on its own, spread across the six phases of the request lifecycle. So let's first look at how they were found.

The benchmark: a WAR that touches every phase

You cannot optimize what you cannot measure, so an early deliverable of the review was a benchmark WAR under test/perf. The first cut was incomplete: it did not exercise the whole pipeline, and PROCESS_VALIDATIONS in particular was barely touched, so the real conversion and validation cost stayed invisible. So it was reworked round after round until every phase and every component was covered under a realistic load. Components like h:selectOneRadio, h:outputScript and h:message were only added in later rounds, which exposed yet more performance levers.

It is a component-family matrix. Four iterating families, h:dataTable, ui:repeat, composite components and build-time unrolled c:forEach, each spanning up to six variants: a read-only GET, a full postback with per-row inputs, a two-level nested variant, a state-restore-only "build" postback, plus ajax twins. On top of that sit flat multi-section forms (text, textarea, select, checkbox, radio over managed converters, validators and Jakarta Validation), a happy and an unhappy validation path, two dynamic-component scenarios and a couple of trivial baselines. 32 scenarios in total, each firing exactly the phases you would expect: GET-only scenarios fire Restore View and Render Response, postbacks fire all six.

A Faces PhaseListener registered in the WAR times every phase of every request and accumulates the result per scenario into a shared PerfStats. A companion servlet exposes those accumulators at /perf-stats as a fixed-width text table parseable by AI. A single integration test drives the whole thing: it resets the stats, loops thousands of GETs and postbacks against a managed app server, then reads /perf-stats at the end to dump the per-scenario, per-phase count/total/avg/min/max table. The same WAR runs against GlassFish, WildFly, TomEE, Payara, OpenLiberty and Tomcat. Every server has a -myfaces twin that swaps in MyFaces instead of Mojarra, so the exact same request stream is measured on both implementations. It lives in the test/perf module of the Mojarra source tree on the 4.1 branch. Run after cloning https://github.com/eclipse-ee4j/mojarra in the repo folder:

git switch 4.1
mvn clean install -pl impl,test/base -DskipTests
cd test/perf

Then run the perf bench using one of the following commands:

mvn clean verify -Dperf=true -Pglassfish           # Mojarra on GlassFish (default)
mvn clean verify -Dperf=true -Pwildfly             # Mojarra on WildFly
mvn clean verify -Dperf=true -Ptomee               # Mojarra on TomEE
mvn clean verify -Dperf=true -Ppayara              # Mojarra on Payara
mvn clean verify -Dperf=true -Pliberty             # Mojarra on OpenLiberty
mvn clean verify -Dperf=true -Ptomcat              # Mojarra on Tomcat
mvn clean verify -Dperf=true -Pglassfish-myfaces   # MyFaces on GlassFish
mvn clean verify -Dperf=true -Pwildfly-myfaces     # MyFaces on WildFly
mvn clean verify -Dperf=true -Ptomee-myfaces       # MyFaces on TomEE
mvn clean verify -Dperf=true -Ppayara-myfaces      # MyFaces on Payara
mvn clean verify -Dperf=true -Pliberty-myfaces     # MyFaces on OpenLiberty
mvn clean verify -Dperf=true -Ptomcat-myfaces      # MyFaces on Tomcat

Each run will take a little over a minute before it prints a fixed-width text table with the numbers. Note that an explicit -Dperf=true is mandatory to activate the PerfBenchIT, otherwise a regular mvn clean install in Mojarra repo root will unintentionally launch it as well.

The versions are not fixed either. -Dmojarra.version picks the Mojarra jar on any profile and -Dmyfaces.version picks the MyFaces jar on the -myfaces twins, so a released version can be benched against the current build without a rebuild. For example, this explicitly runs the 4.1.9 release on Tomcat instead of the current build:

mvn clean verify -Dperf=true -Ptomcat -Dmojarra.version=4.1.9

The server versions themselves are knobs too. Each profile takes a matching -Dglassfish.version, -Dwildfly.version, -Dtomee.version, -Dpayara.version, -Dliberty.version or -Dtomcat.version, and the Tomcat profile additionally takes -Dweld.version and -DhibernateValidator.version for the Weld and Hibernate Validator jars it bundles into the WAR. For example, this pins the GlassFish container version explicitly instead of using the default version hardcoded in test/pom.xml:

mvn clean verify -Dperf=true -Pglassfish -Dglassfish.version=8.0.2

The state-saving knobs, the heap size and the iteration counts are all filtered into the WAR at package time, so some predefined state-saving configurations can be applied without editing anything:

mvn clean verify -Dperf=true -Pwildfly -Dwebapp.stateSavingMethod=client

Or even custom context parameters, which you'll have to spell out fully:

mvn clean verify -Dperf=true -Ptomcat -Dwebapp.additionalContextParams='<context-param><param-name>com.sun.faces.disableIdUniquenessCheck</param-name><param-value>true</param-value></context-param>'

Every server launches with the same -Xmx1g so the cross-server comparison stays apples-to-apples.

The workflow: measure, profile, prototype, discard

The bench tells you which phase is slow. It does not tell you why. For that the same WAR doubles as a JFR driver: append the flight-recorder flags to the forked server VM, run at a tighter iteration count, and you get a per-method CPU profile and per-call-site allocation profile of the entire request stack, Mojarra, EL, CDI, Servlet, the server and the JDK all together. The jfr tool ranks the hot methods and the top allocators; a small script attributes each execution sample to a lifecycle phase by scanning its stack for the phase class.

This is where AI-assisted development earned its keep. The loop, run the A/B bench back-to-back on a quiesced machine (performance governor, no background load, no temperature throttles), profile the slow phase, read the JFR hot-method and allocation rankings, form a hypothesis, prototype a lever, rebuild both arms, remeasure, was run hundreds of times. Claude Code drove the harness, ran the mvn commands, injected the JFR flight-recorder flags into the forked server VM, generated the scripts, aggregated the raw per-phase dumps into readable delta tables, cross-checked JFR self-time against actual wall time, and kept the bookkeeping honest across dozens of candidate changes. That last part matters more than it sounds: a handful of candidate levers do not survive measurement. For example, caching the composite ValueExpression in TagAttributeImpl during Restore View looked obvious, because MyFaces caches it too, and was prototyped and reverted three separate times, each time about 2% slower, and VE creation is only ~1.7% of the phase to begin with. An attributesThatAreSet bitmask regressed buildView by ~20%. A whole class of "obvious" micro-optimizations measured at exactly zero. Only the levers that held up under a clean back-to-back A/B shipped.

A recurring trap worth calling out: JFR self-time percentage is not wall-clock cost. A phase that is thin in wall time starves for samples, and a subtree that shows 9% self-time can move the needle 0% when you actually fix it. The bench wall time, not the profiler, was always the final word.

The machine itself is the other thing that lies to you. A performance run is only as honest as the box it runs on, and a box that is busy, or hot, or on a scaling governor hands you swings the size of a real lever. The early rounds showed it plainly. WildFly came out surprisingly fast and OpenLiberty surprisingly slow, and neither had anything to do with the code: the six-server run was sequential, and a long sequential run heats the CPU until it throttles, so whichever server ran late looked slow. The fix is boring. Pin the governor to performance, drop the background load, let the machine cool between servers, and run each arm back-to-back so the two builds you compare meet the same conditions. A change that only shows up on a loaded or throttling box is not a lever; it is weather. Every number in this post was taken on a quiesced machine for that reason. In the end, all servers were within a few % from each other.

The container can lie to you too, and profiling the whole stack is what caught it. Because the JFR run captures CDI and the app server alongside Mojarra, an early GlassFish profile showed almost half of the HTTP-listener CPU going into GlassFish's own InjectionServicesImpl, not into Faces at all. Every CDI injection was re-walking the superclass and annotation hierarchy, which a view full of initially unoptimized @FacesConverter and @FacesValidator injections hits hard. That was a GlassFish bug, not a Mojarra one, and it made the initial GlassFish bench run about twice as slow. It was reported and fixed in GlassFish 8.0.3 (glassfish#26046); if you reproduce the bench on 8.0.2 you will see that inflation, so use 8.0.3 or later. It hit both implementations equally, so it never tilted the Mojarra-versus-MyFaces comparison, but it is exactly the kind of container cost a Faces-only view would have hidden.

Now the levers, phase by phase.

Restore View

Restore View rebuilds the component tree on every postback and restores its partial state. Mojarra was allocating heavily here. The biggest single lever was #5761: the descendant-id lookup used a per-tree HashMap cache that was rebuilt eagerly; it was replaced with a refresh-gated direct scan, and UIComponentBase.findComponent stopped allocating a fresh collection at every node it walked. #5819 cut further allocation on the unrolled c:forEach restore path, and #5828 memoizes the c:forEach items expression so ForEachHandler evaluates it once per phase instead of on every access. #5822 skips the duplicate-id uniqueness check on postbacks that reuse the already-restored tree, the way MyFaces does, plus two smaller lifecycle levers.

A subtle one came in via #5853: an h:selectOneRadio's @ListenerFor listener was being persisted as a needless per-component partial-state delta on every request. A single non-empty component delta is enough to force the full O(N) clientId-keyed restore walk instead of the cheap view-root-only fast path. Persisting the system-event listeners in the delta only when they actually changed brought the flat-form Restore View back to parity in isolation.

Net Restore View dropped about 65% versus 4.1.9, landing within a few percent of MyFaces; what's left is the shared buildView Facelet re-apply that MyFaces pays too. Notably the unrolled views, which carry the most components and the most per-component state of anything in the suite, are already faster than MyFaces here.

Several smaller levers round out the phase: #5764 (a dynamic-add gate, field-backed Facelets markers and ancestor memoization on the restore walk), #5778 (partial-state-saving and tree-walk trims), #5800 (cheaper per-component createComponent in buildView), #5805 (field-backed component value expressions), #5836 (an indexed Facelets tag-id lookup on refresh) and #5813 (dropping a per-component reflective composite-probe from the restore walk).

Apply Request Values

Apply Request Values decodes the submitted values onto the components. This is per-component overhead territory, and the fix is boring: #5777 reduced the fixed cost paid at every component in the pipeline. There is no single hotspot here; it is a broad, flat cost that only comes down by shaving every node a little. The phase came down about 54% versus 4.1.9, and now runs 18% faster than MyFaces across the suite; the win is dead consistent across all six servers, which is the tell that it is structural and not host-specific.

#5798 reads disabled and readonly through typed getters on decode instead of the reflective attribute map. Two broader request-pipeline levers land hardest in this phase, decisively faster on all six servers, though they touch every phase: #5757 (a FacetsMap empty short-circuit, a descriptor-map computeIfAbsent and hoisted UIData/UIRepeat restore state) and #5759 (per-component state access, a NamingContainer-ancestor cache, per-row state, indexed traversal, event publishing and render-output coalescing).

Process Validations

Of every phase this one fell the furthest against 4.1.9, by nearly 79%. It was also the closer: after the other phases had landed, Process Validations was the one still trailing MyFaces. Once the bench exposed the real conversion and validation load, it measured 44% slower than MyFaces; it is now 32% faster, and Mojarra leads in every component family. It came mainly from three PRs.

#5838 moved the java.time formatters used by f:convertDateTime to application scope instead of rebuilding them per conversion. #5840 shared a single application-scoped BeanValidator, cached the NumberConverter parser, and made UIInput's per-request validation flags (valid, localValueSet, submittedValue) cheaper: they live in transient state, and a default value now costs no map lookup or allocation, so an input whose flags are all default holds no transient map at all, saving a per-input map hash across a table's rows. An intermediate experiment had field-backed those flags as plain fields instead; a MyFaces maintainer rightly flagged that as fishy in the review thread, since per-request lifecycle state does not belong in persisted fields, so it was reverted to transient state and the transient get/put path was optimized instead.

The big one was #5847. Mojarra re-resolved the EL root once per input per validation, and each re-resolution allocated a fresh CDI CreationalContext. On a form with dozens of inputs that is dozens of pointless CDI allocations per request. After the fix, UIInput.validate also skips reading the previous value when nothing observes a value change, and the bean property type is taken from the already-resolved ValueReference instead of a second BeanValidator.getType walk. The form-* family, which carries all the new conversion work the bench added, dropped from behind MyFaces to parity, and the composite family, which had been the single biggest deficit anywhere in the suite at +18%, inverted to 28% faster once the composite cc.attrs EL depth was addressed with #5843 and #5847 (composite component stacks backed by ArrayList, non-contributing ELResolvers skipped, the postback check gated behind the composite check).

Four more levers finish the phase: #5767 (eliminating nested UIData/UIRepeat per-row state cost), #5782 (a guard plus per-attribute memoization in place of the composite-expression cache), #5831 (converter formatter caching and reuse of the built-in by-type converters) and #5834 (a cached parse formatter in DateTimeConverter).

Update Model Values

Update Model Values pushes the validated values into the backing beans, so it walks the same per-component and EL resolution paths as Apply Request Values and Process Validations. It did not get its own dedicated PR; it rode along on the per-component overhead work behind Apply Request Values and the validation EL work behind Process Validations, coming down about 63% versus 4.1.9 and landing ~9% faster than MyFaces. Worth naming as its own phase because that shared-cost effect is the point: a per-component or EL-resolution lever fixed once pays off in every phase that walks the tree.

Invoke Application

Invoke Application is sub-100µs per request in every scenario except the dynamic-component ones which programmatically modify the component tree during an action, so in absolute terms it is too small to matter; the aggregate percentage is a rough indicator only. But the dynamic add/remove machinery the dynamic-component scenarios drive was genuinely broken and got a cluster of fixes. #5783 made dynamic-component restore O(N) instead of O(N²). #5785 fixed a regression where a full-tree clientId index was being built on every postback even when there were zero dynamic actions, and trimmed CDI and event-dispatch overhead on the same path. #5853 landed two more: skipping the redundant dynamic-child reorder in buildView when the children are already in order, and trimming the add/remove path further while removing the dead TREE_HAS_DYNAMIC_COMPONENTS view-root flag that had been write-only since #5761 (for Restore View) dropped its only reader. Finally #5791 trims further per-component overhead on that same add/remove path.

One dynamic case is deliberately left alone, and it comes down to how each impl carries dynamic structure across a postback. Rather than persist an added subtree as full component state the way MyFaces does, Mojarra records each add or remove as a compact action (a ComponentStruct) and replays that action list when the view is rebuilt. It defers that replay to Render Response, because an Invoke Application action may still navigate away; deferring means Mojarra only ever re-applies the actions to the view it actually renders, never one it discards. The subtree itself is fully in the tree from Restore View onward, so nothing is withheld from decode, validate or update; only the render-time re-apply is deferred. That is why the dynamic-toggle-ajax scenario, which toggles on the same view, renders slower than MyFaces while its Restore View is faster; net it is a wash. A sound state-model trade, not a defect, and "fixing" it would break the navigation case.

Render Response

Render Response is the heaviest phase in absolute terms, so even modest percentages here move the suite total more than anything else. It came down about 58% versus 4.1.9 and is now 20% faster than MyFaces on all six servers. The largest lever was #5812: for static views Mojarra was re-applying the Facelet at render time even though nothing had changed since the build, a redundant tree walk that was skipped. #5796 attacked a subtler, per-component cost: the renderers were reading standard attributes like styleClass through getAttributes().get(...), which routes through the reflective AttributesMap, on every component they encoded. Casting to the concrete component subclass and calling its typed getter (getStyleClass() and friends) skips the reflection entirely, and the read is skipped altogether when the attribute is unset. #5822 does the same for the pass-through attribute sweep, which is skipped when a component declares none. Finally #5785 also lives partly here: a @FacesConverter(managed=true) was being re-resolved through CDI for every single table cell, which a small Bean cache eliminated, and the per-event listener dispatch got a fast path that took it off the top of the allocation profile.

The remaining Render Response levers are a spread of per-component and output trims: #5752 (caching CDI bean resolution to remove per-render BeanManager lookups), #5755 (response-writer range-emit and buffer elimination), #5770 (a lazy state map, UIOutput converter, rendererType and AttributesMap getter), #5793 (per-component encode and view-build overhead), #5825 (skipping the render re-apply of static c:forEach and deduping ViewScope FacesContext lookups) and #5839 (emitting an event-handler attribute only once when set via expression).

The numbers

Six servers, default settings (partial state saving, server state saving, Production stage), 1000-run suite, JDK 21, all three implementations built back-to-back on a quiesced machine. Values below are aggregated across servers; negative means Mojarra 4.1.10 is faster.

phasevs Mojarra 4.1.9vs MyFaces
Restore View−65%−4%
Apply Request Values−54%−18%
Process Validations−79%−32%
Update Model Values−63%−9%
Invoke Application−56%+22% †
Render Response−58%−20%
whole suite−64%−18%

† Invoke Application is sub-100µs per request outside the dynamic-toggle-ajax scenario, so its aggregate percentage is a rough indicator, not a real cost.

The vs-4.1.9 column is the cumulative gain since the last release: 64% faster, dead consistent across every clean server at −64 to −65%. The vs-MyFaces column is measured against the current MyFaces 4.1.4-SNAPSHOT development build (a72f081e2), itself 22% faster than its own 4.1.3 release, so this is Mojarra ahead of MyFaces at its best, not against a stale yardstick. The wins are structural: the tight cross-server spread on Apply Request Values and Render Response is what tells you these are real code changes and not one lucky host.

The same levers ship across all three active release lines: authored on the 4.1 branch or backported from master, they are all present in 4.1.10, backported to 4.0.19, and forward-ported to 5.0.0-M3.

What made the iteration fast

A performance review is only as fast as your validation loop, because every lever has to pass the full Jakarta Faces TCK before it can ship. A lever that shaves 3% is worthless if it quietly breaks a spec-mandated edge case, and the only way to know is to run the TCK, all of it.

Not long ago that meant hours. The Faces TCK ran the old JavaTest harness through repeated GlassFish cold starts and took over three hours end to end, which is fatal to an iterate-measure-validate loop; you get one or two validated levers per day and you lose your train of thought between them. The GlassFish pool work described in From hours to minutes: GlassFish pool for Jakarta EE TCKs brought that same TCK down to under four minutes with a reusable, leased server pool and parallel execution.

Four minutes changes the character of the work. A candidate lever could be prototyped, benched, and TCK-validated within an hour, so it was cheap to try a change, measure it, and throw it away when it did not pan out; and, as noted, a handful did not. The speed of the TCK is what let the review be exhaustive rather than conservative. 64% faster than the previous release is the combined result of being able to afford that many attempts in a relatively short time.

The fast TCK was only half of the loop; the other half was that same Claude Code workflow on the authoring side. Drafting a candidate lever, then forward- and backporting it across the release lines once it held up, is mechanical work that scales badly by hand. The judgement stayed human: which lever is worth shipping, whether a change is spec-legal, and reading the TCK output when a change was not. What came off your hands was the bulk, and for a review this wide that bulk is most of the hours.

The traffic went both ways. Digging into why Restore View was slow surfaced a corner case worth guarding: a UICommand bound into an h:dataTable via binding must fire its action exactly once per click instead of multiple times, and an earlier Restore View fix (#4128) was the thing keeping it that way. Analyzing that fix, because it badly impacted performance, turned up that its regression test, which had guarded exactly this behaviour, was nowhere in the TCK: it had been dropped from Mojarra's own test tree in the 3.0-to-4.0 migration and never migrated across. So it was ported back into the TCK as jakartaee/faces#2179, so that no Restore View optimization could quietly reintroduce the double-fire.

That one test exposed a bigger gap: Mojarra's old suite of 577 integration tests had been dropped wholesale in that migration, and only a part was ever migrated into the TCK, because porting each one by hand was too much work to justify. With AI assistance it no longer was. jakartaee/faces#2181 resurrected 138 of the spec-relevant ones with help of Claude Code, filtering out the ones not yet covered by the existing tests, each ported from the old HtmlUnit harness to Selenium, modernised to Jakarta namespaces and CDI, and named after the issue it covers. All within hours instead of weeks. Those resurrected tests promptly caught three real Mojarra regressions, unrelated to the performance work, fixed in #5774. And the pool swallowed all 138 new tests without complaint: the full 6026-test 5.0 TCK still finishes in less than four minutes at my machine with -T8.

The bottom line

"Mojarra is slow" was a fair thing to say against 4.0.18, 4.1.9 and 5.0.0-M2 or earlier. Now it does not hold anymore. Since 4.0.19, 4.1.10 and 5.0.0-M3, Mojarra is not just almost 3 times faster than its own previous release, it runs ahead of MyFaces at its best, on every one of six tested servers. If you last benchmarked Mojarra a release or two ago and wrote it off, the number you remember is stale; the reputation outlived the reality, and the numbers have caught up.

Appendix: all levers at a glance

Every lever from the sections above, collected in one place and grouped by the phase it moves most. All are present in 4.0.19, 4.1.10 and 5.0.0-M3: those authored on the 4.1 branch landed directly; those authored on master reached the release lines as a backport. A handful of small regression-guard fixes that accompanied the perf work are omitted.

phasePRwhat it does
Restore View#5761Descendant-id HashMap cache replaced with a refresh-gated direct scan; findComponent stops allocating per node
#5764Dynamic-add gate, field-backed Facelets markers, ancestor memoization on the restore walk
#5778Partial-state-saving and component-tree-walk improvements
#5800Cut per-component createComponent overhead in buildView
#5805Field-back component value expressions to cut restore cost
#5813Drop the per-component composite-probe reflective lookup from the restore walk
#5818Reduce restore allocation on JSTL c:forEach views
#5821Skip the id-uniqueness check on tree-reusing postbacks, plus two more view-lifecycle levers
#5828Memoize the c:forEach items expression per phase
#5835Index the Facelets tag-id lookup on refresh
#5853Persist system-event listeners in the partial-state delta only when changed
Apply Request Values#5757FacetsMap empty short-circuit, descriptor-map computeIfAbsent, UIData/UIRepeat restore-state hoist, indexed child traversal
#5759Per-component state/property access, NamingContainer-ancestor cache, per-row state, indexed traversal, event publishing, render-output coalescing
#5777Reduce per-component request-pipeline overhead
#5798Read disabled/readonly via typed getters on decode; less StateHelper and reflective overhead
Process Validations#5767Eliminate nested UIData/UIRepeat per-row state cost
#5782Replace the composite-expression cache with a guard plus per-attribute memoization (cc.attrs)
#5829Reuse the built-in by-type converters per target class
#5831Cache converter formatters/parsers
#5838Move the java.time f:convertDateTime formatters to application scope
#5840Shared BeanValidator, cached NumberConverter parser, lazy transient UIInput state
#5843EL resolution, composite component (ArrayList-backed stacks, non-contributing ELResolver skip) and component-id
#5847No per-input EL-root re-resolution; skip the previous-value read when unobserved; bean type from the ValueReference
Invoke Application#5783Dynamic component add/remove restore made O(N) instead of O(N²)
#5785Dynamic-action zero-actions regression fix, plus CDI and event-dispatch overhead
#5791Trim per-component overhead on the dynamic add/remove path
#5848Skip the redundant dynamic-child reorder; trim the add/remove path
#5851Remove the dead TREE_HAS_DYNAMIC_COMPONENTS flag, collapse the DYNAMIC_CHILD_COUNT counter
Render Response#5752Cache CDI bean resolution to eliminate per-render BeanManager lookups
#5755Response-writer range-emit and buffer elimination
#5770Lazy state map, UIOutput converter, rendererType, AttributesMap getter
#5793Reduce per-component overhead in encode and view build
#5796Read renderer attributes (e.g. styleClass) via typed getters instead of the reflective AttributesMap; skip when unset
#5811Skip the render-time Facelet re-apply for static views
#5824Skip the render re-apply of static c:forEach; dedup ViewScope FacesContext lookups
#5839Render an event-handler attribute only once when set via expression

All of it was validated on GlassFish, WildFly, TomEE, Payara, OpenLiberty and Tomcat. The full breakdown, round by round, is on issue #5753.

OmniFaces 5.4 released, now compatible with Faces 5.0

OmniFaces 5.4.1 has been released! This is the first OmniFaces version which is compatible with Jakarta Faces 5.0, while still keeping the Faces 4.1 minimum of the whole 5.x line. In other words, one and the same JAR runs on Jakarta EE 11 (Faces 4.1) as well as on the upcoming Faces 5.0. Next to that there are a few new features, two deprecations and two notable fixes.

New: Jakarta Faces 5.0 compatibility

Until now the whole OmniFaces 5.x line required Jakarta Faces 4.1 as minimum and was not verified against Faces 5.0. As of 5.4 the integration test suite also runs against both Mojarra 5.0.0-SNAPSHOT and MyFaces 5.0.0-SNAPSHOT on Tomcat, next to the existing Faces 4.1 runs. Note that these are still snapshots; Jakarta Faces 5.0 has not been finalized yet, so consider this a first compatibility milestone rather than a guarantee against the final release. The changes needed to span both generations turned out to be small; OmniFaces only had to catch up with the in Faces 5.0 renamed implementation packages and with a changed rendering of the on* attributes. The minimum stays at Faces 4.1, so upgrading to 5.4 is safe on Jakarta EE 11 while you have the opportunity to move to Faces 5.0 whenever you want.

New: OmniFaces.Ajax.validationFailed

Ever needed to know at the client side whether a Faces ajax request failed on validation? Until now you had to inspect the returned partial response or add a hidden component whose changed value acted as a flag. As of 5.4, each OmniFaces ajax response exposes FacesContext#isValidationFailed() to the client side as a boolean OmniFaces.Ajax.validationFailed. So your JavaScript can react to a validation failure without any server or DOM round-trip.

if (OmniFaces.Ajax.validationFailed) {
    // Do your thing.
}

This works out of the box; there is nothing to configure. See also Ajax in the showcase. (#955)

New: org.omnifaces.CDN_RESOURCE_HANDLER_EXCLUDED_RESOURCES

The CDNResourceHandler rewrites resource URLs to a CDN host. When you use a wildcard mapping for a whole library, it may happen that a specific resource of that library is not actually hosted on the CDN (a typical example is PrimeFaces dynamiccontent.properties). As of 5.4 you can exclude such resources from rewriting via the new context parameter org.omnifaces.CDN_RESOURCE_HANDLER_EXCLUDED_RESOURCES. It takes a comma separated list of libraryName:resourceName identifiers which are then served as-is by the default Faces resource handler. The match is exact; wildcards are not supported here.

<context-param>
    <param-name>org.omnifaces.CDN_RESOURCE_HANDLER_EXCLUDED_RESOURCES</param-name>
    <param-value>primefaces:dynamiccontent.properties</param-value>
</context-param>

See also CDNResourceHandler in the showcase. (#954)

New: query params in FullAjaxExceptionHandler error pages

The FullAjaxExceptionHandler forwards to the error page declared in web.xml. Until now the declared <location> could only be a plain path. As of 5.4 you can add query params to it and they are honored on the forward.

<error-page>
    <exception-type>java.lang.NullPointerException</exception-type>
    <location>/WEB-INF/errorpages/general.xhtml?type=NPE</location>
</error-page>

See also FullAjaxExceptionHandler in the showcase. (#962)

Improved: OnDemandResponseBufferFilter

The OnDemandResponseBufferFilter, which backs <o:cache>, previously buffered only responses written via the Writer. It now also buffers responses written via the OutputStream. This was never a problem in practice; it was a long-standing TODO, addressed so the filter is reusable for binary or streamed responses too. (#959)

Deprecated: <o:selectItemGroups>

The <o:selectItemGroups> component has been deprecated. Faces 4.0 introduced a standard <f:selectItemGroups> which is functionally equivalent, so the OmniFaces one is not needed anymore. As of 5.4 a warning is logged once at runtime, and the component is marked for removal in a future version. Replace it with the standard <f:selectItemGroups>. (#958)

Deprecated: @Param without @Inject

Using @Param without @Inject on the same field has been deprecated. As of 5.4 a warning is logged at deployment time for each affected field. Add @Inject next to @Param to get rid of the warning. (#960)

Fixes

<o:inputFile> would duplicate the client side validation script in its onchange when the component was re-rendered. This has been fixed; the script is now added only once. (#963)

@ViewScoped could lose an active bean under concurrent requests. When the LRU eviction of the view scope bean storage kicked in, it could destroy a bean storage that was still actively used by a concurrent request. This has been fixed; a storage in active use is never anymore destroyed. (#966)

Installation

Non-Maven users: download OmniFaces 5.4.1 JAR and drop it in /WEB-INF/lib the usual way, replacing the older version if any.

Maven users: use the following coordinates.

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

The <o:inputFile> and @ViewScoped fixes (#963 and #966) have also been backported to 4.x and 3.x, so OmniFaces 4.7.11 and OmniFaces 3.14.22 have been released as well. The Jakarta Faces 5.0 compatibility, the new features and the deprecations are exclusive to 5.4.1.

For the complete list of additions, changes and fixes, see What's new in OmniFaces 5.4.1? in the showcase.

Thursday, June 4, 2026

From hours to minutes: GlassFish pool for Jakarta EE TCKs

Jakarta EE TCKs are notoriously slow. The bulk of the wall clock is not test execution but GlassFish cold-start: every test module unpacks a dist, boots a domain, deploys, undeploys, and stops the domain. With one hundred-ish test modules and several seconds of start/stop per module, a full TCK run easily takes hours. The arquillian-glassfish-server-pool module and the glassfish-pool-maven-plugin, both released as part of OmniFish arquillian-container-glassfish 2.2.0, eliminate that overhead by sharing a pool of pre-started GlassFish instances across the entire reactor.

The proof of concept: Faces TCK

The Jakarta Faces TCK historically consisted of two parts. The "old TCK" was the original Oracle suite: an Ant-driven JavaTest harness inherited from the JSF 1.x days, with around 5000 tests. The "new TCK" was the body of contributed and later-added tests built on JUnit + Arquillian, optionally with HtmlUnit or Selenium for browser interaction. Running both ends to end easily took over 3 hours on Jenkins CI, dominated by the old TCK.

Folding the old TCK into the new-TCK style was always the goal, but per-test manual conversion was prohibitively cumbersome; AI-assisted development is what finally made it feasible. Pull requests #2145, #2146, #2147, #2149, and #2150 mechanically migrated the entire old TCK, with Claude Code doing the bulk of the rewriting and consolidating the remaining HtmlUnit assertions onto Selenium along the way. WAR consolidation (one WAR per feature group instead of one per test) brought wall clock down to ~1h.

The second step was an in-house gf-pool prototype (#2156) that pre-started a pool of GlassFish instances and leased one slot per failsafe-forked JVM. With mvn clean verify -T8 (8 threads), the full Faces TCK reactor now finishes in under 4 minutes. The prototype proved the model works; the natural next step was extracting it into a reusable Maven plugin so other TCKs do not have to copy-paste the wiring.

PhaseLinuxMacBookJenkins
Pre-migration02:57 h02:53 h03:19 h
Post-migration01:05 h40:52 m01:18 h
With gf-pool3:46 m (-T8)4:47 m (-T5)13:06 m (-T2)

Linux: Intel Core i9-10900X with 32GB
MacBook: M1 Pro 10 Core with 16GB
Jenkins: Eclipse Jiro with "2 CPU" and "8 GB"

What gf-pool is

The pool ships in two artifacts:

  • arquillian-glassfish-server-pool: a runtime Arquillian DeployableContainer that leases a slot for the duration of a test JVM and deploys against the leased slot's DAS through the standard CommonGlassFishManager.
  • glassfish-pool-maven-plugin: lifecycle goals (up, down, provision, status, nuke) that provision and start slots before integration-test and stop them after.

Provisioning clones a single source GlassFish install into slot-1/, slot-2/, …, rewrites each slot's domain.xml so its ports land in a non-overlapping window (adminBase + (slot - 1) * portStride), and starts every slot in parallel. Each test JVM acquires an exclusive FileChannel.tryLock() on slot-N/lock, reads slot-N/ports.properties, and holds the lock for the JVM's lifetime. The lease protocol is pure Java; there's no -javaagent, no surefire argLine plumbing, and no shell scripts.

The pool grows on demand. A sequential build uses one slot; mvn clean verify -T4 grows to four; -T8 grows to eight. A JVM shutdown hook installed on Maven's own JVM stops every slot at session end (or on Ctrl+C), so no orphaned processes survive a hard build failure.

The optimal -TN for your machine is bounded by available RAM, not by core count. Each slot is a full GlassFish JVM plus a failsafe-forked test JVM, so the dominant cost is heap and resident memory, not CPU. Moreover, if your TCK drives a browser (as Faces does), each slot also spawns its own Chrome plus chromedriver, which pushes the total to ~1.5GB per slot. A 16-core box with 16GB will usually thrash at -T8 while a 8-core box with 32GB happily handles -T8; pick N by watching resident memory and swap, not number of processors.

Maven setup

Two plugin blocks: run the pool plugin (which resolves and unpacks GlassFish itself), and point failsafe at the same <poolDir> and <poolSource>.

<build>
    <plugins>
        <plugin>
            <groupId>ee.omnifish.arquillian</groupId>
            <artifactId>glassfish-pool-maven-plugin</artifactId>
            <version>2.2.0</version>
            <configuration>
                <poolDir>${project.build.directory}/pool</poolDir>
                <poolSource>${project.build.directory}/dist/glassfish9</poolSource>
                <distribution>
                    <groupId>org.glassfish.main.distributions</groupId>
                    <artifactId>glassfish</artifactId>
                    <version>9.0.0-M2</version>
                    <type>zip</type>
                </distribution>
            </configuration>
            <executions>
                <execution><id>pool-up</id><goals><goal>up</goal></goals></execution>
                <execution><id>pool-down</id><goals><goal>down</goal></goals></execution>
            </executions>
        </plugin>

        <plugin>
            <artifactId>maven-failsafe-plugin</artifactId>
            <configuration>
                <systemPropertyVariables>
                    <gf.pool.dir>${project.build.directory}/pool</gf.pool.dir>
                    <gf.pool.source>${project.build.directory}/dist/glassfish9</gf.pool.source>
                </systemPropertyVariables>
            </configuration>
        </plugin>
    </plugins>
</build>

The <distribution> block tells the plugin to resolve the named artifact through your usual Maven repositories and unpack it under ${project.build.directory}/dist before provisioning runs. Staging is idempotent: re-runs fast-exit when the marker file written after a successful unpack is still present.

Add the runtime as a test-scope dependency:

<dependency>
    <groupId>ee.omnifish.arquillian</groupId>
    <artifactId>arquillian-glassfish-server-pool</artifactId>
    <version>2.2.0</version>
    <scope>test</scope>
</dependency>

No arquillian.xml is needed. The failsafe <systemPropertyVariables> above forward gf.pool.dir and gf.pool.source to the test JVM, and the container adapter reads them at start() when it leases a slot. Drop in an arquillian.xml with <container qualifier="glassfish-pool"> only if you need to override inherited fields like adminPassword, or if you're running against a hand-staged pool without the plugin.

That's it. mvn clean verify works sequentially; mvn clean verify -T8 fans out across eight slots.

Need to peek at the pool? mvn glassfish-pool:status in a separate terminal redraws a top-style table once per second:

mvn glassfish-pool:status command output

Overlays

For TCK-style builds that test a SNAPSHOT impl jar against a released distribution (or vice versa), the plugin can copy overlay jars into glassfish/modules/ after unpack and before slot cloning. Declare zero or more <overlay> blocks:

<configuration>
    <overlays>
        <overlay>
            <groupId>org.glassfish</groupId>
            <artifactId>jakarta.faces</artifactId>
            <version>5.0.0-SNAPSHOT</version>
            <destFileName>mojarra.jar</destFileName>
        </overlay>
    </overlays>
</configuration>

Each overlay accepts a <skip> child that wires to your existing per-profile property switches, so you can hold back individual jars per release line without forking the pom.

Bring your own unpack

If you'd rather use maven-dependency-plugin for the unpack (e.g. because your build already has one for unrelated reasons), drop the <distribution> block from the pool plugin and add a regular unpack execution that lands in the same directory <poolSource> points at:

<plugin>
    <artifactId>maven-dependency-plugin</artifactId>
    <executions>
        <execution>
            <id>unpack-glassfish</id>
            <phase>process-test-classes</phase>
            <goals><goal>unpack</goal></goals>
            <configuration>
                <artifactItems>
                    <artifactItem>
                        <groupId>org.glassfish.main.distributions</groupId>
                        <artifactId>glassfish</artifactId>
                        <version>9.0.0-M2</version>
                        <type>zip</type>
                        <outputDirectory>${project.build.directory}/dist</outputDirectory>
                    </artifactItem>
                </artifactItems>
            </configuration>
        </execution>
    </executions>
</plugin>

The pool plugin will then skip staging and clone slots directly from ${project.build.directory}/dist/glassfish9.

JVM system properties at slot boot

Some properties have to be on the GF JVM at startup. The canonical example is javax.net.ssl.trustStorePassword: a PKCS12 truststore needs the password before SSL is used for the first time, because Java caches the default SSLContext after first use and never reloads from disk. The plugin's <systemProperties> hook bakes each key=value into every <java-config> of each slot's domain.xml at provisioning time:

<configuration>
    <systemProperties>
        javax.net.ssl.trustStorePassword=changeit
        java.awt.headless=true
    </systemProperties>
</configuration>

Adoption: Security TCK

The Jakarta Security TCK followed the same old-TCK / new-TCK split as Faces: a JavaTest "old TCK" from Oracle plus a JUnit + Arquillian "new TCK" of later contributions. The combined suite ran in just under 13 minutes. Unlike Faces, the old-TCK side wasn't the bottleneck: it deployed its apps onto a single long-running GlassFish domain and replayed all 83 of its JavaTest clients against them, so it was already efficient on its own. Migrating it was therefore not about runtime; it was about consolidating on a single test harness. Pull request #365 did exactly that, mechanically rewriting the old TCK into the new-TCK style with Claude Code doing the bulk of the assertion work. The unavoidable trade-off is that each migrated test now spins up its own Arquillian-managed GlassFish instead of sharing one domain, so single-threaded runtime nearly doubled to ~24 minutes. That is the cost of trading a shared harness for per-test isolation. Pull request #368 recovers that cost (and then some) by swapping the JVM-scoped arquillian-glassfish-server-managed container for arquillian-glassfish-server-pool. With mvn clean verify -T8, the Security TCK now finishes in under 2 minutes.

Per-test isolation is a much bigger deal for Faces, whose old TCK has ~5000 tests and takes 2+ hours on a single shared GlassFish; there the post-migration single-threaded runtime would be prohibitive without the pool. Security is the small-scale case where you can see the trade clearly; Faces is where parallelism stops being optional.

PhaseLinuxMacBookJenkins
Pre-migration12:50 m9:38 m19:05 m
Post-migration23:42 m16:34 m
With gf-pool1:49 m (-T8)1:30 m (-T5)

Linux: Intel Core i9-10900X with 32GB
MacBook: M1 Pro 10 Core with 16GB
Jenkins: Eclipse Jiro with "2 CPU" and "8 GB"

Parallelism: what you'll discover

Moving to a shared pool surfaces parallelism issues that a sequential build hides. The Security TCK migration is a good cross-section. None of these are pool bugs; they're latent contracts that finally see daylight when two slots run side by side.

Hardcoded ports. Tests that embed an LDAP server, a Tomcat instance, or any other side-process on a fixed port collide as soon as two slots co-run. Pick distinct ports per module, or derive them from the slot index (gf.pool.slot is published as a system property by the leaser). The Security TCK's embedded LDAP modules split 33389 onto 33390 and 33391 for its two extra app-ldap variants.

Hardcoded URLs. Tests that publish http://localhost:8080/... URLs to an external party (OAuth callback URIs, OIDC issuer metadata) break the moment the slot's HTTP port is anything but 8080. Replace literals with UriInfo-derived or request-derived URLs at runtime, so the URL tracks the slot's actual HTTP port.

Pre-registered redirect URIs for every slot. External identity providers that need redirect URIs pre-registered (Mitre OIDC in the Security TCK case) have to be told about every slot the pool may grow to. Maven exposes ${session.request.degreeOfConcurrency} as the -TN value; bsh-property can promote it to a regular property if your plugin only consumes typed properties.

Cross-app singletons. A java:global/ DataSource shared across apps, or any other JNDI/CDI/resource a previous app deploy leaves behind, can leak into the next app's lookup on the same slot. The Security TCK adoption uncovered a related upstream GlassFish bug where ComponentEnvManagerImpl.getResourceId returned an empty string for ScopeType.GLOBAL, fixed in eclipse-ee4j/glassfish#26029. Worth re-running your TCK against this fix if you exercise cross-app global resources.

Persisted state across re-runs. Anything written to work/, sessions/, or other on-disk caches survives a slot's lease release and can resurrect into the next consumer. If your test relies on a known starting state or an existing HTTP session variable, wipe the relevant directories on container start.

Aggregator goals on the reactor root. Goals like failsafe-report-only, cyclonedx:makeAggregateBom, or install-file bound to a per-module phase stall the -T reactor because Maven serialises them across modules. Move these to inherited=false on the reactor root only.

For Jakarta EE TCK maintainers

If your Jakarta EE TCK still drags a JavaTest "old TCK" alongside its JUnit + Arquillian "new TCK", or runs sequentially against a freshly-unpacked GlassFish per module, the migration path is the one Faces and Security walked. First, fold any remaining old TCK into the new-TCK style (JUnit + Arquillian, plus Selenium if your tests drive a browser, as Faces does); AI-assisted development handles the mechanical rewriting well enough that the bulk of the work is reviewing diffs, not writing them. Then wire arquillian-glassfish-server-pool and glassfish-pool-maven-plugin in. The result is a TCK that finishes in minutes instead of hours, and a build that still runs sequentially under mvn clean verify for vendors who prefer that.

The README at glassfish-pool-maven-plugin/README.md documents the full configuration surface; the working example at integration-tests/src/it/pool is ~150 lines of pom and runs as a smoke test in the project's own CI. Both Faces TCK (faces#2165) and Security TCK (security#368) are open and worth studying as real-world consumers.

Thursday, April 23, 2026

OmniFaces 5.3 released!

OmniFaces 5.3 has been released!

This is a relatively small feature release on top of 5.2. One new component has been added, and the rest of the changes are under the hood for better long-term maintenance: automated code formatting, a simpler and faster JavaScript build, and reorganized TypeScript sources. You can find the complete list of additions, changes and fixes at What's new in OmniFaces 5.3? in the showcase.

New: <o:lazyPanel>

Ever had a page with an expensive region below the fold which the user may never scroll to, but which gets built on every page load anyway? The traditional workaround is to wire up an IntersectionObserver in custom JavaScript and fire an ajax request yourself, which is a lot of boilerplate for something so common.

The new <o:lazyPanel> defers rendering of its children until the panel has scrolled into view:

<o:lazyPanel>
    <h:dataTable value="#{bean.expensiveList}" var="row">
        ...
    </h:dataTable>
</o:lazyPanel>

On initial render, the component writes a wrapper element with an optional placeholder and schedules a viewport intersection listener on it via OmniFaces.js, which uses IntersectionObserver when available and falls back to scroll/resize/orientationChange listeners otherwise. As soon as the wrapper intersects the viewport, a single faces.ajax.request targeting its own client id is fired. The component then flips its loaded flag, optionally invokes a listener bean method with a LazyPanelEvent, and renders its children in place of the placeholder.

The loaded attribute is a server-side escape hatch: when true, the children are rendered immediately without any client side observer. This is useful for print views, SEO crawlers, or tests.

<o:lazyPanel loaded="#{bean.printPreview}">
    ...
</o:lazyPanel>

Nested <f:param> or <o:param> children are sent along with the lazy panel ajax request, so that a single listener can serve multiple panels by distinguishing on an entity id, filter key, or page number:

<o:lazyPanel listener="#{bean.preload}">
    <f:param name="productId" value="#{product.id}" />
    ...
</o:lazyPanel>

The listener can read them via Faces#getRequestParameter(). Parameter values are evaluated at initial render (snapshot semantics), consistent with UIParameter usage elsewhere in Faces.

The closest equivalent in PrimeFaces is <p:outputPanel deferred="true" deferredMode="visible">, which loads its contents once the panel is scrolled into view. Under the hood however it uses jQuery scroll handlers on the window combined with $.offset() and window height math, which fires on every scroll event and scales poorly when you have multiple deferred panels on the same page. <o:lazyPanel> uses the native IntersectionObserver which is browser-native, more efficient, and only observes the panel itself; it falls back to scroll/resize/orientationChange listeners only when IntersectionObserver is unavailable. <o:lazyPanel> also has no jQuery or PrimeFaces runtime dependency, it's just a standard faces.ajax.request, so it works in vanilla Faces applications without PrimeFaces. On top of that, <o:lazyPanel> supports <f:param>/<o:param> for passing context to the listener, which <p:outputPanel> does not natively offer.

The homegrown alternative is to wire up an IntersectionObserver yourself which then calls a <h:commandScript> or <p:remoteCommand> from the intersection callback, and to manually swap placeholder markup on response. This works, but it's imperative JavaScript scattered across the view, and you'll have to repeat it for every lazy region. <o:lazyPanel> is the declarative equivalent: one tag, no JavaScript, and the placeholder and listener are just regular Faces markup.

Under the hood

Relatively a lot of things have been cleaned up in the build and source tree. These have no impact on runtime behavior, but they do make the project easier to maintain and contribute to:

  • Automated code formatting via Spotless and Stylistic; all Java, XML, XHTML and TypeScript sources are now formatted consistently on every build. This avoids inconsistently formatted source code coming in with pull requests.
  • The TypeScript sources have been reorganized into their own src/main/ts subfolder. This keeps the context of src/main/webapp clean.
  • The JavaScript build has been improved: browserify and closure-compiler-maven-plugin have been replaced by esbuild for performance and simplicity.
  • Vdlgen now also runs during Eclipse incremental builds, so workspace resolution into sandbox projects continues to work.

Fixes

MultiViews welcome file resolution failed on Windows-based servers due to wrong parent path handling. This has been fixed (#949).

<o:validateBean> did not collect nested properties of @Valid-annotated beans, so validation could silently skip nested constraints. This has been fixed (#951).

@ViewScoped unload threw a NullPointerException during pending view state removal in the specific combination of Spring WebFlow with MyFaces. This has been fixed (#952).

Installation

Non-Maven users: download OmniFaces 5.3.4 JAR and drop it in /WEB-INF/lib the usual way, replacing the older version if any.

Maven users: add below entry to pom.xml, replacing the older version if any.

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

The 5.3.4 fixes have also been backported to 4.x and 3.x, so OmniFaces 4.7.10 and OmniFaces 3.14.21 have been released as well.

Monday, April 20, 2026

OmniHai counts the cost

OmniHai 1.4 is out! After 1.1 gave the library ears, 1.2 a voice, and 1.3 the ability to step outside and browse the web, 1.4 teaches it to count. Token usage becomes actual money, runaway spend can be capped, reasoning effort is now dial-able across providers, and ChatOptions knows how to serialize itself to portable JSON.

<dependency>
    <groupId>org.omnifaces</groupId>
    <artifactId>omnihai</artifactId>
    <version>1.4</version>
</dependency>

Cost Calculation

1.3 introduced ChatUsage so you could see how many tokens a call consumed. Useful, but tokens are not what the invoice at the end of the month is denominated in. 1.4 closes that gap with ChatPricing and ChatCost.

Attach a pricing to your ChatOptions, make a call, read back the cost:

ChatPricing pricing = new ChatPricing(
    new BigDecimal("3.00"),       // input price per 1M tokens
    new BigDecimal("0.30"),       // cached-input price per 1M tokens (optional)
    new BigDecimal("15.00"),      // output price per 1M tokens (includes reasoning)
    Currency.getInstance("USD")); // optional; purely for presentation.

ChatOptions options = ChatOptions.newBuilder()
    .pricing(pricing)
    .build();

String response = service.chat("Explain quantum computing", options);

ChatCost cost = options.getLastCost();
System.out.println("Input cost:        " + cost.inputCost());
System.out.println("Cached input cost: " + cost.cachedInputCost());
System.out.println("Output cost:       " + cost.outputCost());
System.out.println("Total cost:        " + cost.totalCost() + " " + cost.currency());

Prices are expressed per one million tokens to match how providers publish their rate sheets. There are deliberately no built-in rate presets; provider rates drift and differ per model tier, so you look up the current numbers for your chosen model and pass them in. The optional currency is passed through to ChatCost for display; it does not affect any arithmetic, so use whatever unit you supplied the prices in.

The cachedInputTokenPrice is optional. When null, cached tokens are billed at the regular input rate. Set it explicitly to reflect the provider's cache-read discount (Anthropic charges roughly 10% of the input rate for cache reads, OpenAI and Google roughly 25%). Reasoning tokens are always billed at the output rate, consistent with how providers invoice them.

If you want the full positional constructor to be a bit less ceremonial, there are two factory methods:

ChatPricing simple = ChatPricing.of(new BigDecimal("3.00"), new BigDecimal("15.00"));
ChatPricing withCache = ChatPricing.of(new BigDecimal("3.00"), new BigDecimal("0.30"), new BigDecimal("15.00"));

And if you have a ChatUsage in hand and want the cost ad-hoc without configuring options at all:

ChatCost cost = usage.calculateCost(pricing);

One caveat worth mentioning up front: this is a simplified three-tier scheme (base input, cached input, output) that covers the common case. Provider-specific billing axes like Anthropic's 5-minute and 1-hour cache-write premiums are not modeled and may cause under-counting for workloads that rely heavily on explicit prompt caching. For strict accuracy, reconcile against the provider's own billing API. For "roughly what did that call cost me" it is good enough.

Budget Cap

Cost visibility is nice. Cost protection is nicer. 1.4 also lets you attach a cumulative-cost ceiling alongside the pricing so runaway spend on a given ChatOptions instance gets stopped rather than logged after the fact:

ChatOptions options = ChatOptions.newBuilder()
    .pricing(pricing, new BigDecimal("1.00")) // hard stop at $1.00
    .build();

while (hasMoreWork()) {
    try {
        service.chat(next(), options);
    } catch (AIBudgetExceededException e) {
        log.warn("Spent {} of {} {} — stopping", e.getTotalCost(), e.getMaxTotalCost(), e.getCurrency());
        break;
    }
}

The cap is checked before each call using the accumulated ChatOptions.getTotalCost(). It is a soft ceiling: the call that pushes the running total at or over the cap still completes and is billed; the next call is refused with AIBudgetExceededException. That keeps the behavior predictable; the alternative of estimating an upcoming call's cost before dispatching it would require knowing the output token count in advance, which of course you don't.

After you have caught the exception, you can call options.resetBudget() to zero the counter and start a fresh window on the same instance, or switch to a different ChatOptions instance, or even fail over to a different AIService (e.g. a cheaper model) to continue processing.

Cached Input Tokens

While we are on the subject of prompt caches, ChatUsage has gained a fourth field: cachedInputTokens().

ChatUsage usage = options.getLastUsage();
System.out.println("Input tokens:         " + usage.inputTokens());
System.out.println("Cached input tokens:  " + usage.cachedInputTokens()); // subset of inputTokens
System.out.println("Output tokens:        " + usage.outputTokens());
System.out.println("Reasoning tokens:     " + usage.reasoningTokens());   // subset of outputTokens
System.out.println("Total tokens:         " + usage.totalTokens());

It reports the subset of input tokens that was served from the provider's prompt cache. This is the number that drives the cheaper cachedInputCost on ChatCost, and it is useful on its own too; a low cache-hit ratio on a workload that should mostly be reused content is a good signal that your system prompts are drifting or the provider's cache TTL has elapsed. As with the other fields, a value of -1 means the provider did not report it.

Reasoning Effort

Modern frontier models (GPT-5, Claude extended thinking, Gemini thinking, Grok reasoning) all let you tune how many tokens they should spend on internal reasoning before answering. The knobs are called different things across providers; in OmniHai they live behind a single enum:

ChatOptions options = ChatOptions.newBuilder()
    .reasoningEffort(ReasoningEffort.HIGH)
    .build();

String answer = service.chat("Prove the Pythagorean theorem.", options);

The available levels are AUTO (the default, defers to the provider's own default), NONE (actively disable reasoning where supported, for minimum cost and latency), LOW (~20% of budget), MEDIUM (~50% of budget), HIGH (~80% of budget), and XHIGH (~95% of budget). Providers that do not support a given level map to the closest equivalent, so you can leave the same ChatOptions in place while switching the underlying provider.

Higher levels typically improve answer quality on hard problems (math, multi-step planning, non-trivial code) at the cost of more tokens and latency. On trivial prompts they just spend money without any measurable upside, so do not set HIGH or XHIGH as the default for all your calls :) Keep in mind that a higher effort may also require a correspondingly higher maxTokens to avoid truncated responses.

Portable JSON for ChatOptions

ChatOptions has been Serializable since day one, which is enough to stash it in an HTTP session. For portable storage, REST payloads, JSON columns, audit logs, or cross-service transport, Java serialization is not what you want. 1.4 adds an explicit JSON form:

String json = options.toJson();
ChatOptions restored = ChatOptions.fromJson(json);

All user-facing settings are included: system prompt, JSON schema, temperature, maxTokens, reasoning effort, topP, web search location, pricing, maxTotalCost, maxHistory, and the full conversation history (including any recorded uploaded file references). Null or unset fields are omitted for a compact payload. Runtime state, the last usage and the cumulative total cost, is deliberately not serialized; a restored instance starts with a fresh zero total cost counter.

Round-tripping a shared default constant (DEFAULT, CREATIVE, DETERMINISTIC) yields a mutable copy, equivalent to calling copy(). That way you do never accidentally end up with a restored instance that still rejects mutations because it was derived from an immutable template.

Default Models

Under the hood, default model identifiers per provider have been refreshed to match the current state of technology. The exact identifiers are documented on the GitHub README. If you were relying on the provider default, you get the newer model automatically on upgrade; if you were pinning a specific model, nothing changes for you.

Getting 1.4

Non-Maven users: download the OmniHai 1.4 JAR and drop it in /WEB-INF/lib the usual way, replacing the older version if any.

Maven users:

<dependency>
    <groupId>org.omnifaces</groupId>
    <artifactId>omnihai</artifactId>
    <version>1.4</version>
</dependency>

Give It a Try

As always, feedback and contributions are welcome on GitHub. If you run into any issues, open an issue. Pull requests are welcome too.

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.3</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 (current Chrome version has a limit of 6), and multiple SSE channels across tabs may hit 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, else your application will simply stop working when a page containing a SSE connection is being opened in multiple browser tabs.

When using <o:sse>, then you also need to make sure that every single servlet filter which is mapped on match-all URL pattern of /* is explicitly configured as @WebFilter(asyncSupported=true) on the class or as <async-supported>true</async-supported> in web.xml. In case that's not possible for your filter (e.g because it relies on some request/thread-specific state after invoking chain.doFilter() such as DB connection, locked/shared resource, ThreadLocal, etc), then you'll need to map it on a more specific URL-pattern excluding /omnifaces.sse/* and create yet another filter on /* which forwards to that filter when the request path does not match /omnifaces.sse/*.

In any case, this is a big candidate to end up as new <f:sse> component in a future Jakarta Faces version (like as that <o:socket> ultimately ended up as <f:websocket> in JSF 2.3).

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 = "example.taglib")
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 or proxypass configurations 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: was incompatible with mixed "use strict" scripts. When all scripts are "use strict" then it works fine but when at least one script is not "use strict", then it would break the entire script. So all "use strict" lines will now be stripped during combining. (#921)

@ViewScoped: unload threw IllegalArgumentException in Hacks#removeViewState() when session was already expired at same moment. It's now suppressed and logged as FINEST. (#937)

All 5.2.3 fixes are also available in 4.7.5 for Faces 3.0/4.0 and 3.14.16 for JSF 2.3.

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, refactoring shared code, writing javadocs, writing unit and integration 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.