On 02/11/2026 17:13, markt.apache.org via dev wrote:
This is an automated email from the ASF dual-hosted git repository.

markt-asf pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/main by this push:
      new 1b2785c63c Fix access to Bean like properties on a Record
1b2785c63c is described below

commit 1b2785c63ca0e0f9a242745b16f26924b5488ddd
Author: Mark Thomas <[email protected]>
AuthorDate: Mon Nov 2 17:12:13 2026 +0000

     Fix access to Bean like properties on a Record

This will need back-porting but I am going to delay it until the spec project has agreed the fix.

Mark


---
  java/jakarta/el/RecordELResolver.java     | 93 +++++++++++++++++--------------
  test/jakarta/el/TestRecordELResolver.java | 17 ++++++
  test/jakarta/el/TesterBeanC.java          | 22 ++++++++
  test/jakarta/el/TesterBeanCImpl.java      | 25 +++++++++
  webapps/docs/changelog.xml                |  5 ++
  5 files changed, 120 insertions(+), 42 deletions(-)

diff --git a/java/jakarta/el/RecordELResolver.java 
b/java/jakarta/el/RecordELResolver.java
index 01b4dc6792..862f9e3b02 100644
--- a/java/jakarta/el/RecordELResolver.java
+++ b/java/jakarta/el/RecordELResolver.java
@@ -43,11 +43,17 @@ public class RecordELResolver extends ELResolver {
      }
/**
-     * If the base object is an instance of {@link Record}, returns the value 
of the given field of this {@link Record}.
+     * If the base object is an instance of {@link Record} and a no-arg method 
exists with the name of the property,
+     * returns the value of the given property of this {@link Record}.
       * <p>
-     * If the base object is an instance of {@link Record}, the {@code 
propertyResolved} property of the provided
-     * {@link ELContext} must be set to {@code true} by this resolver before 
returning. If this property is not {@code
-     * true} after this method is called, the caller should ignore the return 
value.
+     * If the base object is an instance of {@link Record} and no-arg method 
exists with the name of the property, the
+     * {@code propertyResolved} property of the provided {@link ELContext} 
must be set to {@code true} by this resolver
+     * before returning. If this property is not {@code true} after this 
method is called, the caller should ignore the
+     * return value.
+     * <p>
+     * The {@link Record} may include properties using JavaBean naming 
conventions for which no-arg methods do not
+     * exist. Such properties will not be discovered by this resolver. In 
typical usage, such properties would be
+     * resolved by the next resolver in the chain, the {@code BeanELResolver}.
       *
       * @param context  The context of this evaluation.
       * @param base     The {@link Record} on which to get the property.
@@ -57,8 +63,6 @@ public class RecordELResolver extends ELResolver {
       *             the value of the given property. Otherwise, undefined.
       *
       * @throws NullPointerException      if the provided {@link ELContext} is 
{@code null}.
-     * @throws PropertyNotFoundException if the {@code base} is an instance of 
{@link Record} and the specified property
-     *                                       does not exist.
       * @throws ELException               if an exception was throws while 
performing the property resolution. The thrown
       *                                       exception must be included as 
the cause of this exception, if available.
       */
