How to process comma separated string of ids using Mono<String> - spring-boot

I have a api which gives me a list of comma separated ids and I am using Spring Boot WebClient to make a call to the same and process the response.
The API response is [1,2,3,4] in plain/text (not json) and I am doing the following to process the results -
Mono<String> ids = client.get()
.uri("http://localhost:8080")
.retrieve()
.bodyToMono(String.class);
I wanted to know how can I process the items inside the ids variable from thereon.
newbie to this api.

You could convert response directly into Flux, and process items one by one
webClient.get()
.uri("http://localhost:8080")
.retrieve()
.bodyToFlux(Integer.class)
.doOnNext(id -> {
System.out.println(id);
// process id...
})
.subscribe();
Another possible option is to convert Mono<String> into Flux<Integer> using method flatMapIterable:
ids.flatMapIterable(line -> Arrays.asList(line.replace("[", "").replace ("]", "").split(",")))
.doOnNext(id -> System.out.println(id))
.subscribe();

Related

How to extract content from Mono<List<T>> in WebFlux to pass it down the call chain

I want to be able to extract the List<Payload> from the Mono<List<Payload>> to pass it to a downstream service for processing (or maybe return from the read(RequestParams params) method, instead of it returning void):
#PostMapping("/subset")
public void read(#RequestBody RequestParams params){
Mono<List<Payload>> result = reader.read(params.getDate(), params.getAssetClasses(), params.getFirmAccounts(), params.getUserId(), params.getPassword());
....
}
where reader.read(...) is a method on an autowired Spring service utilizing a webClient to get the data from external web service API:
public Mono<List<Payload>> read(String date, String assetClasses, String firmAccounts, String id, String password) {
Flux<Payload> nodes = client
.get()
.uri(uriBuilder -> uriBuilder
.path("/api/subset")
.queryParam("payloads", true)
.queryParam("date", date)
.queryParam("assetClasses", assetClasses)
.queryParam("firmAccounts", firmAccounts)
.build())
.headers(header -> header.setBasicAuth("abc123", "XXXXXXX"))
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> {
System.out.println("4xx error");
return Mono.error(new RuntimeException("4xx"));
})
.onStatus(HttpStatus::is5xxServerError, response -> {
System.out.println("5xx error");
return Mono.error(new RuntimeException("5xx"));
})
.bodyToFlux(Payload.class);
Mono<List<Payload>> records = nodes
.collectList();
return records;
}
Doing a blocking result.block() is not allowed in WebFlux and throws an exception:
new IllegalStateException("block()/blockFirst()/blockLast() are blocking, which is not supported in thread ..." ;
What is a proper way to extract the contents of a Mono in WebFlux?
Is it some kind of a subscribe()? What would be the syntax?
Thank you in advance.
There is no "proper way" and that is the entire point. To get the value you need to block, and blocking is bad in webflux for many reasons (that I won't go into right now).
What you should do is to return the publisher all the way out to the calling client.
One of the things that many usually have a hard time understanding is that webflux works with a producer (Mono or Flux) and a subscriber.
Your entire service is also a producer, and the calling client can be seen as the subscriber.
Think of it as a long chain, that starts at the datasource, and ends up in the client showing the data.
A simple rule of thumb is that whomever is the final consumer of the data is the subscriber, everyone else is a producer.
So in your case, you just return the Mono<List<T> out to the calling client.
#PostMapping("/subset")
public Mono<List<Payload>> read(#RequestBody RequestParams params){
Mono<List<Payload>> result = reader.read(params.getDate(), params.getAssetClasses(), params.getFirmAccounts(), params.getUserId(), params.getPassword());
return result;
}
While the following does return the value of the Mono observable in the logs:
#PostMapping("/subset")
#ResponseBody
public Mono<ResponseEntity<List<Payload>>> read1(#RequestBody RequestParams params){
Mono<List<Payload>> result = reader.read(params.getDate(), params.getAssetClasses(), params.getFirmAccounts(), params.getUserId(), params.getPassword());
return result
.map(e -> new ResponseEntity<List<PayloadByStandardBasis>>(e, HttpStatus.OK));
}
the understanding I was seeking was a proper way to compose a chain of calls, with WebFlux, whereby a response from one of its operators/legs (materialized as as a result from a webclient call, producing a set of records, as above) could be passed downstream to another operator/leg to facilitate a side effect of saving those records in a DB, or something to that effect.
It would probably be a good idea to model each of those steps as a separate REST endpoint, and then have another endpoint for a composition operation which internally calls each independent endpoint in the right order, or would other design choices be more preferred?
That is ultimately the understanding I was looking for, so if anyone wants to share an example code as well as opinions to better implement the set of steps described above, I'm willing to accept the most comprehensive answer.
Thank you.

WebClient 5.3+ exchange vs exchangeToMono. Extracting cookies and body together

I am currently trying to consume a whole request coming back via webClient. I need to be able to read the body and the cookies that come with it. However I am currently having issues doing both together in one call. Below I have two ways of grabbing the data I want. I am pretty new at using lambdas and webClient. I do realize that exchangeToMono() is the latest method call to return a Mono or Flux because of the possible memory leak found in exchange().
I want to say subscribing() to a return monoBody might help me but so far I have had no luck extracting the data out that way as well. Thank you all for your input.
Mono<String> monoBody = webBuilder.build()
.get()
.uri(baseURI + "?Level=&pageSize=50&pageNo=&name=&id=&authToken=" + sessionToken)
.accept(MediaType.ALL)
.header("authToken", sessionToken)
.header("x-Token", sessionToken)
.header("X-token-X", sessionToken)
.header("Ref", ref)
.exchangeToMono(clientResponse -> clientResponse.bodyToMono(String.class));
Mono<MultiValueMap<String,ResponseCookie>> monoMap = webBuilder.build()
.get()
.uri(baseURI + "?Level=&pageSize=50&pageNo=&unitName=&id=&authToken=" + sessionToken)
.accept(MediaType.ALL)
.header("authToken", sessionToken)
.header("x-Token", sessionToken)
.header("X-token-X", sessionToken)
.header("Ref", ref)
.exchangeToMono(clientResponse -> Mono.fromCallable(()-> clientResponse.cookies() ));
The key thing to remember about the Reactor API (used by WebClient) is that you are always composing operations into a 'plan' of what to do and then at some point (subscription) you execute the entire plan.
In your case you've found the two methods to get the information you want but with a Reactive stream you can only ever have one type at any point in the stream, so if you need two types of data, you can always compose them into a composite object.
record DataWithCookies(MultiValueMap<String,ResponseCookie> cookies, String body){}
...
Mono<DataWithCookies> dataWithCookies =
webBuilder.build()
.get()
.uri(baseURI + "?Level=&pageSize=50&pageNo=&unitName=&id=&authToken=" + sessionToken)
.accept(MediaType.ALL)
.header("authToken", sessionToken)
.header("x-Token", sessionToken)
.header("X-token-X", sessionToken)
.header("Ref", ref)
.exchangeToMono(response ->
response.bodyToMono(String.class)
.map(stringBody -> new DataWithCookies(
stringBody,
response.cookies())
)
);
I used the previewed JDK 16 feature "Records" for brevity, but you could use a simple class for DataWithCookies.
You now have a Mono that will yield the body along with the cookies. You might want to even extract that lambda into a private method for readability.
...
.exchangeToMono(this::extractBodyAndCookies);
...
private DataWithCookies extractBodyAndCookies(final ClientResponse response) {
return response.bodyToMono(String.class)
.map(stringBody -> new DataWithCookies(stringBody,
response.cookies())
);
}
Another option is to use Tuples.of(stringBody, response.cookies()) which avoids creating another type while still composing the two data.

How can I flatMapMany with multiple web calls and conditionally signal complete?

I need to write a method which does
Gets a Location header form an endpoint
Generates a series of Some which each should retrieved from the Location fetched from the first endpoint.
Need to return a Flux<Some>
Which looks like this.
private WebClient client;
Flux<Some> getSome() {
// Get the Location header from an endpoint
client
.post()
.exchange()
.map(r -> r.headers().header("Location"))
// Now I need to keep invoking the Location endpoint for Some.class
// And Some.class has an attribute for pending/completion status.
// e.g.
while (true)
final Some some = client
.get()
.url(...) // the Location header value above
.retrieve()
.bodyToMono(Some.class)
.block()
;
}
}
How can I map the Mono<String> to a Flux<Some> while using the Some#status attribute for completion?
// should return Flux<Some>
client
.post()
.exchange()
.map(r -> r.headers().header("Location"))
.flatMapMany(location -> {
// invokes location while retrieved Some#status is not `completed`
// ##?
})
;
inside the flatMapMany, perform the call that retrieves the Location. apply a repeat().takeUntil(loc -> loc.complete()) to it (still inside the flatMapMany lambda)

Multiple Mono and common subscriber

I am a newbie to reactive programming in Java. I plan to use spring-webclient instead of restclient as the latter is being decommissioned. I have a situation when I make several http post requests to different endpoints and the response structure is identical. With webclient code as below,
List<Mono<CommonResponse>> monolist = new ArrayList<>();
for(String endpoint : endpoints) {
Mono<CommonResponse> mono = webClient.post()
.uri(URI.create(endPoint))
.body(Mono.just(requestData), RequestData.class)
.retrieve()
.bodyToMono(CommonResponse.class);
monolist.add(mono);
}
I get a mono per request. As the response is common, I would like each mono to be subscribed a common method, but how can I distinguish the endpoints, assuming that the response data is not helping.
Can I pass additional arguments to the method while subscribing?
You can do this in following way. If you have many monos you can treat team as flux which actually means that you have many of Mono. Then you can subscribe all of them with single method. To pass to subscribing method some extra arguments like information about endpoint, you can create extra object with additional information.
Flux<ResponseWithEndpoint> commonResponseFlux = Flux.fromIterable(endpoints)
.flatMap(endpoint -> webClient.post()
.uri(URI.create(endpoint))
.body(Mono.just(requestData), RequestData.class)
.retrieve()
.bodyToMono(CommonResponse.class)
.map(response -> new ResponseWithEndpoint(response, endpoint)));
...
class ResponseWithEndpoint {
CommonResponse commonResponse;
String endpoint;
public ResponseWithEndpoint(CommonResponse commonResponse, String endpoint) {
this.commonResponse = commonResponse;
this.endpoint = endpoint;
}
}

How to convert the body of a client response to a Mono of a certain type?

So, I send a request using the WebClient and after retrieving the response using exchange() I need to extract the body to a Mono of Object_1. The function needs to return Mono>. Normally you would use bodyToMono(SomeObject.class) but this doesn't work in this case.
public Mono<Object1<Object2>> getAll(String someParam) {
return WebClient.create(this.baseUrl)
.get()
.uri(uriBuilder -> uriBuilder.path("/some_path")
.queryParam("someParam", someParam)
.build())
.exchange()
.flatMap(clientResponse -> clientResponse.bodyToMono(????));
}
I am trying to figure out what to put inside the bodyToMono()
I believe you can do that with the overload of bodyToMono that takes a ParameterizedTypeReference.
ParameterizedTypeReference<Object1<Object2>> typeRef =
new ParameterizedTypeReference<Object1<Object2>>() {};
// . . .
.flatMap(clientResponse -> clientResponse.bodyToMono(typeRef));
You could write it inline, if you prefer and don't find it too hard to read:
.flatMap(clientResponse -> clientResponse.bodyToMono(
new ParameterizedTypeReference<Object1<Object2>>() {}));

Resources