Search Expressions

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 Jakarta Faces 4.1 (API jakarta.faces-api 4.1.2, part of Jakarta EE 11) with Eclipse Mojarra 4.1.17 and Apache MyFaces Core 4.1.4, Jakarta Servlet 6.1 and Java 17 or later (the examples were compiled with --release 21), and, for the section on PrimeFaces, PrimeFaces 16.0.0. Search expressions were added in Faces 2.3 (the package is jakarta.faces.component.search since the move from javax to jakarta), and the specification’s change log mentions the facility only for 2.3 (and, indirectly, an Ajax change for composites in 4.0), so everything below also applies to Faces 4.0, but only 4.1 was run. Every view and class in this page was deployed as an exploded Bookshelf WAR on Tomcat 11.0.26 with Weld 6.0.4.Final, once with Mojarra and once with MyFaces (a third pair of runs added the PrimeFaces 16.0.0 jar). The Ajax behaviour was observed by loading each page in jsdom 30.1.2 with the faces.js that each implementation ships, clicking the button and reading the request that script sent and the partial response it received; the results of the SearchExpressionHandler calls were read from a Faces view that calls the handler. All of it was observed on 2026-10-08. Whatever was not run is marked as such.

When a page says render="results" or execute="@form" it uses a search expression: a string that names a component relative to the component that carries the attribute. Faces 2.3 turned what used to be a pile of per-library conventions into one specified facility with an extension point, and every standard attribute that points at another component goes through it. This page explains the grammar, the keywords, where in the request processing lifecycle an expression is resolved (which decides what it can see), what the two implementations do with the cases the specification leaves open, and how to add keywords and decorate the engine.

What a Search Expression Is

The API documentation of SearchExpressionHandler defines it: a search expression is either an identifier (matched exactly against the id property of a component), a keyword such as @this or @form, or a series of such identifiers and keywords linked by the naming container separator character, which is the colon unless jakarta.faces.SEPARATOR_CHAR says otherwise. A leading separator makes the expression absolute: the search starts at the view root. Without it the search starts at the source, the component that carries the attribute. Several expressions in one attribute are separated by spaces (and, on MyFaces, by commas; see execute and render in f:ajax).

The places that accept search expressions in Faces 4.1:

Where Attribute Notes

f:ajax

execute, render

Documented in the VDL as "search expressions"; defaults @this and @none. See execute and render in f:ajax.

h:commandScript

execute, render

Same renderer path as f:ajax; the VDL names only the four classic keywords. The page on HTML components ran it and render="out" came out as the client id of the output.

h:outputLabel

for

The VDL still calls it a "client identifier"; both implementations call the handler (single result, no exception when nothing matches). See Labels and Messages.

h:message, h:messages

for

Same as the label. See Labels and Messages.

UIComponent.findComponent(String)

the argument

The older method: identifiers and the separator only, no keywords. See The Resolution Algorithm.

SearchExpressionHandler

any

The programmatic API; see The SearchExpressionHandler API.

PrimeFaces components

process, update, trigger and others

Through the same handler plus PrimeFaces' own keywords; see PrimeFaces Search Expressions.

Ids, Client Ids and Naming Containers

Two names are in play. The component id is the id you write; it has to be unique only inside the closest naming container (h:form, h:dataTable, ui:repeat, a composite component, f:subview). The client id is what the browser sees: the ids of the enclosing naming containers and the component id joined with the separator, plus the row index inside an iterating component (search:title, shelf:books:1:status). The pages on the architecture and on the HTML components work through how client ids are built; what matters here is how the two names are used:

  • A search expression written with component ids is relative to the naming container of the source. Inside <h:form id="search"> the expression title means the component search:title.

  • An expression that starts with the separator is absolute and is written with ids from the root: :search:title is the same component from anywhere in the view, and a component outside every form is :results.

  • The client ids that come out are what travel in the Ajax request. Search expressions exist so that nobody has to concatenate them by hand.

  • The separator is configurable. With the context parameter jakarta.faces.SEPARATOR_CHAR set to _ the same views rendered search_author and a relative id and an absolute expression starting with _ (_search_author) resolved to it on both implementations. Code must call UINamingContainer.getSeparatorChar(FacesContext) rather than hard-code the colon.

The specification calls the separator out in 3.2.2 (NamingContainer) and defines findComponent in 3.1.8; the latter is the ancestor of the whole facility.

The Resolution Algorithm

The handler splits an expression at the separator into commands and resolves them from left to right. The API documentation states the algorithm:

  1. If the expression starts with the separator, the base is the view root and the separator is dropped; otherwise the base is the source component.

  2. The first command is resolved starting at the base. A command that starts with @ is a keyword and is given to the keyword resolvers. Any other command is an id.

  3. Every further command is resolved starting at the component the previous command produced.

  4. A leaf keyword (@all and @none among the built-in ones) cannot be followed by anything. Both implementations throw a FacesException for @none:@form and @all:@form, and isValidExpression returns false for them.

For an id the rules of UIComponent.findComponent apply: the lookup starts at the closest naming container of the base (the base itself when it is one), and in an expression of several identifiers every identifier before the last must name a naming container. filters:isbn, with filters a plain h:panelGroup, throws an IllegalArgumentException on both implementations.

What the specification leaves open is what happens when findComponent finds nothing, and the two implementations differ. Mojarra falls back to a client-id lookup from the view root, so an id such as results (a component that is not in the form) or a full client id such as catalogue:title still resolves. MyFaces walks up through the enclosing naming containers. Both therefore accept results from inside a form, which is why render="results" works in practice even though the component lives outside the form; but they disagree about absolute client ids that contain a row index (Inside Tables and ui:repeat). Both are observable with the probe view of the section The SearchExpressionHandler API; the table was produced from a button in the form probe while the form catalogue held an input title:

Expression Result (Mojarra and MyFaces alike unless noted) Component

expression, probe:expression, :probe:expression, @form:expression

client id probe:expression

an input in the same form

@form:@child(0)

probe:expression

the first child of the form

@next, @previous

probe:lines, probe:single

the siblings of the button

title

ComponentNotFoundException

the input is in the other form

catalogue:title, :catalogue:title

client id catalogue:title

the other form is a naming container

@root:@id(title)

client id catalogue:title

searches the whole view

nosuch, @bogus

ComponentNotFoundException

