Should I rely on "mono of item" or "plain item" arguments when composing reactive chains? - reactive-programming

I am have two versions of a Webflux/Reactor handler class. This class mimics a user sign up use case - as far as the business logic is concerned.
The first version of the class relies upon a Mono<User> whereas the second version uses a plain User.
First version of the class: this is the version relying on a Mono<User> argument down the chain. Notice the top level public method createUser uses a userMono.
#Component
#RequiredArgsConstructor
public class UserHandler {
private final #NonNull UserRepository userRepository;
private final #NonNull UserValidator userValidator;
public Mono<ServerResponse> createUser(ServerRequest serverRequest) {
Mono<User> userMono = serverRequest.bodyToMono(User.class).cache();
return validateUser(userMono)
.switchIfEmpty(validateEmailNotExists(userMono))
.switchIfEmpty(saveUser(userMono))
.single();
}
private Mono<ServerResponse> validateUser(Mono<User> userMono) {
return userMono
.map(this::computeErrors)
.filter(AbstractBindingResult::hasErrors)
.flatMap(err ->
status(BAD_REQUEST)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromObject(err.getAllErrors()))
);
}
private AbstractBindingResult computeErrors(User user) {
AbstractBindingResult errors = new BeanPropertyBindingResult(user, User.class.getName());
userValidator.validate(user, errors);
return errors;
}
private Mono<ServerResponse> validateEmailNotExists(Mono<User> userMono) {
return userMono
.flatMap(user -> userRepository.findByEmail(user.getEmail()))
.flatMap(existingUser ->
status(BAD_REQUEST)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromObject("User already exists."))
);
}
private Mono<ServerResponse> saveUser(Mono<User> userMono) {
return userMono
.flatMap(userRepository::save)
.flatMap(newUser -> status(CREATED)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromObject(newUser))
);
}
}
Second version of the class: that is the version relying on a User argument.
#Component
#RequiredArgsConstructor
public class UserHandler {
private final #NonNull UserRepository userRepository;
private final #NonNull UserValidator userValidator;
public Mono<ServerResponse> createUser(ServerRequest serverRequest) {
return serverRequest
.bodyToMono(User.class)
.flatMap(user ->
validateUser(user)
.switchIfEmpty(validateEmailNotExists(user))
.switchIfEmpty(saveUser(user))
.single()
);
}
private Mono<ServerResponse> validateUser(User user) {
return Mono.just(new BeanPropertyBindingResult(user, User.class.getName()))
.doOnNext(err -> userValidator.validate(user, err))
.filter(AbstractBindingResult::hasErrors)
.flatMap(err ->
status(BAD_REQUEST)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromObject(err.getAllErrors()))
);
}
private Mono<ServerResponse> validateEmailNotExists(User user) {
return userRepository.findByEmail(user.getEmail())
.flatMap(existingUser ->
status(BAD_REQUEST)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromObject("User already exists."))
);
}
private Mono<ServerResponse> saveUser(User user) {
return userRepository.save(user)
.flatMap(newUser -> status(CREATED)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromObject(newUser))
);
}
}
Now my questions:
What are the pros and cons of each of the two versions?
Which of the two would you recommend keeping?
Any feedback, advice and opinion welcome.

you want feedback here is my feedback
your methods dont really make sense. If you look at the declarations of your methods without the containing code.
private Mono<ServerResponse> validateUser(User user)
this makes no sense, this method should validate a user but you return a ServerResponse? In my opinion a validation should first takes place, and return some sort of boolean, or a list of validation errors.
I do not recommend any of the solutions you have presented, you should instead looking into and start using Mono.error instead of doing switchIfEmpty
You should seperate response building from validation logic.
What happens if validation rules change? or you want other responses based on what validation fails? right now they are together.
you can already see that you are returning the same bad request in two places but with different error messages. Duplications
This is my opinion and what i would do:
receive a request
map request to a user (bodyToMono)
validate the user in a method that vill return a list containing number of errors
check this list if user validation has failed, and if failed map the mono user to a mono error containing a illegalArgumentException with some sort of text of the error.
map the exception in the mono error to a status code
if validation passes save the user in a Mono.doOnSuccess block
this to me is much more clear and predictable with separations of return codes, and validation logic.

