Custom Components and Renderers

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 code was compiled with the local JDK 26 (the component library of the packaging section with maven.compiler.release 17), deployed as an exploded Bookshelf WAR on Tomcat 11.0.26 with Weld 6.0.4.Final, once with Mojarra and once with MyFaces, and driven with curl and a small script that keeps the session and the view state between requests. Every output shown was observed on 2026-10-08. Faces 4.0 differences are noted where they matter, and claims that were not run are marked as such.

The standard components are enough for most pages, but sooner or later a project needs something they do not offer: an ISBN input that knows what an ISBN is, a call-out box with a title, a button that renders its children, markup that fits a CSS framework. Faces was designed for this. Every component on this page is a Java class that is registered with the runtime and can be used in a view like any other, and the split between a component (state and behaviour) and a renderer (markup and request decoding) is what lets one component have several appearances and one appearance serve several components. This page covers the whole path: choosing between a custom component and its lighter alternatives, the component classes, the state that survives between requests, renderers and the ResponseWriter, where each piece runs in the request lifecycle, component resources, client behaviours, render kits, and packaging the result as a library. The standard component model (tree, families, renderers and render kits in general, the binding pitfalls) is described on the architecture page; this page builds on it and does not repeat it. The composite components page covers the XHTML-only route, and the tag files and tag libraries page the Facelets tag handlers and EL functions that complement a component library.

The examples add five components to the Bookshelf project, all in the package org.example.bookshelf.component and in the tag library namespace urn:bookshelf:components (prefix b):

  • b:callout: a call-out box that renders itself (the direct implementation model)

  • b:isbnInput: an ISBN-13 input made of a UIInput subclass and a renderer (the delegated model), with Ajax support

  • b:shelfButton: a command button that decodes a click into an ActionEvent and renders its children

  • b:stateProbe: a throw-away component that shows which kinds of state survive a postback

  • a print render kit that re-renders the ISBN input as plain text

Choosing the Right Tool

A custom component is the heaviest option, and Faces offers lighter ones. The decision follows from what is missing.

flowchart TD A[A new reusable piece of a view] --> B{Only markup and existing components?} B -- yes, no typed contract needed --> C[Tag file] B -- yes, typed attributes and facets --> D[Composite component] B -- no --> E{Different markup for an existing component?} E -- yes --> F[Custom Renderer, or a decorated RenderKit] E -- no --> G{New state, decoding or events?} G -- yes --> H[Custom UIComponent, with or without a Renderer] G -- only script on a DOM event --> I[ClientBehavior] G -- only a value conversion or check --> J[Converter or Validator]
Table 1. The alternatives side by side
Option What you write Can it decode, hold state or fire events? Typical reason to choose it

Tag file

One XHTML file and an entry in a tag library

Only through the components it contains

Remove duplicated markup; no contract needed

Composite component

One XHTML file in a resource library, an interface of declared attributes, optionally a backing component

Yes, through the components it contains, its interface and a backing component

Reusable fragment with a typed contract (the Bookshelf star rating)

Custom Renderer

One class registered for an existing family and renderer type

It decodes the request for a component that already exists

Different markup, or an accessibility fix, for a standard component

Custom component

A UIComponent subclass, usually plus a renderer, and a tag

Yes: own properties, decoding, validation, events, children and facets

Behaviour the standard set lacks: a new kind of input, a container with logic, output that is not HTML

ClientBehavior

A class that returns the script for a DOM event

No; it only attaches script to an event

Confirm dialogs, tracking, new Ajax-like interactions

Converter or validator

One class

No, it works on a value that a component already holds

The data type or rule is new, not the control

Three rules of thumb come out of the table. First, if the new thing is expressible with the components that exist, write a composite component or a tag file, because they need no Java and no packaging. Second, if the control exists but the markup is wrong, replace its renderer, not the component: nothing else about it changes. Third, write a component only for state, decoding or events the standard set cannot provide. The Jakarta EE Tutorial reaches the same conclusions: it lists new events, different request processing of the value, HTML capabilities the standard components do not use and non-HTML clients as reasons for a custom component, and aggregating components (use a composite), manipulating data (use a bean bound to a standard component), converting data, validating and registering listeners as cases that do not need one.

Anatomy of a Component

Every component extends jakarta.faces.component.UIComponentBase, directly or through one of the standard classes. The base class implements the tree operations (parent, children, facets, findComponent, client ids, visitTree), the lifecycle traversal (processDecodes, processValidators, processUpdates), event queueing and listener management, attribute and ValueExpression maps, and the state operations. The standard classes add the behaviour you usually want to reuse.

Table 2. The classes worth extending
Class What it adds, and when to extend it

UIComponentBase

Nothing but the plumbing. Output-only containers and components that render themselves. Direct subclasses in the API include UIColumn, UICommand, UIData, UIForm, UIGraphic, UIMessage, UIMessages, UINamingContainer, UIOutput, UIPanel, UIParameter, UIViewAction, UIViewRoot and UIWebsocket

UIOutput

A value and a Converter (ValueHolder). Components that display a model value

UIInput

Submitted value, local value, validators, the required flag, value-change events and model update (EditableValueHolder). Components that edit a model value; extends UIOutput

UICommand

ActionSource: action method (the getActionExpression()/setActionExpression() default methods that Faces 4.1 moved here from ActionSource2), action listeners, immediate. Buttons and links

UIPanel, UIData, UIForm, UINamingContainer

Containers; UIForm and UINamingContainer also scope client ids (NamingContainer)

UISelectOne, UISelectMany, UISelectBoolean

Choices and their validation

If the new control resembles a standard one, extend the standard class, as the Tutorial advises: an editable menu extends UISelectOne and only adds editing. The Bookshelf ISBN input extends UIInput and so inherits conversion, validation, required, model update and value-change events without a line of code, and the shelf button extends UICommand and inherits the action machinery. The behavioural interfaces describe what a component can do, and a class may implement additional ones: a UIInput subclass that must also fire actions has to implement ActionSource itself (in Faces 4.0 and earlier, ActionSource2; in 4.1 that interface is deprecated for removal and only extends ActionSource, so new code should not name it). The other core classes of the framework (ViewHandler, NavigationHandler, ELResolver, ExceptionHandler and the factories) are extended with the same wrapper classes that appear in the render kit section and are registered in faces-config.xml; the extending Faces page covers them.

classDiagram direction TB class UIComponent class UIComponentBase class UIOutput class UIInput class UICommand class Callout class IsbnInput class ShelfButton class ValueHolder { <<interface>> } class EditableValueHolder { <<interface>> } class ActionSource { <<interface>> } class ClientBehaviorHolder { <<interface>> } UIComponent <|-- UIComponentBase UIComponentBase <|-- UIOutput UIOutput <|-- UIInput UIComponentBase <|-- UICommand UIComponentBase <|-- Callout UIInput <|-- IsbnInput UICommand <|-- ShelfButton ValueHolder <|.. UIOutput EditableValueHolder <|.. UIInput ActionSource <|.. UICommand ClientBehaviorHolder <|.. IsbnInput
Table 3. The behavioural interfaces of the component model
Interface Meaning

ValueHolder

The component has a local value and may carry a converter (UIOutput and subclasses)

EditableValueHolder

Adds submitted value, validity, validators, required and value-change listeners (UIInput)

ActionSource

The component fires ActionEvent objects and has an action method expression (UICommand); ActionSource2, which carried the expression property until Faces 4.0, is deprecated for removal since 4.1

NamingContainer

The ids of the descendants are prefixed with the container’s id (UIForm, UINamingContainer, UIData)

ClientBehaviorHolder

The component can carry client behaviours such as f:ajax; the standard HTML components implement it, but UIComponentBase only supplies the default method bodies, so a custom class must add implements ClientBehaviorHolder

StateHolder, PartialStateHolder, TransientStateHolder

State saving contracts; UIComponentBase implements all three

ComponentSystemEventListener, SystemEventListenerHolder

Receiving and subscribing to system events (UIComponentBase implements both)

Direct and Delegated Implementation

The specification names two programming models, and a component author picks one per component. In the direct implementation the component overrides decode, encodeBegin, encodeChildren and encodeEnd itself, and has no renderer type. It is the least code, and it welds the markup to the class. In the delegated implementation the component has a renderer type (set with setRendererType, usually in the constructor), a family (getFamily) and no encoding code; UIComponentBase forwards decoding and encoding to the Renderer that the current render kit has registered for the pair (family, renderer type). The delegated model is the one the standard components use, and the one to choose when the markup might ever change: another renderer type, or another render kit, gives the component a new appearance without touching its class. The b:callout of this page uses the direct model, the ISBN input and the shelf button the delegated one, so both appear in full.

The component API has many methods, and a custom class overrides few. The table lists the ones that matter and who calls them.

Table 4. The UIComponent methods a component author meets
Method Role

getFamily()

Required. Names the family that, with the renderer type, selects the renderer

getRendererType(), setRendererType(String)

The renderer within the family; null means the component renders itself

decode(FacesContext)

Apply Request Values, once for this component; with a renderer it delegates to Renderer.decode

encodeBegin, encodeChildren, encodeEnd, encodeAll

Render Response; encodeAll renders this component and all its rendered children, whatever getRendersChildren() says

getRendersChildren()

true means encodeChildren is called and must render the children; false means Faces renders them after encodeBegin

processDecodes, processValidators, processUpdates

Tree traversal for the three phases; a component that is not rendered skips them

queueEvent(FacesEvent), broadcast(FacesEvent)

Queue an event during a phase and deliver it to the listeners at the end of the phase

getAttributes(), getValueExpression, setValueExpression, getPassThroughAttributes()

Generic attributes, bound expressions and HTML5 pass-through attributes

getStateHelper(), getTransientStateHelper(), saveState, restoreState, markInitialState

State, see below

getFacet(String), getFacets(), getChildren()

Structure

getClientId(FacesContext), getContainerClientId(FacesContext)

The id on the client; descendants of a naming container get the container’s client id as a prefix

visitTree, invokeOnComponent

Visit or find components by id (used by Ajax render and search expressions)

Registering a Component and Giving It a Tag

A component class must be registered under a component type, the id by which Application.createComponent finds it, and a Facelets tag must exist for the page author to write it. Faces 4.1 offers three ways, and they combine.

Table 5. Three ways to register
Way What happens

@FacesComponent with createTag = true

The runtime registers the type and generates a Facelets tag handler for the namespace and tag name given in the annotation. Nothing else is needed