nothing matches; isValidExpression is false for @bogus

The Keywords

The API documentation of SearchKeywordResolver lists the twelve keywords every implementation must support:

Keyword Meaning (from the API documentation, with what was observed)

@this

The base component: the component that carries the attribute (for a behavior, the component the behavior is attached to). In f:ajax execute this is the default.

@form

The closest ancestor form.

@all

All components in the view. A leaf. In f:ajax it is sent as @all, and the partial response updates the pseudo id jakarta.faces.ViewRoot.

@none

No component. A leaf. It is the default of render.

@parent

The parent of the base component.

@namingcontainer

The closest ancestor naming container.

@composite

The composite component the base belongs to. Outside a composite both implementations throw a NullPointerException in the handler (programmatic probe) and the page that renders such an f:ajax fails with HTTP 500.

@next, @previous

The component after or before the base among the children of the parent. They are siblings of the base, whatever type they are: a neighbouring h:outputText counts.

@child(n)

The n-th child of the base, counting from 0. When the base has fewer children the handler throws a FacesException ("has fewer children"); an n that is not a number is not recognised as the keyword at all.

@id(id)

The components with that id (an id, not a client id), found by visiting the tree below the base. See Chaining and the @id Keyword.

@root

The UIViewRoot. Meant as the start of a chain; see Chaining and the @id Keyword.

The Jakarta EE Tutorial, as of this writing, lists only four keywords for f:ajax (@all, @form, @none, @this); the specification and the API documentation have the twelve above, and the VDL for f:ajax points to SearchKeywordResolver for "the complete list of keywords".

The view below uses a number of them in one Bookshelf catalogue search form. The bean is the small one every example of the section uses; the scenario is the same as in the page on navigation.

CatalogueBean.java — the backing bean of the search form
package org.example.bookshelf.web;

import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.example.bookshelf.model.Book;

/** The catalogue search of the Bookshelf example. */
@Named
@ViewScoped
public class CatalogueBean implements Serializable {

    private static final List<Book> ALL = List.of(
            new Book(1, "Dune", "Frank Herbert", "9780441172719"),
            new Book(2, "Emma", "Jane Austen", "9780141439587"),
            new Book(3, "Ubik", "Philip K. Dick", "9780575079939"));

    private String title = "";
    private String author = "";
    private String isbn = "";
    private String year = "";
    private List<Book> results = new ArrayList<>(ALL);
    private final Set<Long> borrowed = new HashSet<>();
    private int searches;

    public void search() {
        searches++;
        String t = title == null ? "" : title.trim().toLowerCase();
        results = new ArrayList<>(ALL.stream().filter(b -> b.title().toLowerCase().contains(t)).toList());
    }

    public void borrow(Book book) {
        borrowed.add(book.id());
    }

    public String status(Book book) {
        return borrowed.contains(book.id()) ? "borrowed" : "available";
    }

    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    public String getAuthor() { return author; }
    public void setAuthor(String author) { this.author = author; }
    public String getIsbn() { return isbn; }
    public void setIsbn(String isbn) { this.isbn = isbn; }
    public String getYear() { return year; }
    public void setYear(String year) { this.year = year; }
    public List<Book> getResults() { return results; }
    public List<String> getFilterIds() { return List.of("title", "author"); }
    public int getSearches() { return searches; }
}
keywords.xhtml — search expressions in f:ajax
<!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">
<h:head><title>Bookshelf search</title></h:head>
<h:body>
<h:form id="search">
    <h:inputText id="title" value="#{catalogueBean.title}"/>
    <h:panelGroup id="filters" layout="block">
        <h:inputText id="author" value="#{catalogueBean.author}"/>
        <h:commandButton id="refine" value="Refine" action="#{catalogueBean.search}">
            <f:ajax execute="@this @parent" render="@next"/>
        </h:commandButton>
        <h:outputText id="hint" value="#{catalogueBean.searches} searches"/>
    </h:panelGroup>
    <h:commandButton id="byId" value="By id" action="#{catalogueBean.search}">
        <f:ajax execute="title author" render=":results"/>
    </h:commandButton>
    <h:commandButton id="fromBean" value="From a bean" action="#{catalogueBean.search}">
        <f:ajax execute="#{catalogueBean.filterIds}" render="results"/>
    </h:commandButton>
    <h:commandButton id="relative" value="Relative" action="#{catalogueBean.search}">
        <f:ajax execute="@this" render="results"/>
    </h:commandButton>
    <h:commandButton id="wholeForm" value="Whole form" action="#{catalogueBean.search}">
        <f:ajax execute="@form" render="@form results"/>
    </h:commandButton>
    <h:commandButton id="chain" value="Chain" action="#{catalogueBean.search}">
        <f:ajax execute="@form:@child(0)" render="@root:@id(results)"/>
    </h:commandButton>
    <h:commandButton id="everything" value="Everything" action="#{catalogueBean.search}">
        <f:ajax execute="@all" render="@all"/>
    </h:commandButton>
</h:form>
<h:panelGroup id="results" layout="block">
    <ui:repeat value="#{catalogueBean.results}" var="book">
        <div>#{book.title}</div>
    </ui:repeat>
</h:panelGroup>
</h:body>
</html>

Clicking each button with the real faces.js produced these requests. The columns are the values of jakarta.faces.partial.execute and jakarta.faces.partial.render; the partial response updated the elements listed last. The source button is always added to execute:

Button execute sent (Mojarra) render sent Elements updated

refine

search:refine search:filters

search:hint

search:hint (the @next of the button is the h:outputText after it)

byId

search:byId search:title search:author

results

results (the absolute :results was turned into the client id results)

fromBean

search:fromBean search:title search:author

results

results; an EL expression that returns a Collection<String> works as the VDL says

relative

search:relative

results

results; results is not in the form, yet it resolved

wholeForm

search:wholeForm search

search results

search and results

chain

search:chain search:title

results

results

everything

@all

@all

jakarta.faces.ViewRoot

MyFaces sent the same sets of ids, with the source last (search:title search:author search:byId) where Mojarra puts it first. Neither order matters to the lifecycle.

Chaining and the @id Keyword

