OmniFaces

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against OmniFaces 5.5.5 (released on 2026-10-06; the 5.x line requires Java 17, Jakarta Faces 4.1, Jakarta EL 6.0, Servlet 6.1 and CDI 4.1), Jakarta Faces 4.1 (API jakarta.faces-api 4.1.2, part of Jakarta EE 11), Eclipse Mojarra 4.1.17 and Apache MyFaces Core 4.1.4 with Java 17 or later, and PrimeFaces 16.0.0 where the two libraries meet. OmniFaces 4.7.15 is the release for Faces 4.0 and 3.0; where its behaviour differs, What Changed per Version says so. The examples were compiled and deployed in a scratch Bookshelf WAR on Apache Tomcat 11.0.26 with Weld 6.0.4.Final, once with Mojarra and once with MyFaces, and driven with scripted HTTP requests and a headless Chrome; every result quoted below as observed comes from those runs (checked on 2026-10-08). Features that were read in the documentation and not run say so. Faces 5.0 is still a milestone release; OmniFaces 5.4 and later declare that they are compatible with it (What Changed per Version), and nothing here describes Faces 5.0 as current behaviour.

What OmniFaces Is, and What It Is Not

Every Faces project grows the same helper classes. One fetches a request parameter without navigating FacesContext.getCurrentInstance().getExternalContext().getRequestParameterMap(); one adds a FacesMessage in one line; one turns a byte[] into an image; one registers a filter to compress the response. OmniFaces is that collection, maintained as a library: a utility library for standard Faces. Its own description is that it focuses on utilities that ease everyday tasks with the standard Faces API, and that it does not and will not contain visual components such as rich tables or look and feels. It works with any component library; the OmniFaces showcase application is itself built with PrimeFaces.

That scope separates it from PrimeFaces (see the PrimeFaces pages of this section, which start with the setup and continue with Ajax and forms, data components and PrimeFaces Extensions), which is a UI library: PrimeFaces gives you a data table and a dialog, OmniFaces gives you the missing @Param annotation, the validator that compares two fields, a handler that makes an Ajax failure show your error page, and a few output components (o:cache, o:tree, o:highlight) that render no widget markup of their own.

OmniFaces is organised by the kind of artefact it adds. The table lists the packages under org.omnifaces and the sections of this page that cover them; the showcase groups its examples the same way.

Table 1. What the OmniFaces packages contain
Package (under org.omnifaces) What it holds Covered in

util

Static utility classes: Faces, FacesLocal, Messages, MessagesLocal, Components, Ajax, Beans, Events, Exceptions, Servlets, Validators, Platform, Renderers, Lazy, JNDI

The Utility Classes: Faces, FacesLocal and Friends, The Messages Utility and o:messages, The Ajax Utility

cdi

CDI annotations and scopes: @Param, @Cookie, @ContextParam, @Eager, @Startup, @ViewScoped, @RateLimit, @GraphicImageBean, @Push

@Param, @Cookie and @ContextParam: Injecting Request Data, @Eager and @Startup, OmniFaces @ViewScoped and @RateLimit, o:graphicImage and @GraphicImageBean, Push: o:socket, o:sse and o:notification

component, taghandler

The o: tags: components, validators, converters and tag handlers

The o: Tags, Component by Component, View Parameters Done Right: o:viewParam, o:viewAction and Friends, Validators: o:validateBean and the Multi-Field Family, Tag Handlers: importConstants, importFunctions and the rest

converter, validator

Converters and validators with converter and validator ids omnifaces.*

Converters, Validators: o:validateBean and the Multi-Field Family

el.functions

The EL functions, registered under the o: prefix since 5.0

The o: EL Functions (formerly of:)

exceptionhandler, filter

FullAjaxExceptionHandler and its relatives; CompressedResponseFilter, CacheControlFilter and four more filters

Exception Handlers, Filters

resourcehandler, renderer

CombinedResourceHandler, CDNResourceHandler, VersionedResourceHandler, UnmappedResourceHandler, PWAResourceHandler, CorsAwareResourceRenderer

Resource Handlers and Renderers

facesviews, servlet

FacesViews (extensionless URLs, MultiViews, dynamic routes); FileServlet

FacesViews: Extensionless URLs, MultiViews and Dynamic Routes, FileServlet: Serving Files From Outside the WAR

security

The sec:authorize, sec:isAuthenticated and sec:isAnonymous tags (since 5.0)

The sec: Tags (Jakarta Security)

Three design choices explain how the library behaves. First, it is a WAR library: it is a web fragment with CDI extensions and Faces configuration that it registers by itself, and the jar belongs in WEB-INF/lib of each web application (Installing OmniFaces). Second, it keeps its own dependencies to a minimum: Bean Validation is optional, and the library bundles its own JSON encoder, XML helpers and cache. Third, it prefers the standard to the proprietary where the standard has caught up. Several ideas that OmniFaces offered first reached Faces itself: f:importConstants and f:websocket (both in Faces 2.3; o:socket of OmniFaces 2.3 came first), CDI injection into converters and validators (Faces 2.3, and managed=true in 4.0), and a CDI replacement for @ManagedProperty (jakarta.faces.annotation.ManagedProperty, covered on the backing beans page), which injects a value by EL expression and coerces it by the EL rules but, unlike @Param (@Param, @Cookie and @ContextParam: Injecting Request Data), has no Faces converter, validator or message. The OmniFaces documentation says so on the page of the feature concerned, and this page does the same: for every feature it states whether the standard now covers the case and when to reach for OmniFaces anyway.

Installing OmniFaces

OmniFaces is one jar without required third-party dependencies. In Maven, a Faces 4.1 application adds:

pom.xml (the dependency)
<dependency>
  <groupId>org.omnifaces</groupId>
  <artifactId>omnifaces</artifactId>
  <version>5.5.5</version>
</dependency>

Pick the artifact line by the Faces version of the target server, because the minimum versions change with the major version:

Table 2. OmniFaces lines and what they require
Line Java Faces EL Servlet CDI Status (2026-10-08)

5.x (5.5.5)

17

4.1 (5.0 too, since 5.4)

6.0

6.1

4.1

Active

4.x (4.7.15)

11

3.0 or 4.0

4.0

5.0

3.0

Bugfix-only

3.x (3.14.26)

1.8

2.3 (javax.*)

3.0

3.1

2.0

Bugfix-only

2.x (2.7.34), 1.x (1.14.4)

1.7, 1.6

2.2, 2.0 or 2.1

2.2, 2.1

3.0, 2.5

1.1, none

Security fixes only

The 4.x and 3.x lines receive the bugfixes of 5.x but no new features. The 5.5.5 and 4.7.15 releases are dated the same day and carry the same fixes. A Faces 4.0 server (Jakarta EE 10) therefore stays on 4.7.15 until it moves to Jakarta EE 11, and a Faces 4.1 server uses 5.5.5. The one optional dependency worth knowing is Bean Validation (version 3.1 for 5.x), needed only for o:validateBean and FacesLabelMessageInterpolator.

Three rules about where the jar goes, from the project documentation:

  • Only in WEB-INF/lib of the WAR. Placed in an EAR lib directory or in the server’s own library directory, the webapp cannot find the Faces and CDI annotated classes inside the jar, and fails at deployment or at run time.

  • Never twice. A second copy of the jar makes CDI report ambiguous dependencies at deployment.

  • Servlet container. On Tomcat (Tomcat and Weld) the jar works with Weld and either Faces implementation (all examples of this page ran that way); a server such as WildFly or GlassFish needs nothing else (Jakarta EE servers). On Spring Boot the JoinFaces starter omnifaces-spring-boot-starter pulls in the Faces and Weld starters and maps joinfaces.omnifaces.* properties to the org.omnifaces.* context parameters. On Quarkus the Quarkiverse extension quarkus-omnifaces exists. The Spring Boot with JoinFaces and Quarkus pages of this section cover those setups.

On start-up the library logs one line that confirms it was found (INFO: Using OmniFaces version 5.5.5 in the scratch runs, from ApplicationInitializer).

The XML namespace

OmniFaces 5 declares every tag and every EL function under one URN namespace:

The namespace declarations
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:o="omnifaces"
      xmlns:sec="omnifaces.security">

The history matters when you read older code. OmniFaces 1.x to 4.x declared the tags with xmlns:o="http://omnifaces.org/ui" and the EL functions with a second prefix, xmlns:of="http://omnifaces.org/functions", so that the books and blog posts you find call the functions as #{of:abbreviate(...)}. In 5.0 both were merged into xmlns:o="omnifaces", and the functions are called as #{o:abbreviate(...)}. The security tags (sec:) are new in 5.0 and use xmlns:sec="omnifaces.security". The migration is three steps: change the o namespace, rename of: calls to o:, remove the of declaration. The old URL namespaces still work in 5.5.5 (a page declaring xmlns:o="http://omnifaces.org/ui" and xmlns:of="http://omnifaces.org/functions" rendered #{of:abbreviate('The quick brown fox', 10)} as The quick... on both implementations, with no warning in the log), but the taglibs that provide them state that they are deprecated and will be removed in 6.x. New code uses the URN namespace.

What registers itself

OmniFaces needs no entry in web.xml or faces-config.xml for most of its features: the jar ships a faces-config.xml that is ordered after all other configuration files, a web-fragment.xml marked <distributable/>, a beans.xml, four CDI extensions and one ServletContainerInitializer. The following list was read from the jar of 5.5.5, and it explains behaviour that otherwise looks magical: the extra omnifaces.js script in the head, the hidden form that OmniFaces adds to a page without a form when a component needs one (omnifaces_form in the scratch run of o:deferredScript), or the way a view scoped bean is destroyed.

Table 3. What the OmniFaces jar registers by itself (5.5.5)
Kind Registered class

View handler

OmniViewHandler (o:enableRestorableView; the view scope unload request; in the Development stage an IllegalStateException for a nested form; the XML writer of ViewResourceHandler)

Resource handlers

FacesViewsResourceHandler, GraphicResourceHandler (o:graphicImage), OmniVersionResourceHandler (appends the version to resources of library omnifaces), PWAResourceHandler. CombinedResourceHandler, CDNResourceHandler, VersionedResourceHandler, UnmappedResourceHandler and ViewResourceHandler are not registered; you opt in.

Factories

OmniApplicationFactory, OmniExternalContextFactory, OmniPartialViewContextFactory (the Ajax utility), OmniRenderKitFactory (CorsAwareResourceRenderer and the other renderers)

Phase listeners

InvokeActionEventListener, CallbackPhaseListener (the Events utility), EagerBeansPhaseListener (@Eager(viewId))

System event listeners

ApplicationProcessor (on PostConstructApplicationEvent), ViewScopeEventListener (on PreDestroyViewMapEvent)

EL resolver

