How to write custom insert Query Spring Webflux, and return custom response class? - spring

I am new to Reactive programming and I got stuck writing a custom Insert query.
So far I have a FriendshipRepository.java class.
public interface FriendshipRepository extends R2dbcRepository<Friendship, String> {
#Query(value = "INSERT INTO public.friendship(requester_id, addressee_id) values (:requesterid::uuid, :addresseeid::uuid)")
public Mono<Void> insertFriendRequest(
#Param("requesterid") String requesterId,
#Param("addresseeid") String addresseeId
);
}
And a FriendshipController.java class.
#RestController
public class FriendsController {
private final FriendshipRepository friendshipRepository;
public FriendsController(FriendshipRepository friendshipRepository) {
this.friendshipRepository = friendshipRepository;
}
#PostMapping(value = "/request", produces = "application/json")
public Mono<ResponseEntity<RequestResponse>> sendFriendRequest(#RequestBody FriendRequest friendRequest, #AuthenticationPrincipal Mono<User> principal) throws Exception {
String id = principal.map(User::getId).toFuture().get();
return friendshipRepository.insertFriendRequest(id, friendRequest.getUserId()).log()
.then(Mono.just("NEXT"))
.map(e -> {
return ResponseEntity.status(HttpStatus.CREATED).body(new RequestResponse("Success", ResponseCode.SUCCESS));
}).onErrorResume(e -> {
return Mono.just(ResponseEntity.status(HttpStatus.FORBIDDEN).body(new RequestResponse("Friend request was unsuccessful ", ResponseCode.REFUSED)));
});
}
}
This is a working example.
But I dont understand why I have to call .then(Mono.just("NEXT")) and create a new Mono to be able to return a custom ResponseEntity<RequestResponse>>. I also tried merge the the whole process. I meen by this at the begining when I get the Id from the ReactiveSpringSecutiryContext that is a blocking line of code and If I know it correctly that is a bad approach in Reactive programming.
I tried this approach but in this case, I can only retrun the Id of the user.
Mono<String> userId = principal.map(User::getId);
return userId.doOnNext(id -> {
friendshipRepository.insertFriendRequest(id, friendRequest.getUserId()).log()
.map(e -> {
return ResponseEntity.status(HttpStatus.CREATED).body(new RequestResponse("SIKER", ResponseCode.SUCCESS));
})
.onErrorResume(e -> {
return Mono.just(ResponseEntity.status(HttpStatus.FORBIDDEN).body(new RequestResponse("Friend request was unsuccessful ", ResponseCode.REFUSED)));
}).subscribe();
// .doOnSuccess(e -> ServerResponse.noContent().build((new RequestResponse("SIKER", ResponseCode.SUCCESS)), Void.class));
});
How could I rewrite this endpoint? Or does my whole approach inaproptirate?
Thank you in advance for your help.

Probably not the nicest solution, but better then it was.
My query:
public interface FriendshipRepository extends R2dbcRepository<Friendship, String> {
#Query(value = "INSERT INTO public.friendship(requester_id, addressee_id) values (:requesterid::uuid, :addresseeid::uuid) RETURNING id")
public Mono<String> insertFriendRequest(
#Param("requesterid") String requesterId,
#Param("addresseeid") String addresseeId
);
}
My api:
#PostMapping(value = "/request", produces = "application/json")
public Mono<ResponseEntity<RequestResponse>> sendFriendRequest(#RequestBody FriendRequest friendRequest, #AuthenticationPrincipal Mono<User> principal) throws Exception {
String id = principal.map(User::getId).toFuture().get();
return friendshipRepository.insertFriendRequest(id, friendRequest.getUserId()).log()
.map(e -> {
return ResponseEntity.status(HttpStatus.CREATED).body(new RequestResponse("Success", ResponseCode.SUCCESS));
}).onErrorResume(e -> {
return Mono.just(ResponseEntity.status(HttpStatus.FORBIDDEN).body(new RequestResponse("Friend request was unsuccessful ", ResponseCode.REFUSED)));
});
}

Related

Create Mono with Map object present within another Mono object

