Getter in an interface with default method JSF
Asked Answered
B

3

8

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?

Bindweed answered 31/7, 2017 at 18:15 Comment(5)
and what makes you believe that an error about some 'anoAtual' is in any way related to a method called getCurrentYear() ?Boak
And how is it related to java-se or jsf?Bellamy
@MikeNakis anoAtual is currentYear in portuguese. OP has translated his code but not his exception. Probably his actual method is getAnoActual.Westerfield
This could be related to Java 8 interface default method doesn't seem to declare property, pointing to bug JDK-8071693 Introspector ignores default interface methods, still unresolved…Hudis
This can be workarounded with a custom EL resolver, or by treating the property as a method.Microscopium
E
3

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();
    }
}
Emoryemote answered 12/11, 2020 at 10:43 Comment(0)
D
2

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>
Dally answered 22/10, 2021 at 10:8 Comment(2)
Thanks for your code but it will not resolve the issue in all cases since it will only take the first PropertyDescriptor in the hierarchy and if that descriptor or interface only has a write method for example but another interface or superclass has the read method you will get the exception read property not available. I solved this adding a list of PropertyDescriptor to the BeanProperty class and populating all of them. Further to check if any read or write descriptor is available and not just the first. Slightly changed the hierarchy method but not sure if necessary.Very
Thank you for your feedback @djmj. I fixed code in my answer. I added method processInterface, which recursively processes all interface hierarchy.Dally
K
0

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

Kenlay answered 3/8, 2017 at 23:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.