Monday, November 29, 2021

What's new in Faces 4.0?

Introduction

At 22 september 2022, Faces 4.0 has been released as part of Jakarta EE 10!

History

First a little bit of history as a refresher:

  • 31 Aug 2017: Java EE 8 was released by Oracle. This included JSF 2.3, then still named "JavaServer Faces". All changes could be found in the What's new in JSF 2.3? blogpost of Arjan Tijms, then also a committer of Mojarra, but now a project lead of among others Jakarta Faces, Jakarta Security and Jakarta Expression Language.
  • 12 Sep 2017: Oracle announced to transfer Java EE to the Eclipse Foundation.

    In the meanwhile: a lot of forth and back discussion about the Oracle-owned trademark on "Java"; ultimately the Eclipse Foundation had to rename "Java EE" to "Jakarta EE" all over place, including the source code and the documentation.

  • 10 Sep 2019: Jakarta EE 8 was released by the Eclipse Foundation. This is basically exactly the same as Java EE 8, only with a different brand/product name. JSF was basically renamed to "Jakarta Server Faces" and the version was still JSF 2.3. The spec and impl are still exactly the same as the one in Java EE 8.

    In the meanwhile: Oracle claimed rights on the "javax" root package name, so the Eclipse Foundation should also rename "javax.*" root package name to "jakarta.*" all over place.

  • 22 Nov 2020: Jakarta EE 9 was released by the Eclipse Foundation. This is basically exactly the same as Jakarta EE 8, only with a different root package name for all APIs. E.g. "javax.faces.*" has become "jakarta.faces.*", "javax.servlet.*" has become "jakarta.servlet.*", etcetera. The JSF version was bumped to 3.0. The spec and impl are still exactly the same as 2.3, it's really basically a find&replace of "javax" by "jakarta", except for those still provided by Java SE, such as JNDI (javax.naming.*) and JAXB (javax.xml.*).

    It was originally also planned to bump the minimum required Java SE version from 8 to 11, however in the end it turned out to be less than trivial to make the existing GlassFish server, still considered the "reference implementation", compatible with Java SE 11. Due to the time constraints and lack of GlassFish committers, Jakarta EE 9 was stuck at Java SE 8.

  • 25 May 2021: Jakarta EE 9.1 was released by the Eclipse Foundation. This is basically the Java SE 11 compatible variant of Jakarta EE 9. Also here the JSF version is still 3.0. It was on its own already compatible with Java SE 11.
  • 22 Sep 2022: Jakarta EE 10 has been released by the Eclipse Foundation. This is the first Jakarta EE release with real spec changes as compared to Java EE 8. Among others, "JSF" will be officially renamed to "Jakarta Faces" or just short "Faces", and the Faces version will be bumped further to 4.0. This will — finally — be the first release with real spec and impl changes since 2.3.

Overview

Here's an overview based on Faces 4.0 milestone of what's changed in Faces 4.0 as compared to Faces 3.0 which is in turn basically exactly the same as JSF 2.3 but then with "javax" renamed to "jakarta" all over place.

Removal of deprecated stuff

Changes to existing stuff

Addition of new stuff

Remove all JSP support

#1546: JSP support was for the first time deprecated in JSF 2.0 (Dec 2009) in favor of Facelets, because the JSP life cycle did not fit very well in the JSF life cycle. The core issue was that JSP writes template text to response as soon as it was encountered during the view build time, while it should be postponed to the render response phase, causing the developers to fiddle around with <f:verbatim> tags and/or forcing themselves an unnecessary "everything must be a JSF component" mindset. JSP support was during JSF 2.x and 3.x maintained as backwards compatibility for old JSF 1.x applications whose JSP pages need to be gradually migrated to Facelets.

With Faces 4.0, JSP support is now finally physically removed. This means that it is not anymore possible to use JSP as a view technology for Faces. It's thanks to the enormous pluggability of Faces theoretically still possible to maintain JSP support via an external library with custom ViewDeclarationLanguage, ViewHandler and StateManager implementations, but this is clearly not recommended.

Remove @ManagedBean and friends

#1547: During the annotation hype around the time of JSF 2.0 development (2009), the @ManagedBean and all associated scopes in javax.faces.bean.* package were added as an annotation based alternative to XML based configuration of <managed-bean> in faces-config.xml.

In hindsight, this was actually a mistake, because around the same time CDI was born and offered the same functionality via @Named and friends. See also Is @javax.faces.bean.ManagedBean Dead on Arrival? It was unfortunately too late to switch from @ManagedBean to @Named before the JSF 2.0 release, so the @ManagedBean sticked around for a bit of time, perhaps to the joy of Spring/Tomcat/Jetty based custom stack adepts. The @ManagedBean and friends were ultimately officially deprecated in JSF 2.3 in favor of CDI and are now completely removed.