I am new to reactive and not able to get around this.
I have following Dtos:
public class User {
int id;
Map<String, Car> carsMap;
}
public class Car {
String carName;
}
// Response object
public class VehiclesInfo {
List<String> vehicleName;
}
From database I am getting Mono<User> when querying by userId.
And I have to return Mono<VehiclesInfo>.
So, I have to map the carsMap received from Mono<User> into List i.e. List of carName and set that into VehiclesInfo and return that as Mono i.e. Mono<VehiclesInfo>.
I am doing it like below. Please let me know how this can be done without blocking.
// userMono is returned by database query
Mono<User> userMono = getUserInfoById(userId);
Optional<User> userOptional = userMono.blockOptional();
if (userOptional.isPresent()) {
User user1 = userOptional.get();
Flux<Car> carFlux = Flux.fromIterable(user1.getCarsMap().keySet())
.flatMap(i -> {
final Car c = new Car();
c.setCarName(i);
return Mono.just(c);
});
carFlux.subscribe(c -> System.out.println(c.getCarName()));
}

WebClient: Waiting for blocking call in API to perform a new call

I am currently making a microservice to create test users for our automated test environment. The database is reachable through another API, so in order to create a test user, I need to perform a call to this API.
The test users should be created and then later disposed of when the test has been executed. The identifier of the test users is the SSN (national identifier digit), and it is unique to each citizen. My API/microservice generates a new user with a generated SSN and should post it to a DB over the API to the backend service controlling the database. This backend service is not reactive.
The problem is that in the database, there are already many existing users which are used by other tests executed manually. The existing test users cannot be tampered with, so I need to verify that the generated SSN is not already existing in the DB.
My approach is as follows:
generate a new ssn
while(ssn exists in db){
generate new ssn
}
post generated user to db
However, when placing a .block() on the check if the user exists (bad practice, I know...) the program halts in a deadlock and nothing happens.
My controller:
#ResponseBody
#PostMapping("normal")
public Mono<User> createNormalUser() throws Exception {
return userService.createNormalUser();
}
#ResponseBody
#GetMapping("{ssn}")
public Mono<User> getUserBySSN(#PathVariable String ssn){
return userService.getUserBySsn(ssn);
}
My service:
public Mono<User> createNormalUser(){
String ssn = generateNewSsnNotInDB();
Mono<UserResource> newUserMono = Mono.just(
UserResource.builder()
.ssn(ssn)
.email(ssn + "-test#somedomain.com")
.state("NORMAL")
.preferred2FaMethod("some2FAMethod")
.build()
);
return postUser(newUserMono)
.then(updatePassword(ssn))
.then(setState(ssn, "NORMAL"));
}
private String generateNewSsnNotInDB() {
String ssn;
boolean userExists = false;
do {
ssn = ssnGenerator.generateOneValidSsnOnDate(ssnGenerator.generateRandomSsnDate());
userExists = checkIfUserExists(ssn);
} while (userExists);
return ssn;
}
private boolean checkIfUserExists(String ssn) {
User user;
try {
user = getUserBySsn(ssn).share().block();
return true;
} catch (WebClientResponseException.NotFound exception) {
return false;
}
}
public Mono<User> getUserBySsn(String ssn) {
return webClient.get()
.uri(userBySsnURI(ssn))
.retrieve()
.bodyToMono(User.class);
}
public Mono<User> postUser(Mono<UserResource> userMono) {
return webClient.post()
.uri(setUserURI())
.body(userMono, UserResource.class)
.retrieve()
.bodyToMono(User.class);
}
public Mono<User> postUser(User user) {
user.setPid(generateNewSsnNotInDB());
UserResource res = UserResource.builder()
.ssn(user.getPid())
.email(user.getEmail())
.phoneNumber(user.getPhoneNumber())
.state(user.getState())
.preferred2FaMethod(user.getPreferred2FaMethod())
.password(user.getPassword())
.build();
log.info("Resource generated in post-user is: " + res.toString());
return postUser(Mono.just(res));
}
public Mono<User> updatePassword(String ssn) {
Mono<User> user = Mono.just(User.builder()
.pid(ssn)
.password("password01")
.build());
return webClient.patch()
.uri(setUpdatePasswordURI())
.body(user, User.class)
.retrieve()
.bodyToMono(User.class);
}
private Mono<User> setState(String ssn, String state) {
return webClient.put()
.uri(updateStateURI(ssn, state))
.retrieve()
.bodyToMono(User.class);
}
I have chained the calls in the createNormalUser function because the backend requires this sequence in order to set the required attributes for the user. I am not sure why this is the required sequence, and changing this is not part of my scope.
I have also omitted some functions which probably aren't relevant for this question.
Can somebody please help me in the right direction on how to perform the calls with checkIfUsersExist and then post the user? I have been trying to wrap my head around this for a week now with no luck.
The strangest thing is that if I first call getUser with a valid ssn, then postUser works fine. If I try to call postUser without calling getUser first, it deadlocks on the .block().
Avoid the block() call and user chained calls instead as follows (createNormalUser() and generateNewSsnNotInDB() were updated and checkIfUserExists() deleted):
public Mono<User> createNormalUser(){
Mono<UserResource> newUserMono = generateNewSsnNotInDB().map( ssn ->
UserResource.builder()
.ssn(ssn)
.email(ssn + "-test#somedomain.com")
.state("NORMAL")
.preferred2FaMethod("some2FAMethod")
.build()
);
return postUser(newUserMono)
.then(updatePassword(ssn))
.then(setState(ssn, "NORMAL"));
}
private Mono<String> generateNewSsnNotInDB() {
return Mono.just(ssnGenerator.generateOneValidSsnOnDate(ssnGenerator.generateRandomSsnDate()))
.flatMap(ssn -> getUserBySsn(ssn))
.switchIfEmpty(Mono.defer(() -> generateNewSsnNotInDB()));
}
public Mono<User> getUserBySsn(String ssn) {
return webClient.get()
.uri(userBySsnURI(ssn))
.retrieve()
.bodyToMono(User.class);
}
public Mono<User> postUser(Mono<UserResource> userMono) {
return webClient.post()
.uri(setUserURI())
.body(userMono, UserResource.class)
.retrieve()
.bodyToMono(User.class);
}
public Mono<User> postUser(User user) {
user.setPid(generateNewSsnNotInDB());
UserResource res = UserResource.builder()
.ssn(user.getPid())
.email(user.getEmail())
.phoneNumber(user.getPhoneNumber())
.state(user.getState())
.preferred2FaMethod(user.getPreferred2FaMethod())
.password(user.getPassword())
.build();
log.info("Resource generated in post-user is: " + res.toString());
return postUser(Mono.just(res));
}
public Mono<User> updatePassword(String ssn) {
Mono<User> user = Mono.just(User.builder()
.pid(ssn)
.password("password01")
.build());
return webClient.patch()
.uri(setUpdatePasswordURI())
.body(user, User.class)
.retrieve()
.bodyToMono(User.class);
}
private Mono<User> setState(String ssn, String state) {
return webClient.put()
.uri(updateStateURI(ssn, state))
.retrieve()
.bodyToMono(User.class);
}