@FacesComponent alone, or a <component> entry in faces-config.xml

The type is registered; the page can use it only through a tag library file that names the type, or in Java through Application.createComponent(...)

A tag library file (*.taglib.xml)

Maps a tag to a component type and an optional renderer type, or to a hand-written TagHandler

The annotation has four elements: value (the component type; if omitted it defaults to the simple class name with the first character lower-cased), createTag (default false), tagName (the simple class name, first character lower-cased, if omitted) and namespace. The default namespace is the constant FacesComponent.NAMESPACE, jakarta.faces.component in 4.0 and 4.1. It was http://xmlns.jcp.org/jsf/component in 2.3 and 3.0, so a project that relied on the default namespace has to change the page declarations when it moves to Faces 4. Always set namespace explicitly, to a name you own: a URN such as urn:bookshelf:components or a URL under a domain you control.

A generated tag accepts any attribute. An attribute that names a property of the class is set on the property, one that does not is stored in getAttributes(), an EL expression becomes a ValueExpression, and an attribute in the jakarta.faces.passthrough namespace becomes a pass-through attribute (shown with the ISBN input below). There is no completion or validation of attribute names, which is the price of not declaring them; a tag library file can declare the attributes for tools.

A component registered without a tag works as soon as a tag library names it. The Bookshelf Plain component is registered only by type:

Plain.java — registered by type, no tag
package org.example.bookshelf.component;

import jakarta.faces.component.FacesComponent;
import jakarta.faces.component.UIComponentBase;
import jakarta.faces.context.FacesContext;
import java.io.IOException;

@FacesComponent("bookshelf.Plain")
public class Plain extends UIComponentBase {

    @Override
    public String getFamily() {
        return "bookshelf.Plain";
    }

    @Override
    public void encodeEnd(FacesContext context) throws IOException {
        context.getResponseWriter().writeText("[plain component]", null);
    }
}
bookshelf.taglib.xml — a tag for a registered component type
<?xml version="1.0" encoding="UTF-8"?>
<facelet-taglib 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-facelettaglibrary_4_1.xsd"
                version="4.1">
  <namespace>urn:bookshelf:plain</namespace>
  <tag>
    <tag-name>plainBox</tag-name>
    <component>
      <component-type>bookshelf.Plain</component-type>
    </component>
  </tag>
</facelet-taglib>

The <component> element of a tag has a required <component-type> and optional <renderer-type> (to set the renderer on creation), <handler-class> and <resource-id> children. The same component can be registered in faces-config.xml instead of by annotation, together with a renderer:

faces-config.xml — registering a component and a renderer without annotations
<?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">
  <component>
    <component-type>bookshelf.Plain</component-type>
    <component-class>org.example.bookshelf.component.Plain</component-class>
  </component>
  <render-kit>
    <renderer>
      <component-family>bookshelf.Isbn</component-family>
      <renderer-type>bookshelf.IsbnField</renderer-type>
      <renderer-class>org.example.bookshelf.component.IsbnRenderer</renderer-class>
    </renderer>
  </render-kit>
</faces-config>

A page uses the Plain tag through its namespace:

plain.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:pl="urn:bookshelf:plain">
<h:head><title>Plain</title></h:head>
<h:body><pl:plainBox/></h:body>
</html>

Observed on Tomcat 11.0.26 with the tag library file in WEB-INF/bookshelf.taglib.xml, the component registered in faces-config.xml and the renderer annotated:

  • Without the context parameter jakarta.faces.FACELETS_LIBRARIES (value /WEB-INF/bookshelf.taglib.xml; several files are separated by semicolons) the tag stayed in the output as the literal text <pl:plainBox> and the development stage showed a warning, on both implementations. Mojarra’s said that no tag library exists for the namespace, MyFaces' that the page "declares namespace urn:bookshelf:plain and uses the tag pl:plainBox, but no TagLibrary associated to namespace".

  • With the parameter set, both rendered [plain component].

  • A tag library file inside a JAR under META-INF and ending in .taglib.xml is discovered automatically, with no context parameter (see the packaging section).

In Java, a component is created with Application.createComponent(FacesContext, String componentType, String rendererType), which looks up the type, instantiates the class and sets the renderer type. This is how programmatic view construction creates components, and it is the reason a component needs a public no-argument constructor.

A Component That Renders Itself

The call-out box is output only: a title, some content and a severity that selects a CSS class. It needs no decoding and has no reason to be re-skinned, so it renders itself. The class keeps its three properties in the StateHelper (the next section explains why), puts a facet named title at the top, and can render its children either the default way or one by one inside wrappers.

Callout.java — a component that implements its own rendering
package org.example.bookshelf.component;

import jakarta.faces.application.ResourceDependency;
import jakarta.faces.component.FacesComponent;
import jakarta.faces.component.UIComponent;
import jakarta.faces.component.UIComponentBase;
import jakarta.faces.context.FacesContext;
import jakarta.faces.context.ResponseWriter;
import java.io.IOException;

@FacesComponent(value = "bookshelf.Callout", createTag = true,
        namespace = "urn:bookshelf:components", tagName = "callout")
@ResourceDependency(library = "bookshelf", name = "callout.css")
public class Callout extends UIComponentBase {

    public static final String COMPONENT_FAMILY = "bookshelf.Callout";

    private enum PropertyKeys {
        severity, styleClass, itemised
    }

    @Override
    public String getFamily() {
        return COMPONENT_FAMILY;
    }

    public String getSeverity() {
        return (String) getStateHelper().eval(PropertyKeys.severity, "info");
    }

    public void setSeverity(String severity) {
        getStateHelper().put(PropertyKeys.severity, severity);
    }

    public String getStyleClass() {
        return (String) getStateHelper().eval(PropertyKeys.styleClass);
    }

    public void setStyleClass(String styleClass) {
        getStateHelper().put(PropertyKeys.styleClass, styleClass);
    }

    public boolean isItemised() {
        return (Boolean) getStateHelper().eval(PropertyKeys.itemised, false);
    }

    public void setItemised(boolean itemised) {
        getStateHelper().put(PropertyKeys.itemised, itemised);
    }

    @Override
    public boolean getRendersChildren() {
        return isItemised();
    }

    @Override
    public void encodeBegin(FacesContext context) throws IOException {
        super.encodeBegin(context); // pushes the component to EL, publishes PreRenderComponentEvent
        ResponseWriter writer = context.getResponseWriter();
        writer.startElement("aside", this);
        writer.writeAttribute("id", getClientId(context), "id");
        String css = "callout callout-" + getSeverity() + (getStyleClass() == null ? "" : " " + getStyleClass());
        writer.writeAttribute("class", css, "styleClass");
        writer.writeAttribute("role", "note", null);
        UIComponent title = getFacet("title");
        if (title != null) {
            writer.startElement("strong", this);
            title.encodeAll(context);
            writer.endElement("strong");
        }
    }

    @Override
    public void encodeChildren(FacesContext context) throws IOException {
        ResponseWriter writer = context.getResponseWriter();
        for (UIComponent child : getChildren()) {
            if (child.isRendered()) {
                writer.startElement("div", this);
                writer.writeAttribute("class", "callout-item", null);
                child.encodeAll(context);
                writer.endElement("div");
            }
        }
    }

    @Override
    public void encodeEnd(FacesContext context) throws IOException {
        context.getResponseWriter().endElement("aside");
        super.encodeEnd(context); // pops the component from the EL stack
    }
}

The matching style sheet lives in the resource library bookshelf (see the resources section):