Remove MethodBinding, ValueBinding and friends

#1548: During JSF 1.0 development around 2004, a bunch of things were missing in the existing standard EL (Expression Language) spec, such as auto-creation of objects (managed beans) and being able to call setter methods instead of only getter methods. So a kind of an extension to the existing EL spec was added in flavor of javax.faces.el.* package. With this extension, JSF pages can use #{...} instead of ${...} if managed beans should be auto-created and/or if bean properties should be set instead of get (via input components) and/or if JSP should be prevented from immediately evaluating those expressions (in case JSP is used instead of Facelets).

This functionality was ultimately unified with standard EL as part of JSP 2.1 during JSF 1.2 development around 2005. The classes in the javax.faces.el.* package were therefore since JSF 1.2 deprecated in favor of the new classes in the javax.el.* package. Among them are the known MethodBinding and ValueBinding classes, which more than often unexpectedly surface when you as Faces developer want to create a custom component. They are now finally removed.

Remove CURRENT_COMPONENT constants from UIComponent class

#1571: The UIComponent#getCurrentComponent() and UIComponent#getCurrentCompositeComponent() utility methods were introduced in JSF 2.0 and at the same moment as the UIComponent#CURRENT_COMPONENT and UIComponent#CURRENT_COMPOSITE_COMPONENT constants were introduced to obtain "the current component" in kind of alternative way as follows:

UIComponent currentComponent = (UIComponent) FacesContext.getCurrentInstance().getAttributes().get(UIComponent.CURRENT_COMPONENT);

In hindsight, this alternative way should never have been introduced as it not only caused confusion but was also less thread-safe. Hence these constants were deprecated right away in JSF 2.1 maintenance release shortly following JSF 2.0. Now they are removed.

Remove deprecated methods of StateManager class

#1578: The StateManager was polished a lot since JSF 1.0 and thereby left behind a trail of deprecated methods growing longer almost every new JSF version. These are now all removed as part of a major clean up for the fresh new Faces 4.0.

Remove entire ResourceResolver class

#1583: The ResourceResolver was inherited into JSF 2.0 as part of the original version of Facelets. It basically offered a way to obtain a reference to an URL to a physical Facelets file based on a given path, so that one could get e.g. "last modified" timestamp or even an input stream out of it. However, at the same moment, in JSF 2.0, the ResourceHandler class was introduced which is able of doing more or less the same thing, obtaining a reference to a "resource" based on a path, but it was not possible to obtain an URL from it. Still, there was a big overlap in the functionality and this caused confusion.

In JSF 2.2, the ResourceHandler#createViewResource() method was finally introduced for the very purpose of (indirectly) obtaining a referece to an URL to a physical Facelets file based on a given path, so the ResourceResolver could be deprecated. It's now finally removed.

Noted should be that all those above mentioned removals of deprecated stuff have together decreased the total JAR file size with nearly a half megabyte.

Rename "JSF" to "Faces" all over place

#1552: With the rename of the Java EE brand to Jakarta EE, "JavaServer Faces" was promptly renamed to "Jakarta Server Faces". It was originally intended to also drop the "Server" part from the name in order to simplify the name and to remove the need for an abbreviation (which people would only keep questioning the full form for), but that slipped through during the chaos. With Faces 4.0, we have now the opportunity to completely drop the "Server" part and the "JSF" abbreviation. The full name is now "Jakarta Faces" and the short name is just "Faces". The package root and the Maven coordinate and the base name of the JAR file were already "jakarta.faces", so that leaves basically only the variable and file names in public API open for the rename.

The following changes were done in the public API:

  • jsf.js JavaScript file was renamed to faces.js
  • window.jsf JavaScript global variable was renamed to window.faces
  • jsf/ClientSideSecretKey JNDI variable was renamed to faces/ClientSideSecretKey
  • jsf/FlashSecretKey JNDI variable was renamed to faces/FlashSecretKey
  • jsf/ProjectStage JNDI variable was renamed to faces/ProjectStage
  • xmlns:jsf default XML namespace prefix for passthrough elements was renamed to xmlns:faces
  • ResourceHandler.JSF_SCRIPT_LIBRARY_NAME constant was renamed to FACES_SCRIPT_LIBRARY_NAME
  • ResourceHandler.JSF_SCRIPT_RESOURCE_NAME constant was renamed to FACES_SCRIPT_RESOURCE_NAME
  • PreJsf2ExceptionHandlerFactory class was just completely removed as that never proved to be useful