Using Thomas Andolf's advice together with that of other users, I came up with the following implementation:
#Component
#RequiredArgsConstructor
public class UserHandler {
private final #NonNull UserRepository userRepository;
private final #NonNull UserValidator userValidator;
public Mono<ServerResponse> findUsers(ServerRequest serverRequest) {
return ok()
.contentType(APPLICATION_JSON)
.body(userRepository.findAll(), User.class);
}
public Mono<ServerResponse> createUser(ServerRequest serverRequest) {
return serverRequest.bodyToMono(User.class)
.flatMap(this::validate)
.flatMap(this::validateEmailNotExists)
.flatMap(this::saveUser)
.flatMap(newUser -> status(CREATED)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromValue(newUser))
)
.onErrorResume(ValidationException.class, e -> status(BAD_REQUEST)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromValue(e.getErrors()))
)
.onErrorResume(DuplicateUserException.class, e -> status(CONFLICT)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromValue(e.getErrorMessage()))
);
}
private Mono<User> validateEmailNotExists(User user) {
return userRepository.findByEmail(user.getEmail())
.flatMap(userMono -> Mono.<User>error(new DuplicateUserException("User already exists")))
.switchIfEmpty(Mono.just(user));
}
private Mono<User> saveUser(User user) {
return userRepository.save(user);
}
private Mono<User> validate(User user) {
AbstractBindingResult errors = computeErrors(user);
return errors.hasErrors() ? Mono.error(new ValidationException(errors.getAllErrors())) : Mono.just(user);
}
private AbstractBindingResult computeErrors(User user) {
AbstractBindingResult errors = new BeanPropertyBindingResult(user, User.class.getName());
userValidator.validate(user, errors);
return errors;
}
}
It relies upon Mono.error, custom exceptions, and the onErrorResume() operator. It is equivalent to the two implementations in the question but somewhat leaner.

Related

Mutiny reactive - persist to DB if upstream is not null

