Navigation

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), Eclipse Mojarra 4.1.17 and Apache MyFaces Core 4.1.4, with Java 17 or later. The change lists of the 4.0 and 4.1 specifications contain no entry about navigation, so what follows also describes Faces 4.0 (nothing was run on 4.0); the older releases are covered in What Changed. 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 curl and, for the Ajax case, a real browser. The HTTP responses, the HTML, the log lines and every "what happened" statement come from those runs (checked on 2026-10-08), and the places where the two implementations disagree are collected in Mojarra and MyFaces.

Navigation answers one question: after the user did something, which page comes next? Faces does not answer it with a URL that the page author writes. An action method (or the action attribute itself) produces a short string, the outcome, and a navigation handler turns that string, together with the current view, into the next view. Because the page only names an outcome, the same button can lead to different places depending on what the application decided, and the decision can be changed in one place. Faces also supports the opposite model, where the link already contains its target and the browser just sends a GET: h:link and h:button compute the URL while the page is rendered, so the result can be bookmarked and opened in a new tab. This page covers both models, the three ways of choosing the target (implicit, rule-based and conditional), redirects, and the API under all of it.

The Navigation Model

Outcomes and the Navigation Handler

A logical outcome is a String. It comes from the action attribute of a command component, either as a literal (action="borrow") or as the return value of a method expression (action="#{borrowBean.submit}"). The default ActionListener calls the method, takes the returned value and hands three things to the application’s NavigationHandler: the current view (read from the FacesContext), the from action (the text of the action expression, or the literal) and the outcome. The handler picks the next view, and the lifecycle either renders it or, for a redirect, ends the response. The logged calls of the Bookshelf example show what the handler receives:

NAV /catalogue.xhtml action=#{catalogueBean.borrow(book)} outcome=borrow?faces-redirect=true&id=2 -> /borrow.xhtml redirect=true
NAV /borrow.xhtml action=#{borrowBean.submit} outcome=success -> /confirmation.xhtml redirect=true
NAV /borrow.xhtml action=help outcome=help -> /help.xhtml redirect=false

For a method expression, the "action" is the expression text exactly as it is written on the page, and for a literal it is the literal itself (third line: action help, outcome help). The from-action is what a <from-action> element of a navigation rule is compared with.

Where Navigation Happens in the Lifecycle

Navigation is decided in Invoke Application, phase 5 of the request processing lifecycle: the action runs, returns an outcome and the handler runs straight after it, inside the same phase. Consequences, all of which follow from the phase order described in the lifecycle page:

  • If conversion or validation failed, phases 4 and 5 are skipped, so the action never runs and there is no navigation. The user stays on the same view with the messages. A Borrow click with an empty required member name redisplayed the form on both implementations.

  • A command with immediate="true" runs its action in Apply Request Values, so the navigation happens there and the other inputs are not validated. The Help button of the borrow form works this way (see the listing in Rule-Based Navigation in faces-config.xml).

  • After a forward the lifecycle continues with Render Response on the new view, in the same HTTP request.

  • After a redirect the handler has completed the response (it calls responseComplete()), so Render Response does not run for the old view. A PhaseListener showed responseComplete=true after Invoke Application on both implementations.

  • A null outcome, and an empty string, mean "stay on this view". The specification says that this is the only case in which the same view and its component tree are reused; every match creates a new view, which also clears the view map.

Two Kinds of Navigation

POST navigation GET navigation

Components

h:commandButton, h:commandLink, and Ajax commands

h:link, h:button (both are UIOutcomeTarget components)

Outcome comes from

The action attribute, evaluated when the command is invoked (phase 5)

The outcome attribute, evaluated while the page is rendered (phase 6)

HTTP exchange

A postback to the current view, answered by the next view (forward) or by a 302 (redirect)

A plain GET straight to the target, with no postback and no view state

Rules that apply

Explicit rules with or without <from-action>, then implicit navigation

The same rules, but the from-action is always null, so rules with a <from-action> never match

Use for

Operations that change something

Links, menus, "back" and "details" buttons, bookmarkable pages

The specification defines the second kind as preemptive navigation: the renderer asks the navigation handler, at render time, which case an outcome would select, and writes the resulting URL into the page. Both kinds go through the same rules, and both can be redirected or forwarded where it makes sense; the sections below treat the POST kind first and GET Navigation: h:link and h:button afterwards.

Navigation in Bookshelf

The running example is the Bookshelf catalogue. A reader opens a book, borrows it, and ends up on a confirmation page, on a waiting list when no copy is free, or on a "loan limit" page. The diagram shows the outcomes used on this page. Edges that are GET links are labelled with the component; the others are outcomes of actions, with the way they are decided.

flowchart LR catalogue["/catalogue.xhtml"] details["/details.xhtml?id=n"] borrow["/borrow.xhtml?id=n"] confirmation["/confirmation.xhtml?id=n"] waitlist["/waitlist.xhtml?id=n"] limit["/limit.xhtml"] notfound["/notfound.xhtml"] help["/help.xhtml"] catalogue -- "h:link outcome details (GET)" --> details catalogue -- "borrow?faces-redirect=true (implicit, redirect)" --> borrow details -- "h:link outcome catalogue (GET)" --> catalogue details -- "h:link outcome borrow with includeViewParams (GET)" --> borrow details -- "viewAction returns notfound (implicit, redirect)" --> notfound borrow -- "success (rule, redirect)" --> confirmation borrow -- "waitlist (rule, redirect)" --> waitlist borrow -- "no outcome and blocked member (conditional rule)" --> limit borrow -- "help (global rule, immediate button)" --> help confirmation -- "home (global rule, redirect)" --> catalogue

The views are plain XHTML files in src/main/webapp. The ones that only show a heading (limit.xhtml, notfound.xhtml, help.xhtml, help-admin.xhtml and help-panel.xhtml) are not listed. The others are listed where they first matter. They use the packages org.example.bookshelf.model, .service and .web of the project layout described in the project setup page. The service the examples rely on is small:

src/main/java/org/example/bookshelf/service/BookService.java
package org.example.bookshelf.service;

import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
import org.example.bookshelf.model.Book;

@ApplicationScoped
public class BookService {

    private final List<Book> books = List.of(
        new Book(1, "Pride and Prejudice", "Jane Austen", 1813),
        new Book(2, "Emma", "Jane Austen", 1815),
        new Book(3, "Frankenstein", "Mary Shelley", 1818),
        new Book(4, "Jane Eyre", "Charlotte Bronte", 1847));

    /** Copies in stock per book id; book 3 has none, so a loan goes to the waiting list. */
    private final Map<Long, Integer> stock = new ConcurrentHashMap<>(Map.of(1L, 2, 2L, 1, 3L, 0, 4L, 1));

    public List<Book> findAll() {
        return books;
    }

    public Optional<Book> findById(long id) {
        return books.stream().filter(b -> b.id() == id).findFirst();
    }

    public boolean hasCopy(long id) {
        return stock.getOrDefault(id, 0) > 0;
    }