Rename "http://xmlns.jcp.org/jsf/*" URL to "jakarta.faces.*" URN

#1553: The XML namespace URIs could of course not be left untouched during the big rename of "JSF" to "Faces" as they still contain the /jsf part. We however went a step further: the URL form of "http://xmlns.jcp.org/jsf/*" was also converted to the URN form of "jakarta.faces.*". This was primarily because they did not and will never represent real web addresses and people keep getting confused on why they resemble a web address and why they couldn't find anything over there and why there did not seem to be any XSD documents backing them. They were just URLs in first place because it was during the 90s the norm to use whole URLs as namespace URIs, as you can also see in good ol' JSP files with those <%@ taglib uri="http://java.sun.com/..." %> which actually also never existed as real web pages and never had something like XSD documents.

With this change, this eternal confusion and potential future renames should be stopped, hopefully for good. Similarly, the "http://xmlns.jcp.org/jsp/jstl/*" namespace URIs of Jakarta Tags (formerly known as JSTL) are in Jakarta Faces also converted to "jakarta.tags.*", see also jstl-api#144. Summarized, here are the new XML namespace URIs:

<html
    xmlns:faces="jakarta.faces"
    xmlns:ui="jakarta.faces.facelets"
    xmlns:f="jakarta.faces.core"
    xmlns:h="jakarta.faces.html"
    xmlns:pt="jakarta.faces.passthrough"
    xmlns:cc="jakarta.faces.composite"
    xmlns:yourCompositeComponents="jakarta.faces.composite/libraryname"
    xmlns:yourFacesComponents="jakarta.faces.component"
    xmlns:c="jakarta.tags.core"
    xmlns:fn="jakarta.tags.functions"
>
    ...
</html>

Noted should be that Faces 4.0 still recognizes previous XML namespaces for sake of backwards compatibility, even the original "http://java.sun.com/jsf/*" ones where applicable. You can find below an useful overview of package names and XML namespace URIs used across the time:

Release date JEE version Faces version Root Java package Root XML namespace
11 Nov 2003J2EE 1.4JSF 1.0-1.1javax.faces.*http://java.sun.com/jsf/*
11 May 2006Java EE 5JSF 1.2javax.faces.*http://java.sun.com/jsf/*
10 Dec 2009Java EE 6JSF 2.0-2.1javax.faces.*http://java.sun.com/jsf/*
28 May 2013Java EE 7JSF 2.2javax.faces.*http://xmlns.jcp.org/jsf/*
31 Aug 2017Java EE 8JSF 2.3javax.faces.*http://xmlns.jcp.org/jsf/*
10 Sep 2019Jakarta EE 8JSF 2.3javax.faces.*http://xmlns.jcp.org/jsf/*
22 Nov 2020Jakarta EE 9JSF 3.0jakarta.faces.*http://xmlns.jcp.org/jsf/*
22 Sep 2022Jakarta EE 10Faces 4.0jakarta.faces.*jakarta.faces.*

Needless to say is that people should find'n'replace older XML namespace URIs to the latest ones as soon as possible.

Make UIComponent#subscribeToEvent() more convenient

#1558: In JSF 2.x, if you wanted to subscribe a listener to a component event, then the following approach is actually not safe:

component.subscribeToEvent(PreRenderViewEvent.class, new YourListener());

This is because of the following 2 problems with UIComponent#getListenersForEventClass() which is being used under the covers:

  1. It can contain duplicates
  2. It can return null while trying to search for duplicates

So you had to rewrite it as follows:

Class<PreRenderViewEvent> event = PreRenderViewEvent.class;
ComponentSystemEventListener listener = new YourListener();
List<SystemEventListener> existingListeners = component.getListenersForEventClass(event);
if (existingListeners != null && !existingListeners.contains(listener)) {
    component.subscribeToEvent(event, listener);
}

In Faces 4.0, this clumsy approach is history thanks to new API changes demanding that:

  1. UIComponent#subscribeToEvent() must precheck if given listener is not already installed
  2. UIComponent#getListenersForEventClass() may never return null

So that you can write it as follows:

component.subscribeToEvent(PreRenderViewEvent.class, new YourListener());

Skip type attribute from <link> and <script> when doctype is HTML5

#1565: The default renderers of <h:outputStylesheet> and <h:outputScript> by default also wrote the type attribute as in:

<link rel="stylesheet" type="text/css" href="/jakarta.faces.resource/style.css.xhtml" />
<script type="text/javascript" src="/jakarta.faces.resource/script.js.xhtml"></script>