Chaining makes the keywords composable: @form:@child(0) is the first child of the form, @parent:isbn is the sibling named isbn reached through the parent, @composite:label is the component label inside the composite. Each step starts where the previous one ended. Five rules and pitfalls decide whether a chain works:

  • @id(x) starts at the base and looks below it. From a button it finds nothing, because a button has no children. The intuitive render="@id(results)" therefore fails: on Mojarra the unresolved token is sent to the browser as it is and the server ignores it silently (execute and render stayed @id(author) and nothing was processed); on MyFaces the page that contains the f:ajax fails to render with a ComponentNotFoundException (HTTP 500). Anchor it: @form:@id(author) or @root:@id(results) worked on both implementations (the second is in the view above).

  • The @id keyword delivers every match unless told to stop. @form:@id(note) in a test view with a repeated row returned the client ids of all three rows when called programmatically. f:ajax asks for a single component, and both implementations then used only the first row (search:rows:0:note).

  • MyFaces 4.1.4 recognises @id(x) only when the id has three or more characters. The source of IdSearchKeywordResolver tests that the command is longer than six characters (id() counts), so @id(box) works and @id(b) does not; I observed @root:@id(b) failing and @root:@id(box) resolving on MyFaces while both worked on Mojarra.

  • @root is for chains. As execute it made the server recurse until a StackOverflowError on both implementations; as render Mojarra answered with IllegalStateException: CDATA tags may not nest and MyFaces with an error response. Write @all instead of a lone @root.

  • Leaf keywords end the chain. Called on the handler, @all:@form and @none:@form throw a FacesException (message "It’s not valid to place a keyword or id after a leaf keyword" on Mojarra, "Expression cannot have keywords or ids at the right side" on MyFaces). In f:ajax, which resolves with RESOLVE_CLIENT_SIDE, the expression is treated as a pass-through keyword expression and sent on unresolved, and the request then rendered nothing: the mistake is silent on both.

When an Expression Is Resolved

This is the part that decides what an expression can express. For f:ajax the expressions are resolved while the page is rendered, not while the Ajax request is processed:

  1. During Render Response of the page that contains the f:ajax (the initial GET, a postback, or a partial response that re-renders the component), the renderer of the behavior calls the handler for every token of execute and render. Mojarra asks resolveClientId once per token; MyFaces asks resolveClientIds once per attribute. The handler returns client ids, the renderer writes them into the JavaScript call (myfaces.ab(this,event,'action','search:title search:author','results',{})), and keywords that the browser can handle itself (@all, @none, @form, and, on Mojarra only, @this) are written as they are.

  2. The Ajax request therefore carries client ids. During Apply Request Values and the later phases the PartialViewContext hands those ids to the lifecycle (getExecuteIds(), getRenderIds()); the handler is not asked again. A PhaseListener logged executeIds=[search:byId, search:title, search:author] and renderIds=[results] for the byId button, and a wrapper like the one in Decorating the Handler that logged every call to the handler logged them all before Apply Request Values and none between there and the end of the request.

  3. h:outputLabel and h:message resolve their for while they are rendered, in Render Response, in the same request that made the validation messages.

  4. A handler call from your own code resolves at the moment you make it (The SearchExpressionHandler API).

Four consequences follow, and every one of them was observed:

  • The tree that matters is the tree of the page that rendered the script. A component that is not in the tree at that moment cannot be resolved by keyword.

  • State of the postback is invisible to f:ajax expressions. A keyword such as @invalid (Custom Keywords) works in h:message for, because that attribute is resolved after validation, but in render= it would be resolved before the user has typed anything.

  • Resolution happens once per rendering of the script, not once per click. A component that a partial response re-renders gets its script resolved again: when the refresh button of the rows view below re-rendered the list, the status expressions of the three borrow buttons were resolved again while the response was rendered (Mojarra log).

  • In a PhaseListener or a bean, PartialViewContext.getRenderIds() is a list of client ids, never an expression. Use the handler if you want to resolve one.

Where this sits in the lifecycle is the Render Response phase described in the page on the request processing lifecycle (see Render Response and the partial lifecycle); execution of the ids is the job of the other phases of the partial lifecycle.

execute and render in f:ajax

The attributes are the main user of the facility; the page on Ajax covers the tag, the events and the partial lifecycle, and this section covers only the expressions in it.

  • execute defaults to @this and render to @none. The tags the keywords select take part in Apply Request Values, Process Validations, Update Model Values and Invoke Application of the Ajax request (execute) or are encoded into the partial response (render).

  • The value is either a literal of expressions separated by white space, or an EL expression that evaluates to a Collection<String> (the fromBean button above). The tokens of a literal are trimmed; extra spaces are harmless (author isbn and ` +@this + ` gave the same ids).

  • Separate expressions with spaces. f:ajax execute="author,isbn" was split by MyFaces (search:author search:isbn) but Mojarra sent the single token author,isbn, which matched nothing, so neither component was processed and no error was reported. The API documentation says that comma and space are the separator characters of the handler (getExpressionSeperatorChars, spelled as in the API), and a literal that f:ajax hands to Mojarra’s handler one token at a time never reaches that splitting.

  • Unresolvable expressions fail loudly on MyFaces and silently on Mojarra. For render="nosuch" MyFaces throws ComponentNotFoundException: Cannot find component for expression "nosuch" referenced from "search:go" while rendering the page (HTTP 500). Mojarra catches the exception, sends the token on as it is, and the browser later finds no element with that id. In development you want to know; the wrapper in Decorating the Handler logs each case on both.

  • faces.js adds the source component to execute (the requests above show search:go next to the ids) unless the value is @none or @all: with execute="@none" the request carried no jakarta.faces.partial.execute at all.

Table 1. The scripts written for the view above
Button Mojarra MyFaces

refine

mojarra.ab(this,event,'action','@this search:filters','search:hint')

myfaces.ab(this,event,'action','search:refine search:filters','search:hint',{})

wholeForm

mojarra.ab(this,event,'action','@form','@form results')

myfaces.ab(this,event,'action','@form','@form results',{})

everything

mojarra.ab(this,event,'action','@all','@all')

myfaces.ab(this,event,'action','@all','@all',{})

The rendered form was identical in everything else.

Inside Tables and ui:repeat

An iterating component is a naming container whose children exist once per row. A button inside a row is resolved per row: the script for each row is written while the row is current, and every relative expression lands on the same row. That is the single most useful behaviour of the facility.

