This is an automated email from the ASF dual-hosted git repository.

markt 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 b7186591a7 Add support to EL for Records
b7186591a7 is described below

commit b7186591a7364d6493b8ad093432cfbf2c52b1c0
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Mon Oct 9 15:40:12 2023 -0300

    Add support to EL for Records
---
 java/jakarta/el/RecordELResolver.java           | 220 ++++++++++++++++++++++++
 java/jakarta/el/StandardELContext.java          |   1 +
 java/org/apache/jasper/el/ELContextImpl.java    |   2 +
 java/org/apache/jasper/el/JasperELResolver.java |   2 +
 test/jakarta/el/TestRecordELResolver.java       |  75 ++++++++
 test/jakarta/el/TesterRecordA.java              |  20 +++
 webapps/docs/changelog.xml                      |   7 +
 7 files changed, 327 insertions(+)

diff --git a/java/jakarta/el/RecordELResolver.java 
b/java/jakarta/el/RecordELResolver.java
new file mode 100644
index 0000000000..b12bd66bd0
--- /dev/null
+++ b/java/jakarta/el/RecordELResolver.java
@@ -0,0 +1,220 @@
+/*
+ * 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;
+
+import java.lang.reflect.Method;
+import java.util.Objects;
+
+/**
+ * Defines property resolution behavior on instances of {@link Record}.
+ * <p>
+ * The resolver handles base objects of type {@link Record}. It accepts any 
object as a property and coerces it to a
+ * String using {@code String#valueOf(Object)}. The property string is used to 
find find an accessor method for a field
+ * with the same name.
+ * <p>
+ * This resolver is always read-only since {@link Record}s are always 
read-only.
+ * <p>
+ * {@code ELResolver}s are combined together using {@link 
CompositeELResolver}s to define rich semantics for evaluating
+ * an expression. See the javadocs for {@link ELResolver} for details.
+ */
+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}.
+     * <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.
+     *
+     * @param context  The context of this evaluation.
+     * @param base     The {@link Record} on which to get the property.
+     * @param property The property to get. Will be coerced to a String.
+     *
+     * @return If the {@code propertyResolved} property of the provided {@link 
ELContext} was set to {@code true} then
+     *             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.
+     */
+    @Override
+    public Object getValue(ELContext context, Object base, Object property) {
+        Objects.requireNonNull(context);
+
+        if (base instanceof Record) {
+            context.setPropertyResolved(base, property);
+
+            String propertyName = String.valueOf(property);
+
+            Method method;
+            try {
+                method = base.getClass().getMethod(propertyName);
+            } catch (NoSuchMethodException nsme) {
+                throw new PropertyNotFoundException(
+                        Util.message(context, "propertyNotFound", 
base.getClass().getName(), property.toString()),
+                        nsme);
+            }
+
+            try {
+                return method.invoke(base);
+            } catch (ReflectiveOperationException e) {
+                throw new ELException(
+                        Util.message(context, "propertyReadError", 
base.getClass().getName(), property.toString()), e);
+            }
+        }
+        return null;
+    }
+
+
+    /**
+     * If the base object is an instance of {@link Record}, always 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.
+     *
+     * @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.
+     *
+     * @return Always {@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) {
+            context.setPropertyResolved(base, property);
+
+            String propertyName = String.valueOf(property);
+
+            try {
+                base.getClass().getMethod(propertyName);
+            } catch (NoSuchMethodException nsme) {
+                throw new PropertyNotFoundException(
+                        Util.message(context, "propertyNotFound", 
base.getClass().getName(), property.toString()),
+                        nsme);
+            }
+        }
+        return null;
+    }
+
+
+    /**
+     * If the base object is an instance of {@link Record}, always 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.
+     *
+     * @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.
+     */
+    @Override
+    public void setValue(ELContext context, Object base, Object property, 
Object value) {
+        Objects.requireNonNull(context);
+        if (base instanceof Record) {
+            context.setPropertyResolved(base, property);
+
+            String propertyName = String.valueOf(property);
+
+            try {
+                base.getClass().getMethod(propertyName);
+            } catch (NoSuchMethodException nsme) {
+                throw new PropertyNotFoundException(
+                        Util.message(context, "propertyNotFound", 
base.getClass().getName(), property.toString()),
+                        nsme);
+            }
+
+            throw new PropertyNotWritableException(
+                    Util.message(context, "resolverNotWritable", 
base.getClass().getName()));
+        }
+    }
+
+
+    /**
+     * If the base object is an instance of {@link Record}, always 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.
+     *
+     * @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) {
+            context.setPropertyResolved(base, property);
+
+            String propertyName = String.valueOf(property);
+
+            try {
+                base.getClass().getMethod(propertyName);
+            } catch (NoSuchMethodException nsme) {
+                throw new PropertyNotFoundException(
+                        Util.message(context, "propertyNotFound", 
base.getClass().getName(), property.toString()),
+                        nsme);
+            }
+
+            return true;
+        }
+        return false;
+    }
+
+
+    /**
+     * If the base object is an instance of {@link Record}, returns the most 
general type this resolver accepts for the
+     * {@code property} argument. Otherwise, returns {@code null}.
+     * <p>
+     * If the base object is an instance of {@link Record} this method will 
always return {@link Object} since any
+     * object is accepted for the property argument and coerced to a String.
+     *
+     * @param context The context of this evaluation.
+     * @param base    The {@link Record} to analyze.
+     *
+     * @return {@link Object} is base is an instance of {@link Record}, 
otherwise {@code null}.
+     */
+    @Override
+    public Class<?> getCommonPropertyType(ELContext context, Object base) {
+        if (base instanceof Record) {
+            // Fields can be of any type
+            return Object.class;
+        }
+        return null;
+    }
+}
diff --git a/java/jakarta/el/StandardELContext.java 
b/java/jakarta/el/StandardELContext.java
index 11121b7c0b..17c5f44b3d 100644
--- a/java/jakarta/el/StandardELContext.java
+++ b/java/jakarta/el/StandardELContext.java
@@ -53,6 +53,7 @@ public class StandardELContext extends ELContext {
         standardResolver.add(new ResourceBundleELResolver());
         standardResolver.add(new ListELResolver());
         standardResolver.add(new ArrayELResolver());
+        standardResolver.add(new RecordELResolver());
         standardResolver.add(new BeanELResolver());
     }
 
