Implement alternative JavaBean support

Add support for multiple JavaBean specification implementations.

If the class java.beans.BeanInfo is available, the full JavaBeans
implementation is used (same behaviour as before this patch).

If the full JavaBeans implementation is not available, a built-in,
stand-alone implementation is used that only provides the JavaBeans
functionality that does not depend on any of the java.beans.* classes
(essentially getter/setter support).

Note that the configuration system properties are intended for testing
only. They are NOT part of the public API.
diff --git a/api/pom.xml b/api/pom.xml
index bbb1e15..df5c7f2 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -83,6 +83,12 @@
             <version>5.8.1</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-params</artifactId>
+            <version>5.8.1</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <properties>
diff --git a/api/src/main/java/jakarta/el/BeanELResolver.java b/api/src/main/java/jakarta/el/BeanELResolver.java
index 0435572..00857a4 100644
--- a/api/src/main/java/jakarta/el/BeanELResolver.java
+++ b/api/src/main/java/jakarta/el/BeanELResolver.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1997, 2022 Oracle and/or its affiliates and others.
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates and others.
  * All rights reserved.
  * Copyright 2004 The Apache Software Foundation
  *
@@ -20,10 +20,6 @@
 
 import static jakarta.el.ELUtil.getExceptionMessageString;
 
-import java.beans.BeanInfo;
-import java.beans.IntrospectionException;
-import java.beans.Introspector;
-import java.beans.PropertyDescriptor;
 import java.lang.ref.ReferenceQueue;
 import java.lang.ref.SoftReference;
 import java.lang.reflect.InvocationTargetException;
@@ -146,20 +142,20 @@
     /*
      * Defines a property for a bean.
      */
