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:outputScript and h:graphicImage are UIOutput/UIGraphic components 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, the 304 Not Modified answers and the filtering of file types happen. In lifecycle terms (see the phases) a resource request never enters the six phases at all: FacesServlet calls isResourceRequest() 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

resources/<resourceIdentifier> under the web application root

The default resources directory of the WAR.

Its name can be changed with jakarta.faces.WEBAPP_RESOURCES_DIRECTORY (no leading /, may contain /).

META-INF/resources/<resourceIdentifier> on the classpath

The same layout inside a JAR, normally in WEB-INF/lib.

The way a component library or a shared "assets" JAR ships its files. Also holds META-INF/flows/... for Faces Flows.

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:

The resources directory of Bookshelf
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.txt existed 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, so GET /bookshelf/resources/css/bookshelf.css answered 200 with the file as it is on disk, with the EL expression of the next sections unevaluated, and so did resources/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.css returned the META-INF/resources/shared/shared.css of the JAR. Never put anything secret in resources/ or in META-INF/resources/: the type filter of the resource handler does not protect it.

  • A hidden resources directory works. With jakarta.faces.WEBAPP_RESOURCES_DIRECTORY set to WEB-INF/resources and the directory moved there, a resource request still returned 200 while /bookshelf/resources/css/bookshelf.css and /bookshelf/WEB-INF/resources/css/bookshelf.css returned 404, on both implementations. This is the safe place for files that are not meant to be fetched directly.

Moving the resources directory out of reach of direct requests
<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 libraryName must not be an underscore-separated sequence of numbers (such as 2_3) nor a locale string (such as de or de_AT), because those would be taken for a version or a locale prefix.

  • libraryVersion and resourceVersion are underscore-separated lists of integers such as 1_0_0; a resource version may carry a file extension (1_2.css).

  • If a libraryVersion directory exists, files placed directly in the library folder are ignored.

The specification’s own examples, expressed with the Bookshelf names:

Identifier Meaning

logo.svg

A resource without a library.

img/logo.svg

Library img, resource logo.svg.

css/admin/tables.css

Library css, resource name admin/tables.css (a resource name may contain /).

theme/1_1_0/theme.css

Library theme in version 1_1_0.

css/print.css/1_2.css

Resource print.css in version 1_2 (the extension belongs to the version file).

es/img/logo.svg

The Spanish variant (locale prefix es) of img/logo.svg.

Observations that go beyond the text of the specification:

  • A library folder named de or 2_3 was 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 returned 404; 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 the ln parameter. Referring to the same file once as name="css/bookshelf.css" and once as library="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

h:outputStylesheet

A <link rel="stylesheet" href="..." /> in the head, or a <style> element when it has body content and no name.

library, name (required unless the content is inline), rendered, id, binding.

h:outputScript

A <script src="...">, or an inline <script> when it has body content and no name.

library, name, target, rendered, id, binding.

h:graphicImage

An <img>. With library and name the source is a resource; with url or value it is a context-relative URL.

library, name, url, value, alt, and the HTML pass-through attributes (width, height, style, …​).

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.

catalogue.xhtml: resources in the head and in the body
<!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:

Rendered markup of catalogue.xhtml (Mojarra 4.1.17, line breaks added)
<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&amp;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 target is rendered where it stands, inside h: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[...]} in h:graphicImage and in plain HTML all produce /bookshelf/jakarta.faces.resource/logo.svg.xhtml?ln=img.

  • jsessionid can appear in the URLs. On the first request of a visitor without a session cookie, both implementations rendered every resource URL as bookshelf.css.xhtml;jsessionid=...?ln=css, because the URL goes through ViewHandler.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> in web.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.

Inline script and style
<!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:

Where each script landed (Mojarra 4.1.17, abbreviated, annotations added; MyFaces differs only inside the form)
<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.

The rules that this and further runs gave:

  • h:outputStylesheet ignores target. The VDL lists no target attribute 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() and markResourceRendered() for it). Two tags for catalogue.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.js declared with target="form" and again with target="body", and b.js declared with body then with form, were both rendered once, in the form, because the form comes before the end of the body. A script declared both without target and with target="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:head and h:body. With a plain <head>/<body> pair, none of the three resources of a test page (a style sheet, target="head" and target="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().

