Peter Luttrell opened SPR-15878 and commented

This is a feature request to add support for Java 8 Optionals to the Spring Expression Language.

One use case that I just ran into is wanting to use @PostAuthorize on a method that returns an Optional in conjunctions with custom expressions. For example the following fails:

@PostAuthorize("canAccessOrganization(returnObject.organiztionId)")
public Optional<Person> getPerson(String personId){
    ...
}

In this case, if the returned reference isn't present, that @PostAuthorize would allow the response, which should be Optional.empty(). If it is present, then it'd be dereferenced into the returnObject, so we'd have direct access to its fields for use in the expression.


4 votes, 6 watchers

Comment From: spring-projects-issues

Juergen Hoeller commented

Rob Winch, I figure this might have to be handled in Spring Security's authorization interceptor, specifically detecting an Optional return value there and unwrapping it?

Comment From: spring-projects-issues

Rob Winch commented

Thanks for the report Peter Luttrell

. For example the following fails:

This does fail because Optional does not have a method of getOrganizationId() on it.

In this case, if the returned reference isn't present, that @PostAuthorize would allow the response,

This is not true. In either case, the PostAuthorize will fail because Optional does not have a method getOrganizationId()

Juergen Hoeller I don't think it really makes sense to automatically unwrap Optional. If the method signature is Optional, then returnType should be Optional. If Optional is automatically unwrapped, then how would someone use Optional return types?

Users can always do something like @PostAuthorize("canAccessOrganization(returnObject.orElse(null)?.organiztionId)"). Finally, if someone really wants to automatically unwrap the returnValue when it is Optional, they can override DefaultMethodSecurityExpressionHandler.setReturnObject.

Comment From: spring-projects-issues

Mohamed Amine Mrad commented

Hello, I was not able to create an issue. I have a suggestion to add here: In fact SpEL should support writing an Optional properly like writing an Enum. There is an EnumToStringConverter added to DefaultConversionService method addScalarConverters There should be an OptionalToStringConverter also. I'm using thymeleaf and I'm struggling with the ugly get() in HTML.

Comment From: spring-projects-issues

Stéphane Toussaint commented

I have another use case for this feature request.

I use Spring Integration and some of my service layer bean methods are now returning Optional generic types.

This is a sample of two successive service-activator call, the payload is the Optional\ return from retrieveByUsername.

<int:service-activator expression="@personService.retrieveByUsername(headers.username)" />
<int:service-activator expression="@personService.doWithPerson(payload)" />

The doWithPerson method has not changed ; still waiting for a Person.

Class PersonService {
  public void doWithPerson(Person person) {
    ...
  }
}

The retrieveByUsername however now returns an Optional of Person

Class PersonService {
  public Optional<Person> retrieveByUsername(String username) {
    return Optional.of(...)
  }
}

Now Spring Integration (actually the Spring Expression Evaluator) complains it can't find the targeted method with a message like :

Expression evaluation failed: @personService.doWithPerson(payload); nested exception is org.springframework.expression.spel.SpelEvaluationException: EL1004E: Method call: Method doWithPerson(java.util.Optional) cannot be found on PersonService.

Will an OptionalToObjectConverter be feasible ? Maybe it can be possible to rely on Nullable annotation on the target method to handle the Optional.empty() case ?(return null or throw ?).

Comment From: wimdeblauwe

Another use case is with @PreAuthorize. Assume a Task has a reference to a User and there is a TaskServiceImpl class with:

    Optional<Task> getTask(int taskId);

The controller could check if the task is linked to the authorized user like this:

    @DeleteMapping("/{taskId}")
    @PreAuthorize("@taskServiceImpl.getTask(#taskId).orElse(null)?.user.id == #userDetails.id")
    public String destroy(@AuthenticationPrincipal ApplicationUserDetails userDetails,
                          @PathVariable("taskId") Integer taskId) {
        return "redirect:/tasks";
    }

The orElse(null) is perfect for this, not sure if anything else is needed, but just wanted to let you know about this use case.

