Resource Library Contracts

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), Eclipse Mojarra 4.1.17 and Apache MyFaces Core 4.1.4, with Java 17 or later (the Java classes were compiled with --release 21). Resource library contracts exist since Faces 2.2, so everything below also applies to Faces 4.0, except for the schema and namespace versions noted in What Changed. The listings are files, or excerpts of files, of a scratch Bookshelf WAR that was deployed on Apache Tomcat 11.0.26 with Weld 6.0.4.Final, once with Mojarra and once with MyFaces, and requested with curl and a small Python client: every output and error message quoted comes from those runs (checked on 2026-10-08). Where the two implementations disagree, the text says so. Faces 5.0 is still a milestone release and is not described as current behaviour.

A web application usually has more than one face. The same catalogue may have to look like a public library’s site, like a university’s, and like a plain back office, and the three must not diverge in behaviour. Copying the views three times is wrong, and so is sprinkling rendered conditions and inline style switches over every page. Faces solves this with resource library contracts: a contract is a named directory that holds the templates and the resources (stylesheets, scripts, images) of one look and feel, and Faces decides, per view and per request, which contracts the view may use. The views keep saying template="/layout.xhtml"; which layout.xhtml they get depends on the contract.

What a Contract Is

The specification defines a resource library contract as a resource library, with the rules described on the resource handling page, that lives in the contracts directory of the web application root, or in the META-INF/contracts entry of a JAR, instead of in resources or META-INF/resources. Three ideas make the feature work:

  • A contract has a name, the name of its directory (classic, northside, campus). The name is what you list in configuration, in f:view or in code.

  • A contract has a declaration. The specification calls the templates a contract offers its declared templates, the ui:insert names in them declared insertion points and the files they use declared resources. No file naming is imposed, but a view that is a template client of layout.xhtml and fills the insertion points title, nav and content can only work with a contract whose layout.xhtml offers exactly those. Two contracts that declare the same templates and insertion points are interchangeable, and that is the whole point: swap the contract and the views stay as they are.

  • Template lookup becomes contract aware. When a view says <ui:composition template="/layout.xhtml">, Facelets asks ResourceHandler.createViewResource for /layout.xhtml. If the contract list of the current view is not empty, the implementation looks for the file inside each listed contract, in order, and uses the first one found; only when the list is empty does it look in the web root. Resources that a page or a template refers to by name alone (without a library) are looked up the same way, and the rendered URL carries the contract name in a con request parameter.

Where this sits in the request processing lifecycle: the list of contracts of a view is calculated when the view is created (ViewDeclarationLanguage.createView calls calculateResourceLibraryContracts with the view id and stores the result on the FacesContext), and the f:view tag handler can replace it while the view is built. With partial state saving (the default) the view is built again in Restore View on a postback, so the list is calculated again. The template and the h:outputStylesheet/h:graphicImage references are resolved during that build. The browser then fetches the stylesheet and the images in separate requests to jakarta.faces.resource/..., which are served by the resource handler and never go through the lifecycle. The earlier pages of this guide kept the template in /WEB-INF/templates/layout.xhtml (see Templating and the project layout); moving it into a contract moves it out of that path-based lookup and into a name-based one.

Where Contracts Live

Location Contract name Notes

contracts/<name>/ in the web root

The directory name

The default. Any sub-directory of contracts is a contract; no marker file is needed.

A directory named by jakarta.faces.WEBAPP_CONTRACTS_DIRECTORY, for example WEB-INF/contracts/<name>/

The directory name

The value is a path relative to the web root and must not start with a slash. Changing it is the way to keep the contracts out of reach of browsers (see Security).

META-INF/contracts/<name>/ inside a JAR on the class path, typically WEB-INF/lib

The directory name

Each contract must contain the marker file jakarta.faces.contract.xml, which may be empty. See Packaging Contracts as JARs.

The context parameter is ResourceHandler.WEBAPP_CONTRACTS_DIRECTORY_PARAM_NAME in the API and jakarta.faces.WEBAPP_CONTRACTS_DIRECTORY in web.xml; its default is contracts and it exists since 2.2. The marker file name is the constant ResourceHandler.RESOURCE_CONTRACT_XML. The specification says a future version may use the file to declare the contract; today an empty file is enough.

The Bookshelf ships four contracts, kept in WEB-INF/contracts so that browsers cannot request them directly:

src/main/webapp/
  WEB-INF/
    contracts/
      classic/   layout.xhtml  footer.xhtml  theme.css  logo.svg
      northside/ layout.xhtml                theme.css  logo.svg
      campus/    layout.xhtml                theme.css  logo.svg
      admin/     layout.xhtml                theme.css  logo.svg
    web.xml  faces-config.xml  beans.xml
  catalogue.xhtml
  about.xhtml
  admin/catalogue.xhtml
web.xml (excerpt): the contracts directory
    <context-param>
        <param-name>jakarta.faces.WEBAPP_CONTRACTS_DIRECTORY</param-name>
        <param-value>WEB-INF/contracts</param-value>
    </context-param>

The Bookshelf Contracts

All four contracts declare the template layout.xhtml with the insertion points title, nav and content, and the resources theme.css and logo.svg. The default contract, classic:

WEB-INF/contracts/classic/layout.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:ui="jakarta.faces.facelets">
<h:head>
    <title><ui:insert name="title">Bookshelf</ui:insert></title>
    <h:outputStylesheet name="theme.css"/>
</h:head>
<h:body styleClass="classic">
    <header>
        <h:graphicImage name="logo.svg" alt="Bookshelf" width="48" height="48"/>
        <span class="brand">Bookshelf</span>
        <nav><ui:insert name="nav"/></nav>
    </header>
    <main>
        <ui:insert name="content"/>
    </main>
    <ui:include src="/footer.xhtml"/>
</h:body>
</html>
WEB-INF/contracts/classic/footer.xhtml
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="jakarta.faces.facelets">
    <footer>Bookshelf, classic theme</footer>