How to return Mono<ServerResponse>

Please see my question embedded in the comments section of the code below -
// This should return a list of all ids and their name and status info, e.g. as json
public Mono<ServerResponse> f1(String originalId) {
// this gives list of ids which are comma separated
Mono<String> ids = f2(originalId);
ids.flatMapIterable(line -> Arrays.asList(line.split(COMMA)))
.doOnNext(id -> {
Mono<String> idName = f3(id);
Mono<String> idStatus = f4(id);
Mono<Tuple2<String, String>> combined = Mono.zip(idName, idStatus);
// How do i return all the combined Mono tuples as Mono<ServerResponse>
}).subscribe();
// Need to return proper Mono - not empty
return Mono.empty();
}
Also I am not sure whether I should return a Flux or a Mono as return type of f1() as its a list of ids and their respective values
You need to return Mono<ServerResponse>.
public Mono<ServerResponse> f1(String key) {
Mono<String> ids = f2(key);
Flux<IdInfo> idInfoFlux = ids.flatMapIterable(line -> Arrays.asList(line.split(",")))
.flatMap(id -> {
Mono<String> idName = getIdName(id);
Mono<String> idStatus = getIdStatus(id);
return Mono.zip(idName, idStatus, (name, status) -> new IdInfo(name, status));
})
.doOnNext(id -> System.out.println(id));
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(idInfoFlux, IdInfo.class);
}
// your DTO class that you would like to pass in response as json
public static class IdInfo {
String idName;
String idStatus;
public IdInfo(String idName, String idStatus) {
this.idName = idName;
this.idStatus = idStatus;
}
public String toString() {
return String.format("IdInfo [idName=%s, idStatus=%s]", idName, idStatus);
}
}

UpdateById method in Spring Reactive Mongo Router Handler