This was however unnecessary when a HTML5 doctype was used because those values are then already implied as the default values. A sane HTML5 validator will emit warnings on this. Since Faces 4.0, when a HTML5 doctype is used, the default renderers of <h:outputStylesheet> and <h:outputScript> will automatically skip the type attribute as in:

<link rel="stylesheet" href="/jakarta.faces.resource/style.css.xhtml" />
<script src="/jakarta.faces.resource/script.js.xhtml"></script>

Improve <f:ajax> behavior in composite components

#1567: Imagine the following composite <my:inputLocalDate>:

<cc:interface ...>
    ...
    <cc:clientBehavior name="change" default="true" targets="day month year" event="change" />
</cc:interface>
<cc:implementation>
    ...
    <h:selectOneMenu id="day" />
    <h:selectOneMenu id="month" />
    <h:selectOneMenu id="year" />
    ...
</cc:implementation>

When you nest a <f:ajax> as follows:

<my:inputLocalDate ...>
    <f:ajax execute="@this" />
</my:inputLocalDate>

And you change only one of them, then you'd intuitively expect that all the three dropdowns "day", "month" and "year" are processed (applied, converted and validated) during the Faces Ajax request, basically "the whole composite component". However, in reality only the dropdown which the end user interacted with is processed, and the other two were not processed at all. This does not match the expectation of execute="@this" and might give skewed behavior during any customized conversion and validation within the composite component's implementation.

This unintuitive behavior was addressed with the following addition to the API spec of the execute attribute:

When nested within a composite component, and the value contains or implies the keyword @this, then the keyword must be remapped to the targets attribute of the associated <composite:clientBehavior> declaration, if any, else if the value is not an absolute search expression, then it must be reinterpreted relative to the location of the <f:ajax> declaration.

Similiar change was done to the render attribute because <f:ajax render="@this"> also unintuitively only re-rendered the individual dropdown component being interacted instead of the whole composite component:

When nested within a composite component, and the value contains or implies the keyword @this, then the keyword must be remapped to the client ID of the associated <composite:implementation>, else if the value is not an absolute search expression, then it must be reinterpreted relative to the location of the <f:ajax> declaration.

As this was in hindsight considered a bug/oversight in the API spec, and the execute already worked like this in MyFaces, all the impl changes were in Mojarra also backported to 2.3.16 and 3.0.1.

Support custom cookie attributes such as SameSite in ExternalContext#addResponseCookie()

#1570: The SameSite attribute was proposed around 2016 and introduced in Google Chrome. Basically, it was yet another measure against CSRF and phishing attacks which kind of exploded around that time. Imagine the following use case:

  • User visits yourdomain.com and a new session cookie gets set
  • User navigates away from yourdomain.com to anotherdomain.com during the same browser session
  • User navigates back from anotherdomain.com to yourdomain.com and resumes the existing session cookie

When the SameSite attribute is set to Strict or Lax, then the user won't anymore be able to resume the existing session cookie. This is particularly inconvenient when your site needs to redirect to a 3rd party site which then redirects back to your site, more than often to perform a payment. The solution would be to set SameSite attribute to None on the cookie(s) relevant to the resume.

Imagine another use case:

  • User visits yourdomain.com and a new session cookie gets set
  • User navigates to a page containing an iframe pointing to anotherdomain.com during the same browser session
  • The iframe navigates back from anotherdomain.com to yourdomain.com and resumes the existing session cookie

When the SameSite attribute is set to Strict, then the user won't anymore be able to resume the existing session cookie. This is particularly inconvenient when the iframe needs to redirect to a 3rd party site which then redirects back to your site, more than often to perform a payment. The solution would be to set SameSite attribute to Lax or None on the cookie(s) relevant to the resume (or to simply switch to a modern and transparent API client running in the backend so that there is no need at all for an iframe ;) ).

Since February 2020, Google Chrome has bumped the default value of SameSite from None to Lax, causing a bit of panic in web development world. This is where all the trouble started with particularly Servlet based web applications, because the Servlet Cookie API did not offer any way to set a custom attribute. Adding a way to the Servlet API spec was yet more troublesome, because the SameSite attribute is to the day of today still in draft mode and still not included in the standard HTTP cookie spec. The Servlet API guys did not like their spec to be dependent on a moving target and hence the long-demanding Cookie#setSameSite() method (it was requested for the first time on May 2017) couldn't be introduced without potentially getting broken when the SameSite spec unexpectedly gets changed before being set in stone. Hence a new general method was proposed: Cookie#setAttribute(). This will allow you to set any kind of custom attribute on the cookie. It finally made it into Servlet 6.0 spec (also part of Jakarta EE 10).