diff --git a/java/org/apache/jasper/el/ELContextImpl.java 
b/java/org/apache/jasper/el/ELContextImpl.java
index 7c5fb6c13b..6b9f86f6e6 100644
--- a/java/org/apache/jasper/el/ELContextImpl.java
+++ b/java/org/apache/jasper/el/ELContextImpl.java
@@ -29,6 +29,7 @@ import jakarta.el.ELResolver;
 import jakarta.el.FunctionMapper;
 import jakarta.el.ListELResolver;
 import jakarta.el.MapELResolver;
+import jakarta.el.RecordELResolver;
 import jakarta.el.ResourceBundleELResolver;
 import jakarta.el.StaticFieldELResolver;
 import jakarta.el.ValueExpression;
@@ -85,6 +86,7 @@ public class ELContextImpl extends ELContext {
         ((CompositeELResolver) DefaultResolver).add(new 
ResourceBundleELResolver());
         ((CompositeELResolver) DefaultResolver).add(new ListELResolver());
         ((CompositeELResolver) DefaultResolver).add(new ArrayELResolver());
+        ((CompositeELResolver) DefaultResolver).add(new RecordELResolver());
         ((CompositeELResolver) DefaultResolver).add(new BeanELResolver());
     }
 
diff --git a/java/org/apache/jasper/el/JasperELResolver.java 
b/java/org/apache/jasper/el/JasperELResolver.java
index 31dd3e44c4..8af1f7c2e1 100644
--- a/java/org/apache/jasper/el/JasperELResolver.java
+++ b/java/org/apache/jasper/el/JasperELResolver.java
@@ -30,6 +30,7 @@ import jakarta.el.ELResolver;
 import jakarta.el.ListELResolver;
 import jakarta.el.MapELResolver;
 import jakarta.el.PropertyNotFoundException;
