Fix #168

Explicitly document that ELResolver.getType() is expected to return null
if the property is resolved and either the property or the resolver is
read-only.
diff --git a/api/src/main/java/jakarta/el/ArrayELResolver.java b/api/src/main/java/jakarta/el/ArrayELResolver.java
index 3b32377..a2596ff 100644
--- a/api/src/main/java/jakarta/el/ArrayELResolver.java
+++ b/api/src/main/java/jakarta/el/ArrayELResolver.java
@@ -73,9 +73,9 @@
      * </p>
      *
      * <p>
-     * Assuming the base is an <code>array</code>, this method will always return
-     * <code>base.getClass().getComponentType()</code>, which is the most general type of component that can be stored at
-     * any given index in the array.
+     * Assuming the base is an <code>array</code> and that this resolver was not constructed in read-only mode, this
+     * method will return <code>base.getClass().getComponentType()</code>, which is the most general type of component
+     * that can be stored at any given index in the array.
      * </p>
      *
      * @param context The context of this evaluation.
@@ -83,7 +83,8 @@
      * @param property The index of the element in the array to return the acceptable type for. Will be coerced into an
      * integer, but otherwise ignored by this resolver.
      * @return If the <code>propertyResolved</code> property of <code>ELContext</code> was set to <code>true</code>, then
-     * the most general acceptable type; otherwise undefined.
+     * the most general acceptable type which must be {@code null} if the either the property or the resolver is
+     * read-only; otherwise undefined
      * @throws PropertyNotFoundException if the given index is out of bounds for this array.
      * @throws NullPointerException if context is <code>null</code>
      * @throws ELException if an exception was thrown while performing the property or variable resolution. The thrown
@@ -102,6 +103,15 @@
             if (index < 0 || index >= Array.getLength(base)) {
                 throw new PropertyNotFoundException();
             }
+
+            /*
+             * The resolver may have been created in read-only mode but the
+             * array and its elements will always be read-write.
+             */
+            if (isReadOnly) {
+                return null;
+            }
+
             return base.getClass().getComponentType();
         }
         return null;
diff --git a/api/src/main/java/jakarta/el/BeanELResolver.java b/api/src/main/java/jakarta/el/BeanELResolver.java
index 5905007..d69163d 100644
--- a/api/src/main/java/jakarta/el/BeanELResolver.java
+++ b/api/src/main/java/jakarta/el/BeanELResolver.java
@@ -253,16 +253,19 @@
      * </p>
      *
      * <p>
-     * The provided property will first be coerced to a <code>String</code>. If there is a <code>BeanInfoProperty</code> for
-     * this property and there were no errors retrieving it, the <code>propertyType</code> of the
-     * <code>propertyDescriptor</code> is returned. Otherwise, a <code>PropertyNotFoundException</code> is thrown.
+     * The provided property will first be coerced to a <code>String</code>. If there is a <code>BeanInfoProperty</code>
+     * for this property, there were no errors retrieving it and neither the property nor the resolver are read-only,
+     * the <code>propertyType</code> of the <code>propertyDescriptor</code> is returned. If the property is resolved but
+     * either the property or the resolver is read-only then {@code null} will be returned. Otherwise, a
+     * <code>PropertyNotFoundException</code> is thrown.
      * </p>
      *
      * @param context The context of this evaluation.
      * @param base The bean to analyze.
      * @param property The name of the property to analyze. Will be coerced to a <code>String</code>.
      * @return If the <code>propertyResolved</code> property of <code>ELContext</code> was set to <code>true</code>, then
-     * the most general acceptable type; otherwise undefined.
+     * the most general acceptable type which must be {@code null} if the either the property or the resolver is
+     * read-only; otherwise undefined
      * @throws NullPointerException if context is <code>null</code>
      * @throws PropertyNotFoundException if <code>base</code> is not <code>null</code> and the specified property does not
      * exist or is not readable.
@@ -281,6 +284,11 @@
 
         BeanProperty beanProperty = getBeanProperty(context, base, property);
         context.setPropertyResolved(true);
+        
+        if (isReadOnly || beanProperty.isReadOnly()) {
+            return null;
+        }
+        
         return beanProperty.getPropertyType();
     }
 
diff --git a/api/src/main/java/jakarta/el/BeanNameELResolver.java b/api/src/main/java/jakarta/el/BeanNameELResolver.java
index 31cc068..8b80761 100644
--- a/api/src/main/java/jakarta/el/BeanNameELResolver.java
+++ b/api/src/main/java/jakarta/el/BeanNameELResolver.java
@@ -138,8 +138,9 @@
      * @param context The context of this evaluation.
      * @param base <code>null</code>
      * @param property The name of the bean.
