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?

BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
  • 2
    and what makes you believe that an error about some 'anoAtual' is in any way related to a method called `getCurrentYear()` ? – Mike Nakis Jul 31 '17 at 18:21
  • 3
    And how is it related to java-se or jsf? – Kukeltje Jul 31 '17 at 18:22
  • 1
    @MikeNakis anoAtual is currentYear in portuguese. OP has translated his code but not his exception. Probably his actual method is getAnoActual. – RubioRic Aug 01 '17 at 06:25
  • 2
    This could be related to [Java 8 interface default method doesn't seem to declare property](https://stackoverflow.com/q/31703563/2711488), pointing to bug [JDK-8071693 Introspector ignores default interface methods](https://bugs.openjdk.java.net/browse/JDK-8071693), still unresolved… – Holger Aug 01 '17 at 09:46
  • 1
    This can be workarounded with a custom EL resolver, or by treating the property as a method. – BalusC Aug 04 '17 at 07:04

3 Answers3

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();
    }
}
Thies
  • 686
  • 6
  • 17
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>
mnyakushev
  • 61
  • 4
  • 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. – djmj Sep 29 '22 at 14:38
  • Thank you for your feedback @djmj. I fixed code in my answer. I added method processInterface, which recursively processes all interface hierarchy. – mnyakushev Oct 06 '22 at 13:19
0

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

Karim
  • 1,004
  • 8
  • 18