rows.xhtml — row-local and list-wide Ajax
<!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">
<h:head><title>Bookshelf rows</title></h:head>
<h:body>
<h:form id="shelf">
    <h:panelGroup id="list" layout="block">
        <ui:repeat id="books" value="#{catalogueBean.results}" var="book">
            <h:panelGroup id="row" layout="block">
                <h:outputText id="caption" value="#{book.title}"/>
                <h:commandButton id="borrow" value="Borrow" action="#{catalogueBean.borrow(book)}">
                    <f:ajax render="status"/>
                </h:commandButton>
                <h:outputText id="status" value="#{catalogueBean.status(book)}"/>
            </h:panelGroup>
        </ui:repeat>
    </h:panelGroup>
    <h:commandButton id="refresh" value="Refresh" action="#{catalogueBean.search}">
        <f:ajax render="list"/>
    </h:commandButton>
</h:form>
</h:body>
</html>

For the button of the second row the scripts were (from shelf:books:1:borrow, identical on both implementations): render="status" sent shelf:books:1:status and the response updated only that element; the refresh button, which is outside the repeat, sent shelf:list. A test view with the same structure produced these results (the row button is inside ui:repeat id="rows" in a form search; second row; "item" is a wrapper h:panelGroup, "caption" an output in the same row):

render= Mojarra MyFaces

caption, item, @parent, @parent:caption, @form:rows:caption, rows:caption

the second row’s client id, e.g. search:rows:1:caption, element updated

same

@previous, @next

search:rows:1:note, search:rows:1:caption

same

:search:rows:2:caption (absolute, other row)

search:rows:2:caption, updated

ComponentNotFoundException, HTTP 500 while rendering the page

@namingcontainer, rows (the repeat’s id)

search:rows:1: no such element, nothing updated

search:rows: no such element either (a repeat writes no markup)

@form:@id(caption)

search:rows:0:caption (the first row only)

same

Four lessons:

  • Prefer relative ids and @parent inside a row. They are resolved per row on both implementations and need no knowledge of the row index.

  • Avoid absolute ids with a row index. :search:rows:2:caption works on Mojarra and fails the rendering of the whole page on MyFaces.

  • Do not render a ui:repeat (or the iterating component) by its id. ui:repeat writes no element, so there is nothing to update, in either implementation: when the refresh button rendered the repeat itself (render="books") the response carried an update for shelf:books, MyFaces reported that the element was not found, and Mojarra’s script reported an error to faces.ajax.addOnError. Wrap it in a component with an id, as list above, and render that. @namingcontainer and the repeat’s id from inside a row additionally resolved to search:rows:1 on Mojarra, a client id that is a row, not an element.

  • SKIP_VIRTUAL_COMPONENTS and @id show the iteration. Without the hint @form:@id(note) returned search:rows:0:note, search:rows:1:note and search:rows:2:note; with SKIP_VIRTUAL_COMPONENTS it returned the single component with the client id search:rows:note, the one that exists in the tree without a row index (The SearchExpressionHandler API).

Inside Composite Components

A composite component is a naming container: the id of the tag is a prefix for everything in its implementation, and that is what makes ids inside the implementation relative to the composite.

resources/ez/stars.xhtml — a composite that updates itself
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:cc="jakarta.faces.composite">
<cc:interface>
    <cc:attribute name="value"/>
</cc:interface>
<cc:implementation>
    <h:panelGroup id="box" layout="block">
        <h:inputText id="rating" value="#{cc.attrs.value}"/>
        <h:commandButton id="rate" value="Rate" action="#{catalogueBean.search}">
            <f:ajax execute="@this rating" render="label"/>
        </h:commandButton>
        <h:outputText id="label" value="rated #{cc.attrs.value}"/>
    </h:panelGroup>
</cc:implementation>
</html>
stars.xhtml — using it, and updating a part of it from outside
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ez="jakarta.faces.composite/ez">
<h:head><title>Bookshelf stars</title></h:head>
<h:body>
<h:form id="detail">
    <ez:stars id="stars" value="#{catalogueBean.year}"/>
    <h:commandButton id="outside" value="Outside" action="#{catalogueBean.search}">
        <f:ajax render="stars:label"/>
    </h:commandButton>
</h:form>
</h:body>
</html>

Inside the implementation, rating and label resolved to detail:stars:rating and detail:stars:label; from outside, stars:label (a path through the composite) and :detail:stars:label resolved to the same component on both implementations. Some other results with the same views (the VDL puts it more generally: inside a composite, @this in f:ajax is remapped to the composite’s behavior targets, and a relative value is reinterpreted relative to the declaration of f:ajax; Faces 4.0 improved f:ajax in composites, issue 1567 in the change log):

  • @composite from inside resolved to the composite, client id detail:stars, and @composite:label and @composite:box resolved to the inner components, on both implementations.

  • render="@composite" or render="stars" (outside) fail to update anything: the composite itself writes no element with its own client id; the markup starts at the first component of the implementation (detail:stars:box). MyFaces reported that the element detail:stars was not found and Mojarra’s script reported an error to faces.ajax.addOnError. Give the implementation a wrapper with an id, as box above, and render that.

  • @namingcontainer from the button inside the composite resolved to the composite for the same reason, with the same non-update.

  • @composite evaluated outside a composite threw a NullPointerException (on both), so the page failed to render.

The attributes and the retargeting rules of cc:clientBehavior belong to the page on composite components.

Labels and Messages

h:outputLabel and h:message take a search expression in for (the VDL text predates the facility and calls it a client id): both implementations call the handler with the hints RESOLVE_SINGLE_COMPONENT and IGNORE_NO_RESULT, so an expression that matches nothing renders no for or no message instead of failing. The view below uses @next so the labels do not need ids, and the custom keyword @invalid of the section Custom Keywords to show the first message of the form wherever it comes from.

review.xhtml — labels and one message for the whole form
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core">
<h:head><title>Bookshelf review</title></h:head>
<h:body>
<h:form id="review">
    <h:outputLabel for="@next" value="Reviewer"/>
    <h:inputText id="reviewer" value="#{reviewBean.reviewer}" required="true"/>
    <h:outputLabel for="@next" value="Rating (1 to 5)"/>
    <h:inputText id="rating" value="#{reviewBean.rating}">
        <f:validateLongRange minimum="1" maximum="5"/>
    </h:inputText>
    <h:message id="firstError" for="@invalid"/>
    <h:commandButton id="send" value="Send" action="#{reviewBean.send}"/>