Following this change, the existing ExternalContext#addResponseCookie() method, which already supported a Map argument representing the cookie attributes, has been changed to not anymore throw an IllegalArgumentException when an undocumented attribute was present. It will now be added via the new Cookie#setAttribute() method.

Map<String, Object> attributes = new HashMap<>();
attributes.put("maxAge", -1);
attributes.put("SameSite", "None");
externalContext.addResponseCookie(name, value, attributes);

New automatic extensionless mapping

#1508: A new context parameter has been added to enable automatic extensionless mapping. You can enable it as follows:

<context-param>
    <param-name>jakarta.faces.AUTOMATIC_EXTENSIONLESS_MAPPING</param-name>
    <param-value>true</param-value>
</context-param>

So with this, when you have e.g. a /foo.xhtml page, then the FacesServlet will automatically be registered to /foo as well and interpret it internally as /foo.xhtml.

New annotation @ClientWindowScoped

#1509: This new managed bean scope was introduced for the first time in DeltaSpike and it relies on the JSF 2.2 introduced ClientWindow API which is under the covers used by @FlowScoped. It's basically a less restrictive variant of the in JSF 2.2 introduced @FlowScoped. Where the @FlowScoped is only available in a specific set of views in an isolated subfolder, and only starts when a specific entry page is hit, and ends when the user navigates away from the subfolder. The @ClientWindowScoped doesn't do any of this. The @ClientWindowScoped basically starts when a new jfwid request parameter is generated and basically lives as long as this parameter is reused while navigating across the pages via Faces link components.

New attribute <h:inputFile multiple="...">

#1555: When this new attribute is set to true, then the client side file browse dialog will support selection of multiple files (not folders), and the model value must be of the type List<Part> instead of Part (note: Collection<Part> will also work). Here's an example:

<h:inputFile value="#{bean.files}" multiple="true" />
private List<Part> files; // +getter +setter

public void submit() {
    for (Part file : files) {
        String name = Paths.get(part.getSubmittedFileName()).getFileName().toString();
        long size = part.getSize();
        // ...
    }
}

While at it, also an improvement was made to completely skip empty files (files with an empty name and/or a size of 0) from the submitted values. This also affects single selection. So, effectively the required="true" validation will work better and you don't anymore need to doublecheck for empty files in the bean. All these changes were inspired by OmniFaces <o:inputFile>.

New attribute <h:inputFile accept="...">

#1556: This will basically allow you to set a comma separated string of mime types of files to filter in client side file browse dialog. Here are some examples:

<h:inputFile value="#{bean.anyImage}" accept="image/*" />
<h:inputFile value="#{bean.specificImage}" accept="image/jpeg,image/png,image/gif" />

This will basically force the browser to only show files of matching types.

This change was also inspired by OmniFaces <o:inputFile>, with the exception that there is no built-in server side validation to double-check whether the file extension of the actually submitted files indeed satisfy the accept attribute. The developer shall have to provide a custom validator for this which compares it against ExternalContext#getMimeType().

New method FacesContext#getLifecycle()

#1557: There has never been an API-provided way to programmatically obtain the currently used Lifecycle. The currently only possible way is unnecessarily convoluted:

FacesContext context = FacesContext.getCurrentInstance();
String lifecycleId = context.getExternalContext().getInitParameter(FacesServlet.LIFECYCLE_ID_ATTR);
if (lifecycleId == null) {
    lifecycleId = LifecycleFactory.DEFAULT_LIFECYCLE;
}
LifecycleFactory lifecycleFactory = (LifecycleFactory) FactoryFinder.getFactory(FactoryFinder.LIFECYCLE_FACTORY);
Lifecycle lifecycle = lifecycleFactory.getLifecycle(lifecycleId);

Faces 4.0 will finally add a new method FacesContext#getLifecycle(), making the life easier:

Lifecycle lifecycle = FacesContext.getCurrentInstance().getLifecycle();

This will be useful when you want to programmatically add or remove an application wide PhaseListener, because those can only be added and removed on the Lifecycle instance.

New tag <f:selectItemGroups>

#1559: There has never been a dedicated tag to declare <optgroup> elements in the view, basically representing a nested collection of <option> elements for a selection component. They had to be manually created in the bean as SelectItemGroup instances which is then provided to <f:selectItems>. This could end up in quite some boilerplate code in the model, with legacy SelectItem instances. Below is an example demonstrating the shortest possible approach to convert a nested collection as such:

private List<SelectItem> categorySelectItems;

@PostConstruct
public void init() {
    List<Category> categories = loadCategories();
    categorySelectItems = categories.stream().map(category -> {
        SelectItemGroup group = new SelectItemGroup(category.getName());
        group.setSelectItems(category.getProducts().stream()
            .map(product -> new SelectItem(product, product.getName()))
            .toArray(SelectItem[]::new));
        return group;
    }).collect(toList());
}