src/main/webapp/resources/bookshelf/callout.css
.callout { border-left: 4px solid #36c; padding: .5em 1em; margin: 1em 0; }
.callout-warn { border-color: #c63; }
callout.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:b="urn:bookshelf:components">
<h:head><title>Callouts</title></h:head>
<h:body>
  <b:callout id="c1" severity="warn" styleClass="big">
    <f:facet name="title">Return by Friday</f:facet>
    <h:outputText value="Two books"/>
    <h:outputText value="One DVD"/>
  </b:callout>
  <b:callout id="c2" itemised="true">
    <h:outputText value="Item A"/>
    <h:outputText value="Item B" rendered="false"/>
    <h:outputText value="Item C"/>
  </b:callout>
  <b:callout id="c3" rendered="false"/>
</h:body>
</html>

The page renders the same markup on Mojarra and MyFaces, apart from a ;jsessionid=... that MyFaces appends to the resource URL when the session cookie is not yet known (line breaks added):

<head><title>Callouts</title>
  <link rel="stylesheet" href="/bookshelf/jakarta.faces.resource/callout.css.xhtml?ln=bookshelf" /></head>
<body>
<aside id="c1" class="callout callout-warn big" role="note"><strong>Return by Friday</strong>Two booksOne DVD</aside>
<aside id="c2" class="callout callout-info" role="note"><div class="callout-item">Item A</div>
  <div class="callout-item">Item C</div></aside>

Several details of the direct model show in this output:

  • getRendersChildren() decides who renders the children. The first call-out left it false, so Faces rendered the two outputs right after encodeBegin and encodeChildren was never called: the children follow the title without any wrapper. The second one returned true through itemised="true", so encodeChildren ran and wrapped every rendered child in a div; the child with rendered="false" was skipped by the isRendered() test. The API recommends that component authors return true and implement encodeChildren, because that keeps the rendering of the children visible in the component. A component that decides at run time, as here, must make getRendersChildren() agree with what encodeChildren expects.

  • A component with rendered="false" renders nothing. The third call-out produced no markup at all. encodeAll tests isRendered() before calling anything.

  • The base-class methods are part of the contract. UIComponentBase.encodeBegin pushes the component to the EL stack (that is what makes #{component} work while it renders) and publishes the PreRenderComponentEvent, and encodeEnd pops it. A class that overrides both without calling super leaves the stack unbalanced and never publishes the event, so listeners registered with @ListenerFor or subscribeToEvent are never called (the events page reports this effect for @ListenerFor on both implementations). Call super.encodeBegin first and super.encodeEnd last.

  • Facets are rendered by the component. The facet named title is rendered with encodeAll where the component wants it. A facet is not a child, so Faces never renders it on its own; a component that forgets a facet silently drops it.

  • Literal attributes use the setters, expressions use eval. severity="warn" and itemised="true" were applied through setSeverity and setItemised, with the string true coerced to a boolean. An attribute written as an expression works too: a call-out with severity="#{probeBean.log eq '' ? 'warn' : 'info'}" rendered callout-warn while the bean property was empty, because getSeverity() goes through StateHelper.eval, which evaluates the ValueExpression stored under the property name.

State: the StateHelper, Plain Fields and the Transient Helper

A component instance lives for one request when the view is rebuilt, so any value it holds has to be restored on the next postback. Faces calls the mechanism state saving (the state saving page covers the strategies and their settings); a component author only needs to know what is saved with the view, and what is not.

With partial state saving, the default, the component tree is rebuilt from the Facelets source at the start of every postback, and the saved state holds only the changes since the view was first built: the delta. Everything the page author wrote in the tag attributes is applied again by the tag handler, so it does not have to be saved. What a component changes at run time has to be saved, and the means is the StateHelper, a Map-like object that every component returns from getStateHelper():

Table 6. The StateHelper operations
Operation Meaning

put(key, value)

Store a value (it stands in for an instance field) and return the previous one

put(key, mapKey, value)

Store an entry in a map kept under key; changes to the map must go through the helper to be tracked

add(key, value)

Add to a list kept under key; same rule

get(key)

The stored value only

eval(key)

The stored value if there is one, otherwise the result of the ValueExpression stored under the same name in the component’s expression map

eval(key, defaultValue), eval(key, Supplier)

The same with a default (the supplier form is new in 4.0)

remove(key), remove(key, valueOrKey)

Remove an entry, or an element of the inner list or map

The keys are Serializable; the idiom is a private enum named PropertyKeys with one constant per property, and the getter/setter pair of the example above. The getter reads with eval so that a property written as an expression in the page works too.

Three other kinds of state exist, and a small probe component shows what each does. The probe has a plain instance field, a StateHelper property, a field that it saves by hand through saveState and restoreState, and a counter in the TransientStateHelper:

StateProbe.java — four ways to hold a value
package org.example.bookshelf.component;

import jakarta.faces.component.FacesComponent;
import jakarta.faces.component.UIComponentBase;
import jakarta.faces.context.FacesContext;
import jakarta.faces.context.ResponseWriter;
import java.io.IOException;

@FacesComponent(value = "bookshelf.StateProbe", createTag = true,
        namespace = "urn:bookshelf:components", tagName = "stateProbe")
public class StateProbe extends UIComponentBase {

    private int plain = 1;       // an instance field: not saved with the view
    private String extra = "e0"; // saved by hand in saveState / restoreState

    private enum Keys { helper }

    @Override
    public String getFamily() {
        return "bookshelf.Probe";
    }

    public int getPlain() { return plain; }
    public void setPlain(int plain) { this.plain = plain; }

    public int getHelper() { return (Integer) getStateHelper().eval(Keys.helper, 1); }
    public void setHelper(int helper) { getStateHelper().put(Keys.helper, helper); }

    public String getExtra() { return extra; }
    public void setExtra(String extra) { this.extra = extra; }

    @Override
    public Object saveState(FacesContext context) {
        Object[] values = new Object[2];
        values[0] = super.saveState(context);
        values[1] = extra;
        return values;
    }

    @Override
    public void restoreState(FacesContext context, Object state) {
        if (state == null) {
            return;
        }
        Object[] values = (Object[]) state;
        super.restoreState(context, values[0]);
        extra = (String) values[1];
    }

    @Override
    public void encodeEnd(FacesContext context) throws IOException {
        Integer hits = (Integer) getTransientStateHelper().getTransient("hits");
        getTransientStateHelper().putTransient("hits", hits == null ? 1 : hits + 1);
        ResponseWriter w = context.getResponseWriter();
        w.startElement("span", this);
        w.writeAttribute("id", getClientId(context), "id");
        w.writeText("plain=" + getPlain() + " helper=" + getHelper() + " extra=" + getExtra()
                + " transient=" + getTransientStateHelper().getTransient("hits"), null);
        w.endElement("span");
    }
}
state.xhtml and the bean action that mutates the probe
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:b="urn:bookshelf:components">
<h:head><title>State</title></h:head>
<h:body>
  <h:form id="f">
    <b:stateProbe id="probe"/>
    <b:stateProbe id="probeEl" plain="#{probeBean.n}" helper="#{probeBean.n}"/>
    <h:commandButton id="bump" value="bump" action="#{probeBean.bump}"/>
    <h:commandButton id="noop" value="noop"/>
  </h:form>
</h:body>
</html>
@Named
@RequestScoped
public class ProbeBean {

    public int getN() {
        return 7;
    }

    public void bump() {
        StateProbe probe = (StateProbe) FacesContext.getCurrentInstance()
                .getViewRoot().findComponent("f:probe");
        probe.setPlain(9);
        probe.setHelper(9);
        probe.setExtra("e9");
    }
}

Requesting the page, pressing bump and then noop twice (each postback sends the view state returned by the previous response) printed the following on Mojarra and on MyFaces, identically:

Table 7. What each request printed for the two probes
Request b:stateProbe id="probe" b:stateProbe id="probeEl"

initial GET

plain=1 helper=1 extra=e0 transient=1

plain=1 helper=7 extra=e0 transient=1

postback with bump

plain=9 helper=9 extra=e9 transient=1

plain=1 helper=7 extra=e0 transient=1

next postback (noop)

plain=1 helper=9 extra=e9 transient=1

plain=1 helper=7 extra=e0 transient=1

one more postback

plain=1 helper=9 extra=e9 transient=1

plain=1 helper=7 extra=e0 transient=1

Four conclusions follow, and they are the rules of component state:

  • A plain instance field is reset on every postback. After bump the field showed 9 in the same response, and in the next request it was back to its initial value 1: the tag handler rebuilt the component, and nothing saved the 9. A field is safe only for values that are recomputed or reapplied every time.

  • A StateHelper property survives. helper=9 was restored on every later request, because put recorded the change in the delta.

  • A plain field ignores an EL attribute; eval honours it. The second probe received plain="#{probeBean.n}" and helper="#{probeBean.n}". A tag handler stores an expression in the component’s expression map instead of calling the setter, so the plain getter returned the field (1) and the helper getter returned the evaluated expression (7). This is the reason every property that a page author might bind has to be read with eval.

  • saveState and restoreState work for what the helper cannot hold, with the pattern the Tutorial shows: an Object[], the superclass state in the first slot. The hand-saved extra field survived (e9). Use it only for values the helper cannot hold or that need a custom serialisation. The override above saves extra on every request, which is correct but larger than a delta: the PartialStateHolder contract says that while initialStateMarked() is true a component returns only what changed. Such an override must also call saveState on any StateHolder objects it references (validators, listeners) and must not save children or facets, and restoreState must do nothing when the state is null. The helper is the recommended route, and it is what the standard components use.

  • The transient helper is for the current request. getTransientStateHelper() (since 2.1) keeps values that must never be saved: caches, flags, intermediate results. The counter read 1 on every request, as the response of each request starts from an empty transient state.

Everything held in the StateHelper, and everything returned by saveState, must be serialisable, because the implementation may write the view to the session, to disk or to the client, depending on the state-saving method. Never keep a FacesContext, an HttpServletRequest, a CDI bean reference or a UIComponent in it. A component for which isTransient() is true (set with setTransient(true)) does not take part in state saving or restoring at all.

A component that keeps its state in several nested objects may implement PartialStateHolder for those objects (markInitialState, initialStateMarked, clearInitialState): once the initial state is marked, the object returns only what changed. A component with only simple properties never needs it, because the StateHelper does the delta tracking.

A Component With a Renderer: the ISBN Input

The ISBN input is a UIInput that edits an ISBN-13. Its model value is a String of 13 digits; the user may type hyphens and spaces, which the renderer strips. The component class contains the rules that belong to the component, and the renderer all the markup and request handling. This is the delegated model: the constructor sets the renderer type, and getFamily returns the family under which the renderer is registered.

IsbnInput.java — the component
package org.example.bookshelf.component;

import jakarta.faces.application.FacesMessage;
import jakarta.faces.component.FacesComponent;
import jakarta.faces.component.UIInput;
import jakarta.faces.component.behavior.ClientBehaviorHolder;
import jakarta.faces.context.FacesContext;
import java.util.Collection;
import java.util.List;

@FacesComponent(value = "bookshelf.IsbnInput", createTag = true,
        namespace = "urn:bookshelf:components", tagName = "isbnInput")
public class IsbnInput extends UIInput implements ClientBehaviorHolder {

    public static final String COMPONENT_FAMILY = "bookshelf.Isbn";
    public static final String RENDERER_TYPE = "bookshelf.IsbnField";
    private static final List<String> EVENTS = List.of("valueChange", "change", "blur", "focus");

    public IsbnInput() {
        setRendererType(RENDERER_TYPE);
    }

    @Override
    public String getFamily() {
        return COMPONENT_FAMILY;
    }

    @Override
    public Collection<String> getEventNames() {
        return EVENTS;
    }

    @Override
    public String getDefaultEventName() {
        return "valueChange";
    }

    @Override
    protected void validateValue(FacesContext context, Object newValue) {
        super.validateValue(context, newValue); // required check and the registered validators
        if (isValid() && newValue != null && !newValue.toString().isEmpty() && !validChecksum(newValue.toString())) {
            setValid(false);
            context.addMessage(getClientId(context),
                    new FacesMessage(FacesMessage.SEVERITY_ERROR, "Not a valid ISBN-13", "Check the digits."));
        }
    }

    static boolean validChecksum(String digits) {
        if (!digits.matches("\\d{13}")) {
            return false;
        }
        int sum = 0;
        for (int i = 0; i < 13; i++) {
            sum += (digits.charAt(i) - '0') * (i % 2 == 0 ? 1 : 3);
        }
        return sum % 10 == 0;
    }
}

validateValue is the hook of UIInput for component-level validation: it runs inside validate() after conversion, performs the required check and calls the validators, and a subclass adds its own rule after calling super. A rule that several components share, or that a page author should be able to attach to other inputs, belongs in a Validator instead (see the validators page); a rule that is intrinsic to the component, as the ISBN checksum is, can live here. The message uses a literal in this example. A library should read its messages from a resource bundle declared as <message-bundle> or as a resource bundle for the locale in faces-config.xml.

The renderer decodes the field, converts the string and encodes the markup:

IsbnRenderer.java — decode, getConvertedValue and encodeEnd
package org.example.bookshelf.component;

import jakarta.faces.application.FacesMessage;
import jakarta.faces.application.ResourceDependency;
import jakarta.faces.component.behavior.ClientBehavior;
import jakarta.faces.component.behavior.ClientBehaviorContext;
import jakarta.faces.context.FacesContext;
import jakarta.faces.context.ResponseWriter;
import jakarta.faces.convert.ConverterException;
import jakarta.faces.render.FacesRenderer;
import jakarta.faces.render.Renderer;
import java.io.IOException;
import java.util.List;
import java.util.Map;

@ResourceDependency(library = "bookshelf", name = "isbn.js", target = "body")
@FacesRenderer(componentFamily = IsbnInput.COMPONENT_FAMILY, rendererType = IsbnInput.RENDERER_TYPE)
public class IsbnRenderer extends Renderer<IsbnInput> {

    @Override
    public void decode(FacesContext context, IsbnInput component) {
        if (isTrue(component, "disabled") || isTrue(component, "readonly")) {
            return;
        }
        Map<String, String> params = context.getExternalContext().getRequestParameterMap();
        String clientId = component.getClientId(context);
        if (params.containsKey(clientId)) {
            component.setSubmittedValue(params.get(clientId));
        }
        decodeBehaviors(context, component, params);
    }

    /** Literal tag attributes arrive as Strings, EL ones as whatever the expression returns. */
    private static boolean isTrue(IsbnInput component, String name) {
        Object value = component.getAttributes().get(name);
        return value != null && Boolean.parseBoolean(value.toString());
    }

    private void decodeBehaviors(FacesContext context, IsbnInput component, Map<String, String> params) {
        Map<String, List<ClientBehavior>> behaviors = component.getClientBehaviors();
        if (behaviors.isEmpty()) {
            return;
        }
        String event = params.get(ClientBehaviorContext.BEHAVIOR_EVENT_PARAM_NAME);
        String source = params.get(ClientBehaviorContext.BEHAVIOR_SOURCE_PARAM_NAME);
        if (event != null && component.getClientId(context).equals(source) && behaviors.containsKey(event)) {
            for (ClientBehavior behavior : behaviors.get(event)) {
                behavior.decode(context, component);
            }
        }
    }

    @Override
    public Object getConvertedValue(FacesContext context, IsbnInput component, Object submitted) {
        if (submitted == null || submitted.toString().isBlank()) {
            return null;
        }
        String digits = submitted.toString().replaceAll("[\\s-]", "");
        if (!digits.matches("\\d{13}")) {
            throw new ConverterException(new FacesMessage(FacesMessage.SEVERITY_ERROR,
                    "An ISBN-13 has 13 digits", "Hyphens and spaces are ignored."));
        }
        return digits;
    }

    @Override
    public void encodeEnd(FacesContext context, IsbnInput component) throws IOException {
        ResponseWriter writer = context.getResponseWriter();
        String clientId = component.getClientId(context);
        Object shown = component.getSubmittedValue() != null ? component.getSubmittedValue() : component.getValue();
        writer.startElement("input", component);
        writer.writeAttribute("id", clientId, "id");
        writer.writeAttribute("name", clientId, null);
        writer.writeAttribute("type", "text", null);
        writer.writeAttribute("inputmode", "numeric", null);
        writer.writeAttribute("data-isbn", "true", null);
        writer.writeAttribute("maxlength", 17, null);
        if (shown != null) {
            writer.writeAttribute("value", shown, "value");
        }
        Object placeholder = component.getAttributes().get("placeholder");
        if (placeholder != null) {
            writer.writeAttribute("placeholder", placeholder, "placeholder");
        }
        if (isTrue(component, "disabled")) {
            writer.writeAttribute("disabled", "disabled", "disabled");
        }
        String onchange = script(context, component, "valueChange");
        if (onchange == null) {
            onchange = script(context, component, "change");
        }
        if (onchange != null) {
            writer.writeAttribute("onchange", onchange, null);
        }
        writer.endElement("input");
    }

    private String script(FacesContext context, IsbnInput component, String event) {
        List<ClientBehavior> list = component.getClientBehaviors().get(event);
        if (list == null || list.isEmpty()) {
            return null;
        }
        String clientId = component.getClientId(context);
        StringBuilder sb = new StringBuilder();
        for (ClientBehavior behavior : list) {
            ClientBehaviorContext bc = ClientBehaviorContext.createClientBehaviorContext(
                    context, component, event, clientId, null);
            String s = behavior.getScript(bc);
            if (s != null) {
                sb.append(s).append(';');
            }
        }
        return sb.toString();
    }
}

The page uses the tag with a required value, a placeholder, an HTML5 pass-through attribute and an Ajax behaviour; a second input with a value-change listener and a disabled third one show the other attribute paths:

isbn.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:p="jakarta.faces.passthrough"
      xmlns:b="urn:bookshelf:components">
<h:head><title>ISBN</title></h:head>
<h:body>
  <h:form id="f">
    <b:isbnInput id="isbn" value="#{probeBean.isbn}" required="true" placeholder="978-..." p:data-test="x">
      <f:ajax listener="#{probeBean.listener}" render="out"/>
    </b:isbnInput>
    <b:isbnInput id="isbn2" value="#{probeBean.isbn}" valueChangeListener="#{probeBean.valueChanged}"/>
    <b:isbnInput id="isbn3" value="#{probeBean.isbn}" disabled="true"/>
    <b:shelfButton id="save" actionListener="#{probeBean.saved}" value="Save &amp; &lt;go&gt;"/>
    <h:outputText id="out" value="#{probeBean.log}"/>
    <h:messages/>
  </h:form>
</h:body>
</html>

The first GET produced (Mojarra; MyFaces differs only in the script call):

<input id="f:isbn" name="f:isbn" type="text" inputmode="numeric" data-isbn="true" maxlength="17"
       value="9783161484100" placeholder="978-..." onchange="mojarra.ab('f:isbn',event,'valueChange',0,'f:out');"
       data-test="x" />
<input id="f:isbn2" name="f:isbn2" type="text" inputmode="numeric" data-isbn="true" maxlength="17" value="9783161484100" />
<input id="f:isbn3" name="f:isbn3" type="text" inputmode="numeric" data-isbn="true" maxlength="17" value="9783161484100"
       disabled="disabled" />

and the postbacks behaved as follows, the same on both implementations unless noted:

Table 8. Postbacks of the ISBN input
Submitted f:isbn Result

978-3-16-148410-0

Converted to 9783161484100; the action listener ran and probeBean.isbn held the digits only

978316148410 (12 digits)

Message An ISBN-13 has 13 digits from the renderer’s ConverterException; the field redisplayed what the user typed

9783161484101 (bad checksum)

Message Not a valid ISBN-13 from validateValue; the field redisplayed the submitted text

empty

Message f:isbn: Validation Error: Value is required. (the required check in super.validateValue)

9780306406157 in f:isbn2

The value-change listener ran with old value 9783161484100 and new value 9780306406157

Ajax valueChange event on f:isbn

The partial response updated f:out with the text written by the listener, which ran in Invoke Application

The renderer follows the contract of the delegated model, and each point is a rule for every renderer:

  • decode only stores the submitted value. It reads the request parameter named by the client id and calls setSubmittedValue; it does not convert, validate or touch the model. A renderer must also respect disabled and readonly: a disabled field is not submitted by browsers, and a forged request must not set it. The renderer above honours both by returning before it reads the parameter, which is the usual practice for renderers.

  • getConvertedValue turns the submitted value into the component value and reports a problem by throwing a ConverterException with a FacesMessage. It is called by UIInput.getConvertedValue during Process Validations; if a Converter is attached to the component, a generic renderer normally lets it do the work. This renderer owns the conversion because the component’s value type is fixed.

  • encodeEnd shows the submitted value when there is one. It reads getSubmittedValue() before getValue(), so a rejected input appears exactly as typed (the 12-digit and checksum cases above).

  • Optional attributes come from getAttributes(). The renderer is shared by all components of its type, so it cannot have a field per attribute, and it reads placeholder, disabled and the like from the attribute map. A tag attribute that is not a property lands there, and it is a String when written literally. The first draft of this renderer tested Boolean.TRUE.equals(...) and disabled="true" did nothing: the value was the string "true", not a Boolean. The helper isTrue above parses it. A property declared on the component class (such as required on UIInput) is already typed.

  • The renderer holds no per-request state. The same instance serves concurrent requests on several threads, so it must not keep a field that depends on the component or the request.

  • Pass-through attributes are written for you. p:data-test="x" appeared on the input without any code in the renderer, because writer.startElement("input", component) takes the component’s pass-through attributes (getPassThroughAttributes(), since 2.2) from the second argument. Always pass the component to startElement. A pass-through attribute named by the constant Renderer.PASSTHROUGH_RENDERER_LOCALNAME_KEY replaces the element name.

  • writeAttribute with a Boolean. Boolean.TRUE was written as data-t="data-t" and Boolean.FALSE omitted by both implementations, which is why the renderer can pass booleans straight through. A null value was omitted by Mojarra and written as an empty attribute data-n="" by MyFaces: do not rely on either, and test for null yourself.

Where Each Method Runs in the Lifecycle

The component methods are called by the lifecycle, in an order that is worth seeing once. A subclass of the ISBN input overrides the interesting methods of the component and of the renderer and prints the current phase and the method name:

TracedIsbnInput.java and TracedIsbnRenderer.java — logging each hook
@FacesComponent(value = "bookshelf.TracedIsbnInput", createTag = true,
        namespace = "urn:bookshelf:components", tagName = "tracedIsbn")
public class TracedIsbnInput extends IsbnInput {

    public static void log(String what) {
        System.out.println("TRACE " + FacesContext.getCurrentInstance().getCurrentPhaseId() + " " + what);
    }

    public TracedIsbnInput() {
        setRendererType("bookshelf.TracedIsbn");
    }

    @Override public void processDecodes(FacesContext c) { log("processDecodes"); super.processDecodes(c); }
    @Override public void decode(FacesContext c) { log("component.decode"); super.decode(c); }
    @Override public void processValidators(FacesContext c) { log("processValidators"); super.processValidators(c); }
    @Override public void validate(FacesContext c) { log("validate"); super.validate(c); }
    @Override public Object getConvertedValue(FacesContext c, Object s) {
        log("component.getConvertedValue");
        return super.getConvertedValue(c, s);
    }
    @Override protected void validateValue(FacesContext c, Object v) {
        log("validateValue(" + v + ")");
        super.validateValue(c, v);
    }
    @Override public void processUpdates(FacesContext c) { log("processUpdates"); super.processUpdates(c); }
    @Override public void updateModel(FacesContext c) { log("updateModel"); super.updateModel(c); }
    @Override public void broadcast(FacesEvent e) throws AbortProcessingException {
        log("broadcast " + e.getClass().getSimpleName());
        super.broadcast(e);
    }
    @Override public void encodeBegin(FacesContext c) throws IOException { log("encodeBegin"); super.encodeBegin(c); }
    @Override public void encodeEnd(FacesContext c) throws IOException { log("encodeEnd"); super.encodeEnd(c); }
    @Override public void processEvent(ComponentSystemEvent e) {
        log("processEvent " + e.getClass().getSimpleName());
        super.processEvent(e);
    }
}

@FacesRenderer(componentFamily = IsbnInput.COMPONENT_FAMILY, rendererType = "bookshelf.TracedIsbn")
public class TracedIsbnRenderer extends IsbnRenderer {

    @Override public void decode(FacesContext c, IsbnInput i) { TracedIsbnInput.log("renderer.decode"); super.decode(c, i); }
    @Override public Object getConvertedValue(FacesContext c, IsbnInput i, Object s) {
        TracedIsbnInput.log("renderer.getConvertedValue");
        return super.getConvertedValue(c, i, s);
    }
    @Override public void encodeEnd(FacesContext c, IsbnInput i) throws IOException {
        TracedIsbnInput.log("renderer.encodeEnd");
        super.encodeEnd(c, i);
    }
}
trace.xhtml
<h:form id="f">
  <b:tracedIsbn id="isbn" value="#{probeBean.isbn}" valueChangeListener="#{probeBean.valueChanged}"/>
  <h:commandButton id="go" value="go" action="#{probeBean.saved}"/>
</h:form>

A GET of the page logged only the rendering calls. A postback that submitted a new ISBN and pressed the button logged the following, and the sequence was the same on Mojarra and MyFaces (MyFaces prints RESTORE_VIEW(1) where Mojarra prints RESTORE_VIEW 1):

TRACE RESTORE_VIEW 1 processEvent PostRestoreStateEvent
TRACE APPLY_REQUEST_VALUES 2 processDecodes
TRACE APPLY_REQUEST_VALUES 2 component.decode
TRACE APPLY_REQUEST_VALUES 2 renderer.decode
TRACE PROCESS_VALIDATIONS 3 processValidators
TRACE PROCESS_VALIDATIONS 3 validate
TRACE PROCESS_VALIDATIONS 3 component.getConvertedValue
TRACE PROCESS_VALIDATIONS 3 renderer.getConvertedValue
TRACE PROCESS_VALIDATIONS 3 validateValue(9780306406157)
TRACE PROCESS_VALIDATIONS 3 broadcast ValueChangeEvent
TRACE UPDATE_MODEL_VALUES 4 processUpdates
TRACE UPDATE_MODEL_VALUES 4 updateModel
TRACE RENDER_RESPONSE 6 encodeBegin
TRACE RENDER_RESPONSE 6 encodeEnd
TRACE RENDER_RESPONSE 6 renderer.encodeEnd
Table 9. Lifecycle position of the component and renderer hooks
Phase Calls Notes

Restore View

The tree is rebuilt; processEvent receives PostRestoreStateEvent

The component’s StateHelper delta is restored; state kept in fields is not (see the state section)

Apply Request Values

processDecodes, decode, Renderer.decode

Only a rendered component is visited; decode stores the submitted value and may queue events. An immediate command’s ActionEvent is broadcast at the end of this phase

Process Validations

processValidators, validate, getConvertedValue on the component and then on the renderer, validateValue, then broadcast of the queued value-change event

validateValue receives the converted value. A conversion or validation failure marks the component invalid, adds a message and skips Update Model Values for the whole view

Update Model Values

processUpdates, updateModel

Writes the local value through the value expression

Invoke Application

Action listeners and the action of commands

A non-immediate ActionEvent queued in decode is delivered here (UICommand.queueEvent sets the phase)

Render Response

encodeBegin, encodeChildren if getRendersChildren(), encodeEnd, each delegating to the renderer when there is one

In a partial (Ajax) request only the components named by render are encoded

The practical consequences are a few rules: do all request reading in decode, never in an encode method; convert and validate in the Process Validations hooks, never in decode; never change the model from a component method (that is Update Model Values, and it does it for you); and keep the encode methods free of side effects, because they run once per render and an Ajax request may render a component several times.

The ResponseWriter

A renderer writes markup only through the ResponseWriter that FacesContext.getResponseWriter() returns. The class extends java.io.Writer and adds markup-aware methods, so the same renderer code produces well-formed output without concatenating strings.

Table 10. The ResponseWriter methods a renderer uses
Method What it does

startElement(name, component), endElement(name)

Opens an element (the tag is left open until the next call, so attributes can follow) and closes it. Pass the component so that its pass-through attributes are written; elements must be closed in the reverse order

writeAttribute(name, value, property)

Writes name="value", escaping the value. The third argument names the component property the attribute comes from (for tools; null if none). Only valid right after startElement

writeURIAttribute(name, value, property)

Writes a URI attribute, applying URI encoding (a space becomes %20). It is not a safety filter, see below

writeText(object, property), writeText(char[], off, len), writeText(object, component, property)

Writes text content with markup escaping; closes an open start tag first

writeComment(object)

Writes a comment

write(...) (from Writer)

Writes raw text with no escaping at all

startCDATA(), endCDATA()

Open and close an XML CDATA section

writeDoctype(String), writePreamble(String)

Write a doctype or an XML preamble verbatim (since 2.2)

startDocument(), endDocument(), flush()

Document boundaries; flush() only clears the writer’s own buffer

getContentType(), getCharacterEncoding(), cloneWithWriter(Writer)

Information about the output, and a copy of the writer that writes to another Writer

The Echo component below writes the same untrusted text four ways. It is deliberately unsafe in its last statement, to show what the safe methods do:

Echo.java — the same value through four writer methods
@FacesComponent(value = "bookshelf.Echo", createTag = true,
        namespace = "urn:bookshelf:components", tagName = "echo")
public class Echo extends UIComponentBase {

    @Override
    public String getFamily() {
        return "bookshelf.Echo";
    }

    @Override
    public void encodeEnd(FacesContext context) throws IOException {
        ResponseWriter w = context.getResponseWriter();
        Object text = getAttributes().get("text");
        w.startElement("p", this);
        w.writeAttribute("title", text, "text");
        w.writeText(text, "text");
        w.endElement("p");
        w.startElement("a", this);
        w.writeURIAttribute("href", text, "text");
        w.writeText("link", null);
        w.endElement("a");
        w.write("<div class=\"raw\">" + text + "</div>"); // NEVER do this with user data
    }
}
echo.xhtml
<h:body><b:echo text="#{probeBean.payload}"/></h:body>

With the bean property set to "><script>alert(1)</script>&x=1 javascript:alert(2) the output was:

Mojarra 4.1.17
<p title="&quot;&gt;&lt;script&gt;alert(1)&lt;/script&gt;&amp;x=1 javascript:alert(2)">"&gt;&lt;script&gt;alert(1)&lt;/script&gt;&amp;x=1 javascript:alert(2)</p>
<a href="%22><script>alert(1)</script>&x=1%20javascript:alert(2)">link</a>
<div class="raw">"><script>alert(1)</script>&x=1 javascript:alert(2)</div>
MyFaces 4.1.4
<p title="&quot;&gt;&lt;script&gt;alert(1)&lt;/script&gt;&amp;x=1 javascript:alert(2)">&quot;&gt;&lt;script&gt;alert(1)&lt;/script&gt;&amp;x=1 javascript:alert(2)</p>
<a href="%22%3E%3Cscript%3Ealert(1)%3C/script%3E&x=1%20javascript:alert(2)">link</a>
<div class="raw">"><script>alert(1)</script>&x=1 javascript:alert(2)</div>

What this shows:

  • writeAttribute and writeText escape <, >, & on both implementations, and writeAttribute also escapes the quote. They differ in one detail: MyFaces escapes the quote in text content as well, Mojarra does not (harmless in element content).

  • write(String) escapes nothing. The raw div carries a working script element: this is the cross-site scripting hole that escape="false" opens in the standard components, and it appears in custom renderers whenever a developer builds markup with string concatenation. The rule is absolute: never pass component values, request parameters or anything that users can influence to write. Use writeText for text and writeAttribute for attributes, even for values that "can only be numbers".

  • writeURIAttribute is an encoder, not a filter. Mojarra encoded only the quote (%22) and left < and > in the URL, MyFaces encoded the quote and the angle brackets, and neither touched &. Neither blocks a javascript: scheme: with the value javascript:alert(document.cookie) both wrote href="javascript:alert(document.cookie)" unchanged. A renderer that writes a link from data must check the scheme itself. An allow list is the reliable form, because browsers ignore some control characters inside a scheme and every deny list misses a variant:

A scheme allow list for links built from data
static String safeHref(String candidate) {
    if (candidate == null) {
        return null;
    }
    String url = candidate.strip();
    String lower = url.toLowerCase(java.util.Locale.ROOT);
    boolean absolute = lower.startsWith("https://") || lower.startsWith("http://");
    boolean relative = lower.startsWith("/") && !lower.startsWith("//");
    return absolute || relative ? url : null;
}

Run against javascript:alert(1), the same with a tab and mixed case inside the scheme, //evil.example and data:text/html,x, it returned null for all four, and it returned https://example.org/x and /books/1 unchanged. The renderer then writes the attribute only when the result is not null.

Commands, Children and Events: the Shelf Button

A component that fires events queues them in decode. The shelf button extends UICommand, so it inherits the action properties (action, actionListener, immediate) and the delivery logic, and its renderer does two things: it turns a click into an ActionEvent and it renders the button together with its children.

ShelfButton.java and ShelfButtonRenderer.java
@FacesComponent(value = "bookshelf.ShelfButton", createTag = true,
        namespace = "urn:bookshelf:components", tagName = "shelfButton")
public class ShelfButton extends UICommand {

    public static final String COMPONENT_FAMILY = "bookshelf.Command";
    public static final String RENDERER_TYPE = "bookshelf.ShelfButton";

    public ShelfButton() {
        setRendererType(RENDERER_TYPE);
    }

    @Override
    public String getFamily() {
        return COMPONENT_FAMILY;
    }
}

@FacesRenderer(componentFamily = ShelfButton.COMPONENT_FAMILY, rendererType = ShelfButton.RENDERER_TYPE)
public class ShelfButtonRenderer extends Renderer<ShelfButton> {

    @Override
    public void decode(FacesContext context, ShelfButton component) {
        Map<String, String> params = context.getExternalContext().getRequestParameterMap();
        if (params.containsKey(component.getClientId(context))) {
            component.queueEvent(new ActionEvent(context, component));
        }
    }

    @Override
    public boolean getRendersChildren() {
        return true;
    }

    @Override
    public void encodeBegin(FacesContext context, ShelfButton component) throws IOException {
        ResponseWriter writer = context.getResponseWriter();
        String clientId = component.getClientId(context);
        writer.startElement("button", component);
        writer.writeAttribute("id", clientId, "id");
        writer.writeAttribute("name", clientId, "name");
        writer.writeAttribute("type", "submit", null);
        Object value = component.getValue();
        if (value != null) {
            writer.writeText(value, "value");
        }
    }

    @Override
    public void encodeChildren(FacesContext context, ShelfButton component) throws IOException {
        for (UIComponent child : component.getChildren()) {
            child.encodeAll(context);
        }
    }

    @Override
    public void encodeEnd(FacesContext context, ShelfButton component) throws IOException {
        context.getResponseWriter().endElement("button");
    }
}
btn.xhtml
<h:form id="f">
  <b:shelfButton id="b" value="V: " action="#{probeBean.saved}">
    <h:outputText value="child &lt;b&gt;"/>
  </b:shelfButton>
</h:form>

Both implementations rendered <button id="f:b" name="f:b" type="submit">V: child &lt;b&gt;</button>: the label from value, escaped, then the child, then the closing tag. Pressing the button submits the form with the parameter f:b; decode sees it, queues the event, and UICommand.queueEvent marks the event for Invoke Application (or, if the button is immediate, for Apply Request Values). The default action listener then calls the action method and the navigation handler; in the isbn.xhtml example a shelf button with actionListener="#{probeBean.saved}" ran the listener on both implementations.

The rules for events in a component are short. Call queueEvent from decode, never fire a listener directly, because queueing keeps the lifecycle ordering and the immediate handling intact. queueEvent throws an IllegalStateException if the component is not in a view, so tests must put it under a UIViewRoot. A component that needs its own event type subclasses FacesEvent, defines a listener interface that extends FacesListener, implements isAppropriateListener and processListener in the event, and registers listeners with the protected addFacesListener of UIComponentBase; the API documentation of that method contains a complete FooEvent and FooListener pattern. The system events that a component can subscribe to (PostAddToViewEvent, PreRenderComponentEvent and the others) are covered on the events page.

The shelf button writes type="submit" and relies on the browser. Making it Ajax-aware is the subject of the next section.

Making a Component Ajax-Aware

f:ajax attaches an AjaxBehavior to its parent, and the parent must be a ClientBehaviorHolder. The standard HTML components are; a custom component has to declare it. The ISBN input did so with three pieces, which are the whole contract:

  • implements ClientBehaviorHolder, with getEventNames() returning the names of the events a behaviour can attach to and getDefaultEventName() the one used when f:ajax has no event attribute. UIComponentBase already provides addClientBehavior and getClientBehaviors().

  • In the renderer, decode must give each behaviour a chance to decode when it is the source of the request: it compares the request parameters jakarta.faces.behavior.event and jakarta.faces.source (the constants ClientBehaviorContext.BEHAVIOR_EVENT_PARAM_NAME and BEHAVIOR_SOURCE_PARAM_NAME) with the component’s event names and client id, and calls behavior.decode(context, component). For an AjaxBehavior that queues the AjaxBehaviorEvent, which is how the listener of f:ajax runs.

  • In the encode method, the renderer asks each behaviour for its script with ClientBehaviorContext.createClientBehaviorContext(context, component, eventName, sourceId, parameters) and behavior.getScript(...), and writes the result into the DOM event handler attribute (onchange in the example). Mojarra produced onchange="mojarra.ab('f:isbn',event,'valueChange',0,'f:out');" and MyFaces onchange="myfaces.ab('f:isbn',event,'valueChange','','f:out',{});": the renderer is implementation independent because the script comes from the behaviour.

The code for all three is in the IsbnInput and IsbnRenderer listings above. The Ajax request for the first field (jakarta.faces.behavior.event=valueChange, jakarta.faces.source=f:isbn, execute f:isbn, render f:out) returned a partial response with one update for f:out, whose text had been written by the listener running in Invoke Application, and the new view state. A second request that asked to render the custom component itself (render="f:isbn") returned an update for f:isbn with the renderer’s input element, on both implementations: a custom component needs nothing special to be re-rendered by Ajax.

Three failures show what the contract enforces, and the messages differ per implementation:

Table 11. Misusing f:ajax on custom components
Page fragment Mojarra 4.1.17 MyFaces 4.1.4

<b:isbnInput ...><f:ajax event="foo" render="@this"/></b:isbnInput>

'foo' is not a supported event for IsbnInput. Please specify one of these supported event names: blur, change, focus, valueChange.

TagAttributeException: ... event="foo" event it is not a valid eventName defined for this component

<b:callout ...><f:ajax render="@this"/></b:callout> (not a ClientBehaviorHolder)

TagException: ... <f:ajax> Unable to attach <f:ajax> to non-ClientBehaviorHolder parent

TagException: ... <f:ajax> Parent is not composite component or of type ClientBehaviorHolder

<b:isbnInput ...><f:ajax event="blur" render="i"/></b:isbnInput> (an advertised event)

The page rendered with no error, but the input has no onblur

The same (the renderer ignores the event)

The third row is the trap. Listing an event in getEventNames() only lets the page compile; the renderer must emit the script for that event, and the IsbnRenderer emits valueChange and change (as onchange) only. The blur behaviour was accepted and silently did nothing. Either render every advertised event or advertise only those the renderer renders. The client behaviors page covers writing the behaviours themselves, and the Ajax page the f:ajax attributes and the JavaScript API.

Component Resources

A component usually needs a style sheet or a script. Hard-coding a link or script element in encodeBegin produces duplicates when the component appears twice and bypasses resource versioning, minification and localisation. The @ResourceDependency annotation (repeatable, or in a @ResourceDependencies container) on a UIComponent or a Renderer declares a resource by name, optional library and optional target. When the component is added to the view on an initial request, the runtime creates a resource component for it and adds it to the view root, under head unless the target says otherwise. The resource renderers skip a resource that has already been rendered in the response, so one declaration per component class is enough. The three attributes accept EL.

The Callout declared @ResourceDependency(library = "bookshelf", name = "callout.css") on the component and the IsbnRenderer declared @ResourceDependency(library = "bookshelf", name = "isbn.js", target = "body") on the renderer, with this script:

src/main/webapp/resources/bookshelf/isbn.js
document.addEventListener('input', function (event) {
  var field = event.target;
  if (field.dataset && field.dataset.isbn === 'true') {
    field.value = field.value.replace(/[^0-9 -]/g, '');
  }
});

Four call-outs on a page produced a single <link rel="stylesheet" href="/bookshelf/jakarta.faces.resource/callout.css.xhtml?ln=bookshelf" /> in the head on both implementations; three ISBN inputs produced a single <script src="/bookshelf/jakarta.faces.resource/isbn.js.xhtml?ln=bookshelf"> just before </body>, with faces.js (added because the page uses f:ajax) still in the head. The .xhtml suffix in the URL comes from the application’s FacesServlet mapping and is removed by the resource handler. The library bookshelf is a directory, resources/bookshelf/ in the web application or META-INF/resources/bookshelf/ in a JAR, and the packaging section shows both.

The data-isbn attribute lets the script find the inputs without relying on ids. A renderer that must pass server data to a script should write it as data-* attributes with writeAttribute, which escapes it, and not splice it into inline script text.

A Rich Input: Wrapping a JavaScript Widget

A "rich" input (a date picker, a rating control, a rich text editor) is the same recipe with a bigger script: the component is an ordinary UIInput, the renderer writes an element that carries the submitted value plus data-* attributes that configure the widget, and the widget’s own files are declared with @ResourceDependency so that the library is loaded once per page however many instances there are. The widget script finds its elements in the browser, builds the user interface, and writes the user’s choice back into the form field that the renderer’s decode reads. Nothing about decoding, conversion, validation or model update changes, which is the benefit of wrapping a widget this way: the server side stays a normal UIInput. The sketch below is a star rating whose widget is a script of your own; a third-party library is wrapped identically by adding its files to the dependency list (@ResourceDependencies, the library files first) and by initialising it in the small script that reads the data-* attributes. This sketch was written against the API and was not run on either implementation.

RatingRenderer.java — a hidden field plus a widget host element
@ResourceDependencies({
    @ResourceDependency(library = "bookshelf", name = "rating.css", target = "head"),
    @ResourceDependency(library = "bookshelf", name = "rating.js", target = "body")
})
@FacesRenderer(componentFamily = "jakarta.faces.Input", rendererType = "bookshelf.Rating")
public class RatingRenderer extends Renderer<UIInput> {

    @Override
    public void decode(FacesContext context, UIInput component) {
        if (Boolean.parseBoolean(String.valueOf(component.getAttributes().get("disabled")))) {
            return;
        }
        String id = component.getClientId(context);
        String submitted = context.getExternalContext().getRequestParameterMap().get(id);
        if (submitted != null) {
            component.setSubmittedValue(submitted);
        }
    }

    @Override
    public void encodeEnd(FacesContext context, UIInput component) throws IOException {
        ResponseWriter out = context.getResponseWriter();
        String id = component.getClientId(context);
        Object value = component.getValue();
        out.startElement("div", component);
        out.writeAttribute("class", "rating", null);
        out.writeAttribute("data-rating-for", id, null);
        out.writeAttribute("data-max", component.getAttributes().getOrDefault("max", 5), null);
        out.endElement("div");
        out.startElement("input", component);
        out.writeAttribute("type", "hidden", null);
        out.writeAttribute("id", id, "id");
        out.writeAttribute("name", id, "clientId");
        out.writeAttribute("value", value == null ? "" : value, "value");
        out.endElement("input");
    }
}
resources/bookshelf/rating.js — the widget, started when the page has loaded
document.querySelectorAll('[data-rating-for]').forEach(function (host) {
  var field = document.getElementById(host.dataset.ratingFor);
  var max = parseInt(host.dataset.max, 10);
  for (var i = 1; i <= max; i++) {
    var star = document.createElement('button');
    star.type = 'button';
    star.textContent = i <= Number(field.value) ? '\u2605' : '\u2606';
    star.dataset.value = i;
    star.addEventListener('click', function (event) {
      field.value = event.currentTarget.dataset.value;
      field.dispatchEvent(new Event('change', { bubbles: true }));
      host.querySelectorAll('button').forEach(function (b) {
        b.textContent = Number(b.dataset.value) <= Number(field.value) ? '\u2605' : '\u2606';
      });
    });
    host.appendChild(star);
  }
});

Two points decide whether such a widget keeps working. The script runs once when the page is loaded, so after an Ajax update that re-renders the component the stars are gone unless the script is called again for the new elements (a callback registered with faces.ajax.addOnEvent that re-initialises on the success status, or a widget that uses event delegation on the document as isbn.js does). And the value sent to the server is the hidden field, so the validation and the conversion messages attach to its client id; see the Ajax page for the render side of that.

Render Kits and Switching Them

A render kit is the registry in which renderers live: getRenderer(family, rendererType) returns a renderer, addRenderer(...) registers one, and the kit also creates the ResponseWriter and ResponseStream, holds the client behaviour renderers (addClientBehaviorRenderer, getClientBehaviorRenderer) and returns the ResponseStateManager that writes the view state into the response. Every application has the kit HTML_BASIC (the constant RenderKitFactory.HTML_BASIC_RENDER_KIT). The architecture page shows how to add renderers to it and how to decorate it with a RenderKitWrapper; this section registers a kit of its own and switches a view to it, which is what the render-kit chapters of the older books do.

The Bookshelf print kit renders the ISBN input as plain text, as a printable page would, and delegates everything else to HTML_BASIC. The renderer for the kit is the ISBN renderer’s subclass, annotated for the new kit:

PrintIsbnRenderer.java and PrintRenderKit.java
@FacesRenderer(renderKitId = "BOOKSHELF_PRINT",
        componentFamily = IsbnInput.COMPONENT_FAMILY, rendererType = IsbnInput.RENDERER_TYPE)
public class PrintIsbnRenderer extends IsbnRenderer {

    @Override
    public void decode(FacesContext context, IsbnInput component) {
        // nothing to decode: the print kit never posts back
    }

    @Override
    public void encodeEnd(FacesContext context, IsbnInput component) throws IOException {
        ResponseWriter writer = context.getResponseWriter();
        writer.startElement("span", component);
        writer.writeAttribute("id", component.getClientId(context), "id");
        writer.writeAttribute("class", "isbn", null);
        Object value = component.getValue();
        writer.writeText(value == null ? "" : value, "value");
        writer.endElement("span");
    }
}

public class PrintRenderKit extends RenderKitWrapper {

    private final Map<String, Renderer<?>> own = new ConcurrentHashMap<>();
    private volatile RenderKit basic;

    public PrintRenderKit() {
        super(null); // the no-argument constructor is deprecated; getWrapped() is overridden instead
    }

    @Override
    public RenderKit getWrapped() {
        if (basic == null) {
            RenderKitFactory factory = (RenderKitFactory) FactoryFinder.getFactory(FactoryFinder.RENDER_KIT_FACTORY);
            basic = factory.getRenderKit(FacesContext.getCurrentInstance(), RenderKitFactory.HTML_BASIC_RENDER_KIT);
        }
        return basic;
    }

    @Override
    public Renderer getRenderer(String family, String rendererType) {
        Renderer<?> mine = own.get(family + "|" + rendererType);
        return mine != null ? mine : super.getRenderer(family, rendererType);
    }

    @Override
    public void addRenderer(String family, String rendererType, Renderer renderer) {
        own.put(family + "|" + rendererType, renderer); // never add to HTML_BASIC by accident
    }
}
faces-config.xml — the kit gets a new id and a class with a public no-argument constructor
<?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">
  <render-kit>
    <render-kit-id>BOOKSHELF_PRINT</render-kit-id>
    <render-kit-class>org.example.bookshelf.component.PrintRenderKit</render-kit-class>
  </render-kit>
</faces-config>

A view selects the kit with renderKitId on f:view:

kit.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:b="urn:bookshelf:components">
<f:view renderKitId="BOOKSHELF_PRINT">
  <h:head><title>Print</title></h:head>
  <h:body>
    <h:form id="f"><b:isbnInput id="isbn" value="#{probeBean.isbn}"/></h:form>
    <h:outputText value="plain output"/>
  </h:body>
</f:view>
</html>

Both implementations rendered <span id="f:isbn" class="isbn">9783161484100</span> for the input and plain output for the standard output component, whose renderer the print kit took from HTML_BASIC. The same component class, with the same page tag, produced an input element under the default kit and a span under the print kit. That is the mechanism behind the render-kit chapters of the older books, in which one set of components was rendered with different kits for different client technologies.

A view can select its kit in four ways, in decreasing order of locality: the renderKitId attribute of f:view (it accepts an expression), UIViewRoot.setRenderKitId from code, a <default-render-kit-id> in the <application> element of faces-config.xml, and a custom ViewHandler whose calculateRenderKitId(FacesContext) returns the id for each request (the method must not return null, and it is how a per-device or per-user choice is made). Only the first was run here. The API documentation of RenderKit notes that having several kits in play in one application requires a ViewHandler that is aware of it; the f:view attribute worked on both implementations without one.

Registration mistakes are not reported alike:

Table 12. When the kit does not exist or is misconfigured
Situation Mojarra 4.1.17 MyFaces 4.1.4

A view names an id that no kit has

Error page No render kit was available for id "NOCLASS"

IllegalStateException - No render kit was available for id ... (error page)

A <render-kit> with a new id and no <render-kit-class>, plus <renderer> entries

Startup log: ConfigurationException: Unable to register renderers with RenderKit using id NOCLASS. RenderKit does not exist., then Using HTML_BASIC instead, and a view naming the id fails

MyFaces created a kit, the renderer was registered and the view used it

@FacesRenderer(renderKitId = "NO_SUCH_KIT")

Startup log: Error processing annotated Renderer ... Unable to find specified RenderKit; the application kept answering, and in that run (the class in WEB-INF/classes) a page with standard components came back without its doctype, head and body, with empty output components; in the JAR run described below the pages stayed normal

Nothing logged and no effect on the pages

The API says the first case of the third row is fatal and must keep the application out of service. Mojarra logged the error and carried on, in a state that varied between my two runs, and MyFaces ignored the annotation, so a typo in a kit id has to be found in the log, not on a page. The safe form is a <render-kit-class> for every new id, and a test that renders one page per kit.

The kits that the older books show — XUL and HTC (HTML Components) for desktop-like browsers, kits for WML, for printers — targeted client technologies that no current browser runs or that nobody deploys; of that era only the mechanism survives. Today a second render kit is justified by output that is not the default HTML (a printable or e-mail rendition, an accessibility profile, markup for a CSS framework chosen per view), and a library that only restyles the standard components is better served by replacing individual renderers, as the architecture page shows.

Packaging Components in a JAR

A component library is an ordinary JAR placed in WEB-INF/lib. Its layout is what lets the runtime find everything without configuration in the application:

bookshelf-components-1.0.0.jar
├── META-INF/faces-config.xml          marker for annotation scanning; may be empty; holds <render-kit> entries
├── META-INF/bookshelf.taglib.xml      tag library, found automatically
├── META-INF/resources/bookshelf/      resource library "bookshelf": callout.css, isbn.js
└── org/example/bookshelf/component/   components, renderers, the render kit

The Maven project needs only the Faces API on the compile classpath, with scope provided because the server or the WAR supplies the implementation. The listing was built with mvn -o package:

pom.xml of the component library
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.example.bookshelf</groupId>
  <artifactId>bookshelf-components</artifactId>
  <version>1.0.0</version>
  <packaging>jar</packaging>

  <properties>
    <maven.compiler.release>17</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>jakarta.faces</groupId>
      <artifactId>jakarta.faces-api</artifactId>
      <version>4.1.2</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>
</project>

The application declares the library like any dependency and the page imports the namespace:

<dependency>
  <groupId>org.example.bookshelf</groupId>
  <artifactId>bookshelf-components</artifactId>
  <version>1.0.0</version>
</dependency>

Three runs of the same WAR, with the component classes only in a JAR placed in WEB-INF/lib and none in WEB-INF/classes, show which parts of the layout are required:

Table 13. What the runtime found in the JAR
JAR contents Mojarra 4.1.17 MyFaces 4.1.4

Classes and META-INF/resources only

b:callout and the resource worked (Mojarra scans the classes anyway); the BOOKSHELF_PRINT kit was missing because it is declared in faces-config.xml (and the annotated print renderer made the startup log carry the "Unable to find specified RenderKit" error of the previous table); pl:plainBox unknown

b:callout unknown: warning "declares namespace urn:bookshelf:components and uses the tag b:callout, but no TagLibrary associated to namespace"; the kit was missing

Plus META-INF/faces-config.xml (with the <render-kit>)

Components, tags, resources and kit worked; pl:plainBox still unknown

The same, and b:callout now worked

Plus META-INF/bookshelf.taglib.xml

Everything worked, including pl:plainBox mapped to the bookshelf.Plain type

Everything worked

The specification’s rule for annotation scanning explains the MyFaces column: every class in WEB-INF/classes is scanned, but a JAR in WEB-INF/lib is scanned only if it contains META-INF/faces-config.xml or a file matching *.faces-config.xml, even an empty one. Mojarra scanned the JAR without the marker file, which is more than the rule requires. A library must not depend on that: ship META-INF/faces-config.xml (an empty <faces-config version="4.1"/> with the schema namespace is enough). The same section says that metadata-complete="true" on the <faces-config> of WEB-INF/faces-config.xml switches annotation scanning off for the application; with the attribute set, the page with b:callout and the page with the print kit stopped working on both implementations, because the library’s tags were gone. A library author should know that an application can do this.

Several other facts apply to a library:

  • Tag library discovery. A file in META-INF of a JAR whose name ends in .taglib.xml is registered at startup without any context parameter, and a malformed file stops the deployment. A tag library inside the WAR is not discovered; it is listed in the context parameter jakarta.faces.FACELETS_LIBRARIES (observed above).

  • Resources. META-INF/resources/<library>/<resource> is the classpath equivalent of the web application’s resources/<library>/<resource>. The specification does not require implementations to support the optional library-version and resource-version path segments for resources in JARs, so do not rely on versioned directory names inside META-INF/resources.

  • Ordering. When several libraries register the same artifact, the order is defined by <name> and <ordering> in each faces-config.xml, with <absolute-ordering> in the application’s own file overriding it. This was not run here.

  • CDI. Components and renderers are not CDI beans. A @Inject field in the StateProbe component and in the IsbnRenderer stayed null on Mojarra and on MyFaces, in both cases next to a request-scoped bean that Weld had discovered. A library that needs a service reads it through a value expression the page passes in, or through CDI.current(), never through injection into the component, and never keeps the result in the StateHelper. If the JAR contains beans, add META-INF/beans.xml so that CDI scans it.

  • Namespaces. Choose a namespace you control and keep it stable, because every page that uses the library repeats it. The default namespace of @FacesComponent changed in 4.0 (see below), and the old Faces namespaces are planned to be removed in 5.0.

  • Two implementations. Test the library on Mojarra and MyFaces. Everything in the table above came from running both, and the differences were all in registration and configuration, none in the component API.

The Design Patterns Behind the Component API

The component API is built from a handful of classic patterns, and recognising them tells you where to hook in.

Table 14. Patterns and the API that realises them
Pattern Where it appears

Composite

The component tree: a UIComponent holds children and facets that are themselves components, and operations such as processDecodes, encodeAll and visitTree recurse over the tree

Strategy

Renderer chosen by family and renderer type; Converter and Validator attached to an input; the ClientBehaviorRenderer

Observer

FacesEvent and FacesListener for action, value-change and behaviour events; system events and their listeners (SystemEvent); PhaseListener

Decorator

RenderKitWrapper, RendererWrapper and the other FacesWrapper classes; the factories are decorated through constructors that take the wrapped instance (specification section on delegating implementation support)

Template method

UIComponentBase fixes the algorithm and offers the hooks (decode, validateValue, updateModel, encodeBegin); UIInput.validateValue is a typical hook

Factory

RenderKitFactory, Application.createComponent, and the other factories obtained through FactoryFinder

Model-View-Controller

Backing beans as model, the component tree as view, FacesServlet with the lifecycle as controller

The practical value of the list is in choosing the extension point. Changing how something looks is a Strategy: provide another renderer. Adding behaviour to an existing renderer or kit without copying it is a Decorator: wrap it. Reacting to what happens is an Observer: listen to the event. Changing what a component is — its properties and rules — is the Template method: subclass and override a hook, calling super.

Mojarra Versus MyFaces

The component API is the same on both, and the sequence of calls in the lifecycle trace was identical. The differences observed while writing this page concern output details and configuration:

Table 15. Differences observed with Mojarra 4.1.17 and MyFaces 4.1.4
Area Mojarra MyFaces

Annotation scanning of a JAR without META-INF/faces-config.xml

Scans it

Does not scan it (matches the specification)

writeText with a double quote

Not escaped

Escaped as &quot;

writeURIAttribute with <, >, "

Encodes only the quote

Encodes all three

writeAttribute(name, null, ...)

Omits the attribute

Writes name=""

<render-kit> with a new id and no class

Not created; startup error

Created

@FacesRenderer naming a missing kit

Startup error, damaged application

Silently ignored

f:ajax with an unsupported event

Message lists the supported event names

Message says the event is not valid for the component

f:ajax on a component that is not a ClientBehaviorHolder

Unable to attach <f:ajax> to non-ClientBehaviorHolder parent

Parent is not composite component or of type ClientBehaviorHolder

Ajax script written by the behaviour

mojarra.ab(...)

myfaces.ab(...)

Resource URLs on the first response of a page without a form (no session cookie yet)

no jsessionid

;jsessionid=... appended

Hidden form marker used to detect a submitted form

The form’s id as a parameter

<form id>_SUBMIT

PhaseId.toString()

RENDER_RESPONSE 6

RENDER_RESPONSE(6)

The hidden-form-marker row matters to tests: a client that posts form fields by hand, as the curl and Python scripts behind this page did, must send the marker that the implementation expects. The jsessionid row was observed on responses to requests without a session cookie and is cosmetic. For component authors the useful rule is to avoid anything that depends on the cells above: write attributes through the ResponseWriter, never pass null to writeAttribute, escape and validate URLs yourself, and ship the faces-config.xml marker.

What Changed per Version

Table 16. Component and renderer API per Faces version
Version Change

2.0

@FacesComponent, @FacesRenderer and @ResourceDependency replace most faces-config.xml registration; StateHelper and PartialStateHolder (partial state saving); ClientBehaviorHolder; Facelets becomes the standard view declaration language

2.1

TransientStateHelper

2.2

createTag, tagName and namespace on @FacesComponent (the Java EE 6 API of the annotation has no createTag, the Java EE 7 API has it); pass-through attributes (UIComponent.getPassThroughAttributes(), Renderer.PASSTHROUGH_RENDERER_LOCALNAME_KEY); ResponseWriter.writeDoctype and writePreamble

2.3 and 3.0

The default tag namespace of @FacesComponent is http://xmlns.jcp.org/jsf/component in the API documentation of both

3.0

The javax.faces packages become jakarta.faces

4.0

JSP support is removed: the JSP tag classes of jakarta.faces.webapp (UIComponentELTag, UIComponentTagBase and others, still present in the 3.0 API) are gone and the package contains only FacesServlet; the default namespace of @FacesComponent becomes jakarta.faces.component; Renderer is generic (Renderer<T extends UIComponent>); StateHelper.eval(key, Supplier); ValueBinding and MethodBinding are removed (use ValueExpression and MethodExpression)

4.1

ActionSource2 is unified into ActionSource (the expression methods become default methods and UICommand no longer implements ActionSource2), which matters to a component that tests or implements the interface; no other change in the component, renderer and render kit contract was found, and the other 4.1 additions listed on the versions page concern converters, tables and flows

5.0 (upcoming)

The old http://xmlns.jcp.org/jsf/* and http://java.sun.com/jsf/* namespaces are planned to be removed; libraries that still declare them in tag library files should move to their own namespaces

The API documentation of 4.0 and 4.1 shows jakarta.faces.component as the default. Faces 5.0 is still a milestone, and planned changes are not current behaviour.

Where the Books Are Outdated

The component chapters are the most dated part of the older Faces literature, because they were written for JSP. The books by Mann, by Dudney and colleagues, and by Jacobi and Fallows build custom components with a JSP tag handler (a subclass of UIComponentELTag or its predecessors) and a tag library descriptor (.tld) per component. Burns and Schalk, and Leonard, cover the Facelets-era approach with annotations and composite components, for which the namespace and state remarks below are the main updates. Today:

  • Tag handlers and TLDs are gone. JSP support was removed in 4.0 and the JSP tag classes with it (verified above in the API of jakarta.faces.webapp). A component gets its tag from createTag = true or a Facelets .taglib.xml. A reader porting a book example should drop the tag class and the TLD and annotate the component instead.

  • javax.faces is jakarta.faces. Every import in the older books changes (3.0), and so does the namespace default of @FacesComponent (4.0).

  • State is kept in the StateHelper. The books write saveState and restoreState with an Object[] for every property, and getValueBinding calls to read properties that the page binds. StateHelper (2.0) removes the first and eval the second; ValueBinding and MethodBinding were removed in 4.0, and UIComponent.getValueBinding, present in the 3.0 API, is absent from the 4.0 one. The Jakarta EE Tutorial still shows the hand-written pattern for a field, and its sentence that a component implementing StateHolder "also implements" StateHelper is loose: a component returns a StateHelper from getStateHelper().

  • Registration is mostly annotations. The books register every component, renderer and render kit in faces-config.xml. The elements still work (shown above) and are the only way for a class you cannot annotate; annotations (2.0) and createTag (2.2) removed most of it.

  • Ajax-enabling a component meant writing XMLHttpRequest code, Weblets or ajax4jsf glue in the Jacobi and Fallows book. The standard way is ClientBehaviorHolder with f:ajax, and component resources are @ResourceDependency on top of the ResourceHandler; Weblets are history.

  • Render kits for XUL and HTC are dead client technologies, as noted above; the RenderKit mechanism itself is unchanged and is shown in full.

  • Composite components cover many cases that the books solve with Java: the composite components page shows the XHTML-only route with a backing component, and the books' chapters on backing components and attribute retargeting map to it.

  • The design patterns chapters (Dudney and colleagues) are still accurate; the table above maps them to the current API.

Common Mistakes

  • Keeping state in a field. The value is lost on the next postback. Use the StateHelper; the probe above made the loss visible (plain=1 after plain=9).

  • Reading a bound property with a plain getter. An EL attribute is ignored (plain=1 instead of 7). Read with getStateHelper().eval(key, default).

  • Overriding encodeBegin or encodeEnd without super in a direct component. The EL component stack and the pre-render event are skipped.

  • Writing user data with write. It is unescaped output and a cross-site scripting hole. Use writeText and writeAttribute.

  • Trusting writeURIAttribute with a javascript: URL. Neither implementation filters the scheme. Use an allow list.

  • Advertising events that the renderer does not render. The f:ajax is accepted and does nothing.

  • Doing conversion or validation in decode. decode only stores the submitted value. Convert in getConvertedValue, validate in validateValue or a Validator.

  • Ignoring disabled and readonly in decode. A forged request can set a field that the page shows as locked.

  • Testing literal attributes with Boolean.TRUE.equals. A literal disabled="true" is a String.

  • Per-request fields in a renderer. A renderer is a singleton shared by all threads.

  • Injecting into a component or renderer. The field stays null. Reach beans through value expressions or CDI.current().

  • Shipping a JAR without META-INF/faces-config.xml. Works on Mojarra, fails on MyFaces.

  • Registering a render kit without a class. Works on MyFaces, fails on Mojarra.

  • Relying on the default tag namespace. It changed in 4.0; always set namespace.

  • Storing a non-serialisable object, a FacesContext or a bean reference in the StateHelper. View state is serialised.

  • Building client ids by hand. Use getClientId(context), and UINamingContainer.getSeparatorChar(context) for child ids, so the ids stay correct inside naming containers and tables.