</h:form>
</h:body>
</html>
ReviewBean.java
package org.example.bookshelf.web;

import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Named;

/** The review form of the Bookshelf example. */
@Named
@RequestScoped
public class ReviewBean {
    private String reviewer;
    private Integer rating;

    public void send() {
        // a real application would store the review
    }

    public String getReviewer() { return reviewer; }
    public void setReviewer(String reviewer) { this.reviewer = reviewer; }
    public Integer getRating() { return rating; }
    public void setRating(Integer rating) { this.rating = rating; }
}

Both labels rendered for="review:reviewer" and for="review:rating". Posting an empty reviewer and the rating 9 made the message review:reviewer: Validation Error: Value is required. appear (the first invalid input in tree order); posting a reviewer and the rating 9 gave review:rating: Validation Error: Specified attribute is not between the expected values of 1 and 5.; with valid values Mojarra rendered an empty span and MyFaces rendered nothing and logged

SEVERE: Could not render Message. Unable to find component '@invalid' (calling findComponent on component 'review:firstError'). ...

on every such render. That message is MyFaces' way to say "the for resolved to nothing"; it is noise for a keyword that is legitimately empty most of the time. See the page on messages and internationalisation for the message components.

The SearchExpressionHandler API

Application.getSearchExpressionHandler() returns the singleton handler. The package jakarta.faces.component.search has these types:

Type Role

SearchExpressionHandler

The engine. Methods: resolveClientId, resolveClientIds, resolveComponent, resolveComponents, invokeOnComponent (two overloads; "the most essential method"), splitExpressions, isValidExpression, isPassthroughExpression, getExpressionSeperatorChars; constants KEYWORD_PREFIX (@) and EXPRESSION_SEPARATOR_CHARS.

SearchExpressionHandlerWrapper

Base class for decorators; see Decorating the Handler.

SearchExpressionContext

The call state: the source component, the FacesContext, the expression hints and the visit hints. Created with SearchExpressionContext.createSearchExpressionContext(context, source) or with the overload that takes the two hint sets. SearchExpressionContextFactory creates it and can be decorated.

SearchExpressionHint

IGNORE_NO_RESULT, RESOLVE_SINGLE_COMPONENT, RESOLVE_CLIENT_SIDE, SKIP_VIRTUAL_COMPONENTS.

SearchKeywordResolver

One keyword; see Custom Keywords.

SearchKeywordContext

What a keyword resolver receives; see Custom Keywords.

ComponentNotFoundException

The FacesException for an expression that matched nothing.

The hints, as documented and as observed:

Hint Documented effect Observed

IGNORE_NO_RESULT

an expression that resolves to nothing returns null instead of throwing

resolveClientIds returned an empty list and resolveComponents made no callback, for nosuch, @bogus, @none and an empty @invalid; a leaf-keyword violation (@none:@form) still threw FacesException

RESOLVE_SINGLE_COMPONENT

only one component is resolved (lets a keyword stop a tree visit early)

@form:@id(note) returned one client id (search:rows:0:note) instead of three

RESOLVE_CLIENT_SIDE

a keyword that the client can resolve is returned as it is

@all, @form, @none came back as the keyword and isPassthroughExpression was true; @this and @parent were resolved to client ids

SKIP_VIRTUAL_COMPONENTS

only real components, not the per-row copies of iterating components

@form:@id(note) returned search:rows:note; absolute :search:rows:1:title was no longer found on Mojarra

resolveClientId(s) and resolveComponent(s) add RESOLVE_SINGLE_COMPONENT themselves for the single-result forms. The standard components use these hints: f:ajax RESOLVE_CLIENT_SIDE and RESOLVE_SINGLE_COMPONENT; labels and messages IGNORE_NO_RESULT and RESOLVE_SINGLE_COMPONENT (the hints were logged by a wrapper in the runs).

The view and the bean below call the handler from a button and list what it returns. They are the probe that produced the tables of this page.

probe.xhtml — the probe view
<!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">
<h:head><title>Expression probe</title></h:head>
<h:body>
<h:form id="probe">
    <h:inputText id="expression" value="#{expressionProbe.expression}"/>
    <h:selectBooleanCheckbox id="ignoreNoResult" value="#{expressionProbe.ignoreNoResult}"/>
    <h:selectBooleanCheckbox id="clientSide" value="#{expressionProbe.clientSide}"/>
    <h:selectBooleanCheckbox id="skipVirtual" value="#{expressionProbe.skipVirtual}"/>
    <h:selectBooleanCheckbox id="single" value="#{expressionProbe.single}"/>
    <h:commandButton id="resolve" value="Resolve" actionListener="#{expressionProbe.resolve}">
        <f:ajax execute="@form" render="lines"/>
    </h:commandButton>
    <h:panelGroup id="lines" layout="block">
        <ui:repeat value="#{expressionProbe.lines}" var="line"><div>#{line}</div></ui:repeat>
    </h:panelGroup>
</h:form>
<h:form id="catalogue">
    <h:inputText id="title" value="#{catalogueBean.title}"/>
    <h:inputText id="author" value="#{catalogueBean.author}"/>
</h:form>
</h:body>
</html>
ExpressionProbe.java — resolving from the clicked button
package org.example.bookshelf.web;

import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.component.UIComponent;
import jakarta.faces.component.search.SearchExpressionContext;
import jakarta.faces.component.search.SearchExpressionHandler;
import jakarta.faces.component.search.SearchExpressionHint;
import jakarta.faces.context.FacesContext;
import jakarta.faces.event.ActionEvent;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;

/** Resolves a search expression from the clicked button, with the programmatic API. */
@Named
@RequestScoped
public class ExpressionProbe {
    @Inject
    FacesContext context;

    private String expression = "";
    private boolean ignoreNoResult;
    private boolean clientSide;
    private boolean skipVirtual;
    private boolean single;
    private final List<String> lines = new ArrayList<>();

