Getter in an interface with default method JSF

1.3k Views Asked by At

I have an interface with the following default method:

default Integer getCurrentYear() {return DateUtil.getYear();}

I also have a controller that implements this interface, but it does not overwrite the method.

public class NotifyController implements INotifyController

I'm trying to access this method from my xhtml like this:

#{notifyController.currentYear}

However when I open the screen the following error occurs:

The class 'br.com.viasoft.controller.notify.notifyController' does not have the property 'anoAtual'

If I access this method from an instance of my controller, it returns the right value, however when I try to access it from my xhtml as a "property" it occurs this error.

Is there a way to access this interface property from a reference from my controller without having to implement the method?

3

There are 3 best solutions below

0
On

since this bug is related to JDK, you'll have to create a delegate method in the class that needs the property.

2
On

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>
0
On

This may be considered as a bug, or one might argue it is a decision to not support default methods as properties.
See in JDK8 java.beans.Introspector.getPublicDeclaredMethods(Class<?>)
or in JDK13 com.sun.beans.introspect.MethodInfo.get(Class<?>)
at line if (!method.getDeclaringClass().equals(clz))
And only the super class (recursively upto Object, but not the interfaces) are added, see java.beans.Introspector.Introspector(Class<?>, Class<?>, int) when setting superBeanInfo.

Solutions:

  • Use EL method call syntax (i.e. not property access): #{notifyController.getCurrentYear()} in your case.
    Downside: You have to change the JSF code and must consider for each use if it may be a default method. Also refactoring forces changes that are not recognized by the compiler, only during runtime.

  • Create an EL-Resolver to generically support default methods. But this should use good internal caching like the standard java.beans.Introspector to not slow down the EL parsing.
    See "Property not found on type" when using interface default methods in JSP EL for a basic example (without caching).

  • If only a few classes/interfaces are affected simply create small BeanInfo classes.
    The code example below shows this (basing on your example).
    Downside: A separate class must be created for each class (that is used in JSF/EL) implementing such an interface.
    See also: Default method in interface in Java 8 and Bean Info Introspector


=> static getBeanInfo() in the interface with default methods
=> simple+short BeanInfo class for each class extending the interface

interface INotifyController {
    default Integer getCurrentYear() { ... }
    default boolean isAHappyYear() { ... }
    default void setSomething(String param) { ... }

    /** Support for JSF-EL/Beans to get default methods. */
    static java.beans.BeanInfo[] getBeanInfo() {
        try {
            java.beans.BeanInfo info = java.beans.Introspector.getBeanInfo(INotifyController.class);
            if (info != null)  return new java.beans.BeanInfo[] { info };
        } catch (java.beans.IntrospectionException e) {
            //nothing to do
        }
        return null;
    }

}

public class NotifyController implements INotifyController {
    // your class implementation
    ...
}


// must be a public class and thus in its own file
public class NotifyControllerBeanInfo extends java.beans.SimpleBeanInfo {
    @Override
    public java.beans.BeanInfo[] getAdditionalBeanInfo() {
        return INotifyController.getBeanInfo();
    }
}