A component with two style sheets and a renderer that asks for a script at the end of the body
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");
    }
}
badges.xhtml: two badges, one style sheet from the component, one script from the renderer
<!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.

Adding a style sheet to the head from a pre-render listener
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");
    }
}
dynamic.xhtml: the listener runs before the view is rendered
<!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>

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 (*.xhtml, *.faces)

context path + /jakarta.faces.resource/ + name + extension

/bookshelf/jakarta.faces.resource/bookshelf.css.xhtml?ln=css, or ...bookshelf.css.faces?ln=css

Prefix (/faces/*)

context path + prefix + /jakarta.faces.resource/ + name

/bookshelf/faces/jakarta.faces.resource/bookshelf.css?ln=css

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, answer 404.

  • Extract the resource name; if there is none, answer 404. Read the library from the request parameter ln.

  • Create the resource with createResource(name, library) (or createResource(name)); if it cannot be created, answer 404.

  • Ask Resource.userAgentNeedsUpdate(), which consults If-Modified-Since; if the browser is up to date, answer 304.

  • Set the content type from getContentType(), copy every entry of getResponseHeaders() into the response, then stream getInputStream() and set the content length.

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:

Table 1. Requests against the resource handler (identical on Mojarra and MyFaces)
Request Status Comment

bookshelf.css.xhtml?ln=css

200

The resource.

nothing.css.xhtml?ln=css

404

No such resource.

app.properties.xhtml?ln=css, page.xhtml.xhtml?ln=css, Foo.class.xhtml?ln=css

404

The three files exist; their extensions are excluded by default.

bookshelf.css.xhtml?ln=.., web.xml.xhtml?ln=..%2FWEB-INF

404

A library with a relative path never matches.

..%2FWEB-INF%2Fweb.xml.xhtml as the name

400

Rejected by the container before the application.

Same excluded files with jakarta.faces.RESOURCE_EXCLUDES set to .class

200, 200, 404

app.properties and page.xhtml are now served; only Foo.class is still excluded.

Configuring the excluded extensions (this removes .properties and .xhtml from the list: do not do it)
<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 library attribute takes a name only. The page always got the highest version. theme.css returned the contents of 1_1_0, and print.css the contents of 1_2.css.

  • Mojarra puts the version in the URL; MyFaces does not. Mojarra rendered theme.css.xhtml?ln=theme&v=1_1_0 and print.css.xhtml?ln=css&v=1_2, MyFaces theme.css.xhtml?ln=theme and print.css.xhtml?ln=css. Requests for ?ln=theme&v=1_0_0 and &v=1_1_0 returned the same 1_1_0 contents on both implementations: v is 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 the max-age seen, not run in a browser).

  • Files at the library root are ignored once versions exist. A dark.css placed in resources/theme/ next to 1_0_0 and 1_1_0 was not found: Mojarra rendered href="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).

faces-config.xml: the message bundle that supplies the locale prefix, and the two resource handlers of this page
<?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>
org/example/bookshelf/resourcemessages.properties (default) and resourcemessages_es.properties
# resourcemessages.properties: no prefix for the default locale
placeholder=x

# resourcemessages_es.properties
jakarta.faces.resource.localePrefix=es

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:

resources/css/bookshelf.css
/* 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

Development

Cache-Control: no-store, must-revalidate, Last-Modified, weak ETag

Cache-Control: no-cache, Last-Modified

Production

Cache-Control: max-age=604800 (seven days), Last-Modified, weak ETag

Cache-Control: max-age=604800, Last-Modified

Conditional request with If-Modified-Since (Production)

304, no body

304, no body

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 com.sun.faces.defaultResourceMaxAge

Milliseconds a resource may be cached by the client. With 3600000 the header was max-age=3600.

MyFaces org.apache.myfaces.RESOURCE_MAX_TIME_EXPIRES

The same, in milliseconds, "set on the Expires header" in its documentation. With 3600000 the header was max-age=3600.

Mojarra com.sun.faces.compressableMimeTypes

Comma-separated types gzip-compressed when served. With text/css,... a request with Accept-Encoding: gzip answered Content-Encoding: gzip. MyFaces does not have it: the same parameter gave an uncompressed response.

Mojarra com.sun.faces.resourceUpdateCheckPeriod, com.sun.faces.cacheResourceModificationTimestamp, com.sun.faces.resourceBufferSize; MyFaces org.apache.myfaces.RESOURCE_CACHE_LAST_MODIFIED, org.apache.myfaces.RESOURCE_BUFFER_SIZE, org.apache.myfaces.RESOURCE_HANDLER_CACHE_ENABLED

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().

BuildIdResourceHandler.java
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:

Table 2. Cache-Control with and without the current build id (identical on Mojarra and MyFaces, Production)
Request Cache-Control

logo.svg.xhtml?ln=img&b=dev

public, max-age=31536000, immutable

logo.svg.xhtml?ln=img&b=old

max-age=604800

logo.svg.xhtml?ln=img

max-age=604800

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.

PaletteResourceHandler.java
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), but handleResourceRequest() of the default implementation creates resources with its own methods. A Resource returned 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 the createResource() 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 through createResource(), which does not accept relative library names (the request with ln=.. answered 404).

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

&v=1_1_0 for a versioned library, &v=1_2 for a versioned resource

none

Missing h:outputStylesheet or h:outputScript resource

Renders href="RES_NOT_FOUND" or src="RES_NOT_FOUND", in Production too; Development adds messages

Renders nothing and logs a warning ("will be silenty ignored")

Missing h:graphicImage resource

<img src="RES_NOT_FOUND" />, plus a Development message

<img src="RES_NOT_FOUND" />, plus a Development message

Plain <head>/<body> with relocated resources

Nothing rendered; two Development messages

Nothing rendered; no message

@ResourceDependency order against declared tags

Dependencies first

Declaration order

Position of a target="form" script

Before jakarta.faces.ViewState

After it

Same file referenced as name="css/x.css" and library="css" name="x.css"

Two links

One link

Cache-Control in Development

no-store, must-revalidate

no-cache

ETag on resources

Weak ETag

None

Gzip of resources

com.sun.faces.compressableMimeTypes

None

Parameter for the cache lifetime

com.sun.faces.defaultResourceMaxAge

org.apache.myfaces.RESOURCE_MAX_TIME_EXPIRES

What Changed in Which Version

  • 2.0: resources, the resource EL object, h:outputScript, h:outputStylesheet, h:graphicImage with library/name, ResourceHandler, Resource, @ResourceDependency and @ResourceDependencies (Since: 2.0 in the API documentation of ResourceHandler, Resource and ResourceDependency).

  • 2.2: jakarta.faces.WEBAPP_RESOURCES_DIRECTORY and WEBAPP_CONTRACTS_DIRECTORY (Since: 2.2), resource library contracts, ResourceHandler.createViewResource() and createResourceFromId(), ViewResource. The change list also clarifies the pseudocode for libraries and the resource-identifier rules.

  • 2.3: ResourceHandler.markResourceRendered(), isResourceRendered(), getViewResources() and the script constants FACES_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.faces namespace becomes jakarta.faces (change list B.3), which also renames the library of the Ajax script.

  • 4.0: the type attribute is no longer rendered on link and script when the doctype is HTML5 (issue 1565); the Ajax script is renamed from jsf.js to faces.js (see the Ajax page); the JSF_SCRIPT_LIBRARY_NAME/JSF_SCRIPT_RESOURCE_NAME constants are deprecated for removal in favour of the FACES_* ones; ResourceResolver is 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

weblets-config.xml with a name, class and URL pattern

The META-INF/resources/<library>/ directory; no configuration.

weblet://name/file URLs written by renderers

library and name attributes, #{resource['library:name']}, Resource.getRequestPath().

A weblet version in the URL, -SNAPSHOT for development

Library and resource version directories, the project stage for Development versus Production headers, and your own build id (A Custom ResourceHandler). The book’s idea that a stable URL allows an endless cache and a new release a new URL is right; Faces only gives the means.

A view handler and a phase listener integrate it

FacesServlet and ResourceHandler; resource requests skip the lifecycle.

Keep .class files unreachable by putting resources in a resources package

jakarta.faces.RESOURCE_EXCLUDES (How a Resource Request Is Served), and the caveat of Where Resources Live.

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. Use h:head and h: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. Use WEB-INF/resources through jakarta.faces.WEBAPP_RESOURCES_DIRECTORY for 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_FOUND URLs, 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.

  • jsessionid in 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 library containing a slash. It produced a dead link on Mojarra. Put the folder in name.

  • 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:outputScript is written unescaped.

  • Assuming h:outputStylesheet target="body" works. It is rendered in the head.