private List<Category> loadCategories() {
    List<Category> categories = new ArrayList<>();
    categories.add(new Category("Animals", new Product("Cat"), new Product("Dog"), new Product("Parrot")));
    categories.add(new Category("Cars", new Product("Alfa Romeo"), new Product("BMW"), new Product("Hyundai"), new Product("Toyota")));
    return categories;
}

public List<SelectItem> getCategorySelectItems() {
    return categorySelectItems;
}
<h:selectOneMenu>
    <f:selectItems value="#{bean.categorySelectItems}" />
</h:selectOneMenu>

With the new <f:selectItemGroups>, this grouping/nesting logic can be moved from the model to the view, hereby making the logic more declarative:

private List<Category> categories;

@PostConstruct
public void init() {
    categories = loadCategories();
}

private List<Category> loadCategories() {
    List<Category> categories = new ArrayList<>();
    categories.add(new Category("Animals", new Product("Cat"), new Product("Dog"), new Product("Parrot")));
    categories.add(new Category("Cars", new Product("Alfa Romeo"), new Product("BMW"), new Product("Hyundai"), new Product("Toyota")));
    return categories;
}

public List<Category> getCategories() {
    return categories;
}
<h:selectOneMenu>
    <f:selectItemGroups value="#{bean.categories}" var="category" itemLabel="#{category.name}">
        <f:selectItems value="#{category.products}" var="product" itemLabel="#{product.name}" />
    </f:selectItemGroups>
</h:selectOneMenu>

This new component is largely based off OmniFaces <o:selectItemGroups>.

While at it, opportunity was also taken to let SelectItemGroup constructor and setSelectItems() accept SelectItem... varargs as well as Collection<SelectItem> argument instead of only a fixed array SelectItem[].

New attribute <h:inputText type="...">

#1560: This was already since JSF 2.2 possible with help of "passthrough attribute" feature as in <h:inputText pt:type="email">, but it's more clean and robust if it was directly supported by the standard API.

<h:inputText type="email" />
<h:inputText type="search" />
<h:inputText type="tel" />
<h:inputText type="url" />
<h:inputText type="number" />
<h:inputText type="range" />
<h:inputText type="datetime-local" />
<h:inputText type="month" />
<h:inputText type="time" />
<h:inputText type="week" />
<h:inputText type="date" />
<h:inputText type="color" />

This may in some browsers trigger additional functionality during filling, such as the keyboard in mobile devices automatically switching to a number pad in case of tel and number, or showing a date/time picker in case of date/time-related types.

Additionaly, if the Faces project stage is set to Development, and the type is mistakenly set to one of the following ...

<h:inputText type="hidden" />
<h:inputText type="password" />
<h:inputText type="checkbox" />
<h:inputText type="radio" />
<h:inputText type="file" />
<h:inputText type="submit" />
<h:inputText type="image" />
<h:inputText type="reset" />
<h:inputText type="button" />

... for which thus already a dedicated Faces component exist, such as <h:inputHidden>, <h:inputSecret>, <h:selectOneRadio>, etc, then a development stage warning message like below will be shown:

<h:inputText type="file"> is discommended, you should instead use <h:inputFile>

New tag <f:selectItemGroup>

#1563: This actually popped up during working on the <f:selectItemGroups> ticket (#1559). It should be possible to declare a single <optgroup> the same way as it is possible to declare a single <option> via <f:selectItem>.

For example:

<h:selectOneMenu>
    <f:selectItemGroup itemLabel="Animals">
        <f:selectItems value="#{bean.animals}" var="animal" itemLabel="#{animal.name}" />
    </f:selectItemGroup>
    <f:selectItemGroup itemLabel="Cars">
        <f:selectItems value="#{bean.cars}" var="car" itemLabel="#{car.name}" />
    </f:selectItemGroup>
</h:selectOneMenu>

New method UIViewRoot#getDoctype()

#1568: This actually popped up during working on the "Skip type attribute from <link> and <script> when doctype is HTML5" ticket (#1565). There didn't appear to be a standard way to figure out the currently declared DOCTYPE of a Faces view. With this new method, the renderer behind <h:outputStylesheet> and <h:outputScript> can now finally easily figure out whether to skip the type attribute. Of course, this is also going to be useful for other DOCTYPE-dependent things the Faces component library implementors have in mind.

Here's an example how it could be used to sniff whether current doctype is a HTML5 one:

public static boolean isOutputHtml5Doctype(FacesContext context) {
    UIViewRoot viewRoot = context.getViewRoot();

    if (viewRoot == null) {
        return false; // Can happen when view build time hasn't started yet.
    }

    Doctype doctype = viewRoot.getDoctype();

    if (doctype == null) {
        return false; // Can happen when there is no doctype declared at all.
    }

    return "html".equalsIgnoreCase(doctype.getRootElement())
        && doctype.getPublic() == null
        && doctype.getSystem() == null;
}

New attribute <f:websocket onerror="...">

#1573: Sometimes the <f:websocket onclose="..."> is insufficient for the error reporting task. It will only be called when the websocket has given up reconnecting instead of during the 1st reconnect attempt. So there was no way to trigger immediate feedback that the websocket connection has gone lost. With the new onerror attribute this is now possible. Here's an example:

<f:websocket channel="push" onerror="errorListener" onclose="closeListener" />

With these JavaScript functions:

function errorListener(code, channel, event) {
    if (code == 1001) {
        // Server has returned an unexpected response code. E.g. 503, because it's shutting down.
    }
    else if (code == 1006) {
        // Server is not reachable anymore. I.e. it's not anymore listening on TCP/IP requests.
    }
    else {
        // Any other reason which is usually not -1, 1000 or 1008, as the onclose will be invoked instead.
    }

    // In any case, the websocket will attempt to reconnect.
    // Current function will be invoked again if that also fails.
    // Once the websocket gives up reconnecting, the onclose will finally be invoked.
}

function closeListener(code, channel, event) {
    if (code == -1) {
        // Websockets not supported by client at all (e.g. Internet Explorer 9 or older).
    }
    else if (code == 1000) {
        // Normal close (usually as result of expired session or expired view depending on scope="...").
    }
    else {
        // Abnormal close reason (as result of an error).
    }
}

This improvement is inspired by OmniFaces <o:socket>.

New layout="list" for <h:selectManyCheckbox> and <h:selectOneRadio>

#1574: The <h:selectManyCheckbox> and <h:selectOneRadio> were initially only capable of generating a HTML <table> element. This markup was long discommended for non-tabular data, already since the time of "Web 2.0" (around 2004, with Ajax and all). It should have been possible to let the standard components generate a <ul> instead, but this was for unclear reason not done for a long time. Faces developers had to grab a 3rd party component library or to create a custom renderer for the purpose. Since Faces 4.0 this is finally not anymore needed. The both components now support layout="list". The style can this way also much more cleanly be controlled with CSS than it was possible with <table>, because the <table> element has too much implied style which needed to be explicitly overridden. Also W3C WAI accessibility validators will be much more happy with an <ul> for non-tabular data.

Here's an example of the rendering of a <h:selectOneRadio layout="list"> with three items:

<ul>
    <li>
        <input id="id:0" type="radio" ... />
        <label for="id:0">...</label>
    </li>
    <li>
        <input id="id:1" type="radio" ... />
        <label for="id:1">...</label>
    </li>
    <li>
        <input id="id:2" type="radio" ... />
        <label for="id:2">...</label>
    </li>
</ul>

The <h:selectManyCheckbox layout="list"> renders basically the same, only with type="checkbox" instead of type="radio".

New API to programmatically create Facelets

#1581: There has always been a small group of developers demanding a 100% pure Java approach of creating Faces views so that there is no need at all for any physical Facelets file to start with (e.g. with a single "parent" component with explicit binding attribute and/or a <f:event type="postAddToView"> listener in turn triggering the programmatic population of the component tree).

From Faces 4.0 on, it will be possible to 100% programmatically declare a Facelets page as follows, with the new @View annotation on a class extending from Facelet. Here's an example extracted from the test case:

package com.example;

import static jakarta.faces.application.StateManager.IS_BUILDING_INITIAL_STATE;

import java.io.IOException;
import java.util.List;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.faces.annotation.View;
import jakarta.faces.component.UIComponent;
import jakarta.faces.component.UIOutput;
import jakarta.faces.component.html.HtmlBody;
import jakarta.faces.component.html.HtmlCommandButton;
import jakarta.faces.component.html.HtmlForm;
import jakarta.faces.component.html.HtmlOutputText;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.facelets.Facelet;

@View("/hello.xhtml")
@ApplicationScoped
public class Hello extends Facelet {

