I found it will be fixed in Jakarta EE 10.
https://github.com/eclipse-ee4j/el-ri/issues/43
Before Jakarta EE 10 you can use custom EL Resolver.
package ru.example.el;
import javax.el.ELContext;
import javax.el.ELException;
import javax.el.ELResolver;
import java.beans.*;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class DefaultMethodELResolver extends ELResolver {
private static final Map<Class<?>, BeanProperties> properties = new ConcurrentHashMap<>();
@Override
public Object getValue(ELContext context, Object base, Object property) {
if (base == null || property == null) {
return null;
}
BeanProperty beanProperty = getBeanProperty(base, property);
if (beanProperty != null) {
Method method = beanProperty.getReadMethod();
if (method == null) {
throw new ELException(String.format("Read method for property '%s' not found", property));
}
Object value;
try {
value = method.invoke(base);
context.setPropertyResolved(base, property);
} catch (Exception e) {
throw new ELException(String.format("Read error for property '%s' in class '%s'", property, base.getClass()), e);
}
return value;
}
return null;
}
@Override
public Class<?> getType(ELContext context, Object base, Object property) {
if (base == null || property == null) {
return null;
}
BeanProperty beanProperty = getBeanProperty(base, property);
if (beanProperty != null) {
context.setPropertyResolved(true);
return beanProperty.getPropertyType();
}
return null;
}
@Override
public void setValue(ELContext context, Object base, Object property, Object value) {
if (base == null || property == null) {
return;
}
BeanProperty beanProperty = getBeanProperty(base, property);
if (beanProperty != null) {
Method method = beanProperty.getWriteMethod();
if (method == null) {
throw new ELException(String.format("Write method for property '%s' not found", property));
}
try {
method.invoke(base, value);
context.setPropertyResolved(base, property);
} catch (Exception e) {
throw new ELException(String.format("Write error for property '%s' in class '%s'", property, base.getClass()), e);
}
}
}
@Override
public boolean isReadOnly(ELContext context, Object base, Object property) {
if (base == null || property == null) {
return false;
}
BeanProperty beanProperty = getBeanProperty(base, property);
if (beanProperty != null) {
context.setPropertyResolved(true);
return beanProperty.isReadOnly();
}
return false;
}
@Override
public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object base) {
return null;
}
@Override
public Class<?> getCommonPropertyType(ELContext context, Object base) {
return Object.class;
}
private BeanProperty getBeanProperty(Object base, Object property) {
return properties.computeIfAbsent(base.getClass(), BeanProperties::new)
.getBeanProperty(property);
}
private static final class BeanProperties {
private final Map<String, BeanProperty> propertyByName = new HashMap<>();
public BeanProperties(Class<?> cls) {
try {
scanInterfaces(cls);
} catch (IntrospectionException e) {
throw new ELException(e);
}
}
private void scanInterfaces(Class<?> cls) throws IntrospectionException {
for (Class<?> ifc : cls.getInterfaces()) {
processInterface(ifc);
}
Class<?> superclass = cls.getSuperclass();
if (superclass != null) {
scanInterfaces(superclass);
}
}
private void processInterface(Class<?> ifc) throws IntrospectionException {
BeanInfo info = Introspector.getBeanInfo(ifc);
for (PropertyDescriptor propertyDescriptor : info.getPropertyDescriptors()) {
String propertyName = propertyDescriptor.getName();
BeanProperty beanProperty = propertyByName
.computeIfAbsent(propertyName, key -> new BeanProperty(propertyDescriptor.getPropertyType()));
if (beanProperty.getReadMethod() == null && propertyDescriptor.getReadMethod() != null) {
beanProperty.setReadMethod(propertyDescriptor.getReadMethod());
}
if (beanProperty.getWriteMethod() == null && propertyDescriptor.getWriteMethod() != null) {
beanProperty.setWriteMethod(propertyDescriptor.getWriteMethod());
}
}
for (Class<?> parentIfc : ifc.getInterfaces()) {
processInterface(parentIfc);
}
}
public BeanProperty getBeanProperty(Object property) {
return propertyByName.get(property.toString());
}
}
private static final class BeanProperty {
private final Class<?> propertyType;
private Method readMethod;
private Method writeMethod;
public BeanProperty(Class<?> propertyType) {
this.propertyType = propertyType;
}
public Class<?> getPropertyType() {
return propertyType;
}
public boolean isReadOnly() {
return getWriteMethod() == null;
}
public Method getReadMethod() {
return readMethod;
}
public void setReadMethod(Method readMethod) {
this.readMethod = readMethod;
}
public Method getWriteMethod() {
return writeMethod;
}
public void setWriteMethod(Method writeMethod) {
this.writeMethod = writeMethod;
}
}
}
You should register EL Resolver in faces-config.xml.
<?xml version="1.0" encoding="utf-8"?>
<faces-config version="2.3" xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_3.xsd">
<name>el_resolver</name>
<application>
<el-resolver>ru.example.el.DefaultMethodELResolver</el-resolver>
</application>
</faces-config>
getCurrentYear()
? – Boak