Skip to content

Commit

Permalink
Fix #176 - Add support for java.util.Optional (#203)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
markt-asf committed Aug 2, 2023
1 parent 5addb99 commit 48326d5
Show file tree
Hide file tree
Showing 2 changed files with 205 additions and 0 deletions.
179 changes: 179 additions & 0 deletions api/src/main/java/jakarta/el/OptionalELResolver.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
26 changes: 26 additions & 0 deletions spec/src/main/asciidoc/ELSpec.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1277,6 +1277,28 @@ the property `length` for arrays which will return the length of the array as an
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
Expand Down Expand Up @@ -2990,6 +3012,10 @@ This appendix is non-normative.
* 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.

Expand Down

0 comments on commit 48326d5

Please sign in to comment.