I am a newbie in WebClient. I want to consume a rest service and replace some value and return the result.
this is the response I get from rest service:
[
{
"type": "somthing",
"details": {
"d1": "va1",
"d2": "va2",
"d3": "va3"
}
},
{
.....
},
...
]
This is the result I want to return to the user. (the new value is something that I get from the user, so I have it as a parameter.)
[
{
"type": "somthing",
"details": {
"d1": "va1",
"d2": "va2",
**"d3": "Replace with new value"**
}
},
{
.....
},
...
]
Flux<Item> items= webClient.get()
.uri("------URL------")
.retrieve()
.bodyToFlux(Item.class)
Above code correctly return items from the rest service and traditionally I can use collectList().block() to get a List and replace the value inside the object and return it.
I feel that it is an old-fashion way. Is there any better way to handle this situation by using WebClient functionality?
Thanks to #michalk I used map and it worked.
Flux<Item> items= webClient.get()
.uri("------URL------")
.retrieve()
.bodyToFlux(Item.class)..map(item-> {
item.getDetails().setDThree(request.getValue);
return item;
});
Related
I have this controller :
#GetMapping(value = "/stream")
public Flux<DataBuffer> stream() {
...
return webClient.get().uri(builder.buildAndExpand(parametres).toUriString())
.header(HttpHeaders.CONTENT_TYPE, TEXT_CSV).retrieve().bodyToFlux(DataBuffer.class);
}
}
When i called this endpoint with PostMan, i have this reponse :
[
{
"nativeBuffer": {
"direct": true,
"readOnly": false,
"contiguous": true,
"readable": true,
"writable": false
},
"allocated": true
},
...
My question is, how can i consume this endoint with webclient to read the content of flux. Or perhaps, the controller is wrong ?
I tried this, but it's don't work :
Flux<DataBuffer> fluxDataBuffer= webClient.get()
.uri("http://localhost:8080/stream")
.retrieve()
.bodyToFlux(DataBuffer.class);
DataBufferUtils.write(fluxDataBuffer, Paths.get("test.txt"),
StandardOpenOption.CREATE)
.share().block();
Thanks for your help
getUserDetails Method returns Mono of Type JsonNode. But I Actually want to return a Mono<User.java> or Flux<User.java>. please help modifying getBulkUserInfo or getUserDetails to get the Mono<User.java> or Flux<User.java>
public Mono<JsonNode> getUser(BigInteger Id){
return this.client.get()
.uri("/URL/{Id}",Id)
.retrieve()
.bodyToMono(JsonNode.class);
}
public Flux getBulkUsers(List<BigInteger> Ids){
return Flux.fromIterable(Ids).flatMap(this::getUser);
}
But The json response from the Url is something like
{
"resultholder": {
"totalResults": "1",
"profiles": {
"profileholder": {
"user": {
"country": "IND",
"zipCode": "560048",
"name":"Test"
}
}
}
}
}
I tried different ways but nothing worked subscribe() and
.doOnNext(resp -> resp.get("resultholder").get("profiles").get("profileholder").get("user"))
.bodyToMono(JsonNode.class)
.doOnNext(resp ->{return
JSONUtils.deserialize(resp.get("resultholder").get("profiles").get("profileholder").get("user"), User.class)})
This is pretty straightforward and there is no need to block. Its just applying further mappings on the response. You can use the following code for your problem
return webClient
.get()
.uri("profilesEndPoint/" + id)
.retrieve()
.bodyToMono(JsonNode.class)
.map(jsonNode ->
jsonNode.path("resultholder").path("profiles").path("profileholder").path("user")
).map(
userjsonNode -> mapper.convertValue(userjsonNode, User.class)
);
Where mapper is jackson ObjectMapper
private final ObjectMapper mapper = new ObjectMapper();
If you have any issues please refer to this code here :
I'm receiving JSON from REST API looks like:
{
"items": [
{
"id": 60659,
"name": "Display",
"active": true,
"account_id": 235
},
{
"id": 36397,
"name": " Mail Display",
"active": true,
"account_id": 107
}
]
}
I'm using this method to parse it:
Mono<List<Item>> getItems(String token) {
return webCLient
.get()
.headers(httpHeaders -> httpHeaders.setBearerAuth(token))
.retrieve()
.bodyToMono(ItemResponse.class)
.map(ItemResponse::getResponse)
.retryBackoff(RetrySettings.RETRIES, RetrySettings.FIRST_BACKOFF, RetrySettings.MAX_BACKOFF)
.doOnError(e -> log.error("error: " + e.getCause().toString()))
Response:
public class ItemResponse {
#JsonProperty("items")
private List<Item> response;
}
But sometimes 3rd party API returns different response without top level items property and looks like:
[
{
"id": 60659,
"name": "Display",
"active": true,
"account_id": 235
},
{
"id": 36397,
"name": " Mail Display",
"active": true,
"account_id": 107
}
]
At this point my app is crashing with JSON decoding error. I used for this case:
bodyToMono(new ParameterizedTypeReference<List<Item>>() {})
But I can't always refactoring this part of code just to handle their json. How to do it in dynamical way with Spring WebFlux? Like try -> parse#1 -> catch -> parse#2. So i need to parse json in way#1 and if error occurs app should try to parse it with way#2.
You can get the response as a string .bodyToMono(String.class) and do whatever you want, with multiple try catches... but I think your best bet is to create a custom Deserializer and use it with your WebClient via ExchangeStrategies like described here: How to customize SpringWebFlux WebClient JSON deserialization?
.
class MyResponse {
List<Object> data;
MyResponse(List<Object> data) {
this.data = data;
}
}
class MyResponseDeserializer extends JsonDeserializer<MyResponse> {
#Override
public MyResponse deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
TreeNode treeNode = jsonParser.getCodec().readTree(jsonParser);
List<Object> data = new ArrayList<>();
if (treeNode.isArray()) {
// parse it as array
} else {
// parse it as object and put inside list
}
MyResponse myResponse = new MyResponse(data);
return myResponse;
}
}
And then
WebClient getWebClient() {
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.addDeserializer(MyResponse.class, new MyResponseDeserializer());
objectMapper.registerModule(simpleModule);
ExchangeStrategies strategies = ExchangeStrategies
.builder()
.codecs(clientDefaultCodecsConfigurer -> {
clientDefaultCodecsConfigurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(objectMapper, MediaType.APPLICATION_JSON));
clientDefaultCodecsConfigurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper, MediaType.APPLICATION_JSON));
}).build();
return WebClient.builder().exchangeStrategies(strategies).build();
}
Mono<List<Item>> getItems(String token) {
return getWebClient()
.get()
.headers(httpHeaders -> httpHeaders.setBearerAuth(token))
.retrieve()
.bodyToMono(MyResponse.class)
.map(MyResponse::data)
.retryBackoff(RetrySettings.RETRIES, RetrySettings.FIRST_BACKOFF, RetrySettings.MAX_BACKOFF)
.doOnError(e -> log.error("error: " + e.getCause().toString()))
}
The rest is the same as in your example just change the class name and add appropriate fields.
And of course this is just a fast written demo and everything hardcoded and within a one method, better to have them injected
Code sample:-
public List<UserDto> getUserCandidates(String taskId) {
List<UserCandidates> listResponse;
ResponseEntity<String> response=restTemplate.getForEntity(configProperties.getUrl()+"/task/"+taskId+"/identity-links",
String.class);
listResponse =new Gson().fromJson(response.getBody(), new TypeToken<ArrayList<UserCandidates>>(){}.getType());
listResponse.forEach(result->{
if(!StringUtils.isEmpty(result.getUserId())){
ResponseEntity<UserRefer> userResponse=restTemplate.getForEntity(configProperties.getUrl()+"/user/"+result.getUserId()+"/profile", UserRefer.class);
userDtoList.add(new UserDto(result.getUserId(), Arrays.asList(result.getGroupId()), Arrays.asList(result.getType()), userResponse.getBody().getFirstName(),
userResponse.getBody().getLastName(), userResponse.getBody().getEmail()));
}
else if(!StringUtils.isEmpty(result.getGroupId())) {
ResponseEntity<String> responseGroup=restTemplate.getForEntity(configProperties.getUrl()+"/user"+"?memberOfGroup="+result.getGroupId(), String.class);
List<UserResponse> listGroup=new Gson().fromJson(responseGroup.getBody(), new TypeToken<ArrayList<UserResponse>>(){}.getType());
listGroup.forEach(resultGroup->{
userDtoList.add(new UserDto(resultGroup.getId(),Arrays.asList(result.getGroupId()),
Arrays.asList(result.getType()),resultGroup.getFirstName(),resultGroup.getLastName(),resultGroup.getEmail()));
});
}
});
return userDtoList;
}
So in if condition the response from API I'm getting is
UserRefer(id=demo, firstName=Demo, lastName=Demo, email=demo#camunda.org) - userResponse object
And from listResponse object data is [UserCandidates(userId=null, groupId=accounting, type=candidate), UserCandidates(userId=null, groupId=sales, type=candidate), UserCandidates(userId=demo, groupId=null, type=assignee)]
next in else if condition the response for listGroup is [UserResponse(status=null, id=demo, firstName=Demo, lastName=Demo, email=demo#camunda.org), UserResponse(status=null, id=mary, firstName=Mary, lastName=Anne, email=mary#camunda.org)]
So now you can see the data is duplicate. The output i want is for when userId is not empty from the data it should take type and merge the array
else if grouped not empty the data it should take for groupType and merge in the array removing duplicte and merging in same object
Output :-
[
{
"userId": "demo",
"name": "Demo Demo",
"type": [
"candidate",
"assignee"
],
"email": "demo#camunda.org",
"groupId": [
"accounting",
"sales"
]
},
{
"userId": "mary",
"name": "Mary Anne",
"type": [
"candidate"
],
"email": "mary#camunda.org",
"groupId": [
"accounting",
"sales"
]
}
]
You need some fundamental changes in your code.
1- instead of using ResponseEntity<String> use ResponseEntity<UserCandidates[]> response by this changing you don't need use Gson() dependency.
2- You don't need to use StringUtils to check to be empty. there is same method for both string and list objects.
3- For the duplicate date I define a Map<String,UserDto> with id as key and userDto object as a value. and where the userDto data is created I store it in the map with the id. as you see for storing userDto object in the map I used merge method that for the duplicate key(id) it has a merge function.
Tip: for readability would be nice to separate the restTemplate call in other class may you reuse it too.
mergeFunction is somthing like this:
private UserDto mergeFunction(UserDto u1,UserDto u2){
u1.getType().addAll(u2.getType());
u1.getGroupId().addAll(u2.getGroupId());
return u1;
}
and complete code is:
public List<UserDto> getUserCandidates(String taskId) {
Map<String, UserDto> userDtoMap = new HashMap<>();
Map<String, String> params = new HashMap<>();
ResponseEntity<UserCandidates[]> response = restTemplate
.getForEntity(configProperties.getUrl() + "/task/" + taskId + "/identity-links",
UserCandidates[].class, params);
Arrays.asList(response.getBody()).forEach(result -> {
if (!result.getUserId().isEmpty()) {
ResponseEntity<UserRefer> userResponse = restTemplate
.getForEntity(configProperties.getUrl() + "/**", UserRefer.class);
userDtoMap.merge(result.getUserId(), new UserDto(result.getUserId(),
new ArrayList<>(Arrays.asList(result.getGroupId())), Arrays.asList(result.getType()),
userResponse.getBody().getFirstName(),
userResponse.getBody().getLastName(),
userResponse.getBody().getEmail()), (u1, u2) -> mergeFunction(u1,u2));
} else if (!result.getGroupId().isEmpty()) {
String requestUri = configProperties.getUrl() + "/user" +
"?memberOfGroup={memberOfGroup}";
Map<String, String> userResParam = new HashMap<>();
userResParam.put("memberOfGroup", result.getGroupId());
ResponseEntity<UserResponse[]> responseGroup = restTemplate
.getForEntity(requestUri, UserResponse[].class, userResParam);
Arrays.asList(responseGroup.getBody()).forEach(resultGroup -> {
userDtoMap.merge(resultGroup.getId(), new UserDto(resultGroup.getId(),
Arrays.asList(result.getGroupId()),
Arrays.asList(result.getType()), resultGroup.getFirstName(),
resultGroup.getLastName(),
resultGroup.getEmail()), (u1, u2) -> mergeFunction(u1,u2));
});
}
});
return new ArrayList<>(userDtoMap.values());
}
I have a controller with method, which returns PagedResource, which looks like this:
#RequestMapping(value = "search/within", method = RequestMethod.POST)
public #ResponseBody PagedResources within(#RequestBody GeoJsonBody body,
Pageable pageable, PersistentEntityResourceAssembler asm) {
// GET PAGE
return pagedResourcesAssembler.toResource(page, asm);
}
Now, I want to add that method as a link to the root resource, so I do the following:
public RepositoryLinksResource process(RepositoryLinksResource repositoryLinksResource) {
repositoryLinksResource.add(linkTo(methodOn(ShipController.class).within(null, null, null)).withRel("within"));
return repositoryLinksResource;
}
Which works and I get my link, however it add that link without pagination parameters. So it look like this:
"within": {
"href": "http://127.0.0.1:5000/search/within"
},
and I want to turn it into:
"within": {
"href": "http://127.0.0.1:5000/search/within{?page, size}"
},
This previous question on stackoverflow suggests that after fixing the corresponding issue on GitHub it should work by default, however, it doesn't.
What am I doing wrong ?
Automatic Creation of Paginated Links with PagedResourcesAssembler
I had success using PagedResourcesAssembler. Let's say your entity is called
MyEntity. Your within method should return HttpEntity<PagedResources<MyEntity>>.
Your within method should look something similar to the example shown below.
#RequestMapping(value = "search/within", method = RequestMethod.POST)
#ResponseBody
public HttpEntity<PagedResources<MyEntity>>
within(#RequestBody GeoJsonBody body,Pageable pageable,
PagedResourcesAssembler assembler) {
// GET PAGE
Page<MyEntity> page = callToSomeMethod(pageable);
return new ResponseEntity<>(assembler.toResource(page), HttpStatus.OK);
}
Here is a simple example. In this example, the response looked like the one shown below,
{
"_embedded": {
"bookList": [
{
"id": "df3372ef-a0a2-4569-982a-78c708d1f609",
"title": "Tales of Terror",
"author": "Edgar Allan Poe"
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/books?page=0&size=20"
}
},
"page": {
"size": 20,
"totalElements": 1,
"totalPages": 1,
"number": 0
}
}
Manual Creation of Paginated Self Link
If you're interested in creating the paginated link manually, here's the code snippet you can use,
Page<MyEntity> page = callToSomeMethod(pageable);
ControllerLinkBuilder ctrlBldr =
linkTo(methodOn(ShipController.class).within(body, pageable, asm));
UriComponentsBuilder builder = ctrlBldr.toUriComponentsBuilder();
int pageNumber = page.getPageable().getPageNumber();
int pageSize = page.getPageable().getPageSize();
int maxPageSize = 2000;
builder.replaceQueryParam("page", pageNumber);
builder.replaceQueryParam("size", pageSize <= maxPageSize ?
page.getPageable().getPageSize() : maxPageSize);
Link selfLink =
new Link(new UriTemplate(builder.build().toString()), "self");