Showing posts with label EL. Show all posts
Showing posts with label EL. Show all posts

Friday, November 13, 2020

Using Java 14 Records in JSF via Eclipse

Introduction

Java 14 introduced the record type. It's basically some sort of an immutable JavaBean without the need to write/generate all these accessor/equals/hashCode/toString methods.

Summarized, the following record in its full form:

public record Person(
    Long id,
    String email,
    LocalDate dateOfBirth
) {}

Corresponds less or more to the following class:

public class Person {
    private final Long id;
    private final String email;
    private final LocalDate dateOfBirth;
    
    public Person(
        Long id,
        String email,
        LocalDate dateOfBirth
    ) {
        this.id = id;
        this.email = email;
        this.dateOfBirth = dateOfBirth;
    }
    
    public Long id() {
        return id;
    }
    
    public String email() {
        return email;
    }
    
    public LocalDate dateOfBirth() {
        return dateOfBirth;
    }
    
    @Override
    public boolean equals(Object other) {
        return other == this || other instanceof Person
            && Objects.equals(id, ((Person) other).id)
            && Objects.equals(email, ((Person) other).email)
            && Objects.equals(dateOfBirth, ((Person) other).dateOfBirth);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(
            id,
            email,
            dateOfBirth
        );
    }
    
    @Override
    public String toString() {
        return getClass().getSimpleName() + "["
            + "id=" + id + ", "
            + "email=" + email + ", "
            + "dateOfBirth=" + dateOfBirth
        + "]";
    }
}

But then without all the repeated boilerplate :)

It almost matches the JavaBeans Spec, with the exception that the method names of the accessor methods are not prefixed with get/is.

Preparing Eclipse for Java 14 Records

Being able to develop Java 14 Records in Eclipse requires a minimum of Eclipse 2020-06, because Java 14 support was only introduced in that version for the first time. In Eclipse's preferences, you need to ensure that Java ➤ Compiler ➤ Compiler compliance level is set to at least 14.

If you're using Maven, then you need to ensure that the pom.xml properly instructs Maven to use Java 14 as well. Assuming that you've a Maven project which is created the same way as instructed in JSF 2.3 tutorial with Eclipse, Maven, WildFly and H2, then you just need to adjust the maven.compiler.* properties as below:

<properties>
    ...
    <maven.compiler.source>14</maven.compiler.source>
    <maven.compiler.target>14</maven.compiler.target>
    ...
</properties>

And then right-click the project in Project Explorer view and choose Maven ➤ Update Project. It will automatically sort out the Java version set in Project Facets based on the above setting in pom.xml.

However, this is not sufficient. If you attempt to create a record via the New Java Record wizard as available by right-clicking the project and choosing New ➤ Record, and fill out the fields, then Eclipse will error out as:

Project 'projectname' does not have preview features enabled.

Or when you manually create a class representing a record without using the wizard, then Eclipse will still error out as:

Type record is a preview feature and disabled by default. Use --enable-preview to enable

Indeed, the Java 14 Records feature is only available as a so-called "preview feature". This means that it's by default disabled. It's only available when you explicitly pass --enable-preview argument to the java/javac command.

This can be done in Eclipse via Java Compiler settings in project-specific properties or in window-global properties. The one in project-specific properties will be overridden every time you run Maven Update. It's better to configure it in window-global properties. Open Window ➤ Preferences ➤ Java ➤ Compiler. In the Compiler section, uncheck the Use default compliance settings option and then check the Enable preview features for Java 14 option.

Apply and close.

Creating and using Java Records in JSF

Once having configured the Eclipse project to use Java Records as instructed in previous chapter, then creating a new record will not anymore error out in Eclipse.

public record Person(
    Long id,
    String email,
    LocalDate dateOfBirth
) {}

It will only still show a warning like below:

You are using a preview language feature that may or may not be supported in a future release

It basically boils down that Java Records in its current form is not guaranteed to be backwards compatible in a newer Java version. For example, in a future Java version, the accessor methods might be adjusted to follow JavaBeans Spec to have the get or is method name prefix. If this is going to happen, then any calls to person.id() in existing code will not compile anymore and should be adjusted to person.getId(). Or perhaps the feature will even be completely removed. This potential tech debt has just to be carefully taken into account.

