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
- #1546: Remove all JSP support
- #1547: Remove @ManagedBean and friends
- #1548: Remove MethodBinding, ValueBinding and friends
- #1571: Remove CURRENT_COMPONENT constants from UIComponent class
- #1578: Remove deprecated methods of StateManager class
- #1583: Remove entire ResourceResolver class
Changes to existing stuff
- #1552: Rename "JSF" to "Faces" all over place
- #1553: Rename "http://xmlns.jcp.org/jsf/*" URL to "jakarta.faces.*" URN
- #1558: Make UIComponent#subscribeToEvent() more convenient
- #1565: Skip type attribute from <link> and <script> when doctype is HTML5
- #1567: Improve <f:ajax> behavior in composite components
- #1570: Support custom cookie attributes such as SameSite in ExternalContext#addResponseCookie()
Addition of new stuff
- #1508: New automatic extensionless mapping
- #1509: New annotation @ClientWindowScoped
- #1555: New attribute <h:inputFile multiple="...">
- #1556: New attribute <h:inputFile accept="...">
- #1557: New method FacesContext#getLifecycle()
- #1559: New tag <f:selectItemGroups>
- #1560: New attribute <h:inputText type="...">
- #1563: New tag <f:selectItemGroup>
- #1568: New method UIViewRoot#getDoctype()
- #1573: New attribute <f:websocket onerror="...">
- #1574: New layout="list" for <h:selectManyCheckbox> and <h:selectOneRadio>
- #1581: New API to programmatically create Facelets
- #1582: New annotation literals for all @Qualifiers
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 "
The following changes were done in the public API:
jsf.js
JavaScript file was renamed tofaces.js
window.jsf
JavaScript global variable was renamed towindow.faces
jsf/ClientSideSecretKey
JNDI variable was renamed tofaces/ClientSideSecretKey
jsf/FlashSecretKey
JNDI variable was renamed tofaces/FlashSecretKey
jsf/ProjectStage
JNDI variable was renamed tofaces/ProjectStage
xmlns:jsf
default XML namespace prefix for passthrough elements was renamed toxmlns:faces
ResourceHandler.JSF_SCRIPT_LIBRARY_NAME
constant was renamed toFACES_SCRIPT_LIBRARY_NAME
ResourceHandler.JSF_SCRIPT_RESOURCE_NAME
constant was renamed toFACES_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 2003 | J2EE 1.4 | JSF 1.0-1.1 | javax.faces.* | http://java.sun.com/jsf/* |
11 May 2006 | Java EE 5 | JSF 1.2 | javax.faces.* | http://java.sun.com/jsf/* |
10 Dec 2009 | Java EE 6 | JSF 2.0-2.1 | javax.faces.* | http://java.sun.com/jsf/* |
28 May 2013 | Java EE 7 | JSF 2.2 | javax.faces.* | http://xmlns.jcp.org/jsf/* |
31 Aug 2017 | Java EE 8 | JSF 2.3 | javax.faces.* | http://xmlns.jcp.org/jsf/* |
10 Sep 2019 | Jakarta EE 8 | JSF 2.3 | javax.faces.* | http://xmlns.jcp.org/jsf/* |
22 Nov 2020 | Jakarta EE 9 | JSF 3.0 | jakarta.faces.* | http://xmlns.jcp.org/jsf/* |
22 Sep 2022 | Jakarta EE 10 | Faces 4.0 | jakarta.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:
- It can contain duplicates
- 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:
UIComponent#subscribeToEvent()
must precheck if given listener is not already installedUIComponent#getListenersForEventClass()
may never returnnull
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 thetargets
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.