MYFACES-4327 Implement BeanELResolver which uses LambdaMetafactory
diff --git a/impl/src/main/java/org/apache/myfaces/el/ELResolverBuilderForFaces.java b/impl/src/main/java/org/apache/myfaces/el/ELResolverBuilderForFaces.java
index f294922..8bb9dd8 100644
--- a/impl/src/main/java/org/apache/myfaces/el/ELResolverBuilderForFaces.java
+++ b/impl/src/main/java/org/apache/myfaces/el/ELResolverBuilderForFaces.java
@@ -46,6 +46,7 @@
 import org.apache.myfaces.el.resolver.ScopedAttributeResolver;
 import org.apache.myfaces.el.resolver.implicitobject.ImplicitObjectResolver;
 import org.apache.myfaces.config.MyfacesConfig;
+import org.apache.myfaces.el.resolver.MethodHandleBeanELResolver;
 import org.apache.myfaces.util.lang.ClassUtils;
 
 /**
@@ -139,8 +140,15 @@
         list.add(new MapELResolver());
         list.add(new ListELResolver());
         list.add(new ArrayELResolver());
-        list.add(new BeanELResolver());
-        
+        if (MethodHandleBeanELResolver.isSupported())
+        {
+            list.add(new MethodHandleBeanELResolver());
+        }
+        else
+        {
+            list.add(new BeanELResolver());
+        }
+
         // give the user a chance to sort the resolvers
         sortELResolvers(list, Scope.Faces);
         
diff --git a/impl/src/main/java/org/apache/myfaces/el/resolver/MethodHandleBeanELResolver.java b/impl/src/main/java/org/apache/myfaces/el/resolver/MethodHandleBeanELResolver.java
new file mode 100644
index 0000000..6d54a05
--- /dev/null
+++ b/impl/src/main/java/org/apache/myfaces/el/resolver/MethodHandleBeanELResolver.java
@@ -0,0 +1,349 @@
+/*

+ * Licensed to the Apache Software Foundation (ASF) under one

+ * or more contributor license agreements.  See the NOTICE file

+ * distributed with this work for additional information

+ * regarding copyright ownership.  The ASF licenses this file

+ * to you under the Apache License, Version 2.0 (the

+ * "License"); you may not use this file except in compliance

+ * with the License.  You may obtain a copy of the License at

+ *

+ *   http://www.apache.org/licenses/LICENSE-2.0

+ *

+ * Unless required by applicable law or agreed to in writing,

+ * software distributed under the License is distributed on an

+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY

+ * KIND, either express or implied.  See the License for the

+ * specific language governing permissions and limitations

+ * under the License.

+ */

+package org.apache.myfaces.el.resolver;

+

+import java.beans.Introspector;

+import java.beans.PropertyDescriptor;

+import java.lang.invoke.CallSite;

+import java.lang.invoke.LambdaConversionException;

+import java.lang.invoke.LambdaMetafactory;

+import java.lang.invoke.MethodHandle;

+import java.lang.invoke.MethodHandles;

+import java.lang.invoke.MethodType;

+import java.lang.reflect.Method;

+import java.util.Map;

+import java.util.Objects;

+import java.util.concurrent.ConcurrentHashMap;

+import java.util.function.BiConsumer;

+import java.util.function.Function;

+import java.util.function.ObjDoubleConsumer;

+import java.util.function.ObjIntConsumer;

+import java.util.function.ObjLongConsumer;

+

+import javax.el.BeanELResolver;

+import javax.el.ELContext;

+import javax.el.ELException;

+import javax.el.PropertyNotFoundException;

+import javax.el.PropertyNotWritableException;

+

+public class MethodHandleBeanELResolver extends BeanELResolver