This is okay for now.

Now, let's create an example JSF bean which uses these records:

@Named
@RequestScoped
public class Bean {

    private List<Person> persons;
	
    @PostConstruct
    public void init() { 
        persons = new ArrayList<>();
        persons.add(new Person(1L, "john.doe@example.com", LocalDate.of(1978, 3, 26)));
        persons.add(new Person(2L, "jane.doe@example.com", LocalDate.of(1980, 10, 31)));
        persons.add(new Person(3L, "joe.bloggs@example.com", LocalDate.of(2002, 10, 5)));
    }
	
    public List<Person> getPersons() {
        return persons;
    }
}

And a XHTML file which just iterates over this list and prints each item plain:

<ui:repeat value="#{bean.persons}" var="person">
    #{person}
    <br/>
</ui:repeat>

After deploying it to a Jakarta EE server such as WildFly 21.0.0, you will face the following warning in server logs and the #{bean} will not be available in EL (newlines introduced for readability):

WARN [org.jboss.modules.define] (Weld Thread Pool -- 1)
    Failed to define class com.example.Bean in Module "deployment.playground-wildfly-0.0.1-SNAPSHOT.war" from Service Module Loader:
    java.lang.UnsupportedClassVersionError:
        Failed to link com/example/Bean (Module "deployment.playground-wildfly-0.0.1-SNAPSHOT.war" from Service Module Loader): 
        Preview features are not enabled for com/example/Bean (class file version 58.65535). 
        Try running with '--enable-preview'
    at java.base/java.lang.ClassLoader.defineClass1(Native Method)
    at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1017)
    at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1096)
    at org.jboss.modules.ModuleClassLoader.doDefineOrLoadClass(ModuleClassLoader.java:424)
    at org.jboss.modules.ModuleClassLoader.defineClass(ModuleClassLoader.java:555)
    at org.jboss.modules.ModuleClassLoader.loadClassLocal(ModuleClassLoader.java:339)
    at org.jboss.modules.ModuleClassLoader$1.loadClassLocal(ModuleClassLoader.java:126)
    at org.jboss.modules.Module.loadModuleClass(Module.java:731)
    at org.jboss.modules.ModuleClassLoader.findClass(ModuleClassLoader.java:247)
    at org.jboss.modules.ConcurrentClassLoader.performLoadClassUnchecked(ConcurrentClassLoader.java:410)
    at org.jboss.modules.ConcurrentClassLoader.performLoadClass(ConcurrentClassLoader.java:398)
    at org.jboss.modules.ConcurrentClassLoader.loadClass(ConcurrentClassLoader.java:116)
    at org.jboss.as.weld@21.0.0.Final//org.jboss.as.weld.WeldModuleResourceLoader.classForName(WeldModuleResourceLoader.java:68)
    at org.jboss.weld.core@3.1.5.Final//org.jboss.weld.bootstrap.AnnotatedTypeLoader.loadClass(AnnotatedTypeLoader.java:82)
    at org.jboss.weld.core@3.1.5.Final//org.jboss.weld.bootstrap.FastAnnotatedTypeLoader.createContext(FastAnnotatedTypeLoader.java:114)
    at org.jboss.weld.core@3.1.5.Final//org.jboss.weld.bootstrap.FastAnnotatedTypeLoader.loadAnnotatedType(FastAnnotatedTypeLoader.java:103)
    at org.jboss.weld.core@3.1.5.Final//org.jboss.weld.bootstrap.BeanDeployer.addClass(BeanDeployer.java:87)
    at org.jboss.weld.core@3.1.5.Final//org.jboss.weld.bootstrap.ConcurrentBeanDeployer$1.doWork(ConcurrentBeanDeployer.java:55)
    at org.jboss.weld.core@3.1.5.Final//org.jboss.weld.bootstrap.ConcurrentBeanDeployer$1.doWork(ConcurrentBeanDeployer.java:52)
    at org.jboss.weld.core@3.1.5.Final//org.jboss.weld.executor.IterativeWorkerTaskFactory$1.call(IterativeWorkerTaskFactory.java:62)
    at org.jboss.weld.core@3.1.5.Final//org.jboss.weld.executor.IterativeWorkerTaskFactory$1.call(IterativeWorkerTaskFactory.java:55)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)
    at java.base/java.lang.Thread.run(Thread.java:832)
    at org.jboss.threads@2.4.0.Final//org.jboss.threads.JBossThread.run(JBossThread.java:513)

