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 <[email protected]>
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: [email protected]
For additional commands, e-mail: [email protected]