I working on a Quarkus + MongoDB Reactive+ Mutiny application. I have a Person object and Event Object. I am creating a new event for a person. My uri looks like this
POST /person/{personId}/event
I need to first check if the person exists in MongoDB. If the person exists then save event. If person does not exist then create a Error Status and return. I am tried everything but I am stuck and getting error that required return type is Uni but required type is Uni. I tried with transformToUni as well but it did not work. Also tried few other ways like onItemOrFailure() etc. but nothing seems to work.
Here's the full Code.
public class EventResource {
#Inject
EventRepository eventRepository;
#Inject
PersonRepository personRepository;
#POST
#Path("/{person_id}/event")
public Uni<Response> create(Event event, #PathParam("person_id") String personId){
//Check if personId exist.
Uni<Person> uniPerson = personRepository.getPersonById(personId);
//THIS WORKS BUT ON FAILURE IS TREATED WHEN ERROR IS RAISED FOR EeventRepository.craete() and not if person is not found.
/*return uniPerson.onItem().ifNotNull()
.transformToUni(pid -> eventRepository.create(event, pid.getId()))
.onItem().transform(e -> Response.ok().entity(e).build())
.onFailure()
.recoverWithItem(f-> {
AStatus status = createErrorStatus(f.getMessage());
return Response.serverError().entity(status).build();
});
*/
Uni<Response> eventResp = uniPerson.onItem().transform(person -> {
if(person==null)
return Response.serverError().build();
else{
return eventRepository.create(event, person.getId())
.onItem().transform(event1 -> Response.ok(event1).build());
}
});
return eventResp;
}
You can use mutiny ifNull:
#POST
#Path("/{person_id}/event")
public Uni<Response> create(Event event, #PathParam("person_id") String personId){
return personRepository
.getPersonById(personId)
.onItem().ifNotNull().transformToUni(person -> createEvent(event, person))
.onItem().ifNull().continueWith(this::personNotFound)
// This onFailure will catch all the errors
.onFailure()
.recoverWithItem(f-> {
AStatus status = createErrorStatus(f.getMessage());
return Response.serverError().entity(status).build();
});
}
private Uni<Response> createEvent(Event event, Person person) {
return eventRepository
.create(event, person.getId())
.map( e -> Response.ok().entity(e).status(CREATED).build())
}
private Response personNotFound() {
return Response.serverError().build();
}
The error you are seeing is because when the item is not null, you are returning a Uni<Uni<Response>>. This is one way to fix it:
Uni<Response> eventResp = uniPerson
.chain(person -> {
if (person==null)
return Uni.createFrom().item(Response.serverError().build());
else {
return eventRepository
.create(event, person.getId())
.map(event1 -> Response.ok(event1).build());
}
});
I'm using map and chain because they are shorter, but you can replace them with onItem().transform(...) and onItem().transformToUni(...).

RxJava: how to do a second api call if first is successful and then create a combinded response

This is what I want to do:
call first rest API
if first succeeds call seconds rest API
if both are successful -> create an aggregated response
I'm using RxJava2 in Micronaut.
This is what I have but I'm not sure it's correct. What would happen if the first or second API call fails?
#Singleton
public class SomeService {
private final FirstRestApi firstRestApi;
private final SecondRestApi secondRestApi;
public SomeService(FirstRestApi firstRestApi, SecondRestApi secondRestApi) {
this.firstRestApi = firstRestApi;
this.secondRestApi = secondRestApi;
}
public Single<AggregatedResponse> login(String data) {
Single<FirstResponse> firstResponse = firstRestApi.call(data);
Single<SecondResponse> secondResponse = secondRestApi.call();
return firstResponse.zipWith(secondResponse, this::convertResponse);
}
private AggregatedResponse convertResponse(FirstResponse firstResponse, SecondResponse secondResponse) {
return AggregatedResponse
.builder()
.something1(firstResponse.getSomething1())
.something2(secondResponse.getSomething2())
.build();
}
}
This should be as simple as
public Single<AggregatedResponse> login(String data) {
return firstRestApi.call(data)
.flatMap((firstResponse) -> secondRestApi.call().map((secondResponse) -> {
return Pair.create(firstResponse, secondResponse);
})
.map((pair) -> {
return convertResponse(pair.getFirst(), pair.getSecond());
});
}
In which case you no longer need zipWith. Errors just go to error stream as usual.

Issue with use of project reactor's flatMap and switchIfEmpty operators

I have an issue with a reactive chain relying on flatMap() and switchIfEmpty(). For some reason, one of the Mono does not emit anything...
This is the public handler method calling the others:
//Throws: NoSuchElementException: Source was empty
public Mono<ServerResponse> createUser(ServerRequest serverRequest) {
Hooks.onOperatorDebug();
Mono<User> userMono = serverRequest.bodyToMono(User.class);
return validateUser(userMono)
.switchIfEmpty(saveUser(userMono))
.single();
}
This is the first method called by createUser. Note that it is not called from a switchIfEmpty() (see above) and it does emit an error if there is any.
private Mono<ServerResponse> validateUser(Mono<User> userMono) {
return userMono
.map(this::computeErrors)
.filter(AbstractBindingResult::hasErrors)
.flatMap(err ->
status(BAD_REQUEST)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromObject(err.getAllErrors()))
);
}
This is just a helper method:
private AbstractBindingResult computeErrors(User user) {
AbstractBindingResult errors = new BeanPropertyBindingResult(user, User.class.getName());
userValidator.validate(user, errors);
return errors;
}
This is the saveUser method. It does not emit any result!!. It is called from a switchIfEmpty (see above).
private Mono<ServerResponse> saveUser(Mono<User> userMono) {
return userMono
.flatMap(userRepository::save)
.flatMap(newUser -> status(CREATED)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromObject(newUser))
);
}
However, if I just call the saveUser method directly, it will emit a result.
//Works fine
public Mono<ServerResponse> createUser(ServerRequest serverRequest) {
Hooks.onOperatorDebug();
Mono<User> userMono = serverRequest.bodyToMono(User.class);
return saveUser(userMono) // Compare this to the above version
.single();
}
Can anyone please help figure out why the saveUser method does not emit anything when called from a switchIfEmpty()?
Here is the error I get:
java.util.NoSuchElementException: Source was empty
at reactor.core.publisher.MonoSingle$SingleSubscriber.onComplete(MonoSingle.java:165) ~[reactor-core-3.3.0.RC1.jar:3.3.0.RC1]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Assembly trace from producer [reactor.core.publisher.MonoSingleMono] :
reactor.core.publisher.Mono.single(Mono.java:3898)
org.example.contracttestingdemo.handler.UserHandler.createUser(UserHandler.java:55)
It's not to do with flatMap() or switchIfEmpty() directly - it's because you're trying to consume the same Mono twice:
Mono<User> userMono = serverRequest.bodyToMono(User.class);
return validateUser(userMono)
.switchIfEmpty(saveUser(userMono))
.single();
In the above example, you're passing userMono first to validateUser(), and then to saveUser() (but by that point the User has already been emitted.)
If you want the publisher to be subscribed to multiple times, and output the same result, you'll need to cache it by calling serverRequest.bodyToMono(User.class).cache();.