Note specifically the hint Try running with '--enable-preview'. That's indeed what we need to do.

Doubleclick the WildFly server in Servers view and then click the Open launch configuration link. In the VM arguments field, add the new VM argument --enable-preview as highlighted in the below screenshot:

Restart the server. The warning in server logs is now gone. Opening the example XHTML file will produce this result:

Accessing Java Record "properties" in EL

Trouble will start when you adjust the XHTML to access the "properties" of the Java Record in EL as if they were JavaBean properties:

<h:dataTable value="#{bean.persons}" var="person">
    <h:column>#{person.id}</h:column>
    <h:column>#{person.email}</h:column>
    <h:column>#{person.dateOfBirth}</h:column>
</h:dataTable>

It will throw an exception with the following root cause:

javax.el.ELException: /test.xhtml: The class 'com.example.Person' does not have the property 'id'.
    at com.sun.jsf-impl@2.3.14.SP01//com.sun.faces.facelets.compiler.TextInstruction.write(TextInstruction.java:47)
    at com.sun.jsf-impl@2.3.14.SP01//com.sun.faces.facelets.compiler.UIInstructions.encodeBegin(UIInstructions.java:41)
    at com.sun.jsf-impl@2.3.14.SP01//com.sun.faces.renderkit.html_basic.HtmlBasicRenderer.encodeRecursive(HtmlBasicRenderer.java:276)
    at com.sun.jsf-impl@2.3.14.SP01//com.sun.faces.renderkit.html_basic.TableRenderer.renderRow(TableRenderer.java:374)
    at com.sun.jsf-impl@2.3.14.SP01//com.sun.faces.renderkit.html_basic.TableRenderer.encodeChildren(TableRenderer.java:137)
    at javax.faces.api@3.0.0.SP04//javax.faces.component.UIComponentBase.encodeChildren(UIComponentBase.java:566)
    at javax.faces.api@3.0.0.SP04//javax.faces.component.UIComponent.encodeAll(UIComponent.java:1647)
    at javax.faces.api@3.0.0.SP04//javax.faces.component.UIComponent.encodeAll(UIComponent.java:1650)
    at javax.faces.api@3.0.0.SP04//javax.faces.component.UIComponent.encodeAll(UIComponent.java:1650)
    at com.sun.jsf-impl@2.3.14.SP01//com.sun.faces.application.view.FaceletViewHandlingStrategy.renderView(FaceletViewHandlingStrategy.java:468)
    at com.sun.jsf-impl@2.3.14.SP01//com.sun.faces.application.view.MultiViewHandler.renderView(MultiViewHandler.java:170)
    at javax.faces.api@3.0.0.SP04//javax.faces.application.ViewHandlerWrapper.renderView(ViewHandlerWrapper.java:132)
    at javax.faces.api@3.0.0.SP04//javax.faces.application.ViewHandlerWrapper.renderView(ViewHandlerWrapper.java:132)
    at com.sun.jsf-impl@2.3.14.SP01//com.sun.faces.lifecycle.RenderResponsePhase.execute(RenderResponsePhase.java:102)
    at com.sun.jsf-impl@2.3.14.SP01//com.sun.faces.lifecycle.Phase.doPhase(Phase.java:76)
    at com.sun.jsf-impl@2.3.14.SP01//com.sun.faces.lifecycle.LifecycleImpl.render(LifecycleImpl.java:199)
    at javax.faces.api@3.0.0.SP04//javax.faces.webapp.FacesServlet.executeLifecyle(FacesServlet.java:708)
    ... 51 more