    public void resolve(ActionEvent event) {
        UIComponent source = event.getComponent();
        SearchExpressionHandler handler = context.getApplication().getSearchExpressionHandler();
        Set<SearchExpressionHint> hints = EnumSet.noneOf(SearchExpressionHint.class);
        if (ignoreNoResult) hints.add(SearchExpressionHint.IGNORE_NO_RESULT);
        if (clientSide) hints.add(SearchExpressionHint.RESOLVE_CLIENT_SIDE);
        if (skipVirtual) hints.add(SearchExpressionHint.SKIP_VIRTUAL_COMPONENTS);
        if (single) hints.add(SearchExpressionHint.RESOLVE_SINGLE_COMPONENT);
        SearchExpressionContext sec = SearchExpressionContext.createSearchExpressionContext(context, source, hints, null);
        try {
            lines.add("valid=" + handler.isValidExpression(sec, expression)
                    + " passthrough=" + handler.isPassthroughExpression(sec, expression));
        } catch (RuntimeException e) {
            lines.add("valid/passthrough threw " + e.getClass().getSimpleName() + ": " + e.getMessage());
        }
        try {
            lines.add("clientIds=" + handler.resolveClientIds(sec, expression));
        } catch (RuntimeException e) {
            lines.add("clientIds threw " + e.getClass().getSimpleName() + ": " + e.getMessage());
        }
        try {
            List<String> found = new ArrayList<>();
            handler.resolveComponents(sec, expression, (ctx, c) -> found.add(c.getClass().getSimpleName() + "#" + c.getId()));
            lines.add("components=" + found);
        } catch (RuntimeException e) {
            lines.add("components threw " + e.getClass().getSimpleName() + ": " + e.getMessage());
        }
        System.out.println("PROBE " + expression + " hints=" + hints + " -> " + lines);
    }

    public String getExpression() { return expression; }
    public void setExpression(String expression) { this.expression = expression; }
    public boolean isIgnoreNoResult() { return ignoreNoResult; }
    public void setIgnoreNoResult(boolean v) { this.ignoreNoResult = v; }
    public boolean isClientSide() { return clientSide; }
    public void setClientSide(boolean v) { this.clientSide = v; }
    public boolean isSkipVirtual() { return skipVirtual; }
    public void setSkipVirtual(boolean v) { this.skipVirtual = v; }
    public boolean isSingle() { return single; }
    public void setSingle(boolean v) { this.single = v; }
    public List<String> getLines() { return lines; }
}

The exceptions to expect from a call: ComponentNotFoundException when nothing matched and IGNORE_NO_RESULT is off (an unknown keyword such as @bogus gave this one, although isValidExpression was false); a plain FacesException for a keyword after a leaf keyword and for @child(n) with too few children; IllegalArgumentException from findComponent for an intermediate id that is not a naming container; NullPointerException for @composite outside a composite. Check isValidExpression first if the expression comes from configuration.

Custom Keywords

A keyword is a SearchKeywordResolver: isResolverForKeyword says whether the instance is responsible, resolve calls SearchKeywordContext.invokeContextCallback for each component it finds and setKeywordResolved(true), and isLeaf and isPassthrough (default false) describe the keyword. Two facts decide whether a resolver works:

  • The keyword string does not contain the @. The handler strips the prefix before it asks the resolvers, so @invalid arrives as invalid and @child(0) as child(0). A resolver that compares with "@invalid" matches nothing, on both implementations; isValidExpression then reports false. The API documentation does not say this; the built-in resolvers (for example "this".equals(keyword) in Mojarra) show it, and the failure was observed.

  • What the keyword can see. It runs when the expression is resolved (When an Expression Is Resolved): at page render for f:ajax, so it must depend on the tree, not on the request. In h:message for it runs after validation.

InvalidKeywordResolver.java — @invalid, the inputs whose validation failed (registered in faces-config.xml)
package org.example.bookshelf.search;

import jakarta.faces.component.UIComponent;
import jakarta.faces.component.UIInput;
import jakarta.faces.component.search.SearchExpressionContext;
import jakarta.faces.component.search.SearchKeywordContext;
import jakarta.faces.component.search.SearchKeywordResolver;
import jakarta.faces.component.visit.VisitContext;
import jakarta.faces.component.visit.VisitResult;
import jakarta.faces.context.FacesContext;

/** Resolves {@code @invalid} to every input of the view whose validation failed in this request. */
public class InvalidKeywordResolver extends SearchKeywordResolver {

    @Override
    public boolean isResolverForKeyword(SearchExpressionContext context, String keyword) {
        return "invalid".equals(keyword);
    }

    @Override
    public void resolve(SearchKeywordContext context, UIComponent current, String keyword) {
        FacesContext facesContext = context.getSearchExpressionContext().getFacesContext();
        facesContext.getViewRoot().visitTree(VisitContext.createVisitContext(facesContext), (visit, component) -> {
            if (component instanceof UIInput input && !input.isValid()) {
                context.invokeContextCallback(input);
            }
            return VisitResult.ACCEPT;
        });
        context.setKeywordResolved(true);
    }
}
InputsKeywordResolver.java — @inputs, every input below the base component
package org.example.bookshelf.search;

import jakarta.faces.component.UIComponent;
import jakarta.faces.component.UIInput;
import jakarta.faces.component.search.SearchExpressionContext;
import jakarta.faces.component.search.SearchKeywordContext;
import jakarta.faces.component.search.SearchKeywordResolver;
import jakarta.faces.component.visit.VisitContext;
import jakarta.faces.component.visit.VisitResult;

/** {@code @inputs}: every input below the base component. */
public class InputsKeywordResolver extends SearchKeywordResolver {

    @Override
    public boolean isResolverForKeyword(SearchExpressionContext context, String keyword) {
        return "inputs".equals(keyword);
    }

    @Override
    public void resolve(SearchKeywordContext context, UIComponent current, String keyword) {
        var facesContext = context.getSearchExpressionContext().getFacesContext();
        current.visitTree(VisitContext.createVisitContext(facesContext), (visit, component) -> {
            if (component instanceof UIInput) {
                context.invokeContextCallback(component);
            }
            return VisitResult.ACCEPT;
        });
        context.setKeywordResolved(true);
    }
}
RegisterResolver.java — registering a resolver in code
package org.example.bookshelf.search;

import jakarta.faces.application.Application;
import jakarta.faces.event.AbortProcessingException;
import jakarta.faces.event.PostConstructApplicationEvent;
import jakarta.faces.event.SystemEvent;
import jakarta.faces.event.SystemEventListener;

