javaproject-reactor

Preserving distinct types downstream for different operators


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;
        }
    } 

Solution

  • 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.