What is happening here? Well, EL only understands JavaBeans Spec and the Java Records do not exactly comply the JavaBeans Spec. EL is basically trying to find the getId() method on the record, but there's only the id() method. Hence EL thinks it does not exist. Moreover, in case of Java Records we shouldn't really talk about "properties", but about "fields".

Adjusting EL expressions from the #{bean.property} syntax to the #{record.accessor()} syntax as below:

<h:dataTable value="#{bean.persons}" var="person">
    <h:column>#{person.id()}</h:column>
    <h:column>#{person.email()}</h:column>
    <h:column>#{person.dateOfBirth()}</h:column>
</h:dataTable>

Will make it work in some EL implementations.

It worked for me in WildFly 21, but it has been reported to not work in other EL implementations.

Resolving Java Records as JavaBeans in EL

In case you want to be able to keep using the #{bean.property} syntax to access fields of Java Records, then you can always create a custom ELResolver which detects Java Records and resolves the EL expressions accordingly. This will be very useful when you intend to share some XHTML templates among beans and records with the same properties.

Detecting whether a given class is a Java Record can be done by checking Class#isRecord(). Obtaining all the record components associated with the record class can be done by Class#getRecordComponents(). Given this information, the following custom ELResolver which basically converts record fields to bean properties should do it:

public class RecordELResolver extends ELResolver {

    private static final Map<Class<?>, Map<String, PropertyDescriptor>> RECORD_PROPERTY_DESCRIPTOR_CACHE = new ConcurrentHashMap<>();

    private static boolean isRecord(Object base) {
        return base != null && base.getClass().isRecord();
    }
    
    private static Map<String, PropertyDescriptor> getRecordPropertyDescriptors(Object base) {
        return RECORD_PROPERTY_DESCRIPTOR_CACHE
            .computeIfAbsent(base.getClass(), clazz -> Arrays
                .stream(clazz.getRecordComponents())
                .collect(Collectors
                    .toMap(RecordComponent::getName, recordComponent -> {
                        try {
                            return new PropertyDescriptor(recordComponent.getName(), recordComponent.getAccessor(), null);
                        }
                        catch (IntrospectionException e) {
                            throw new IllegalStateException(e);
                        }
                    })));
    }
    
    private static PropertyDescriptor getRecordPropertyDescriptor(Object base, Object property) {
        PropertyDescriptor descriptor = getRecordPropertyDescriptors(base).get(property);
        
        if (descriptor == null) {
            throw new PropertyNotFoundException("The record '" + base.getClass().getName() + "' does not have the field '" + property + "'.");
        }

        return descriptor;
    }

    @Override
    public Object getValue(ELContext context, Object base, Object property) {
        if (!isRecord(base) || property == null) {
            return null;
        }

        PropertyDescriptor descriptor = getRecordPropertyDescriptor(base, property);

        try {
            Object value = descriptor.getReadMethod().invoke(base);
            context.setPropertyResolved(base, property);
            return value;
        }
        catch (Exception e) {
            throw new ELException(e);
        }
    }

    @Override
    public Class<?> getType(ELContext context, Object base, Object property) {
        if (!isRecord(base) || property == null) {
            return null;
        }

        PropertyDescriptor descriptor = getRecordPropertyDescriptor(base, property);
        context.setPropertyResolved(true);
        return descriptor.getPropertyType();
    }

    @Override
    public Class<?> getCommonPropertyType(ELContext context, Object base) {
        if (!isRecord(base)) {
            return null;
        }

        return String.class;
    }

    @Override
    public boolean isReadOnly(ELContext context, Object base, Object property) {
        if (!isRecord(base)) {
            return false;
        }

        getRecordPropertyDescriptor(base, property); // Forces PropertyNotFoundException if necessary.
        context.setPropertyResolved(true);
        return true; // Java Records are per definition immutable.
    }

    @Override
    public void setValue(ELContext context, Object base, Object property, Object value) {
        if (!isRecord(base)) {
            return;
        }

        getRecordPropertyDescriptor(base, property); // Forces PropertyNotFoundException if necessary.
        throw new PropertyNotWritableException("Java Records are immutable");
    }

