Fix #176 - Add support for java.util.Optional (#203) * Fix #176 - Add support for java.util.Optional Support is implemented via a new resolver : OptionalELResolver The resolver is not included in the default ELResolvers. It must be explicitly added by the user if they wish to enable this functionality.
diff --git a/api/src/main/java/jakarta/el/OptionalELResolver.java b/api/src/main/java/jakarta/el/OptionalELResolver.java new file mode 100644 index 0000000..6902ee5 --- /dev/null +++ b/api/src/main/java/jakarta/el/OptionalELResolver.java
@@ -0,0 +1,179 @@ +/* + * 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 static jakarta.el.ELUtil.getExceptionMessageString; + +import java.util.Objects; +import java.util.Optional; + +/** + * Defines property resolution behaviour on {@link Optional}s. + * <p> + * This resolver handles base objects that are instances of {@link Optional}. + * <p> + * 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}. + * <p> + * 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}). + * <p> + * 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. + * <p> + * 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: + * <ul> + * <li>The {@link ELContext} is the current context</li> + * <li>The base object is the result of calling {@link Optional#get()} on the current base object</li> + * <li>The property object is the current property object</li> + * </ul> + * <p> + * 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} + * <p> + * 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} + * <p> + * 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(getExceptionMessageString( + context, "resolverNotwritable", new Object[] { base.getClass().getName() })); + } + } + + + /** + * {@inheritDoc} + * <p> + * 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} + * <p> + * 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> T convertToType(ELContext context, Object obj, Class<T> 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 ELUtil.isCoercibleFrom(). + */ + } + } else { + context.setPropertyResolved(true); + return null; + } + } + return null; + } +}
diff --git a/spec/src/main/asciidoc/ELSpec.adoc b/spec/src/main/asciidoc/ELSpec.adoc index 8a22a39..54370aa 100644 --- a/spec/src/main/asciidoc/ELSpec.adoc +++ b/spec/src/main/asciidoc/ELSpec.adoc
@@ -1277,6 +1277,28 @@ integer. +=== `java.util.Optional` + +Note: Unified EL implementations have their own `Optional` class (see +<<Optional>>) that is separate to `java.util.Optional` and the behaviour +described in this section. + +By default, instances of `java.util.Optional` are treated by the unified EL in +an identical manner to any other Java object. This means, for example, that +resolving the expression `someObject.optionalProperty.otherProperty` would +result in a `PropertyNotFoundException` if `optionalProperty` was an instance of +`java.util.Optional`. + +The EL API provides an additional `ELResolver` implementation, +`OptionalELResolver` that provides alternative handling for +`java.util.Optional`. With this resolver added to the set of standard resolvers, +the expression `someObject.optionalProperty.otherProperty` would result in +`null` if the `optionalProperty` was an empty `java.util.Optional` and the value +of `otherProperty` if the `java.util.Optional` was populated with an object that +had a property named `otherProperty`. The behaviour of the `OptionalELResolver` +is fully documented in its Javadoc. + + === Static Field and Method Reference A static field or static method of a Java @@ -2990,6 +3012,10 @@ * https://github.com/jakartaee/expression-language/issues/175[#175] Add support for a new property, `length`, for arrays. +* https://github.com/jakartaee/expression-language/issues/176[#176] + Add support for `java.util.Optional`. The support is not enabled by default. + Users must add the new resolver `OptionalELResolver` to enable this support. + * https://github.com/jakartaee/expression-language/issues/188[#188] Allow for module visibility when accessing class methods.