Vertx : how to separate routers to a different class keeping a single verticle

We have a MainVerticle which is growing with the number of routes such as :
router.get("/vehicles/:id").handler(ctx -> {
LOG.info("Entering get vehicle details");
getVehicleDetailsCounter.inc();
vehicleApiImpl.getVehicleDetails(ctx, httpClient);
});
router.post("/vehicles/:id/journey").handler(ctx -> {
// BOOK VEHICLE
LOG.info("Entering book vehicle");
startJourneyCounter.inc();
vehicleApiImpl.startJourney(ctx, httpClient);
});
router.post("/vehicles/:id/trips/:tripId/reports").handler(ctx -> {
LOG.info("Entering trip reports");
tripReportsCounter.inc();
getTripReports(ctx, httpClient);
});
router.get("/availableVehicles/:cityId").handler(ctx -> {
LOG.info("Entering available vehicles");
availableVehCounter.inc();
getAvailableVehicles(ctx, httpClient);
});
router.get("/zonesDetails/:cityId").handler(ctx -> {
LOG.info("Entering zones details");
getZonesDetailsCounter.inc();
vehicleApiImpl.getZonesDetails(ctx, httpClient);
});
router.get("/models").handler(ctx -> {
LOG.info("Entering models");
modelsCounter.inc();
vehicleApiImpl.getModels(ctx, httpClient);
});
router.get("/options").handler(ctx -> {
LOG.info("Entering options");
optionsCounter.inc();
vehicleApiImpl.getOptions(ctx, httpClient);
});
// ============================
// USER
// ============================
LOG.info("Handler register : USER");
// Payment Details
router.post("/user/notifyAppPaymentTransaction").handler(ctx -> {
LOG.info("Entering payment transaction");
notifyAppPaymentTransaction(ctx, httpClient);
});
// The user current journey
router.get("/user/currentJourney").handler(ctx -> {
LOG.info("Entering get current journey");
getCurrentJourneyCounter.inc();
userApiImpl.getCurrentJourney(ctx, httpClient);
});
// Create a new user
router.post("/users").handler(ctx -> {
LOG.info("Entering create user");
createUserCounter.inc();
createUser(ctx, httpClient);
});
...
We need to keep listening to a single IP : port.
What would be a good idea to break this MainVerticle into several classes while keeping the single verticle?
One obvious way would be static helper classes that take in the router and do the mappings inside. But in case there is an existing pattern in Vertx, using routers for example, it would really help.
You can for example extract your vehicle routes in a different handler. Then in the handler you can choose to implement your business logic there or better send a message through the eventbus, consume that message in any other Verticle, do your business logic there, and reply with an answer for the message, which you will send as a response.
router.route("/vehicles/*").handler(VehicleHandler.create(vertx, router));
VehicleHandler
public interface VehicleHandler extends Handler<RoutingContext> {
static VehicleHandler create(Vertx vertx, Router router) {
return new VehicleHandlerImpl(vertx, router);
}
}
VehicleHandlerImpl
public class VehicleHandlerImpl implements VehicleHandler {
private Router router;
public VehicleHandlerImpl(Vertx vertx, Router router) {
this.router = router;
router.get("/:id/").handler(this::getVehicle);
router.post("/:id/trips/:tripId/reports").handler(this::postReports);
router.post(":id/journey").handler(this::postJourney);
}
#Override
public void handle(final RoutingContext ctx) {
router.handleContext(ctx);
}
private void getVehicle(RoutingContext ctx) {
//Option A: do you business logic here
}
private void postReports(RoutingContext ctx) {
//Option B: send an eventbus message, handle the message in the MainVerticle and serve the response here
}
private void postJourney(RoutingContext ctx) {
//Option C: send an eventbus message, handle the message in a new Verticle and serve the response here
}
}
// Main Clss
class Main : AbstractVerticle() {
override fun start() {
val router = Router.router(vertx)
router.route().handler(BodyHandler.create())
router.get("/vehicles/:id").handler { req -> Controller.get_vehicle(req)}
router.get("/vehicles/:id/journey").handler{req-> Controller.startJourney(req)}
}
}
// Controller Class
open class Controller {
companion object {
fun get_vehicle(routingContext: RoutingContext) {
// enter code here
}
}
}