    @Override
    public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object base) {
        if (!isRecord(base)) {
            return null;
        }

        Map rawDescriptors = getRecordPropertyDescriptors(base);
        return rawDescriptors.values().iterator();
    }
}

In order to get it to run, register as below in faces-config.xml:

<application>
    <el-resolver>com.example.RecordELResolver</el-resolver>
</application>

Now you can successfully access record fields in EL as if they were bean properties:

<h:dataTable value="#{bean.persons}" var="person">
    <h:column>#{person.id}</h:column>
    <h:column>#{person.email}</h:column>
    <h:column>#{person.dateOfBirth}</h:column>
</h:dataTable>

Using Java Records as JPA Entities

Unfortunately, due to the immutable nature of Java Records, they are not at all useful as JPA entities. But they are definitely useful as DTOs. This has been blogged by Thorben Janssen and Vlad Mihalcea before.

Imagine that you've an entity with "too many" properties wihch would only unnecessarily select every single column when using SELECT p FROM Person p in JPA:

@Entity
public class Person {

    @Id
    private Long id;
    
    @Column
    private String email;
    
    @Column
    private boolean emailVerified;
    
    @Column
    private String firstName;
    
    @Column
    private String lastName;
    
    @Column
    private LocalDate dateOfBirth;
    
    @Column
    private String phoneNumber;
    
    @Enumerated(STRING)
    private Status status;

    @OneToOne(fetch = LAZY)
    private Image profileImage;
    
    @ManyToOne(fetch = LAZY)
    private Address homeAddress;

    @OneToMany(mappedBy = "person", fetch = LAZY)
    private Set<LoginToken> loginTokens;

    @ElementCollection(fetch = EAGER) @Enumerated(STRING)
    private Set<Group> groups;

    // etc ...
}

Then you can use a Java Record as DTO to select only a specific subset of columns, such as ID, first name, last name and profile image in order to present it in some list of cards:

public record PersonCard(
    Long id,
    String firstName,
    String lastName,
    Image profileImage
) {}

Then you can use the constructor expression in JPQL query as demonstrated in the listCards() method below:

@Stateless
public class PersonService {

    @TransactionAttribute(NOT_SUPPORTED)
    public List<Person> listAll() {
        return entityManager.createQuery(
            """
                SELECT 
                    p
                FROM
                    Person p
            """
            , Person.class)
            .getResultList();
    }

    @TransactionAttribute(NOT_SUPPORTED)
    public List<PersonCard> listCards() {
        return entityManager.createQuery(
            """
                SELECT 
                    new com.example.PersonCard(
                        p.id,
                        p.firstName,
                        p.lastName,
                        pi
                    )
                FROM
                    Person p
                        LEFT JOIN FETCH
                    p.profileImage pi
            """
            , PersonCard.class)
            .getResultList();
    }
}

Notice the Text Blocks syntax """ ... """, which is yet another preview feature, but then already introduced in Java 13. This saves you from fiddling with string-concatenating the newlines in JPQL queries which would only produce barely manageable JPQL queries in Java source code.

This shall work just fine in JSF. For example:

@Named
@RequestScoped
public class Bean {

    private List<PersonCard> personCards;

    @Inject
    private PersonService personService;

    @PostConstruct
    public void init() { 
        personCards = personService.listCards();
    }
	
    public List<PersonCard> getPersonCards() {
        return personCards;
    }
}
<ul>
    <ui:repeat value="#{bean.personCards}" var="personCard">
        <li>
            <a href="person.xhtml?id=#{personCard.id}">
                <img src="#{personCard.profileImage.url}" />
                #{personCard.firstName} #{personCard.lastName}
            </a>
        </li>
    </ui:repeat>
</ul>

That's all, folks :) Happy coding!

Friday, October 30, 2015

The empty String madness

Introduction