    public void lend(long id) {
        stock.computeIfPresent(id, (k, v) -> Math.max(0, v - 1));
    }
}

with the model class public record Book(long id, String title, String author, int year) implements Serializable {} from the earlier pages. Book 3 has no copy, which is how the waiting-list outcome is reached.

Producing an Outcome

Four shapes of action cover almost everything:

Written on the page What the handler receives Notes

action="borrow"

from-action borrow, outcome borrow

A literal. No bean is involved; useful for links such as "Help" that always go to the same place.

action="#{borrowBean.submit}"

from-action #{borrowBean.submit}, outcome the returned string

The usual form. The method returns String; null or an empty string means "stay".

action="#{catalogueBean.borrow(book)}"

from-action the whole expression text, outcome the returned string

A method with parameters, resolved by the expression language; the row object of a ui:repeat or h:dataTable is passed directly, with no f:setPropertyActionListener needed.

action="#{bean.doSomething}" on a void method

outcome null

The same view is shown again, unless a conditional rule applies (Conditional Navigation).

An outcome is a view reference, not a URL, and not a place for business logic. Keep the decision in the bean (the member is blocked, no copy is free) and keep the consequence either in the outcome string (implicit navigation) or in the rules. The two beans below are the ones used in the following sections:

src/main/java/org/example/bookshelf/web/CatalogueBean.java
package org.example.bookshelf.web;

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;

@Named
@RequestScoped
public class CatalogueBean {

    @Inject
    private BookService books;

    private List<Book> list;

    public List<Book> getBooks() {
        if (list == null) {
            list = books.findAll();
        }
        return list;
    }

    /** Implicit navigation with a redirect and a query string built in the outcome. */
    public String borrow(Book book) {
        return "borrow?faces-redirect=true&id=" + book.id();
    }
}
src/main/java/org/example/bookshelf/web/BorrowBean.java
package org.example.bookshelf.web;

import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import org.example.bookshelf.service.BookService;

@Named
@ViewScoped
public class BorrowBean implements Serializable {

    @Inject
    private BookService books;

    private Long id;
    private String member;
    private boolean blocked;

    /** Returns "success" or "waitlist", or null (no outcome) when the member may not borrow. The member named "blocked" stands in for a real check such as a loan limit. */
    public String submit() {
        if (id == null) {
            return "home";
        }
        if ("blocked".equals(member)) {
            blocked = true;
            return null;
        }
        if (books.hasCopy(id)) {
            books.lend(id);
            FacesContext context = FacesContext.getCurrentInstance();
            context.addMessage(null, new FacesMessage("Enjoy the book, " + member));
            context.getExternalContext().getFlash().setKeepMessages(true);
            return "success";
        }
        return "waitlist";
    }

    public boolean isBlocked() {
        return blocked;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getMember() {
        return member;
    }

    public void setMember(String member) {
        this.member = member;
    }
}

BorrowBean is view scoped, so the book id read from the URL (a view parameter, see GET Navigation: h:link and h:button and the view parameters page) is still there when the form is posted back. A request-scoped bean would lose it, because the postback goes to /borrow.xhtml without a query string. getFlash().setKeepMessages(true) is explained in What a Redirect Costs.

Implicit Navigation

When no rule matches, the handler tries implicit navigation: it treats the outcome as the name of a view. This is what makes the simplest applications work without any navigation configuration, and it is the first thing to reach for. The specification describes the steps in this order:

  1. If the outcome contains a ?, the query string is split off. A faces-redirect=true in it makes the navigation a redirect; includeViewParams=true (or faces-include-view-params=true) asks for the view parameters of the target to be added to the redirect URL.

  2. If the remaining text has no file extension, the extension of the current view is added.

  3. If it does not start with /, it is resolved against the folder of the current view.

  4. The view handler’s deriveViewId is asked whether such a view exists. If it does not, there is no implicit match.

The catalogue uses it for the Borrow button. Its h:link is GET navigation and is covered in GET Navigation: h:link and h:button:

src/main/webapp/catalogue.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets">
<h:head>
  <title>Bookshelf catalogue</title>
</h:head>
<h:body>
  <h1>Catalogue</h1>
  <h:messages id="msgs" globalOnly="true"/>
  <h:form id="list">
    <ui:repeat id="rows" var="book" value="#{catalogueBean.books}">
      <div>
        <h:link id="details" value="#{book.title}" outcome="details">
          <f:param name="id" value="#{book.id}"/>
        </h:link>
        <h:commandButton id="borrow" value="Borrow" action="#{catalogueBean.borrow(book)}"/>
      </div>
    </ui:repeat>
  </h:form>
</h:body>
</html>

Pressing Borrow for the second book sent POST /bookshelf/catalogue.xhtml and the bean returned borrow?faces-redirect=true&id=2. The response was a redirect (the rest of the headers omitted):

HTTP/1.1 302
Location: /bookshelf/borrow.xhtml?id=2

The Forms an Outcome Can Take

All of these were run from the pages of the example; the relative forms from a view in a sub-folder, /admin/panel.xhtml:

Outcome Resolved view Observed

borrow (from /catalogue.xhtml)

/borrow.xhtml (extension of the current view)

Forward: the response to the POST to /catalogue.xhtml contained the borrow page, so the browser’s URL stays on the catalogue.

reports (from /admin/panel.xhtml)

/admin/reports.xhtml

Relative to the folder of the current view.

reports.xhtml

/admin/reports.xhtml

An explicit extension is accepted.

/catalogue

/catalogue.xhtml

A leading / starts at the web application root.

borrow?faces-redirect=true

/borrow.xhtml

Redirect, Location: /bookshelf/borrow.xhtml.

borrow?faces-redirect=true&id=2

/borrow.xhtml

Redirect with the query string kept: Location: /bookshelf/borrow.xhtml?id=2.

waitlist?faces-redirect=true&includeViewParams=true

/waitlist.xhtml

Redirect, with the view parameters declared by waitlist.xhtml added (?id=3). Same result on both implementations.

waitlist?faces-redirect=true&faces-include-view-params=true

/waitlist.xhtml

MyFaces added ?id=3. Mojarra 4.1.17 answered Location: /bookshelf/waitlist.xhtml?faces-include-view-params=true: it left the flag in the query string and added nothing. Use includeViewParams=true.

../catalogue (from /admin/panel.xhtml)

not defined

Mojarra: no match, the panel was redisplayed. MyFaces: navigated to /catalogue.xhtml. Do not rely on ...

The sub-folder view that these rows use has one button for each form:

src/main/webapp/admin/panel.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets">
<h:head><title>Panel</title></h:head>
<h:body>
<h1>Admin panel</h1>
<h:form id="f">
  <h:commandButton id="rel" value="rel" action="reports"/>
  <h:commandButton id="relext" value="relext" action="reports.xhtml"/>
  <h:commandButton id="abs" value="abs" action="/catalogue"/>
  <h:commandButton id="up" value="up" action="../catalogue"/>
  <h:commandButton id="help" value="help" action="help"/>
</h:form>
</h:body>
</html>

The second admin view, /admin/reports.xhtml, is a heading and a button with action="help" and the id help.

In an XHTML attribute the & of a query string has to be written &amp;; returned from a Java method it is a plain &. Explicit rules win over implicit navigation: with a rule that maps the outcome limit to /help.xhtml and a limit.xhtml file next to it, the outcome went to the help page on both implementations.

An Outcome That Matches Nothing

When neither a rule nor a view matches, the same view is shown again and nothing else happens. That is also what a typo does. To make the mistake visible, Faces adds a message unless the project stage is Production. A button with action="nosuchpage", added to the catalogue for the experiment, gave:

Implementation Message (project stage Development)

Mojarra 4.1.17

Unable to find matching navigation case with from-view-id '/catalogue.xhtml' for action 'nosuchpage' with outcome 'nosuchpage'

MyFaces 4.1.4

No navigation case match for viewId /catalogue.xhtml, action nosuchpage and outcome nosuchpage

The message is a global one, so an h:messages globalOnly="true" on the page (the catalogue has one) shows it, and in Development the implementation prints unhandled messages at the end of the page anyway. With the stage set to Production the page was redisplayed with no message at all on both implementations. The empty string is treated as null by the specification; Mojarra stayed silent, MyFaces added the "no match" message in Development.

Redirects and Post/Redirect/Get

A navigation case is either a forward or a redirect. After a forward the new view is rendered in the response to the same POST. The browser’s address bar still shows the old URL, and a reload asks the user to resubmit the form, which would borrow the book twice. After a redirect the server answers the POST with a 302 and the browser fetches the new URL with a GET. This is the Post/Redirect/Get pattern, and it should be the default for every action that changes something.

A redirect is requested in one of three ways:

  • faces-redirect=true in an implicit outcome (borrow?faces-redirect=true);

  • a <redirect/> element in an explicit <navigation-case> (Rule-Based Navigation in faces-config.xml);

  • automatically for GET navigation and for a view action that moves to another view (the specification treats that case as if <redirect/> had been given).

What the implementation does for a redirect follows the specification: it asks the view handler for the redirect URL (ViewHandler.getRedirectURL), marks the flash as a redirect, sends the redirect and calls responseComplete(). Because the response is already complete, nothing in Render Response runs for the old view.

What a Redirect Costs

The new request builds a new view. Request-scoped beans and the view-scoped beans of the old view are gone, and so are the messages added with FacesContext.addMessage. The BorrowBean above adds an "Enjoy the book" message and then returns success. Measured on both implementations, with the confirmation page printing h:messages:

Mojarra 4.1.17 MyFaces 4.1.4

Without setKeepMessages(true)

<h1>Loan confirmed</h1> and an empty message list

the same

With getFlash().setKeepMessages(true) before returning

The message Enjoy the book, eve shown on the confirmation page

the same

The flash is the small store that survives exactly one redirect; the flash and Post/Redirect/Get page covers it. Anything else the next page needs travels in the URL: a query string in the outcome, a <redirect-param>, or a view parameter.

A nested f:param on the command component is not a way to add to the redirect URL. With <h:commandButton action="confirmation?faces-redirect=true&amp;id=1"><f:param name="id" value="7"/>... the redirect URL on both implementations was still /bookshelf/confirmation.xhtml?id=1: the f:param children of a command component travel as request parameters of the POST, not into the redirect.

Rule-Based Navigation in faces-config.xml

Rules move the decision out of the pages and the beans into WEB-INF/faces-config.xml. They are worth the indirection in three situations: one outcome has to lead to different views depending on where it was produced, the page must not know where the flow goes next, or the same outcome (home, login, help) must work from every page.

Element Meaning

<navigation-rule>

One rule per source view pattern. Several rules with the same pattern are treated as one.

<from-view-id>

The current view: an exact view id (/borrow.xhtml), a prefix ending in * (/admin/*) or just *. The schema of 4.1 declares the element without minOccurs, so a validating editor flags a rule without it, although the specification says that a missing element matches every view and both implementations accepted a rule without it. Write *.

<navigation-case>

One possible target of the rule.

<from-action>

The action expression text, compared with the from-action of the call.

<from-outcome>

The outcome, compared with the returned string.

<if>

A single value expression that must evaluate to true (Conditional Navigation).

<to-view-id>

The target view id, or a value expression that evaluates to one. In a flow, also a node id.

<redirect>

Makes the case a redirect. The include-view-params attribute adds the view parameters of the target; <redirect-param> children (<name> and <value>, the value may be an expression) add query parameters.

<to-flow-document-id>

The document that defines a flow entered by the case (see the Faces Flows page).

The elements of a case appear in this order in the schema: from-action, from-outcome, if, to-view-id, to-flow-document-id, redirect. The Bookshelf configuration, complete:

src/main/webapp/WEB-INF/faces-config.xml
<?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>
    <navigation-handler>org.example.bookshelf.web.AuditingNavigationHandler</navigation-handler>
  </application>

  <navigation-rule>
    <from-view-id>/borrow.xhtml</from-view-id>
    <navigation-case>
      <from-action>#{borrowBean.submit}</from-action>
      <from-outcome>success</from-outcome>
      <to-view-id>/confirmation.xhtml</to-view-id>
      <redirect>
        <redirect-param>
          <name>id</name>
          <value>#{borrowBean.id}</value>
        </redirect-param>
      </redirect>
    </navigation-case>
    <navigation-case>
      <from-outcome>waitlist</from-outcome>
      <to-view-id>/waitlist.xhtml</to-view-id>
      <redirect>
        <redirect-param>
          <name>id</name>
          <value>#{borrowBean.id}</value>
        </redirect-param>
      </redirect>
    </navigation-case>
    <navigation-case>
      <from-action>#{borrowBean.submit}</from-action>
      <if>#{borrowBean.blocked}</if>
      <to-view-id>/limit.xhtml</to-view-id>
    </navigation-case>
  </navigation-rule>

  <navigation-rule>
    <from-view-id>*</from-view-id>
    <navigation-case>
      <from-outcome>home</from-outcome>
      <to-view-id>/catalogue.xhtml</to-view-id>
      <redirect/>
    </navigation-case>
    <navigation-case>
      <from-outcome>help</from-outcome>
      <to-view-id>/help.xhtml</to-view-id>
    </navigation-case>
  </navigation-rule>

  <navigation-rule>
    <from-view-id>/admin/*</from-view-id>
    <navigation-case>
      <from-outcome>help</from-outcome>
      <to-view-id>/help-admin.xhtml</to-view-id>
    </navigation-case>
  </navigation-rule>

  <navigation-rule>
    <from-view-id>/admin/panel.xhtml</from-view-id>
    <navigation-case>
      <from-outcome>help</from-outcome>
      <to-view-id>/help-panel.xhtml</to-view-id>
    </navigation-case>
  </navigation-rule>
</faces-config>

The page that triggers the three /borrow.xhtml rules is the borrow form. It receives the book id through a view parameter (the metadata block) and has one normal button and one immediate Help button:

src/main/webapp/borrow.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core">
<f:metadata>
  <f:viewParam name="id" value="#{borrowBean.id}"/>
</f:metadata>
<h:head>
  <title>Borrow a book</title>
</h:head>
<h:body>
  <h1>Borrow book <h:outputText value="#{borrowBean.id}"/></h1>
  <h:form id="f">
    <h:outputLabel for="member" value="Member"/>
    <h:inputText id="member" value="#{borrowBean.member}" required="true"/>
    <h:message for="member"/>
    <h:commandButton id="go" value="Borrow" action="#{borrowBean.submit}"/>
    <h:commandButton id="ajax" value="Borrow (Ajax)" action="#{borrowBean.submit}">
      <f:ajax execute="@form" render="@form"/>
    </h:commandButton>
    <h:commandButton id="help" value="Help" action="help" immediate="true"/>
  </h:form>
</h:body>
</html>

The Ajax button is explained in Navigation and Ajax. In the rules above, success is reached only from the submit method of borrowBean (both from-action and from-outcome are given), waitlist is accepted from any action on this view, and the third case has no outcome at all (Conditional Navigation). The redirect-param puts the id of the book on the confirmation and the waiting-list URL (Location: /bookshelf/confirmation.xhtml?id=2 and /bookshelf/waitlist.xhtml?id=3 in the runs), and a page that wants it declares a view parameter or reads #{param.id}. Submitting the form with a member name of ann for book 2 (one copy) gave the confirmation, a second submission for the same book (bob) the waiting list, and a member called blocked the limit page, on both implementations.

How Rules and Cases Are Chosen

The handler first selects the rules whose <from-view-id> matches the current view, in this order: an exact match, then prefix patterns ending in * (the longest prefix first), then the pattern * or a rule without <from-view-id>. The first rule that contains a matching case wins; if a rule has none, the next one is tried.

Inside one rule, the cases are tried in this order:

  1. cases with both <from-action> and <from-outcome>, which must both be non-null and equal to the call’s values;

  2. cases with only <from-outcome>, which must be non-null and equal to the outcome;

  3. cases with only <from-action>; without an <if> they match only a non-null outcome, with an <if> any outcome;

  4. any remaining case, which matches when the outcome is non-null or an <if> is present.

A case that matched up to here and has an <if> must also see its condition evaluate to true. The configuration above has three rules for the outcome help, which shows the first ordering:

Pressing "help" on Rule used View shown

/borrow.xhtml

*

/help.xhtml (global)

/admin/reports.xhtml

/admin/*

/help-admin.xhtml

/admin/panel.xhtml

exact /admin/panel.xhtml

/help-panel.xhtml

If no rule and no implicit view matches, the algorithm ends with the "same view again" behaviour of the previous section.

flowchart TD A["handleNavigation with fromAction and outcome"] --> B{"A rule matches the current view?"} B -- "yes, next rule in the order" --> C{"A case in it matches action, outcome and if?"} C -- "yes" --> D["Target view is to-view-id, or the implicit view"] C -- "no" --> B B -- "no rule left" --> E{"Does the outcome name an existing view?"} E -- "yes" --> D E -- "no" --> F["Redisplay the current view, with a message unless Production"] D --> G{"Redirect?"} G -- "yes" --> H["302 to the URL, responseComplete"] G -- "no" --> I["New view root, then Render Response"]

h:link and h:button consult the same rules, but they have no action, so the from-action is null. The first case of the borrow rule needs a from-action, therefore a <h:link outcome="success"/> added to the borrow page for the experiment was rendered as a disabled span on both implementations, while <h:link outcome="waitlist"/>, whose case has no <from-action>, became a normal anchor, and the expression of its <redirect-param> was evaluated at render time (.../waitlist.xhtml?id=3 for book 3, on both implementations). When a case must work from a link, leave <from-action> out of it.

include-view-params in a Rule

<redirect include-view-params="true"/> is what the 4.1 schema defines. In a run with such a case for the outcome waitlist, Mojarra 4.1.17 redirected to /bookshelf/waitlist.xhtml?id=3 and MyFaces 4.1.4 to /bookshelf/waitlist.xhtml. MyFaces reported isIncludeViewParams() as false for the case. Reading its configuration reader showed that it looks for a child element <include-view-params>true</include-view-params> inside <redirect>, which is not in the 4.1 schema, and the run confirmed that this form worked on MyFaces (and was ignored by Mojarra). The portable options are a <redirect-param> with an expression, as the example does, or an implicit outcome with includeViewParams=true.

Expressions in to-view-id and redirect-param

<to-view-id> may be an expression, evaluated when the case is used. With <to-view-id>/#{borrowBean.destination}.xhtml</to-view-id> and a getter returning limit, the conditional case went to /limit.xhtml on both implementations. Use this sparingly: the rule file stops being a readable map of the application. The value of a <redirect-param> is an expression too, as the #{borrowBean.id} above shows.

Conditional Navigation

The <if> element makes a case depend on the state of the application. It was added in Faces 2.0. The third case of the borrow rule has no outcome:

<navigation-case>
  <from-action>#{borrowBean.submit}</from-action>
  <if>#{borrowBean.blocked}</if>
  <to-view-id>/limit.xhtml</to-view-id>
</navigation-case>

BorrowBean.submit() returns null for a blocked member and sets blocked. Without <if>, a case without an outcome never matches a null outcome. With <if>, the specification treats the condition as "an alternate, contextual means of obtaining a logical outcome", so the case is evaluated even when the action returned null or was a void method. Both implementations showed the limit page for the member blocked. The log of the auditing handler of NavigationHandler and ConfigurableNavigationHandler records it:

NAV /borrow.xhtml action=#{borrowBean.submit} outcome=null -> /limit.xhtml redirect=false

Rules to remember:

  • The content of <if> must be a single value expression that resolves to a boolean. The specification says that content that does not meet this requirement is ignored.

  • A case with an <if> must also match on its <from-action> and <from-outcome>, if it has them; <if> adds a condition, it does not replace the other tests.

  • Two cases that have the same combination of from-action and from-outcome in rules with the same <from-view-id> are an error unless each has its own <if>.

  • Cases that name no outcome are tried after those that do, so a conditional case does not shadow the cases with an explicit outcome.

  • The Javadoc of NavigationHandler says that a null outcome never matches any rule. That sentence predates conditional navigation; the specification and the runs above contradict it for cases with an <if>.

Conditions are evaluated during Invoke Application, after the model was updated, so they see the values that the user just submitted. Keep them cheap and free of side effects, because the handler can evaluate them more than once. A counter in isBlocked() saw one evaluation for one navigation on Mojarra and five on MyFaces; a decorator that also calls getNavigationCase (Decorating the Handler) added one more on each.

GET Navigation: h:link and h:button

h:link renders an anchor and h:button renders <input type="button"> whose onclick sets window.location.href. Neither submits a form, neither needs JavaScript from Faces for the link variant, and neither creates a postback. The target is computed while the page is rendered: the renderer takes outcome, applies the rules and implicit navigation exactly as for an action, and writes the URL of the resulting view. The details page of the example:

src/main/webapp/details.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core">
<f:metadata>
  <f:viewParam name="id" value="#{bookDetailsBean.id}"/>
  <f:viewAction action="#{bookDetailsBean.load}"/>
</f:metadata>
<h:head>
  <title>Book details</title>
</h:head>
<h:body>
  <h1><h:outputText value="#{bookDetailsBean.book.title}"/></h1>
  <p>by <h:outputText value="#{bookDetailsBean.book.author}"/>, <h:outputText value="#{bookDetailsBean.book.year}"/></p>

  <h:link id="back" value="Back to the catalogue" outcome="catalogue"/>
  <h:link id="borrow" value="Borrow this book" outcome="borrow" includeViewParams="true"/>
  <h:button id="again" value="Reload" outcome="details" includeViewParams="true"/>
</h:body>
</html>

Rendered by Mojarra for details.xhtml?id=2:

<a id="back" name="back" href="/bookshelf/catalogue.xhtml">Back to the catalogue</a>
<a id="borrow" name="borrow" href="/bookshelf/borrow.xhtml?id=2">Borrow this book</a>
<input id="again" type="button" onclick="window.location.href='/bookshelf/details.xhtml?id=2'; return false;" value="Reload">

MyFaces produced the same URLs with a different attribute order. The catalogue’s h:link with a nested f:param (<f:param name="id" value="#{book.id}"/>) rendered /bookshelf/details.xhtml?id=1.

Attribute Meaning

outcome

The logical outcome used to resolve a navigation case. Implicit navigation works here too, so outcome="catalogue" needs no rule. A ?faces-redirect=true is unnecessary: GET navigation always produces a URL.

includeViewParams

Whether to include view parameters in the target URL (below).

fragment

The identifier of a page fragment, appended after a #. outcome="catalogue" fragment="top" gave /bookshelf/catalogue.xhtml#top.

disabled

true renders a span (for h:link) instead of the anchor, or a disabled button.

value

The text of the link or button. A h:button also has image.

disableClientWindow

Do not append the ClientWindow identifier to this URL (relevant when client windows are enabled).

nested f:param

Adds query parameters, URL-encoded by the renderer.

The common attributes (styleClass, rel, target, accesskey and so on) are the usual ones; the HTML components page lists them.

An Outcome That Cannot Be Resolved

A link must never point nowhere, so an outcome with no match makes the component disabled. <h:link outcome="doesnotexist"/> was rendered as <span id="missing">Missing outcome</span> (Mojarra, in every project stage except Production, added the explanation "This link is disabled because a navigation case could not be matched." inside the span; MyFaces printed the text only), and a h:button is rendered disabled. In Development both implementations also queue a message that names the view and the outcome. The same span appears in Production, without the explanation (see the project stage effects).

includeViewParams

Query parameters of a link have three sources. When the URL is built, they are consulted in this order, and a later source replaces the same name from an earlier one: the outcome (a query string in an implicit outcome), the view parameters, and the nested f:param children.

includeViewParams="true" adds the view parameters declared by the target view (its f:metadata and f:viewParam elements), filling each from the parameter of the same name in the current view. The target matters: the details page links to /borrow.xhtml, which declares f:viewParam name="id", so the link got ?id=2. An implicit outcome with includeViewParams=true that targets the confirmation page, which declares no view parameter, produced no id at all. Further findings from the runs:

  • The values are re-read at render time, so a link rendered after a failed postback (the borrow form with an empty member name) still contained ?id=3 on both implementations.

  • An f:param with the same name as a view parameter wins: <h:link outcome="borrow" includeViewParams="true"><f:param name="id" value="99"/></h:link> rendered /bookshelf/borrow.xhtml?id=99 while the page itself had id=3.

  • The default is false, as for the <redirect> element of a rule.

View parameters, conversion and validation of query parameters and f:viewAction have their own page.

h:outputLink renders an anchor for a value that you write yourself. It has no outcome, so it does not consult any navigation case and knows nothing about view parameters. Use it for addresses outside the application. For pages inside it, h:link keeps the URL consistent with the rules, so renaming a page or changing a rule changes every link. h:commandLink is the POST version; it requires JavaScript, cannot be opened in a new tab and should not be used for plain navigation.

A GET must be safe. A link is followed by crawlers and prefetchers and anyone who has a copy of the URL, so nothing may be deleted, lent or approved from an h:link or h:button; use a command button, which is a POST.

Navigating from Code

The right way to navigate is to return an outcome from the action. Some situations are not actions: a listener that finds the session has expired, a system event handler, a bean method called from several places. For those, the same machinery is available in three layers.

src/main/java/org/example/bookshelf/web/NavBean.java
package org.example.bookshelf.web;

import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.application.ConfigurableNavigationHandler;
import jakarta.faces.application.NavigationCase;
import jakarta.faces.context.FacesContext;
import jakarta.inject.Named;
import java.io.IOException;
import java.util.HashSet;

@Named
@RequestScoped
public class NavBean {

    private String target;

    /** Navigates from code, with the same outcome syntax as an action method. */
    public void goTo(String outcome) {
        FacesContext context = FacesContext.getCurrentInstance();
        ConfigurableNavigationHandler navigation =
                (ConfigurableNavigationHandler) context.getApplication().getNavigationHandler();
        navigation.performNavigation(outcome);
    }

    /** Sends a raw redirect; no navigation case is involved. */
    public void redirectTo() throws IOException {
        FacesContext.getCurrentInstance().getExternalContext().redirect(target);
    }

    /** Adds a navigation case at run time. */
    public void register() {
        FacesContext context = FacesContext.getCurrentInstance();
        ConfigurableNavigationHandler navigation =
                (ConfigurableNavigationHandler) context.getApplication().getNavigationHandler();
        navigation.getNavigationCases()
                .computeIfAbsent("/nav.xhtml", from -> new HashSet<>())
                .add(new NavigationCase("/nav.xhtml", null, "dyn", null, "/help-panel.xhtml", null, false, false));
    }

    /** Anti-example: the outcome comes straight from user input. */
    public String follow() {
        return target;
    }

    public String getTarget() {
        return target;
    }

    public void setTarget(String target) {
        this.target = target;
    }
}
src/main/webapp/nav.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html">
<h:head>
  <title>Navigation lab</title>
</h:head>
<h:body>
  <h:form id="f">
    <h:inputText id="target" value="#{navBean.target}"/>
    <h:commandButton id="perform" value="performNavigation" action="#{navBean.goTo(navBean.target)}"/>
    <h:commandButton id="redirect" value="redirect" action="#{navBean.redirectTo}"/>
    <h:commandButton id="register" value="Register" action="#{navBean.register}"/>
    <h:commandButton id="follow" value="Follow" action="#{navBean.follow}"/>
    </h:form>
</h:body>
</html>
  • ConfigurableNavigationHandler.performNavigation(String outcome) navigates as if an action had returned the outcome. The current view is the "from view" and the from-action is null. With the target home the response was the redirect of the global rule (Location: /bookshelf/catalogue.xhtml); with /admin/panel it forwarded to the panel. The default action listener still reports the void method’s null outcome afterwards, and that did not undo the navigation.

