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