When we submit a HTML form with empty input fields which are bound to non-primitive bean properties, we'd rather like to keep them null instead of being polluted with empty strings or zeroes. This is very significant as to validation constraints such as @NotNull in Bean Validation and NOT NULL in relational databases. Across years and JSF/EL versions this turned out to be troublesome as not anyone agreed on each other. I sometimes even got momentarily confused myself when it would work and when not. I can imagine that a lot of other JSF developers have the same feeling. So let's do some digging in history and list all the facts and milestones in one place for best overview, along with an useful summary table with the correct solutions.

JSF 1.0/1.1 (2004-2006)

Due to the nature of HTTP, empty input fields arrive as empty strings instead of null. The underlying servlet request.getParameter(name) call returns an empty string on empty input fields. Nothing to do against, that's just how HTTP and Servlets work. A value of null represents the complete absence of the request parameter, which is also very significant (e.g. the servlet could this way check if a certain form button is pressed or not, irrespective of its value/label which could be i18n'ed). So we can't fix this in HTTP/Servlet side and have to do it in MVC framework's side. To avoid the model being polluted with empty strings, you would in JSF 1.0/1.1 need to create a custom Converter like below which you explicitly register on the inputs tied to java.lang.String typed model value.

public class EmptyToNullStringConverter implements Converter {

    @Override
    public Object getAsObject(FacesContext facesContext, UIComponent component, String submittedValue) {
        if (submittedValue == null || submittedValue.isEmpty()) {
            if (component instanceof EditableValueHolder) {
                ((EditableValueHolder) component).setSubmittedValue(null);
            }

            return null;
        }

        return submittedValue;
    }

    @Override
    public String getAsString(FacesContext facesContext, UIComponent component, Object modelValue) {
        return (modelValue == null) ? "" : modelValue.toString();
    }

}

Which is registered in faces-config.xml as below:

<converter>
    <converter-id>emptyToNull</converter-id>
    <converter-class>com.example.EmptyToNullStringConverter</converter-class>
</converter>

And used as below:

<h:inputText value="#{bean.string1}" converter="emptyToNull" />
<h:inputText value="#{bean.string2}" converter="emptyToNull" />
<h:inputText value="#{bean.string3}" converter="emptyToNull" />

The converter-for-class was not supported on java.lang.String until JSF 1.2.

The non-primitive numbers wasn't a problem in JSF 1.x, but only in specific server/EL versions. See later.

JSF 1.2 (2006-2009)

Since JSF 1.2, the converter-for-class finally supports java.lang.String (see also spec issue 131). So you can simply register the above converter as below and it'll get automatically applied on all inputs tied to java.lang.String typed model value.

<converter>
    <converter-for-class>java.lang.String</converter-for-class>
    <converter-class>com.example.EmptyToNullStringConverter</converter-class>
</converter>
<h:inputText value="#{bean.string1}" />
<h:inputText value="#{bean.string2}" />
<h:inputText value="#{bean.string3}" />

Tomcat 6.0.16 - 7.0.x (2007-2009)

Someone reported Tomcat issue 42385 wherein EL failed to set an empty String value representing an integer into a primitive int bean property. This uncovered a long time RI bug which violated section 1.18.3 of EL 2.1 specification.

1.18.3 Coerce A to Number type N

  • If A is null or "", return 0.
  • ...

In other words, when the model type is a number, and the submitted value is an empty string or null, then EL should coerce all integer based numbers int, long, Integer, Long and BigInteger to 0 (zero) before setting the model value. The same applies to decimal based numbers float, double, Float, Double and BigDecimal, which will then be coerced to 0.0. This was not done rightly in Oracle (Sun) nor in Apache EL implementations at the date. They both just set null in the number/decimal typed model value and only Apache EL failed on primitives whereas Oracle EL properly set the default value of zero (and hence that Tomcat issue report).

Since Tomcat 6.0.16, Apache EL started to set all number/decimal typed model values with 0 and 0.0 respectively. That's okay for primitive types like int, long, float and double, but that's absolutely not okay for non-primitive types like Integer, Long, Float, Double, BigInteger and BigDecimal. They should stay null when the submitted value is empty or null. The same applies to Boolean fields which got a default value of false and Character fields which got a default value of \u0000.

