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 aUIInputsubclass and a renderer (the delegated model), with Ajax support -
b:shelfButton: a command button that decodes a click into anActionEventand 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.
| 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 |
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 |
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 |
|
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.
| Class | What it adds, and when to extend it |
|---|---|
|
Nothing but the plumbing. Output-only containers and components that render themselves. Direct subclasses in the API include |
|
A |
|
Submitted value, local value, validators, the |
|
|
|
Containers; |
|
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.
| Interface | Meaning |
|---|---|
|
The component has a local value and may carry a converter ( |
|
Adds submitted value, validity, validators, |
|
The component fires |
|
The ids of the descendants are prefixed with the container’s id ( |
|
The component can carry client behaviours such as |
|
State saving contracts; |
|
Receiving and subscribing to system events ( |
Source: Jakarta EE Tutorial: Creating Custom Component Classes, Jakarta Faces 4.1 specification: Component Behavioral Interfaces and UIComponentBase API
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.
| Method | Role |
|---|---|
|
Required. Names the family that, with the renderer type, selects the renderer |
|
The renderer within the family; |
|
Apply Request Values, once for this component; with a renderer it delegates to |
|
Render Response; |
|
|
|
Tree traversal for the three phases; a component that is not rendered skips them |
|
Queue an event during a phase and deliver it to the listeners at the end of the phase |
|
Generic attributes, bound expressions and HTML5 pass-through attributes |
|
State, see below |
|
Structure |
|
The id on the client; descendants of a naming container get the container’s client id as a prefix |
|
Visit or find components by id (used by Ajax |
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.
| Way | What happens |
|---|---|
|
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 |
|
The type is registered; the page can use it only through a tag library file that names the type, or in Java through |
A tag library file ( |
Maps a tag to a component type and an optional renderer type, or to a hand-written |
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:
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);
}
}
<?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:
<?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:
<!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-INFand ending in.taglib.xmlis 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.
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):
.callout { border-left: 4px solid #36c; padding: .5em 1em; margin: 1em 0; }
.callout-warn { border-color: #c63; }
<!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 itfalse, so Faces rendered the two outputs right afterencodeBeginandencodeChildrenwas never called: the children follow the title without any wrapper. The second one returnedtruethroughitemised="true", soencodeChildrenran and wrapped every rendered child in adiv; the child withrendered="false"was skipped by theisRendered()test. The API recommends that component authors returntrueand implementencodeChildren, because that keeps the rendering of the children visible in the component. A component that decides at run time, as here, must makegetRendersChildren()agree with whatencodeChildrenexpects. -
A component with
rendered="false"renders nothing. The third call-out produced no markup at all.encodeAlltestsisRendered()before calling anything. -
The base-class methods are part of the contract.
UIComponentBase.encodeBeginpushes the component to the EL stack (that is what makes#{component}work while it renders) and publishes thePreRenderComponentEvent, andencodeEndpops it. A class that overrides both without callingsuperleaves the stack unbalanced and never publishes the event, so listeners registered with@ListenerFororsubscribeToEventare never called (the events page reports this effect for@ListenerForon both implementations). Callsuper.encodeBeginfirst andsuper.encodeEndlast. -
Facets are rendered by the component. The facet named
titleis rendered withencodeAllwhere 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"anditemised="true"were applied throughsetSeverityandsetItemised, with the stringtruecoerced to aboolean. An attribute written as an expression works too: a call-out withseverity="#{probeBean.log eq '' ? 'warn' : 'info'}"renderedcallout-warnwhile the bean property was empty, becausegetSeverity()goes throughStateHelper.eval, which evaluates theValueExpressionstored 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():
| Operation | Meaning |
|---|---|
|
Store a value (it stands in for an instance field) and return the previous one |
|
Store an entry in a map kept under |
|
Add to a list kept under |
|
The stored value only |
|
The stored value if there is one, otherwise the result of the |
|
The same with a default (the supplier form is new in 4.0) |
|
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:
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");
}
}
<!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:
| Request | b:stateProbe id="probe" |
b:stateProbe id="probeEl" |
|---|---|---|
initial GET |
|
|
postback with bump |
|
|
next postback (noop) |
|
|
one more postback |
|
|
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
StateHelperproperty survives.helper=9was restored on every later request, becauseputrecorded the change in the delta. -
A plain field ignores an EL attribute;
evalhonours it. The second probe receivedplain="#{probeBean.n}"andhelper="#{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 witheval. -
saveStateandrestoreStatework for what the helper cannot hold, with the pattern the Tutorial shows: anObject[], the superclass state in the first slot. The hand-savedextrafield survived (e9). Use it only for values the helper cannot hold or that need a custom serialisation. The override above savesextraon every request, which is correct but larger than a delta: thePartialStateHoldercontract says that whileinitialStateMarked()is true a component returns only what changed. Such an override must also callsaveStateon anyStateHolderobjects it references (validators, listeners) and must not save children or facets, andrestoreStatemust do nothing when the state isnull. 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.
Source: StateHelper API, TransientStateHelper API, Jakarta Faces 4.1 specification: State Saving Methods and Jakarta EE Tutorial: Saving and Restoring State
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.
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:
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:
<!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 & <go>"/>
<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:
Submitted f:isbn |
Result |
|---|---|
|
Converted to |
|
Message |
|
Message |
empty |
Message |
|
The value-change listener ran with old value |
Ajax |
The partial response updated |
The renderer follows the contract of the delegated model, and each point is a rule for every renderer:
-
decodeonly stores the submitted value. It reads the request parameter named by the client id and callssetSubmittedValue; it does not convert, validate or touch the model. A renderer must also respectdisabledandreadonly: 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. -
getConvertedValueturns the submitted value into the component value and reports a problem by throwing aConverterExceptionwith aFacesMessage. It is called byUIInput.getConvertedValueduring Process Validations; if aConverteris 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. -
encodeEndshows the submitted value when there is one. It readsgetSubmittedValue()beforegetValue(), 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 readsplaceholder,disabledand the like from the attribute map. A tag attribute that is not a property lands there, and it is aStringwhen written literally. The first draft of this renderer testedBoolean.TRUE.equals(...)anddisabled="true"did nothing: the value was the string"true", not aBoolean. The helperisTrueabove parses it. A property declared on the component class (such asrequiredonUIInput) 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, becausewriter.startElement("input", component)takes the component’s pass-through attributes (getPassThroughAttributes(), since 2.2) from the second argument. Always pass the component tostartElement. A pass-through attribute named by the constantRenderer.PASSTHROUGH_RENDERER_LOCALNAME_KEYreplaces the element name. -
writeAttributewith aBoolean.Boolean.TRUEwas written asdata-t="data-t"andBoolean.FALSEomitted by both implementations, which is why the renderer can pass booleans straight through. Anullvalue was omitted by Mojarra and written as an empty attributedata-n=""by MyFaces: do not rely on either, and test fornullyourself.
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:
@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);
}
}
<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
| Phase | Calls | Notes |
|---|---|---|
Restore View |
The tree is rebuilt; |
The component’s |
Apply Request Values |
|
Only a rendered component is visited; |
Process Validations |
|
|
Update Model Values |
|
Writes the local value through the value expression |
Invoke Application |
Action listeners and the action of commands |
A non- |
Render Response |
|
In a partial (Ajax) request only the components named by |
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.
| Method | What it does |
|---|---|
|
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 |
|
Writes |
|
Writes a URI attribute, applying URI encoding (a space becomes |
|
Writes text content with markup escaping; closes an open start tag first |
|
Writes a comment |
|
Writes raw text with no escaping at all |
|
Open and close an XML CDATA section |
|
Write a doctype or an XML preamble verbatim (since 2.2) |
|
Document boundaries; |
|
Information about the output, and a copy of the writer that writes to another |
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:
@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
}
}
<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:
<p title=""><script>alert(1)</script>&x=1 javascript:alert(2)">"><script>alert(1)</script>&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>
<p title=""><script>alert(1)</script>&x=1 javascript:alert(2)">"><script>alert(1)</script>&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:
-
writeAttributeandwriteTextescape<,>,&on both implementations, andwriteAttributealso 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 rawdivcarries a workingscriptelement: this is the cross-site scripting hole thatescape="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 towrite. UsewriteTextfor text andwriteAttributefor attributes, even for values that "can only be numbers". -
writeURIAttributeis 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 ajavascript:scheme: with the valuejavascript:alert(document.cookie)both wrotehref="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:
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.
@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");
}
}
<h:form id="f">
<b:shelfButton id="b" value="V: " action="#{probeBean.saved}">
<h:outputText value="child <b>"/>
</b:shelfButton>
</h:form>
Both implementations rendered <button id="f:b" name="f:b" type="submit">V: child <b></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, withgetEventNames()returning the names of the events a behaviour can attach to andgetDefaultEventName()the one used whenf:ajaxhas noeventattribute.UIComponentBasealready providesaddClientBehaviorandgetClientBehaviors(). -
In the renderer,
decodemust give each behaviour a chance to decode when it is the source of the request: it compares the request parametersjakarta.faces.behavior.eventandjakarta.faces.source(the constantsClientBehaviorContext.BEHAVIOR_EVENT_PARAM_NAMEandBEHAVIOR_SOURCE_PARAM_NAME) with the component’s event names and client id, and callsbehavior.decode(context, component). For anAjaxBehaviorthat queues theAjaxBehaviorEvent, which is how thelisteneroff:ajaxruns. -
In the encode method, the renderer asks each behaviour for its script with
ClientBehaviorContext.createClientBehaviorContext(context, component, eventName, sourceId, parameters)andbehavior.getScript(...), and writes the result into the DOM event handler attribute (onchangein the example). Mojarra producedonchange="mojarra.ab('f:isbn',event,'valueChange',0,'f:out');"and MyFacesonchange="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:
| Page fragment | Mojarra 4.1.17 | MyFaces 4.1.4 |
|---|---|---|
|
|
|
|
|
|
|
The page rendered with no error, but the input has no |
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:
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.
Source: ResourceDependency API and UIViewRoot API
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.
@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");
}
}
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.
Source: ResourceDependency API, ResourceDependencies API and Renderer API
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:
@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
}
}
<?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:
<!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:
| Situation | Mojarra 4.1.17 | MyFaces 4.1.4 |
|---|---|---|
A view names an id that no kit has |
Error page |
|
A |
Startup log: |
MyFaces created a kit, the renderer was registered and the view used it |
|
Startup log: |
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:
<?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:
| JAR contents | Mojarra 4.1.17 | MyFaces 4.1.4 |
|---|---|---|
Classes and |
|
|
Plus |
Components, tags, resources and kit worked; |
The same, and |
Plus |
Everything worked, including |
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-INFof a JAR whose name ends in.taglib.xmlis 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 parameterjakarta.faces.FACELETS_LIBRARIES(observed above). -
Resources.
META-INF/resources/<library>/<resource>is the classpath equivalent of the web application’sresources/<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 insideMETA-INF/resources. -
Ordering. When several libraries register the same artifact, the order is defined by
<name>and<ordering>in eachfaces-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
@Injectfield in theStateProbecomponent and in theIsbnRendererstayednullon 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 throughCDI.current(), never through injection into the component, and never keeps the result in theStateHelper. If the JAR contains beans, addMETA-INF/beans.xmlso 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
@FacesComponentchanged 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.
| Pattern | Where it appears |
|---|---|
Composite |
The component tree: a |
Strategy |
|
Observer |
|
Decorator |
|
Template method |
|
Factory |
|
Model-View-Controller |
Backing beans as model, the component tree as view, |
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:
| Area | Mojarra | MyFaces |
|---|---|---|
Annotation scanning of a JAR without |
Scans it |
Does not scan it (matches the specification) |
|
Not escaped |
Escaped as |
|
Encodes only the quote |
Encodes all three |
|
Omits the attribute |
Writes |
|
Not created; startup error |
Created |
|
Startup error, damaged application |
Silently ignored |
|
Message lists the supported event names |
Message says the event is not valid for the component |
|
|
|
Ajax script written by the behaviour |
|
|
Resource URLs on the first response of a page without a form (no session cookie yet) |
no |
|
Hidden form marker used to detect a submitted form |
The form’s id as a parameter |
|
|
|
|
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
| Version | Change |
|---|---|
2.0 |
|
2.1 |
|
2.2 |
|
2.3 and 3.0 |
The default tag namespace of |
3.0 |
The |
4.0 |
JSP support is removed: the JSP tag classes of |
4.1 |
|
5.0 (upcoming) |
The old |
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 fromcreateTag = trueor a Facelets.taglib.xml. A reader porting a book example should drop the tag class and the TLD and annotate the component instead. -
javax.facesisjakarta.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 writesaveStateandrestoreStatewith anObject[]for every property, andgetValueBindingcalls to read properties that the page binds.StateHelper(2.0) removes the first andevalthe second;ValueBindingandMethodBindingwere removed in 4.0, andUIComponent.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 implementingStateHolder"also implements"StateHelperis loose: a component returns aStateHelperfromgetStateHelper(). -
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) andcreateTag(2.2) removed most of it. -
Ajax-enabling a component meant writing
XMLHttpRequestcode, Weblets or ajax4jsf glue in the Jacobi and Fallows book. The standard way isClientBehaviorHolderwithf:ajax, and component resources are@ResourceDependencyon top of theResourceHandler; Weblets are history. -
Render kits for XUL and HTC are dead client technologies, as noted above; the
RenderKitmechanism 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=1afterplain=9). -
Reading a bound property with a plain getter. An EL attribute is ignored (
plain=1instead of 7). Read withgetStateHelper().eval(key, default). -
Overriding
encodeBeginorencodeEndwithoutsuperin 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. UsewriteTextandwriteAttribute. -
Trusting
writeURIAttributewith ajavascript:URL. Neither implementation filters the scheme. Use an allow list. -
Advertising events that the renderer does not render. The
f:ajaxis accepted and does nothing. -
Doing conversion or validation in
decode.decodeonly stores the submitted value. Convert ingetConvertedValue, validate invalidateValueor aValidator. -
Ignoring
disabledandreadonlyindecode. A forged request can set a field that the page shows as locked. -
Testing literal attributes with
Boolean.TRUE.equals. A literaldisabled="true"is aString. -
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 orCDI.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
FacesContextor a bean reference in theStateHelper. View state is serialised. -
Building client ids by hand. Use
getClientId(context), andUINamingContainer.getSeparatorChar(context)for child ids, so the ids stay correct inside naming containers and tables.