-     * @return If the <code>propertyResolved</code> property of <code>ELContext</code> was set to <code>true</code>, then
-     * the type of the bean with the given name. Otherwise, undefined.
+     * @return If the <code>propertyResolved</code> property of <code>ELContext</code> was set to <code>true</code> and
+     * the associated BeanNameResolver is not read-only then the type of the bean with the given name. If the given
+     * bean name was resolved but the associated BeanNameResolver is read-only then {@code null}. Otherwise, undefined.
      * @throws NullPointerException if context is <code>null</code>.
      * @throws ELException if an exception was thrown while performing the property or variable resolution. The thrown
      * exception must be included as the cause property of this exception, if available.
@@ -153,6 +154,14 @@
         if (base == null && property instanceof String) {
             if (beanNameResolver.isNameResolved((String) property)) {
                 context.setPropertyResolved(true);
+                
+                /*
+                 * No resolver level isReadOnly property for this resolver
+                 */
+                if (beanNameResolver.isReadOnly((String) property)) {
+                    return null;
+                }
+                
                 return beanNameResolver.getBean((String) property).getClass();
             }
         }
diff --git a/api/src/main/java/jakarta/el/CompositeELResolver.java b/api/src/main/java/jakarta/el/CompositeELResolver.java
index a805e7e..f87d378 100644
--- a/api/src/main/java/jakarta/el/CompositeELResolver.java
+++ b/api/src/main/java/jakarta/el/CompositeELResolver.java
@@ -245,7 +245,8 @@
      * variable.
      * @param property The property or variable to return the acceptable type for.
      * @return If the <code>propertyResolved</code> property of <code>ELContext</code> was set to <code>true</code>, then
-     * the most general acceptable type; otherwise undefined.
+     * the most general acceptable type which must be {@code null} if the either the property or the resolver is
+     * read-only; otherwise undefined
      * @throws NullPointerException if context is <code>null</code>
      * @throws PropertyNotFoundException if the given (base, property) pair is handled by this <code>ELResolver</code> but
      * the specified variable or property does not exist or is not readable.
diff --git a/api/src/main/java/jakarta/el/ELResolver.java b/api/src/main/java/jakarta/el/ELResolver.java
index 8fa83f0..e951039 100644
--- a/api/src/main/java/jakarta/el/ELResolver.java
+++ b/api/src/main/java/jakarta/el/ELResolver.java
@@ -181,12 +181,17 @@
      * superclass of the type of the actual element that is currently in the specified array element.
      * </p>
      *
+     * <p>
+     * If the resolver or the property is read-only, this method must return {@code null}.
+     * </p>
+     * 
      * @param context The context of this evaluation.
      * @param base The base object whose property value is to be analyzed, or <code>null</code> to analyze a top-level
      * variable.
      * @param property The property or variable to return the acceptable type for.
-     * @return If the <code>propertyResolved</code> property of <code>ELContext</code> was set to <code>true</code>, then
-     * the most general acceptable type; otherwise undefined.
+     * @return If the <code>propertyResolved</code> property of <code>ELContext</code> was set to <code>true</code>,
+     * the most general acceptable type which must be {@code null} if the either the property or the resolver is
+     * read-only; otherwise undefined
      * @throws PropertyNotFoundException if the given (base, property) pair is handled by this <code>ELResolver</code> but
      * the specified variable or property does not exist or is not readable.
      * @throws ELException if an exception was thrown while performing the property or variable resolution. The thrown
diff --git a/api/src/main/java/jakarta/el/ListELResolver.java b/api/src/main/java/jakarta/el/ListELResolver.java
index 8920c3d..ce3edea 100644
--- a/api/src/main/java/jakarta/el/ListELResolver.java
+++ b/api/src/main/java/jakarta/el/ListELResolver.java
@@ -78,8 +78,9 @@
      * </p>
      *
      * <p>
-     * Assuming the base is a <code>List</code>, this method will always return <code>Object.class</code>. This is because
-     * <code>List</code>s accept any object as an element.
+     * Assuming the base is a <code>List</code>, this method will return <code>Object.class</code> unless the resolver
+     * is constructed in read-only mode in which case {@code null} will be returned. This is because <code>List</code>s
+     * accept any object as an element.
      * </p>
      *
      * @param context The context of this evaluation.
@@ -87,7 +88,8 @@
      * @param property The index of the element in the list to return the acceptable type for. Will be coerced into an
      * integer, but otherwise ignored by this resolver.
      * @return If the <code>propertyResolved</code> property of <code>ELContext</code> was set to <code>true</code>, then
-     * the most general acceptable type; otherwise undefined.
+     * the most general acceptable type which must be {@code null} if the either the property or the resolver is
+     * read-only; otherwise undefined
      * @throws PropertyNotFoundException if the given index is out of bounds for this list.
      * @throws NullPointerException if context is <code>null</code>
      * @throws ELException if an exception was thrown while performing the property or variable resolution. The thrown