</ui:composition>
WEB-INF/contracts/classic/theme.css
body.classic { font-family: Georgia, serif; background: #fffdf7; color: #222; }
body.classic header { border-bottom: 2px solid #8a6d3b; padding: .5rem 1rem; }
body.classic .brand { font-size: 1.4rem; margin-left: .5rem; }

Notice that the template refers to theme.css and logo.svg by name only, without a library attribute, and that ui:include src="/footer.xhtml" also names a file that exists in the contract and nowhere else: includes, decorations and templates reached from a contract template are resolved through the same contract list (an include of an absolute path worked in both implementations).

The campus contract keeps the declaration but changes the arrangement completely, a sidebar instead of a header:

WEB-INF/contracts/campus/layout.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:ui="jakarta.faces.facelets">
<h:head>
    <title>Campus Books | <ui:insert name="title">Catalogue</ui:insert></title>
    <h:outputStylesheet name="theme.css"/>
</h:head>
<h:body styleClass="campus">
    <aside>
        <h:graphicImage name="logo.svg" alt="Campus Books" width="48" height="48"/>
        <nav><ui:insert name="nav"/></nav>
    </aside>
    <main>
        <ui:insert name="content"/>
    </main>
</h:body>
</html>
WEB-INF/contracts/campus/theme.css
body.campus { display: flex; font-family: system-ui, sans-serif; background: #eef3fb; color: #0e2147; }
body.campus aside { width: 14rem; background: #1b3a8a; color: #fff; padding: 1rem; }
body.campus main { flex: 1; padding: 1rem; }

The northside contract has a green header and its own title pattern (Northside Library - ...); its theme.css:

WEB-INF/contracts/northside/theme.css
body.northside { font-family: system-ui, sans-serif; background: #f2faf4; color: #12301c; }
body.northside header { background: #1f7a3d; color: #fff; padding: .5rem 1rem; }
body.northside .brand { font-size: 1.4rem; margin-left: .5rem; }

The admin contract is the plain back-office look used for the /admin/ area. A view is a template client of the declared template and knows nothing about any of this:

catalogue.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:ui="jakarta.faces.facelets">
<ui:composition template="/layout.xhtml">
    <ui:define name="title">Catalogue</ui:define>
    <ui:define name="nav">
        <h:link outcome="/catalogue" value="Catalogue"/>
        <h:link outcome="/about" value="About"/>
    </ui:define>
    <ui:define name="content">
        <h1>Catalogue</h1>
        <p>Tenant contract: <h:outputText value="#{tenantContext.contract}"/></p>
        <ul>
            <ui:repeat value="#{catalogueBean.books}" var="book">
                <li>#{book.title} by #{book.author}</li>
            </ui:repeat>
        </ul>
    </ui:define>
</ui:composition>
</html>

Choosing the Contracts of a View

There are four ways to decide which contracts a view may use. They are applied in this order, and the last one to speak wins.

flowchart TD A[Request for a view] --> B["createView calls calculateResourceLibraryContracts(viewId)"] B --> C{Is a custom ViewDeclarationLanguage installed?} C -- yes --> D[Your code returns the list] C -- no --> E{Does a contract-mapping match the view id?} E -- yes --> F["Contracts of the best match: exact, longest prefix, then *"] E -- "mappings exist, none matches" --> G[Empty list] E -- "no mapping configured" --> H[All discovered contracts, order not defined] D --> I[Stored on the FacesContext] F --> I G --> I H --> I I --> J{Does the root of the view have f:view contracts?} J -- yes --> K[The f:view list replaces the stored one] J -- no --> L[Keep the stored list] K --> M["Templates and name-only resources: first listed contract that has the file, else the web root"] L --> M

No configuration: every contract applies

If the application has no resource-library-contracts element at all, every discovered contract is made available to every view. That is convenient for the specification’s own example, where a single JAR with a single contract is dropped into WEB-INF/lib, and risky as soon as two contracts declare the same template. With the three Bookshelf contracts in WEB-INF/contracts and a JAR contract seasonal that also declares layout.xhtml, a view without any configuration rendered the classic template on Mojarra and the seasonal template on MyFaces. Neither answer is wrong, because the order is not defined; the lesson is to configure the choice (see Pitfalls).

contract-mapping in faces-config.xml

The resource-library-contracts element of the application element maps view ids to contracts. Each contract-mapping has one or more url-pattern elements and one or more contracts elements. The three kinds of url-pattern value are the literal * (all views), an absolute prefix such as /admin/* and an exact view path such as /admin/catalogue.xhtml, all relative to the web root. When several mappings could apply, ViewDeclarationLanguage.calculateResourceLibraryContracts picks, in this order, an exact match, the longest prefix match and then *. If mappings exist and none matches the view, the list is empty.

faces-config.xml: the admin area gets its own look, everything else is classic
<?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-library-contracts>
            <contract-mapping>
                <url-pattern>/admin/*</url-pattern>
                <contracts>admin</contracts>
            </contract-mapping>
            <contract-mapping>
                <url-pattern>*</url-pattern>
                <contracts>classic</contracts>
            </contract-mapping>
        </resource-library-contracts>
    </application>
</faces-config>

With this file, /catalogue.xhtml and /about.xhtml rendered with classic and /admin/catalogue.xhtml with admin, on both implementations. With only the /admin/* mapping (no *), /catalogue.xhtml failed with HTTP 500 because the list was empty and /layout.xhtml does not exist in the web root; /admin/catalogue.xhtml worked. A three-level test with *, /a/b/* and /a/* confirmed the longest-prefix rule: /a/bx/p.xhtml matched /a/* and /a/b/p.xhtml matched /a/b/*, on both implementations.

The specification says the contracts element holds a comma separated list of contract names, and the XML Schema allows the element to be repeated. Mojarra accepted <contracts>northside,classic</contracts>. MyFaces 4.1.4 treated the whole string as one contract name and logged Resource Library Contract northside,classic was not found while scanning for available contracts., after which every view under that pattern failed with /layout.xhtml Not Found in ExternalContext as a Resource. Repeating the element works on both and is the portable form:

faces-config.xml (excerpt): a list of contracts, in order
<contract-mapping>
    <url-pattern>/a/*</url-pattern>
    <contracts>northside</contracts>
    <contracts>classic</contracts>
</contract-mapping>

A mapping can only name contracts that were discovered: in Mojarra a mapping to a name that does not exist (or a JAR contract without the marker file) logs Added contract: ... at startup and then every request to the view fails with Invalid path : /layout.xhtml. Mojarra also logs one INFO line per mapping, Processing resource library contract mapping for url-pattern: ..., which is the quickest way to see what was read.

The contracts attribute of f:view

f:view has a contracts attribute: a value expression that evaluates to a comma separated list of contract names. The VDL documentation says that if the attribute exists on the tag, even empty, the tag handler creates a list from it and unconditionally sets it on the FacesContext; it also says the attribute must only be on the outer-most file of the Facelets chain, and that any use elsewhere is undefined. In Bookshelf the outer-most file is the template client, so that is where it goes: either as the root element, or around the ui:composition.

A view that always uses the campus look, whatever the mapping says
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets">
<f:view contracts="campus">
<ui:composition template="/layout.xhtml">
    <ui:define name="title">Campus view</ui:define>
    <ui:define name="content"><h1>Campus view</h1></ui:define>
</ui:composition>
</f:view>
</html>

What was observed, identically on Mojarra and MyFaces:

  • f:view contracts overrides both the mapping and a custom ViewDeclarationLanguage for that view.

  • It can name a contract that no mapping names: with a single mapping of * to classic, a view with contracts="campus" rendered the campus template. The sentence in the specification that only the contracts named in resource-library-contracts are made available is therefore about the default list, not a restriction on f:view.

  • The attribute accepts an expression, and a comma list. contracts="northside, campus" (with a space) worked too.

  • A name that matches no contract, or an empty attribute, leaves the list without the template, and the view fails with HTTP 500 (Invalid path : /layout.xhtml on Mojarra, /layout.xhtml Not Found in ExternalContext as a Resource on MyFaces).

  • The tag worked when it was the root, around the ui:composition, inside the composition next to the ui:define elements, and even inside a ui:define. The last two are outside what the VDL documentation defines, so do not rely on them.

  • A contract that was added under WEB-INF/contracts while the server was running worked at once through f:view; a mapping would only see it after a restart, because contracts are discovered at startup.

Programmatically: ViewDeclarationLanguage

ViewDeclarationLanguage.calculateResourceLibraryContracts(FacesContext, String viewId) is the method that implements the mapping rules above, and it is an extension point: decorate the ViewDeclarationLanguage and return your own list. The ViewDeclarationLanguageFactory and ViewDeclarationLanguageWrapper classes are the decorators; the factory is registered in faces-config.xml with view-declaration-language-factory (see the page on extending Faces for the general decorator mechanism). The Bookshelf version below returns admin for everything under /admin/ and the contract of the current tenant for everything else; the tenant side is the topic of Multi-Tenant Theming of Bookshelf.

TenantViewDeclarationLanguageFactory.java
package org.example.bookshelf.tenant;

import jakarta.enterprise.inject.spi.CDI;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewDeclarationLanguage;
import jakarta.faces.view.ViewDeclarationLanguageFactory;
import jakarta.faces.view.ViewDeclarationLanguageWrapper;
import java.util.List;

/** Chooses the resource library contract of every view from the tenant of the request. */
public class TenantViewDeclarationLanguageFactory extends ViewDeclarationLanguageFactory {

    public TenantViewDeclarationLanguageFactory(ViewDeclarationLanguageFactory wrapped) {
        super(wrapped);
    }

    @Override
    public ViewDeclarationLanguage getViewDeclarationLanguage(String viewId) {
        return new TenantViewDeclarationLanguage(getWrapped().getViewDeclarationLanguage(viewId));
    }

    private static final class TenantViewDeclarationLanguage extends ViewDeclarationLanguageWrapper {

        TenantViewDeclarationLanguage(ViewDeclarationLanguage wrapped) {
            super(wrapped);
        }

        @Override
        public List<String> calculateResourceLibraryContracts(FacesContext context, String viewId) {
            if (viewId.startsWith("/admin/")) {
                return List.of("admin");
            }
            TenantContext tenant = CDI.current().select(TenantContext.class).get();
            return List.of(tenant.getContract());
        }
    }
}
faces-config.xml: registering the factory
<?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">
    <factory>
        <view-declaration-language-factory>org.example.bookshelf.tenant.TenantViewDeclarationLanguageFactory</view-declaration-language-factory>
    </factory>
</faces-config>

Notes on the code:

  • ViewDeclarationLanguageFactory has a deprecated no-argument constructor and a constructor that takes the wrapped factory; call super(wrapped) and use getWrapped(). The wrapper has the same pair.

  • The factory returns a wrapper for every view id and the wrapper overrides only calculateResourceLibraryContracts, so all other behaviour is delegated.

  • CDI.current().select(...) finds the request-scoped tenant bean. The method runs inside a request, so the bean is active; the same call during start-up would throw ContextNotActiveException.

  • Do not call FacesContext.setResourceLibraryContracts from your own code. Its API documentation allows it only during createView and in the tag handler of the UIViewRoot tag, and the list you pass is copied into an immutable one.

With this factory registered and no mapping, /catalogue.xhtml took its contract from the tenant and /admin/catalogue.xhtml was always admin, on both implementations; a view with f:view contracts still overrode the factory.

Resources Inside a Contract

A resource that a contract template refers to by name only is searched in the contracts of the current list, so the same <h:outputStylesheet name="theme.css"/> yields a different file per contract. The generated URL records which contract supplied the resource, so the browser’s later request knows where to look. For the Bookshelf templates the HTML contained:

<link rel="stylesheet" href="/bookshelf/jakarta.faces.resource/theme.css.xhtml?con=classic" />
<img src="/bookshelf/jakarta.faces.resource/logo.svg.xhtml?con=classic" alt="Bookshelf" height="48" width="48" />

#{resource['theme.css']} inside a page gave the same URL with ?con=classic. Requests to that URL were answered with the contract’s bytes (200, text/css), and a request with an unknown contract (con=nope), without con, or for layout.xhtml through the resource path was answered with 404, on both implementations. The MyFaces URLs also carry ;jsessionid=... on the first response of a session, which is URL rewriting and unrelated to contracts.

Two consequences follow:

  • The resource is addressable by anyone who knows the contract name: ...?con=northside returned the Northside stylesheet to a client that had never visited the Northside host. Contract resources are public, see Security.

  • The con value is part of the URL, so a browser that cached theme.css.xhtml?con=classic does not mix it up with the northside file; changing a file’s content, however, does not change its URL, and the usual cache considerations of the resource handling page apply.

The templates themselves are never served through the resource path: jakarta.faces.resource/layout.xhtml?con=northside was a 404.

Several Contracts in a List

A list can hold more than one contract, for example a tenant contract followed by a shared base contract that provides the templates the tenant did not override. The rule is that the first contract in the list that contains the file wins and the later ones are fallbacks.

f:view contracts="northside,campus"  template /layout.xhtml   northside layout, northside theme.css
f:view contracts="campus,northside"  template /layout.xhtml   campus layout, campus theme.css
f:view contracts="northside,campus"  template /sidebar.xhtml  found only in campus, so campus
f:view contracts="northside"         template /sidebar.xhtml  HTTP 500, not found in northside

(sidebar.xhtml was a scratch file that exists only in campus.) Mojarra and MyFaces agreed on all four rows, for contracts under WEB-INF/contracts and also for two contracts that live in the same JAR. They disagreed when the list mixed a directory contract with a JAR contract that declares the same names (classic in the web application and seasonal in a JAR, both with layout.xhtml and theme.css):

List Mojarra 4.1.17 MyFaces 4.1.4

seasonal,northside

Template from seasonal, theme.css from northside (con=northside)

Template and theme.css from seasonal

northside,seasonal

Template and theme.css from northside

Template and theme.css from seasonal

campus,seasonal and classic,seasonal

Template and theme.css from the first (the directory contract)

Template and theme.css from seasonal

In other words, the template lookup of Mojarra follows the list, but its lookup of name-only resources prefers directory contracts over JAR contracts; MyFaces prefers the JAR contract for both. A page can therefore get the layout of one contract and the stylesheet of another. The cure is not to rely on the order across the two kinds: give every contract of a list distinct file names, or keep all the contracts of a list in the same place.

Multi-Tenant Theming of Bookshelf

The scenario: the same Bookshelf WAR serves two libraries that each want their own look, and everybody else gets the default one. Tenants are told apart by the host name of the request: northside.localhost (the library) and campus.localhost (the university). The /admin/ back office is the same for everybody. The design has three parts: a tenant directory that maps hosts to contract names, a request-scoped bean that applies it to the current request, and the ViewDeclarationLanguage decorator shown in Programmatically: ViewDeclarationLanguage.

TenantDirectory.java: host to contract, from configuration
package org.example.bookshelf.tenant;

import java.util.HashMap;
import java.util.Map;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;

/** Maps the host name of a request to the name of the tenant's resource library contract. */
@ApplicationScoped
public class TenantDirectory {

    public static final String DEFAULT_CONTRACT = "classic";

    private final Map<String, String> contractByHost = new HashMap<>();

    @PostConstruct
    void load() {
        // "northside.example.org=northside,campus.example.org=campus", from configuration, never from user input
        String config = System.getProperty("bookshelf.tenants", "");
        for (String entry : config.split(",")) {
            String[] pair = entry.split("=", 2);
            if (pair.length == 2) {
                contractByHost.put(pair[0].trim().toLowerCase(), pair[1].trim());
            }
        }
    }

    public String contractFor(String host) {
        return contractByHost.getOrDefault(host == null ? "" : host.toLowerCase(), DEFAULT_CONTRACT);
    }
}
TenantContext.java: the contract of the current request
package org.example.bookshelf.tenant;

import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.servlet.http.HttpServletRequest;

@Named
@RequestScoped
public class TenantContext {

    @Inject
    HttpServletRequest request;

    @Inject
    TenantDirectory directory;

    public String getContract() {
        return directory.contractFor(request.getServerName());
    }
}

The mapping comes from a system property here (-Dbookshelf.tenants=northside.localhost=northside,campus.localhost=campus); in a real deployment it would come from the server configuration, an environment variable or a database table. It is data of the deployment, never data from the request: the host name selects an entry, and an unknown host falls back to classic. Everything else is the code of Programmatically: ViewDeclarationLanguage. Starting the scratch application and requesting the same view with different Host headers (curl -H "Host: northside.localhost" http://localhost:8410/bookshelf/catalogue.xhtml) gave:

Host View body class title

localhost or an unknown host

/catalogue.xhtml

classic

Catalogue

northside.localhost

/catalogue.xhtml

northside

Northside Library - Catalogue

campus.localhost

/catalogue.xhtml

campus

Starts with Campus Books, then the page title

any

/admin/catalogue.xhtml

admin

Catalogue

The stylesheet and logo URLs of the Northside response ended in ?con=northside; the Tenant contract: line of the page printed northside. The results were identical on Mojarra and MyFaces. The views, the beans and the navigation are the same for every tenant; adding a tenant is a new contract directory (or JAR) plus one entry in the directory.

Two details are worth knowing:

  • The tenant is decided from the request, so it is also decided again on every postback. A postback to the same host gets the same contract. If a tenant were derived from something the user can change between the GET and the POST, the template used for the postback could differ from the one that rendered the form.

  • The Host header is supplied by the client. Behind a reverse proxy, use the host that the proxy is configured to forward (and reject or normalise anything else), exactly as you would for building absolute URLs. Falling back to the default contract for unknown hosts is the safe behaviour; echoing the header into a contract name is not (see Security).

A Per-User Theme

Some applications let the user pick the look. That is a different source for the same list: a session-scoped preference (see Scopes) feeding an f:view expression. The bean validates the choice against a closed list, so no request value ever reaches the contract name:

ThemePreference.java
package org.example.bookshelf.tenant;

import java.io.Serializable;
import java.util.List;
import jakarta.enterprise.context.SessionScoped;
import jakarta.inject.Named;

/** A per-user theme chosen from a closed list of contract names. */
@Named
@SessionScoped
public class ThemePreference implements Serializable {

    private static final List<String> THEMES = List.of("classic", "northside", "campus");

    private String theme = "classic";

    public String getTheme() {
        return theme;
    }

    public void setTheme(String theme) {
        if (THEMES.contains(theme)) {
            this.theme = theme;
        }
    }

    public List<String> getThemes() {
        return THEMES;
    }

    public void next() {
        theme = THEMES.get((THEMES.indexOf(theme) + 1) % THEMES.size());
    }
}
pref.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="jakarta.faces.html" xmlns:f="jakarta.faces.core" xmlns:ui="jakarta.faces.facelets">
<f:view contracts="#{themePreference.theme}">
<ui:composition template="/layout.xhtml">
    <ui:define name="title">Preference</ui:define>
    <ui:define name="content">
        <h:form id="f">
            <h:outputText id="cur" value="theme=#{themePreference.theme}"/>
            <h:commandButton id="next" value="Next theme" action="#{themePreference.next}"/>
            <h:commandButton id="ajax" value="Ajax next" action="#{themePreference.next}"><f:ajax render="cur"/></h:commandButton>
        </h:form>
    </ui:define>
</ui:composition>
</f:view>
</html>

A GET showed classic. Three consecutive POSTs of the first button (Next theme) moved the preference through northside, campus and back to classic, and a GET afterwards showed the current preference. The two implementations differ in what the POST response itself looks like:

  • Mojarra rendered the POST response with the new contract: the first POST returned the northside template.

  • MyFaces rendered the POST response with the contract in force when the view was restored: the first POST returned the classic template and the new text (theme=northside) inside it, and the new template appeared only on the next request.

The Ajax button (f:ajax render="cur") only re-rendered the h:outputText, as always, and never switched the surrounding template. Because of the difference, switch a theme with a redirect to the same view (see Flash and Post-Redirect-Get) so that the next GET renders consistently on both.

(The scratch client for MyFaces had to send the hidden f_SUBMIT field that MyFaces renders in each form, as in the earlier pages.)

Packaging Contracts as JARs

A contract in a JAR is how a theme becomes a reusable artefact: a separate Maven module that several applications (or several customers' builds) depend on. The files go under META-INF/contracts/<name>/ and each contract directory needs the marker file jakarta.faces.contract.xml. The specification’s own example shows a JAR with two contracts, each with a marker.

Layout of the bookshelf-themes module
bookshelf-themes/
  pom.xml
  src/main/resources/META-INF/contracts/
    seasonal/
      jakarta.faces.contract.xml     (empty file)
      layout.xhtml
      theme.css
      logo.svg
pom.xml of the themes module
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example.bookshelf</groupId>
    <artifactId>bookshelf-themes</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>
    <name>Bookshelf themes (resource library contracts)</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
</project>

No plugin configuration is needed: Maven copies src/main/resources into the JAR as is, an empty marker file included (mvn -o package produced META-INF/contracts/seasonal/jakarta.faces.contract.xml with length 0). The application then depends on the module (a normal jar dependency; the JAR ends up in WEB-INF/lib), and a view or a mapping names the contract:

<f:view contracts="seasonal">

What was observed with the JAR in WEB-INF/lib, identically on Mojarra and MyFaces:

  • f:view contracts="seasonal" rendered the JAR’s template, and the stylesheet URL carried ?con=seasonal.

  • A second contract in the same JAR without the marker file (nomarker) also worked through f:view. The marker file is required by the specification for discovery, and discovery is what the default list and the mappings rely on: a mapping of * to nomarker failed on Mojarra with Invalid path : /layout.xhtml and happened to work on MyFaces. Ship the marker for every contract; do not depend on either behaviour.

  • Replacing the JAR with another one that declares the same templates and insertion points changes the look without touching a view. This is the interchangeability the specification describes for its siteLayout.jar and newSiteLayout.jar example.

Security

Theming looks harmless, and it still touches templates, EL and URLs, so these points belong to the feature.

  • Keep the contracts directory out of reach. With contracts in the default contracts directory and the *.xhtml mapping, a browser can request them directly. GET /bookshelf/contracts/campus/layout.xhtml returned 200 with the template rendered on its own (the insertion points empty), and contracts/campus/theme.css and contracts/northside/logo.svg were served by the container as static files, on both implementations. With jakarta.faces.WEBAPP_CONTRACTS_DIRECTORY set to WEB-INF/contracts the same requests returned 404 and the contracts still worked. Contracts in a JAR in WEB-INF/lib are not part of the web root at all.

  • Contract resources are public. The con parameter lets any client download the static files of any contract it can name (see Resources Inside a Contract), whatever tenant it belongs to. Do not put anything in a contract that one tenant must not see: no customer-specific documents, no keys, no internal URLs. A tenant’s confidential files belong behind an authenticated endpoint (see File Upload and Download).

  • Never build a contract name from request input. In the tests a name containing .. segments was accepted by both implementations. With a page that used f:view contracts="#{param.theme}" (the anti-pattern), theme=northside rendered the Northside template, theme=campus/../northside rendered it as well, and theme=../templates rendered the file /WEB-INF/templates/layout.xhtml, the directory the earlier pages of this guide use for templates, because the name is resolved as a path below the contracts directory. A crafted value can therefore steer the template lookup to any directory that holds a file named like the template. Choose the name from a closed set, as ThemePreference does and as the tenant directory does (an unknown host gets the default contract), and never pass a request parameter, header or cookie through.

  • Templates run with full EL. A contract template can read any bean, so a theme JAR is code. Install only JARs from sources you build or trust, pin their versions, and review the templates of a third-party theme as you would any dependency.

  • Theming is not isolation. A different look says nothing about which data a user may see; the authorisation of the data is the job of the service and security layers (see the security page).

  • No secrets in configuration. Tenant tables, hostnames and theme options come from deployment configuration; no credentials or keys appear in faces-config.xml, web.xml or contract files.

Mojarra and MyFaces

Topic Mojarra 4.1.17 MyFaces 4.1.4

contracts element with a comma list

Accepted

Treated as one name and logged as not found; repeat the element

Leading slash in jakarta.faces.WEBAPP_CONTRACTS_DIRECTORY

Tolerated in the test

IllegalStateException: jakarta.faces.WEBAPP_CONTRACTS_DIRECTORY cannot start with '/ on each request

Mapping to a name without a marker file or a directory

Request fails with Invalid path : /layout.xhtml

The JAR contract without a marker worked; a missing name fails with Not Found in ExternalContext as a Resource

No mapping and the same template in a directory contract and a JAR contract

Directory contract chosen

JAR contract chosen

List mixing directory and JAR contracts

Template by list order, name-only resources from the directory contract first

JAR contract first for both

Postback that changes the contract

The POST response already uses the new contract

The POST response uses the old one, the next request the new one

Startup log

INFO Processing resource library contract mapping for url-pattern: ..., Added contract: ...

INFO Resource Library Contract ... was not found while scanning for available contracts. for unknown names

Missing template message

Invalid path : /layout.xhtml (with the ui:composition line and column)

/layout.xhtml Not Found in ExternalContext as a Resource

Everything else listed on this page behaved the same: the contracts directory and WEB-INF/contracts, mappings by exact path, longest prefix and *, f:view contracts (including names no mapping lists and names added at runtime), the ViewDeclarationLanguage decorator, ui:decorate and ui:include from a contract, the con parameter and the 404 answers. Contract templates are hot reloaded like any other template, subject to the refresh period of the implementation (see the page on configuration and project stages); in the tests an edit appeared within a few seconds, but not always on the first reload.

What Changed

Version Change

Faces 2.2

Resource library contracts are introduced: the contracts directory and META-INF/contracts, the marker file, jakarta.faces.WEBAPP_CONTRACTS_DIRECTORY, resource-library-contracts in faces-config.xml, the contracts attribute of f:view, FacesContext.get/setResourceLibraryContracts and ViewDeclarationLanguage.calculateResourceLibraryContracts. This is the version where the books you may be reading end.

Faces 3.0

The javax.faces.* packages become jakarta.faces.*: jakarta.faces.WEBAPP_CONTRACTS_DIRECTORY, jakarta.faces.contract.xml.

Faces 4.0

Facelets namespaces become jakarta.faces.html, jakarta.faces.core and jakarta.faces.facelets; faces-config.xml uses the Jakarta EE 10 schema. JSP view support is removed, so Facelets templates are the only kind of template a contract can hold.

Faces 4.1

I found no change specific to contracts in the 4.1 change log; the feature is stable. The schema version of faces-config.xml is 4.1 (web-facesconfig_4_1.xsd), as in the listings above.

Faces 5.0 is under development (the latest milestone, 5.0.0-M9, was published on 2026-09-18) and is not described here.

Where the Books Are Outdated

The JSF books consulted for this guide were written for JSF 1.2 and 2.0, before contracts existed (2.2), and therefore handle looks in other ways that are either gone or limited:

  • The Facelets books (Aranda and Wadia, and the recipes in Leonard’s) choose a template by a literal path in ui:composition, possibly with an expression in the template attribute. That selects one template file and nothing else: the stylesheets, scripts and images of the look still have to be switched by hand, and nothing says whether two templates are interchangeable. A contract bundles the template and its resources and defines the swap.

  • Burns and Schalk, and Mann, describe skinning through component libraries and render kits, and the render-kit chapters of Jacobi and Fallows switch the whole rendering at run time. Render kits are still part of Faces, but they change how components are encoded, not which page layout a view uses. The skin mechanisms of RichFaces, ICEfaces and Trinidad that those books pair with them are discontinued.

  • Çalışkan and Varaksin’s PrimeFaces theme switcher and custom themes are about the component library’s own themes (stylesheets for PrimeFaces components), which is a separate mechanism from contracts; the PrimeFaces themes section covers it. A contract can carry the application-specific part of a theme (layout, logos, overrides), and both can be used together.

  • All the books use javax.faces.*, the http://java.sun.com/jsf/facelets namespace and web.xml or faces-config.xml from the JSP era; the examples on this page use the jakarta.faces.* namespaces and the 4.1 schema.

Pitfalls

  • A contract is missing a declared template or insertion point. The view fails with HTTP 500 (Invalid path or Not Found in ExternalContext) when the template is missing, and a typo in a ui:define name is silently ignored (see the templating page). Keep the declaration of every contract of a family identical and test each one.

  • Two contracts declare the same names and nothing chooses between them. The result depends on the implementation. Always map, set f:view, or install a ViewDeclarationLanguage decorator.

  • Mappings and lists written in the non-portable form. Repeat <contracts> instead of using commas, and never start the contracts directory with a slash.

  • A mapping that matches nothing. If any mapping exists and none matches a view, the list is empty and the template cannot be found. Add a * mapping as the default.

  • f:view placed deep in the page. The VDL documentation defines it only on the outer-most file of the chain. It worked elsewhere in the tests; do not depend on that.

  • Contract names built from request input. See Security.

  • Mixed directory and JAR contracts in one list. See Several Contracts in a List.

  • Missing marker file in a JAR contract. Add jakarta.faces.contract.xml to every contract directory in the JAR.

  • Expecting a theme switch to show up in the POST response. It does on Mojarra and does not on MyFaces; redirect after the change.

  • Assuming a restart is not needed. The set of discovered contracts, used by the default list and by mappings, is built at startup; f:view can use a directory added later.