    @Override
    public void apply(FacesContext facesContext, UIComponent root) throws IOException {
        if (!facesContext.getAttributes().containsKey(IS_BUILDING_INITIAL_STATE)) {
            return;
        }

        ComponentBuilder components = new ComponentBuilder(facesContext);
        List<UIComponent> rootChildren = root.getChildren();

        UIOutput output = new UIOutput();
        output.setValue("<html xmlns=\"http://www.w3.org/1999/xhtml\">");
        rootChildren.add(output);

        HtmlBody body = components.create(HtmlBody.COMPONENT_TYPE);
        rootChildren.add(body);

        HtmlForm form = components.create(HtmlForm.COMPONENT_TYPE);
        form.setId("form");
        body.getChildren().add(form);

        HtmlOutputText message = components.create(HtmlOutputText.COMPONENT_TYPE);
        message.setId("message");
        form.getChildren().add(message);

        HtmlCommandButton actionButton = components.create(HtmlCommandButton.COMPONENT_TYPE);
        actionButton.setId("button");
        actionButton.addActionListener(e -> message.setValue("Hello, World"));
        actionButton.setValue("Do action");
        form.getChildren().add(actionButton);

        output = new UIOutput();
        output.setValue("</html>");
        rootChildren.add(output);
    }

    private static class ComponentBuilder {
        FacesContext facesContext;

        ComponentBuilder(FacesContext facesContext) {
            this.facesContext = facesContext;
        }

        @SuppressWarnings("unchecked")
        <T> T create(String componentType) {
            return (T) facesContext.getApplication().createComponent(facesContext, componentType, null);
        }
    }
}

Invoking http://localhost:8080/context/hello.xhtml will run this view and populate as instructed without the need for a physical Facelets file. The API is currently indeed very minimal. Convenience methods (e.g. "is it view build time or view render time?") and/or component tree builders could be added later on depending on the reception and demand.

New annotation literals for all @Qualifiers

#1582: CDI has several API methods taking an Annotation as argument representing the desired @Qualifier. However, just passing e.g. FacesConfig.class or ManagedProperty.class doesn't work on them. It needs to be a concrete AnnotationLiteral instance and this was missing all over Faces API, forcing developers to manually create them.

This has now been added all over place and the developers do not anymore need to manually create AnnotationLiteral instances. The general pattern is QualifierClass.Literal.INSTANCE, exactly the same as those in standard CDI API. Here's an usage example which obtains the current instance of bean having @FacesConfig qualifier:

Instance<Object> facesConfigInstance = CDI.current().select(FacesConfig.Literal.INSTANCE);

The Definitive Guide to Jakarta Faces

Having said that, the book The Definitive Guide to JSF in Java EE 8 has in the meanwhile also been updated to catch up all of above. Yes, I can now proudly announce that the latest draft is finished just this week and that a second edition has been released at March 2022 and that it's titled The Definitive Guide to Jakarta Faces in Jakarta EE 10.

10 comments:

Louis Collet said...

Excellent : clear and complete !

Paolo said...

🥇 as usual ... waiting for the book.

A little suggestion for the book: make an appendix for a complete JakartaEE 10 full profile app deployment on Tomcat 10.1.x

Clement Levallois said...

Thank you!

Unknown said...

Will the new JSF release be free of view getting rebuild on each ajax request?
This rebuild is very time consuming on pages with many components.

Sébastien said...

Awaesome post, very helpful! I have one question though: how do you migrate composites following this https://stackoverflow.com/tags/composite-component/info into the new namespace? is there anything particular to do?

BalusC said...

@sebastien: it'll be jakarta.faces.composite/libraryname, see also https://jakarta.ee/specifications/faces/4.0/jakarta-faces-4.0.html#a1545

jinicode said...

@BaluSc
Great Post, we too are migrating from myfaces 2.x to 4.x
but the issue is that all the external additional libraries (myfaces-shared-core ,myfaces-shared-impl,tomahawk20) used in our applications are still using javax.faces internally.
How can we fix this? as the myfaces 4.x libraries no more have javax.* packages as they are migrated to jakarta.* packages.

@mbrandl87 said...

Hi @BalusC,
first of all thank you very much for this indepth introduction. I think there's however a very little typo which caused me quite a bit of headache :D
I was looking up the new namespaces and it reads xmlns:yourCompositeComponent="jakarta.faces.component/yourCompositeComponent
whereas it schould be .composite instead of .component :)
Would you mind editing the post so that other novices like me don't fall into the same trap? :D

Best
Martin

cmwange said...

Hello BalusC, great article as always. Many thanks for the same.
Could you please provide an example use case of the first class support for views based on Java (#1581: New API to programmatically create Facelets), where the page is based on a template. So, essentially, it has ui:composition, ui:define etc templating tags?

Hr said...

For the programmatic Facelets creation approach, everything will be escaped and rendered as string, essentially showing the source. To rectify, can change T create(String componentType) {} to include: newComponent.getAttributes().put("escape", false);