@@ -107,6 +109,14 @@
                 throw new PropertyNotFoundException();
             }
 
+            /*
+             * Not perfect as a custom list implementation may be read-only but
+             * consistent with isReadOnly().
+             */
+            if (list.getClass() == theUnmodifiableListClass || isReadOnly) {
+                return null;
+            }
+            
             return Object.class;
         }
 
diff --git a/api/src/main/java/jakarta/el/MapELResolver.java b/api/src/main/java/jakarta/el/MapELResolver.java
index cacd175..870f827 100644
--- a/api/src/main/java/jakarta/el/MapELResolver.java
+++ b/api/src/main/java/jakarta/el/MapELResolver.java
@@ -82,7 +82,8 @@
      * </p>
      *
      * <p>
-     * Assuming the base is a <code>Map</code>, this method will always return <code>Object.class</code>. This is because
+     * Assuming the base is a <code>Map</code>, this method will always return <code>Object.class</code> unless the
+     * resolver is constructed in read-only mode in which case {@code null} will be returned. This is because
      * <code>Map</code>s accept any object as the value for a given key.
      * </p>
      *
@@ -90,7 +91,8 @@
      * @param base The map to analyze. Only bases of type <code>Map</code> are handled by this resolver.
      * @param property The key to return the acceptable type for. Ignored by this resolver.
      * @return If the <code>propertyResolved</code> property of <code>ELContext</code> was set to <code>true</code>, then
-     * the most general acceptable type; otherwise undefined.
+     * the most general acceptable type which must be {@code null} if the either the property or the resolver is
+     * read-only; otherwise undefined
      * @throws NullPointerException if context is <code>null</code>
      * @throws ELException if an exception was thrown while performing the property or variable resolution. The thrown
      * exception must be included as the cause property of this exception, if available.
@@ -103,6 +105,12 @@
 
         if (base != null && base instanceof Map) {
             context.setPropertyResolved(true);
+            
+            Map<?, ?> map = (Map<?, ?>) base;
+            if (isReadOnly || map.getClass() == theUnmodifiableMapClass) {
+                return null;
+            }
+
             return Object.class;
         }
 
diff --git a/api/src/main/java/jakarta/el/ResourceBundleELResolver.java b/api/src/main/java/jakarta/el/ResourceBundleELResolver.java
index e8fb0be..5e066dd 100644
--- a/api/src/main/java/jakarta/el/ResourceBundleELResolver.java
+++ b/api/src/main/java/jakarta/el/ResourceBundleELResolver.java
@@ -117,6 +117,10 @@
 
         if (base instanceof ResourceBundle) {
             context.setPropertyResolved(true);
+            /*
+             * ResourceBundles are always read-only so fall-through to return
+             * null
+             */
         }
 
         return null;
diff --git a/api/src/main/java/jakarta/el/StaticFieldELResolver.java b/api/src/main/java/jakarta/el/StaticFieldELResolver.java
index c498a50..e64f40b 100644
--- a/api/src/main/java/jakarta/el/StaticFieldELResolver.java
+++ b/api/src/main/java/jakarta/el/StaticFieldELResolver.java
@@ -188,14 +188,11 @@
      * resolver, before returning. If this property is not <code>true</code> after this method is called, the caller can
      * safely assume no value has been set.
      *
-     * <p>
-     * If the property string is a public static field of class specified in ELClass, return the type of the static field.
-     *
      * @param context The context of this evaluation.
      * @param base An <code>ELClass</code>.
      * @param property The name of the field.
      * @return If the <code>propertyResolved</code> property of <code>ELContext</code> was set to <code>true</code>, then
-     * the type of the type of the field.
+     * <code>null</code>; otherwise undefined.
      * @throws NullPointerException if context is <code>null</code>.
      * @throws PropertyNotFoundException if field is not a public static filed of the class, or if the field is
      * inaccessible.
@@ -211,15 +208,16 @@
             String fieldName = (String) property;
             try {
                 context.setPropertyResolved(true);
-                Field field = klass.getField(fieldName);
-
-                int mod = field.getModifiers();
-                if (isPublic(mod) && isStatic(mod)) {
-                    return field.getType();
-                }
+                
+                klass.getField(fieldName);
+                
+                /*
+                 * This resolver is always read-only so fall-through to return
+                 * null.
+                 */
             } catch (NoSuchFieldException ex) {
+                throw new PropertyNotFoundException(getExceptionMessageString(context, "staticFieldReadError", new Object[] { klass.getName(), fieldName }));
             }
-            throw new PropertyNotFoundException(getExceptionMessageString(context, "staticFieldReadError", new Object[] { klass.getName(), fieldName }));
         }
 
         return null;