-    final static class BeanProperty {
+    abstract static class BeanProperty {
 
         final private Class<?> baseClass;
-        final private PropertyDescriptor descriptor;
+        final private Class<?> type;
         private Method readMethod;
         private Method writeMethod;
 
-        public BeanProperty(Class<?> baseClass, PropertyDescriptor descriptor) {
+        public BeanProperty(Class<?> baseClass, Class<?> type) {
             this.baseClass = baseClass;
-            this.descriptor = descriptor;
+            this.type = type;
         }
 
         public Class<?> getPropertyType() {
-            return descriptor.getPropertyType();
+            return this.type;
         }
 
         public boolean isReadOnly(Object base) {
@@ -168,67 +164,35 @@
 
         public Method getReadMethod(Object base) {
             if (readMethod == null) {
-                readMethod = ELUtil.getMethod(baseClass, base, descriptor.getReadMethod());
+                readMethod = ELUtil.getMethod(baseClass, base, getReadMethod());
             }
             return readMethod;
         }
 
         public Method getWriteMethod(Object base) {
             if (writeMethod == null) {
-                writeMethod = ELUtil.getMethod(baseClass, base, descriptor.getWriteMethod());
+                writeMethod = ELUtil.getMethod(baseClass, base, getWriteMethod());
             }
             return writeMethod;
         }
+        
+        abstract Method getWriteMethod();
+        
+        abstract Method getReadMethod();
     }
 
     /*
      * Defines the properties for a bean.
      */
-    final static class BeanProperties {
+    abstract static class BeanProperties {
 
-        private final Map<String, BeanProperty> propertyMap = new HashMap<>();
+        protected final Map<String, BeanProperty> propertyMap = new HashMap<>();
+        protected final Class<?> baseClass;
 
-        public BeanProperties(Class<?> baseClass) {
-            PropertyDescriptor[] descriptors;
-            try {
-                BeanInfo info = Introspector.getBeanInfo(baseClass);
-                descriptors = info.getPropertyDescriptors();
-                for (PropertyDescriptor descriptor : descriptors) {
-                    propertyMap.put(descriptor.getName(), new BeanProperty(baseClass, descriptor));
-                }
-                /**
-                 * Populating from any interfaces solves two distinct problems:
-                 * 1. When running under a security manager, classes may be
-                 *    unaccessible but have accessible interfaces.
-                 * 2. It enables default methods to be included.
-                 */
-                populateFromInterfaces(baseClass, baseClass);
-            } catch (IntrospectionException ie) {
-                throw new ELException(ie);
-            }
-
+        BeanProperties(Class<?> baseClass) {
+            this.baseClass = baseClass;
         }
 
-        private void populateFromInterfaces(Class<?> baseClass, Class<?> aClass) throws IntrospectionException {
-            Class<?> interfaces[] = aClass.getInterfaces();
-            if (interfaces.length > 0) {
-                for (Class<?> ifs : interfaces) {
-                    BeanInfo info = Introspector.getBeanInfo(ifs);
-                    PropertyDescriptor[] pds = info.getPropertyDescriptors();
-                    for (PropertyDescriptor pd : pds) {
-                        if (!this.propertyMap.containsKey(pd.getName())) {
-                            this.propertyMap.put(pd.getName(), new BeanProperty(
-                                    baseClass, pd));
-                        }
-                    }
-                }
-            }
-            Class<?> superclass = aClass.getSuperclass();
-            if (superclass != null) {
-                populateFromInterfaces(baseClass, superclass);
-            }
-        }
-        
         public BeanProperty getBeanProperty(String property) {
             return propertyMap.get(property);
         }
@@ -568,7 +532,7 @@
 
         BeanProperties beanProperties = properties.get(baseClass);
         if (beanProperties == null) {
-            beanProperties = new BeanProperties(baseClass);
+            beanProperties = BeanSupport.getInstance().getBeanProperties(baseClass);
             properties.put(baseClass, beanProperties);
         }
 
diff --git a/api/src/main/java/jakarta/el/BeanSupport.java b/api/src/main/java/jakarta/el/BeanSupport.java
new file mode 100644
index 0000000..01e46b4
--- /dev/null
+++ b/api/src/main/java/jakarta/el/BeanSupport.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2023 Contributors to the Eclipse Foundation
+ * 
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package jakarta.el;
+
+import jakarta.el.BeanELResolver.BeanProperties;
+
+/*
+ * Provides an abstraction so the BeanELResolver can obtain JavaBeans specification support via different
+ * implementations.
+ */
+abstract class BeanSupport {
+
+    private static final BeanSupport beanSupport;
+
+    static {
+        // Only intended for unit tests. Not intended to be part of public API.
+        boolean doNotCacheInstance = Boolean.getBoolean("jakarta.el.BeanSupport.doNotCacheInstance");
+        if (doNotCacheInstance) {
+            beanSupport = null;
+        } else {
+            beanSupport = createInstance();
+        }
+    }
+
+    private static BeanSupport createInstance() {
+        // Only intended for unit tests. Not intended to be part of public API.
+        boolean useFull = !Boolean.getBoolean("jakarta.el.BeanSupport.useStandalone");
+
+        if (useFull) {
+            // If not explicitly configured to use standalone, use the full implementation unless it is not available.
+            try {
+                Class.forName("java.beans.BeanInfo");
+            } catch (Exception e) {
+                // Ignore: Expected if using modules and java.desktop module is not present
+                useFull = false;
+            }
+        }
+        if (useFull) {
+            // The full implementation provided by the java.beans package
+            return new BeanSupportFull();
+        } else {
+            // The cut-down local implementation that does not depend on the java.beans package
+            return new BeanSupportStandalone();
+        }
+    }
+
+    static BeanSupport getInstance() {
+        if (beanSupport == null) {
+            return createInstance();
+        }
+        return beanSupport;
+    }
+
+    abstract BeanProperties getBeanProperties(Class<?> type);
+}
diff --git a/api/src/main/java/jakarta/el/BeanSupportFull.java b/api/src/main/java/jakarta/el/BeanSupportFull.java
new file mode 100644
index 0000000..b3a6a1d
--- /dev/null
+++ b/api/src/main/java/jakarta/el/BeanSupportFull.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2023 Contributors to the Eclipse Foundation
+ * 
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package jakarta.el;
+
+import java.beans.BeanInfo;
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.Method;
+
+import jakarta.el.BeanELResolver.BeanProperties;
+import jakarta.el.BeanELResolver.BeanProperty;
+
+class BeanSupportFull extends BeanSupport {
+
+    @Override
+    BeanProperties getBeanProperties(Class<?> type) {
+        return new BeanPropertiesFull(type);
+    }
+
+    static final class BeanPropertiesFull extends BeanProperties {
+
+        BeanPropertiesFull(Class<?> baseClass) throws ELException {
+            super(baseClass);
+            try {
+                BeanInfo info = Introspector.getBeanInfo(this.baseClass);
+                PropertyDescriptor[] pds = info.getPropertyDescriptors();
+                for (PropertyDescriptor pd : pds) {
+                    this.propertyMap.put(pd.getName(), new BeanPropertyFull(baseClass, pd));
+                }
+                /*
+                 * Populating from any interfaces causes default methods to be included.
+                 */
+                populateFromInterfaces(baseClass);
+            } catch (IntrospectionException ie) {
+                throw new ELException(ie);
+            }
+        }
+
+        private void populateFromInterfaces(Class<?> aClass) throws IntrospectionException {
+            Class<?> interfaces[] = aClass.getInterfaces();
+            if (interfaces.length > 0) {
+                for (Class<?> ifs : interfaces) {
+                    BeanInfo info = Introspector.getBeanInfo(ifs);
+                    PropertyDescriptor[] pds = info.getPropertyDescriptors();
+                    for (PropertyDescriptor pd : pds) {
+                        if (!this.propertyMap.containsKey(pd.getName())) {
+                            this.propertyMap.put(pd.getName(), new BeanPropertyFull(this.baseClass, pd));
+                        }
+                    }
+                    populateFromInterfaces(ifs);
+                }
+            }
+            Class<?> superclass = aClass.getSuperclass();
+            if (superclass != null) {
+                populateFromInterfaces(superclass);
+            }
+        }
+    }
+
+    static final class BeanPropertyFull extends BeanProperty {
+
+        private final PropertyDescriptor descriptor;
+
+        BeanPropertyFull(Class<?> owner, PropertyDescriptor descriptor) {
+            super(owner, descriptor.getPropertyType());
+            this.descriptor = descriptor;
+        }
+
+        @Override
+        Method getWriteMethod() {
+            return descriptor.getWriteMethod();
+        }
+
+        @Override
+        Method getReadMethod() {
+            return descriptor.getReadMethod();
+        }
+    }
+}
diff --git a/api/src/main/java/jakarta/el/BeanSupportStandalone.java b/api/src/main/java/jakarta/el/BeanSupportStandalone.java
new file mode 100644
index 0000000..bb34351
--- /dev/null
+++ b/api/src/main/java/jakarta/el/BeanSupportStandalone.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (c) 2023 Contributors to the Eclipse Foundation
+ * 
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package jakarta.el;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import jakarta.el.BeanELResolver.BeanProperties;
+import jakarta.el.BeanELResolver.BeanProperty;
+
+/*
+ * Implements those parts of the JavaBeans Specification that can be implemented without reference to the java.beans
+ * package.
+ */
+class BeanSupportStandalone extends BeanSupport {
+
+    @Override
+    BeanProperties getBeanProperties(Class<?> type) {
+        return new BeanPropertiesStandalone(type);
+    }
+
+
+    private static PropertyDescriptor[] getPropertyDescriptors(Class<?> baseClass) {
+        Map<String, PropertyDescriptor> pds = new HashMap<>();
+        Method[] methods = baseClass.getMethods();
+        for (Method method : methods) {
+            if (!Modifier.isStatic(method.getModifiers())) {
+                String methodName = method.getName();
+                if (methodName.startsWith("is")) {
+                    if (method.getParameterCount() == 0 && method.getReturnType() == boolean.class) {
+                        String propertyName = getPropertyName(methodName.substring(2));
+                        PropertyDescriptor pd = pds.computeIfAbsent(propertyName, k -> new PropertyDescriptor());
+                        pd.setName(propertyName);
+                        pd.setReadMethodIs(method);
+                    }
+                } else if (methodName.startsWith("get")) {
+                    if (method.getParameterCount() == 0) {
+                        String propertyName = getPropertyName(methodName.substring(3));
+                        PropertyDescriptor pd = pds.computeIfAbsent(propertyName, k -> new PropertyDescriptor());
+                        pd.setName(propertyName);
+                        pd.setReadMethod(method);
+                    }
+                } else if (methodName.startsWith("set")) {
+                    if (method.getParameterCount() == 1 && method.getReturnType() == void.class) {
+                        String propertyName = getPropertyName(methodName.substring(3));
+                        PropertyDescriptor pd = pds.computeIfAbsent(propertyName, k -> new PropertyDescriptor());
+                        pd.setName(propertyName);
+                        pd.addWriteMethod(method);
+                    }
+
+                }
+            }
+        }
+        return pds.values().toArray(new PropertyDescriptor[0]);
+    }
+
+
+    private static String getPropertyName(String input) {
+        if (input.length() == 0) {
+            return null;
+        }
+        if (!Character.isUpperCase(input.charAt(0))) {
+            return null;
+        }
+        if (input.length() > 1 && Character.isUpperCase(input.charAt(1))) {
+            return input;
+        }
+        char[] chars = input.toCharArray();
+        chars[0] = Character.toLowerCase(chars[0]);
+        return new String(chars);
+    }
+
+
+    private static class PropertyDescriptor {
+        private String name;
+        private boolean usesIs;
+        private Method readMethod;
+        private Method writeMethod;
+        private List<Method> writeMethods = new ArrayList<>();
+
+        String getName() {
+            return name;
+        }
+
+        void setName(String name) {
+            this.name = name;
+        }
+
+        Class<?> getType() {
+            if (readMethod == null) {
+                return getWriteMethod().getParameterTypes()[0];
+            }
+            return readMethod.getReturnType();
+        }
+
+        Method getReadMethod() {
+            return readMethod;
+        }
+
+        void setReadMethod(Method readMethod) {
+            if (usesIs) {
+                return;
+            }
+            this.readMethod = readMethod;
+        }
+
+        void setReadMethodIs(Method readMethod) {
+            this.readMethod = readMethod;
+            this.usesIs = true;
+        }
+
+        Method getWriteMethod() {
+            if (writeMethod == null) {
+                Class<?> type;
+                if (readMethod != null) {
+                    type = readMethod.getReturnType();
+                } else {
+                    type = writeMethods.get(0).getParameterTypes()[0];
+                }
+                for (Method candidate : writeMethods) {
+                    if (type.isAssignableFrom(candidate.getParameterTypes()[0])) {
+                        type = candidate.getParameterTypes()[0];
+                        this.writeMethod = candidate;
+                    }
+                }
+            }
+            return writeMethod;
+        }
+
+        void addWriteMethod(Method writeMethod) {
+            this.writeMethods.add(writeMethod);
+        }
+    }
+
+
+    static final class BeanPropertiesStandalone extends BeanProperties {
+
+        BeanPropertiesStandalone(Class<?> baseClass) throws ELException {
+            super(baseClass);
+            PropertyDescriptor[] pds = getPropertyDescriptors(this.baseClass);
+            for (PropertyDescriptor pd : pds) {
+                this.propertyMap.put(pd.getName(), new BeanPropertyStandalone(baseClass, pd));
+            }
+            /*
+             * Populating from any interfaces causes default methods to be included.
+             */
+            populateFromInterfaces(baseClass);
+        }
+
+        private void populateFromInterfaces(Class<?> aClass) {
+            Class<?> interfaces[] = aClass.getInterfaces();
+            if (interfaces.length > 0) {
+                for (Class<?> ifs : interfaces) {
+                    PropertyDescriptor[] pds = getPropertyDescriptors(baseClass);
+                    for (PropertyDescriptor pd : pds) {
+                        if (!this.propertyMap.containsKey(pd.getName())) {
+                            this.propertyMap.put(pd.getName(), new BeanPropertyStandalone(this.baseClass, pd));
+                        }
+                    }
+                    populateFromInterfaces(ifs);
+                }
+            }
+            Class<?> superclass = aClass.getSuperclass();
+            if (superclass != null) {
+                populateFromInterfaces(superclass);
+            }
+        }
+    }
+
+
+    static final class BeanPropertyStandalone extends BeanProperty {
+
+        private final Method readMethod;
+        private final Method writeMethod;
+
+        BeanPropertyStandalone(Class<?> owner, PropertyDescriptor pd) {
+            super(owner, pd.getType());
+            readMethod = pd.getReadMethod();
+            writeMethod = pd.getWriteMethod();
+        }
+
+        @Override
+        Method getReadMethod() {
+            return readMethod;
+        }
+
+        @Override
+        Method getWriteMethod() {
+            return writeMethod;
+        }
+    }
+}
diff --git a/api/src/test/java/jakarta/el/ELBaseTest.java b/api/src/test/java/jakarta/el/ELBaseTest.java
new file mode 100644
index 0000000..2696cb4
--- /dev/null
+++ b/api/src/test/java/jakarta/el/ELBaseTest.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2023 Contributors to the Eclipse Foundation
+ * 
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package jakarta.el;
+
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Base class for tests that (indirectly) use BeanSupport and want to test both implementations.
+ */
+public abstract class ELBaseTest {
+
+    public static Stream<String> data() {
+        return Stream.of("true", "false");
+    }
+
+    @BeforeAll
+    public static void setup() {
+        // Disable caching so we can switch implementations within a JVM instance.
+        System.setProperty("jakarta.el.BeanSupport.doNotCacheInstance", "true");
+    }
+
+    /*
+     * Double check test has been configured as expected
+     */
+    @ParameterizedTest
+    @MethodSource("data")
+    public void testImplementation(boolean useStandalone) {
+        configureBeanSupport(useStandalone);
+        if (useStandalone) {
+            Assertions.assertEquals(BeanSupportStandalone.class, BeanSupport.getInstance().getClass());
+        } else {
+            Assertions.assertEquals(BeanSupportFull.class, BeanSupport.getInstance().getClass());
+        }
+    }
+     
+    
+    protected final void configureBeanSupport(boolean useStandalone) {
+        // Set up the implementation for this test run
+        System.setProperty("jakarta.el.BeanSupport.useStandalone", Boolean.toString(useStandalone));
+    }
+}
diff --git a/api/src/test/java/jakarta/el/TestBeanSupport.java b/api/src/test/java/jakarta/el/TestBeanSupport.java
new file mode 100644
index 0000000..7b16101
--- /dev/null
+++ b/api/src/test/java/jakarta/el/TestBeanSupport.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (c) 2023 Contributors to the Eclipse Foundation
+ * 
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package jakarta.el;
+
+import jakarta.el.BeanELResolver.BeanProperties;
+import jakarta.el.BeanELResolver.BeanProperty;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class TestBeanSupport extends ELBaseTest {
+
+    @ParameterizedTest
+    @MethodSource("data")
+    public void testSimpleBean(boolean useStandalone) {
+        doTest(useStandalone, SimpleBean.class, "value", TypeA.class, TypeA.class, TypeA.class);
+    }
+
+    @ParameterizedTest
+    @MethodSource("data")
+    public void testInvalidIs01Bean(boolean useStandalone) {
+        doTest(useStandalone, InvalidIs01Bean.class, "value", TypeA.class, TypeA.class, TypeA.class);
+    }
+
+    @ParameterizedTest
+    @MethodSource("data")
+    public void testInvalidIs02Bean(boolean useStandalone) {
+        doTest(useStandalone, InvalidIs02Bean.class, "value", TypeA.class, null, TypeA.class);
+    }
+
+    @ParameterizedTest
+    @MethodSource("data")
+    public void testInvalidIs03Bean(boolean useStandalone) {
+        doTest(useStandalone, InvalidIs03Bean.class, "value", TypeA.class, null, TypeA.class);
+    }
+
+    @ParameterizedTest
+    @MethodSource("data")
+    public void testReadOnlyBean(boolean useStandalone) {
+        doTest(useStandalone, ReadOnlyBean.class, "value", TypeA.class, TypeA.class, null);
+    }
+
+    @ParameterizedTest
+    @MethodSource("data")
+    public void testWriteOnlyBean(boolean useStandalone) {
+        doTest(useStandalone, WriteOnlyBean.class, "value", TypeA.class, null, TypeA.class);
+    }
+
+    @ParameterizedTest
+    @MethodSource("data")
+    public void testOverLoadedWithGetABean(boolean useStandalone) {
+        doTest(useStandalone, OverLoadedWithGetABean.class, "value", TypeA.class, TypeA.class, TypeAAA.class);
+    }
+
+    @ParameterizedTest
+    @MethodSource("data")
+    public void testOverLoadedWithGetAABean(boolean useStandalone) {
+        doTest(useStandalone, OverLoadedWithGetAABean.class, "value", TypeAA.class, TypeAA.class, TypeAAA.class);
+    }
+
+    @ParameterizedTest
+    @MethodSource("data")
+    public void testOverLoadedWithGetAAABean(boolean useStandalone) {
+        doTest(useStandalone, OverLoadedWithGetAAABean.class, "value", TypeAAA.class, TypeAAA.class, TypeAAA.class);
+    }
+
+    @ParameterizedTest
+    @MethodSource("data")
+    public void testMismatchBean(boolean useStandalone) {
+        doTest(useStandalone, MismatchBean.class, "value", TypeA.class, TypeA.class, null);
+    }
+
+    /*
+     * The first setter found "wins".
+     */
+    @ParameterizedTest
+    @MethodSource("data")
+    public void testAmbiguousBean(boolean useStandalone) {
+        doTest(useStandalone, AmbiguousBean.class, "value", TypeA.class, null, TypeA.class);
+    }
+
+
+    private void doTest(boolean useStandalone, Class<?> clazz, String propertyName, Class<?> type, Class<?> typeGet,
+            Class<?> typeSet) {
+        configureBeanSupport(useStandalone);
+        BeanProperties beanProperties = BeanSupport.getInstance().getBeanProperties(clazz);
+        BeanProperty beanProperty = beanProperties.propertyMap.get(propertyName);
+
+        Assertions.assertNotNull(beanProperty);
+        Assertions.assertEquals(type, beanProperty.getPropertyType());
+
+        if (typeGet == null) {
+            Assertions.assertNull(beanProperty.getReadMethod());
+        } else {
+            Assertions.assertEquals(0, beanProperty.getReadMethod().getParameterCount());
+            Assertions.assertEquals(typeGet, beanProperty.getReadMethod().getReturnType());
+        }
+
+        if (typeSet == null) {
+            Assertions.assertNull(beanProperty.getWriteMethod());
+        } else {
+            Assertions.assertEquals(void.class, beanProperty.getWriteMethod().getReturnType());
+            Assertions.assertEquals(1, beanProperty.getWriteMethod().getParameterCount());
+            Assertions.assertEquals(typeSet, beanProperty.getWriteMethod().getParameterTypes()[0]);
+        }
+    }
+
+
+    public static class SimpleBean {
+        public TypeA getValue() {
+            return null;
+        }
+
+        public void setValue(@SuppressWarnings("unused") TypeA value) {
+        }
+    }
+
+
+    public static class InvalidIs01Bean {
+        public TypeA isValue() {
+            return null;
+        }
+
+        public TypeA getValue() {
+            return null;
+        }
+
+        public void setValue(@SuppressWarnings("unused") TypeA value) {
+        }
+    }
+
+
+    public static class InvalidIs02Bean {
+        public TypeA isValue() {
+            return null;
+        }
+
+        public void setValue(@SuppressWarnings("unused") TypeA value) {
+        }
+    }
+
+
+    public static class InvalidIs03Bean {
+        public Boolean isValue() {
+            return null;
+        }
+
+        public void setValue(@SuppressWarnings("unused") TypeA value) {
+        }
+    }
+
+
+    public static class ReadOnlyBean {
+        public TypeA getValue() {
+            return null;
+        }
+    }
+
+
+    public static class WriteOnlyBean {
+        public void setValue(@SuppressWarnings("unused") TypeA value) {
+        }
+    }
+
+
+    public static class OverLoadedWithGetABean {
+        public TypeA getValue() {
+            return null;
+        }
+
+        public void setValue(@SuppressWarnings("unused") TypeA value) {
+        }
+
+        public void setValue(@SuppressWarnings("unused") TypeAA value) {
+        }
+
+        public void setValue(@SuppressWarnings("unused") TypeAAA value) {
+        }
+    }
+
+
+    public static class OverLoadedWithGetAABean {
+        public TypeAA getValue() {
+            return null;
+        }
+
+        public void setValue(@SuppressWarnings("unused") TypeA value) {
+        }
+
+        public void setValue(@SuppressWarnings("unused") TypeAA value) {
+        }
+
+        public void setValue(@SuppressWarnings("unused") TypeAAA value) {
+        }
+    }
+
+
+    public static class OverLoadedWithGetAAABean {
+        public TypeAAA getValue() {
+            return null;
+        }
+
+        public void setValue(@SuppressWarnings("unused") TypeA value) {
+        }
+
+        public void setValue(@SuppressWarnings("unused") TypeAA value) {
+        }
+
+        public void setValue(@SuppressWarnings("unused") TypeAAA value) {
+        }
+    }
+
+
+    public static class MismatchBean {
+        public TypeA getValue() {
+            return null;
+        }
+
+        public void setValue(@SuppressWarnings("unused") String value) {
+        }
+    }
+
+
+    public static class AmbiguousBean {
+        public void setValue(@SuppressWarnings("unused") TypeA value) {
+        }
+
+        public void setValue(@SuppressWarnings("unused") String value) {
+        }
+    }
+
+
+    public static class TypeA {
+    }
+
+
+    public static class TypeAA extends TypeA {
+    }
+
+
+    public static class TypeAAA extends TypeAA {
+    }
+}
diff --git a/api/src/test/java/jakarta/el/TestELUtil.java b/api/src/test/java/jakarta/el/TestELUtil.java
index a7db81b..7b8f148 100644
--- a/api/src/test/java/jakarta/el/TestELUtil.java
+++ b/api/src/test/java/jakarta/el/TestELUtil.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2022 Contributors to the Eclipse Foundation
+ * Copyright (c) 2023 Contributors to the Eclipse Foundation
  * 
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -18,15 +18,18 @@
 import java.lang.reflect.Method;
 import java.util.TimeZone;
 
-import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
 
-public class TestELUtil {
+public class TestELUtil extends ELBaseTest {
 
     /*
      * https://github.com/jakartaee/expression-language/issues/188
      */
-    @Test
-    public void testAccessibleMethod() throws Exception {
+    @ParameterizedTest
+    @MethodSource("data")
+    public void testAccessibleMethod(boolean useStandalone) throws Exception {
+        configureBeanSupport(useStandalone);
         TimeZone tz = TimeZone.getDefault();
         Method m = ELUtil.findMethod(tz.getClass(), tz, "getRawOffset", null, null);
         m.invoke(tz);