diff --git a/java/jakarta/el/OptionalELResolver.java b/java/jakarta/el/OptionalELResolver.java new file mode 100644 index 000000000000..6f9c3da73fec --- /dev/null +++ b/java/jakarta/el/OptionalELResolver.java @@ -0,0 +1,182 @@ +/* + * 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.util.Objects; +import java.util.Optional; + +/** + * Defines property resolution behaviour on {@link Optional}s. + * + *

+ * This resolver handles base objects that are instances of {@link Optional}. + * + *

+ * If the {@link Optional#isEmpty()} is {@code true} for the base object and the property is {@code null} then the + * resulting value is {@code null}. + * + *

+ * If the {@link Optional#isEmpty()} is {@code true} for the base object and the property is not {@code null} then the + * resulting value is the base object (an empty {@link Optional}). + * + *

+ * If the {@link Optional#isPresent()} is {@code true} for the base object and the property is {@code null} then the + * resulting value is the result of calling {@link Optional#get()} on the base object. + * + *

+ * If the {@link Optional#isPresent()} is {@code true} for the base object and the property is not {@code null} then the + * resulting value is the result of calling {@link ELResolver#getValue(ELContext, Object, Object)} using the + * {@link ELResolver} obtained from {@link ELContext#getELResolver()} with the following parameters: + *

+ * + *

+ * This resolver is always a read-only resolver. + */ +public class OptionalELResolver extends ELResolver { + + @Override + public Object getValue(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + if (base instanceof Optional) { + context.setPropertyResolved(base, property); + if (((Optional) base).isEmpty()) { + if (property == null) { + return null; + } else { + return base; + } + } else { + if (property == null) { + return ((Optional) base).get(); + } else { + Object resolvedBase = ((Optional) base).get(); + return context.getELResolver().getValue(context, resolvedBase, property); + } + } + } + + return null; + } + + /** + * {@inheritDoc} + * + *

+ * If the base object is an {@link Optional} this method always returns {@code null} since instances of this + * resolver are always read-only. + */ + @Override + public Class getType(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + if (base instanceof Optional) { + context.setPropertyResolved(base, property); + } + + return null; + } + + /** + * {@inheritDoc} + * + *

+ * If the base object is an {@link Optional} this method always throws a {@link PropertyNotWritableException} since + * instances of this resolver are always read-only. + */ + @Override + public void setValue(ELContext context, Object base, Object property, Object value) { + Objects.requireNonNull(context); + + if (base instanceof Optional) { + throw new PropertyNotWritableException( + Util.message(context, "resolverNotWritable", base.getClass().getName())); + } + } + + /** + * {@inheritDoc} + * + *

+ * If the base object is an {@link Optional} this method always returns {@code true} since instances of this + * resolver are always read-only. + */ + @Override + public boolean isReadOnly(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + if (base instanceof Optional) { + context.setPropertyResolved(base, property); + return true; + } + + return false; + } + + /** + * {@inheritDoc} + * + *

+ * If the base object is an {@link Optional} this method always returns {@code Object.class}. + */ + @Override + public Class getCommonPropertyType(ELContext context, Object base) { + if (base instanceof Optional) { + return Object.class; + } + + return null; + } + + @Override + public T convertToType(ELContext context, Object obj, Class type) { + Objects.requireNonNull(context); + if (obj instanceof Optional) { + if (((Optional) obj).isPresent()) { + Object value = ((Optional) obj).get(); + // If the value is assignable to the required type, do so. + if (type.isAssignableFrom(value.getClass())) { + context.setPropertyResolved(true); + @SuppressWarnings("unchecked") + T result = (T) value; + return result; + } + + try { + Object convertedValue = context.convertToType(value, type); + context.setPropertyResolved(true); + @SuppressWarnings("unchecked") + T result = (T) convertedValue; + return result; + } catch (ELException e) { + /* + * TODO: This isn't pretty but it works. Significant refactoring would be required to avoid the + * exception. See also OptionalELResolver.convertToType(). + */ + } + } else { + context.setPropertyResolved(true); + return null; + } + } + return null; + } +} diff --git a/java/jakarta/el/Util.java b/java/jakarta/el/Util.java index 168f89ce4211..f44b662063b4 100644 --- a/java/jakarta/el/Util.java +++ b/java/jakarta/el/Util.java @@ -493,8 +493,10 @@ static boolean isAssignableFrom(Class src, Class target) { * This method duplicates code in org.apache.el.util.ReflectionUtil. When making changes keep the code in sync. */ private static boolean isCoercibleFrom(ELContext context, Object src, Class target) { - // TODO: This isn't pretty but it works. Significant refactoring would - // be required to avoid the exception. + /* + * TODO: This isn't pretty but it works. Significant refactoring would be required to avoid the exception. See + * also OptionalELResolver.convertToType(). + */ try { context.convertToType(src, target); } catch (ELException e) { diff --git a/test/jakarta/el/TestOptionalELResolver.java b/test/jakarta/el/TestOptionalELResolver.java new file mode 100644 index 000000000000..38b171d3f2ee --- /dev/null +++ b/test/jakarta/el/TestOptionalELResolver.java @@ -0,0 +1,210 @@ +/* + * 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.util.Optional; + +import org.junit.Assert; +import org.junit.Test; + +public class TestOptionalELResolver { + + @Test(expected = PropertyNotFoundException.class) + public void testIssue176WithoutOptionalResolverOptionalEmpty() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + StandardELContext context = new StandardELContext(factory); + + TesterBeanA beanA = new TesterBeanA(); + + ValueExpression varBeanA = factory.createValueExpression(beanA, TesterBeanA.class); + context.getVariableMapper().setVariable("beanA", varBeanA); + + ValueExpression ve = factory.createValueExpression(context, "${beanA.beanBOpt.name}", String.class); + ve.getValue(context); + } + + + @Test(expected = PropertyNotFoundException.class) + public void testIssue176WithoutOptionalResolverOptionalPresent() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + StandardELContext context = new StandardELContext(factory); + + TesterBeanA beanA = new TesterBeanA(); + TesterBeanB beanB = new TesterBeanB("test"); + beanA.setBeanB(beanB); + + ValueExpression varBeanA = factory.createValueExpression(beanA, TesterBeanA.class); + context.getVariableMapper().setVariable("beanA", varBeanA); + + ValueExpression ve = factory.createValueExpression(context, "${beanA.beanBOpt.name}", String.class); + ve.getValue(context); + } + + + @Test + public void testIssue176WithOptionalResolverOptionalEmptyWithProperty() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + StandardELContext context = new StandardELContext(factory); + context.addELResolver(new OptionalELResolver()); + + TesterBeanA beanA = new TesterBeanA(); + + ValueExpression varBeanA = factory.createValueExpression(beanA, TesterBeanA.class); + context.getVariableMapper().setVariable("beanA", varBeanA); + + ValueExpression ve = factory.createValueExpression(context, "${beanA.beanBOpt.name}", String.class); + Object result = ve.getValue(context); + + Assert.assertNull(result); + } + + + @Test + public void testIssue176WithOptionalResolverOptionalPresentWithProperty() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + StandardELContext context = new StandardELContext(factory); + context.addELResolver(new OptionalELResolver()); + + TesterBeanA beanA = new TesterBeanA(); + TesterBeanB beanB = new TesterBeanB("test"); + beanA.setBeanB(beanB); + + ValueExpression varBeanA = factory.createValueExpression(beanA, TesterBeanA.class); + context.getVariableMapper().setVariable("beanA", varBeanA); + + ValueExpression ve = factory.createValueExpression(context, "${beanA.beanBOpt.name}", String.class); + Object result = ve.getValue(context); + + Assert.assertEquals("test", result); + } + + + @Test + public void testIssue176WithOptionalResolverOptionalEmptyWithoutProperty() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + StandardELContext context = new StandardELContext(factory); + context.addELResolver(new OptionalELResolver()); + + TesterBeanA beanA = new TesterBeanA(); + + ValueExpression varBeanA = factory.createValueExpression(beanA, TesterBeanA.class); + context.getVariableMapper().setVariable("beanA", varBeanA); + + ValueExpression ve = factory.createValueExpression(context, "${beanA.beanBOpt}", TesterBeanB.class); + Object result = ve.getValue(context); + + Assert.assertNull(result); + } + + + @Test + public void testIssue176WithOptionalResolverOptionalPresentWithoutProperty() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + StandardELContext context = new StandardELContext(factory); + context.addELResolver(new OptionalELResolver()); + + TesterBeanA beanA = new TesterBeanA(); + TesterBeanB beanB = new TesterBeanB("test"); + beanA.setBeanB(beanB); + + ValueExpression varBeanA = factory.createValueExpression(beanA, TesterBeanA.class); + context.getVariableMapper().setVariable("beanA", varBeanA); + + ValueExpression ve = factory.createValueExpression(context, "${beanA.beanBOpt}", TesterBeanB.class); + Object result = ve.getValue(context); + + Assert.assertEquals(beanB, result); + } + + + @Test + public void testIssue176WithoutOptionalResolverOptionalEmptyWithMap() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + StandardELContext context = new StandardELContext(factory); + + TesterBeanA beanA = new TesterBeanA(); + + ValueExpression varBeanA = factory.createValueExpression(beanA, TesterBeanA.class); + context.getVariableMapper().setVariable("beanA", varBeanA); + + ValueExpression ve = factory.createValueExpression(context, "${beanA.beanBOpt.map(b -> b.name)}", Optional.class); + Object result = ve.getValue(context); + + Assert.assertNotNull(result); + Assert.assertEquals(Optional.class, result.getClass()); + Assert.assertTrue(((Optional) result).isEmpty()); + } + + + @Test + public void testIssue176WithoutOptionalResolverOptionalPresentWithMap() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + StandardELContext context = new StandardELContext(factory); + + TesterBeanA beanA = new TesterBeanA(); + TesterBeanB beanB = new TesterBeanB("test"); + beanA.setBeanB(beanB); + + ValueExpression varBeanA = factory.createValueExpression(beanA, TesterBeanA.class); + context.getVariableMapper().setVariable("beanA", varBeanA); + + ValueExpression ve = factory.createValueExpression(context, "${beanA.beanBOpt.map(b -> b.name)}", Optional.class); + Object result = ve.getValue(context); + + Assert.assertNotNull(result); + Assert.assertEquals(Optional.class, result.getClass()); + Assert.assertEquals("test", ((Optional) result).get()); + } + + + @Test + public void testIssue176WithOptionalResolverOptionalEmptyWithMap() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + StandardELContext context = new StandardELContext(factory); + context.addELResolver(new OptionalELResolver()); + + TesterBeanA beanA = new TesterBeanA(); + + ValueExpression varBeanA = factory.createValueExpression(beanA, TesterBeanA.class); + context.getVariableMapper().setVariable("beanA", varBeanA); + + ValueExpression ve = factory.createValueExpression(context, "${beanA.beanBOpt.map(b -> b.name)}", String.class); + Object result = ve.getValue(context); + + Assert.assertNull(result); + } + + + @Test + public void testIssue176WithOptionalResolverOptionalPresentWithMap() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + StandardELContext context = new StandardELContext(factory); + context.addELResolver(new OptionalELResolver()); + + TesterBeanA beanA = new TesterBeanA(); + TesterBeanB beanB = new TesterBeanB("test"); + beanA.setBeanB(beanB); + + ValueExpression varBeanA = factory.createValueExpression(beanA, TesterBeanA.class); + context.getVariableMapper().setVariable("beanA", varBeanA); + + ValueExpression ve = factory.createValueExpression(context, "${beanA.beanBOpt.map(b -> b.name)}", String.class); + Object result = ve.getValue(context); + + Assert.assertEquals("test", result); + } +} diff --git a/test/jakarta/el/TestOptionalELResolverInJsp.java b/test/jakarta/el/TestOptionalELResolverInJsp.java new file mode 100644 index 000000000000..0b81fa3db04e --- /dev/null +++ b/test/jakarta/el/TestOptionalELResolverInJsp.java @@ -0,0 +1,87 @@ +/* + * 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.io.File; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.jsp.JspApplicationContext; +import jakarta.servlet.jsp.JspFactory; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + + +public class TestOptionalELResolverInJsp extends TomcatBaseTest { + + @Test + public void test() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + Context ctx = tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + ctx.addApplicationListener(ResolverConfigListener.class.getName()); + + tomcat.start(); + + ByteChunk responseBody = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/test/el-optional.jsp", responseBody, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + String result = responseBody.toString(); + assertEcho(result, "00-null"); + assertEcho(result, "01-null"); + assertEcho(result, "02-null"); + assertEcho(result, "10-This is an instance of TesterBeanB"); + assertEcho(result, "11-test"); + assertEcho(result, "12-test"); + assertEcho(result, "20-null"); + assertEcho(result, "21-null"); + assertEcho(result, "22-null"); + assertEcho(result, "30-This is an instance of TesterBeanB"); + assertEcho(result, "31-test"); + assertEcho(result, "32-test"); + } + + + // Assertion for text contained with

, e.g. printed by tags:echo + private static void assertEcho(String result, String expected) { + Assert.assertTrue(result, result.indexOf("

" + expected + "

") > 0); + } + + + public static class ResolverConfigListener implements ServletContextListener { + + @Override + public void contextInitialized(ServletContextEvent sce) { + ServletContext servletContext = sce.getServletContext(); + JspFactory jspFactory = JspFactory.getDefaultFactory(); + JspApplicationContext jspApplicationContext = jspFactory.getJspApplicationContext(servletContext); + jspApplicationContext.addELResolver(new OptionalELResolver()); + } + } +} diff --git a/test/jakarta/el/TesterBeanA.java b/test/jakarta/el/TesterBeanA.java new file mode 100644 index 000000000000..97f153a4403d --- /dev/null +++ b/test/jakarta/el/TesterBeanA.java @@ -0,0 +1,33 @@ +/* + * 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.util.Optional; + +public class TesterBeanA { + private TesterBeanB beanB; + + public Optional getBeanBOpt() { + return Optional.ofNullable(beanB); + } + + public void setBeanB(TesterBeanB beanB) { + this.beanB = beanB; + } +} + + diff --git a/test/jakarta/el/TesterBeanB.java b/test/jakarta/el/TesterBeanB.java new file mode 100644 index 000000000000..66f9de0d4cdc --- /dev/null +++ b/test/jakarta/el/TesterBeanB.java @@ -0,0 +1,35 @@ +/* + * 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 class TesterBeanB { + + private final String name; + + public TesterBeanB(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return "This is an instance of TesterBeanB"; + } +} diff --git a/test/webapp/el-optional.jsp b/test/webapp/el-optional.jsp new file mode 100644 index 000000000000..8fcf3b919d2f --- /dev/null +++ b/test/webapp/el-optional.jsp @@ -0,0 +1,48 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> +<%@ page import="jakarta.el.TesterBeanA" %> +<%@ page import="jakarta.el.TesterBeanB" %> + + EL method test cases + + <% + TesterBeanA beanA1 = new TesterBeanA(); + + TesterBeanA beanA2 = new TesterBeanA(); + TesterBeanB beanB = new TesterBeanB("test"); + beanA2.setBeanB(beanB); + + pageContext.setAttribute("testBeanA1", beanA1, PageContext.REQUEST_SCOPE); + pageContext.setAttribute("testBeanA2", beanA2, PageContext.REQUEST_SCOPE); + %> + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index 45dc7ce86cc7..6d58f1ceb270 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -135,6 +135,16 @@ + + + + Add java.util.Optional support via the + jakarta.el.OptionalELResolver to Tomcat's implementation + of the Jakarta EL API to align with the latest proposals for the Jakarta + EL 6.0 API. (markt) + + +