Extending Faces: Wrappers, Factories and Listeners
|
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 Expression Language 6.0, Jakarta Servlet 6.1 and Java 17 or later (the examples
were compiled with --release 21). The extension mechanisms described here are stable: everything below also applies to
Faces 4.0 unless the version section says otherwise, but only 4.1 was run. The code of this page was deployed as an exploded
Bookshelf WAR on Tomcat 11.0.26 with Weld 6.0.4.Final, once with Mojarra and once with MyFaces, and driven with curl and
a small script that performs the form POSTs (every header, redirect and log line shown was observed on 2026-10-08). Claims
that were not run are marked as such.
Almost everything that Faces does is done by an object that can be replaced: the object that builds a view, the one that
serves a stylesheet, the one that decides where an outcome leads, the one that resolves #{name}, the one that creates the
request context. None of these replacements needs a fork of Mojarra or MyFaces and none needs the implementation’s internal
classes. The specification defines one uniform recipe, the decorator pattern with FacesWrapper classes, plus a handful of
listener interfaces for the places where you only want to observe. This page is the map of those extension points and a
worked example of each of the non-visual ones. The visual ones — components, renderers, render kits, converters,
validators, behaviours — have their own pages, and the places where the other pages already show an extension point in depth
are named in the table that follows.
The Extension Points at a Glance
The table lists what a project can replace or observe, the base class or interface to extend, the element of
faces-config.xml that registers it, and where this section treats it. A *Wrapper class implements the whole type by
delegating to the wrapped instance, so a subclass overrides only what it changes.
| Need | Extend | Registered with (faces-config.xml) |
Covered |
|---|---|---|---|
Change how views are built, found, rendered, or how locale and URLs are calculated |
|
|
ViewHandlerWrapper below |
Change how stylesheets, scripts and images are found and served |
|
|
ResourceHandlerWrapper and ResourceWrapper below; the resource handling page |
Change how an outcome leads to a view |
|
|
NavigationHandlerWrapper and ConfigurableNavigationHandlerWrapper below; the navigation page |
Add names or properties to Expression Language |
|
|
A Custom ELResolver below; the expression language page |
Run code around a lifecycle phase on every request |
|
|
PhaseListener below; the request processing lifecycle page |
React to something that happened in the framework (application start, view rendered, component added) |
|
|
SystemEventListener below; the events and listeners page |
Wrap every action event |
|
|
|
Replace how view state is saved |
|
|
|
Decorate the objects that exist once per request |
|
|
Factories: Decorating What Is Created Once per Request below; the architecture, Ajax and exception handling pages |
Decorate the application-wide objects: the |
|
|
|
Decorate rendering |
|
|
|
Add search keywords or change how search expressions are resolved |
|
|
The Decorator Pattern the Specification Mandates
Section 11.3.7 of the specification requires the runtime to support decoration for every artifact that has a wrapper class.
The list names about three dozen types, from Application and ViewHandler to VisitContext and
ViewDeclarationLanguage, and says "among others". The mechanism has three parts.
-
The extension class extends the wrapper class (
ViewHandlerWrapper,ResourceHandlerWrapper, …) or implements the type itself, and declares a public constructor with one parameter, of the type it extends. -
When Faces starts and reads the configuration, it creates the extension through that constructor and passes in the instance that filled the role before.
getWrapped()(from thejakarta.faces.FacesWrapperinterface, which every wrapper implements) returns it. -
The extension overrides the methods it wants to change and calls
getWrapped()orsuperfor the rest.
Three consequences follow, and all three were observed on Mojarra and on MyFaces.
-
The one-argument constructor is what makes a class a decorator. A class that has only a public no-argument constructor is not decorating: it replaces the previous instance, and anything it does not implement is lost. A
NavigationHandlerwith a no-argument constructor that only logged was installed on both implementations, and from then on no form navigated anywhere, because nothing called the original. Use the replacement form only when that is the intent. -
The no-argument constructors of the wrapper classes (
ViewHandlerWrapper(),ResourceHandlerWrapper(),NavigationHandlerWrapper(),ResourceWrapper()) are deprecated in the 4.1 API ("use the other constructor taking the implementation being wrapped"). The constructors that take the wrapped object exist since Faces 2.3. Always use them. -
Several extensions of the same artifact form a chain, and the order of the chain is the order in which the configuration resources are processed, which section Ordering Several Extensions explains. Faces itself may also put its own wrappers into the chain.
Wrappers also work in code, outside the registration mechanism: new ResourceWrapper(resource) in a handler, or the
FacesContextWrapper created by a factory in the architecture page, is the same decorator used by hand.
The first complete example is the one the specification itself uses: a ViewHandler that changes how the locale is
calculated and delegates everything else.
ViewHandlerWrapper
The ViewHandler is the application’s object for everything about a view that is not the view’s content: it derives
the view id from the request path (deriveViewId), restores or creates the view (restoreView, createView), renders it
(renderView), builds the URLs of actions, redirects, bookmarks, resources and WebSocket channels (getActionURL,
getRedirectURL, getBookmarkableURL, getResourceURL, getWebsocketURL), answers which view declaration language
handles a view (getViewDeclarationLanguage), manages the protected views (addProtectedView, getProtectedViewsUnmodifiable,
removeProtectedView), and calculates the locale, character encoding and render kit id (calculateLocale,
calculateCharacterEncoding, calculateRenderKitId). ViewHandlerWrapper (since Faces 1.2) delegates all of them.
The Bookshelf catalogue is offered in English and Spanish. The handler below lets a lang request parameter choose the
locale, remembers the choice in the session, and otherwise leaves the decision to the wrapped handler. The parameter is
checked against the locales that the application declares as supported, so an arbitrary value never reaches the session.
package org.example.bookshelf.web;
import jakarta.faces.application.ViewHandler;
import jakarta.faces.application.ViewHandlerWrapper;
import jakarta.faces.context.ExternalContext;
import jakarta.faces.context.FacesContext;
import java.util.Iterator;
import java.util.Locale;
import java.util.logging.Logger;
/**
* Lets a "lang" request parameter choose the locale, remembers the choice in the session and otherwise keeps the
* default calculation of the wrapped ViewHandler.
*/
public class LocaleViewHandler extends ViewHandlerWrapper {
private static final String KEY = LocaleViewHandler.class.getName() + ".locale";
private static final Logger LOG = Logger.getLogger(LocaleViewHandler.class.getName());
// Faces calls this constructor and passes the ViewHandler that was installed before this one.
public LocaleViewHandler(ViewHandler wrapped) {
super(wrapped);
}
@Override
public Locale calculateLocale(FacesContext context) {
ExternalContext external = context.getExternalContext();
String lang = external.getRequestParameterMap().get("lang");
Locale requested = lang == null ? null : findSupported(context, lang);
if (requested != null) {
external.getSessionMap().put(KEY, requested);
return requested;
}
if (external.getSession(false) != null && external.getSessionMap().get(KEY) instanceof Locale remembered) {
return remembered;
}
return getWrapped().calculateLocale(context);
}
private Locale findSupported(FacesContext context, String language) {
Iterator<Locale> supported = context.getApplication().getSupportedLocales();
while (supported.hasNext()) {
Locale candidate = supported.next();
if (candidate.getLanguage().equals(language)) {
return candidate;
}
}
LOG.fine(() -> "Ignoring unsupported language " + language);
return null;
}
}
It is registered in WEB-INF/faces-config.xml, next to the locales it validates against:
<?xml version="1.0" encoding="UTF-8"?>
<faces-config xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-facesconfig_4_1.xsd"
version="4.1">
<application>
<locale-config>
<default-locale>en</default-locale>
<supported-locale>es</supported-locale>
</locale-config>
<view-handler>org.example.bookshelf.web.LocaleViewHandler</view-handler>
</application>
</faces-config>
The page that shows the result prints #{facesContext.viewRoot.locale}. The runs below used curl-style requests with
a cookie jar: a GET with ?lang=es and then a POST of the form without the parameter.
GET /catalogue.xhtml?lang=es -> locale es
POST /catalogue.xhtml (no lang) -> locale es (remembered in the session)
GET /catalogue.xhtml?lang=xx -> locale en (xx is not supported; the default applies)
The same outcome on Mojarra and on MyFaces. Three details matter when you write a ViewHandler decorator.
-
calculateLocaleis called for the postback as well as for the initial request, on both implementations: a first version of this handler that read only the parameter producedeson the GET andenon the POST, because the POST did not carrylang. A decision that has to last must be stored by the handler itself, as here. -
The decorator sits in a chain with the implementation’s own wrappers.
getWrapped().getClass()is therefore not necessarily the class you expect: with no other extension installed,LocaleViewHandlerwrapped aConversationAwareViewHandleron both implementations, an internal CDI helper of Mojarra or of MyFaces and not the default handler itself, and the ordering section below shows that helper moving around when more decorators are present. Do not castgetWrapped()to an implementation type. -
An
@Injectfield of aViewHandlernamed infaces-config.xmlstayednullon both implementations (the instance is created by Faces through the constructor, not by CDI).CDI.current()works on both when a decorator really needs a bean; prefer to read what you need from theFacesContextinstead.
The wrapper is the right tool for the cases the ViewHandler was designed for: a different way to compute the locale, a
different URL scheme (getActionURL, getBookmarkableURL), a global pre- or post-processing of rendering
(renderView), extra protected views. It is not the place for per-request business logic, and it must be thread-safe: one
instance serves every request.
ResourceHandlerWrapper and ResourceWrapper
The ResourceHandler finds the files of the resources directory (and of META-INF/resources in JARs), answers
requests to /jakarta.faces.resource/*, and tracks which scripts and stylesheets a page already rendered. Its methods fall
into three groups, and ResourceHandlerWrapper delegates all of them:
-
creating resources:
createResource(name),createResource(name, library),createResource(name, library, contentType),createResourceFromId(id)(since Faces 2.2) andcreateViewResource(context, name)(the lookup that finds Facelets files and, throughgetViewResources, the views of a directory); -
serving:
isResourceRequest(context),handleResourceRequest(context),isResourceURL(url),libraryExists(name); -
page bookkeeping:
markResourceRendered,isResourceRendered(since 2.3) andgetRendererTypeForResourceName.
A decorator that changes what is served overrides the createResource methods and returns a different, or a wrapped,
Resource. ResourceWrapper is the matching wrapper for the Resource object, with getInputStream(), getURL(),
getResponseHeaders(), getRequestPath(), getContentType(), getLibraryName(), getResourceName() and
userAgentNeedsUpdate. The Bookshelf example adds a cache header to the files of its own library bookshelf and leaves all
other resources alone:
package org.example.bookshelf.web;
import jakarta.faces.application.Resource;
import jakarta.faces.application.ResourceHandler;
import jakarta.faces.application.ResourceHandlerWrapper;
import jakarta.faces.application.ResourceWrapper;
import java.util.HashMap;
import java.util.Map;
/** Adds a Cache-Control header to the resources of the "bookshelf" library; everything else is untouched. */
public class CacheControlResourceHandler extends ResourceHandlerWrapper {
public CacheControlResourceHandler(ResourceHandler wrapped) {
super(wrapped);
}
@Override
public Resource createResource(String resourceName, String libraryName) {
return decorate(super.createResource(resourceName, libraryName), libraryName);
}
@Override
public Resource createResource(String resourceName, String libraryName, String contentType) {
return decorate(super.createResource(resourceName, libraryName, contentType), libraryName);
}
private Resource decorate(Resource resource, String libraryName) {
if (resource == null || !"bookshelf".equals(libraryName)) {
return resource; // a missing resource must stay null
}
return new ResourceWrapper(resource) {
@Override
public Map<String, String> getResponseHeaders() {
Map<String, String> headers = new HashMap<>(super.getResponseHeaders());
headers.put("Cache-Control", "public, max-age=3600");
return headers;
}
};
}
}
<application>
<resource-handler>org.example.bookshelf.web.CacheControlResourceHandler</resource-handler>
</application>
With bookshelf/site.css and other/other.css in the resources directory, the stylesheet link in the page is
/bookshelf/jakarta.faces.resource/site.css.xhtml?ln=bookshelf (the .xhtml suffix comes from the extension mapping of the
FacesServlet), and the responses were:
GET .../site.css.xhtml?ln=bookshelf 200 Cache-Control: public, max-age=3600
GET .../other.css.xhtml?ln=other 200 Cache-Control: no-store, must-revalidate (Mojarra, Development stage)
Cache-Control: no-cache (MyFaces, Development stage)
GET .../none.css.xhtml?ln=bookshelf 404
The header of the decorated resource replaced the implementation’s default on both, and the missing resource still produced
a 404 because the decorator returns null unchanged. Three rules keep a resource handler decorator correct:
-
Return
nullwhen the wrapped handler returnsnull: anullis how a missing resource is reported, and the 404 of the example depends on it. -
A decorator that overrides
createResourcemust override every variant that matters to it. The header appeared on both implementations with only the two variants above overridden; a decorator that changes names or libraries should also considercreateResourceFromId(not run here), which creates a resource from an identifier. -
Resource requests do not run the lifecycle. The
X-Request-Idheader that the phase listener below adds to every page response was absent from the resource responses on both implementations, so a decorator is the place for resource-specific logic and aPhaseListeneris not. A servlet filter is the place for logic that must see all requests.
The resource handling page covers the resource directories, libraries, versioning, the
ResourceHandler constants and the jakarta.faces.RESOURCE_EXCLUDES parameter that decide what a request reaches.
NavigationHandlerWrapper and ConfigurableNavigationHandlerWrapper
The NavigationHandler turns the outcome of an action into a navigation. It has one working method,
handleNavigation(context, fromAction, outcome), plus the overload with a toFlowDocumentId for flows.
NavigationHandlerWrapper (since Faces 2.2) delegates both. The navigation page explains the algorithm and
shows a logging decorator; this page shows the point that matters for extensions: there are two base classes, and they are
not interchangeable.
ConfigurableNavigationHandler adds getNavigationCase, getNavigationCases and performNavigation, and
Faces' own handler is of this type. Components that need to know the target of an outcome without navigating — h:link, h:button, and every UIOutcomeTarget component — call the configurable methods. A decorator that extends
only NavigationHandlerWrapper is a plain NavigationHandler, so the object that Application.getNavigationHandler()
returns is no longer configurable. The Bookshelf migration needs the old site’s outcomes (home, books) to keep
working while the pages move to catalogue. This is the decorator written the safe way, with the configurable wrapper:
package org.example.bookshelf.web;
import jakarta.faces.application.ConfigurableNavigationHandler;
import jakarta.faces.application.ConfigurableNavigationHandlerWrapper;
import jakarta.faces.context.FacesContext;
import java.util.Map;
/** Maps the outcomes of the old site to the current ones before the wrapped handler sees them. */
public class LegacyOutcomeNavigationHandler extends ConfigurableNavigationHandlerWrapper {
private static final Map<String, String> ALIASES = Map.of("home", "catalogue", "books", "catalogue");
public LegacyOutcomeNavigationHandler(ConfigurableNavigationHandler wrapped) {
super(wrapped);
}
@Override
public void handleNavigation(FacesContext context, String fromAction, String outcome) {
String mapped = outcome == null ? null : ALIASES.getOrDefault(outcome, outcome);
super.handleNavigation(context, fromAction, mapped);
}
}
<application>
<navigation-handler>org.example.bookshelf.web.LegacyOutcomeNavigationHandler</navigation-handler>
</application>
<navigation-rule>
<navigation-case>
<from-outcome>catalogue</from-outcome>
<to-view-id>/catalogue.xhtml</to-view-id>
<redirect/>
</navigation-case>
</navigation-rule>
Both buttons action="home" and action="books" answered 302 with Location: /bookshelf/catalogue.xhtml on Mojarra and
MyFaces. The plain-wrapper version of the same class (PlainLegacyNavigationHandler extends NavigationHandlerWrapper,
constructor taking a NavigationHandler) navigated just as well for buttons, and showed the difference in the links:
ConfigurableNavigationHandlerWrapper NavigationHandlerWrapper
h:link outcome="catalogue" Mojarra link works disabled: "...navigation case could not be matched"
h:link outcome="catalogue" MyFaces link works link works
On Mojarra the plain wrapper disabled even the link whose outcome was valid; MyFaces tolerated it. The portable rule is
therefore: extend ConfigurableNavigationHandlerWrapper, whose constructor takes a ConfigurableNavigationHandler.
The decorator above changes the outcome only in handleNavigation, so the links did not benefit from the alias: the
h:link outcome="home" of the same page stayed disabled with the alias handler on Mojarra (and was rendered as plain text
without a target on MyFaces), because getNavigationCase still saw home. An alias that must also work for links
would have to be applied in getNavigationCase as well (not run here). This is the typical trap of partial
decoration: every method the other components call must agree.
A Custom ELResolver
Expression Language evaluates #{features.reviews} by asking a chain of jakarta.el.ELResolver objects, first for the top-level
name features (with a null base) and then for the property reviews of whatever the first answer was. Faces builds the
chain from the implicit objects, the CDI resolver and its own resolvers; an application adds its resolvers in
faces-config.xml or in code, and they are consulted before the bean, list, map and array resolvers. The expression
language page explains how the chain works and shows a resolver that exposes build information; this one
exposes feature flags, to show what a resolver that handles two levels (the name and then the properties) looks like.
A flag bookshelf.feature.reviews is a system property (set with -Dbookshelf.feature.reviews=true or by the server
configuration), and the page asks #{features.reviews}. The resolver reads only the flags it knows, never a property chosen by
the page, so a view cannot turn an expression into a read of an arbitrary system property.
package org.example.bookshelf.web;
import jakarta.el.ELContext;
import jakarta.el.ELResolver;
import jakarta.el.PropertyNotWritableException;
import java.util.Set;
/** Exposes #{features.reviews} and #{features.live}: the system properties bookshelf.feature.* as booleans. */
public class FeatureFlagsELResolver extends ELResolver {
private static final Set<String> KNOWN = Set.of("reviews", "live");
/** The marker object that #{features} resolves to; further segments are resolved by this resolver too. */
public static final class Features {
private Features() { }
}
private static final Features FEATURES = new Features();
@Override
public Object getValue(ELContext context, Object base, Object property) {
if (base == null && "features".equals(property)) {
context.setPropertyResolved(true);
return FEATURES;
}
if (base instanceof Features && property instanceof String name) {
context.setPropertyResolved(true);
// Only whitelisted names are read: never turn an arbitrary property into a system property lookup.
return KNOWN.contains(name) && Boolean.getBoolean("bookshelf.feature." + name);
}
return null;
}
@Override
public Class<?> getType(ELContext context, Object base, Object property) {
if (isMine(base, property)) {
context.setPropertyResolved(true);
}
return null;
}
@Override
public void setValue(ELContext context, Object base, Object property, Object value) {
if (isMine(base, property)) {
throw new PropertyNotWritableException("feature flags are read-only");
}
}
@Override
public boolean isReadOnly(ELContext context, Object base, Object property) {
if (isMine(base, property)) {
context.setPropertyResolved(true);
return true;
}
return false;
}
@Override
public Class<?> getCommonPropertyType(ELContext context, Object base) {
return base == null || base instanceof Features ? String.class : null;
}
private boolean isMine(Object base, Object property) {
return (base == null && "features".equals(property)) || base instanceof Features;
}
}
<application>
<el-resolver>org.example.bookshelf.web.FeatureFlagsELResolver</el-resolver>
</application>
<h:outputText value="#{features.reviews}"/>
<h:outputText value="#{features.secret}"/>
Started with -Dbookshelf.feature.reviews=true -Dbookshelf.feature.secret=true, the page rendered true for features.reviews
and false for features.secret (set, but not on the whitelist) on Mojarra and on MyFaces. The page without the properties
rendered false for both. Notes on writing resolvers:
-
Call
setPropertyResolved(true)only when the resolver really handled the segment. A resolver that claims too much hides beans and implicit objects. -
getFeatureDescriptors()no longer exists in the Expression Language 6.0 API, so the class has the five abstract methods shown and no more. -
The resolver can also be added in code with
Application.addELResolver(resolver), typically from a listener for the application start (the section on system event listeners below shows such a listener).Application.addELContextListeneris the hook for code that must configure eachELContext. -
An
@Injectfield in the resolver wasnullon Mojarra and injected on MyFaces; useCDI.current()if a bean is needed, or keep the resolver free of state, as here. -
VariableResolverandPropertyResolver, the JSF 1.x ways of adding names, were removed in Faces 4.0. Everything they did is done with anELResolver.
PhaseListener
A PhaseListener is called before and after each phase of the request processing lifecycle. It sees every Faces request,
which makes it the right place for cross-cutting behaviour that depends on lifecycle position: timing, auditing, a request
id, a per-request setup and clean-up. The interface has three methods: getPhaseId() (the phase it cares about, or
PhaseId.ANY_PHASE), beforePhase(PhaseEvent) and afterPhase(PhaseEvent). The specification adds two rules that are
easy to forget:
-
If
beforePhasewas called on a listener,afterPhasewill also be called, even when the phase threw an exception. TheafterPhasecalls run in the reverse order of thebeforePhasecalls. If listener B throws inbeforePhase, listener C is never called, and neitherafterPhaseof B or C. -
Exceptions thrown by the listeners are caught and published to the
ExceptionHandler, they do not propagate out of the lifecycle.
Listeners must be thread-safe: Faces creates one instance of a listener named in faces-config.xml and calls it from every
request thread, so a per-request value cannot be a field. The request map is the place for it.
The example gives every Faces request an id. It generates a random UUID in beforePhase(RESTORE_VIEW) — the first moment of
the lifecycle — stores it in the request map for the beans and logs, and sends it back in an X-Request-Id response header
that a support person can ask the user for.
package org.example.bookshelf.web;
import jakarta.faces.context.ExternalContext;
import jakarta.faces.event.PhaseEvent;
import jakarta.faces.event.PhaseId;
import jakarta.faces.event.PhaseListener;
import java.util.UUID;
import java.util.logging.Logger;
/** Gives every Faces request an id, in the request map and in an X-Request-Id response header. */
public class RequestIdPhaseListener implements PhaseListener {
public static final String ATTRIBUTE = RequestIdPhaseListener.class.getName() + ".id";
private static final Logger LOG = Logger.getLogger(RequestIdPhaseListener.class.getName());
@Override
public PhaseId getPhaseId() {
return PhaseId.RESTORE_VIEW;
}
@Override
public void beforePhase(PhaseEvent event) {
ExternalContext external = event.getFacesContext().getExternalContext();
String id = UUID.randomUUID().toString();
external.getRequestMap().put(ATTRIBUTE, id);
external.setResponseHeader("X-Request-Id", id);
LOG.fine(() -> "request id " + id);
}
@Override
public void afterPhase(PhaseEvent event) {
// nothing to do
}
}
<lifecycle>
<phase-listener>org.example.bookshelf.web.RequestIdPhaseListener</phase-listener>
</lifecycle>
Every page, postback and redirect response of the example carried an X-Request-Id header (a GET, a POST that answered
200, and a POST that answered 302, on both implementations). The responses that ended in a server error did not: the
container discarded the headers when it rendered its error page. As noted in the resource section, requests for resources
did not carry it either, because they do not run the lifecycle.
A listener can be attached in four places, and the events and listeners page of this section shows the order in which they run when several are present:
-
<lifecycle><phase-listener>infaces-config.xml, for the whole application (this example); -
Lifecycle.addPhaseListener(listener)in code, for example from a start-up listener; -
<f:phaseListener type="..."/>insidef:view, for one view; -
the
beforePhaseandafterPhaseattributes off:view, for one view and with method expressions.
SystemEventListener
A SystemEventListener observes the events that the framework publishes about itself: the application starting
(PostConstructApplicationEvent) or stopping (PreDestroyApplicationEvent), a component being added to the tree, a view
about to render (PreRenderViewEvent), a validation about to run, and so on. The interface has two methods:
isListenerForSource(Object), which says whether this listener wants the events of a given source object, and
processEvent(SystemEvent). Listeners that run for every event of a type must therefore check the source cheaply.
The events and listeners page lists all events and their order. This page shows the extension side: how a listener is registered, and the pattern most libraries use, a listener for the application start that does the setup that needs code.
Registration has two forms. In faces-config.xml the system-event-listener element names the listener class, the event
class (SystemEvent if omitted) and optionally the source class. In code, Application.subscribeToEvent(eventClass, listener) (or the overload with a source class) subscribes, and unsubscribeFromEvent removes it. The example uses both.
ExtensionBootstrap is registered in XML for PostConstructApplicationEvent, with Application as source, and subscribes a
second listener in code. The second one adds two defensive headers to each Faces page just before it renders:
package org.example.bookshelf.web;
import jakarta.faces.application.Application;
import jakarta.faces.event.AbortProcessingException;
import jakarta.faces.event.PostConstructApplicationEvent;
import jakarta.faces.event.PreRenderViewEvent;
import jakarta.faces.event.SystemEvent;
import jakarta.faces.event.SystemEventListener;
import java.util.logging.Logger;
/** Runs once when the Application is ready and registers the listeners that need code to be set up. */
public class ExtensionBootstrap implements SystemEventListener {
private static final Logger LOG = Logger.getLogger(ExtensionBootstrap.class.getName());
@Override
public boolean isListenerForSource(Object source) {
return source instanceof Application;
}
@Override
public void processEvent(SystemEvent event) throws AbortProcessingException {
Application application = (Application) event.getSource();
application.subscribeToEvent(PreRenderViewEvent.class, new SecurityHeadersListener());
LOG.info("SecurityHeadersListener subscribed for " + PostConstructApplicationEvent.class.getSimpleName());
}
}
package org.example.bookshelf.web;
import jakarta.faces.component.UIViewRoot;
import jakarta.faces.context.ExternalContext;
import jakarta.faces.context.FacesContext;
import jakarta.faces.event.AbortProcessingException;
import jakarta.faces.event.SystemEvent;
import jakarta.faces.event.SystemEventListener;
/** Adds two defensive response headers just before a view is rendered. */
public class SecurityHeadersListener implements SystemEventListener {
@Override
public boolean isListenerForSource(Object source) {
return source instanceof UIViewRoot;
}
@Override
public void processEvent(SystemEvent event) throws AbortProcessingException {
ExternalContext external = FacesContext.getCurrentInstance().getExternalContext();
if (!external.isResponseCommitted()) {
external.setResponseHeader("X-Content-Type-Options", "nosniff");
external.setResponseHeader("Referrer-Policy", "same-origin");
}
}
}
<application>
<system-event-listener>
<system-event-listener-class>org.example.bookshelf.web.ExtensionBootstrap</system-event-listener-class>
<system-event-class>jakarta.faces.event.PostConstructApplicationEvent</system-event-class>
<source-class>jakarta.faces.application.Application</source-class>
</system-event-listener>
</application>
At start-up both implementations logged SecurityHeadersListener subscribed for PostConstructApplicationEvent once, and
each page response then carried X-Content-Type-Options: nosniff and Referrer-Policy: same-origin. The listener is a
demonstration of the mechanism; headers that apply to every response of an application, not only to Faces views, belong in a
servlet filter or in the server configuration, and the security page treats the headers a Faces
application should send.
Notes that apply to all system event listeners:
-
The listener named in XML is created by Faces with its public no-argument constructor. An
@Injectfield in it wasnullon Mojarra and injected on MyFaces (the events and listeners page records the same difference); useCDI.current()for portable code. -
Faces does not fire CDI events for system events. The events and listeners page shows a bridge that does.
-
A listener subscribed in code is subscribed for the life of the application; subscribe it once, from a start-up event, and not from request code, or listeners accumulate.
-
The specification publishes
PostConstructApplicationEventimmediately after all application configuration resources have been processed, so it is the moment to register things inApplicationprogrammatically (addELResolver,addELContextListener). It is the usual alternative to aServletContextListenerin a library that must work with any server.
Factories: Decorating What Is Created Once per Request
A factory creates the objects that Faces needs per request, per application or per view: the FacesContext, the
ExternalContext, the PartialViewContext, the ExceptionHandler, the Flash, the Lifecycle, the RenderKit, the
ClientWindow, the Application itself. FactoryFinder.getFactory(name) returns the factory registered under one of
the FactoryFinder constants, and every factory type is decoratable in the same way as a handler: the extension class
extends the factory, takes the previous factory in its constructor, and delegates to it. The usual shape is a factory that
asks the wrapped factory for the object and wraps the result in a wrapper.
| Element | Factory | Creates (wrapper) |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
the Facelet cache and the tag handler delegates |
The Bookshelf example protects its redirects. Anything that calls ExternalContext.redirect(url) — an action that
redirects to a returnTo address, the navigation handler for a faces-redirect=true outcome, a viewAction — goes through
that method, so a decorator on the ExternalContext is a single place to refuse redirects that leave the application. A log line in
redirect showed that both an action calling redirect directly and a navigation case with <redirect/> end there, on
Mojarra and on MyFaces. The
decorator allows relative paths and the https URLs of hosts listed in a context parameter, and rejects everything else, in
particular absolute URLs to other hosts and the //host form that browsers read as a different host. The allow-list comes
from web.xml, not from the code, and holds no secret.
package org.example.bookshelf.web;
import jakarta.faces.FacesException;
import jakarta.faces.context.ExternalContext;
import jakarta.faces.context.ExternalContextFactory;
import jakarta.faces.context.ExternalContextWrapper;
import java.io.IOException;
import java.net.URI;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
/** Decorates the ExternalContext so that redirect() cannot leave the application (or an allow-list of hosts). */
public class SafeRedirectExternalContextFactory extends ExternalContextFactory {
public SafeRedirectExternalContextFactory(ExternalContextFactory wrapped) {
super(wrapped);
}
@Override
public ExternalContext getExternalContext(Object context, Object request, Object response) throws FacesException {
ExternalContext original = getWrapped().getExternalContext(context, request, response);
return new SafeRedirectExternalContext(original);
}
static final class SafeRedirectExternalContext extends ExternalContextWrapper {
SafeRedirectExternalContext(ExternalContext wrapped) {
super(wrapped);
}
@Override
public void redirect(String url) throws IOException {
if (!isAllowed(url)) {
throw new IllegalArgumentException("Redirect to " + url + " is not allowed");
}
super.redirect(url);
}
private boolean isAllowed(String url) {
if (url == null) {
return false;
}
URI uri;
try {
uri = URI.create(url);
} catch (IllegalArgumentException e) {
return false;
}
if (uri.getHost() == null) {
return !url.startsWith("//") && !url.startsWith("\\"); // relative to this server
}
String allowed = getWrapped().getInitParameter("bookshelf.redirect.allowedHosts");
Set<String> hosts = allowed == null ? Set.of()
: Arrays.stream(allowed.split(",")).map(String::trim).collect(Collectors.toSet());
return hosts.contains(uri.getHost()) && "https".equalsIgnoreCase(uri.getScheme());
}
}
}
<factory>
<external-context-factory>org.example.bookshelf.web.SafeRedirectExternalContextFactory</external-context-factory>
</factory>
<context-param>
<param-name>bookshelf.redirect.allowedHosts</param-name>
<param-value>login.example.org</param-value>
</context-param>
Three buttons of the test page call an action that redirects. The results were the same on Mojarra and MyFaces:
redirect(contextPath + "/catalogue.xhtml") -> 302 Location: /bookshelf/catalogue.xhtml
redirect("https://login.example.org/start") -> 302 Location: https://login.example.org/start (host on the allow-list)
redirect("https://evil.example/phish") -> 500 IllegalArgumentException: Redirect to https://evil.example/phish is not allowed
The refused redirect surfaces as an exception, so the exception handling page applies to it: in production
an error page, not the stack trace, is what the user should see. A redirect that is produced by user input (a returnTo
parameter) should still be validated where it is read; the decorator is the second line of defence, not the first.
Factories can be registered in three ways, and the first two decorate in the same way:
-
<factory>elements inWEB-INF/faces-config.xmlor inMETA-INF/faces-config.xmlof a JAR (the usual way); -
META-INF/services/jakarta.faces.context.ExternalContextFactory(the file name is the factory class name, its first line is the implementation class) in a JAR visible to the web application’s class loader, as section 11.2.6.1 of the specification describes forFactoryFinder; -
FactoryFinder.setFactory(factoryName, implementationClassName), which the API documents as a hook for implementations while they parse the configuration; it has no effect oncegetFactoryhas been called for that name, so it is not a way for applications.
The service-file route was tried with a second ExternalContextFactory in a JAR that had no faces-config.xml at all. Both
implementations picked it up, and the decoration order was: the implementation’s factory, then the one from the service
file, then SafeRedirectExternalContextFactory from faces-config.xml (that constructor received the service-file factory
as the wrapped one). So the configuration file of the application is always the outermost.
Source: Faces 4.1 specification, 11.2.6.1 FactoryFinder, API: ExternalContextFactory, ExternalContextWrapper and FactoryFinder
Two More Factories: Application and Lifecycle
The ApplicationFactory and the LifecycleFactory are the two factories of the list that applications decorate least often, and both
return objects that live as long as the application (one Application, one Lifecycle per id), so the decorated object has to be cached. The factory classes are abstract and the
implementation hands the previous factory to the one-argument constructor.
ApplicationFactory has getApplication() and setApplication(Application); ApplicationWrapper decorates the Application that
getApplication() returns, so it intercepts the calls that go through that object: here createConverter(String), which is called when a tag names a
converter by id.
package org.example.bookshelf.web;
import jakarta.faces.application.Application;
import jakarta.faces.application.ApplicationFactory;
import jakarta.faces.application.ApplicationWrapper;
import jakarta.faces.convert.Converter;
import java.util.logging.Logger;
/** Decorates the Application so that every converter created by id is logged. */
public class ConverterLogApplicationFactory extends ApplicationFactory {
private volatile Application decorated;
public ConverterLogApplicationFactory(ApplicationFactory wrapped) {
super(wrapped);
}
@Override
public Application getApplication() {
if (decorated == null) {
decorated = new LoggingApplication(getWrapped().getApplication());
}
return decorated;
}
@Override
public void setApplication(Application application) {
decorated = null;
getWrapped().setApplication(application);
}
static final class LoggingApplication extends ApplicationWrapper {
private static final Logger LOG = Logger.getLogger(LoggingApplication.class.getName());
LoggingApplication(Application wrapped) {
super(wrapped);
}
@Override
public Converter<?> createConverter(String converterId) {
LOG.info("createConverter(" + converterId + ")");
return super.createConverter(converterId);
}
}
}
LifecycleFactory maps lifecycle ids to Lifecycle instances. A decorator that returns a LifecycleWrapper can register a
PhaseListener on the lifecycle however faces-config.xml is configured, which suits a library that must always measure or audit the
request. LifecycleFactory has three abstract methods in 4.1 (addLifecycle, getLifecycle and getLifecycleIds), so the decorator delegates the two it does not
change:
package org.example.bookshelf.web;
import jakarta.faces.event.PhaseEvent;
import jakarta.faces.event.PhaseId;
import jakarta.faces.event.PhaseListener;
import jakarta.faces.lifecycle.Lifecycle;
import jakarta.faces.lifecycle.LifecycleFactory;
import jakarta.faces.lifecycle.LifecycleWrapper;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
/** Decorates every Lifecycle so that a PhaseListener is always registered on it, whatever faces-config.xml says. */
public class TimingLifecycleFactory extends LifecycleFactory {
private final Map<String, Lifecycle> decorated = new ConcurrentHashMap<>();
public TimingLifecycleFactory(LifecycleFactory wrapped) {
super(wrapped);
}
@Override
public Lifecycle getLifecycle(String lifecycleId) {
return decorated.computeIfAbsent(lifecycleId, id -> new TimingLifecycle(getWrapped().getLifecycle(id)));
}
@Override
public void addLifecycle(String lifecycleId, Lifecycle lifecycle) {
getWrapped().addLifecycle(lifecycleId, lifecycle);
}
@Override
public Iterator<String> getLifecycleIds() {
return getWrapped().getLifecycleIds();
}
static final class TimingLifecycle extends LifecycleWrapper {
TimingLifecycle(Lifecycle wrapped) {
super(wrapped);
wrapped.addPhaseListener(new RenderTimer());
}
}
static final class RenderTimer implements PhaseListener {
private static final Logger LOG = Logger.getLogger(RenderTimer.class.getName());
@Override
public void beforePhase(PhaseEvent event) {
event.getFacesContext().getAttributes().put("renderStart", System.nanoTime());
}
@Override
public void afterPhase(PhaseEvent event) {
Long start = (Long) event.getFacesContext().getAttributes().get("renderStart");
LOG.info("render response took " + (System.nanoTime() - start) / 1_000_000 + " ms");
}
@Override
public PhaseId getPhaseId() {
return PhaseId.RENDER_RESPONSE;
}
}
}
<factory>
<application-factory>org.example.bookshelf.web.ConverterLogApplicationFactory</application-factory>
<lifecycle-factory>org.example.bookshelf.web.TimingLifecycleFactory</lifecycle-factory>
</factory>
Source: Jakarta Faces 4.1 API: ApplicationFactory, ApplicationWrapper, LifecycleFactory and LifecycleWrapper
Both classes were compiled with --release 21 and run on Mojarra 4.1.17 and MyFaces 4.1.4 on Tomcat 11.0.26 with Weld 6.0.4.Final, with a page
holding <h:inputText converter="jakarta.faces.Integer"/>. Each request logged createConverter(jakarta.faces.Integer) and, after Render Response,
render response took 34 ms for the first request (Mojarra; 41 ms on MyFaces) and 1 ms for the following ones, on both implementations. Two points follow from the code. Which calls inside an implementation go through the decorated Application was not examined, so a registration that must always apply is safer made with an
PostConstructApplicationEvent listener or the <application> elements. And the lifecycle
is a singleton per id: the decorator adds the listener once, when it wraps, and caches the wrapper, otherwise every call of getLifecycle would add another listener.
Registering Extensions: faces-config.xml and Its Neighbours
Everything above is registered in an application configuration resource, a file in the faces-config.xml format. At
start-up, before any request, the specification (11.3.2) has Faces build a list of resources in this way: if the context
parameter jakarta.faces.CONFIG_FILES exists, its comma-separated, context-relative paths are the list and the search of
META-INF is skipped; otherwise every META-INF/faces-config.xml and every file named *.faces-config.xml directly in
META-INF (in the JARs of WEB-INF/lib) is added; and the documents supplied by the ServiceLoader service
ApplicationConfigurationPopulator are added too (described below). The list is parsed before /WEB-INF/faces-config.xml,
which is therefore always last. The order matters because a decorator wraps what was registered before it. The elements used
on this page, all taken from the 4.1 schema (web-facesconfig_4_1.xsd), are:
| Parent | Element | Effect |
|---|---|---|
|
|
Decorates the corresponding handler through its one-argument constructor (replaces it when only a no-argument constructor exists) |
|
|
Adds a resolver to the application’s chain |
|
|
Subscribes the listener with |
|
|
Decorates the search expression handler, or adds a keyword resolver |
|
one element per factory (table above) |
Decorates the factory |
|
|
Adds the listener to the |
|
|
Names the file and orders it relative to others (next section) |
Classes named in these elements need a public constructor of the right shape: no-argument for el-resolver, phase-listener
and system-event-listener-class, and the specification says the same for the factories that only replace; one argument
of the wrapped type for the decorators. Annotations do not replace these registrations: the annotations of the configuration (@FacesComponent,
@FacesConverter, @FacesRenderer, @FacesBehavior and so on) cover components, converters, validators, behaviours and
renderers, while handlers, resolvers, listeners and factories are registered only in XML, in service files (factories) or in
code, or through the populator service of the next section. The project setup page of this section shows the layout of a
project’s faces-config.xml.
Source: Faces 4.1 specification, 11.3.2 Application Startup Behavior, 11.3.6 Configuration Impact on Jakarta Faces Runtime and Jakarta EE Tutorial: Configuring Jakarta Faces Applications
Registering from Code: ApplicationConfigurationPopulator
A library that cannot ship a faces-config.xml (or that computes its configuration) can implement
jakarta.faces.application.ApplicationConfigurationPopulator, a java.util.ServiceLoader service (since Faces 2.2). Faces
calls populateApplicationConfiguration(Document) once at start-up and merges the document, which has the namespace of the
configuration format, with the other application configuration resources. The Bookshelf example adds a phase listener this
way; the populator and the listener are in a JAR whose only descriptor is the service file.
package org.example.bookshelf.pop;
import jakarta.faces.application.ApplicationConfigurationPopulator;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
/** Registers a PhaseListener by adding a lifecycle element to the application configuration document. */
public class BookshelfPopulator extends ApplicationConfigurationPopulator {
@Override
public void populateApplicationConfiguration(Document toPopulate) {
String namespace = toPopulate.getDocumentElement().getNamespaceURI();
Element phaseListener = toPopulate.createElementNS(namespace, "phase-listener");
phaseListener.setTextContent("org.example.bookshelf.pop.PopulatedPhaseListener");
Element lifecycle = toPopulate.createElementNS(namespace, "lifecycle");
lifecycle.appendChild(phaseListener);
toPopulate.getDocumentElement().appendChild(lifecycle);
}
}
org.example.bookshelf.pop.BookshelfPopulator
With the JAR in WEB-INF/lib, the listener was called before the render response phase of every request on Mojarra and on
MyFaces, without any faces-config.xml entry. Use the elements of the schema in the order the schema defines when you build
the document; the populator is for libraries, and an application should keep its registrations in faces-config.xml,
where they can be read.
Ordering Several Extensions
When two libraries decorate the same artifact, the order decides which sees the call first. The specification (11.3.8)
defines it. Each configuration resource can have a <name>, and a resource in WEB-INF/lib can declare <ordering> with
<before> or <after> (a list of names, or <others/>). WEB-INF/faces-config.xml may instead declare
<absolute-ordering>, which lists the other resources by name and overrides every relative ordering. The application’s own
WEB-INF/faces-config.xml is always processed last, so its decorators are the outermost, and a decorator registered later
wraps those registered earlier.
The experiment used two small JARs, each with a META-INF/faces-config.xml that registers one ViewHandler decorator that
logs the class of the handler it wraps. The first JAR is named trace; the second, named audit, asks to be processed after it:
<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">
<name>audit</name>
<ordering>
<after>
<name>trace</name>
</after>
</ordering>
<application>
<view-handler>org.example.bookshelf.audit.AuditOrderViewHandler</view-handler>
</application>
</faces-config>
The trace JAR has the same shape with <name>trace</name>, no ordering and its own class. The calls to
calculateLocale then travelled through the chain below. The application’s LocaleViewHandler is outermost, the audit JAR
(processed after trace) is next, and the trace JAR is innermost, in front of the implementation’s own handler.
The log lines (class of the wrapped object on the right) were:
Mojarra 4.1.17 MyFaces 4.1.4
LocaleViewHandler -> AuditOrderViewHandler LocaleViewHandler -> AuditOrderViewHandler
AuditOrderViewHandler -> ConversationAwareViewHandler AuditOrderViewHandler -> TraceViewHandler
TraceViewHandler -> MultiViewHandler TraceViewHandler -> ConversationAwareViewHandler
With <before> in place of <after> in the audit JAR, both implementations produced the order LocaleViewHandler,
TraceViewHandler, AuditOrderViewHandler: the ordering elements work as specified on both. The two columns differ in
where the implementation’s own CDI helper (ConversationAwareViewHandler) sits in the chain — between two of the
application’s decorators on Mojarra, innermost on MyFaces — which is why a decorator must never assume a particular class
below it.
Names must be unique: a duplicate <name> among the JARs is a deployment error, and circular <before>/<after>
constraints also fail the deployment. Without any ordering element the relative order of the JARs is unspecified and may
differ between implementations and between builds, so a library whose decorator has to be inside or outside another one
should state it with <ordering>, and an application that needs a definite order of all libraries should use
<absolute-ordering> in WEB-INF/faces-config.xml. An <absolute-ordering> that does not contain <others/> ignores
every resource it does not list, including the extensions of unlisted libraries; that is rarely what is intended.
Mojarra and MyFaces
The specification fixes the contracts, and on the extension points of this page the two implementations agreed in every run. The differences are in the details around them.
| Situation | Mojarra | MyFaces |
|---|---|---|
|
|
injected |
|
|
|
|
link disabled |
link works |
Position of the implementation’s own CDI helper in the |
between two decorators |
innermost |
Dev-stage cache header of a normal resource |
|
|
A JAR in |
Scanned anyway (recorded on the custom components page) |
Scanned for configuration only with |
The form of a POST also differs, which matters if a script drives the page: MyFaces expects the hidden f_SUBMIT field
that its h:form renders, Mojarra does not. A request without it was treated as a non-postback by MyFaces and no
action ran.
What Changed
| Version | Change |
|---|---|
1.2 |
|
2.0 |
|
2.2 |
|
2.3 |
Constructors that take the wrapped instance on the wrapper classes (their no-argument constructors are deprecated in the
4.1 API), |
3.0 |
The |
4.0 |
|
4.1 |
No new decoration mechanism. The release removed the references to the Java |
5.0 (upcoming) |
Under development ( |
Source: Faces 4.1 specification, Appendix B Changes Between Releases and the "Since" and "Deprecated" notes of the API pages linked in the sections above
Where the Books Are Outdated
The JSF books consulted for this section treat extension points in two places: a discussion of the design patterns behind JSF (the decorator and observer patterns, in the book of Dudney and colleagues) and a chapter on the non-UI extension points — phase listeners, the view handler, the exception handler, EL resolvers, the factories and the decorator pattern (Burns and Schalk, appendix and later chapters) — with related material on extending the core classes (Mann), custom resource handlers (Leonard) and component resources (Jacobi and Fallows). The concepts hold; the details need correcting.
-
The books register extensions with the
javax.facespackages and the Java EE namespace. In 4.1 the classes are injakarta.faces.*, the file uses thehttps://jakarta.ee/xml/ns/jakartaeenamespace andversion="4.1". -
The JSF 1.x APIs that the older books use to add names and bean access —
VariableResolver,PropertyResolver,MethodBinding,ValueBinding— were removed in Faces 4.0, as wasResourceResolver. The current mechanism for adding names and properties is theELResolverof this page, and for finding Facelets files theResourceHandler. -
Standalone Facelets 1.x, in the book of Aranda and Wadia, extended the view through its own view handler; since Faces 2.0 Facelets is part of the specification and the view handler is the standard
ViewHandlerwith itsViewDeclarationLanguage. -
Decoration is now specified for about three dozen types, each with a
*Wrapperclass, so a decorator does not have to implement a whole interface such asViewHandlerorNavigationHandler; writing a complete implementation, as the older books show, is no longer necessary and, as the experiment above shows, loses the original behaviour. -
Weblets and similar libraries, used in the books to serve component resources, are replaced by the standard
ResourceHandler(andResourceHandlerWrapperfor customisation) and byh:outputScriptandh:outputStylesheet. -
An extension that needs a bean gets it from CDI (
CDI.current()) or through an expression (Application.evaluateExpressionGet), not from the JSF 1.x managed-bean facility, and the phase listeners and decorators are single instances shared by all requests: per-request state belongs in the request map, not in a field.
Pitfalls
-
A class with only a no-argument constructor replaces, it does not decorate. Everything it does not implement is lost. Give every wrapper a constructor that takes the wrapped type.
-
Partial decoration of related methods. The navigation alias worked for buttons and not for links, because only
handleNavigationwas overridden. Decide which methods carry the behaviour and override all of them. -
Extending
NavigationHandlerWrapperinstead ofConfigurableNavigationHandlerWrapper. Mojarra then disablesh:linkandh:buttonwhose outcome is valid. -
Casting
getWrapped()to an implementation class, or relying on what is below the decorator. The chain contains the implementation’s own wrappers. -
Assuming a decorator order without declaring it. Use
<ordering>in libraries and<absolute-ordering>in the application, and remember that an absolute ordering without<others/>silently drops the unlisted resources. -
Per-request state in a field. Handlers, resolvers, listeners and factories are shared by all requests; keep request data in the request map.
-
@Injectin classes that Faces instantiates from XML. It is not portable (it differed between implementations on this page); useCDI.current()or read from theFacesContext. -
Doing too much in
beforePhase. The listener runs on every request; anything slow or blocking (a database call, a remote call) becomes the latency floor of the application. -
Expecting a
PhaseListenerto see resource requests. It sees only requests that run the lifecycle. -
Subscribing in code on every request.
Application.subscribeToEventandLifecycle.addPhaseListeneradd a listener permanently; subscribe from a start-up event, once. -
Redirect allow-lists in code. A host list belongs in configuration, and a redirect target taken from user input must be validated where it is read in addition to the decorator that guards
ExternalContext.redirect. -
Hard-coded secrets in extension classes or in
faces-config.xml. Read them from the server configuration or the environment, as the allow-list above is read from a context parameter.
References
-
Jakarta Faces 4.1 specification: 11.2.6.1 FactoryFinder (
#a6147), 11.3.2 Application Startup Behavior (#a6201), 11.3.6 Configuration Impact on Jakarta Faces Runtime (#a6297), 11.3.7 Delegating Implementation Support (#a6336), 11.3.8 Ordering of Artifacts (#a6435), 12.3 PhaseListener (#a6635), 5.6.2 Expression Language Resolution (#a3070) -
Jakarta Faces 4.1 API: FacesWrapper, ViewHandlerWrapper, ResourceHandlerWrapper, ResourceWrapper, NavigationHandlerWrapper, ConfigurableNavigationHandlerWrapper, ApplicationWrapper
-
Jakarta Faces 4.1 API: ApplicationFactory, ApplicationWrapper, LifecycleFactory, LifecycleWrapper
-
Jakarta Faces 4.1 API: PhaseListener, SystemEventListener, PostConstructApplicationEvent, PreRenderViewEvent
-
Jakarta Faces 4.1 API: FactoryFinder, ExternalContextFactory, ExternalContextWrapper
-
Jakarta EE Tutorial: Configuring Jakarta Faces Applications (the
currenttutorial describes Jakarta EE 10 and Faces 4.0) -
Publisher page: JavaServer Faces 2.0: The Complete Reference (Burns and Schalk)