+import jakarta.el.RecordELResolver;
 import jakarta.el.ResourceBundleELResolver;
 import jakarta.el.StaticFieldELResolver;
 import jakarta.servlet.jsp.el.ImplicitObjectELResolver;
@@ -71,6 +72,7 @@ public class JasperELResolver extends CompositeELResolver {
         if (JspRuntimeLibrary.GRAAL) {
             add(new GraalBeanELResolver());
         }
+        add(new RecordELResolver());
         add(new BeanELResolver());
         add(new ScopedAttributeELResolver());
         add(new ImportELResolver());
diff --git a/test/jakarta/el/TestRecordELResolver.java 
b/test/jakarta/el/TestRecordELResolver.java
new file mode 100644
index 0000000000..22151eb8b5
--- /dev/null
+++ b/test/jakarta/el/TestRecordELResolver.java
@@ -0,0 +1,75 @@
+/*
+ * 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;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestRecordELResolver {
+
+    private static final String TEXT_DATA = "text";
+    private static final long LONG_DATA = 1234;
+
+
+    @Test
+    public void testRecordTextField() {
+        ExpressionFactory factory = ExpressionFactory.newInstance();
+        StandardELContext context = new StandardELContext(factory);
+
+        TesterRecordA recordA = new TesterRecordA(TEXT_DATA, LONG_DATA);
+
+        ValueExpression varRecordA = factory.createValueExpression(recordA, 
TesterRecordA.class);
+        context.getVariableMapper().setVariable("recordA", varRecordA);
+
+        ValueExpression ve = factory.createValueExpression(context, 
"${recordA.text}", String.class);
+        String result = ve.getValue(context);
+
+        Assert.assertEquals(TEXT_DATA, result);
+    }
+
+
+    @Test(expected = ELException.class)
+    public void testRecordUnknownField() {
+        ExpressionFactory factory = ExpressionFactory.newInstance();
+        StandardELContext context = new StandardELContext(factory);
+
+        TesterRecordA recordA = new TesterRecordA(TEXT_DATA, LONG_DATA);
+
+        ValueExpression varRecordA = factory.createValueExpression(recordA, 
TesterRecordA.class);
+        context.getVariableMapper().setVariable("recordA", varRecordA);
+
+        ValueExpression ve = factory.createValueExpression(context, 
"${recordA.unknown}", String.class);
+        ve.getValue(context);
+    }
+
+
+    @Test
+    public void testRecordNumericField() {
+        ExpressionFactory factory = ExpressionFactory.newInstance();
+        StandardELContext context = new StandardELContext(factory);
+
+        TesterRecordA recordA = new TesterRecordA(TEXT_DATA, LONG_DATA);
+
+        ValueExpression varRecordA = factory.createValueExpression(recordA, 
TesterRecordA.class);
+        context.getVariableMapper().setVariable("recordA", varRecordA);
+
+        ValueExpression ve = factory.createValueExpression(context, 
"${recordA.number}", Long.class);
+        Long result = ve.getValue(context);
+
+        Assert.assertEquals(LONG_DATA, result.longValue());
+    }
+}
diff --git a/test/jakarta/el/TesterRecordA.java 
b/test/jakarta/el/TesterRecordA.java
new file mode 100644
index 0000000000..8efe99efc5
--- /dev/null
+++ b/test/jakarta/el/TesterRecordA.java
@@ -0,0 +1,20 @@
+/*
+ * 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 TesterRecordA(String text, long number) {
+}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index d5c18e7ccf..575b08f3dc 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -158,6 +158,13 @@
       </fix>
     </changelog>
   </subsection>
+  <subsection name="Jasper">
+    <changelog>
+      <add>
+        Add support for Records to expression language. (markt)
+      </add>
+    </changelog>
+  </subsection>
   <subsection name="WebSocket">
     <changelog>
       <fix>


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org

Reply via email to