Comment From: sanatik

My use case would be with @Cacheable

@Cacheable(unless = "#result.isEmpty()")
public Optional<User> getUserById(final String userId);

Comment From: dkfellows

At the very least, @PreFilter and @PostFilter ought to work with Optional as if they were collections containing at most one object. If the filter sees a non-empty optional but doesn't want to let the contained object through, that becomes an empty optional; the obvious semantics.

Comment From: OrangeDog

This also applies to Thymeleaf templating, which uses SpEL in the default Boot configuration.

Comment From: cheatmenot

+1

Comment From: lauroschuck

This is causing me so much trouble now! I created a custom company lib that would benefit a lot from SpEL, and this is essentially making it useless because a lot of our codebase has a bias towards using Optional.

@jhoeller Why declined? Why not planned? This sounds like such a resonable request, so many people here voicing their support, and probably very natural do implement!

Comment From: drekbour

Indeed. Lack of this caused us to drop SpEL long ago as the basis for something.

Comment From: sbrannen

After further consideration, we have decided to introduce first-class support for Optional in SpEL expressions in Spring Framework 7.0. This aligns nicely with our overall null-safety efforts with JSpecify annotations and Kotlin improvements.

The discussions in this issue have presented two separate use cases.

  1. transparent unwrapping of Optional for property and field access
  2. transparent unwrapping on Optional when invoking a constructor, method, or function

After experimentation, I have determined that supporting Optional with the null-safe and Elvis operators would address use case #1.

To address use case #2, I have created #34544 to introduce an OptionalToObjectConverter.

Comment From: sbrannen

After experimentation, I have determined that supporting Optional with the null-safe and Elvis operators would address use case #1.

A proof of concept can be viewed in the following feature branch.

https://github.com/spring-projects/spring-framework/compare/main...sbrannen:spring-framework:issues/gh-20433-spel-null-safe-elvis-optional

That allows the original use case:

@PostAuthorize("canAccessOrganization(returnObject.organizationId)")
public Optional<Person> getPerson(String personId){
    // ...
}

... to be replaced with:

@PostAuthorize("canAccessOrganization(returnObject?.organizationId)")
public Optional<Person> getPerson(String personId){
    // ...
}

If returnObject is either null or an empty Optional, null would be passed to the canAccessOrganization() invocation.

If returnObject is not null and not an Optional, returnObject.organizationId would be passed to the canAccessOrganization() invocation.

If returnObject is a non-null Optional, returnObject.get().organizationId would be passed to the canAccessOrganization() invocation, transparently unwrapping the Optional.

@wimdeblauwe, you should then be able to rewrite your example as follows.

    @DeleteMapping("/{taskId}")
    @PreAuthorize("@taskServiceImpl.getTask(#taskId)?.user?.id == #userDetails.id")
    public String destroy(@AuthenticationPrincipal ApplicationUserDetails userDetails,
                          @PathVariable("taskId") Integer taskId) {
        return "redirect:/tasks";
    }

Comment From: OrangeDog

returnObject.organizationId would be passed to the canAccessOrganization() invocation.

Do you mean returnObject.get().organizationId?

@taskServiceImpl.getTask(#taskId)?.user.id

Do you mean @taskServiceImpl.getTask(#taskId)?.user?.id ?

Comment From: sbrannen

returnObject.organizationId would be passed to the canAccessOrganization() invocation.

Do you mean returnObject.get().organizationId?

No, in that particular example returnObject is "not an Optional".

@taskServiceImpl.getTask(#taskId)?.user.id

Do you mean @taskServiceImpl.getTask(#taskId)?.user?.id ?

Yes, that would indeed be better. I was merely modifying the original example to make use of the proposed new feature. But I'll update that to use null-safe navigation throughout the compound expression.

Comment From: sbrannen

This support will be available in the upcoming Spring Framework 7.0 M3 release.

For details, check out the updated sections of the reference manual.

Feedback is welcome! 😎