On Mon, Nov 2, 2026 at 6:47 PM Mark Thomas via dev
<[email protected]> wrote:
>
> 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.

I like records a lot, until I have to backport the code.

Rémy

> 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]
>

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

Reply via email to