@@ -67,18 +71,17 @@ public class RecordELResolver extends ELResolver {
          Objects.requireNonNull(context);
if (base instanceof Record && property != null) {
-            context.setPropertyResolved(base, property);
-
              String propertyName = property.toString();
Method method;
              try {
                  method = base.getClass().getMethod(propertyName);
              } catch (NoSuchMethodException nsme) {
-                throw new PropertyNotFoundException(
-                        Util.message(context, "propertyNotFound", 
base.getClass().getName(), propertyName), nsme);
+                return null;
              }
+ context.setPropertyResolved(base, property);
+
              try {
                  return method.invoke(base);
              } catch (ReflectiveOperationException e) {
@@ -91,12 +94,17 @@ public class RecordELResolver extends ELResolver {
/**
-     * If the base object is an instance of {@link Record}, always returns 
{@code null} since {@link Record}s are always
-     * read-only.
+     * If the base object is an instance of {@link Record} and a no-arg method 
exists with the name of the property,
+     * returns {@code null} since {@link Record}s are always read-only.
       * <p>
-     * If the base object is an instance of {@link Record}, the {@code 
propertyResolved} property of the provided
-     * {@link ELContext} must be set to {@code true} by this resolver before 
returning. If this property is not {@code
-     * true} after this method is called, the caller should ignore the return 
value.
+     * If the base object is an instance of {@link Record} and no-arg method 
exists with the name of the property, the
+     * {@code propertyResolved} property of the provided {@link ELContext} 
must be set to {@code true} by this resolver
+     * before returning. If this property is not {@code true} after this 
method is called, the caller should ignore the
+     * return value.
+     * <p>
+     * The {@link Record} may include properties using JavaBean naming 
conventions for which no-arg methods do not
+     * exist. Such properties will not be discovered by this resolver. In 
typical usage, such properties would be
+     * resolved by the next resolver in the chain, the {@code BeanELResolver}.
       *
       * @param context  The context of this evaluation.
       * @param base     The {@link Record} to analyze.
@@ -105,43 +113,43 @@ public class RecordELResolver extends ELResolver {
       * @return Always {@code null}
       *
       * @throws NullPointerException      if the provided {@link ELContext} is 
{@code null}.
-     * @throws PropertyNotFoundException if the {@code base} is an instance of 
{@link Record} and the specified property
-     *                                       does not exist.
       */
      @Override
      public Class<?> getType(ELContext context, Object base, Object property) {
          Objects.requireNonNull(context);
          if (base instanceof Record && property != null) {
-            context.setPropertyResolved(base, property);
-
              String propertyName = property.toString();
try {
                  base.getClass().getMethod(propertyName);
              } catch (NoSuchMethodException nsme) {
-                throw new PropertyNotFoundException(
-                        Util.message(context, "propertyNotFound", 
base.getClass().getName(), propertyName), nsme);
+                return null;
              }
+
+            context.setPropertyResolved(base, property);
          }
          return null;
      }
/**
-     * If the base object is an instance of {@link Record}, always throws an 
exception since {@link Record}s are
-     * read-only.
+     * If the base object is an instance of {@link Record} and a no-arg method 
exists with the name of the property,
+     * throws an exception since {@link Record}s are read-only.
       * <p>
-     * If the base object is an instance of {@link Record}, the {@code 
propertyResolved} property of the provided
-     * {@link ELContext} must be set to {@code true} by this resolver before 
returning. If this property is not {@code
-     * true} after this method is called, the caller should ignore the return 
value.
+     * If the base object is an instance of {@link Record} and no-arg method 
exists with the name of the property, the
+     * {@code propertyResolved} property of the provided {@link ELContext} 
must be set to {@code true} by this resolver
+     * before returning. If this property is not {@code true} after this 
method is called, the caller should ignore the
+     * return value.
+     * <p>
+     * The {@link Record} may include properties using JavaBean naming 
conventions for which no-arg methods do not
+     * exist. Such properties will not be discovered by this resolver. In 
typical usage, such properties would be
+     * resolved by the next resolver in the chain, the {@code BeanELResolver}.
       *
       * @param context  The context of this evaluation.
       * @param base     The {@link Record} on which to set the property.
       * @param property The name of the property to set. Will be coerced to a 
String.
       *
       * @throws NullPointerException         if the provided {@link ELContext} 
is {@code null}.
-     * @throws PropertyNotFoundException    if the {@code base} is an instance 
of {@link Record} and the specified
-     *                                          property does not exist.
       * @throws PropertyNotWritableException if the {@code base} is an 
instance of {@link Record} and the specified
       *                                          property exists.
       */
@@ -149,17 +157,16 @@ public class RecordELResolver extends ELResolver {
      public void setValue(ELContext context, Object base, Object property, 
Object value) {
          Objects.requireNonNull(context);
          if (base instanceof Record && property != null) {
-            context.setPropertyResolved(base, property);
-
              String propertyName = property.toString();
try {
                  base.getClass().getMethod(propertyName);
              } catch (NoSuchMethodException nsme) {
-                throw new PropertyNotFoundException(
-                        Util.message(context, "propertyNotFound", 
base.getClass().getName(), propertyName), nsme);
+                return;
              }
+ context.setPropertyResolved(base, property);
+
              throw new PropertyNotWritableException(
                      Util.message(context, "resolverNotWritable", 
base.getClass().getName()));
          }
@@ -167,35 +174,37 @@ public class RecordELResolver extends ELResolver {
/**
-     * If the base object is an instance of {@link Record}, always returns 
{@code true}.
+     * If the base object is an instance of {@link Record} and a no-arg method 
exists with the name of the property,
+     * returns {@code true}.
       * <p>
-     * If the base object is an instance of {@link Record}, the {@code 
propertyResolved} property of the provided
-     * {@link ELContext} must be set to {@code true} by this resolver before 
returning. If this property is not {@code
-     * true} after this method is called, the caller should ignore the return 
value.
+     * If the base object is an instance of {@link Record} and no-arg method 
exists with the name of the property, the
+     * {@code propertyResolved} property of the provided {@link ELContext} 
must be set to {@code true} by this resolver
+     * before returning. If this property is not {@code true} after this 
method is called, the caller should ignore the
+     * return value.
+     * <p>
+     * The {@link Record} may include properties using JavaBean naming 
conventions for which no-arg methods do not
+     * exist. Such properties will not be discovered by this resolver. In 
typical usage, such properties would be
+     * resolved by the next resolver in the chain, the {@code BeanELResolver}.
       *
       * @param context  The context of this evaluation.
       * @param base     The {@link Record} to analyze.
       * @param property The name of the property to analyze. Will be coerced 
to a String.
       *
       * @throws NullPointerException      if the provided {@link ELContext} is 
{@code null}.
-     * @throws PropertyNotFoundException if the {@code base} is an instance of 
{@link Record} and the specified property
-     *                                       does not exist.
       */
      @Override
      public boolean isReadOnly(ELContext context, Object base, Object 
property) {
          Objects.requireNonNull(context);
          if (base instanceof Record && property != null) {
-            context.setPropertyResolved(base, property);
-
              String propertyName = property.toString();
try {
                  base.getClass().getMethod(propertyName);
              } catch (NoSuchMethodException nsme) {
-                throw new PropertyNotFoundException(
-                        Util.message(context, "propertyNotFound", 
base.getClass().getName(), propertyName), nsme);
+                return false;
              }
+ context.setPropertyResolved(base, property);
              return true;
          }
          return false;
diff --git a/test/jakarta/el/TestRecordELResolver.java 
b/test/jakarta/el/TestRecordELResolver.java
index 22151eb8b5..d5c65dbeae 100644
--- a/test/jakarta/el/TestRecordELResolver.java
+++ b/test/jakarta/el/TestRecordELResolver.java
@@ -72,4 +72,21 @@ public class TestRecordELResolver {
Assert.assertEquals(LONG_DATA, result.longValue());
      }
+
+
+    @Test
+    public void testRecordFallBackToBean() {
+        ExpressionFactory factory = ExpressionFactory.newInstance();
+        StandardELContext context = new StandardELContext(factory);
+
+        TesterBeanCImpl beanC = new TesterBeanCImpl();
+
+        ValueExpression varBeanC = factory.createValueExpression(beanC, 
TesterBeanCImpl.class);
+        context.getVariableMapper().setVariable("beanC", varBeanC);
+
+        ValueExpression ve = factory.createValueExpression(context, 
"${beanC.valid}", Boolean.class);
+        Boolean result = ve.getValue(context);
+
+        Assert.assertEquals(Boolean.TRUE, result);
+    }
  }
diff --git a/test/jakarta/el/TesterBeanC.java b/test/jakarta/el/TesterBeanC.java
new file mode 100644
index 0000000000..4ba7675e82
--- /dev/null
+++ b/test/jakarta/el/TesterBeanC.java
@@ -0,0 +1,22 @@
+/*
+ * 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 jakarta.el;
+
+public interface TesterBeanC {
+
+    boolean isValid();
+}
diff --git a/test/jakarta/el/TesterBeanCImpl.java 
b/test/jakarta/el/TesterBeanCImpl.java
new file mode 100644
index 0000000000..d5c41793fe
--- /dev/null
+++ b/test/jakarta/el/TesterBeanCImpl.java
@@ -0,0 +1,25 @@
+/*
+ * 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 jakarta.el;
+
+public record TesterBeanCImpl() implements TesterBeanC {
+
+    @Override
+    public boolean isValid() {
+        return true;
+    }
+}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 94fda8a5ca..09fea371bc 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -819,6 +819,11 @@
          keeps the parameters of each tag to itself and never touches page
          scope attributes. (remm)
        </fix>
+      <fix>
+        <code>RecordELResolver</code> no longer prevents
+        <code>BeanELResolver</code> from resolving bean-style properties (such
+        as <code>isValid()</code>) on a <code>Record</code>. (markt)
+      </fix>
      </changelog>
    </subsection>
    <subsection name="Cluster">


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]



---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to