Resource Handling
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Jakarta Faces 4.1 (API jakarta.faces-api 4.1.2, part of Jakarta EE 11) with Eclipse Mojarra
4.1.17 and Apache MyFaces Core 4.1.4, Jakarta Servlet 6.1 and Java 17 or later (the examples were compiled with
--release 21). Resource handling has the same shape since Faces 2.0, so most of the page also applies to 4.0, but only 4.1
was run. The code 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 (every markup fragment, header and status code quoted below was observed on
2026-10-08). Claims that were not run are marked as such.
A web application is more than views. It needs style sheets, scripts, images and fonts, and those files have problems that a
plain <link href="/css/site.css"> does not solve: a component library must ship its own files inside a JAR, a changed
file must reach the browser although the browser cached the old one, a Spanish visitor may need a different logo, and a
script must be loaded once even if five components ask for it. Jakarta Faces solves them with resources: files found by
name in a library under a fixed directory, rendered by the standard tags, and served by a ResourceHandler with caching
headers. This page covers the packaging convention, the tags, relocation, versioning and localization, the request that
fetches a resource, caching, and how to replace or extend the handler.
What a Resource Is
A resource is any file that the Faces runtime finds through a library name and a resource name and serves itself. It is
represented by the abstract class jakarta.faces.application.Resource; the object that creates resources and serves their
requests is the application’s ResourceHandler (Application.getResourceHandler()). Three things distinguish a resource
from a static file:
-
It is found by name, not by path. The view says
library="css" name="bookshelf.css"; where that file physically lives (the web application, a JAR, a localized or versioned variant) is the handler’s business. -
It is rendered by a component.
h:outputStylesheet,h:outputScriptandh:graphicImageareUIOutput/UIGraphiccomponents with a renderer that asks the handler for the resource and writes the URL. Because they are components, a script can be moved to the end of the body, rendered once, or added to the view by another component. -
It is served by the Faces servlet. The URL points at the
FacesServlet, whose first decision for every request is "resource request or view request". That is where the headers, the304 Not Modifiedanswers and the filtering of file types happen. In lifecycle terms (see the phases) a resource request never enters the six phases at all:FacesServletcallsisResourceRequest()and, when it is true,handleResourceRequest(), and returns.
Where Resources Live
The default ResourceHandler must look in two places, in this order:
| Location | Meaning | Notes |
|---|---|---|
|
The default resources directory of the WAR. |
Its name can be changed with |
|
The same layout inside a JAR, normally in |
The way a component library or a shared "assets" JAR ships its files. Also holds |
Source: Jakarta Faces 4.1 specification, 2.6.1 Packaging Resources and Jakarta Faces 4.1 API: ResourceHandler
The Jakarta EE Tutorial describes the same two locations in the same order. The final Bookshelf layout from the project setup page, extended with what this page adds, looks like this:
src/main/webapp/
├── resources/ the default resources directory
│ ├── css/
│ │ ├── bookshelf.css library "css", name "bookshelf.css"
│ │ ├── admin/tables.css library "css", name "admin/tables.css"
│ │ └── print.css/ a versioned resource: the folder is the name
│ │ ├── 1_0.css
│ │ └── 1_2.css
│ ├── js/
│ │ ├── catalogue.js
│ │ └── analytics.js
│ ├── img/
│ │ └── logo.svg
│ ├── es/img/logo.svg Spanish variant, locale prefix "es"
│ ├── theme/ a versioned library
│ │ ├── 1_0_0/theme.css
│ │ └── 1_1_0/theme.css
│ └── generated/palette.css placeholder for a generated resource
└── WEB-INF/
└── lib/bookshelf-assets.jar META-INF/resources/shared/shared.css
What the test of this layout showed:
-
The web root wins. A file
bookshelf/dup.txtexisted in both the web application and the JAR; the resource served was the web application’s copy, on both implementations. A file that exists only in the JAR (bookshelf/jaronly.txt) was found in the same library, so a library is the union of its directories in the WAR and in every JAR. -
Everything in
resources/is also a plain static file. The container serves the web root itself, soGET /bookshelf/resources/css/bookshelf.cssanswered200with the file as it is on disk, with the EL expression of the next sections unevaluated, and so didresources/css/app.properties, a file type that the resource handler refuses (see How a Resource Request Is Served). The JAR contents are exposed the same way:GET /bookshelf/shared/shared.cssreturned theMETA-INF/resources/shared/shared.cssof the JAR. Never put anything secret inresources/or inMETA-INF/resources/: the type filter of the resource handler does not protect it. -
A hidden resources directory works. With
jakarta.faces.WEBAPP_RESOURCES_DIRECTORYset toWEB-INF/resourcesand the directory moved there, a resource request still returned200while/bookshelf/resources/css/bookshelf.cssand/bookshelf/WEB-INF/resources/css/bookshelf.cssreturned404, on both implementations. This is the safe place for files that are not meant to be fetched directly.
<context-param>
<param-name>jakarta.faces.WEBAPP_RESOURCES_DIRECTORY</param-name>
<param-value>WEB-INF/resources</param-value>
</context-param>
Resource Identifiers
Every resource has a resource identifier, the path below the resources directory:
[localePrefix/][libraryName/][libraryVersion/]resourceName[/resourceVersion]
Only resourceName is required. The segments appear in this order and the runtime ignores silently an identifier that does
not follow the rules. A few rules decide most real cases:
-
The characters allowed in a segment are those of an XML NameChar without
/and:. -
A
libraryNamemust not be an underscore-separated sequence of numbers (such as2_3) nor a locale string (such asdeorde_AT), because those would be taken for a version or a locale prefix. -
libraryVersionandresourceVersionare underscore-separated lists of integers such as1_0_0; a resource version may carry a file extension (1_2.css). -
If a
libraryVersiondirectory exists, files placed directly in the library folder are ignored.
The specification’s own examples, expressed with the Bookshelf names:
| Identifier | Meaning |
|---|---|
|
A resource without a library. |
|
Library |
|
Library |
|
Library |
|
Resource |
|
The Spanish variant (locale prefix |
Observations that go beyond the text of the specification:
-
A library folder named
deor2_3was rendered and served (200) by both implementations, although the specification forbids those names. Do not name a library like that; the behaviour is not guaranteed and a locale prefix will collide with it. -
library="css/sub"(a library name with a slash) produced a link on Mojarra whose request returned404; MyFaces rendered nothing. Put the sub-directory in the resource name instead:library="css" name="sub/z.css"worked on both. -
name="css/bookshelf.css"without any library also worked on both and produced a URL without thelnparameter. Referring to the same file once asname="css/bookshelf.css"and once aslibrary="css" name="bookshelf.css"produced two links on Mojarra and one on MyFaces; use one spelling per file.
Rendering Resources in a View
Three standard components render resources. The library and name attributes of each identify the resource; target
(Relocating Resources with target) moves it.
| Tag | Renders | Attributes that matter |
|---|---|---|
|
A |
|
|
A |
|
|
An |
|
Source: Jakarta Faces 4.1 VDL: h:outputStylesheet, Jakarta Faces 4.1 VDL: h:outputScript and Jakarta Faces 4.1 VDL: h:graphicImage
The type attribute is gone in HTML5 mode. Since Faces 4.0 the link and script elements rendered for h:outputStylesheet and
h:outputScript carry no type attribute when the doctype is HTML5 (issue 1565 in the 4.0 change list, "Skip type attribute from link and script when
doctype is HTML5"); the browser defaults are text/css and text/javascript. The default Facelets processing mode is html5, which
simplifies any doctype to <!DOCTYPE html> (see the doctype section), so the
attribute shows only after switching a file extension to process-as xhtml. The same page with an h:outputStylesheet and an h:outputScript gave, on Mojarra 4.1.17 and
MyFaces 4.1.4, the same result:
default (html5): <script src="/bookshelf/jakarta.faces.resource/a.js.xhtml?ln=js"></script>
<link rel="stylesheet" href="/bookshelf/jakarta.faces.resource/a.css.xhtml?ln=css" />
process-as xhtml: <script type="text/javascript" src="/bookshelf/jakarta.faces.resource/a.js.xhtml?ln=js"></script>
<link rel="stylesheet" type="text/css" href="/bookshelf/jakarta.faces.resource/a.css.xhtml?ln=css" />
The resource also has an EL form. The resource implicit object (resolved by ResourceELResolver, see
the implicit objects) turns #{resource['library:name']} into the
request path of the resource, so it can go into any attribute or into plain HTML. #{resource['name']} is the form without a
library. The specification states that these two render identically:
<h:graphicImage library="common" name="images/planets.png" />
<h:graphicImage value="#{resource['common:images/planets.png']}" />
A complete Bookshelf view that uses all of them. Library css holds the site style, theme is the versioned library of
the tree above, and js the scripts.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html">
<h:head>
<title>Bookshelf</title>
<h:outputStylesheet library="css" name="bookshelf.css"/>
<h:outputStylesheet library="css" name="admin/tables.css"/>
<h:outputStylesheet library="theme" name="theme.css"/>
<h:outputScript library="js" name="catalogue.js"/>
</h:head>
<h:body>
<h:graphicImage library="img" name="logo.svg" alt="Bookshelf logo"/>
<img src="#{resource['img:logo.svg']}" alt="Bookshelf logo"/>
<h:outputScript library="js" name="analytics.js" target="body"/>
</h:body>
</html>
The head produced, on Mojarra:
<head>
<title>Bookshelf</title>
<script src="/bookshelf/jakarta.faces.resource/catalogue.js.xhtml?ln=js"></script>
<link rel="stylesheet" href="/bookshelf/jakarta.faces.resource/bookshelf.css.xhtml?ln=css" />
<link rel="stylesheet" href="/bookshelf/jakarta.faces.resource/admin/tables.css.xhtml?ln=css" />
<link rel="stylesheet" href="/bookshelf/jakarta.faces.resource/theme.css.xhtml?ln=theme&v=1_1_0" />
</head>
<body><img src="/bookshelf/jakarta.faces.resource/logo.svg.xhtml?ln=img" alt="Bookshelf logo" />
<img src="/bookshelf/jakarta.faces.resource/logo.svg.xhtml?ln=img" alt="Bookshelf logo" />
<script src="/bookshelf/jakarta.faces.resource/analytics.js.xhtml?ln=js"></script></body>
MyFaces produced the same markup except that the theme.css link had no v parameter (see Versioning Libraries and Resources) and that a
development-stage <div id="jakarta_faces_developmentstage_messages"> was appended to the body.
Three details of this output matter:
-
The script comes before the style sheets although it was declared after them. A script without
targetis rendered where it stands, insideh:head. A style sheet is always relocated to the head component resources and rendered after the head’s own content. The rule holds on both implementations (Relocating Resources with target). -
The two images are identical.
h:graphicImage library name,#{resource[...]}inh:graphicImageand in plain HTML all produce/bookshelf/jakarta.faces.resource/logo.svg.xhtml?ln=img. -
jsessionidcan appear in the URLs. On the first request of a visitor without a session cookie, both implementations rendered every resource URL asbookshelf.css.xhtml;jsessionid=...?ln=css, because the URL goes throughViewHandler.getResourceURL. A URL that differs per visitor cannot be cached by a shared cache. Make the session cookie the only tracking mode (<session-config><tracking-mode>COOKIE</tracking-mode></session-config>inweb.xml): with it the same request produced URLs without the parameter on both implementations.
Inline Content
h:outputScript and h:outputStylesheet without name and with body content render an inline <script> or <style>
element, which is the way to attach a small piece of code to a relocatable position. The body is written as it is, so it must
never contain user-supplied text.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="jakarta.faces.html">
<h:head><title>inline</title></h:head>
<h:body>
<h:outputScript target="body">window.bookshelf = { ready: true };</h:outputScript>
<h:outputStylesheet>.inline { color: red; }</h:outputStylesheet>
<h:outputScript library="js" name="a.js">ignored child</h:outputScript>
</h:body>
</html>
On both implementations this produced <style>.inline { color: red; }</style> in the head and
<script>window.bookshelf = { ready: true };</script> at the end of the body. The third tag has both name and body
content: only the file a.js was rendered and the body was dropped, as the VDL specifies for that combination (Mojarra also
logged a warning).
Relocating Resources with target
A resource tag can be written where it is convenient and rendered somewhere else. The target attribute names the
destination: head, body or form. The mechanism is event-based: the renderer of the tag listens for
PostAddToViewEvent and, when the component has a target attribute, passes it to UIViewRoot.addComponentResource(),
which puts it in the facet of the view root that is named by the target. h:head, h:body and h:form render the
resources of their own facet. The Jakarta EE Tutorial lists the three values.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="jakarta.faces.html">
<h:head>
<title>target</title>
<h:outputScript library="js" name="d.js"/>
</h:head>
<h:body>
<p>before form</p>
<h:outputScript library="js" name="e.js"/>
<h:form id="f">
<h:outputScript library="js" name="a.js" target="head"/>
<h:outputScript library="js" name="b.js" target="body"/>
<h:outputScript library="js" name="c.js" target="form"/>
<h:outputStylesheet library="css" name="admin/tables.css" target="body"/>
<p>in form</p>
</h:form>
<p>after form</p>
</h:body>
</html>
The output of this page was the same on both implementations, except for the position of the form script relative to the hidden fields:
<head><title>target</title>
<script src=".../d.js.xhtml?ln=js"></script> declared in h:head, no target: in place
<script src=".../a.js.xhtml?ln=js"></script> target="head", declared in the form
<link rel="stylesheet" href=".../admin/tables.css.xhtml?ln=css" /> target="body" was not honoured
</head>
<body>
<p>before form</p><script src=".../e.js.xhtml?ln=js"></script> no target, declared in the body: in place
<form id="f" ...>
<p>in form</p><script src=".../c.js.xhtml?ln=js"></script> target="form": at the end of the form
<input type="hidden" name="jakarta.faces.ViewState" ... />
</form>
<p>after form</p><script src=".../b.js.xhtml?ln=js"></script> target="body": before </body>
</body>
On MyFaces the c.js script came after the jakarta.faces.ViewState field, on Mojarra before it.
Source: Jakarta EE Tutorial: Relocatable Resources and Jakarta Faces 4.1 specification, 2.6.2.1 Relocatable Resources
The rules that this and further runs gave:
-
h:outputStylesheetignorestarget. The VDL lists notargetattribute for it and its renderer always relocates to the head;target="body"was accepted without an error and the link stayed in the head on both implementations. The Tutorial says that the tag supports relocation like the script tag; in these runs it is relocated, but only ever to the head. -
A resource is rendered once per response. The resource renderers skip a resource that was already referenced in this request (the VDL algorithm; the handler offers
isResourceRendered()andmarkResourceRendered()for it). Two tags forcatalogue.js, one in the head and one in the form, produced one<script>in the head on both implementations. -
With two different targets, the first position in the document wins.
a.jsdeclared withtarget="form"and again withtarget="body", andb.jsdeclared withbodythen withform, were both rendered once, in the form, because the form comes before the end of the body. A script declared both without target and withtarget="head"was rendered in the head. -
Order inside one destination. Scripts that stand where they are (no
target) are rendered first, in document order, then the relocated resources in the order they were added to the view. When a style sheet must come after another, declare both with the same mechanism. -
Relocation needs
h:headandh:body. With a plain<head>/<body>pair, none of the three resources of a test page (a style sheet,target="head"andtarget="body"scripts) was rendered on either implementation. Mojarra added two development-stage messages at the end of the body ("One or more resources have the target of 'head', but no 'head' component has been defined within the view"); MyFaces said nothing.
Moving scripts to the end of the body (the analytics.js of the example) lets the browser paint before it downloads and
runs them. Scripts that must run before the page renders belong in the head. faces.js is the one to be careful with:
its position is described on the Ajax page (Getting faces.js onto the Page).
Components That Bring Their Own Resources
A component or renderer should not require every page that uses it to remember its style sheet. The class-level annotation
jakarta.faces.application.ResourceDependency (repeatable, with the container ResourceDependencies) declares it. It has
name (required), library and target; all three may be EL expressions that resolve to a string. The annotation can be
placed on classes only (@Target(TYPE)): it works on UIComponent and Renderer classes, not on methods or on managed beans. The runtime
processes it when an instance of the class is inserted into the view on an initial request: it creates a UIOutput with the
renderer type that ResourceHandler.getRendererTypeForResourceName() returns for the name, stores name, library and
target as attributes and, with no target, adds it to the head by calling UIViewRoot.addComponentResource().
package org.example.bookshelf.component;
import jakarta.faces.application.ResourceDependencies;
import jakarta.faces.application.ResourceDependency;
import jakarta.faces.component.FacesComponent;
import jakarta.faces.component.UIOutput;
/** A small badge on a book cover; pulling in its own style sheets whenever it is on a page. */
@FacesComponent(value = "bookshelf.CoverBadge", createTag = true, namespace = "urn:bookshelf:badges", tagName = "badge")
@ResourceDependencies({
@ResourceDependency(library = "bookshelf", name = "badge.css"),
@ResourceDependency(library = "bookshelf", name = "badge-print.css")
})
public class CoverBadge extends UIOutput {
public static final String FAMILY = "bookshelf.CoverBadge";
public CoverBadge() {
setRendererType("bookshelf.CoverBadge");
}
@Override
public String getFamily() {
return FAMILY;
}
}
package org.example.bookshelf.component;
import jakarta.faces.application.ResourceDependency;
import jakarta.faces.component.UIComponent;
import jakarta.faces.context.FacesContext;
import jakarta.faces.context.ResponseWriter;
import jakarta.faces.render.FacesRenderer;
import jakarta.faces.render.Renderer;
import java.io.IOException;
/** Renders the badge and asks for its script at the end of the body. */
@FacesRenderer(componentFamily = CoverBadge.FAMILY, rendererType = "bookshelf.CoverBadge")
@ResourceDependency(library = "js", name = "badge.js", target = "body")
public class CoverBadgeRenderer extends Renderer<CoverBadge> {
@Override
public void encodeEnd(FacesContext context, CoverBadge component) throws IOException {
ResponseWriter writer = context.getResponseWriter();
writer.startElement("span", component);
writer.writeAttribute("class", "cover-badge", null);
writer.writeText(component.getValue(), "value");
writer.endElement("span");
}
}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="jakarta.faces.html" xmlns:b="urn:bookshelf:badges">
<h:head><title>badges</title>
<h:outputStylesheet library="generated" name="palette.css"/>
</h:head>
<h:body>
<b:badge value="NEW"/><b:badge value="HOT"/>
<h:outputStylesheet library="css" name="bookshelf.css"/>
<img src="#{resource['img:logo.svg']}" alt="x"/>
</h:body>
</html>
Two badges on the page produced one badge.css link, one badge-print.css link in the head and a single badge.js script
before </body> on both implementations: the once-per-response rule covers dependencies too. The order of the head links did
differ: Mojarra rendered the dependencies of the component (badge.css, badge-print.css) before the palette.css that the
page declared first, MyFaces rendered palette.css first. Do not rely on the relative order of a declared style sheet and a
dependency; if one overrides the other, give the rule higher specificity or declare both the same way. Custom components and
renderers are covered in Resources of a component and composite
components in Composite Libraries; a composite component that uses
h:outputStylesheet inside its implementation shares the once-per-response rule.
Adding a Resource from Code
The annotation is static. To add a resource conditionally, for example the dark theme chosen in a user setting, do what the
runtime does: create a UIOutput with the renderer type for the file name and add it to the view root.
package org.example.bookshelf.web;
import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.application.Application;
import jakarta.faces.component.UIComponent;
import jakarta.faces.component.UIOutput;
import jakarta.faces.context.FacesContext;
import jakarta.inject.Inject;
import jakarta.inject.Named;
/** Adds the dark theme style sheet to the head of the view, for example for a user setting. */
@Named
@RequestScoped
public class ThemeBean {
@Inject
private FacesContext context;
public void addDarkTheme() {
Application application = context.getApplication();
UIOutput css = (UIOutput) application.createComponent(context, UIOutput.COMPONENT_TYPE,
application.getResourceHandler().getRendererTypeForResourceName("dark.css"));
css.getAttributes().put("library", "css");
css.getAttributes().put("name", "dark.css");
context.getViewRoot().addComponentResource(context, css, "head");
}
}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="jakarta.faces.html" xmlns:f="jakarta.faces.core">
<f:metadata><f:event type="preRenderView" listener="#{themeBean.addDarkTheme}"/></f:metadata>
<h:head><title>dynamic</title>
<h:outputStylesheet library="css" name="bookshelf.css"/>
</h:head>
<h:body>dynamic</h:body>
</html>
Source: Jakarta Faces 4.1 API: UIViewRoot.addComponentResource and Jakarta Faces 4.1 API: ResourceHandler.getRendererTypeForResourceName
Both implementations rendered <link rel="stylesheet" href=".../dark.css.xhtml?ln=css" /> after bookshelf.css. The listener
element and event types are described on the events page (Events and Listeners).
How a Resource Request Is Served
FacesServlet calls ResourceHandler.isResourceRequest() first. The default handler answers true when the request path
contains the constant ResourceHandler.RESOURCE_IDENTIFIER, which is /jakarta.faces.resource. The request path that
Resource.getRequestPath() produces depends on how the servlet is mapped (see
the servlet mapping):
| Mapping | Rule | Observed URL |
|---|---|---|
Extension ( |
context path + |
|
Prefix ( |
context path + prefix + |
|
The query string carries the metadata: ln is the library name, loc the locale prefix and con the name of the resource
library contract (resource library contracts have a page of their own: Resource Library Contracts).
On Mojarra a versioned library or resource also adds a v parameter (Versioning Libraries and Resources). In the test with extension mapping a
request without the mapped extension (/jakarta.faces.resource/bookshelf.css?ln=css) answered 404 from the container,
because the FacesServlet was not involved; with the prefix mapping /faces/* the URL with the prefix answered 200 and the
extension URL 404.
handleResourceRequest() then follows an algorithm that the specification fixes:
-
If the resource identifier ends with an extension listed in
jakarta.faces.RESOURCE_EXCLUDES, answer404. -
Extract the resource name; if there is none, answer
404. Read the library from the request parameterln. -
Create the resource with
createResource(name, library)(orcreateResource(name)); if it cannot be created, answer404. -
Ask
Resource.userAgentNeedsUpdate(), which consultsIf-Modified-Since; if the browser is up to date, answer304. -
Set the content type from
getContentType(), copy every entry ofgetResponseHeaders()into the response, then streamgetInputStream()and set the content length.
Source: Jakarta Faces 4.1 API: ResourceHandler.handleResourceRequest and Jakarta Faces 4.1 API: Resource
The excluded types default to .class .jsp .jspx .properties .xhtml .groovy (ResourceHandler.RESOURCE_EXCLUDES_DEFAULT_VALUE).
A value that you configure replaces the default entirely instead of adding to it. This was run:
| Request | Status | Comment |
|---|---|---|
|
200 |
The resource. |
|
404 |
No such resource. |
|
404 |
The three files exist; their extensions are excluded by default. |
|
404 |
A library with a relative path never matches. |
|
400 |
Rejected by the container before the application. |
Same excluded files with |
200, 200, 404 |
|
<context-param>
<param-name>jakarta.faces.RESOURCE_EXCLUDES</param-name>
<param-value>.class</param-value>
</context-param>
If you need a different list, repeat the default entries and add yours. The configuration above exists to show the replacement rule, not as a recommendation.
Versioning Libraries and Resources
Versions exist so that a browser can cache a resource for a long time and still fetch the new one after a release. The
specification’s rule is simple: if several versions of a library or a resource exist and none is asked for, the highest is
used. Versions are directories in the library (theme/1_0_0, theme/1_1_0) or files in a folder named after the resource
(print.css/1_0.css, print.css/1_2.css).
<h:outputStylesheet library="theme" name="theme.css"/>
<h:outputStylesheet library="css" name="print.css"/>
The test showed three things that the text does not say:
-
The tags cannot select a version. The
libraryattribute takes a name only. The page always got the highest version.theme.cssreturned the contents of1_1_0, andprint.cssthe contents of1_2.css. -
Mojarra puts the version in the URL; MyFaces does not. Mojarra rendered
theme.css.xhtml?ln=theme&v=1_1_0andprint.css.xhtml?ln=css&v=1_2, MyFacestheme.css.xhtml?ln=themeandprint.css.xhtml?ln=css. Requests for?ln=theme&v=1_0_0and&v=1_1_0returned the same1_1_0contents on both implementations:vis only part of the URL, and so of the browser cache key; it does not select a version. On MyFaces a new release therefore does not change the URL, and a browser that cached the style sheet for a week keeps the old one (reasoned from the URL and themax-ageseen, not run in a browser). -
Files at the library root are ignored once versions exist. A
dark.cssplaced inresources/theme/next to1_0_0and1_1_0was not found: Mojarra renderedhref="RES_NOT_FOUND", MyFaces rendered nothing. This is the specification’s rule that leaf files under a versioned library must be ignored.
Because the version is not portable, the reliable way to refresh a long-cached resource on every implementation is to put a changing token in the URL yourself. That is the first example of A Custom ResourceHandler.
Localized Resources
A locale prefix puts a language-specific variant in front of the library: resources/es/img/logo.svg is the Spanish
img/logo.svg. The prefix is read from the application message bundle declared in faces-config.xml, under the key
jakarta.faces.resource.localePrefix (ResourceHandler.LOCALE_PREFIX), so the bundle for each locale defines which
prefix applies. When the localized file does not exist, the handler falls back to the unprefixed one (specified, not run).
<?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>
<resource-handler>org.example.bookshelf.web.BuildIdResourceHandler</resource-handler>
<resource-handler>org.example.bookshelf.web.PaletteResourceHandler</resource-handler>
<message-bundle>org.example.bookshelf.resourcemessages</message-bundle>
<locale-config>
<default-locale>en</default-locale>
<supported-locale>es</supported-locale>
</locale-config>
</application>
</faces-config>
Source: Jakarta Faces 4.1 API: ResourceHandler.LOCALE_PREFIX and Jakarta Faces 4.1 API: ResourceHandlerWrapper
# resourcemessages.properties: no prefix for the default locale
placeholder=x
# resourcemessages_es.properties
jakarta.faces.resource.localePrefix=es
Source: Jakarta Faces 4.1 API: ResourceHandler.LOCALE_PREFIX and Jakarta Faces 4.1 specification, 2.6.1.4
A request with Accept-Language: es rendered logo.svg.xhtml?ln=img&loc=es and that URL returned the Spanish SVG; an English
request rendered the URL without loc and received the English one, on both implementations. This is a different mechanism from
the resource bundles used for text (Messages and Internationalization); the bundle here
is only the place where the prefix is looked up. The default bundle file of the example holds a dummy entry so that the bundle exists for locales that have no prefix (an
empty default file was not tried).
Expressions Inside Resource Files
The bytes of a resource are not sent as they are. the API documentation of Resource.getInputStream() requires every
Jakarta Expression Language expression in the resource to be evaluated before the bytes are served. This is how a style sheet refers to images
that live in the same resource library structure without hard-coding the URL:
/* Bookshelf site style; the EL below is evaluated when the file is served */
body { font-family: sans-serif; background: url(#{resource['img:logo.svg']}) no-repeat top right; }
.book { border: 1px solid #ccc; }
Fetched through the resource handler, the line became
background: url(/bookshelf/jakarta.faces.resource/logo.svg.xhtml?ln=img) no-repeat top right; on both implementations. That
is the reason to keep a style sheet’s images in resources instead of using relative paths such as ../img/logo.svg, which
would break because the style sheet is not served from /css/ (reasoned from the URL, not run).
The same Javadoc warns that, because of browser and server caching, an expression is in practice evaluated once, when the
resource is first served, so an expression that refers to per-request or per-user data goes stale. Use EL in resources for stable values
(other resources, constants, context path), not for the user name or a token. The fetch through the web root bypasses the
evaluation altogether (see Where Resources Live): the style sheet fetched directly from /bookshelf/resources/css/bookshelf.css
still contained #{resource['img:logo.svg']} as text.
Caching Headers
Resource requests are the part of an application whose headers matter most, because they are repeated for every page. What
the two implementations sent, on bookshelf.css and logo.svg, depends on the project stage
(see the context parameters and what the stage changes):
| Mojarra 4.1.17 | MyFaces 4.1.4 | |
|---|---|---|
|
|
|
|
|
|
Conditional request with |
|
|
Neither implementation sent an Expires header in this run. The seven days are the default of both
(604800000 milliseconds). They are configurable, with implementation parameters:
| Parameter | Effect |
|---|---|
Mojarra |
Milliseconds a resource may be cached by the client. With |
MyFaces |
The same, in milliseconds, "set on the Expires header" in its documentation. With |
Mojarra |
Comma-separated types gzip-compressed when served. With |
Mojarra |
Documented by the implementations (update-check interval, timestamp caching, buffer size, resource-existence cache); not run. |
The behaviour on an edited file is the one to know in practice. In Development, a changed file was served with new bytes and a
new Last-Modified a few seconds later (the container’s own static-file cache holds a file for a short time). In Production
the new bytes were served as well, but both implementations kept reporting the Last-Modified of the first request; combined
with max-age=604800, a browser does not even ask again within the week. Treat a production resource as immutable and change
its URL when its content changes. A browser fetches a file again before its max-age has passed only when the URL is different;
the first example of the next section produces such a URL.
A Custom ResourceHandler
The handler is replaceable: the specification’s decorator pattern lets you wrap the existing one. Extend
ResourceHandlerWrapper and pass the wrapped handler to the constructor (the no-argument constructor is deprecated), then
register the class in faces-config.xml with <resource-handler> inside <application> (the file shown in Localized Resources does so
for both examples). The implementation instantiates the class with the handler built so far as the constructor argument.
Cache-Busting URLs and Immutable Caching
Goal: every resource URL ends in the build number, and a response whose URL carries the current build is cacheable for a year.
Two parts are needed. The URL part wraps each Resource with a ResourceWrapper that appends the parameter to
getRequestPath(). The header part cannot simply call setResponseHeader("Cache-Control", ...) before delegating: both
implementations set their own Cache-Control while they serve the resource and overwrote the header (in the first attempt the
response still had max-age=604800 on Production, on both implementations). Replacing the response with a wrapper that rewrites the
header works, through the portable ExternalContext.setResponse().
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 jakarta.faces.context.ExternalContext;
import jakarta.faces.context.FacesContext;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponseWrapper;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
/**
* Adds the build id to every resource URL it renders and marks the responses for URLs that
* carry the current build id as cacheable for a year.
*/
public class BuildIdResourceHandler extends ResourceHandlerWrapper {
static final String PARAM = "b";
private static final String IMMUTABLE = "public, max-age=31536000, immutable";
private final String buildId;
public BuildIdResourceHandler(ResourceHandler wrapped) {
super(wrapped);
// set by the build or the container, for example -Dbookshelf.build=2026.10.1
this.buildId = System.getProperty("bookshelf.build", "dev");
}
@Override
public Resource createResource(String resourceName) {
return wrap(super.createResource(resourceName));
}
@Override
public Resource createResource(String resourceName, String libraryName) {
return wrap(super.createResource(resourceName, libraryName));
}
@Override
public Resource createResource(String resourceName, String libraryName, String contentType) {
return wrap(super.createResource(resourceName, libraryName, contentType));
}
private Resource wrap(Resource resource) {
return resource == null ? null : new BuildIdResource(resource, buildId);
}
@Override
public void handleResourceRequest(FacesContext context) throws IOException {
ExternalContext external = context.getExternalContext();
if (!buildId.equals(external.getRequestParameterMap().get(PARAM))) {
super.handleResourceRequest(context);
return;
}
// The implementation sets its own Cache-Control while it serves the resource, so
// the header is replaced on the way out instead of being set beforehand.
Object original = external.getResponse();
external.setResponse(new HttpServletResponseWrapper((HttpServletResponse) original) {
@Override
public void setHeader(String name, String value) {
super.setHeader(name, "Cache-Control".equalsIgnoreCase(name) ? IMMUTABLE : value);
}
@Override
public void addHeader(String name, String value) {
super.addHeader(name, "Cache-Control".equalsIgnoreCase(name) ? IMMUTABLE : value);
}
});
try {
super.handleResourceRequest(context);
} finally {
external.setResponse(original);
}
}
/** A resource whose request path ends in the build id. */
static class BuildIdResource extends ResourceWrapper {
private final String buildId;
BuildIdResource(Resource wrapped, String buildId) {
super(wrapped);
this.buildId = buildId;
}
@Override
public String getRequestPath() {
String path = super.getRequestPath();
String separator = path.contains("?") ? "&" : "?";
return path + separator + PARAM + "=" + URLEncoder.encode(buildId, StandardCharsets.UTF_8);
}
}
}
The build id comes from a system property (-Dbookshelf.build=2026.10.1, set by the build or the container; it defaults to
dev), not from user input. With this handler and Production, the markup contained
bookshelf.css.xhtml?ln=css&b=dev and the responses were:
| Request | Cache-Control |
|---|---|
|
|
|
|
|
|
A visitor who fetched ...&b=dev keeps the file for a year; the next release has another build id, therefore another URL, and
every browser fetches the new file. The same URL works on both implementations, which the v parameter of Mojarra alone does
not (Versioning Libraries and Resources). Anyone can append b=dev to any resource URL, which only gives them a long-cached copy of what they
already could download, so the build id carries no security meaning. Do not use a value that is secret.
A Generated Resource
Goal: generated/palette.css is not a file but is produced from configuration, and still behaves like any resource in the
views (h:outputStylesheet library="generated" name="palette.css"). The trick is to keep a one-line placeholder file in
resources/generated/palette.css so that the standard code creates the resource with the right library, name and request
path (extension or prefix mapping, ln parameter, jsessionid handling), and to intercept only the request that serves its
bytes. Because the colour comes from configuration and goes into a style sheet, it is validated against a hexadecimal pattern;
an invalid value falls back to the default instead of reaching the response.
package org.example.bookshelf.web;
import jakarta.faces.application.Resource;
import jakarta.faces.application.ResourceHandler;
import jakarta.faces.application.ResourceHandlerWrapper;
import jakarta.faces.context.ExternalContext;
import jakarta.faces.context.FacesContext;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.regex.Pattern;
/**
* Serves the resource generated/palette.css with the accent colour taken from configuration.
* The file in resources/generated/ only exists so that the resource has a library, a name
* and a correct request path; its bytes are never sent.
*/
public class PaletteResourceHandler extends ResourceHandlerWrapper {
private static final String LIBRARY = "generated";
private static final String NAME = "palette.css";
private static final Pattern HEX_COLOUR = Pattern.compile("#[0-9a-fA-F]{6}");
public PaletteResourceHandler(ResourceHandler wrapped) {
super(wrapped);
}
@Override
public void handleResourceRequest(FacesContext context) throws IOException {
ExternalContext external = context.getExternalContext();
if (!LIBRARY.equals(external.getRequestParameterMap().get("ln")) || !isPaletteRequest(external)) {
super.handleResourceRequest(context);
return;
}
if (createResource(NAME, LIBRARY) == null) {
external.setResponseStatus(404);
return;
}
byte[] body = css(accent()).getBytes(StandardCharsets.UTF_8);
external.setResponseContentType("text/css");
external.setResponseCharacterEncoding("UTF-8");
external.setResponseHeader("Cache-Control", "public, max-age=3600");
external.setResponseContentLength(body.length);
try (OutputStream out = external.getResponseOutputStream()) {
out.write(body);
}
}
private static boolean isPaletteRequest(ExternalContext external) {
String path = external.getRequestServletPath()
+ (external.getRequestPathInfo() == null ? "" : external.getRequestPathInfo());
return path.contains(ResourceHandler.RESOURCE_IDENTIFIER + "/" + NAME);
}
/** The accent colour comes from configuration and is validated before it reaches the CSS. */
private static String accent() {
String configured = System.getProperty("bookshelf.accent", "#2b5d8a");
return HEX_COLOUR.matcher(configured).matches() ? configured : "#2b5d8a";
}
private static String css(String accent) {
return ":root { --bookshelf-accent: " + accent + "; }\n";
}
}
The placeholder file is one comment line: /* placeholder: the bytes are generated by PaletteResourceHandler */. On both
implementations GET .../palette.css.xhtml?ln=generated returned :root { --bookshelf-accent: #2b5d8a; } with
Content-Type: text/css;charset=UTF-8 and Cache-Control: public, max-age=3600; every other resource passed through to the
wrapped handler. The accent can be changed with -Dbookshelf.accent=#8a2b2b (a value such as red;}body{display:none failed
the pattern and the response contained the default colour, on Mojarra; MyFaces was not run for that case).
What a replacement must keep in mind:
-
The wrapper’s
createResource()methods are what the renderers call (Application.getResourceHandler()returns the outermost handler), buthandleResourceRequest()of the default implementation creates resources with its own methods. AResourcereturned only from your wrapper is not found when its URL is requested. Either serve the request yourself (as above) or leave the serving to the delegate and change only what is rendered. -
createViewResource()is what the Facelets compiler uses to load a view, so a decorator that only wants to affect views overrides that one and not thecreateResource()family. -
A handler must be thread-safe: one instance serves all requests.
-
Do not read a file path from a request parameter in
handleResourceRequest(); library and name must go throughcreateResource(), which does not accept relative library names (the request withln=..answered404).
OmniFaces ships a CombinedResourceHandler that combines the style sheets and scripts of a page into fewer requests; it is
described on the OmniFaces page (the performance page measured it) and in the OmniFaces showcase, so there is
no need to write another.
A component library may also replace the head renderer and add resources of its own: PrimeFaces renders the h:head through its own HeadRenderer, which orders the theme, the icons, the registered resources and the first, middle and last facets, and appends v=16.0.0 to its resource URLs unless primefaces.HIDE_RESOURCE_VERSION is set; the order and the results of the run are on the PrimeFaces setup page.
Mojarra and MyFaces
Both implementations agree on the specified behaviour. The differences found:
| Mojarra 4.1.17 | MyFaces 4.1.4 | |
|---|---|---|
Version in the URL |
|
none |
Missing |
Renders |
Renders nothing and logs a warning ("will be silenty ignored") |
Missing |
|
|
Plain |
Nothing rendered; two Development messages |
Nothing rendered; no message |
|
Dependencies first |
Declaration order |
Position of a |
Before |
After it |
Same file referenced as |
Two links |
One link |
|
|
|
|
Weak ETag |
None |
Gzip of resources |
|
None |
Parameter for the cache lifetime |
|
|
What Changed in Which Version
-
2.0: resources, the
resourceEL object,h:outputScript,h:outputStylesheet,h:graphicImagewithlibrary/name,ResourceHandler,Resource,@ResourceDependencyand@ResourceDependencies(Since: 2.0in the API documentation ofResourceHandler,ResourceandResourceDependency). -
2.2:
jakarta.faces.WEBAPP_RESOURCES_DIRECTORYandWEBAPP_CONTRACTS_DIRECTORY(Since: 2.2), resource library contracts,ResourceHandler.createViewResource()andcreateResourceFromId(),ViewResource. The change list also clarifies the pseudocode for libraries and the resource-identifier rules. -
2.3:
ResourceHandler.markResourceRendered(),isResourceRendered(),getViewResources()and the script constantsFACES_SCRIPT_LIBRARY_NAME/FACES_SCRIPT_RESOURCE_NAME(Since 2.3 in the API documentation; the change list for 2.3 names the first three). -
3.0: the
javax.facesnamespace becomesjakarta.faces(change list B.3), which also renames the library of the Ajax script. -
4.0: the
typeattribute is no longer rendered onlinkandscriptwhen the doctype is HTML5 (issue 1565); the Ajax script is renamed fromjsf.jstofaces.js(see the Ajax page); theJSF_SCRIPT_LIBRARY_NAME/JSF_SCRIPT_RESOURCE_NAMEconstants are deprecated for removal in favour of theFACES_*ones;ResourceResolveris removed (issue 1583 in the 4.0 change list). -
4.1: the specification’s change list for 4.1 contains no resource-handling entry. This page was run on 4.1 only.
-
5.0 (upcoming): milestones only; this page was not checked against them and nothing here describes 5.0.
Where the Books Are Outdated
Pro JSF and Ajax (2006) was written for JSF 1.1 and 1.2, which had no resource concept. Its answer to "how does a component
library ship its style sheet and script inside a JAR" is the third-party Weblets project: a weblets-config.xml file in the
JAR’s META-INF, a servlet-like "weblet" with a name, a class that reads a Java package from the classpath, a URL pattern and a
version, and an address syntax of the form weblet://<name>/<path> that a custom view handler rewrites into the real URL and
a phase listener serves. Almost every part has a standard replacement:
| Weblets (book) | Faces 4.1 |
|---|---|
|
The |
|
|
A weblet version in the URL, |
Library and resource version directories, the project stage for |
A view handler and a phase listener integrate it |
|
Keep |
|
The book’s XUL and HTC render kits depend on resources of that era (an .htc behaviour file is an Internet Explorer
mechanism) and are historical.
Other examples show a wiring that no longer works. The JSF 2.0 Cookbook loads the Ajax script with
<h:outputScript name="jsf.js" library="javax.faces" target="head"/>; in Faces 4.x the library is jakarta.faces and the file
faces.js (a request for the old names returned 404, see Getting faces.js onto the Page).
JavaServer Faces 2.0: The Complete Reference explains the decorator pattern with a ResourceHandlerWrapper and the
javax.faces.application imports, and says that the implementation first looks for a public one-argument constructor and
falls back to a no-argument one; in 4.1 the no-argument constructor of the wrapper is deprecated and the one-argument
constructor is the way (as in the examples above). Its examples of h:outputStylesheet with library and name and of #{resource['style:logo.jpg']} still work unchanged.
Pitfalls
-
Plain
<head>and<body>. Nothing relocated is rendered, and MyFaces does not say so. Useh:headandh:body. -
Secrets under
resources/. The directory is a static folder as well; the type filter of the resource handler does not apply to direct requests. UseWEB-INF/resourcesthroughjakarta.faces.WEBAPP_RESOURCES_DIRECTORYfor anything that must not be downloadable, and keep real configuration out of it altogether (security page). -
Hard-coded
/bookshelf/resources/...paths. They skip versioning, locale, relocation, EL evaluation and the type filter, and they break when the context path changes. Use the tags or#{resource[...]}. -
Misspelling a library or name on Mojarra. The page still renders, with
RES_NOT_FOUNDURLs, in Production too. Watch the browser’s network tab or the Development messages. -
Relying on
&v=. It exists on Mojarra only and does not select a version. -
A changing file with a seven-day
max-age. In Production the browser will not ask again. Change the URL. -
jsessionidin resource URLs. Set the cookie as the only tracking mode. -
A library named like a locale or a version (
de,2_3). Avoid it. -
A
librarycontaining a slash. It produced a dead link on Mojarra. Put the folder inname. -
A stale EL value in a resource. The value is computed once per cache entry; never put per-user data there.
-
Ordering between a declared style sheet and a
@ResourceDependency. It differs between implementations. -
Inline resource bodies with user data. The body of an inline
h:outputScriptis written unescaped. -
Assuming
h:outputStylesheet target="body"works. It is rendered in the head.
References
-
Jakarta Faces 4.1 specification, Appendix B changes between versions
-
Jacobi, Fallows: Pro JSF and Ajax: Building Rich Internet Components, Apress, 2006, publisher page
-
Burns, Schalk: JavaServer Faces 2.0: The Complete Reference, McGraw-Hill, 2010, publisher page
-
Leonard: JSF 2.0 Cookbook, Packt, 2010, publisher page