  • NavigationHandler.handleNavigation(context, fromAction, outcome) is the lower-level call that performNavigation stands for. Use it only if you need to supply a from-action.

  • ExternalContext.redirect(String url) sends a raw redirect and calls responseComplete(). No rule and no view existence check are involved, so it is the right tool only for addresses outside the navigation model. Its Javadoc says that the URL must be absolute and throws IllegalArgumentException for a relative one; both implementations accepted catalogue.xhtml and sent Location: catalogue.xhtml unchanged, which a browser resolves against the current URL. For an Ajax request the implementation writes a partial-response with a redirect element instead of a 302.

OmniFaces offers a convenience for the last case, Faces.redirect(String url, Object... paramValues), which prepends the context path and URL-encodes the %s placeholders; its documentation is in the references.

Do not call the navigation handler from an ExceptionHandler during Render Response: since Faces 2.3 the specification explicitly prohibits it. The exception handling page shows a safe way to turn an error into a redirect.

Never Build an Outcome from User Input

NavBean.follow() returns whatever the user typed. It is the anti-example of the page: on both implementations the value /admin/panel navigated to the admin panel. An outcome is a view reference, so user input can reach any view of the application, including the ones that were never linked, and ExternalContext.redirect with a user-supplied address is an open redirect (https://example.org/ was accepted as is). Choose the outcome from a fixed set in code, validate a requested target against an allow-list, and protect the views that must be restricted with the container’s security constraints or Jakarta Security, not with the absence of a link. The security page develops it. A view under /WEB-INF was not reachable by an outcome (no match, on Mojarra).

NavigationHandler and ConfigurableNavigationHandler

The default handler is replaceable, and its public API is small:

Type and member Role

NavigationHandler.handleNavigation(FacesContext, String fromAction, String outcome)

Performs the navigation: sets a new view root, or completes the response with a redirect. A second overload takes the defining document id of a flow to enter.

ConfigurableNavigationHandler.getNavigationCase(FacesContext, String fromAction, String outcome)

Returns the NavigationCase that handleNavigation would use, or null. The handler must resolve both calls with the same logic. An overload adds the flow document id.

ConfigurableNavigationHandler.getNavigationCases()

A Map<String, Set<NavigationCase>> from view-id pattern to the cases of that rule. The implementation must support live modifications of the map.

ConfigurableNavigationHandler.performNavigation(String)

Navigates from code, as above.

NavigationHandlerWrapper, ConfigurableNavigationHandlerWrapper

Base classes for decorators. The constructor takes the handler being wrapped; the no-argument constructors are deprecated.

NavigationCase

One case: getFromViewId(), getFromAction(), getFromOutcome(), hasCondition(), getCondition(context), getToViewId(context), getToFlowDocumentId(), isRedirect(), isIncludeViewParams(), getParameters() and the URL builders getActionURL, getRedirectURL, getBookmarkableURL and getResourceURL.

A specification-compliant implementation must make its handler an extension of ConfigurableNavigationHandler, which is why the cast in the beans above is safe. ConfigurableNavigationHandler has existed since Faces 2.0.

Decorating the Handler

A new handler is registered in faces-config.xml as <application><navigation-handler>. Faces instantiates the class with a constructor that takes the handler that was installed before it, so decorators chain. Extend ConfigurableNavigationHandlerWrapper (not just NavigationHandlerWrapper) so that getNavigationCase, getNavigationCases and performNavigation keep working for the components that depend on them. This decorator logs each decision and delegates everything else:

src/main/java/org/example/bookshelf/web/AuditingNavigationHandler.java
package org.example.bookshelf.web;

import jakarta.faces.application.ConfigurableNavigationHandler;
import jakarta.faces.application.ConfigurableNavigationHandlerWrapper;
import jakarta.faces.application.NavigationCase;
import jakarta.faces.context.FacesContext;
import java.util.logging.Logger;

/** Logs every navigation decision, then lets the wrapped handler do the work. */
public class AuditingNavigationHandler extends ConfigurableNavigationHandlerWrapper {

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

    private final ConfigurableNavigationHandler wrapped;

    // Faces calls this constructor and passes the handler that was installed before this one.
    public AuditingNavigationHandler(ConfigurableNavigationHandler wrapped) {
        super(wrapped);
        this.wrapped = wrapped;
    }

    @Override
    public ConfigurableNavigationHandler getWrapped() {
        return wrapped;
    }

    @Override
    public void handleNavigation(FacesContext context, String fromAction, String outcome) {
        String from = context.getViewRoot() == null ? "?" : context.getViewRoot().getViewId();
        NavigationCase target = getNavigationCase(context, fromAction, outcome);
        if (target == null) {
            LOG.info("NAV " + from + " action=" + fromAction + " outcome=" + outcome + " -> no match, redisplay");
        } else {
            LOG.info("NAV " + from + " action=" + fromAction + " outcome=" + outcome + " -> "
                    + target.getToViewId(context) + " redirect=" + target.isRedirect());
        }
        super.handleNavigation(context, fromAction, outcome);
    }
}

The registration is the first element of the configuration shown in Rule-Based Navigation in faces-config.xml. The log of a session that pressed Borrow in the catalogue, submitted the form for book 2 twice (ann, bob), submitted it for a blocked member and pressed Help:

NAV /catalogue.xhtml action=#{catalogueBean.borrow(book)} outcome=borrow?faces-redirect=true&id=2 -> /borrow.xhtml redirect=true
NAV /borrow.xhtml action=#{borrowBean.submit} outcome=success -> /confirmation.xhtml redirect=true
NAV /borrow.xhtml action=#{borrowBean.submit} outcome=waitlist -> /waitlist.xhtml redirect=true
NAV /borrow.xhtml action=#{borrowBean.submit} outcome=null -> /limit.xhtml redirect=false
NAV /borrow.xhtml action=help outcome=help -> /help.xhtml redirect=false

Three details of the run matter when you write a decorator:

  • Calling getNavigationCase for an outcome that matches nothing queues the "no match" message of the Development stage, and handleNavigation queues it again. Both implementations showed the message twice for an unknown outcome with this handler, and once without it. Either leave the getNavigationCase call out of handleNavigation, or accept the duplicate in Development.

  • performNavigation called on the wrapper was delegated to the wrapped handler; the wrapper’s own handleNavigation override was not called for it (the log has no line for the home navigation of NavBean).

  • An @Inject field of the handler was null on Mojarra and set on MyFaces. A decorator should not depend on injection; the result depends on the class (the extending Faces page observed null on both implementations for a ViewHandler decorator and a difference between them for an ELResolver), so use CDI.current() when a bean is needed.

Inspecting and Changing the Rules

getNavigationCases() is a live view of the rule base, which makes two things easy: a navigation map for documentation or tests, and rules created in code. The bean below lists every configured case:

src/main/java/org/example/bookshelf/web/SiteMapBean.java
package org.example.bookshelf.web;

import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.application.ConfigurableNavigationHandler;
import jakarta.faces.application.NavigationCase;
import jakarta.faces.context.FacesContext;
import jakarta.inject.Named;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;

@Named
@RequestScoped
public class SiteMapBean {

    public record Row(String from, String action, String outcome, boolean conditional, String to, boolean redirect) {
    }

    public List<Row> getRows() {
        FacesContext context = FacesContext.getCurrentInstance();
        ConfigurableNavigationHandler navigation =
                (ConfigurableNavigationHandler) context.getApplication().getNavigationHandler();
        List<Row> rows = new ArrayList<>();
        for (Map.Entry<String, Set<NavigationCase>> rule : navigation.getNavigationCases().entrySet()) {
            for (NavigationCase navigationCase : rule.getValue()) {
                rows.add(new Row(rule.getKey(), navigationCase.getFromAction(), navigationCase.getFromOutcome(),
                        navigationCase.hasCondition(), navigationCase.getToViewId(context),
                        navigationCase.isRedirect()));
            }
        }
        // The order of the cases inside a rule is not defined, so sort before showing them.
        rows.sort(Comparator.comparing(Row::from)
                .thenComparing(Row::outcome, Comparator.nullsFirst(Comparator.naturalOrder()))
                .thenComparing(Row::to));
        return rows;
    }
}
src/main/webapp/sitemap.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core">
<h:head>
  <title>Navigation map</title>
</h:head>
<h:body>
  <h:dataTable id="rules" var="row" value="#{siteMapBean.rows}">
    <h:column><f:facet name="header">From view</f:facet>#{row.from}</h:column>
    <h:column><f:facet name="header">Action</f:facet>#{row.action}</h:column>
    <h:column><f:facet name="header">Outcome</f:facet>#{row.outcome}</h:column>
    <h:column><f:facet name="header">Condition</f:facet>#{row.conditional}</h:column>
    <h:column><f:facet name="header">To view</f:facet>#{row.to}</h:column>
    <h:column><f:facet name="header">Redirect</f:facet>#{row.redirect}</h:column>
  </h:dataTable>
</h:body>
</html>

On both implementations the page printed this table (the conditional case shows true in the Condition column; the from-action and the outcome of a case may be null):

From view Action Outcome Condition To view Redirect

*

help

false

/help.xhtml

false

*

home

false

/catalogue.xhtml

true

/admin/*

help

false

/help-admin.xhtml

false

/admin/panel.xhtml

help

false

/help-panel.xhtml

false

/borrow.xhtml

#{borrowBean.submit}

true

/limit.xhtml

false

/borrow.xhtml

#{borrowBean.submit}

success

false

/confirmation.xhtml

true

/borrow.xhtml

waitlist

false

/waitlist.xhtml

true

The bean sorts the rows. The order of the cases inside a rule is not defined: before sorting, Mojarra listed them in declaration order and MyFaces in another order. NavBean.register() adds a case in code with the NavigationCase constructor (fromViewId, fromAction, fromOutcome, condition, toViewId, parameters, redirect, includeViewParams). After pressing that button, the outcome dyn entered in the lab page navigated to /help-panel.xhtml on both implementations, and before it the same outcome matched nothing. Register such cases once during startup rather than from a request: the rule base belongs to the application, not to the user who happened to press the button.

Navigation and Ajax

An Ajax request goes through the same handler, but the response is not a page. The borrow form has a button with f:ajax:

<h:commandButton id="ajax" value="Borrow (Ajax)" action="#{borrowBean.submit}">
  <f:ajax execute="@form" render="@form"/>
</h:commandButton>

With a real browser (Mojarra) the click on this button ended on /bookshelf/confirmation.xhtml?id=2. What the server sent in the two cases:

Navigation partial-response

Redirect (success)

<partial-response><redirect url="/bookshelf/confirmation.xhtml?id=1"></redirect></partial-response> on both implementations. faces.js performs window.location to that URL, so the result is an ordinary full page load.

Forward (the conditional case, /limit.xhtml)

A single <update id="jakarta.faces.ViewRoot"> that contains the complete HTML of the new view, followed by the update of the view state. The URL in the address bar does not change.

The specification obliges the handler to set render-all on the PartialViewContext when the view id changes, which is why the forward replaces the whole document. Two practical rules follow: use a redirect for an action that changes data, as without Ajax, and do not expect the render list of the f:ajax to matter once the action navigates to another view. An Ajax request that does not navigate (null outcome) updates only what render names. The Ajax page covers f:ajax itself.

The bean behind the details page decides, in a view action, whether to navigate:

src/main/java/org/example/bookshelf/web/BookDetailsBean.java
package org.example.bookshelf.web;

import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.context.FacesContext;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.BookService;

@Named
@RequestScoped
public class BookDetailsBean {

    @Inject
    private BookService books;

    private Long id;
    private Book book;

    public String load() {
        if (id != null) {
            book = books.findById(id).orElse(null);
            if (book == null) {
                return "notfound";
            }
        }
        return null;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Book getBook() {
        return book;
    }
}

The other features that meet navigation:

  • View actions. An f:viewAction whose method returns an outcome navigates during the initial request. In the example the details page loads the book and returns notfound for an unknown id. The specification treats this as a redirect whenever the target view differs from the current one, and the runs agreed: a GET of details.xhtml?id=99 answered 302 with Location: /bookshelf/notfound.xhtml. With MyFaces 4.1.4, the same request failed with HTTP 500 and the exception "Cannot create a session after the response has been committed" thrown from its FlashImpl when the request carried no session yet. With an existing session the redirect worked on both. Whether an early session fixes it for every case was not tested. The details of f:viewParam and f:viewAction belong to the view parameters page.

  • Faces Flows. A flow call node or a flow identifier can be a target of <to-view-id>, <to-flow-document-id> names the document that defines the flow, and a return node resumes the caller’s navigation rules. Flows have their own page.

  • Validation and conversion errors prevent navigation, as described under Where Navigation Happens in the Lifecycle.

  • The flash carries messages and objects across one redirect (What a Redirect Costs).

  • Exception handling. A custom ExceptionHandler can redirect to an error page, but must not use the navigation handler during Render Response (exception handling page).

  • Security. Navigation is not authorization. A rule or a missing link does not restrict a view; see Never Build an Outcome from User Input and protected views.

  • Libraries. The PrimeFaces and OmniFaces pages cover the links, buttons and redirect helpers of those libraries.

The confirmation page prints the messages that survived the redirect, and waitlist.xhtml shows the target of a redirect that carries a view parameter:

src/main/webapp/confirmation.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html">
<h:head>
  <title>Confirmed</title>
</h:head>
<h:body>
  <h1>Loan confirmed</h1>
  <h:messages id="msgs" globalOnly="true"/>
  <p>Book <h:outputText value="#{param.id}"/> is yours.</p>
  <h:link value="Back to the catalogue" outcome="home"/>
</h:body>
</html>
src/main/webapp/waitlist.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core">
<f:metadata>
  <f:viewParam name="id" value="#{bookDetailsBean.id}"/>
  <f:viewAction action="#{bookDetailsBean.load}"/>
</f:metadata>
<h:head><title>Waitlist</title></h:head>
<h:body>
<h1>Waitlist: <h:outputText value="#{bookDetailsBean.book.title}"/></h1>
</h:body>
</html>

Mojarra and MyFaces

The core behaviour is the same on both: the rule and case order, implicit navigation, conditional cases, redirect-param with expressions, redirects, the Ajax partial-response forms, h:link and h:button URLs, performNavigation and the live getNavigationCases() map. Differences found in the same scratch application:

  • <redirect include-view-params="true"/> is honoured by Mojarra and ignored by MyFaces 4.1.4, which instead reads a child element <include-view-params> that the schema does not define (include-view-params in a Rule).

  • The implicit flag faces-include-view-params=true works on MyFaces and is left in the query string by Mojarra 4.1.17; includeViewParams=true works on both.

  • ../ in an implicit outcome navigates on MyFaces and does not on Mojarra.

  • The "no match" messages differ in wording (An Outcome That Matches Nothing), and MyFaces also reports the empty-string outcome.

  • A view action that redirects in a request without a session fails on MyFaces 4.1.4 (Navigation and the Rest of Faces).

  • An @Inject field of a navigation handler decorator is set on MyFaces and null on Mojarra (Decorating the Handler).

  • The order of the cases returned by getNavigationCases() differs (Inspecting and Changing the Rules).

  • The unresolved-outcome explanation inside the disabled h:link span appears only on Mojarra (in every stage except Production).

  • MyFaces accepts a postback only when its hidden formId_SUBMIT field is present; this matters only for scripted tests that post forms by hand, as described in the earlier pages.

What Changed

Version Change

Faces 1.x

Navigation rules in faces-config.xml are the mechanism of this era; the features listed under 2.0 did not exist yet.

Faces 2.0

h:link and h:button (UIOutcomeTarget), ConfigurableNavigationHandler and NavigationCase (the API documentation says Since: 2.0 for each). Implicit navigation, the <if> condition, includeViewParams and <redirect> with parameters date from the same release.

Faces 2.0 Rev a

The change log lists the errata: <view-param> inside <redirect> was renamed <redirect-param> (the old element is still accepted and the schema marks it deprecated), the implicit flag was named faces-include-view-params, and an empty outcome is treated like null.

Faces 2.2

Faces Flows add flow nodes as navigation targets, <to-flow-document-id>, the overloads of handleNavigation and getNavigationCase with the flow document id, NavigationHandlerWrapper and ConfigurableNavigationHandlerWrapper (all Since: 2.2 in the API documentation), and the algorithm accounts for UIViewAction.

Faces 2.3

The specification prohibits using the navigation handler from an ExceptionHandler invoked during Render Response.

Faces 3.0, 4.0, 4.1

No navigation change in the change lists. The package is jakarta.faces.application since 3.0, and the configuration schema namespace is https://jakarta.ee/xml/ns/jakartaee.

Where the Books Are Outdated

The books of the section’s bibliography that cover navigation (Burns and Schalk, Dudney and others, Mann, and Leonard for bookmarkable and pretty URLs) were written for Faces 1.x and 2.0. Their account of outcomes, rules, redirects and the navigation handler is still the right idea. Several details have changed:

  • Rules for everything. Material written before Faces 2.0 puts every transition into faces-config.xml. Today implicit navigation covers the common case, and rules remain for conditions, source-specific outcomes and global outcomes.

  • View ids with .jsp. Rules that name /page.jsp or rely on the .faces suffix describe JSP views, which were removed in Faces 4.0; the view ids are now .xhtml and the mapping is configured as in the project setup page.

  • Every link is a postback. Before h:link and h:button, bookmarkable pages were built with h:outputLink and hand-written URLs, or with a library such as PrettyFaces for pretty URLs. Use h:link, f:viewParam and includeViewParams, and a servlet-level rewrite only when the URL must look different.

  • <view-param>. The element inside <redirect> is named <redirect-param> today.

  • MethodBinding and NavigationHandler subclasses. Custom handlers that extend NavigationHandler directly miss getNavigationCase and the metadata that h:link needs; extend ConfigurableNavigationHandlerWrapper. MethodBinding was removed in Faces 4.0.

  • No Post/Redirect/Get by default. Older recipes forward after a POST, with the stale URL and the resubmission warning that go with it. Use faces-redirect=true or <redirect/> and the flash (three ways to redirect).

  • javax names. javax.faces.application.NavigationHandler and the configuration namespace http://java.sun.com/xml/ns/javaee are jakarta.faces.application.NavigationHandler and https://jakarta.ee/xml/ns/jakartaee today (migration page).

  • Struts-style navigation. Material that explains Faces navigation by comparison with Struts action mappings (the books that cover migration) is historical; there is no action-mapping layer in Faces.

Common Mistakes

  • Forwarding after a POST. The browser shows the old URL, and a reload resubmits. Use faces-redirect=true.

  • Expecting messages after a redirect. They are gone unless getFlash().setKeepMessages(true) was called before the redirect (What a Redirect Costs).

  • A typo in an outcome. The same view is redisplayed with no error in Production and only a development-time message otherwise. Keep outcome names in constants or enums, and a h:messages on the page during development.

  • A rule with <from-action> used from h:link. It never matches; the link is disabled (Rules and GET Navigation).

  • A rule that does not match its from-action. The handler compares the expression text as written on the page. Copy it from the page.

  • include-view-params in a rule. Mojarra and MyFaces read it differently; use <redirect-param> or the implicit includeViewParams=true.

  • Relying on the order of cases. Remember that the match order is fixed by the kind of case (action and outcome, outcome, action, the rest), not by the order in the file.

  • Changing state from a link. h:link and h:button issue GET requests. Use a command button.

  • An outcome from a request parameter or a text field. It can reach any view of the application (Never Build an Outcome from User Input).

  • ExternalContext.redirect instead of an outcome. It bypasses the rules; the Javadoc also requires an absolute URL.

  • Heavy <if> expressions. They can be evaluated more than once per navigation.

  • A request-scoped bean behind a navigation form. The postback loses the view parameter; use a view-scoped bean, or carry the parameter in a hidden field.

  • Navigating from a getter. A getter is called while rendering and several times per request; navigation belongs in action methods.

  • Calling the navigation handler from an exception handler during Render Response. The specification forbids it.