So I created JSP spec issue 184 for that (EL was then still part of JSP). This coercion doesn't make sense for non-primitives. The issue got a lot of recognition and votes. After complaints from JSF users, since Tomcat 6.0.17 a new VM argument was added to disable this Apache EL behavior on non-primitive number/decimal types.

-Dorg.apache.el.parser.COERCE_TO_ZERO=false

It became the most famous Tomcat-specific setting among JSF developers. It even worked in JBoss and all other servers using Apache EL parser (WebSphere a.o). It could even be set programmatically with help of a ServletContextListener.

@WebListener
public class Config implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent event) {
        System.setProperty("org.apache.el.parser.COERCE_TO_ZERO", "false");
    }

    @Override
    public void contextDestroyed(ServletContextEvent event) {
        // NOOP.
    }

}

JSF 2.x (2009-current)

To reduce the EmptyToNullStringConverter boilerplate, JSF 2.0 introduced a new context param with a rather long name which should achieve exactly the desired behavior of interpreting empty string submitted values as null.

<context-param>
    <param-name>javax.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL</param-name>
    <param-value>true</param-value>
</context-param>

To avoid non-primitive number/decimal typed model values being set with zeroes, on Tomcat and clones you still need the VM argument for the Apache EL parser as explained in the previous section. See also a.o. the Communication in JSF 2.0 article here.

EL 3.0 (2013-current)

And then EL 3.0 was introduced as part of Java EE 7 (which also covers JSF 2.2). With this version, the aforementioned JSP spec issue 184 was finally fixed. EL specification does no longer require to coerce non-primitive number/decimal types to zero. Apache EL parser was fixed in this regard. The -Dorg.apache.el.parser.COERCE_TO_ZERO=false is now the default behavior and the VM argument became superflous.

However, the EL guys went a bit overboard with fixing issue 184. They also treated java.lang.String the same way as a primitive! See also section 1.23.1 and 1.23.2 of EL 3.0 specification (emphasis mine):

1.23.1 To Coerce a Value X to Type Y

  • If X is null and Y is not a primitive type and also not a String, return null.
  • ...

1.23.2 Coerce A to String

  • If A is null: return “”
  • ...

They didn't seem to realize that coercion can work in two ways: when performing a "get" and when performing a "set". Coercion from null string to empty string makes definitely sense during invoking the getter (you don't want to see "null" being printed over all place in HTML output, right?). Only, it really doesn't make sense during invoking the setter (as the model would be polluted with empty strings over all place).

And suddenly, the javax.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL didn't have any effect anymore. Even when JSF changes the empty string submitted value to null as instructed, EL 3.0 will afterwards coerce the null string back to empty string again right before invoking the model value setter. This was first noticeable in Oracle EL (WildFly, GlassFish, etc) and only later in Apache EL (see next chapter). This was discussed in JSF spec issue 1203 and JSF issue 3071, and finally EL spec issue 18 was created to point out this mistake in EL 3.0.

Until they fix it, this could be workarounded with a custom ELResolver for common property type of java.lang.String like below which utilizes the new EL 3.0 introduced ELResolver#convertToType() method. The remainder of the methods is not relevant.

public class EmptyToNullStringELResolver extends ELResolver {

    @Override
    public Class<?> getCommonPropertyType(ELContext context, Object base) {
        return String.class;
    }

    @Override
    public Object convertToType(ELContext context, Object value, Class<?> targetType) {
        if (value == null && targetType == String.class) {
            context.setPropertyResolved(true);
        }

        return value;
    }

    @Override
    public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object base) {
        return null;
    }

    @Override
    public Class<?> getType(ELContext context, Object base, Object property) {
        return null;
    }

    @Override
    public Object getValue(ELContext context, Object base, Object property) {
        return null;
    }

    @Override
    public boolean isReadOnly(ELContext context, Object base, Object property) {
        return true;
    }

    @Override
    public void setValue(ELContext context, Object base, Object property, Object value) {
        // NOOP.
    }

}

Which is registered in faces-config.xml as below:

<application>
    <el-resolver>com.example.EmptyToNullStringELResolver</el-resolver>
</application>

This was finally fixed in Oracle EL 3.0.1-b05 (July 2014). It is shipped as part of a.o. GlassFish 4.1 and WildFly 8.2. So the above custom ELResolver is unnecessary on those servers. Do note that you still need to keep The Context Param With The Long Name in EL 3.0 reagardless of the fix and the custom ELResolver!

Tomcat 8.0.7 - 8.0.15 (2014)

Apache EL 3.0 worked flawlessly until someone reported Tomcat issue 56522 that it didn't comply the new EL 3.0 requirement of coercing null string to empty string, even though that new requirement didn't make sense. So since Tomcat 8.0.7, Apache EL also suffered from this EL 3.0 problem of unnecessarily coercing null string to empty string during setting the model value. However, the above EmptyToNullStringELResolver workaround in turn still failed in all Tomcat versions until 8.0.15, because it didn't take any custom ELResolver into account. See also Tomcat issue 57309. This was fixed in Tomcat 8.0.16.

If upgrading to at least Tomcat 8.0.16 in order to utilize the EmptyToNullStringELResolver is not an option, the only way to get it to work is to replace Apache EL by Oracle EL in Tomcat-targeted JSF web applications. This can be achieved by dropping the current latest release in webapp's /WEB-INF/lib (which is javax.el-3.0.1-b08.jar at the time of writing) and adding the below context parameter to web.xml to tell Mojarra to use that EL implementation instead:

<context-param>
    <param-name>com.sun.faces.expressionFactory</param-name>
    <param-value>com.sun.el.ExpressionFactoryImpl</param-value>
</context-param>

Or when you're using MyFaces:

<context-param>
    <param-name>org.apache.myfaces.EXPRESSION_FACTORY</param-name>
    <param-value>com.sun.el.ExpressionFactoryImpl</param-value>
</context-param>

Of course, this is also a good alternative to the custom EmptyToNullStringELResolver in its entirety. Also here, you still need to keep The Context Param With The Long Name.

Summary

Here's a summary table which should help you in figuring out what to do in order to keep non-primitive bean properties null when the submitted value is empty or null (so, to avoid pollution of model with empty strings or zeroes over all place).

Note: Tomcat and JBoss use Apache EL, and GlassFish and WildFly use Oracle EL. Other servers (mainly the closed source ones such as WebSphere, WebLogic, etc) are not covered as I can't tell the exact versions being affected, but generally the same rules apply depending on the EL implementation being used.

JSFTomcatJBoss ASWildFlyGlassFish
5.5.x-6.0.156.0.166.0.17+7.0.x8.0.0-68.0.7-158.0.16+4.x/5.05.1-26.x/7.x8.0-18.2/9.0+3.x4.04.1+
1.0-1.1MCUTMC,CZMC,CZMCMC,UEMC,ERMCMC,CZMC,CZMC,ERMCMCMC,ERMC
1.2ACUTAC,CZAC,CZACAC,UEAC,ERACAC,CZAC,CZAC,ERACACAC,ERAC
2.0-2.1JFUTJF,CZJF,CZJFJF,UEJF,ERJFJF,CZJF,CZJF,ERJFJFJF,ERJF
2.2JF,CZJFJF,UEJF,ERJF,CZJF,ERJFJFJF,ERJF
  • MC: manually register EmptyToNullStringConverter over all place in <h:inputXxx converter>.
  • AC: automatically register EmptyToNullStringConverter on java.lang.String class.
  • UT: upgrade Tomcat to at least 6.0.17 as version 6.0.16 introduced the broken behavior on non-primitive number/decimal types and the VM argument was only added in 6.0.17.
  • CZ: add -Dorg.apache.el.parser.COERCE_TO_ZERO=false VM argument.
  • JF: add javax.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL=true context param.
  • ER: register EmptyToNullStringELResolver, or alternatively, just do UE.
  • UE: migrate/upgrade to Oracle EL implementation version 3.0.1-b05 or newer.
  • : this JSF version is not supported on this server anyway.