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]