FacesELResolver (the #{faces} object, The Utility Classes: Faces, FacesLocal and Friends)

CDI extensions

ViewScopeExtension, EagerExtension, ParamExtension, PushExtension

Servlet container initializer

ApplicationInitializer (logs the version, determines the default FacesServlet mapping, registers the FacesViews forwarding filter); ApplicationProcessor then checks that the resource handler chain is unique and registers the FacesViews view handler and the MessagesKeywordResolver. A failure in either fails the deployment unless the context parameter org.omnifaces.SKIP_DEPLOYMENT_EXCEPTION is true, which turns it into a warning

Two consequences follow. A feature that appears inactive (the combined resource, the CDN) is inactive because it is opt-in, not because OmniFaces failed to load. And because the jar replaces the render kit, the partial view context and the view handler by wrapping them, it composes with PrimeFaces and other libraries that do the same; the scratch run of this page (OmniFaces Next to PrimeFaces) used both libraries together on both implementations.

Where OmniFaces Sits in the Request Processing Lifecycle

The utilities that need a lifecycle position hook into the six phases (see the lifecycle page) as the diagram shows. The positions come from the Javadoc and from the source of the 5.5.5 sources jar; the sections below repeat them where they change behaviour.

flowchart LR A[Restore View] --> B[Apply Request Values] --> C[Process Validations] --> D[Update Model Values] --> E[Invoke Application] --> F[Render Response] A1["@Eager(viewId) beans created after the phase; o:enableRestorableView rebuilds an expired view"] -.-> A B1["o:inputHidden converts and updates here; o:viewParam, o:hashParam, o:scriptParam"] -.-> B C1["o:validateEqual and the other multi-field validators when the tree walk reaches them; o:validateBean class level, before and after the phase; o:skipValidators"] -.-> C D1["o:validateBean with method validateActual runs after the phase"] -.-> D E1["o:viewAction evaluates its if attribute here; @RateLimit runs on the action method; Ajax.update and Ajax.oncomplete"] -.-> E F1["o:cache, o:highlight, o:messages, o:graphicImage URLs; CombinedResourceHandler; the handle of FullAjaxExceptionHandler after every phase"] -.-> F

Anything that reads a request parameter, @Param among them, runs when the bean that holds it is created, which is a different moment from all of the above: the first time a view or a class refers to the bean. @Param, @Cookie and @ContextParam: Injecting Request Data shows what that costs.

The Utility Classes: Faces, FacesLocal and Friends

org.omnifaces.util.Faces is the class most people meet first. It is a set of static shortcuts that "flatten" the hierarchy of objects that you reach from FacesContext, plus a number of helpers that remove multi-line boilerplate: Faces.getRequestParameter(name) instead of the chain through the external context, Faces.redirect(...) that prepends the context path and URL-encodes its parameters, Faces.sendFile(...) that streams a download, Faces.getLocale() that never returns null. FacesLocal has the same methods with the FacesContext as first argument; use it in code that calls many of them, to look up the thread-local context once. The classes beside it follow the same pattern for a different part of the API:

Table 4. The static utility classes of org.omnifaces.util
Class What it is for

Faces, FacesLocal

Request, session, cookie, locale, view, project stage, resource bundle, redirect, sendFile, getBookmarkableURL, metadata attributes, evaluateExpressionGet

Messages, MessagesLocal

Create and add FacesMessage objects, global, per client id or flash (The Messages Utility and o:messages)

Components

Find and traverse components, the current form or command, labels, resetInputs, invalidateInputs, programmatic creation of expressions, buildView and encodeHtml to render a view to a string

Ajax

Update ids, call a script on complete and send data to JavaScript from a bean (The Ajax Utility)

Beans, BeansLocal

BeanManager shortcuts: getReference, getInstance, isActive, destroy, fireEvent

Events

Subscribe a lambda to a system event or to the before or after of a phase, for the current request or for the view

Exceptions

unwrap, is and extract on exception chains

Servlets

Request URL parts, getRemoteAddr, query strings, cookie helpers, cache headers, formatContentDispositionHeader, facesRedirect

Validators

Bean Validation helpers: validateBean, validateBeanProperty, resolveViolatedProperty

Platform, JNDI, Renderers, Lazy, Facelets

Look up the FacesServlet mapping, JNDI objects, write attributes in a renderer, a lazily initialised value, the Facelet context

Utils, Json and Xml in the same package are internal; the Javadoc says in as many words that their signatures can change without notice, so do not call them.

A bean that uses them

The bean reads parameters and settings, adds messages, redirects with encoded parameters and sends a file. It is the UtilsBean of the scratch application:

UtilsBean.java
package org.example.bookshelf.web;

import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Named;
import java.nio.charset.StandardCharsets;
import java.util.List;
import org.omnifaces.util.Faces;
import org.omnifaces.util.Messages;

@Named
@RequestScoped
public class UtilsBean {

    public String getQuery() {
        // null when absent, no Map lookup and no null checks on the chain
        return Faces.getRequestParameter("q");
    }

    public Integer getPage() {
        // typed access: converts with the Faces converter for Integer, null when absent or invalid
        return Faces.getRequestParameter("page", Integer.class);
    }

    public String getBaseUrl() {
        return Faces.getRequestBaseURL();
    }

    public String getViewId() {
        return Faces.getViewId();
    }

    public String getStage() {
        return Faces.getProjectStage().toString();
    }

    public String getMapping() {
        return Faces.getMapping();
    }

    public List<String> getLocales() {
        return Faces.getSupportedLocales().stream().map(Object::toString).toList();
    }

    public java.time.LocalDate getStart() { return java.time.LocalDate.of(2020, 1, 15); }
    public java.time.LocalDate getEnd() { return java.time.LocalDate.of(2026, 10, 8); }

    public void greet() {
        Messages.addGlobalInfo("Welcome back, {0}!", "reader");
    }

    public void problem() {
        Messages.addError("form:title", "The title {0} is already on the shelf", "Dune");
        Messages.addGlobalWarn("Your changes were not saved");
    }

    public void builder() {
        Messages.create("Review saved").detail("Thank you, {0}", "reader").add();
    }

    public String flashAndRedirect() {
        Messages.addFlashGlobalInfo("Book {0} added", 42);
        return "utils?faces-redirect=true";
    }

    public void redirectWithParams() {
        // parameters are URL-encoded by Faces.redirect
        Faces.redirect("utils.xhtml?q=%s&page=%d", "war & peace", 2);
    }

    public void download() {
        Faces.sendFile("hello.txt".getBytes(StandardCharsets.UTF_8), "hello.txt", true);
    }

    public void downloadNamed() {
        String name = Faces.getRequestParameter("name");
        Faces.sendFile("x".getBytes(StandardCharsets.UTF_8), name, true);
    }
}
utils.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets"
      xmlns:o="omnifaces">
<h:head><title>Utils</title></h:head>
<h:body>
  <h:messages id="msgs" globalOnly="false" layout="list"/>
  <ul id="out">
    <li>q=<span id="q">#{utilsBean.query}</span></li>
    <li>page=<span id="page">#{utilsBean.page}</span></li>
    <li>base=<span id="base">#{utilsBean.baseUrl}</span></li>
    <li>view=<span id="view">#{utilsBean.viewId}</span></li>
    <li>stage=<span id="stage">#{utilsBean.stage}</span></li>
    <li>mapping=<span id="mapping">#{utilsBean.mapping}</span></li>
    <li>locales=<span id="locales">#{utilsBean.locales}</span></li>
  </ul>
  <h:form id="form">
    <h:inputText id="title"/>
    <h:message for="title" id="titleMsg"/>
    <h:commandButton id="greet" value="greet" action="#{utilsBean.greet}"/>
    <h:commandButton id="problem" value="problem" action="#{utilsBean.problem}"/>
    <h:commandButton id="builder" value="builder" action="#{utilsBean.builder}"/>
    <h:commandButton id="flash" value="flash" action="#{utilsBean.flashAndRedirect}"/>
    <h:commandButton id="redir" value="redir" action="#{utilsBean.redirectWithParams}"/>
    <h:commandButton id="dln" value="dln" action="#{utilsBean.downloadNamed}"/>
    <h:commandButton id="dl" value="dl" action="#{utilsBean.download}"/>
  </h:form>
</h:body>
</html>

What the runs showed, on Mojarra and on MyFaces alike:

  • utils.xhtml?q=dune rendered q=dune; page was empty because Faces.getRequestParameter("page", Integer.class) returns null for an absent parameter, and ?page=3 gave 3. The base URL was http://localhost:8570/bookshelf/, the view id /utils.xhtml, the stage Production and the mapping .xhtml.

  • Faces.getSupportedLocales() returned [en, es] only after the application declared <locale-config> with a default locale en and a supported locale es; without it the list was empty on both implementations, so do not rely on the Javadoc’s remark that the default locale comes first before you configure the locales (see the messages page).

  • Faces.redirect("utils.xhtml?q=%s&page=%d", "war & peace", 2) sent the browser to utils.xhtml?q=war+%26+peace&page=2: the format arguments are URL-encoded, the format string is not, and the context path was added for a relative path. Encode the arguments of a redirect with this method; do not concatenate user input into the path.

  • Faces.sendFile(bytes, name, true) answered Content-Disposition: attachment;filename="hello.txt"; filename*=UTF-8''hello.txt with the content type derived from the file name. The file name goes through percent-encoding: a name with a quote, a backslash or a CR-LF pair (a"b.txt, ..\..\x.txt, a<CR><LF>X-Evil: 1.txt) came out as a%22b.txt, ..%5C..%5Cx.txt and a%0D%0AX-Evil%3A%201.txt in both parameters, so header injection is prevented. The encoding is not a path sanitiser, though: the name still contains the separators, which a browser removes when it saves the file. Build the name from your own data, never from the request, and apply the checks of the download section (including the CSV formula escaping) before you send anything the user typed.

The faces EL object

The FacesELResolver (since 2.6) exposes the parameterless get... and is... methods of Faces that return a String or a boolean as properties of an implicit object #{faces}. The Faces 4.1 specification has no such object, so this is an OmniFaces feature and works only with OmniFaces in the application. Observed on both implementations: #{faces.development} (false in Production), #{faces.requestBaseURL}, #{faces.requestContextPath} (/bookshelf), #{faces.ajaxRequest} and #{faces.serverInfo} (Apache Tomcat/11.0.26) resolved, while #{faces.contextPath} and #{faces.projectStage} failed on both (Mojarra’s message: #{faces.contextPath} does not exist), because there is no getter of that name that returns a string or a boolean. The Javadoc of importFunctions itself prefers #{faces.development} or #{request.contextPath} to calling the function form.

The @messages search keyword

Faces 4.0 lets a library add search keywords (see the search expressions page). OmniFaces registers one, @messages (since 3.1), which resolves to all h:message and h:messages components in the current form. It solves the chore of listing the message ids in every render attribute of a long form:

kw.xhtml (the relevant part)
<h:form id="form">
  <h:inputText id="a" value="#{compBean.title}" required="true"/><h:message id="m_a" for="a"/>
  <h:inputText id="b" value="#{compBean.author}" required="true"/><h:message id="m_b" for="b"/>
  <h:commandButton id="go" value="go" action="#{compBean.save}">
    <f:ajax execute="@form" render="@messages"/>
  </h:commandButton>
  <h:messages id="m_form" globalOnly="true" redisplay="false"/>
</h:form>

In headless Chrome, on Mojarra and on MyFaces, clicking the button with the first field empty replaced form:m_a with form:a: Validation Error: Value is required. and left form:m_b empty, with no message id in the render attribute. The message components need fixed ids, as above: a component with a generated id that has no message is not rendered at all, and the client then cannot find it to update it.

Events, Components and Beans in code

The other classes are best learned from the example the Javadoc gives for each. A few that earn their place in a typical Bookshelf:

Typical calls (from the Javadoc)
// Closest parent of a component, and the form that was submitted
UIForm form = Components.getClosestParent(input, UIForm.class);
UIForm submitted = Components.getCurrentForm();

// The label Faces uses in validation messages
String label = Components.getLabel(input);

// Render a view to a string, for an e-mail body
String html = Components.encodeHtml(Components.buildView("/WEB-INF/mail/receipt.xhtml"));

// Run a callback after the Render Response phase of this request only
Events.subscribeToRequestAfterPhase(PhaseId.RENDER_RESPONSE, () -> audit.log("rendered"));

// Contextual references without @Inject
BookService books = Beans.getReference(BookService.class);

// Unwrap a FacesException to the real cause
Throwable cause = Exceptions.unwrap(exception, FacesException.class);

These calls come straight from the class documentation and were not run; they depend on being inside a Faces request. Two cautions apply to all of them. They fetch the FacesContext from a thread-local variable, so they work in a bean or a listener and fail when called from a thread that Faces did not start (a scheduled task, a thread pool). And they hide the object structure, which is fine in a bean and poor design in a library: a service that other applications reuse should take plain arguments, not call Faces.

The Messages Utility and o:messages

Adding a message by hand takes a client id, a severity, a summary and a detail, and a MessageFormat call if there are arguments. Messages reduces each case to one line. The summary is formatted with MessageFormat and the given arguments by default, so {0} is a placeholder (a literal quote or brace in the text has the usual MessageFormat rules).

The ways to add a message
// A global message (no client id)
Messages.addGlobalInfo("Welcome back, {0}!", "reader");
Messages.addGlobalWarn("Your changes were not saved");

// A message for one component, by its full client id
Messages.addError("form:title", "The title {0} is already on the shelf", "Dune");

// With a detail, through the builder
Messages.create("Review saved").detail("Thank you, {0}", "reader").add();

// Survives a redirect (flash scope), for Post/Redirect/Get
Messages.addFlashGlobalInfo("Book {0} added", 42);
return "utils?faces-redirect=true";

// In a validator or a converter
Messages.throwValidatorException("Invalid input.");
throw Messages.asConverterException("Unknown input.");

Observed with the UtilsBean above: the three actions rendered Welcome back, reader! in h:messages, the pair The title Dune is already on the shelf (also on the h:message for="title" of the form) and Your changes were not saved, and Review saved for the builder (the detail Thank you, reader is added too, but h:messages shows the summary unless showDetail="true"). The flash message appeared on the redirected page, Book 42 added, which is what the flash is for (see the flash page). The methods exist per severity (Info, Warn, Error, Fatal) in the global, per-client-id and flash variants, with clear... methods and isEmpty() checks beside them.

Resource bundle keys. The shortcuts take the message text, not a bundle key, so an internationalised application sets a resolver once: Messages.setResolver(...) with an implementation of Messages.Resolver that looks the text up in the bundle for Faces.getLocale() (the Javadoc has the example). The resolver can be set only once, early (in a @Startup bean for instance, @Eager and @Startup). That example was not run; the standard way of putting localised messages on the page is on the messages page.

o:messages

o:messages extends h:messages with four things. for accepts several client ids separated by spaces, and ids of non-input containers that hold inputs, so one list of errors can serve a group of fields. message shows one fixed text whenever any of the listed components has a message ("There are validation errors. Please fix them."). var hands each FacesMessage to the body, so the markup is yours (the iteration is stateless and meant for output). escape="false" switches the escaping off; the Javadoc itself warns of cross-site scripting when user-controlled text is redisplayed, and the rule of this section applies: leave it true. The scratch run used <o:messages for="form:title form:author"/> and showed Book title: Validation Error: Value is required. for the first field, where the label Book title comes from o:outputLabel (The o: Tags, Component by Component).

The Ajax Utility

Standard Ajax decides what is re-rendered in the view: the render attribute of f:ajax lists the ids (see the Ajax page). When the bean knows better, org.omnifaces.util.Ajax adds to the list from Java, runs a script when the response has been processed, and passes data to that script. It works through the OmniPartialViewContext that the jar installs, so it needs nothing in the page:

AjaxUtilBean.java
package org.example.bookshelf.web;

import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.Map;
import org.omnifaces.util.Ajax;

@Named
@ViewScoped
public class AjaxUtilBean implements Serializable {
    private static final long serialVersionUID = 1L;
    private int clicks;
    private int other;

    public void click() {
        clicks++;
        other++;
        // adds the component to the render list although the f:ajax tag did not name it
        Ajax.update("other");
        // arguments for the on-complete script, available as OmniFaces.Ajax.data in JavaScript
        Ajax.data(Map.of("clicks", clicks, "title", "Dune"));
        Ajax.oncomplete("window.result = OmniFaces.Ajax.data.title + ':' + OmniFaces.Ajax.data.clicks + ':' + OmniFaces.Ajax.validationFailed");
    }

    public int getClicks() { return clicks; }
    public int getOther() { return other; }
}
ajaxutil.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:o="omnifaces">
<h:head><title>Ajax util</title></h:head>
<h:body>
  <h:form id="form">
    <h:commandButton id="b" value="click" action="#{ajaxUtilBean.click}"><f:ajax render="mine"/></h:commandButton>
    <h:outputText id="mine" value="#{ajaxUtilBean.clicks}"/>
  </h:form>
  <h:outputText id="other" value="#{ajaxUtilBean.other}"/>
  <o:onloadScript>window.loads = (window.loads || 0) + 1;</o:onloadScript>
</h:body>
</html>

In headless Chrome, two clicks on the button on Mojarra and on MyFaces updated form:mine (named in f:ajax render) and the other output outside the form (named only in the bean), both to 2; the script set window.result to Dune:2:false, that is the data from Ajax.data and OmniFaces.Ajax.validationFailed (new in 5.4: the value of FacesContext.isValidationFailed() for that response, so a script can react to a failed validation without reading the DOM). o:onloadScript ran its body on the initial load and after each of the two Ajax responses (three times), which is how to re-apply a jQuery plugin to markup that an Ajax response replaced. Other methods of the class are updateRow(UIData, int) and updateColumn(UIData, int) (the cell content must be wrapped in a container with a fixed id), load(library, resource) to add a script resource on completion, updateAll() and isExecuted(clientId).

Two limits to remember. The data values are encoded as JSON by the library’s own encoder, which supports numbers, strings, booleans, dates, arrays, collections, maps and (as a last resort) JavaBeans, so send plain values, and never put secrets in them: everything lands in the browser. And Ajax.update takes client ids, so an id inside a naming container needs its full path (form:mine), as everywhere else in Faces.

o:form (The o: Tags, Component by Component) complements this on the request side: it posts only the parts of the form that the Ajax execute list needs.

@Param, @Cookie and @ContextParam: Injecting Request Data

A bookmarkable page (see the view parameters page) usually declares f:viewParam in f:metadata, adds a f:viewAction or a preRenderView listener to load the data, and puts the loaded value into the bean in a method the framework calls at a particular phase. @Param collapses this: the request parameter is injected into the bean’s field, converted and validated, and is available in @PostConstruct.

DetailParams.java
package org.example.bookshelf.web;

import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.util.List;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.BookService;
import org.omnifaces.cdi.Cookie;
import org.omnifaces.cdi.Eager;
import org.omnifaces.cdi.ContextParam;
import org.omnifaces.cdi.Param;
import org.omnifaces.cdi.param.ParamValue;
import jakarta.faces.validator.LengthValidator;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import org.omnifaces.cdi.param.Attribute;

@Eager(viewId = "/param-first.xhtml")
@Named
@RequestScoped
public class DetailParams {

    @Inject
    @Param(name = "id", label = "Book id")
    private Long id;

    @Inject
    @Param(name = "tag")
    private List<String> tags;

    @Inject
    @Param(name = "isbn", validatorClasses = LengthValidator.class,
           validatorAttributes = { @Attribute(name = "minimum", value = "10"),
                                   @Attribute(name = "maximum", value = "13") },
           validatorMessage = "{1}: the ISBN must have 10 to 13 characters", label = "ISBN")
    private String isbn;

    @Inject
    @Param(name = "n", label = "Page size")
    @Min(1) @Max(100)
    private Integer pageSize;

    @Inject
    @Param(name = "year", converter = "jakarta.faces.Integer")
    private ParamValue<Integer> year;

    @Inject
    @Cookie(name = "theme")
    private String theme;

    @Inject
    @ContextParam(name = "jakarta.faces.PROJECT_STAGE")
    private String stage;

    @Inject
    private BookService books;

    private Book book;

    @PostConstruct
    void init() {
        // the injected values are already available here, no f:viewAction or preRenderView needed
        if (id != null) {
            book = books.find(id);
        }
    }

    public Long getId() { return id; }
    public List<String> getTags() { return tags; }
    public String getIsbn() { return isbn; }
    public Integer getPageSize() { return pageSize; }
    public Integer getYear() { return year.getValue(); }
    public String getTheme() { return theme; }
    public String getStage() { return stage; }
    public Book getBook() { return book; }
}
param.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets"
      xmlns:o="omnifaces">
<h:head><title>Param</title></h:head>
<h:body>
  <ul>
    <li>id=<span id="id">#{detailParams.id}</span></li>
    <li>book=<span id="book">#{detailParams.book.title}</span></li>
    <li>tags=<span id="tags">#{detailParams.tags}</span></li>
    <li>isbn=<span id="isbn">#{detailParams.isbn}</span></li>
    <li>n=<span id="n">#{detailParams.pageSize}</span></li>
    <li>year=<span id="year">#{detailParams.year}</span></li>
    <li>theme=<span id="theme">#{detailParams.theme}</span></li>
    <li>stage=<span id="stage">#{detailParams.stage}</span></li>
  </ul>
  <h:messages id="msgs" for="#{view.id}"/>
</h:body>
</html>

The elements of @Param (all @Nonbinding, so they do not change which bean CDI injects):

Table 5. The elements of @Param (5.5.5)
Element Meaning

name

The request parameter name; defaults to the name of the field (or of the constructor parameter, if the class is compiled with -parameters)

pathIndex, pathName

The zero-based index of a path parameter (needs the FacesServlet mapped by prefix, or FacesViews with MultiViews, FacesViews: Extensionless URLs, MultiViews and Dynamic Routes); the name of a dynamic route segment (5.5). Setting both is an IllegalArgumentException

label

The label in messages; observed: Book id: 'abc' must be a number consisting of one or more digits.

converter, converterClass, converterAttributes, converterMessage

A converter id, an EL expression for an id or an instance, or a class; attributes as @Attribute(name, value)

validators, validatorClasses, validatorAttributes, validatorMessage, required, requiredMessage

The same for validators; {1} in a message is the label

globalMessage

Attach the message to no component (since 4.5); the default attaches it to the view root, shown by <h:messages for="#{view.id}"/>

disableBeanValidation, overrideGlobalBeanValidationDisabled

Switch Bean Validation for this field off, or on despite the global switch

Supported field types are String, every type with a Faces converter, List<T> and arrays (for repeated parameters, ?tag=a&tag=b gave [a, b]), and ParamValue<T> for a converted value that is not serializable: the bean is serializable, the ParamValue is converted again from the original parameter after deserialisation. A ParamValue always holds the submitted string beside the converted value (getSubmittedValue()), which is useful in a message.

What the scratch run showed (identical on Mojarra and MyFaces):

  • ?id=2&tag=a&tag=b&isbn=1234567890&n=20&year=1990 injected 2, [a, b], 1234567890, 20 and 1990, and @PostConstruct loaded the book (Cosmos).

  • ?id=abc left id null and added Book id: 'abc' must be a number consisting of one or more digits.. A failed field is null and the bean still exists (the Javadoc adds that FacesContext.isValidationFailed() is true afterwards): code in @PostConstruct must cope with null.

  • ?isbn=123&n=0 produced ISBN: the ISBN must have 10 to 13 characters (the validatorClasses element, with {1} in the message replaced by the label) and must be greater than or equal to 1 (the Bean Validation constraint @Min(1) on the field; Bean Validation messages carry no label); ?n=500 gave must be less than or equal to 100. Bean Validation annotations on the injected field are honoured, and @Param is a way to get server-side validation of query parameters without writing any Java validator.

  • @Cookie(name = "theme") injected dark for a request that carried Cookie: theme=dark, and @ContextParam(name = "jakarta.faces.PROJECT_STAGE") injected Production. @Cookie has no validation "by design" according to the Javadoc; that is no reason to trust it, because the user controls their cookies as completely as the query string. Validate a cookie value yourself before it reaches a query, a file name or a redirect.

When the messages appear

A @Param field is injected when the bean is created, and in a request scoped bean that is the first time the page refers to it. Messages added at that moment land in the queue only if the component that displays them has not rendered yet. In param.xhtml the h:messages is after the expressions that touch the bean, so the messages showed. Move it to the top and they do not: the scratch page param-first.xhtml, identical but with the messages first, rendered an empty ul id="msgs" for ?id=abc on Mojarra. Two ways to fix it, and a third that does not work:

  • Put the messages component after the first use of the bean. Cheap, and a trap for the next person who tidies the page.

  • Make the bean eager for the view with @Eager(viewId = "/param-first.xhtml") (@Eager and @Startup): it is created after the Restore View phase, before rendering, and the message appeared above the list on both implementations.

  • @Eager(requestURI = ...) does not work here: the bean is then created before the FacesServlet runs, there is no FacesContext, and the injection fails with NullPointerException: Cannot invoke "FacesContext.getApplication()" because "context" is null in ParamProducer; the Javadoc of @Eager states that the FacesContext is not available in that case, and OmniFaces logs Could not instantiate eager request scoped beans for request ....

A related difference to f:viewParam: @Param runs at bean creation, which can be in any phase, including Render Response. A conversion error in a f:viewParam, by contrast, stops the lifecycle at Process Validations for the view, and invokes f:viewAction only if everything passed. When you need the "validate everything first, then act" semantics, use o:viewParam with o:viewAction (View Parameters Done Right: o:viewParam, o:viewAction and Friends).

Path parameters

@Param(pathIndex = 0) reads the first segment after the page name (/book/978-0441013593 mapped to book.xhtml), which needs a prefix-mapped FacesServlet or FacesViews with MultiViews. The scratch run of that is in FacesViews: Extensionless URLs, MultiViews and Dynamic Routes. Path parameters are as untrusted as query parameters; in the run, /books/abc/reviews injected the string abc into a pathName field without complaint, so add a converter or a validator wherever the value is not free text.

What changed

@Param has existed since OmniFaces 1.6; multi-valued parameters came in 2.4, path parameters in 2.5, globalMessage in 4.5 and pathName in 5.5. Between 3.6 and 5.3 a @Param field also worked without @Inject; since 5.4 that is deprecated, a warning is logged at deployment for each such field, and it will be removed, so put @Inject next to every @Param. The standard alternative for a single string is @Inject @ManagedProperty("#{param.id}") (the backing beans page), which has none of the conversion, validation and message features above.

@Eager and @Startup

CDI creates beans lazily, at the first use. That is wrong for a bean that must do something at a defined moment: warm a cache when the application starts, register a listener, or prepare data when a particular URL is requested. The old Faces @ManagedBean(eager = true) did the first; CDI has no equivalent, and @Eager fills the gap for five scopes.

Table 6. Where @Eager creates the bean
Declaration The bean is created

@Eager @ApplicationScoped (or @Startup)

At application start

@Eager @SessionScoped

Whenever a session is created

@Eager(requestURI = "/eager.xhtml") @RequestScoped

When that context-relative URI is requested, before the FacesServlet runs, so no FacesContext yet

@Eager(viewId = "/param-first.xhtml") @RequestScoped

After the Restore View phase of that view, with a FacesContext

@Eager(viewId = "...") @ViewScoped (Faces or OmniFaces)

When that view is created; like the request scoped bean, a view scoped bean needs requestURI or viewId

BookshelfStartup.java and EagerPage.java
package org.example.bookshelf.config;

import jakarta.annotation.PostConstruct;
import java.util.logging.Logger;
import org.omnifaces.cdi.Startup;

@Startup
public class BookshelfStartup {
    private static final Logger LOG = Logger.getLogger("bookshelf");

    @PostConstruct
    void init() {
        LOG.info("BookshelfStartup created at application start");
    }
}

package org.example.bookshelf.web;

import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.RequestScoped;
import java.util.logging.Logger;
import org.omnifaces.cdi.Eager;

@Eager(requestURI = "/eager.xhtml")
@RequestScoped
public class EagerPage {
    private static final Logger LOG = Logger.getLogger("bookshelf");

    @PostConstruct
    void init() {
        LOG.info("EagerPage created for /eager.xhtml");
    }
}

On Tomcat with Weld and Mojarra the log showed BookshelfStartup created at application start right after the line Using OmniFaces version 5.5.5 and before Mojarra reported initialising the context: the startup bean is created before Faces itself is started. The Javadoc says that FacesContext is not available in the @PostConstruct of @Eager beans declared without a viewId; inject the HttpServletRequest, HttpSession, ServletContext or other beans instead. EagerPage created for /eager.xhtml appeared on the first request for /eager.xhtml. The @Startup that OmniFaces defines is a stereotype for @Eager @ApplicationScoped; the identically named jakarta.ejb.Startup needs an EJB container and turns the class into a session bean, so watch the import. The scopes page has the same bean in a different role (the application scope section).

OmniFaces @ViewScoped and @RateLimit

The OmniFaces view scope

org.omnifaces.cdi.ViewScoped is an alternative to jakarta.faces.view.ViewScoped with a stricter lifecycle: the bean is destroyed (and its @PreDestroy called) when the browser unloads the page, by a navigator.sendBeacon or keepalive fetch call, and not only when the session expires or the view map is evicted. The unload can be cancelled by a leave confirmation. It also physically removes the view state of the view from the implementation’s store, which reduces the risk of ViewExpiredException on the other views of the user. Its extra element, saveInViewState, stores the whole bean in the view state (client state saving only, and the Javadoc cautions against it). The standard scope, its limits and the comparison are in the scopes page; what this page adds is a measurement.

The scratch run navigated headless Chrome from a page that used one bean of each kind to another page:

OmniViewBean.java (the two beans differ in the import and the log text)
package org.example.bookshelf.web;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.logging.Logger;
import org.omnifaces.cdi.ViewScoped;

@Named
@ViewScoped
public class OmniViewBean implements Serializable {
    private static final long serialVersionUID = 1L;
    private static final Logger LOG = Logger.getLogger("bookshelf");

    @PostConstruct
    void init() { LOG.info("OmniViewBean created"); }

    @PreDestroy
    void destroy() { LOG.info("OmniViewBean destroyed"); }

    public String getHello() { return "omni"; }
}
  • On Mojarra both beans were destroyed within two seconds of leaving the page: StdViewBean destroyed and OmniViewBean destroyed. This is the side effect the Javadoc describes, that the standard view scoped beans referenced in the same view are destroyed too.

  • On MyFaces only OmniViewBean destroyed appeared; the standard bean stayed until the view map was evicted or the session ended.

So the unload behaviour is a feature of the OmniFaces bean on both, and of the standard bean only on Mojarra, as a side effect. Do not depend on the side effect. Two other facts from the Javadoc: the bean must be Serializable (see serialisability in a cluster) (the scope is passivating), and @PreDestroy must not use FacesContext. The number of live view scopes is limited per session by the context parameter org.omnifaces.VIEW_SCOPE_MANAGER_MAX_ACTIVE_VIEW_SCOPES, or by the Mojarra or MyFaces parameter for the number of views if that one is present, with a default of 20.

@RateLimit

@RateLimit (since 5.0) limits how often a method of a CDI bean may run per client, using a sliding window. It is an interceptor binding; the default client identifier is the client’s IP address.

ReviewApi.java
package org.example.bookshelf.web;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Named;
import org.omnifaces.cdi.RateLimit;

@Named
@ApplicationScoped
public class ReviewApi {
    private int saved;

    /** At most 2 calls per client address and 60 seconds. */
    @RateLimit(maxRequestsPerTimeWindow = 2, timeWindowInSeconds = 60, maxRetries = 0)
    public String submit() {
        saved++;
        return null;
    }

    public int getSaved() { return saved; }
}

Four posts of the form through a button bound to reviewApi.submit: the first two ran (saved=1, saved=2), the third and fourth answered HTTP 500 and the log recorded RateLimitExceededException: Rate limit exceeded for client ID '0:0:0:0:0:0:0:1' (the IPv6 loopback address of the test client). That is the default: the interceptor throws, and the exception handling of your application decides what the user sees (a mapping for the exception in error-page or a handler, see the error pages section). The element maxRetries waits and retries instead of failing at once.

The default client id can be forged. Faces.getRemoteAddr(), which @RateLimit uses, takes the first address of the Forwarded, X-Forwarded-For or X-Real-IP request header when one is present, and the request address only when none is. With the limit above exhausted, five more posts from the same machine, each with a different X-Forwarded-For: 10.0.0.n header, all succeeded (saved=3 to saved=7). A rate limit keyed on the default can therefore be bypassed by any client that can send a header. Either run the application behind a proxy that overwrites those headers (and reaches the application only through it), or set clientId explicitly to something the client cannot choose: the authenticated user name or the session id.

Behind the limit, the same caution applies to Servlets.getRemoteAddr wherever else you use it for logging or access decisions.

View Parameters Done Right: o:viewParam, o:viewAction and Friends

The view parameters page of this section (view parameters and view actions) explains f:viewParam and f:viewAction and the traps in them. OmniFaces offers variants that avoid several of those traps, o:viewParam and o:viewAction, and a tag for what to do on a failure. The two components are drop-in: change the prefix from f: to o:.

The Bookshelf detail view with the OmniFaces tags
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets"
      xmlns:o="omnifaces">
<f:metadata>
  <o:viewParam name="id" value="#{viewParamBean.id}" required="true"/>
  <o:viewParamValidationFailed sendError="400" message="A book id is required"/>
  <o:viewAction action="#{viewParamBean.load}" if="#{viewParamBean.id ne null}"/>
</f:metadata>
<h:head><title>View</title></h:head>
<h:body>
  <h:messages id="msgs"/>
  <p>id=<span id="id">#{viewParamBean.id}</span> title=<span id="title">#{viewParamBean.book.title}</span> loads=<span id="loads">#{viewParamBean.loads}</span></p>
  <h:form id="form">
    <h:commandButton id="again" value="again" action="#{viewParamBean.load}"/>
  </h:form>
</h:body>
</html>
ViewParamBean.java
package org.example.bookshelf.web;

import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.validation.constraints.NotNull;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.BookService;

@Named
@ViewScoped
public class ViewParamBean implements java.io.Serializable {
    private static final long serialVersionUID = 1L;
    @Inject
    private transient BookService books;

    @NotNull
    private Long id;
    private Book book;
    private int loads;

    public void load() {
        loads++;
        book = books.find(id);
    }

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public Book getBook() { return book; }
    public int getLoads() { return loads; }
}

The same page written with the standard tags (f:viewParam and f:viewAction with the same attributes) is the control. Both pages were requested with different query strings and then posted back, on Mojarra and on MyFaces:

Table 7. Standard tags against the OmniFaces tags
Request Standard f:viewParam, f:viewAction o:viewParam, o:viewAction, o:viewParamValidationFailed

no id

HTTP 200, the page with j_idt1: Validation Error: Value is required. (the label is the generated id: j_id__md_1 on MyFaces)

HTTP 400 with the text A book id is required

?id=abc

HTTP 200 and 'abc' must be a number consisting of one or more digits.

HTTP 400

?id=2

HTTP 200, but the action did not load the book: title empty, loads=0

HTTP 200, title=Cosmos, loads=1

postback of the page

the bean is updated again from the query string; loads goes to 1

loads goes to 2; the id is kept by the view scoped bean; the parameter is not converted or validated again

postback to the bare URL (no query)

Mojarra: works (loads=2). MyFaces: Validation Error: Value is required. and loads stays 1

works on both (loads=3)

What the table shows is what the o: tags are for:

  • o:viewParam has a stateless mode. The standard parameter converts, validates and updates the model on every postback, so a required parameter that the form does not carry makes the postback fail (the MyFaces cell above) and a view scoped bean is overwritten. o:viewParam keeps the submitted value in the component and skips conversion, validation and the model update after the first request. Choose a view scoped bean to hold the value, as above: with a request scoped bean the postback finds nothing in the model (the same button bound to load() found id null and failed with a NullPointerException).

  • An absent parameter. In Faces 2.0 to 2.2 the standard tag took a shortcut for a missing value that bypassed Bean Validation and the PreValidateEvent and PostValidateEvent listeners that o:viewParamValidationFailed relies on, and o:viewParam was the fix. The Javadoc says that Faces 2.3 repaired the standard tag, and that the OmniFaces fix now matters only when jakarta.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL is true. On 4.1 the standard page in the run reported its required error normally; the stateless mode is the remaining reason.

  • o:viewParam also gives a default label (its name), accepts a default value for an absent parameter, and does not turn a null model value into an empty parameter in generated links (includeViewParams).

  • o:viewAction evaluates if late. The standard action evaluates if in Apply Request Values, before the view parameter has reached the model, so if="#{viewParamBean.id ne null}" saw null on the GET and the book was never loaded (loads=0 above). o:viewAction evaluates it in Invoke Application, after conversion, validation and the model update. With immediate="true" it behaves like the standard one. Its message attribute adds a flash warning if the action redirects.

  • o:viewParamValidationFailed decides what a failure means. In f:metadata it applies when any view parameter failed; nested in one view parameter it applies to that one only and takes precedence. With sendError="400" it sends an HTTP status (and a page from <error-page> if you declared one); with sendRedirect="login.xhtml" it redirects with the message as a global flash error. message overrides the text. It works with any conversion or validation, Bean Validation included. A redirect always has the status of the redirect; to answer another status, use sendError and an error-page.

Behind the tags, a page that is reached by path instead of query parameters (/book/123) uses o:pathParam and @Param(pathIndex) with FacesViews (FacesViews: Extensionless URLs, MultiViews and Dynamic Routes). Three more input components belong to the same family and were only read, not run: o:hashParam (the fragment after #, kept in sync with a model value by window.onhashchange and every Ajax response), o:scriptParam (the result of a JavaScript expression that the client evaluates and sends, for the time zone offset or the screen width) and o:componentIdParam (renders only the components named by a GET parameter, for mashups). All three put client-controlled data in the model; validate it like any request parameter.

Choosing. A single lookup of a record by id is a good fit for @Param (@Param, @Cookie and @ContextParam: Injecting Request Data). A page that must reject a bad or absent parameter before anything runs, and that posts back, is a good fit for the OmniFaces tags above.

Validators: o:validateBean and the Multi-Field Family

Bean Validation (see the Bean Validation page) validates one property at a time during Process Validations, because that is how Faces works: a model is updated only after all inputs are valid. Two needs fall outside: switching validation off for one button, and validating a constraint that spans several properties of one object. o:validateBean serves both, and the o:validateXxx components do the same for plain inputs without a bean.

o:validateBean: per component and per class

Per component, it is disabled and validationGroups on a command or an input, where the standard f:validateBean offers one setting for a whole form or request:

Skip Bean Validation for a "save draft" button
<h:commandButton id="nobv" value="save without checks" action="#{borrowDraftBean.submit}">
  <o:validateBean disabled="true"/>
</h:commandButton>

On the scratch page a @Size(min = 3) note field with the value ab failed with the message note too short when the ordinary button was pressed, and the button that carried <o:validateBean disabled="true"/> went through and ran its action (Saved ...). Use it for drafts. Do not use it to bypass validation of data that you will persist: the model still receives the unvalidated value, so the service layer must validate again (the rule of the service layer section).

Per class, <o:validateBean value="#{bean.object}"/> validates the whole object with its class-level constraints after all its properties were set. The problem it solves is that the standard flow validates the properties one at a time and never sees the object. Faces 4.1 has its own answer, f:validateWholeBean, and the model class and the constraint here are the ones of the Bean Validation page (class-level validation): a BorrowRequest with @NotNull pick-up and return dates and a class-level @ValidPeriod(groups = PeriodChecks.class), "the return date is after the pick-up date". The tag names the group:

validate.xhtml, the whole form of the examples of this section
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets"
      xmlns:o="omnifaces">
<h:head><title>Validate</title></h:head>
<h:body>
  <h:messages id="msgs"/>
  <h:form id="form">
    <h:inputText id="from" value="#{borrowDraftBean.request.from}"><f:convertDateTime type="localDate" pattern="yyyy-MM-dd"/></h:inputText>
    <h:inputText id="to" value="#{borrowDraftBean.request.to}"><f:convertDateTime type="localDate" pattern="yyyy-MM-dd"/></h:inputText>
    <o:validateBean value="#{borrowDraftBean.request}" validationGroups="org.example.bookshelf.validation.PeriodChecks" showMessageFor="@global"/>
    <h:inputSecret id="pw" value="#{borrowDraftBean.password}" redisplay="true"/>
    <h:inputSecret id="pw2" value="#{borrowDraftBean.confirm}" redisplay="true"/>
    <o:validateEqual id="eq" components="pw pw2" message="Passwords differ"/>
    <h:message for="eq"/>
    <h:inputText id="f2" value="#{borrowDraftBean.from2}"/>
    <h:inputText id="t2" value="#{borrowDraftBean.to2}"/>
    <o:validateOrder id="ord" components="f2 t2" type="lt" message="Second must come after first"/>
    <h:message for="ord"/>
    <h:selectBooleanCheckbox id="ok" value="#{borrowDraftBean.accepted}" requiredMessage="Tick to accept"><o:requiredCheckboxValidator/></h:selectBooleanCheckbox><h:message for="ok"/>
    <h:inputText id="note" value="#{borrowDraftBean.extra}"/>
    <h:commandButton id="nobv" value="nobv" action="#{borrowDraftBean.submit}"><o:validateBean disabled="true"/></h:commandButton>
    <h:commandButton id="go" value="go" action="#{borrowDraftBean.submit}"/>
  </h:form>
  <p>log=<span id="log">#{borrowDraftBean.log}</span></p>
</h:body>
</html>

One change to that page’s code is needed. Its ValidPeriodValidator calls request.getFrom().isBefore(request.getTo()) without a null test, because for f:validateWholeBean the specification guarantees that the properties have already passed. o:validateBean makes no such guarantee: it validates a copy, in the same pass, with every constraint of the group. The scratch run submitted the form with the return date empty:

  • with the validator of the Bean Validation page, both implementations answered HTTP 500: ValidationException: HV000028: Unexpected exception during isValid call, caused by a NullPointerException in the validator;

  • with the guard below, both answered the form again with must not be null, from the @NotNull of the group.

ValidPeriodValidator.java, tolerant of missing dates (the constraint reports them)
package org.example.bookshelf.validation;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.example.bookshelf.model.BorrowRequest;

public class ValidPeriodValidator implements ConstraintValidator<ValidPeriod, BorrowRequest> {

    @Override
    public boolean isValid(BorrowRequest request, ConstraintValidatorContext context) {
        if (request.getFrom() == null || request.getTo() == null) {
            return true; // the @NotNull constraints of the same group report missing dates
        }
        return request.getFrom().isBefore(request.getTo());
    }
}

Observed (both implementations): with a start date after the end date the form was redisplayed with The return date must be after the pick-up date as a global message (showMessageFor="@global") and the action did not run; with valid dates the action ran (Saved 2026-01-01 to 2026-02-01). The mechanism matters because it has consequences. Faces updates the model only when all inputs are valid, so to validate the object OmniFaces first makes a copy of the bean, sets the submitted values on the copy and validates there; if the validation passes, the original is updated. The copy is made with the first strategy that works: Cloneable, Serializable (as BorrowRequest is), a copy constructor, or a new instance; the copier attribute names another one. Where copying is impossible, method="validateActual" updates the model and validates after Update Model Values, with the disadvantages the Javadoc lists: the invalid values stay in the model and the action runs anyway, so it must check FacesContext.isValidationFailed(). The tag collects the values before Process Validations and checks the constraints after it.

Where the message goes is controlled by showMessageFor: @form (the default, on the form’s client id), @all, @global, a list of client ids, or @violating, which puts each violation on the input whose property it concerns and falls back to the form. messageFormat (3.12) overrides the site-wide jakarta.faces.validator.BeanValidator.MESSAGE format for this tag; its {0} is the message and {1} the labels. Give the inputs labels (o:outputLabel, The o: Tags, Component by Component) so that the messages of the individual properties name the field.

The multi-field validators

For constraints between plain inputs there is a family of components with one shared contract. They are placed in the same form as the inputs and take the inputs as a space-separated list of client ids (or, since 3.13, one common parent) in components:

Table 8. The multi-field validators
Tag Passes when Default message

o:validateEqual

all values are equal

{0}: Please fill out the same value for all of those fields

o:validateOrder

the values are in the order type says: lt (default, strictly ascending), lte, gt, gte

{0}: Please fill out the values of all those fields in order

o:validateUnique

no value occurs twice

{0}: Please fill out an unique value for all of those fields

o:validateAll

all are filled

{0}: Please fill out all of those fields

o:validateAllOrNone

all or none are filled

{0}: Please fill out all or none of those fields

o:validateOne, o:validateOneOrMore, o:validateOneOrNone

exactly one, at least one, at most one is filled

...only one of those fields, ...at least one..., ...only one or none...

o:validateMultiple

your validateMethod (signature boolean m(FacesContext, List<UIInput>, List<Object>)) or a MultiFieldValidator object returns true

none; set message

o:validateUniqueColumn

(a tag handler inside a column) the input’s value is unique in all rows of the UIData, also the rows not on the current page; does not work when the model is lazy

{0}: Please fill out an unique value for the entire column. Duplicate found in row {1}

The attributes shared by the component family are components (required), message (a {0} is replaced by the labels), invalidateAll (mark all inputs invalid, default, or only the offending ones), showMessageFor (@this, the default, @all, @invalid, @global or ids), disabled and binding for the read-only validationFailed property. Examples from the scratch form, with the two new tags in validate.xhtml above:

<h:inputSecret id="pw" value="#{borrowDraftBean.password}" redisplay="true"/>
<h:inputSecret id="pw2" value="#{borrowDraftBean.confirm}" redisplay="true"/>
<o:validateEqual id="eq" components="pw pw2" message="Passwords differ"/>
<h:message for="eq"/>

Observed on both implementations: different passwords gave Passwords differ, inputs 2 and 1 under o:validateOrder type="lt" gave Second must come after first, and equal passwords and ascending values passed.

Four rules from the Javadoc decide whether they behave:

  • Placement is the priority. The validators validate when the tree walk reaches them. A validator placed after all its inputs runs after their own converters and validators, and is skipped if one of them failed. A validator placed before them runs first and, if it fails, their own converters and validators do not run. Placing it between the inputs is discouraged because only the preceding inputs are converted. Put it at one end.

  • It always runs in Process Validations, cannot be made immediate, and is skipped entirely when an immediate="true" command calls renderResponse().

  • A disabled, read-only or non-rendered input contributes its model value, so the validator can compare against existing data.

  • disabled takes an EL expression that is evaluated per request. The Javadoc example is disabled="#{param.validationDisabled}". Do not copy that: a condition the client controls is a switch for the client to turn your validation off. Use a server-side condition.

o:requiredCheckboxValidator fixes a different bug: a required="true" on h:selectBooleanCheckbox never fails, because an unticked box is coerced to false, not to null. Nested in the checkbox, with a requiredMessage, it fails when the box is not ticked. The run showed Tick to accept for the unticked box. The standard tags and the standard approach to cross-field validation (a postValidate listener, f:validateWholeBean) are in the validators page; the OmniFaces family is the no-Java version of the first.

Behind both: org.omnifaces.util.Validators runs Bean Validation programmatically (validateBean(bean, groups), resolveViolatedProperty) when a service has to validate by hand.

Converters

A converter turns a submitted string into an object and back (see the converters page). Most of the OmniFaces converters exist to remove the need to write one. Each has a converter id (omnifaces.SelectItemsConverter) for the converter attribute and, since 4.5, a tag (o:selectItemsConverter) for nesting in the component.

Table 9. The OmniFaces converters
Converter What it does Needs

SelectItemsConverter

Converts a selected entity back to the object from the f:selectItems list of the same component, by comparing the string form

a stable, unique toString(), and equals()/hashCode()

SelectItemsIndexConverter

The same, by the position (index) in the list

a list that is identical on the postback

ListConverter, ListIndexConverter

The same for components that take a List and not f:selectItems, such as p:pickList; the list is passed with o:converter or the tag

list attribute

GenericEnumConverter

Converts enum values for a List<E> property, where the type is lost at run time

only if the component has no value; not needed for a bound List<Enum> since Faces 2.3

ToCollectionConverter

A delimited text into a collection (delimiter, collectionType, itemConverter)

the property type

TrimConverter, ToLowerCaseConverter, ToUpperCaseConverter

Trim whitespace; lower or upper case in the current locale (and trim). No conversion in getAsString

none

CompositeConverter (5.1)

Chain several converters (converterIds); getAsObject in the order given, getAsString in reverse

ids of registered converters

ImplicitNumberConverter

A number without the percent or currency symbol, accepting input with or without it

type (percent or currency) and the symbol

The one converter that nearly every project needs is SelectItemsConverter. The usual dropdown over entities fails without a converter, because a submitted string cannot become a Book; the usual workaround is a converter that queries the database by id on every postback. The scratch page:

select.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets"
      xmlns:o="omnifaces">
<h:head><title>Select</title></h:head>
<h:body>
  <h:messages id="msgs"/>
  <h:form id="form">
    <h:selectOneMenu id="plain" value="#{pickBean.selectedPlain}">
      <f:selectItems value="#{pickBean.available}" var="b" itemValue="#{b}" itemLabel="#{b.title}"/>
    </h:selectOneMenu>
    <h:selectOneMenu id="sel" value="#{pickBean.selected}" converter="omnifaces.SelectItemsConverter">
      <f:selectItems value="#{pickBean.available}" var="b" itemValue="#{b}" itemLabel="#{b.title}"/>
    </h:selectOneMenu>
    <h:selectOneMenu id="tag" value="#{pickBean.selectedIndex}">
      <f:selectItems value="#{pickBean.available}" var="b" itemValue="#{b}" itemLabel="#{b.title}"/>
      <o:selectItemsIndexConverter/>
    </h:selectOneMenu>
    <h:selectManyCheckbox id="gen" value="#{pickBean.genres}">
      <f:selectItems value="#{pickBean.availableGenres}"/>
    </h:selectManyCheckbox>
    <h:inputText id="tags" value="#{pickBean.tags}"><o:toCollectionConverter/></h:inputText>
    <h:inputText id="name" value="#{pickBean.name}"><o:trimConverter/></h:inputText>
    <h:inputText id="email" value="#{pickBean.email}"><o:toLowerCaseConverter/></h:inputText>
    <h:inputText id="code" value="#{pickBean.code}"><o:toUpperCaseConverter/></h:inputText>
    <h:inputText id="price" value="#{pickBean.price}"><o:implicitNumberConverter type="currency" currencySymbol="$"/></h:inputText>
    <h:inputText id="euro" value="#{pickBean.euro}"><o:implicitNumberConverter type="currency" currencySymbol="€"/></h:inputText>
    <h:inputText id="pct" value="#{pickBean.pct}"><o:implicitNumberConverter type="percent"/></h:inputText>
    <h:commandButton id="go" value="go" action="#{pickBean.submit}"/>
  </h:form>
  <ul>
    <li>plain=<span id="rPlain">#{pickBean.selectedPlain}</span></li>
    <li>sel=<span id="rSel">#{pickBean.selected}</span></li>
    <li>index=<span id="rIndex">#{pickBean.selectedIndex}</span></li>
    <li>genres=<span id="rGenres">#{pickBean.genres}</span></li>
    <li>tags=<span id="rTags">#{pickBean.tags}</span></li>
    <li>name=[<span id="rName">#{pickBean.name}</span>] email=<span id="rEmail">#{pickBean.email}</span> code=<span id="rCode">#{pickBean.code}</span> price=<span id="rPrice">#{pickBean.price}</span> euro=<span id="rEuro">#{pickBean.euro}</span> pct=<span id="rPct">#{pickBean.pct}</span></li>
  </ul>
</h:body>
</html>

Observed on both implementations, with Book as a record with title, author, year and genre:

  • The menu plain, without a converter, rendered each option’s value as the record’s toString() and failed on the postback: Conversion Error setting value 'Book[id=2, title=Cosmos, author=Carl Sagan, year=1980, genre=SCIENCE]' for 'null Converter'. The error also stops every other component in the form from being updated.

  • The same menu with omnifaces.SelectItemsConverter converted the submitted toString() back to the same instance from the list, so pickBean.selected was the record for Cosmos. No database was touched.

  • The menu with o:selectItemsIndexConverter rendered the values 0, 1, 2, 3 and converted 2 to the third book.

  • A List<Genre> bound to h:selectManyCheckbox held [SCIENCE, HISTORY] with no converter at all; the GenericEnumConverter is obsolete for that case in Faces 4.1, as its Javadoc says.

  • o:toCollectionConverter turned + a , b,c ,a+ into [a, b, c, a] (the default is an ArrayList; a Set property gets a LinkedHashSet); o:trimConverter turned + Ada ` into `+Ada; o:toLowerCaseConverter turned + ADA@Example.ORG ` into `+ada@example.org; o:toUpperCaseConverter turned + zx-1 ` into `+ZX-1; o:implicitNumberConverter type="percent" turned 25 and 25% into 0.25.

What this teaches about SelectItemsConverter and toString():

  • The toString() is the wire format. Every option’s value in the HTML source is the string form of the entity, here all five fields of the record, visible to anyone who views the page source. A toString() that contains sensitive data (a price paid, a note, a personal field) leaks it into the page. Either write a toString() that holds only the id (Book[id=2], the pattern the Javadoc recommends) or subclass the converter and override getAsString (the Javadoc shows it), or use the index converter, which exposes nothing.

  • It must be unique and stable. Two different entities with the same string would be confused, and an entity whose string changes between the render and the postback (a field edited in between) no longer matches.

  • It needs the same list on the postback. The converter looks the string up in the items of the component, which are evaluated again on the postback. A list that is not the same on both requests (a request scoped bean with a changing query) breaks the match; the index variant is the more fragile to a list that changes.

  • equals() and hashCode() of the entity must still be right for Faces to select the right option on render.

An ImplicitNumberConverter limit on user input. The scratch run showed that the converter fails on input that the other converters accept. When the submitted text lacks the symbol, the converter builds the string with Matcher.replaceAll using the submitted value as the replacement; a submitted $1 (for a field with currencySymbol="€" or for a percent field) ended in HTTP 500 on both implementations, with IndexOutOfBoundsException: No group 1 in the log. On MyFaces, a currency field also failed for ordinary input: 12.50 gave '¤12.50' could not be understood as a currency value, the generic currency sign in place of the symbol, and $12.50 gave HTTP 500 for the same replaceAll reason, while Mojarra accepted both. The percent case worked on both. Treat the currency mode as unverified on MyFaces, and write your own converter, or validate the text first, when a currency field is public. This is an observation on 5.5.5, not documented behaviour; test it again on the version you use.

With PrimeFaces, the converter works as in the Javadoc: p:selectOneMenu with omnifaces.SelectItemsConverter, and p:pickList with <o:converter converterId="omnifaces.ListConverter" list="#{pickBean.picks.source}"/>, converted Cosmos back to the right instance and the picked list to a one-element target list on Mojarra and on MyFaces (OmniFaces Next to PrimeFaces).

The o: Tags, Component by Component

OmniFaces 5.5.5 declares 72 tags under the o: prefix (the VDL documentation lists them all with their attributes). They fall into groups, and three of them are the subject of the earlier sections (the validators, the converters, and the view parameter family). The table is the map; the subsections that follow detail the ones that are used all the time, with what the scratch run showed.

Table 10. The 72 tags of OmniFaces 5.5.5 by purpose
Purpose Tags

Forms and links

o:form, o:link, o:url, o:sitemapUrl, o:param, o:pathParam

Inputs

o:inputFile, o:inputHidden, o:viewParam, o:viewAction, o:hashParam, o:scriptParam, o:componentIdParam

Output and feedback

o:outputLabel, o:outputFormat, o:messages, o:highlight, o:graphicImage, o:cache, o:cacheValue, o:conditionalComment, o:lazyPanel, o:resourceInclude, o:tree, o:treeNode, o:treeNodeItem, o:treeInsertChildren

Scripts and styles

o:onloadScript, o:deferredScript, o:criticalStylesheet, o:scriptErrorHandler

Component tree utilities

o:moveComponent, o:resolveComponent, o:selectItemGroups (deprecated for removal since 5.4: use the standard f:selectItemGroups)

Push

o:socket, o:sse, o:notification (Push: o:socket, o:sse and o:notification)

Validators

o:validateBean, o:validateAll, o:validateAllOrNone, o:validateEqual, o:validateMultiple, o:validateOne, o:validateOneOrMore, o:validateOneOrNone, o:validateOrder, o:validateUnique, o:validateUniqueColumn, o:requiredCheckboxValidator, o:validator (Validators: o:validateBean and the Multi-Field Family)

Converters

o:converter, o:compositeConverter, o:genericEnumConverter, o:implicitNumberConverter, o:listConverter, o:listIndexConverter, o:selectItemsConverter, o:selectItemsIndexConverter, o:toCollectionConverter, o:toLowerCaseConverter, o:toUpperCaseConverter, o:trimConverter (Converters)

Tag handlers

o:importConstants, o:importFunctions, o:loadBundle, o:massAttribute, o:methodParam, o:tagAttribute, o:skipValidators, o:ignoreValidationFailed, o:enableRestorableView, o:viewParamValidationFailed (Tag Handlers: importConstants, importFunctions and the rest)

The standard h:form posts to the view id and drops the query string; a request scoped bean that needs the same query parameters on the postback does not get them. o:form posts to the exact request URI with its query string. This is the default since 3.0 (useRequestURI); includeRequestParams="true" keeps just the parameters, and <f:param> or <o:param> inside the form adds parameters to the action URL. Also since 3.0 the form posts, during an Ajax request, only the parts of the form that the execute list needs (partialSubmit, comparable to the PrimeFaces attribute of the same name), which shrinks the payload of a large editable table.

components.xhtml (the relevant parts)
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets"
      xmlns:o="omnifaces">
<h:head><title>Components</title></h:head>
<h:body>
  <o:messages id="msgs" for="form:title form:author"/>
  <o:form id="form">
    <o:outputLabel for="title" value="Book title"/>
    <h:inputText id="title" value="#{compBean.title}" required="true"/>
    <o:outputLabel for="author" value="Author name"/>
    <h:inputText id="author" value="#{compBean.author}" required="true"/>
    <h:commandButton id="save" value="save" action="#{compBean.save}"/>
    <h:commandButton id="skip" value="skip" action="#{compBean.skip}"><o:skipValidators/></h:commandButton>
    <h:commandButton id="ignore" value="ignore" action="#{compBean.ignore}"><o:ignoreValidationFailed/></h:commandButton>
  </o:form>
  <o:highlight/>
  <p>log=<span id="log">#{compBean.log}</span></p>
  <p>link=<o:link id="l1" outcome="components" value="same query" includeRequestParams="true"/>
     <h:link id="l2" outcome="components" value="std link"/>
     url=<span id="url"><o:url includeRequestParams="true"/></span></p>
  <o:cache id="c1" time="60">
    <p>cached hit=<span id="hit">#{compBean.hits}</span></p>
  </o:cache>
</h:body>
</html>

Requested as components.xhtml?x=1 the form’s action was /bookshelf/components.xhtml?x=1 on both implementations. <o:link includeRequestParams="true"/> rendered href=".../components.xhtml?x=1&y=2" and the plain h:link the bare view; <o:url includeRequestParams="true"/> rendered the full URL http://localhost:8570/bookshelf/components.xhtml?x=1&y=2 (with a ;jsessionid=... path parameter on the first request of a client without a cookie, as every URL rewritten by the container has). o:url exists for the places where JSTL’s c:url was used: a link element, a JavaScript variable, a canonical URL, with viewId or value, domain, includeViewParams, nested o:param and a var that holds the URL instead of printing it.

A nested form is a mistake that browsers silently repair. According to the Javadoc of OmniViewHandler, OmniFaces turns it into an IllegalStateException in the Development project stage, which helps when a template already has a form (not run).

o:outputLabel, o:highlight and o:messages: feedback that names the field

o:outputLabel is an h:outputLabel that sets its own value as the label of the input in for, so the validation message names the field the user saw. In the run, with an empty title the message read Book title: Validation Error: Value is required., taken from the label text, and not from a generated id. o:highlight adds an error style class to every invalid input and its label and focuses the first invalid one; one per page, ideally in the master template. The markup it produced for the failed title was a script, OmniFaces.Highlight.apply(["form:title"], 'error', true);, and you write the CSS (input.error {...}); styleClass and focus configure it. The class is removed again when the user starts to use the field. o:messages is in The Messages Utility and o:messages.

o:skipValidators and o:ignoreValidationFailed: buttons that do not need a valid form

Both are tag handlers that go inside a command (or an Ajax-capable component) and change how a failure is treated. o:skipValidators skips validation entirely for that action: converters still run and the model is still updated. o:ignoreValidationFailed lets the action run even though validation failed (inside an o:form; with Ajax, the form must be in the execute list), updating only the inputs that passed. In the run, with the required title empty and author set:

  • the ordinary button redisplayed the form with Book title: Validation Error: Value is required. and did not run the action;

  • the button with o:skipValidators ran its action (log=skip) and showed no message;

  • the button with o:ignoreValidationFailed ran its action (log=ignore(null/Ada): title still null, author updated) and still showed the message.

Use them for "add a row", "save as draft" and "cancel" buttons. Compare the standard answer, immediate="true", and its consequences in the lifecycle page; these tags do not skip the Update Model Values phase the way immediate does. The validators of a skipped action are skipped: an action that processes the model must validate it by itself.

o:enableRestorableView: an expired view that can be rebuilt

An expired view makes a postback fail with ViewExpiredException (see the state saving page and the exception handling page). <o:enableRestorableView/> in f:metadata makes OmniFaces rebuild the view from scratch when the state is missing and the request is a postback. The view comes back exactly as it was on the first GET, so changes made since then (conditionally rendered parts, a view scoped value) are lost: the page must work with a request scoped bean. Two pages differed in the run only by this tag, and posted the form with a view state that the server had never issued:

  • the page with the tag answered HTTP 200 and ran the action, on Mojarra and on MyFaces;

  • the page without it answered HTTP 500 (the unhandled ViewExpiredException) on both.

The Javadoc suggests <f:view transient="true"> instead where it fits, because it keeps no state at all (stateless views). Rebuilding an expired view also means that a request that carries a forged or stale state is accepted as a fresh view; that is the whole point, and it limits the use to pages where a replayed form is harmless. Do not put it on a page that performs a state-changing action protected only by the view state (the CSRF section).

o:cache: caching a rendered fragment

o:cache stores the rendered markup of its children and serves it without evaluating the beans behind it, for time seconds. The default scope is session (one copy per user); scope="application" shares one copy among all users. The key defaults to the view id plus the client id of the component (the Javadoc warns that a client id can change when the view changes dynamically, so set key yourself in that case); reset drops the entry and disabled switches the component off; o:cacheValue caches a single value like c:set. Capacity and lifetime defaults come from the context parameters org.omnifaces.CACHE_SETTING_SESSION_MAX_CAPACITY, org.omnifaces.CACHE_SETTING_SESSION_TTL, org.omnifaces.CACHE_SETTING_APPLICATION_MAX_CAPACITY and org.omnifaces.CACHE_SETTING_APPLICATION_TTL, and the implementation from org.omnifaces.CACHE_PROVIDER.

The scratch page cached.xhtml has three caches around request-driven content: one with scope="application", one with the default scope, one around a form. A bean printed the request parameter u:

  • Alice requested ?u=alice and the application cache stored user=alice. Bob, a different client with a different session, requested ?u=bob and was served user=alice by the application cache and user=bob by the session cache. This is the leak: whatever the first visitor caused to be rendered, every later visitor gets.

  • The counter result: the first fragment evaluation printed a number from a bean counter and the second request of the same session served the same number (the bean was not asked again), on Mojarra and on MyFaces.

  • The cached form broke. On the second request Mojarra served the form with the literal text ~com.sun.faces.saveStateFieldMarker~ where the view state field belongs, and MyFaces served the jakarta.faces.ViewState field of the first request, again on every hit. Neither form can be posted back successfully.

The rules that follow: a fragment that depends on the user (a name, a price list, a menu by role, a token) must never be cached in application scope; keep the default session scope for per-user fragments, and for application scope cache only content that is identical for all users and does not change between requests. Never put a form, or anything that contains view state or a CSRF token, inside o:cache.

Small output helpers

The rest of the group, each shown by the scratch page misc.xhtml on both implementations (the HTML is what the library rendered):

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:o="omnifaces">
<h:head><title>Misc</title>
  <o:criticalStylesheet name="css/crit.css"/>
  <o:deferredScript name="js/late.js" onsuccess="window.late=1"/>
  <o:conditionalComment if="lte IE 9"><link rel="stylesheet" href="old.css"/></o:conditionalComment>
</h:head>
<h:body>
  <o:massAttribute name="styleClass" value="wide">
    <h:inputText id="a"/><h:inputText id="b" styleClass="own"/>
  </o:massAttribute>
  <o:resolveComponent for="a" name="aComp"/>
  <p id="r">#{aComp.clientId}</p>
  <o:lazyPanel id="lp" loaded="true"><p id="inside">lazy content</p></o:lazyPanel>
  <o:lazyPanel id="lp2"><p id="inside2">lazy two</p></o:lazyPanel>
  <o:outputFormat value="Hello {0}" var="_greeting"><f:param value="reader"/></o:outputFormat>
  <p id="g">#{_greeting}</p>
</h:body>
</html>
  • o:conditionalComment renders the markup inside a conditional comment: <!--[if lte IE 9]><link rel="stylesheet" href="old.css" /><![endif]-->, even with jakarta.faces.FACELETS_SKIP_COMMENTS set to true, which is the setting that removes ordinary comments. Only for a legacy browser you still have to support, which in 2026 should be none.

  • o:criticalStylesheet rendered <link rel="preload" href="..." as="style" onload="this.onload=null;this.rel='stylesheet'" /> followed by a noscript fallback with the plain link, and moved it to the top of the head, ahead of the OmniFaces and Faces scripts.

  • o:deferredScript rendered <script>OmniFaces.DeferredScript.add('/bookshelf/jakarta.faces.resource/js/late.js.xhtml','','',null,function() {window.late=1});</script> at the end of the body; the onsuccess code ran after the window load event (window.late was 1).

  • o:lazyPanel with loaded="true" rendered its children; without it, an empty div and OmniFaces.LazyPanel.init('lp2', {}), and the content arrived by an Ajax request when the panel scrolled into view (it did, in the headless Chrome run). The listener receives a LazyPanelEvent once.

  • o:massAttribute put class="wide" on the input without a styleClass and left class="own" on the one that had one.

  • o:resolveComponent made the component a available by name (#{aComp.clientId} printed a); o:moveComponent moves components, facets and behaviors to another place in the tree (BEFORE, AFTER, ADD_FIRST, ADD_LAST, FACET, BEHAVIOR); o:outputFormat var="_greeting" captured Hello reader into a variable instead of printing it.

  • o:tree (with o:treeNode, o:treeNodeItem and o:treeInsertChildren) renders any TreeModel with markup that you write, recursively; it draws nothing itself. Not run.

  • o:inputHidden converts, validates and updates the model immediately in Apply Request Values, regardless of errors on other inputs. o:inputFile adds maxsize, directory and server-side checks of accept to h:inputFile (o:inputFile).

  • o:scriptErrorHandler reports uncaught JavaScript errors and rejected promises by navigator.sendBeacon() to the server, where they arrive as a CDI ScriptError event for an observer to log; o:resourceInclude renders the output of a JSP or servlet and exists only to ease a migration away from JSP. Neither was run.

Two cautions that the markup above makes concrete. Several of these components write inline script (OmniFaces.Highlight.apply(...), DeferredScript.add(...), LazyPanel.init(...)) and an inline onload attribute (o:criticalStylesheet); a Content-Security-Policy without 'unsafe-inline' or a nonce blocks them, which you find out only in a browser. The CSP section of the security page explains the nonce support of Mojarra and what PrimeFaces 16 does; apply the same test to every OmniFaces component you use. And o:resourceInclude, o:scriptParam, o:hashParam and o:componentIdParam are all doors for client data or server resources into the page: do not pass a user-chosen value to path.

o:inputFile

o:inputFile is h:inputFile with accept and maxsize checked on the server (acceptMessage and maxsizeMessage customise the text), multiple and directory (select a folder). It does not replace the controls of the upload page: the accept check compares the declared content type, which the client chooses, so inspect the content too, and keep the container’s multipart limits. It was not run here.

Tag Handlers: importConstants, importFunctions and the rest

Tag handlers work when the view is built, not at render time. The ones that matter most:

o:importConstants and o:importFunctions

Two things that EL cannot reach are public static final constants and static methods. o:importConstants exposes the constants of a class, an interface or an enum as a map in the request scope under the simple name of the type (or var); o:importFunctions registers the public static methods of a class as EL functions, under the prefix you give in var:

consts.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets"
      xmlns:o="omnifaces">
<h:head><title>Consts</title></h:head>
<h:body>
  <o:importConstants type="org.example.bookshelf.model.Limits"/>
  <o:importConstants type="org.example.bookshelf.model.Genre" var="G"/>
  <o:importFunctions type="java.lang.Math" var="m"/>
  <ul>
    <li>max=<span id="max">#{Limits.MAX_TITLE_LENGTH}</span></li>
    <li>sort=<span id="sort">#{Limits.SORT_BY_YEAR}</span></li>
    <li>genre=<span id="genre">#{G.SCIENCE}</span></li>
    <li>members=<span id="members">#{G.members()}</span></li>
    <li>values=<span id="values">#{G.values()}</span></li>
    <li>abs=<span id="abs">#{m:abs(-10)}</span></li>
    <li>abbr=<span id="abbr">#{o:abbreviate('The quick brown fox', 10)}</span></li>
    <li>join=<span id="join">#{o:joinCollection(utilsBean.locales, ' / ')}</span></li>
    <li>fmt=<span id="fmt">#{o:formatBytes(123456789)}</span></li>
    <li>flag=<span id="flag">#{o:flagEmoji('ES')}</span></li>
    <li>json=<span id="json">#{o:toJson(pickBean.available.get(0))}</span></li>
    <li>cap=<span id="cap">#{o:capitalize('dune')}</span> fmt1=<span id="fmt1">#{o:format1('Hello {0}', 'reader')}</span> pretty=<span id="pretty">#{o:prettyURL('Dune: Messiah, 1969!')}</span> strip=<span id="strip">#{o:stripTags('&lt;b&gt;bold&lt;/b&gt; text')}</span></li>
    <li>thousands=<span id="th">#{o:formatThousandsUnit(1234567, 'B')}</span> pct=<span id="pct">#{o:formatPercent(0.256)}</span> years=<span id="yrs">#{o:yearsBetween(utilsBean.start, utilsBean.end)}</span> date=<span id="dt">#{o:formatDate(utilsBean.start, 'dd MMM yyyy')}</span> matches=<span id="mt">#{o:matches('978-0441013593', '[0-9-]{13,17}')}</span></li>
  </ul>
</h:body>
</html>
Limits.java
package org.example.bookshelf.model;

/** Constants shared by views and beans (imported with o:importConstants). */
public final class Limits {
    public static final int MAX_TITLE_LENGTH = 120;
    public static final String SORT_BY_TITLE = "title";
    public static final String SORT_BY_YEAR = "year";

    private Limits() {
    }
}

Observed on both implementations: #{Limits.MAX_TITLE_LENGTH} printed 120, #{Limits.SORT_BY_YEAR} printed year, #{G.SCIENCE} printed SCIENCE for the enum imported as G, #{G.values()} and #{G.members()} both listed [FICTION, SCIENCE, HISTORY], and #{m:abs(-10)} printed 10. Details from the Javadoc: an unknown constant name throws IllegalArgumentException at run time (EL cannot check it earlier); for an enum, members() (since 4.6) lists only the enum constants where values() of an enum that implements an interface lists the interface constants too; loader (4.3) names an object whose class loader finds the type, for a classpath with duplicates.

Faces has had its own f:importConstants since 2.3. It must be a child of f:metadata, which is why it does not suit an include, a tag file or a composite component, where o:importConstants can be placed anywhere (the Javadoc’s argument). In a plain view f:importConstants is enough (see the core tags page). o:importFunctions has no standard counterpart; EL functions otherwise need a tag library XML file (the tag libraries page). EL does not support overloading, so with several methods of the same name the one with the fewest parameters wins.

The other tag handlers

  • o:converter and o:validator extend f:converter and f:validator with deferred value expressions in all attributes, evaluated at each access instead of once when the view is built, so they can differ per row of an iterating component and refer to bean properties. They are how a parameterised converter is configured (<o:converter converterId="omnifaces.ImplicitNumberConverter" type="currency" currencySymbol="$"/>), and what o:listConverter and o:validateBean build on.

  • o:massAttribute, o:tagAttribute, o:methodParam help tag files and templates: set an attribute on every nested component (with target to select a class), declare a tag attribute explicitly so a parent’s value is cleared, pass a method expression through a tag attribute.

  • o:loadBundle extends f:loadBundle with a loader and better default locale handling. The bundle approach of the messages page does the same without a handler.

  • o:skipValidators, o:ignoreValidationFailed, o:enableRestorableView, o:viewParamValidationFailed, o:validateBean, o:validateUniqueColumn are covered above.

The o: EL Functions (formerly of:)

OmniFaces ships 81 EL functions: static methods wrapped as functions for use in value expressions. In 1.x to 4.x they lived under a second prefix, of:, with its own namespace; in 5.0 they merged into o: (Installing OmniFaces). The old code is rewritten by a rename: #{of:formatDate(...)} becomes #{o:formatDate(...)}. They are grouped by the class in org.omnifaces.el.functions:

Table 11. The EL functions by class
Class Functions

Strings

abbreviate, capitalize, concat, encodeBase64, encodeURI, encodeURL, escapeJS, flagEmoji (5.1), format1 to format5, matches, parenthesize, prettyURL, replaceAll, stripTags

Dates

addDays, addHours, addMinutes, addMonths, addSeconds, addWeeks, addYears; daysBetween and the other ...Between functions; formatDate, formatDateWithTimezone; getDayOfWeek, getDaysOfWeek, getMonth, getMonths and their getShort... variants

Numbers

formatBytes, formatCurrency, formatNumber, formatNumberDefault, formatPercent, formatThousands, formatThousandsUnit, and a ...ForLocale variant of each (5.0)

Arrays, Objects

contains, createArray, createIntegerArray, reverseArray; coalesce, isInstance

Converters

convert, iterableToList, iterableToModel, joinArray, joinCollection, joinMap, mapToList, setToList, splitArray, splitList, toJson, printStackTrace

Components, Faces

evalAttribute, graphicImageURL...; getRemoteAddr, getRequestBaseURL, getRequestDomainURL, getRequestURLWithQueryString

Results from the scratch page consts.xhtml above, identical on both implementations:

Expression Result

#{o:abbreviate('The quick brown fox', 10)}

The quick...

#{o:joinCollection(utilsBean.locales, ' / ')}

en / es

#{o:formatBytes(123456789)}

117.7 MiB

#{o:formatThousandsUnit(1234567, 'B')}

1.23 MB

#{o:formatPercent(0.256)}

26%

#{o:flagEmoji('ES')}

the flag of Spain

#{o:toJson(book)} (a record)

{"id":1,"title":"Dune","author":"Frank Herbert","year":1965,"genre":"FICTION"}

#{o:prettyURL('Dune: Messiah, 1969!')}

dune-messiah-1969- (note the trailing hyphen)

#{o:stripTags('<b>bold</b> text')}

bold text

#{o:yearsBetween(start, end)} with two LocalDate values

6

#{o:formatDate(start, 'dd MMM yyyy')}

15 Jan 2020

#{o:matches('978-0441013593', '[0-9-]{13,17}')}

true

Three of the functions are for security-sensitive places, and one is a trap:

  • o:escapeJS escapes a value for a JavaScript string. In the run, a request parameter '; alert(1);//</script><b> came out of var plain = '#{param.t}'; unchanged, which breaks out of the string and out of the script element, and out of var safe = '#{o:escapeJS(param.t)}'; as \'; alert(1);\/\/<\/script><b>, which is inert. The standard EL escaping is for HTML, not for JavaScript (see the XSS section); o:escapeJS closes the gap for string literals. Better still is no data in script at all: use a data- attribute.

  • o:encodeURL percent-encodes a value for a query string: q=#{o:encodeURL(param.t)} gave %27%3B+alert%281%29....

  • o:stripTags removes HTML tags from text; it is a way to produce a plain-text summary, not a sanitiser to make markup safe.

  • o:printStackTrace prints a Throwable, which belongs on a developer error page and nowhere a user can see it (exception messages and class names are internal information, and the stack trace may carry values of the failed request).

Prefer the implicit objects and the plain EL where they do the job; the OmniFaces Javadoc itself says that the o: form of the Faces functions is poor practice next to #{request.contextPath} or #{faces.development}.

o:graphicImage and @GraphicImageBean

The standard h:graphicImage shows a file served as a Faces resource. An image from a database (a book cover) needs a servlet. o:graphicImage accepts a byte[] or an InputStream in its value, and the library serves it through a resource URL, without a servlet. A bean whose methods return the image bytes is marked @GraphicImageBean, a stereotype that makes it a named application scoped bean (the annotation implies @Named and @ApplicationScoped).

CoverBean.java
package org.example.bookshelf.web;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import javax.imageio.ImageIO;
import org.omnifaces.cdi.GraphicImageBean;

/** A GraphicImageBean exposes its methods to o:graphicImage as images, with no servlet. */
@GraphicImageBean
public class CoverBean {
    public byte[] cover(long id) {
        System.setProperty("java.awt.headless", "true");
        BufferedImage image = new BufferedImage(60, 90, BufferedImage.TYPE_INT_RGB);
        Graphics2D g = image.createGraphics();
        g.setColor(new Color((int) (id * 60) % 255, 90, 160));
        g.fillRect(0, 0, 60, 90);
        g.dispose();
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try {
            ImageIO.write(image, "png", out);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
        return out.toByteArray();
    }
}
graphic.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets"
      xmlns:o="omnifaces">
<h:head><title>Graphic</title></h:head>
<h:body>
  <ui:repeat value="#{bookService.all()}" var="b">
    <o:graphicImage id="img" value="#{coverBean.cover(b.id)}" type="png" alt="cover"/>
  </ui:repeat>
  <o:graphicImage id="uri" value="#{coverBean.cover(1)}" dataURI="true" type="png"/>
  <p id="url">#{o:graphicImageURL('coverBean.cover(2)')}</p>
</h:body>
</html>

The rendering on Mojarra and on MyFaces: each img had a src of the form /bookshelf/jakarta.faces.resource/CoverBean_cover.png.xhtml?ln=omnifaces.graphic&v=0&p=1 (the method name and the argument become the resource name and the p parameter), dataURI="true" produced src="data:image/png;base64,...", and #{o:graphicImageURL('coverBean.cover(2)')} printed the URL form. Fetching a URL answered 200 with Content-Type: image/png, a one-week Expires and Last-Modified in 1970 (no lastModified attribute given); lazy="true" defers evaluating the method until the image request, and type, lastModified, width and height are available. An unknown bean or method in the URL (OtherBean_secret.png.xhtml) answered 404, and so did a non-numeric argument for a long parameter.

The security consequence. The Javadoc states that @GraphicImageBean "basically enables endusers to invoke any public method returning a byte[] or InputStream on the bean by just a HTTP GET request." The scratch run fetched ...CoverBean_cover.png.xhtml?ln=omnifaces.graphic&v=0&p=3 with no session cookie and received the image, and any p that the method accepts can be requested. That makes every public image method of the bean a public, guessable URL. Rules that follow:

  • Put only methods on the bean that are meant to be public, and give a private image its own check: authenticate and authorise inside the method (the caller, from an injected SecurityContext or the request), or serve it through a protected servlet instead. This part was not run; the point is that nothing in the URL carries a session.

  • Never return a file path, a name or any other user-supplied string from such a method as a way to choose a file; a method that opens a file from its argument turns the URL into a file reader.

  • Choose identifiers that cannot be guessed where the images are confidential, and set the lastModified or a version so that a changed cover is not cached for a week.

Before @GraphicImageBean (OmniFaces 2.5) the bean could be any @Named @ApplicationScoped bean; the Javadoc explains that after a server restart the images stop working until a Faces page has referenced the method once, because of exactly the security restriction above.

Exception Handlers

The standard ExceptionHandler mechanism and the way an Ajax failure ends up as a blank page are explained on the exception handling page, which also shows OmniFaces' handlers in use (OmniFaces FullAjaxExceptionHandler); the results of that page are not repeated here. What the library offers, in one place:

Table 12. The OmniFaces exception handlers
Handler What it does

FullAjaxExceptionHandler

Renders the error page that web.xml names for the exception, also for an Ajax request, where the standard handler leaves the page unchanged. Needs an error-page for 500 or for java.lang.Throwable. Registers the FacesExceptionFilter itself since 4.5. Supports query parameters in the error page’s location since 5.4 (they need the MutableRequestFilter to be visible as #{param})

ViewExpiredExceptionHandler

Redirects an expired view to the same view with a flash message, org.omnifaces.view_expired (register it last when combined with the full Ajax handler)

FacesMessageExceptionHandler

Adds every exception as a global FATAL FacesMessage; it cannot handle exceptions from Render Response. Not run

ExceptionSuppressor

Suppresses the exception types named in org.omnifaces.EXCEPTION_TYPES_TO_SUPPRESS and refreshes the page, for failures nobody can act on. Not run

faces-config.xml, the registration of the full Ajax handler (a handler that has a factory)
<factory>
  <exception-handler-factory>org.omnifaces.exceptionhandler.FullAjaxExceptionHandlerFactory</exception-handler-factory>
</factory>

The properties that the earlier page verified, on Mojarra 4.1.17 and MyFaces 4.1.4: the Ajax response replaces jakarta.faces.ViewRoot with the error page and answers HTTP 200; the filter unwraps the exception so that a mapping by exception type matches on MyFaces too; an error page that sets its own status made Mojarra answer 500 and leave the browser with a blank page. The error page shows a reference number (#{requestScope['org.omnifaces.exception_uuid']}) and nothing else: no message, no trace.

Filters

Six servlet filters in org.omnifaces.filter; none is registered unless said so, and each is mapped in web.xml like any filter.

Table 13. The OmniFaces filters
Filter Purpose

CompressedResponseFilter

Compression of text responses with Brotli (only when its classes are on the classpath), GZIP or Deflate, as the client accepts. Older documentation calls it GzipResponseFilter; no class of that name exists in 5.5.5 (run on the performance page: 21,383 to 2,061 bytes)

CacheControlFilter

Sets Cache-Control, Expires and Pragma (default: no caching, plus a BFCache buster cookie) on dynamic pages (run on the performance page)

CharacterEncodingFilter

Sets the request body encoding to UTF-8 (or the encoding init parameter) if none was set, for libraries that read parameters too early. POST only

FacesExceptionFilter

Unwraps FacesException and ELException so that error-page matching works, maps Mojarra’s FacesFileNotFoundException to 404 and logs with a UUID; not for Ajax (the handler does that). Auto-registered by FullAjaxExceptionHandler

MutableRequestFilter

Wraps the request so that headers and parameters can be changed (Faces.getMutableRequestParameterMap()); needed for parameters in the error page location. Not for production use to override security-relevant headers

HttpFilter

The abstract base class for your own filter, with doFilter(HttpServletRequest, HttpServletResponse, HttpSession, FilterChain)

The compression and cache control filters are discussed with their measurements on the performance page. Mind one interaction there: OmniFaces' CacheControlFilter disables caching for what it covers, so map it on the views and not on the resources.

Resource Handlers and Renderers

A ResourceHandler produces the URLs of scripts, styles, images and fonts and serves them (see the resource handling page). OmniFaces ships several, wrapping each other like all handlers (the one registered later wraps the earlier ones).

Table 14. The OmniFaces resource handlers
Handler What it does Registered

CombinedResourceHandler

Combines the scripts and styles of the head into one of each. Run, with figures, on the performance page; since 5.5.4 it remembers only the most recently used combined resource ids, so a name derived from request data can no longer grow the cache without bound

no (opt in)

CDNResourceHandler

Maps resource ids to CDN URLs through org.omnifaces.CDN_RESOURCE_HANDLER_URLS, with a wildcard form and an exclusion list (5.4)

no

VersionedResourceHandler

Appends v=<version> to every resource URL; the version from org.omnifaces.VERSIONED_RESOURCE_HANDLER_VERSION, which may be an EL expression

no

UnmappedResourceHandler

Serves resources at /jakarta.faces.resource/css/style.css, without the .xhtml suffix, so that CSS can use relative URLs for images and fonts

no

GraphicResourceHandler

The resource side of o:graphicImage (o:graphicImage and @GraphicImageBean)

yes

PWAResourceHandler

Generates manifest.webmanifest from a WebAppManifest subclass and a service worker; needed by o:notification

yes (inactive without a manifest class)

FacesViewsResourceHandler

FacesViews (FacesViews: Extensionless URLs, MultiViews and Dynamic Routes)

yes

ViewResourceHandler, SourceMapResourceHandler

Serve a Facelets view at a non-.xhtml URL such as /sitemap.xml (for o:sitemapUrl); source maps of combined resources

no

CorsAwareResourceRenderer (5.0) is not a handler but a renderer, and is registered automatically. It adds crossorigin to the script and stylesheet elements whose URL is on another origin (default anonymous, changed with org.omnifaces.DEFAULT_CROSSORIGIN) and, when the resource is a CDNResource, an integrity hash computed from the local content. Before 5.0 CombinedResourceHandler set these attributes itself, and forced CORS mode on same-origin resources, which in 5.5.1 was corrected because it broke jQuery 4 loading.

The three handlers that need configuration, as run in the scratch application:

faces-config.xml
<application>
  <resource-handler>org.omnifaces.resourcehandler.CDNResourceHandler</resource-handler>
  <resource-handler>org.omnifaces.resourcehandler.VersionedResourceHandler</resource-handler>
  <resource-handler>org.omnifaces.resourcehandler.UnmappedResourceHandler</resource-handler>
</application>
web.xml (fragment; the host name is a placeholder)
<context-param>
  <param-name>org.omnifaces.CDN_RESOURCE_HANDLER_URLS</param-name>
  <param-value>cdn:bootstrap.css=https://cdn.example.com/bootstrap/5.3.3/bootstrap.min.css,
               cdn:bootstrap.js=https://cdn.example.com/bootstrap/5.3.3/bootstrap.min.js</param-value>
</context-param>
<context-param>
  <param-name>org.omnifaces.VERSIONED_RESOURCE_HANDLER_VERSION</param-name>
  <param-value>1.2.0</param-value>
</context-param>
<servlet-mapping>
  <servlet-name>facesServlet</servlet-name>
  <url-pattern>/jakarta.faces.resource/*</url-pattern>
</servlet-mapping>
res.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html">
<h:head><title>Res</title>
  <h:outputStylesheet name="css/style.css"/>
  <h:outputStylesheet library="cdn" name="bootstrap.css"/>
  <h:outputScript name="js/app.js" target="head"/>
  <h:outputScript library="cdn" name="bootstrap.js" target="head"/>
</h:head>
<h:body><p>res</p></h:body>
</html>

The rendered head, on Mojarra (MyFaces rendered the same, with a ;jsessionid=... path parameter on the first request of a client without a cookie):

<link rel="stylesheet" href="/bookshelf/jakarta.faces.resource/css/style.css?v=1.2.0" />
<link rel="stylesheet" href="https://cdn.example.com/bootstrap/5.3.3/bootstrap.min.css" crossorigin="anonymous" />
<script src="/bookshelf/jakarta.faces.resource/js/app.js?v=1.2.0"></script>
<script src="https://cdn.example.com/bootstrap/5.3.3/bootstrap.min.js" crossorigin="anonymous"></script>

The local style sheet has the version, the unmapped URL (no .xhtml) and its relative url("images/bg.png") resolved to /jakarta.faces.resource/css/images/bg.png, which answered 200; the CDN resources have the CDN URL and crossorigin="anonymous" and, as the Javadoc of the renderer says, no integrity because the CDN handler does not know the remote content. Order matters, as the Javadoc says: a handler registered after CombinedResourceHandler wraps it, and UnmappedResourceHandler must come after the others or the combined resources keep their mapped URLs.

Three security notes:

  • The CDN handler does not validate the URL (Javadoc). It is a context parameter, so only the deployer controls it; but a URL taken from user-editable configuration turns your pages into a script injection point. Pin versions in the URL, and prefer self-hosting or a CDN with Subresource Integrity (the resources have no integrity here) for scripts.

  • The crossorigin="anonymous" attribute makes the request cookie-less, which is what you want for a CDN; the Javadoc shows overriding it per resource with a passthrough attribute (a:crossorigin="use-credentials") only when the CDN needs the cookies.

  • The versioned handler is a cache buster, not a security control: it makes a changed resource visible at once. Its version can be an EL expression such as #{environmentInfo.version}, so deployments change it without editing the pages.

Push: o:socket, o:sse and o:notification

The push page compares f:websocket and o:socket in a table and shows the sender API; this section adds the other two components and the results of running them. Three transports share one sender API, @Inject @Push PushContext, selected by the type of the annotation:

Table 15. The push components
Component Transport Sender

o:socket (2.3)

WebSocket, one-way (server to client), channel scopes application, session, view, optional user; connected switches it on and off from the server

@Inject @Push(channel = "reviews") PushContext

o:sse (5.2)

Server-Sent Events over plain HTTP (servlet async); the browser reconnects by itself; best with HTTP/2

@Inject @Push(channel = "alerts", type = Push.Type.SSE) PushContext

o:notification (5.2)

The browser’s Web Notifications API on top of SSE; needs the PWA manifest and the user’s permission

@Push(type = NOTIFICATION), Notification.createNotificationMessage(title, body, url)

FeedBean.java
package org.example.bookshelf.web;

import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.omnifaces.cdi.Push;
import org.omnifaces.cdi.PushContext;

@Named
@RequestScoped
public class FeedBean {
    @Inject
    @Push(channel = "reviews")
    private PushContext reviews;

    @Inject
    @Push(channel = "alerts", type = Push.Type.SSE)
    private PushContext alerts;

    public void sendSocket() {
        reviews.send("{\"book\": \"Dune\", \"stars\": 5}");
    }

    public void sendSse() {
        alerts.send("new-review");
    }
}
feed-socket.xhtml and feed-sse.xhtml (each page has one component)
<o:socket channel="reviews" onmessage="onReview"/>
<!-- or, in the other page -->
<o:sse channel="alerts" onmessage="onAlert"/>

Observed in headless Chrome, on Mojarra and on MyFaces: a click on a button with an Ajax action that called reviews.send(...) made the onmessage function receive the message, and likewise for alerts.send("new-review") on the SSE page. A message sent as a String arrives as a JSON string, "{\"book\": ...", so send an object (a map or a bean, encoded by the library’s JSON encoder) when the page should receive structured data. The rendered page registers each channel as reviews?<uuid>, a channel name plus a random token, and the client’s OmniFaces.Push.init call.

  • Mojarra and a view with both components. The page that held o:socket and o:sse worked on MyFaces, but on Mojarra 4.1.17 the second Ajax click failed on the server: ClassCastException: SseChannelManager$ViewScope cannot be cast to SocketChannelManager$ViewScope in SocketPushContext, on every attempt. Give a view one push component until that is fixed in the version you use.

  • Endpoint and idle timeout. Since 5.2 the WebSocket endpoint is registered automatically when an @Inject @Push exists, and the URL pattern changed from /omnifaces.push/* to /omnifaces.socket/*, so security constraints and proxy rules written for the old pattern no longer match; the SSE endpoint is /omnifaces.sse/<channel>. The idle timeout and the maximum number of sessions per channel have context parameters (org.omnifaces.SOCKET_ENDPOINT_IDLE_TIMEOUT, org.omnifaces.SOCKET_MAX_SESSIONS_PER_CHANNEL, org.omnifaces.SSE_ENDPOINT_IDLE_TIMEOUT, org.omnifaces.SSE_MAX_SESSIONS_PER_CHANNEL); the defaults have no limit, so set the session cap on public channels.

  • Security. Release 5.4.3 restricted the session and view scoped SSE and notification channels to their owning HTTP session (advisory GHSA-w3w6-9m25-g88p, per the release notes), so keep the library current. The rules of the f:websocket page apply unchanged: take the user from the authenticated principal, never from a request parameter; put a security constraint on the endpoint path; write received data with textContent, not innerHTML.

  • SSE prerequisites. Every filter mapped on /* must support async (asyncSupported=true), or SSE fails; without HTTP/2, a browser keeps at most six connections per origin and further tabs can block ordinary requests. SSE cannot be connected and disconnected from the server like connected on o:socket.

  • Frameworks. 5.4.4 made o:socket session and view channels work on Quarkus (which does not expose the HTTP session in the handshake) and made o:sse and o:notification work there at all; 5.4.5 fixed both on Open Liberty. The SocketPushContextProducer, deprecated in 5.2, was removed in 5.5.1.

FacesViews: Extensionless URLs, MultiViews and Dynamic Routes

FacesViews serves Facelets at URLs without an extension, selected by directory scanning, with no FacesServlet mapping required. Faces 4.0 has an extensionless mapping of its own (see the project setup page and the configuration page), which maps /index to /index.xhtml and nothing more. FacesViews adds the redirects, the path parameters of MultiViews and, since 5.5, dynamic route segments.

Zero configuration: views below /WEB-INF/faces-views/ become public as /name. For the other places, org.omnifaces.FACES_VIEWS_SCAN_PATHS; its value /*.xhtml/* scans every .xhtml file below the web root (not WEB-INF, META-INF or resources) and turns MultiViews on. The scratch application:

web.xml (fragment)
<context-param>
  <param-name>org.omnifaces.FACES_VIEWS_SCAN_PATHS</param-name>
  <param-value>/*.xhtml/*, !/jakarta.faces.resource</param-value>
</context-param>
<welcome-file-list>
  <welcome-file>index</welcome-file>
</welcome-file-list>
books/[id]/index.xhtml and RouteBean.java
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:o="omnifaces">
<h:head><title>Book</title></h:head>
<h:body><p>book id=<span id="id">#{routeBean.id}</span> view=<span id="view">#{facesContext.viewRoot.viewId}</span></p>
  <h:form id="form"><h:commandButton id="go" value="go" action="#{routeBean.go}"/></h:form>
  <h:link id="lnk" value="reviews" outcome="reviews"><o:pathParam name="id" value="#{routeBean.id}"/></h:link>
</h:body>
</html>
package org.example.bookshelf.web;

import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.omnifaces.cdi.Param;

@Named
@RequestScoped
public class RouteBean {
    @Inject @Param(pathName = "id")
    private String id;
    @Inject @Param(pathIndex = 0)
    private String term;
    @Inject @Param(pathIndex = 1)
    private String page;

    public String go() { return null; }
    public String getId() { return id; }
    public String getTerm() { return term; }
    public String getPage() { return page; }
}
Table 16. What the requests returned (Mojarra and MyFaces identical)
Request Status Result

/index.xhtml

301

redirect to /index (the default REDIRECT_TO_EXTENSIONLESS); /index itself redirects to / when the welcome file is named index

/utils.xhtml?q=1

301

redirect to /utils?q=1

/search/dune/2

200

search.xhtml with @Param(pathIndex = 0) = dune and pathIndex = 1 = 2 (MultiViews)

/books/123/reviews

200

books/[id]/reviews.xhtml with @Param(pathName = "id") = 123; the same for /books/abc/reviews: the segment is injected unvalidated

/books/123

200

books/[id]/index.xhtml through the extensionless welcome file; the link <h:link outcome="reviews"> with <o:pathParam name="id" value="#{routeBean.id}"/> rendered /bookshelf/books/123/reviews

/admin/panel with a /admin/* security constraint

403

the constraint applies to the extensionless URL

The relevant context parameters are FACES_VIEWS_EXTENSION_ACTION (REDIRECT_TO_EXTENSIONLESS, SEND_404, PROCEED), FACES_VIEWS_PATH_ACTION (default SEND_404 for views requested by their path below a scanned directory), FACES_VIEWS_SCANNED_VIEWS_ALWAYS_EXTENSIONLESS (links always without extension), FACES_VIEWS_ENABLED and FACES_VIEWS_LOWERCASED_REQUEST_URI. A link to a dynamic route needs <o:pathParam name=... value=.../> for every segment, because the rendered URL cannot contain the square brackets. A dynamic-route directory name must be unambiguous: two different segment names under one parent, the same name twice in one path, or an unbalanced bracket fail the deployment. A literal sibling directory always wins over the dynamic one (an organisation whose id is settings is unreachable).

Filters and security. FacesViews reaches a view in two ways: by wrapping the request and continuing the chain (the MultiViews /search/dune/2 case) or by forwarding (dynamic routes and the redirects). A forward bypasses the filters that are mapped for the default REQUEST dispatcher. The scratch run mapped a @WebFilter("/*") that set a header:

  • by default the header was present for /search/dune/2 and absent for /books/1/reviews (a dynamic route), /books/1, and the redirects;

  • with org.omnifaces.FACES_VIEWS_FILTER_AFTER_DECLARED_FILTERS set to true, the header was present on all of them.

This is the documented behaviour (the Javadoc says the FacesViews filter is first in the chain by default), and the consequence is the point: an authentication or authorisation filter, a CSRF filter or a security-headers filter that is mapped as usual can be skipped on part of the application. Either set the parameter, or protect by security-constraint (which the container applies before any of this, 403 above), or declare the filter for FORWARD too. Check every protection you rely on with a request to a dynamic route. Also keep in mind that the constraint pattern must match the URL as FacesViews serves it: /admin/panel.xhtml does not constrain /admin/panel (the pitfall of the security page); use /admin/*.

FileServlet: Serving Files From Outside the WAR

Uploads and media belong outside the web application, and the default servlet cannot serve them. org.omnifaces.servlet.FileServlet is an abstract servlet that does the work properly: ETag, If-None-Match, If-Modified-Since, Range and If-Range (needed by media players to seek and by downloads to resume). You override getFile(request):

MediaFileServlet.java
package org.example.bookshelf.web;

import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import org.omnifaces.servlet.FileServlet;

/** Serves files below one folder, with Range and ETag support, and refuses anything outside it. */
@WebServlet("/media/*")
public class MediaFileServlet extends FileServlet {
    private static final long serialVersionUID = 1L;

    private Path folder;

    @Override
    public void init() {
        // A real application reads this from configuration, not from code.
        folder = Path.of(System.getProperty("bookshelf.media", "media-data")).toAbsolutePath().normalize();
    }

    @Override
    protected File getFile(HttpServletRequest request) {
        String pathInfo = request.getPathInfo();

        if (pathInfo == null || pathInfo.isEmpty() || "/".equals(pathInfo)) {
            throw new IllegalArgumentException(); // answered with HTTP 400
        }

        Path file = folder.resolve(pathInfo.substring(1)).normalize();

        if (!file.startsWith(folder)) {
            throw new IllegalArgumentException(); // outside the folder: HTTP 400
        }

        return file.toFile();
    }
}

The run, on Mojarra: /media/readme.txt answered 200 text/plain; a missing file 404; /media/ 400 (the IllegalArgumentException); /media/..%2f..%2fetc/passwd 400 from Tomcat before the servlet; a request with Range: bytes=0-99 for a 20,000-byte file answered 206 with Accept-Ranges: bytes. The default response also carried Cache-Control: public,max-age=2592000,must-revalidate (30 days, public) and Content-Disposition: inline.

Two changes follow for anything that is not a public cover image. The servlet serves exactly the file you return and does no confinement of its own, and the Javadoc example builds new File(folder, pathInfo); the class above resolves, normalises and checks the prefix, which is the minimum (also resolve symbolic links with toRealPath() if the folder may contain any; not run). And a response that is public for 30 days may be stored by shared caches, so override getExpireTime and the headers for files that only some users may see, and authorise inside getFile (return null for 404, throw IllegalArgumentException for 400). Also override isAttachment and getContentType for user-supplied files, so that an uploaded HTML or SVG file is delivered as a download or with a safe content type and not as active content of your origin (the upload validation section).

The sec: Tags (Jakarta Security)

OmniFaces 5.0 added three tags under xmlns:sec="omnifaces.security" that render their content by the state of the Jakarta Security SecurityContext: <sec:authorize role="ADMIN"> (or anyRole="ADMIN, EDITOR", or allRoles="ADMIN, AUDITOR"; exactly one of the three), <sec:isAuthenticated> and <sec:isAnonymous>. var="isAdmin" on authorize exposes the result as a variable for several uses. If no SecurityContext is available a warning is logged and nothing is rendered (it fails closed).

<sec:authorize anyRole="LIBRARIAN, ADMIN">
  <h:link value="Manage catalogue" outcome="/admin/catalogue"/>
</sec:authorize>
<sec:isAnonymous>
  <h:link value="Sign in" outcome="/login"/>
</sec:isAnonymous>

These were read in the documentation and not run. They are tag handlers, evaluated when the view is built like JSTL tags (the JSTL page), so the decision is made once per view build and not on each render. They hide user interface elements; they authorise nothing. Protect the targets with a security-constraint, @RolesAllowed or the checks of the service layer (the roles section), and treat the tags as a convenience that keeps the menu honest.

OmniFaces Next to PrimeFaces

The two libraries do not compete: PrimeFaces supplies widgets, OmniFaces supplies the plumbing, and the OmniFaces showcase is a PrimeFaces application. The scratch page pf.xhtml used PrimeFaces 16.0.0 and OmniFaces 5.5.5 together on Mojarra and on MyFaces:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:p="primefaces"
      xmlns:o="omnifaces">
<h:head><title>PF</title></h:head>
<h:body>
  <h:form id="form">
    <p:messages id="msgs"/>
    <p:selectOneMenu id="sel" value="#{pickBean.pfSelected}" converter="omnifaces.SelectItemsConverter">
      <f:selectItems value="#{pickBean.available}" var="b" itemValue="#{b}" itemLabel="#{b.title}"/>
    </p:selectOneMenu>
    <p:pickList id="pl" value="#{pickBean.picks}" var="b" itemValue="#{b}" itemLabel="#{b.title}">
      <o:converter converterId="omnifaces.ListConverter" list="#{pickBean.picks.source}"/>
    </p:pickList>
    <p:inputText id="pw" value="#{borrowDraftBean.password}"/>
    <p:inputText id="pw2" value="#{borrowDraftBean.confirm}"/>
    <o:validateEqual id="eq" components="pw pw2" message="Differ"/>
    <p:commandButton id="go" value="go" action="#{pickBean.submit}" update="@form"/>
  </h:form>
  <o:highlight/>
  <p>sel=<span id="rSel">#{pickBean.pfSelected}</span> target=<span id="rT">#{pickBean.picks.target}</span></p>
</h:body>
</html>

An Ajax post of the form (PrimeFaces p:commandButton, jakarta.faces.partial.render=@all) converted the p:selectOneMenu value with omnifaces.SelectItemsConverter to the Cosmos record, converted the p:pickList target with o:converter and omnifaces.ListConverter to a one-element list, and failed with Differ (p:messages) when the two p:inputText values were different and passed when equal (o:validateEqual). The head contained the PrimeFaces scripts and omnifaces.js side by side. Where both offer something similar, the division is:

Table 17. Choosing between PrimeFaces and OmniFaces
Need PrimeFaces 16 OmniFaces 5

Post only the processed parts of a form in Ajax

partialSubmit on the Ajax tag

o:form (default partialSubmit=true), also for f:ajax

Server push

Removed in 7.0 (Atmosphere); p:poll for polling

o:socket, o:sse; or standard f:websocket

A friendlier Ajax error

p:ajaxExceptionHandler and the PrimeFaces exception handler (exception handling page)

FullAjaxExceptionHandler (Exception Handlers)

Converter for entities in a select component

your own converter

SelectItemsConverter, ListConverter

Cross-field validation

the standard approaches of the validators page

o:validateBean and the multi-field validators (server side)

The cost of mixing is in the details. Both libraries replace parts of the framework (render kit, partial view context), so a third-party extension that does the same needs checking. PrimeFaces and OmniFaces both ship scripts and styles for the head, which CombinedResourceHandler can combine (the measured figures are on the performance page). And a strict Content-Security-Policy applies to both: the PrimeFaces CSP behaviour is on the security page; the inline scripts of the OmniFaces components are in The o: Tags, Component by Component.

Mojarra and MyFaces Compared

OmniFaces supports both implementations (since 5.4 it also declares itself compatible with Mojarra 5.x and MyFaces 5.x, the Faces 5.0 line); the differences observed in this section’s runs (Mojarra 4.1.17, MyFaces 4.1.4, OmniFaces 5.5.5) are small but real:

Table 18. Differences observed with OmniFaces on Mojarra and MyFaces
Feature Mojarra MyFaces

OmniFaces @ViewScoped unload

also destroys the standard @ViewScoped beans of the view (documented side effect)

destroys only the OmniFaces ones

o:socket and o:sse in one view

ClassCastException in SocketPushContext

works

o:implicitNumberConverter type="currency"

works for ordinary input; $1 in the input is HTTP 500

ordinary input fails ('¤12.50' could not be understood); $12.50 is HTTP 500

Standard f:viewParam posted back without its query string

accepted

Validation Error: Value is required. (o:viewParam is accepted on both)

o:cache around a form

serves ~com.sun.faces.saveStateFieldMarker~ in place of the state field

serves the first request’s jakarta.faces.ViewState value again and again

Generated ids in default labels

j_idt1

j_id__md_1

Everything else in this page behaved the same on both: Faces and Messages, @Param, @Cookie, @ContextParam, the converters other than the one above, the multi-field validators, o:form, o:link, o:url, o:highlight, o:outputLabel, o:skipValidators, o:ignoreValidationFailed, o:enableRestorableView, o:graphicImage, the EL functions, Ajax, @RateLimit, o:socket and o:sse separately, FacesViews and the resource handlers.

What Changed per Version

The OmniFaces release notes (the "What’s new" page of the showcase) and the @since tags of the Javadoc give the history. The table lists what matters for a Faces 4.1 application; older entries show where the features of the earlier sections came from, which helps when you meet them in old code.

Table 19. OmniFaces versions and what they added
Version (date) Change

5.5.5 (2026-10-06)

Fixes only, the same as 4.7.15

5.5.4 (2026-09-20)

CombinedResourceHandler retains only the most recently used combined resource ids; LruCache operations are atomic; each operation of the o:cache provider interface must be atomic

5.5.1 to 5.5.3

SocketPushContextProducer removed (deprecated in 5.2); PWAResourceHandler, o:socket and o:sse no longer create an HTTP session when scope="application" (usable on stateless views); CombinedResourceHandler sets crossorigin only for other origins; FacesViews fixes for dynamic routes

5.5 (2026-08-21)

FacesViews dynamic route segments (/organizations/[id]/members.xhtml, @Param(pathName), o:pathParam name); the leave confirmation of @ViewScoped can be registered with addEventListener("beforeunload")

5.4 (2026-07-14)

Compatible with Mojarra 5.x and MyFaces 5.x (still requires Faces 4.1); OmniFaces.Ajax.validationFailed; org.omnifaces.CDN_RESOURCE_HANDLER_EXCLUDED_RESOURCES; FullAjaxExceptionHandler supports query parameters in the error page location; o:selectItemGroups deprecated for removal; @Param without @Inject deprecated. 5.4.3 restricted session and view scoped SSE and notification channels to their session (GHSA-w3w6-9m25-g88p); 5.4.4 and 5.4.5 fixed Quarkus and Open Liberty

5.3 (2026-04-23)

o:lazyPanel

5.2 (2026-03-25)

o:sse, o:notification, o:scriptErrorHandler; o:socket endpoint registered automatically and its URL pattern changed to /omnifaces.socket/*; PushContextProducer replaces SocketPushContextProducer; 5.2.1 and 5.2.2 fixed the view scope unload (distributed sessions, NotSerializableException)

5.1 (2026-03-09)

o:flagEmoji(), o:compositeConverter

5.0 (2025-12-28)

Baseline of Jakarta EE 11 (Java 17, Faces 4.1, EL 6.0, Servlet 6.1, CDI 4.1, WebSocket 2.2, Bean Validation 3.1, Security 4.0); everything deprecated in 4.x removed; one namespace xmlns:o="omnifaces" in place of http://omnifaces.org/ui and http://omnifaces.org/functions; JsfLabelMessageInterpolator renamed FacesLabelMessageInterpolator; CorsAwareResourceRenderer; @RateLimit; sec: tags; o:formatXyzForLocale functions; Faces.isOutputHtml5Doctype(), resetResponse(), createResource(UIComponent), getResponseHeaders()

4.x (4.0.1 in 2022-11, 4.7.15 in 2026-10)

The Jakarta namespace (jakarta.*, Faces 3.0 and 4.0). 4.3 loader on o:importConstants and o:importFunctions; 4.5 tags for every converter and validator, FullAjaxExceptionHandler registers the FacesExceptionFilter itself, globalMessage on @Param; 4.6 members() on imported enums; 4.7 Faces.getRemoteAddr() honours Forwarded and X-Real-IP

3.x

javax.*, JSF 2.3. 3.0 o:form posts to the request URI and posts partially by default, ImplicitNumberConverter; 3.2 FacesExceptionFilter logs with a UUID; 3.8 Validators utility; 3.9 VersionedResourceHandler; 3.12 messageFormat on o:validateBean; 3.14 MutableRequestFilter

2.x and 1.x

1.3 o:enableRestorableView; 1.4 o:importFunctions, UnmappedResourceHandler; 1.5 o:messages, ListConverter; 1.6 @Param, CDI view scope; 1.8 @Eager; 2.0 o:viewParamValidationFailed; 2.1 @Cookie; 2.2 view scope unload, o:viewAction, @ContextParam, FileServlet; 2.3 o:socket, @Push; 2.5 @GraphicImageBean, path parameters; 2.6 #{faces}, saveInViewState

How this interacts with Faces versions: OmniFaces 5 needs Faces 4.1, so it cannot be used on a Faces 4.0 server (Jakarta EE 10) whose container does not provide Faces 4.1; the 4.7.x line is the right one there, and its tags and classes are the same as far as the table shows, except for the namespace and the 5.0 additions (RateLimit, sec:, o:sse, o:notification, o:lazyPanel, o:compositeConverter, FacesLabelMessageInterpolator instead of the old name). Faces 5.0 is still a milestone; the 5.4 release notes only promise compatibility with its implementations. The upcoming Faces 5.0 is planned to drop the old XML namespaces (see the versions page), and OmniFaces 6.x will drop its URL namespaces as well.

Where the Books Are Outdated

None of the eight books that this section consulted covers OmniFaces 4 or 5: the newest of them, the second edition of the PrimeFaces cookbook, dates from May 2015, and the others from 2004 to 2014. They solve by hand, or by other libraries, problems that OmniFaces now solves, and some of what they teach is no longer possible or no longer safe. Taking the topics that the books treat and that this page covers:

  • Helper code around FacesContext. The books that work with FacesContext and ExternalContext directly (Mann on the context objects, the JSF 2.0 reference of Burns and Schalk, Leonard’s recipes) leave the reader to write the shortcuts for parameters, messages, redirects and file downloads in every project. Faces, Messages, Servlets and Ajax are those shortcuts, maintained, with Ajax-aware redirects (The Utility Classes: Faces, FacesLocal and Friends).

  • Selecting objects and converters. The converter chapters (Leonard on standard and custom converters, Burns and Schalk) write one converter per entity, which usually queries the database on every postback. SelectItemsConverter needs none (Converters).

  • Request parameters. Books written for JSF 2.x inject a parameter with @ManagedProperty from javax.faces.bean, which was removed in Faces 4.0. @Param (@Param, @Cookie and @ContextParam: Injecting Request Data) is the replacement, with conversion, validation and messages the old annotation never had.

  • Custom handlers. The extension chapters (Burns and Schalk on PhaseListener, ExceptionHandler, ViewHandler and factories; Leonard on custom resource handlers and the JSF 2.0 ExceptionHandler; Jonna on Ajax error handling and resource optimisation) end in code you write yourself. The finished versions are FullAjaxExceptionHandler, CombinedResourceHandler, VersionedResourceHandler and CDNResourceHandler (Exception Handlers, Resource Handlers and Renderers).

  • Pretty and bookmarkable URLs. Leonard’s view parameter and pretty URL recipes, and the PrettyFaces library, belong to JSF 2.0; FacesViews and the extensionless mapping of Faces 4.0 serve the need today (FacesViews: Extensionless URLs, MultiViews and Dynamic Routes, View Parameters Done Right: o:viewParam, o:viewAction and Friends).

  • Push. PrimeFaces Push on Atmosphere (Çalışkan and Varaksin) was removed in PrimeFaces 7.0; the answers are o:socket and f:websocket (Push: o:socket, o:sse and o:notification).

  • Cross-field validation. The validation chapters (Leonard on cross-field validation, Burns and Schalk, Dudney and others) write a custom validator for "password equals confirmation"; o:validateEqual, the rest of the family and class-level o:validateBean replace them (Validators: o:validateBean and the Multi-Field Family).

  • Libraries that ended. RichFaces, Tomahawk, Trinidad, Seam 2 and PrettyFaces appear in Leonard’s recipes and in Burns and Schalk; they ended, and PrimeFaces with OmniFaces and the standard features is the maintained route.

  • Everything before Faces 2.3 and Jakarta. javax.faces.* imports, http://java.sun.com/jsf/* namespaces and JSP views in all of them; OmniFaces 4 and 5 use jakarta.* and, from 5.0, one URN namespace (Installing OmniFaces).

Common Mistakes

Security first, then correctness.

References