Help me solve this Project Reactor puzzle.
Basically two operators need different types, I seemingly can't preserve them both downstream.
I could have some data container that stores both the DTO and the User
: the former operator would retrieve the DTO, the latter would retrieve the User
. However, it seems dubious from the design standpoint.
I certainly don't want to load the user once again just because it slipped out of scope.
UserDto
, User
, UserServive
, UserAuthenticationToken
are custom but irrelevant types, so I won't include them.
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.password.PasswordEncoder;
import reactor.core.publisher.Mono;
import java.util.Optional;
public class UserReactiveAuthenticationManager implements ReactiveAuthenticationManager {
private final UserService userService;
private final PasswordEncoder passwordEncoder;
private UserReactiveAuthenticationManager(UserService userService, PasswordEncoder passwordEncoder) {
this.userService = userService;
this.passwordEncoder = passwordEncoder;
}
public static UserReactiveAuthenticationManager of(UserService userService, PasswordEncoder passwordEncoder) {
return new UserReactiveAuthenticationManager(userService, passwordEncoder);
}
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
return Mono.just(authentication.getPrincipal())
.ofType(UserDto.class)
.filter(this::matchesExistingUser) // needs DTO
.map(UserAuthenticationToken::from); // needs User, but loaded User no longer in scope, doesn't compile
}
private boolean matchesExistingUser(UserDto userDto) {
Optional<User> userOptional = userService.find(userDto); // DB call
if (userOptional.isEmpty()) return false;
User user = userOptional.get();
return passwordEncoder.matches(userDto.getPassword(), user.getPassword());
}
}
Here's the dubious solution that I mentioned:
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
return Mono.just(authentication.getPrincipal())
.ofType(UserDto.class)
.map(AuthenticationContext::from)
.filter(this::matchConfirmed)
.map(AuthenticationContext::getUser)
.map(UserAuthenticationToken::from);
}
private boolean matchConfirmed(AuthenticationContext authenticationContext) {
UserDto userDto = authenticationContext.getUserDto();
Optional<User> userOptional = userService.find(userDto);
if (userOptional.isEmpty()) return false;
User user = userOptional.get();
authenticationContext.setUser(user); // cares about downstream operator, that's nice of them
return passwordEncoder.matches(userDto.getPassword(), user.getPassword());
}
@Setter
@Getter
private static class AuthenticationContext {
UserDto userDto;
User user;
static AuthenticationContext from(UserDto userDto) {
AuthenticationContext authenticationContext = new AuthenticationContext();
authenticationContext.setUserDto(userDto);
return authenticationContext;
}
}
I would look into zipWhen()
operator:
/**
* Wait for the result from this mono, use it to create a second mono via the
* provided {@code rightGenerator} function and combine both results into a {@link Tuple2}.
*
* <p>
* <img class="marble" src="doc-files/marbles/zipWhenForMono.svg" alt="">
*
* @param rightGenerator the {@link Function} to generate a {@code Mono} to combine with
* @param <T2> the element type of the other Mono instance
*
* @return a new combined Mono
*/
public final <T2> Mono<Tuple2<T, T2>> zipWhen(Function<T, Mono<? extends T2>> rightGenerator) {
So, there you would call your userService.find(userDto)
and use this Tuple2
downstream for those respective filter
and map
operators.