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.