In Spring Reactive Java how can I write an updateById() method using the Router and Handler?
For example, the Router has this code:
RouterFunctions.route(RequestPredicates.PUT("/employees/{id}").and(RequestPredicates.accept(MediaType.APPLICATION_JSON))
.and(RequestPredicates.contentType(MediaType.APPLICATION_JSON)),
employeeHandler::updateEmployeeById);
My question is how to write the employeeHandler::updateEmployeeById() keeping the ID as same but changing the other members of the Employee object?
public Mono<ServerResponse> updateEmployeeById(ServerRequest serverRequest) {
Mono<Employee> employeeMono = serverRequest.bodyToMono(Employee.class);
<And now what??>
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(employeeMono, Employee.class);
}
The Employee class looks like this:
#Document
#Data
#AllArgsConstructor
#NoArgsConstructor
public class Employee {
#Id
int id;
double salary;
}
Thanks for any help.
First of all, you have to add ReactiveMongoRepository in your classpath. You can also read about it here.
#Repository
public interface EmployeeRepository extends ReactiveMongoRepository<Employee, Integer> {
Mono<Employee> findById(Integer id);
}
Then your updateEmployeeById method can have the following structure:
public Mono<ServerResponse> updateEmployeeById(ServerRequest serverRequest) {
return serverRequest
.bodyToMono(Employee.class)
.doOnSubscribe(e -> log.info("update employee request received"))
.flatMap(employee -> {
Integer id = Integer.parseInt(serverRequest.pathVariable("id"));
return employeeRepository
.findById(id)
.switchIfEmpty(Mono.error(new NotFoundException("employee with " + id + " has not been found")))
// what you need to do is to update already found entity with
// new values. Usually map() function is used for that purpose
// because map is about 'transformation' what is setting new
// values in our case
.map(foundEmployee -> {
foundEmployee.setSalary(employee.getSalary());
return foundEmployee;
});
})
.flatMap(employeeRepository::save)
.doOnError(error -> log.error("error while updating employee", error))
.doOnSuccess(e -> log.info("employee [{}] has been updated", e.getId()))
.flatMap(employee -> ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(BodyInserters.fromValue(employee), Employee.class));
}
UPDATE:
Based on Prana's answer, I have updated the code above merging our solutions in one. Logging with a help of Slf4j was added. And switchIfEmpty() functions for the case when the entity was not found.
I would also suggest your reading about global exception handling which will make your API even better. A part of it I can provide here:
/**
* Returns routing function.
*
* #param errorAttributes errorAttributes
* #return routing function
*/
#Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
}
private HttpStatus getStatus(Throwable error) {
HttpStatus status;
if (error instanceof NotFoundException) {
status = NOT_FOUND;
} else if (error instanceof ValidationException) {
status = BAD_REQUEST;
} else {
status = INTERNAL_SERVER_ERROR;
}
return status;
}
/**
* Custom global error handler.
*
* #param request request
* #return response
*/
private Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
Map<String, Object> errorPropertiesMap = getErrorAttributes(request, false);
Throwable error = getError(request);
HttpStatus errorStatus = getStatus(error);
return ServerResponse
.status(errorStatus)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromValue(errorPropertiesMap));
}
A slightly different version of the above worked without any exceptions:
public Mono<ServerResponse> updateEmployeeById(ServerRequest serverRequest) {
Mono<ServerResponse> notFound = ServerResponse.notFound().build();
Mono<Employee> employeeMono = serverRequest.bodyToMono(Employee.class);
Integer employeeId = Integer.parseInt(serverRequest.pathVariable("id"));
employeeMono = employeeMono.flatMap(employee -> employeeRepository.findById(employeeId)
.map(foundEmployee -> {
foundEmployee.setSalary(employee.getSalary());
return foundEmployee;
})
.flatMap(employeeRepository::save));
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(employeeMono, Employee.class).switchIfEmpty(notFound);
}
Thanks to Stepan Tsybulski.

Map Java Object

I want to create a service in order to populate dropdown from database. I tried this:
Merchant Class:
export class Merchant {
constructor(
public id: string,
public name: string,
public state_raw: string,
public users: string,
) {}
}
Merchant Service:
getList(): Observable<Merchant> {
return this.http.get<Merchant>(environment.api.urls.merchants.base, {});
}
Rest Api impl:
#GetMapping
public ResponseEntity<?> get() {
return merchantRepository
.findAll()
.map(mapper::toDTO)
.map(ResponseEntity::ok)
.orElseGet(() -> notFound().build());
}
SQL query:
#Override
public Iterable<Merchants> findAll() {
String hql = "select e from " + Merchants.class.getName() + " e";
TypedQuery<Merchants> query = entityManager.createQuery(hql, Merchants.class);
List<Merchants> merchants = query.getResultList();
return merchants;
}
But I get this error:
The method map(mapper::toDTO) is undefined for the type Iterable<Merchants>
How should I implement properly this mapping for the response?
Seems like you meant to stream the entities.
#GetMapping
public ResponseEntity<?> get() {
return StreamSupport.stream(merchantRepository.findAll().spliterator(), false)
.map(mapper::toDTO)
.map(ResponseEntity::ok)
.findFirst()
.orElseGet(() -> notFound().build());
}

Resources