+{

+    private static Method privateLookupIn;

+

+    static

+    {

+        try

+        {

+            privateLookupIn = MethodHandles.class.getMethod("privateLookupIn", Class.class,

+                    MethodHandles.Lookup.class);

+        }

+        catch (Exception e)

+        {

+        }

+    }

+

+    public static boolean isSupported()

+    {

+        return privateLookupIn != null;

+    }

+

+    private final ConcurrentHashMap<String, Map<String, PropertyInfo>> cache;

+

+    public MethodHandleBeanELResolver()

+    {

+        cache = new ConcurrentHashMap<>();

+    }

+

+    @Override

+    public Class<?> getType(ELContext context, Object base, Object property)

+    {

+        Objects.requireNonNull(context);

+        if (base == null || property == null)

+        {

+            return null;

+        }

+

+        context.setPropertyResolved(base, property);

+

+        return getPropertyInfo(base, property).type;

+    }

+

+    @SuppressWarnings("unchecked")

+    @Override

+    public Object getValue(ELContext context, Object base, Object property)

+    {

+        Objects.requireNonNull(context);

+        if (base == null || property == null)

+        {

+            return null;

+        }

+

+        context.setPropertyResolved(base, property);

+

+        try

+        {

+            return getPropertyInfo(base, property).getter.apply(base);

+        }

+        catch (Exception e)

+        {

+            throw new ELException(e);

+        }

+    }

+

+    @SuppressWarnings("unchecked")

+    @Override

+    public void setValue(ELContext context, Object base, Object property, Object value)

+    {

+        Objects.requireNonNull(context);

+        if (base == null || property == null)

+        {

+            return;

+        }

+

+        context.setPropertyResolved(base, property);

+

+        PropertyInfo propertyInfo = getPropertyInfo(base, property);

+        if (propertyInfo.setter == null)

+        {

+            throw new PropertyNotWritableException("Property \"" + (String) property

+                    + "\" in \"" + base.getClass().getName() + "\" is not writable!");

+        }

+

+        try

+        {

+            propertyInfo.setter.accept(base, value);

+        }

+        catch (Exception e)

+        {

+            throw new ELException(e);

+        }

+    }

+

+    @Override

+    public Object invoke(ELContext context, Object base, Object method, Class<?>[] paramTypes, Object[] params)

+    {

+        // just use the original BeanELResolver method

+        // it's higher effort to implement this

+        return super.invoke(context, base, method, paramTypes, params);

+    }

+

+    @Override

+    public boolean isReadOnly(ELContext context, Object base, Object property)

+    {

+        Objects.requireNonNull(context);

+        if (base == null || property == null)

+        {

+            return false;

+        }

+

+        context.setPropertyResolved(base, property);

+

+        return getPropertyInfo(base, property).setter == null;

+    }

+

+    @Override

+    public Class<?> getCommonPropertyType(ELContext context, Object base)

+    {

+        if (base != null)

+        {

+            return Object.class;

+        }

+

+        return null;

+    }

+

+    protected class PropertyInfo

+    {

+        Class<?> type;

+        Function getter;

+        BiConsumer setter;

+    }

+

+    protected PropertyInfo getPropertyInfo(Object base, Object property)

+    {

+        Map<String, PropertyInfo> beanCache = cache.computeIfAbsent(base.getClass().getName(),

+                k -> new ConcurrentHashMap<>());

+        return beanCache.computeIfAbsent((String) property, k -> initPropertyInfo(base.getClass(), k));

+    }

+

+    protected PropertyInfo initPropertyInfo(Class<?> target, String fieldName)

+    {

+

+        PropertyInfo info = new PropertyInfo();

+

+        try

+        {

+            PropertyDescriptor pd = null;

+            for (PropertyDescriptor cpd : Introspector.getBeanInfo(target, Object.class).getPropertyDescriptors())

+            {

+                if (fieldName.equals(cpd.getName()))

+                {

+                    pd = cpd;

+                }

+            }

+

+            if (pd == null)

+            {

+                throw new PropertyNotFoundException("Property \"" + fieldName + "\" not found on \""

+                        + target.getName() + "\"");

+            }

+

+            info.type = pd.getPropertyType();

+

+            MethodHandles.Lookup lookup = (MethodHandles.Lookup) privateLookupIn.invoke(null, target,

+                    MethodHandles.lookup());

+

+            Method getter = pd.getReadMethod();

+            if (getter != null)

+            {

+                MethodHandle getterHandle = lookup.unreflect(getter);

+                CallSite getterCallSite = LambdaMetafactory.metafactory(lookup,

+                        "apply",

+                        MethodType.methodType(Function.class),

+                        MethodType.methodType(Object.class, Object.class),

+                        getterHandle,

+                        getterHandle.type());

+                info.getter = (Function) getterCallSite.getTarget().invokeExact();

+            }

+

+            Method setter = pd.getWriteMethod();

+            if (setter != null)

+            {

+                MethodHandle setterHandle = lookup.unreflect(setter);

+                info.setter = createSetter(lookup, info, setterHandle);

+            }

+        }

+        catch (Throwable e)

+        {

+            throw new ELException(e);

+        }

+

+        return info;

+    }

+

+    @SuppressWarnings("unchecked")

+    protected BiConsumer createSetter(MethodHandles.Lookup lookup, PropertyInfo propertyInfo,

+            MethodHandle setterHandle)

+            throws LambdaConversionException, Throwable

+    {

+        // special handling for primitives required, see https://dzone.com/articles/setters-method-handles-and-java-11

+        if (propertyInfo.type.isPrimitive())

+        {

+            if (propertyInfo.type == double.class)

+            {

+                ObjDoubleConsumer consumer = (ObjDoubleConsumer) createSetterCallSite(

+                        lookup, setterHandle, ObjDoubleConsumer.class, double.class).getTarget().invokeExact();

+                return (a, b) -> consumer.accept(a, (double) b);

+            }

+            else if (propertyInfo.type == int.class)

+            {

+                ObjIntConsumer consumer = (ObjIntConsumer) createSetterCallSite(

+                        lookup, setterHandle, ObjIntConsumer.class, int.class).getTarget().invokeExact();

+                return (a, b) -> consumer.accept(a, (int) b);

+            }

+            else if (propertyInfo.type == long.class)

+            {

+                ObjLongConsumer consumer = (ObjLongConsumer) createSetterCallSite(

+                        lookup, setterHandle, ObjLongConsumer.class, long.class).getTarget().invokeExact();

+                return (a, b) -> consumer.accept(a, (long) b);

+            }

+            else if (propertyInfo.type == float.class)

+            {

+                ObjFloatConsumer consumer = (ObjFloatConsumer) createSetterCallSite(

+                        lookup, setterHandle, ObjFloatConsumer.class, float.class).getTarget().invokeExact();

+                return (a, b) -> consumer.accept(a, (float) b);

+            }

+            else if (propertyInfo.type == byte.class)

+            {

+                ObjByteConsumer consumer = (ObjByteConsumer) createSetterCallSite(

+                        lookup, setterHandle, ObjByteConsumer.class, byte.class).getTarget().invokeExact();

+                return (a, b) -> consumer.accept(a, (byte) b);

+            }

+            else if (propertyInfo.type == char.class)

+            {

+                ObjCharConsumer consumer = (ObjCharConsumer) createSetterCallSite(

+                        lookup, setterHandle, ObjCharConsumer.class, char.class).getTarget().invokeExact();

+                return (a, b) -> consumer.accept(a, (char) b);

+            }

+            else if (propertyInfo.type == short.class)

+            {

+                ObjShortConsumer consumer = (ObjShortConsumer) createSetterCallSite(

+                        lookup, setterHandle, ObjShortConsumer.class, short.class).getTarget().invokeExact();

+                return (a, b) -> consumer.accept(a, (short) b);

+            }

+            else if (propertyInfo.type == boolean.class)

+            {

+                ObjBooleanConsumer consumer = (ObjBooleanConsumer) createSetterCallSite(

+                        lookup, setterHandle, ObjBooleanConsumer.class, boolean.class).getTarget().invokeExact();

+                return (a, b) -> consumer.accept(a, (boolean) b);

+            }

+            else

+            {

+                throw new RuntimeException("Type is not supported yet: " + propertyInfo.type.getName());

+            }

+        }

+        else

+        {

+            return (BiConsumer) createSetterCallSite(lookup, setterHandle, BiConsumer.class, Object.class).getTarget()

+                    .invokeExact();

+        }

+    }

+

+    protected CallSite createSetterCallSite(MethodHandles.Lookup lookup, MethodHandle setter, Class<?> interfaceType,

+            Class<?> valueType)

+            throws LambdaConversionException

+    {

+        return LambdaMetafactory.metafactory(lookup,

+                "accept",

+                MethodType.methodType(interfaceType),

+                MethodType.methodType(void.class, Object.class, valueType),

+                setter,

+                setter.type());

+    }

+

+    @FunctionalInterface

+    public interface ObjFloatConsumer<T extends Object>

+    {

+        public void accept(T t, float i);

+    }

+

+    @FunctionalInterface

+    public interface ObjByteConsumer<T extends Object>

+    {

+        public void accept(T t, byte i);

+    }

+

+    @FunctionalInterface

+    public interface ObjCharConsumer<T extends Object>

+    {

+        public void accept(T t, char i);

+    }

+

+    @FunctionalInterface

+    public interface ObjShortConsumer<T extends Object>

+    {

+        public void accept(T t, short i);

+    }

+

+    @FunctionalInterface

+    public interface ObjBooleanConsumer<T extends Object>

+    {

+        public void accept(T t, boolean i);

+    }

+}