/** Registers the {@code @inputs} keyword in code; {@code @invalid} is declared in faces-config.xml. */
public class RegisterResolver implements SystemEventListener {

    @Override
    public void processEvent(SystemEvent event) throws AbortProcessingException {
        Application application = ((PostConstructApplicationEvent) event).getApplication();
        application.addSearchKeywordResolver(new InputsKeywordResolver());
    }

    @Override
    public boolean isListenerForSource(Object source) {
        return source instanceof Application;
    }
}

A resolver is registered in one of two ways, both used above. In faces-config.xml (/faces-config/application/search-keyword-resolver in the specification’s list of application configuration elements) or with Application.addSearchKeywordResolver, which adds to the head of the chain, cannot be undone, and throws IllegalStateException after the first request; the PostConstructApplicationEvent listener above is the usual place. Per the API documentation of Application.getSearchKeywordResolver() the chain is: the resolvers declared in the configuration, then those added in code, then the built-in ones.

faces-config.xml — the registrations of this page
<?xml version="1.0" encoding="UTF-8"?>
<faces-config xmlns="https://jakarta.ee/xml/ns/jakartaee"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-facesconfig_4_1.xsd"
              version="4.1">
    <application>
        <search-expression-handler>org.example.bookshelf.search.DiagnosticSearchExpressionHandler</search-expression-handler>
        <search-keyword-resolver>org.example.bookshelf.search.InvalidKeywordResolver</search-keyword-resolver>
        <system-event-listener>
            <system-event-listener-class>org.example.bookshelf.search.RegisterResolver</system-event-listener-class>
            <system-event-class>jakarta.faces.event.PostConstructApplicationEvent</system-event-class>
            <source-class>jakarta.faces.application.Application</source-class>
        </system-event-listener>
    </application>
</faces-config>

Observed with the two resolvers:

  • h:message for="@invalid" showed the message of the first invalid input, as in the review view above, on both.

  • @form:@inputs resolved to every input in the form in the programmatic probe (clientIds of all five inputs of the probe form). In f:ajax it expanded to every input on MyFaces, but only to the first (search:title) on Mojarra: Mojarra’s renderer keeps the first component delivered for a token, MyFaces' keeps all of them, and my resolver does not look at RESOLVE_SINGLE_COMPONENT (the built-in @id does, and stops at the first match on both). A resolver that should behave the same everywhere checks the hint. Do not rely on a multi-match keyword in f:ajax unless you test both.

  • A keyword that matches nothing is an error where the hints do not include IGNORE_NO_RESULT: execute="@form:@invalid" failed the page on MyFaces, and Mojarra sent a token that the server could not use.

  • PrimeFaces uses the same extension point for its own keywords (PrimeFaces Search Expressions).

Decorating the Handler

The handler itself is replaceable. A class that extends SearchExpressionHandlerWrapper and has a constructor that takes the handler it wraps can be named in the search-expression-handler element (above); two wrappers registered in different configuration files were both active in the runs. The example below logs each expression that matched nothing and then lets the default behaviour continue: it makes the silent Mojarra case visible.

DiagnosticSearchExpressionHandler.java
package org.example.bookshelf.search;

import jakarta.faces.component.search.ComponentNotFoundException;
import jakarta.faces.component.search.SearchExpressionContext;
import jakarta.faces.component.search.SearchExpressionHandler;
import jakarta.faces.component.search.SearchExpressionHandlerWrapper;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/** Logs every expression that cannot be resolved, then lets the default behaviour go on. */
public class DiagnosticSearchExpressionHandler extends SearchExpressionHandlerWrapper {

    private static final Logger LOG = Logger.getLogger(DiagnosticSearchExpressionHandler.class.getName());

    public DiagnosticSearchExpressionHandler(SearchExpressionHandler wrapped) {
        super(wrapped);
    }

    @Override
    public String resolveClientId(SearchExpressionContext context, String expression) {
        try {
            return super.resolveClientId(context, expression);
        } catch (ComponentNotFoundException e) {
            report(context, expression);
            throw e;
        }
    }

    @Override
    public List<String> resolveClientIds(SearchExpressionContext context, String expressions) {
        try {
            return super.resolveClientIds(context, expressions);
        } catch (ComponentNotFoundException e) {
            report(context, expressions);
            throw e;
        }
    }

    private void report(SearchExpressionContext context, String expression) {
        LOG.log(Level.WARNING, "Search expression \"{0}\" matched nothing, source component \"{1}\"",
                new Object[] {expression, context.getSource().getId()});
    }
}

With the wrapper Mojarra logged WARNING: Search expression "nosuch" matched nothing, source component "go" on a page whose f:ajax was otherwise silently accepted. The wrapper overrides resolveClientId because Mojarra’s f:ajax renderer calls that one, and resolveClientIds because MyFaces' calls that one; override the methods your callers use. The factory that creates the contexts, SearchExpressionContextFactory, is decorated the same way through the search-expression-context-factory element of the 4.1 configuration schema (the prose of the specification spells it search-expression-context-kit-factory; I did not register one, so which spelling a runtime accepts is not verified).

PrimeFaces Search Expressions

PrimeFaces 16 builds on the facility: its documentation calls it the Search Expression Framework and says that PrimeFaces enhances the Faces search expressions with its own keywords. process, update, trigger and similar attributes of PrimeFaces components take them. The jar ships its own keyword resolvers (the package org.primefaces.expression has classes for @(selector), @row, @form, the observer and @widgetVar), and PrimeFaces resolves its attributes by calling the handler (the stack trace of a failure ran through SearchExpressionUtils into resolveClientIds). The keywords from the PrimeFaces documentation, 16.0.0:

Keyword Meaning

@this, @all, @form, @none, @namingcontainer, @parent, @composite, @child(n), @previous, @next, @root, @id(id)

The Faces keywords as above.

@row(n)

The n-th row of a data table; the last resolved component must be a UIData.

@widgetVar(name)

The component with that widgetVar.

@(selector)

PrimeFaces Selectors (PFS): a jQuery selector evaluated in the browser.

@obs(event)

Targets an event, as described in the Event/Observer pattern of the PrimeFaces documentation (not exercised here).

Chaining works as in Faces (@form:@parent, @composite:mybuttonid), and the documentation adds that the framework applies wherever one component references another, not only to partial processing and updates. The Bookshelf view below uses a selector, a widget variable, a chain and a row:

