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.
Package (under org.omnifaces) |
What it holds | Covered in |
|---|---|---|
|
Static utility classes: |
The Utility Classes: Faces, FacesLocal and Friends, The Messages Utility and o:messages, The Ajax Utility |
|
CDI annotations and scopes: |
@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 |
|
The |
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 |
|
Converters and validators with converter and validator ids |
Converters, Validators: o:validateBean and the Multi-Field Family |
|
The EL functions, registered under the |
|
|
|
|
|
|
|
|
FacesViews (extensionless URLs, MultiViews, dynamic routes); |
FacesViews: Extensionless URLs, MultiViews and Dynamic Routes, FileServlet: Serving Files From Outside the WAR |
|
The |
Source: OmniFaces home page and OmniFaces showcase
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:
<dependency>
<groupId>org.omnifaces</groupId>
<artifactId>omnifaces</artifactId>
<version>5.5.5</version>
</dependency>
Source: OmniFaces: installation
Pick the artifact line by the Faces version of the target server, because the minimum versions change with the major version:
| 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 ( |
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/libof the WAR. Placed in an EARlibdirectory 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-starterpulls in the Faces and Weld starters and mapsjoinfaces.omnifaces.*properties to theorg.omnifaces.*context parameters. On Quarkus the Quarkiverse extensionquarkus-omnifacesexists. 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:
<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">
Source: OmniFaces: XML namespaces
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.
| Kind | Registered class |
|---|---|
View handler |
|
Resource handlers |
|
Factories |
|
Phase listeners |
|
System event listeners |
|
EL resolver |
|
CDI extensions |
|
Servlet container initializer |
|
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.
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:
| Class | What it is for |
|---|---|
|
Request, session, cookie, locale, view, project stage, resource bundle, redirect, |
|
Create and add |
|
Find and traverse components, the current form or command, labels, |
|
Update ids, call a script on complete and send data to JavaScript from a bean (The Ajax Utility) |
|
|
|
Subscribe a lambda to a system event or to the before or after of a phase, for the current request or for the view |
|
|
|
Request URL parts, |
|
Bean Validation helpers: |
|
Look up the |
Source: OmniFaces 5.5.5 API: Faces and OmniFaces showcase: Faces
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:
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);
}
}
<!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>
Source: OmniFaces 5.5.5 API: Faces, Messages
What the runs showed, on Mojarra and on MyFaces alike:
-
utils.xhtml?q=dunerenderedq=dune;pagewas empty becauseFaces.getRequestParameter("page", Integer.class)returnsnullfor an absent parameter, and?page=3gave3. The base URL washttp://localhost:8570/bookshelf/, the view id/utils.xhtml, the stageProductionand the mapping.xhtml. -
Faces.getSupportedLocales()returned[en, es]only after the application declared<locale-config>with a default localeenand a supported localees; 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 toutils.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)answeredContent-Disposition: attachment;filename="hello.txt"; filename*=UTF-8''hello.txtwith 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 asa%22b.txt,..%5C..%5Cx.txtanda%0D%0AX-Evil%3A%201.txtin 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:
<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:
// 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);
Source: OmniFaces 5.5.5 API: Components and Events
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).
// 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:
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; }
}
<!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>
Source: OmniFaces 5.5.5 API: Ajax and OmniFaces showcase: Ajax
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.
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; }
}
<!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>
Source: OmniFaces 5.5.5 API: Param and OmniFaces showcase: Param
The elements of @Param (all @Nonbinding, so they do not change which bean CDI injects):
| Element | Meaning |
|---|---|
|
The request parameter name; defaults to the name of the field (or of the constructor parameter, if the class is compiled with |
|
The zero-based index of a path parameter (needs the |
|
The label in messages; observed: |
|
A converter id, an EL expression for an id or an instance, or a class; attributes as |
|
The same for validators; |
|
Attach the message to no component (since 4.5); the default attaches it to the view root, shown by |
|
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=1990injected2,[a, b],1234567890,20and1990, and@PostConstructloaded the book (Cosmos). -
?id=abcleftidnull and addedBook id: 'abc' must be a number consisting of one or more digits.. A failed field isnulland the bean still exists (the Javadoc adds thatFacesContext.isValidationFailed()istrueafterwards): code in@PostConstructmust cope withnull. -
?isbn=123&n=0producedISBN: the ISBN must have 10 to 13 characters(thevalidatorClasseselement, with{1}in the message replaced by the label) andmust be greater than or equal to 1(the Bean Validation constraint@Min(1)on the field; Bean Validation messages carry no label);?n=500gavemust be less than or equal to 100. Bean Validation annotations on the injected field are honoured, and@Paramis a way to get server-side validation of query parameters without writing any Java validator. -
@Cookie(name = "theme")injecteddarkfor a request that carriedCookie: theme=dark, and@ContextParam(name = "jakarta.faces.PROJECT_STAGE")injectedProduction.@Cookiehas 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 theFacesServletruns, there is noFacesContext, and the injection fails withNullPointerException: Cannot invoke "FacesContext.getApplication()" because "context" is nullinParamProducer; the Javadoc of@Eagerstates that theFacesContextis not available in that case, and OmniFaces logsCould 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.
| Declaration | The bean is created |
|---|---|
|
At application start |
|
Whenever a session is created |
|
When that context-relative URI is requested, before the |
|
After the Restore View phase of that view, with a |
|
When that view is created; like the request scoped bean, a view scoped bean needs |
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");
}
}
Source: OmniFaces 5.5.5 API: Eager, Startup and OmniFaces showcase: Eager
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:
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 destroyedandOmniViewBean 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 destroyedappeared; 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.
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:.
<!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>
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:
| Request | Standard f:viewParam, f:viewAction |
o:viewParam, o:viewAction, o:viewParamValidationFailed |
|---|---|---|
no |
HTTP 200, the page with |
HTTP 400 with the text |
|
HTTP 200 and |
HTTP 400 |
|
HTTP 200, but the action did not load the book: |
HTTP 200, |
postback of the page |
the bean is updated again from the query string; |
|
postback to the bare URL (no query) |
Mojarra: works ( |
works on both ( |
What the table shows is what the o: tags are for:
-
o:viewParamhas 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:viewParamkeeps 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 toload()foundidnull and failed with aNullPointerException). -
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
PreValidateEventandPostValidateEventlisteners thato:viewParamValidationFailedrelies on, ando:viewParamwas the fix. The Javadoc says that Faces 2.3 repaired the standard tag, and that the OmniFaces fix now matters only whenjakarta.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULListrue. On 4.1 the standard page in the run reported its required error normally; the stateless mode is the remaining reason. -
o:viewParamalso gives a default label (itsname), accepts adefaultvalue for an absent parameter, and does not turn anullmodel value into an empty parameter in generated links (includeViewParams). -
o:viewActionevaluatesiflate. The standard action evaluatesifin Apply Request Values, before the view parameter has reached the model, soif="#{viewParamBean.id ne null}"sawnullon the GET and the book was never loaded (loads=0above).o:viewActionevaluates it in Invoke Application, after conversion, validation and the model update. Withimmediate="true"it behaves like the standard one. Itsmessageattribute adds a flash warning if the action redirects. -
o:viewParamValidationFaileddecides what a failure means. Inf:metadatait applies when any view parameter failed; nested in one view parameter it applies to that one only and takes precedence. WithsendError="400"it sends an HTTP status (and a page from<error-page>if you declared one); withsendRedirect="login.xhtml"it redirects with the message as a global flash error.messageoverrides 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, usesendErrorand anerror-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:
<h:commandButton id="nobv" value="save without checks" action="#{borrowDraftBean.submit}">
<o:validateBean disabled="true"/>
</h:commandButton>
Source: OmniFaces showcase: validateBean
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:
<!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 aNullPointerExceptionin the validator; -
with the guard below, both answered the form again with
must not be null, from the@NotNullof the group.
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:
| Tag | Passes when | Default message |
|---|---|---|
|
all values are equal |
|
|
the values are in the order |
|
|
no value occurs twice |
|
|
all are filled |
|
|
all or none are filled |
|
|
exactly one, at least one, at most one is filled |
|
|
your |
none; set |
|
(a tag handler inside a column) the input’s value is unique in all rows of the |
|
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 animmediate="true"command callsrenderResponse(). -
A disabled, read-only or non-rendered input contributes its model value, so the validator can compare against existing data.
-
disabledtakes an EL expression that is evaluated per request. The Javadoc example isdisabled="#{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.
| Converter | What it does | Needs |
|---|---|---|
|
Converts a selected entity back to the object from the |
a stable, unique |
|
The same, by the position (index) in the list |
a list that is identical on the postback |
|
The same for components that take a |
|
|
Converts enum values for a |
only if the component has no |
|
A delimited text into a collection ( |
the property type |
|
Trim whitespace; lower or upper case in the current locale (and trim). No conversion in |
none |
|
Chain several converters ( |
ids of registered converters |
|
A number without the percent or currency symbol, accepting input with or without it |
|
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:
<!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’stoString()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.SelectItemsConverterconverted the submittedtoString()back to the same instance from the list, sopickBean.selectedwas the record forCosmos. No database was touched. -
The menu with
o:selectItemsIndexConverterrendered the values0,1,2,3and converted2to the third book. -
A
List<Genre>bound toh:selectManyCheckboxheld[SCIENCE, HISTORY]with no converter at all; theGenericEnumConverteris obsolete for that case in Faces 4.1, as its Javadoc says. -
o:toCollectionConverterturned+ a , b,c ,a+into[a, b, c, a](the default is anArrayList; aSetproperty gets aLinkedHashSet);o:trimConverterturned+ Ada ` into `+Ada;o:toLowerCaseConverterturned+ ADA@Example.ORG ` into `+ada@example.org;o:toUpperCaseConverterturned+ zx-1 ` into `+ZX-1;o:implicitNumberConverter type="percent"turned25and25%into0.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. AtoString()that contains sensitive data (a price paid, a note, a personal field) leaks it into the page. Either write atoString()that holds only the id (Book[id=2], the pattern the Javadoc recommends) or subclass the converter and overridegetAsString(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()andhashCode()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.
| Purpose | Tags |
|---|---|
Forms and links |
|
Inputs |
|
Output and feedback |
|
Scripts and styles |
|
Component tree utilities |
|
Push |
|
Validators |
|
Converters |
|
Tag handlers |
|
Source: OmniFaces 5.5.5 VDL documentation and OmniFaces showcase
o:form, o:link and o:url: keeping the request’s query string
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.
<!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>
Source: OmniFaces VDL: o:form and OmniFaces showcase: form
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:skipValidatorsran its action (log=skip) and showed no message; -
the button with
o:ignoreValidationFailedran its action (log=ignore(null/Ada):titlestillnull,authorupdated) 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=aliceand the application cache storeduser=alice. Bob, a different client with a different session, requested?u=boband was serveduser=aliceby the application cache anduser=bobby 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 thejakarta.faces.ViewStatefield 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.
Source: OmniFaces 5.5.5 API: Cache and OmniFaces showcase: 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:conditionalCommentrenders the markup inside a conditional comment:<!--[if lte IE 9]><link rel="stylesheet" href="old.css" /><![endif]-->, even withjakarta.faces.FACELETS_SKIP_COMMENTSset totrue, 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:criticalStylesheetrendered<link rel="preload" href="..." as="style" onload="this.onload=null;this.rel='stylesheet'" />followed by anoscriptfallback with the plainlink, and moved it to the top of the head, ahead of the OmniFaces and Faces scripts. -
o:deferredScriptrendered<script>OmniFaces.DeferredScript.add('/bookshelf/jakarta.faces.resource/js/late.js.xhtml','','',null,function() {window.late=1});</script>at the end of the body; theonsuccesscode ran after the window load event (window.latewas1). -
o:lazyPanelwithloaded="true"rendered its children; without it, an emptydivandOmniFaces.LazyPanel.init('lp2', {}), and the content arrived by an Ajax request when the panel scrolled into view (it did, in the headless Chrome run). Thelistenerreceives aLazyPanelEventonce. -
o:massAttributeputclass="wide"on the input without astyleClassand leftclass="own"on the one that had one. -
o:resolveComponentmade the componentaavailable by name (#{aComp.clientId}printeda);o:moveComponentmoves components, facets and behaviors to another place in the tree (BEFORE,AFTER,ADD_FIRST,ADD_LAST,FACET,BEHAVIOR);o:outputFormat var="_greeting"capturedHello readerinto a variable instead of printing it. -
o:tree(witho:treeNode,o:treeNodeItemando:treeInsertChildren) renders anyTreeModelwith markup that you write, recursively; it draws nothing itself. Not run. -
o:inputHiddenconverts, validates and updates the model immediately in Apply Request Values, regardless of errors on other inputs.o:inputFileaddsmaxsize,directoryand server-side checks ofaccepttoh:inputFile(o:inputFile). -
o:scriptErrorHandlerreports uncaught JavaScript errors and rejected promises bynavigator.sendBeacon()to the server, where they arrive as a CDIScriptErrorevent for an observer to log;o:resourceIncluderenders 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:
<!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('<b>bold</b> 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>
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() {
}
}
Source: OmniFaces showcase: importConstants, importFunctions and OmniFaces 5.5.5 API: ImportConstants
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:converterando:validatorextendf:converterandf:validatorwith 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 whato:listConverterando:validateBeanbuild on. -
o:massAttribute,o:tagAttribute,o:methodParamhelp tag files and templates: set an attribute on every nested component (withtargetto select a class), declare a tag attribute explicitly so a parent’s value is cleared, pass a method expression through a tag attribute. -
o:loadBundleextendsf:loadBundlewith aloaderand 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:validateUniqueColumnare 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:
| Class | Functions |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Source: OmniFaces VDL documentation: library overview, with the functions and OmniFaces showcase: functions
Results from the scratch page consts.xhtml above, identical on both implementations:
| Expression | Result |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
the flag of Spain |
|
|
|
|
|
|
|
|
|
|
|
|
Three of the functions are for security-sensitive places, and one is a trap:
-
o:escapeJSescapes a value for a JavaScript string. In the run, a request parameter'; alert(1);//</script><b>came out ofvar plain = '#{param.t}';unchanged, which breaks out of the string and out of the script element, and out ofvar 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:escapeJScloses the gap for string literals. Better still is no data in script at all: use adata-attribute. -
o:encodeURLpercent-encodes a value for a query string:q=#{o:encodeURL(param.t)}gave%27%3B+alert%281%29.... -
o:stripTagsremoves HTML tags from text; it is a way to produce a plain-text summary, not a sanitiser to make markup safe. -
o:printStackTraceprints aThrowable, 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).
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();
}
}
<!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
SecurityContextor 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
lastModifiedor 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:
| Handler | What it does |
|---|---|
|
Renders the error page that |
|
Redirects an expired view to the same view with a flash message, |
|
Adds every exception as a global |
|
Suppresses the exception types named in |
<factory>
<exception-handler-factory>org.omnifaces.exceptionhandler.FullAjaxExceptionHandlerFactory</exception-handler-factory>
</factory>
Source: OmniFaces showcase: FullAjaxExceptionHandler and OmniFaces 5.5.5 API: FullAjaxExceptionHandler
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.
| Filter | Purpose |
|---|---|
|
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 |
|
Sets |
|
Sets the request body encoding to UTF-8 (or the |
|
Unwraps |
|
Wraps the request so that headers and parameters can be changed ( |
|
The abstract base class for your own filter, with |
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).
| Handler | What it does | Registered |
|---|---|---|
|
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) |
|
Maps resource ids to CDN URLs through |
no |
|
Appends |
no |
|
Serves resources at |
no |
|
The resource side of |
yes |
|
Generates |
yes (inactive without a manifest class) |
|
FacesViews (FacesViews: Extensionless URLs, MultiViews and Dynamic Routes) |
yes |
|
Serve a Facelets view at a non- |
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:
<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>
<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>
<!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>
Source: OmniFaces 5.5.5 API: CDNResourceHandler, UnmappedResourceHandler and VersionedResourceHandler
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
integrityhere) 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:
| Component | Transport | Sender |
|---|---|---|
|
WebSocket, one-way (server to client), channel scopes |
|
|
Server-Sent Events over plain HTTP (servlet async); the browser reconnects by itself; best with HTTP/2 |
|
|
The browser’s Web Notifications API on top of SSE; needs the PWA manifest and the user’s permission |
|
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");
}
}
<o:socket channel="reviews" onmessage="onReview"/>
<!-- or, in the other page -->
<o:sse channel="alerts" onmessage="onAlert"/>
Source: OmniFaces showcase: o:socket, o:sse and o:notification
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:socketando:sseworked on MyFaces, but on Mojarra 4.1.17 the second Ajax click failed on the server:ClassCastException: SseChannelManager$ViewScope cannot be cast to SocketChannelManager$ViewScopeinSocketPushContext, 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 @Pushexists, 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:websocketpage apply unchanged: take theuserfrom the authenticated principal, never from a request parameter; put a security constraint on the endpoint path; write received data withtextContent, notinnerHTML. -
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 likeconnectedono:socket. -
Frameworks. 5.4.4 made
o:socketsession and view channels work on Quarkus (which does not expose the HTTP session in the handshake) and madeo:sseando:notificationwork there at all; 5.4.5 fixed both on Open Liberty. TheSocketPushContextProducer, 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:
<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>
<!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; }
}
| Request | Status | Result |
|---|---|---|
|
301 |
redirect to |
|
301 |
redirect to |
|
200 |
|
|
200 |
|
|
200 |
|
|
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/2and absent for/books/1/reviews(a dynamic route),/books/1, and the redirects; -
with
org.omnifaces.FACES_VIEWS_FILTER_AFTER_DECLARED_FILTERSset totrue, 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):
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:
| Need | PrimeFaces 16 | OmniFaces 5 |
|---|---|---|
Post only the processed parts of a form in Ajax |
|
|
Server push |
Removed in 7.0 (Atmosphere); |
|
A friendlier Ajax error |
|
|
Converter for entities in a select component |
your own converter |
|
Cross-field validation |
the standard approaches of the validators page |
|
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:
| Feature | Mojarra | MyFaces |
|---|---|---|
OmniFaces |
also destroys the standard |
destroys only the OmniFaces ones |
|
|
works |
|
works for ordinary input; |
ordinary input fails ( |
Standard |
accepted |
|
|
serves |
serves the first request’s |
Generated ids in default labels |
|
|
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.
| Version (date) | Change |
|---|---|
5.5.5 (2026-10-06) |
Fixes only, the same as 4.7.15 |
5.5.4 (2026-09-20) |
|
5.5.1 to 5.5.3 |
|
5.5 (2026-08-21) |
FacesViews dynamic route segments ( |
5.4 (2026-07-14) |
Compatible with Mojarra 5.x and MyFaces 5.x (still requires Faces 4.1); |
5.3 (2026-04-23) |
|
5.2 (2026-03-25) |
|
5.1 (2026-03-09) |
|
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 |
4.x (4.0.1 in 2022-11, 4.7.15 in 2026-10) |
The Jakarta namespace ( |
3.x |
|
2.x and 1.x |
1.3 |
Source: OmniFaces showcase: What’s new and OmniFaces home page
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 withFacesContextandExternalContextdirectly (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,ServletsandAjaxare 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.
SelectItemsConverterneeds none (Converters). -
Request parameters. Books written for JSF 2.x inject a parameter with
@ManagedPropertyfromjavax.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,ViewHandlerand factories; Leonard on custom resource handlers and the JSF 2.0ExceptionHandler; Jonna on Ajax error handling and resource optimisation) end in code you write yourself. The finished versions areFullAjaxExceptionHandler,CombinedResourceHandler,VersionedResourceHandlerandCDNResourceHandler(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:socketandf: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-levelo:validateBeanreplace 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 usejakarta.*and, from 5.0, one URN namespace (Installing OmniFaces).
Common Mistakes
Security first, then correctness.
-
Trusting
Faces.getRemoteAddr()or the default of@RateLimit. Both take the client address from theForwarded,X-Forwarded-FororX-Real-IPheader when present; a client that sends a different one each time defeats the limit (OmniFaces @ViewScoped and @RateLimit). Behind a proxy that overwrites the header, fine; otherwise giveclientId. -
Public image methods in a
@GraphicImageBean. Every public method that returnsbyte[]orInputStreamis a public GET URL with a guessable parameter (o:graphicImage and @GraphicImageBean). -
o:cachewithscope="application"for anything user-specific, or around a form (The o: Tags, Component by Component). -
o:enableRestorableViewon a page whose action changes data with only the view state as protection (The o: Tags, Component by Component). -
Filters that FacesViews skips. Security filters mapped only for
REQUESTdo not run for dynamic routes and redirects unlessFACES_VIEWS_FILTER_AFTER_DECLARED_FILTERSis set (FacesViews: Extensionless URLs, MultiViews and Dynamic Routes). -
FileServletwithout confinement, and with the default public 30-day cache (FileServlet: Serving Files From Outside the WAR);Faces.sendFilewith a user-chosen name (The Utility Classes: Faces, FacesLocal and Friends). -
Conditions the client controls.
disabled="#{param.x}"on a validator, a CDN URL in user-editable configuration, a path or channel name from a request (Validators: o:validateBean and the Multi-Field Family, Resource Handlers and Renderers, Push: o:socket, o:sse and o:notification). -
escape="false"ono:messagesando:printStackTraceon a page a user can see (The Messages Utility and o:messages, The o: EL Functions (formerly of:)). -
A
toString()that holds more than an id underSelectItemsConverter: all of it goes into the page source (Converters). -
Parameters that arrive unchecked.
@Parampath segments,@Cookievalues,o:scriptParamando:hashParamare client data; validate them (@Param, @Cookie and @ContextParam: Injecting Request Data). -
A
@Parammessage that never shows because the bean was created after the messages component rendered; use@Eager(viewId = ...), notrequestURI, which fails without aFacesContext(@Param, @Cookie and @ContextParam: Injecting Request Data). -
Mixing up the two
@ViewScopedannotations and the two@Startupannotations:jakarta.faces.view.ViewScopedagainstorg.omnifaces.cdi.ViewScoped,jakarta.ejb.Startupagainstorg.omnifaces.cdi.Startup(@Eager and @Startup). -
The jar in the wrong place or twice (Installing OmniFaces); old namespaces that disappear in 6.x (Installing OmniFaces);
@Paramwithout@Injectthat is deprecated since 5.4 (@Param, @Cookie and @ContextParam: Injecting Request Data). -
o:socketando:ssein one view on Mojarra (Push: o:socket, o:sse and o:notification);o:implicitNumberConverterfor public currency input (Converters). -
Placing a multi-field validator between its inputs (Validators: o:validateBean and the Multi-Field Family).
-
Treating
sec:authorizeas access control. It hides links; the constraint must exist on the server (The sec: Tags (Jakarta Security)).