Stripes : RedirectResolution; How can I redirect to specific action event?

I have an action bean in my stripes application. The default handler/method will display a list of data, a list of all my MarketResearch objects
On my JSP, I can click on one to view its details, this takes me to a different JSP with a pre-populated form based on the particular MarketResearch object that you selected.
I have another method on my action bean which is mapped to the save submit button, this takes in what is on the amended form, and persists it. After this has taken place, I want it to redirect back to the form, rather than to the listing (default handler) action, is this possible?
My action is as follows :
public class MarketResearchAction extends BaseAction
{
#SpringBean
ClientService clientService;
private static final String VIEW = "/jsp/marketResearch.jsp";
private Client client;
private Client clientBeforeChanges;
public Client getClient()
{
return client;
}
public void setClient(Client client)
{
this.client = client;
}
#DefaultHandler
public Resolution viewAll()
{
return new ForwardResolution(VIEW);
}
public Resolution viewClientMarketResearch()
{
if (client.getSector().equals("Education"))
{
return new ForwardResolution("/jsp/marketResearchEducation.jsp");
} else if (client.getSector().equals("Local Government"))
{
return new ForwardResolution("/jsp/marketResearchLocalGovernment.jsp");
} else if (client.getSector().equals("Housing Association"))
{
return new ForwardResolution("/jsp/marketResearchHousing.jsp");
}
return new ForwardResolution("/jsp/viewClientMarketResearch.jsp");
}
public Resolution save()
{
clientBeforeChanges = clientService.getClientById(client.getId());
clientService.persistClient(client);
getContext().getMessages().add(new SimpleMessage("{0} updated", client.getName()));
return new RedirectResolution("/MarketResearch.action").flash(this);
}
public Client getClientBeforeChanges()
{
return clientBeforeChanges;
}
public void setClientBeforeChanges(Client clientBeforeChanges)
{
this.clientBeforeChanges = clientBeforeChanges;
}
public ClientService getClientService()
{
return clientService;
}
public void setClientService(ClientService clientService)
{
this.clientService = clientService;
}
}
Is it possible? Or am I approaching the situation from a bad angle and should re-factor?
Thanks
Yes. You could return a RedirectResolution to the form jsp. If you're having difficulty with the parameters, if you have them in the save() method, you could do like so:
return new RedirectResolution("/theJsp.jsp")
.addParameter("one", one)
.addParameter("two", two)
.addParameter("three", three)
.flash(this);
If you don't have the params that were passed to the form, you'll have to keep them going somehow. You could pass the MarketResearch object through the form so you'd have it there.
<stripes:hidden name="marketResearch" value="${ActionBean.marketResearch}"/>
And add the requisite instance variable/getter/setter on your MarketResearchActionBean.