primefaces.xhtml — PrimeFaces keywords
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:p="http://primefaces.org/ui">
<h:head><title>Bookshelf search with PrimeFaces</title></h:head>
<h:body>
<h:form id="search">
    <p:inputText id="title" styleClass="criteria" value="#{catalogueBean.title}"/>
    <p:inputText id="author" styleClass="criteria" value="#{catalogueBean.author}"/>
    <p:commandButton id="byClass" value="By style class" action="#{catalogueBean.search}"
                     process="@(.criteria)" update="results"/>
    <p:commandButton id="byWidget" value="By widget" action="#{catalogueBean.search}"
                     process="@this title" update="@widgetVar(resultsPanel)"/>
    <p:commandButton id="byChain" value="By chain" action="#{catalogueBean.search}"
                     process="@form:@child(0)" update="@form:results"/>
    <p:commandButton id="oneRow" value="One row" action="#{catalogueBean.search}"
                     process="@this" update="books:@row(0)"/>
    <p:panel id="results" widgetVar="resultsPanel" header="Results">
        <p:dataTable id="books" var="book" value="#{catalogueBean.results}">
            <p:column headerText="Title"><h:outputText value="#{book.title}"/></p:column>
        </p:dataTable>
    </p:panel>
</h:form>
</h:body>
</html>

Observed on Mojarra and MyFaces with PrimeFaces 16.0.0 (the same on both):

  • process="@(.criteria)" was sent to the browser and resolved there: the request carried search:title search:author. A selector is therefore not subject to the naming container limits, but it only matches components that have an explicit id in the markup. The documentation says the same and links to its list of limitations.

  • @form:@child(0) and @form:results resolved on the server to search:title and search:results; @this title became search:byWidget,search:title (PrimeFaces joins with commas).

  • A PrimeFaces selector cannot be combined with other keywords: @(.field):@parent failed the page with FacesException: jQuery selectors aka PFS are not supported on the server side.

  • @id(inside) from a button, an unknown id such as nosuch, and @bogus failed the rendering with a ComponentNotFoundException / FacesException on both implementations, so the strictness of PrimeFaces components does not depend on the implementation, unlike f:ajax (Mojarra and MyFaces Compared). @row(1) outside a table failed with "The last resolved component must be instance of UIData to support @row".

  • books:@row(0) resolved to a generated id inside the row (search:books:0:j_idt6 on Mojarra, search:books:0:j_id_g on MyFaces). It is documented but gives an id that is not stable; I would not use it to update a cell.

  • Comma and space both separated expressions (q,r and q r gave the same two ids).

  • @widgetVar(resultsPanel) was sent to the browser unresolved. In jsdom no widget was created, so the request fell back to another value; I could not run it end to end and make no claim about it. @obs(event) and @composite inside a composite were not exercised either.

Mojarra and MyFaces Compared

Case (Mojarra 4.1.17 / MyFaces 4.1.4) Mojarra MyFaces

How f:ajax asks the handler

resolveClientId once per token, RESOLVE_CLIENT_SIDE and RESOLVE_SINGLE_COMPONENT

resolveClientIds once per attribute, same hints

Separators in execute/render

white space only (a,b is one token that matches nothing, silently)

white space and comma

Expression that matches nothing, in f:ajax

caught; the token is sent on unresolved (silent)

ComponentNotFoundException, HTTP 500 at page render

Keyword that delivers several components, in f:ajax (custom @inputs)

the first one only

all of them (the built-in @id stops at the first on both)

@id(x) as the first command from a button

sent on unresolved, ignored by the server

ComponentNotFoundException; ids shorter than three characters never match

Relative id outside the naming container (results)

found through a client-id lookup from the view root

found by walking up the naming containers

Absolute id with a row index (:search:rows:2:caption)

resolved

ComponentNotFoundException

@namingcontainer / the repeat’s id from inside a row

search:rows:1 (a row, not an element)

search:rows

Order of the ids in the request

source first

source last

@this in the script

kept for faces.js

replaced by the client id

@root client id

j_id1

j_id__v_0

h:message for that matches nothing

silent

SEVERE: Could not render Message... in the log

The rest, including all the keywords of the table in The Keywords, the leaf rule, the composite results, SKIP_VIRTUAL_COMPONENTS and the labels, behaved identically.

What Changed per Version

Version Change

JSF 2.0 to 2.2

f:ajax execute and render took ids and the four keywords @all, @form, @none, @this that the Jakarta EE Tutorial still lists; component libraries such as PrimeFaces added their own conventions on top (when each library introduced its framework was not checked).

Faces 2.3

The enhanced component search facility (issue 1238 in the specification’s change log): the package javax.faces.component.search, the twelve keywords, chaining, SearchExpressionHandler, SearchKeywordResolver, Application.addSearchKeywordResolver, the search-expression-handler and search-keyword-resolver configuration elements. PrimeFaces' framework became an extension of it.

Faces 3.0

javax.faces becomes jakarta.faces; no change to the facility.

Faces 4.0

"Improve f:ajax behavior in composite components" (issue 1567) — relevant to @this and relative ids inside composites; no change to the API.

Faces 4.1

Nothing in the change log concerns search expressions.

Faces 5.0 (upcoming)

Not examined for this page (latest milestone 5.0.0-M9, 2026-09-18); no change is claimed.

Where the Books Are Outdated

PrimeFaces Cookbook, Second Edition (Çalışkan and Varaksin, 2015) treats process, update and its own search expressions in its first chapters, for PrimeFaces 5.x on JSF 2.2. When it was written the PrimeFaces framework was a library feature; since Faces 2.3 the core specification owns the facility and PrimeFaces extends it, so the same expressions now work in f:ajax and in the standard components (labels, messages) too. Its widget and selector keywords remain PrimeFaces specific, and PrimeFaces 16 documents more keywords than a book of that age can show (@row, @obs, @widgetVar, chaining). JavaServer Faces 2.0: The Complete Reference (Burns and Schalk, 2010) and the JSF 2.0 Cookbook (Leonard, 2010) were published before Faces 2.3, so they cannot describe the standard keywords beyond the four of f:ajax, chaining, the handler or custom keywords, and their code uses javax.faces. The Jakarta EE Tutorial itself still lists four keywords. Everything above was checked on 4.1.

Pitfalls

References