Explanation of the question is bit a long. Kindly take a minute and help!
I have two http calls which will give the following data.
1st http request call will return <Mono<List<Chips>>
[
{
"id": 1,
"name": "redlays"
},
{
"id": 2,
"name": "yellowlays"
},
{
"id": 3,
"name": "kurkure"
}
]
Chips Model is
#Data
#Setter
#Getter
#NoArgsConstructor
#AllArgsConstructor
public class Chips {
private int id;
private String name;
}
2nd http request call will return Mono<ChipsDetails> based on Id
{
"id": 1,
"color": "red",
"grams": "50"
}
ChipsDetails Model as below,
#Data
#Setter
#Getter
#NoArgsConstructor
#AllArgsConstructor
public class ChipsDetails {
private int id;
private String color;
private String grams;
}
I have done the Implementation using Webflux. Here I have used three models which are Chips, ChipsDetails and ChipsFullDetails.
Model Chips will have two attributes id and name then Model ChipsDetails will have three attributes id,color and grams whereas Model ChipsFullDetails will have combination of Chips and ChipsDetails attributes which are id, name, color and grams
#RestController
#RequestMapping("/chips")
public class ChipsController {
#Autowired
ChipsService chipsService;
#GetMapping
public Mono<List<ChipsFullDetails>> getAllChips() {
return chipsService.getChips()
.map(f -> {
List<ChipsFullDetails> chipsFullDetails = new ArrayList<>();
f.forEach(a -> {
ChipsFullDetails chipsFullDetail = new ChipsFullDetails();
chipsFullDetail.setId(a.getId());
chipsFullDetail.setName(a.getName());
chipsService.getChipsDetails(a.getId())
.subscribe(b -> {
chipsFullDetail.setColor(b.getColor());
chipsFullDetail.setGrams(b.getGrams());
});
chipsFullDetails.add(chipsFullDetail);
});
return chipsFullDetails;
}
);
}
}
Here chipsService.getChips() will return Mono<List<Chips>> This is the 1st call and chipsService.getChipsDetails(a.getId()) will return Mono<ChipsDetails> This is the 2nd http request call.
The result of the implementation will be ChipsFullDetails
#Data
#Setter
#Getter
#NoArgsConstructor
#AllArgsConstructor
public class ChipsFullDetails {
private int id;
private String name;
private String color;
private String grams;
}
The problem is ChipsFullDetails returns null for color and grams attributes which we are getting from the 2nd http call even though it is subscribed inside.
How to achieve when Second Http call i.e chipsService.getChipsDetails(a.getId()) depending on the result of 1st http call (chipsService.getChips()) in asynchronous way?
Is this possible to achieve without blocking both the calls?
I'd transform the initial Mono<List<Chips>> into a Flux<Chips> first, so that you can flatMap on each element, e.g. something along those lines:
public Mono<List<ChipsFullDetails>> getAllChips() {
return chipsService
.getChips()
// Mono<List> to Flux:
.flatMapIterable(Function.identity())
// flat map on each element:
.flatMap(this::buildChipsFullDetails)
// Flux back to Mono<List>:
.collectList();
}
private Mono<ChipsFullDetails> buildChipsFullDetails(Chips chips) {
return chipsService
.getChipsDetails(chips.getId())
// once you get both chips and details, aggregate:
.map(details -> buildChipsFullDetails(chips, details));
}
private ChipsFullDetails buildChipsFullDetails(Chips chips, ChipsDetails details) {
// straightforward synchronous code:
ChipsFullDetails chipsFullDetail = new ChipsFullDetails();
chipsFullDetail.setId(chips.getId());
chipsFullDetail.setName(chips.getName());
chipsFullDetail.setColor(details.getColor());
chipsFullDetail.setGrams(details.getGrams());
return chipsFullDetail;
}
I basically disagree with the idea of working with a Flux, though I admit I have it as well.
I would say that if you want to get details for a list of chips then you should make an endpoint that does that. Then it will be a single call.
For you original question, there is a way to do it without going to Flux, but it reads a bit funny:
ParameterizedTypeReference<List<Chip>> chipList = new ParameterizedTypeReference<List<Chip>>() {};
public Mono<List<ChipDetails>> getChipDetails() {
return webClient.get().uri("chips").retrieve().bodyToMono(chipList).flatMap(chips -> {
return Mono.zip(chips.stream().map(chip -> webClient.get().uri("chipDetails?id="+chip.getId()).retrieve().bodyToMono(ChipDetails.class)).collect(Collectors.toList()), details -> {
List<ChipDetails> chipDetails = new ArrayList<>();
for (Object o : details) {
chipDetails.add((ChipDetails) o);
}
return chipDetails;
});
});
}
This uses Mono.zip to create a sort of batch request out of each of the Chip entries in the list executes them all at once. Flux will probably end up doing more or less the same thing but not really.
If you just make the endpoint you need, then:
ParameterizedTypeReference<List<ChipDetails>> detailsList = new ParameterizedTypeReference<List<ChipDetails>>() {};
public Mono<List<ChipDetails>> getChipDetailsReal() {
return webClient.post().uri("chipDetails").body(webClient.get().uri("chips").retrieve().bodyToMono(chipList), chipList).retrieve().bodyToMono(detailsList);
}
This approach avoids repeated calls to the same endpoint and is doing what you want.
I'm not a fan of using Flux when you really mean List. A Flux is a streaming thing with backpressure and sophisticated capabilities whereas a List is just a List.
Related
I'm having some trouble mapping a GraphQL response to a class and I'm probably missing something obvious so I need a second pair of eyes on my code I think.
I have a response that looks like this:
{
"data": {
"Area": [
{
"id": "1"
},
{
"id": "2"
}
]
}
}
My consumer and client looks like this:
public class Consumer {
private final WebClient webClient;
private static final String QUERY = "query { Area { id }}";
public Consumer(WebClient webClient) {
this.webClient = webClient;
}
public AreaResponse.AreaData getAreas() {
var request = new GraphQLRequest(QUERY, null);
var res = webClient.post()
.bodyValue(request)
.retrieve()
.bodyToMono(AreaResponse.class)
.block();
return res.getData();
}
}
And finally my response dto:
#Data
public class AreaResponse {
private AreaData data;
#Data
public class AreaData{
private Collection<Area> Area;
#Data
public class Area{
private String id;
}
}
}
If I map the response to String.class I get the same data as in Postman so there's nothing wrong with the query or the endpoint. But when I try to map to AreaResponse the Area object is null. I am not sure if the hierarchy in my response class is correct or if I should move the Collection a step down?
I think the problem comes from the use of upper-case Area in the response. By default, Jackson will try to map all of the stuff to a lower-case version of the class name (or lower-camelcase).
One way to fix it:
#JsonProperty("Area")
private Collection<Area> Area;
P.S. nested/ inner classes can be static, so public static class AreaData etc.
what I am trying to do is,
If I take one pojo class like
#Entity
#Table(name = "property_table")
public class Property {
#Id
#Column(name = "property_id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int propertyId;
#Column(name = "property_name")
private String propertyName;
#Column(name = "property_type")
private String propertyType;
}
In RestController I wrote Two Methods like
#GetMapping(value = "/getProperties", produces = { "application/json",
"application/xml" }, consumes = { "application/xml", "application/json" })
#ResponseBody
public List<Property> getProperties() {
//some code
}
#GetMapping(value = "/getPropertyById", produces = { "application/json",
"application/xml" }, consumes = { "application/xml", "application/json" })
#ResponseBody
public Property getPropertyById() {
//some code
}
So, hear what I am trying to do is
for first api method I want return json like some parameters from Property pojo class i.e., like
for getProperties api method
{
"property":[
{
"propertyId":001,
"propertyName":"PROPERTY 1"
},
{
"propertyId":002,
"propertyName":"PROPERTY 2"
}
],
In the Above json I want to return only two parameters i.e propertyId,propertyName and remaining parameter i.e propertyType I dont want to retun in json.
How to return like that?
and for the second api method I want to return all three parameters. i.e., like below
for getPropertyById api method
{
"propertyId":001,
"propertyName":"PROPERTY 1",
"propertyType:"PROPERTY_TYPE 1"
},
how to maintain different json response using same pojo class with different parameters for different api methods.
please help me to solve this isuue.
Thanks.
REST API under/over-fetching is a well-known problem. There's only two (classical ways) to handle that.
The first one is to build one model per each attribute visibility state. So, in your case, you'll need to create two different models (this kind of models are called DTO - Data Transfert Object). One model will have a propertyType attribute, the other will not. The model Property you've shared shows that you use the same class as entity and as transfert object. This solution will add some complexity to your app because you will have to implement some mappers to convert your entity to a corresponding DTO.
The second one is to accept that you send an attribute that will not be useful (be aware of the over-fetching). This solution is often the most adopted one. The cons of this solution is when you don't want to send something to your client (imagine a User model, you want to get the password from your client but you don't want to sent it back to it). Another obvious negative point is that the transactions will be larger but it is negligible in most cases
I would strongly advice you to keep your #Entity isolated in the 'db' layer. So that changes on the database side don't affect your API and vice versa. Also, you will have much better control over what data is exposed in your API. For your needs you can create 2 true DTOs, like PropertyDto and PropertyDetailsDto (or using private fields and getters/setters).
public class PropertyDto {
public String propertyId;
public String propertyName;
}
public class PropertyDetailsDto extends PropertyDto {
public String propertyType;
}
Map your #Entity to a specific dto corresponding to your needs.
EDIT
public List<PropertyDto> getProperties() {
return toPropertyDtos(repository.findAll());
}
public PropertyDetailsDto getPropertyById(Long id) {
return toPropertyDetailsDto(repository.findBy(id));
}
in some Mapper.java
...
public static List<PropertyDto> toPropertyDtos(List<Property> properties) {
return properties.stream()
.map(Mapper::toPropertyDto)
.collect(toList());
}
private static PropertyDto toPropertyDto(Property property) {
PropertyDto dto = new PropertyDto();
dto.propertyId = property.propertyId;
dto.propertyName = property.propertyName;
return dto;
}
// same stuff for `toPropertyDetailsDto`, you could extract common mapping parts in a separate private method inside `Mapper`
...
Given a RESTful web service developed using the Spring Boot framework, I wanted a way to suppress the birthDate of all Users in the response. This is what I implemented after looking around for a solution :
#RestController
public class UserResource {
#Autowired
private UserDAOservice userDAOService;
#GetMapping("/users")
public MappingJacksonValue users() {
List<User> users = userDAOService.findAll();
SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter
.filterOutAllExcept("id", "name");
FilterProvider filters = new SimpleFilterProvider().addFilter(
"UserBirthDateFilter", filter);
MappingJacksonValue mapping = new MappingJacksonValue(users);
mapping.setFilters(filters);
return mapping;
}
}
However, when I hit the rest end point in the browser, I can still see the birth date of the user in the response :
{
"id": 1,
"name": "Adam",
"birthDate": "1980-03-31T16:56:28.926+0000"
}
Question 1 : What API can I use to achieve my objective?
Next, assuming that I want to adhere to HATEOAS in combination with filtering, how can I go about doing this. I am unable to figure out the APIs that can be used for using these two features together :
#GetMapping("/users/{id}")
public EntityModel<User> users(#PathVariable Integer id) {
User user = userDAOService.findById(id);
if (user == null) {
throw new ResourceNotFoundException("id-" + id);
}
EntityModel<User> model = new EntityModel<>(user);
WebMvcLinkBuilder linkTo = linkTo(methodOn(this.getClass()).users());
model.add(linkTo.withRel("all-users"));
//how do I combine EntityModel with filtering?
return model;
}
Question 2 : How do I combine EntityModel with MappingJacksonValue?
Note : I am aware of #JsonIgnore annotation but that would apply the filter for all end points that use the domain; however, I want to restrict the filtering only to the two endpoints above.
Turns out for this to work, I have to add the #JsonFilter annotation above the DTO and provide the same name that was used while creating the SimpleFilterProvider.
#JsonFilter("UserBirthDateFilter")
public class User {
private Integer id;
#Size(min=2, message="user name must be atleast 2 characters")
#ApiModelProperty(notes="user name must be atleast 2 characters")
private String name;
#Past
#ApiModelProperty(notes="birth date cannot be in the past")
private Date birthDate;
//other methods
}
There is an easier way to do this, on your transfer object (the class you are sending back to the client), you can simply use the #JsonIgnore annotation to make sure the field is not serialized, and therefore sent to the client. So simply add #JsonIgnore inside your User class for your birthDay field.
You can also read more here about this approach:
https://www.baeldung.com/jackson-ignore-properties-on-serialization
If you need to return a different object for different endpoints (User without birthDay in your case, only for specific) you should create separate transfer objects and use those for their respective endpoints. You can pass your original entity (User) in the constructor to those classes and copy over all fields needed.
You can use Jackson's #JsonView feature. With this, you can tell a certain request mapping to produce serialized JSON with chosen set of properties.
public class View {
interface UserDetails {}
}
public class User {
#JsonView(View.UserDetails.class)
private Long id;
#JsonView(View.UserDetails.class)
private String name;
private String birthdate;
}
Controller be like
#JsonView(View.UserDetails.class)
#GetMapping("/users")
public MappingJacksonValue users() {
....
}
For question 2, I had the exact same question as you did, and here's what I did. It seems to be working:
#GetMapping(path = "/users/{id}")
public MappingJacksonValue retrieveUser(#PathVariable int id){
User user = service.findOne(id);
if(user==null){
throw new UserNotFoundException("id-"+id);
}
//"all-users", SERVER_PATH + "/users"
EntityModel<User> resource = EntityModel.of(user);
WebMvcLinkBuilder linkTo =
linkTo(methodOn(this.getClass()).retrieveAllUsers());
resource.add(linkTo.withRel("all-users"));
SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter.filterOutAllExcept("id");
FilterProvider filters = new SimpleFilterProvider().addFilter("UserFilter",filter);
MappingJacksonValue mapping = new MappingJacksonValue(resource);
mapping.setFilters(filters);
return mapping;
}
Response for HTTP GET localhost:8080/users/1
{
"id": 1,
"links": [
{
"rel": "all-users",
"href": "http://localhost:8080/users"
}
]}
In my spring boot application, I have a Question as a resource, with following fields.
[
{
"questionId":6,
"area":"TECHNICAL",
"title":"Find the index of first 1 in an infinite sorted array of 0s and 1s",
"description":"Given an infinite sorted array consisting 0s and 1s. The problem is to find the index of first 1 in that array. As the array is infinite, therefore it is guaranteed that number 1 will be present in the array.",
"state":"ACTIVE",
"difficultyLevel":"EASY",
"skills":[
{
"skillId":1,
"skillName":"ALGORITHM"
},
{
"skillId":2,
"skillName":"PROGRAMMING"
}
],
"proposedBy":"agrawalo",
"noOfTimesUsed":0,
"examples":null,
"probes":null,
"approvedBy":null,
"addedBy":null,
"dateCreated":"2018-05-16T19:29:11.113",
"dateLastUpdated":"2018-05-16T19:29:11.113"
},
{
...
},
...
]
I want to write controllers to filter questions.
For example:
1./questions?area="technical". A controller that returns questions with area as "technical". For this I wrote following method.
#RestController
public class QuestionController {
#RequestMapping("/questions", method = GET)
String getFilteredQuestions(#RequestParam("area") String questionArea) {
}
}
2./questions?area="technical"&skill="programming". Now, I want to write a controller that returns question with area as "technical" and skill as "programming".
One way I can achieve this is by adding one more request param to the "getFilteredQuestion" method.
But with that I will end up writing a very messy code (say if I want to add more filters) that checks what request param is null and what is not and based on that apply filters.
#RestController
public class QuestionController {
#RequestMapping("/questions", method = GET)
String getFilteredQuestions(#RequestParam("area") String questionArea, #RequestParam("skill") String questionSkill) {
}
}
3./questions?skill="algorithm"&proposedby="agrawalo" ...
Any clean way of writing such controllers?
Maybe you can write Question class as an Entity class.
public class Question {
private String title;
private String description;
private List<String> skills;
private Date createdAt;
public void setTitle(String title) {
this.title = title;
}
public String getTitle() {
return this.title;
}
}
And then in this way you can get your data like that
#RestController
public class QuestionController {
#RequestMapping("/questions", method = GET)
String getFilteredQuestions#RequestBody Question question) {
}
}
Create an Java POJO with all the query and filter parameters you support. Make sure you can map that POJO to JSON and vice versa. Your client can edit the JSON version and send it along with the request. You map it back to your POJO and pass it to the business logic that takes care of preparing the appropriate query. That way, you can leave your Controller's methods untouched.
I have to parse a REST response in json and it has a lot of nested lists with many objects.
The response contains an item called "ObjectList" which has a list and inside, two elements, "ObjectA" and "ObjectB". I don't know how to parse the response to objects using Jackson annotations.
The json looks like this:
"ObjectList": [
{
"ObjectA": {
"property1": false,
"property2": true
},
"ObjectB": {
"property1": 66,
"property2": true
},
{
"ObjectA": {
"property1": false,
"property2": true
},
"ObjectB": {
"property1": 66,
"property2": true
}
}
]
}
My code looks like this
ResponseEntity<Response> response = restTemplate.exchange(URL, HttpMethod.GET, request, Response.class);
Response response = response.getBody();
Response is:
#JsonIgnoreProperties(ignoreUnknown = true)
public class TimesheetListResponse {
#JsonProperty("ObjectA")
private List<ObjectA> objectAList;
#JsonProperty("ObjectB")
private List<ObjectB> objectBList;
That does not work at all, and I'm confused about how to map this.
According to your requirement the model structure may look like below. Within the objectList map in Response object, you need to add HashMap with keys as "ObjectA"/"ObjectB" string and value as instance of ObjectA/ObjectB. I have taken value type of Map as Object, so that any object type A/B can fit in there. Add corresponding #JsonXXX annotations.
public class Response {
private List<Map<String,Object>> objectList;
//Getters & Setters
}
public class ObjectB {
String propB1;
String propB2;
}
public class ObjectA {
String propA;
String propA1;
}
I also would consider the entry in the list as another wrapper object that can either ObjectA or ObjectB. I.e.
#JsonIgnoreProperties(ignoreUnknown = true)
public final class Parent {
#JsonProperty("ObjectList")
private List<ChildWrapper> objectList = new ArrayList<>();
}
#JsonIgnoreProperties(ignoreUnknown = true)
public final class ChildWrapper {
#JsonProperty("ObjectA")
private Child ObjectA;
#JsonProperty("ObjectB")
private Child ObjectB;
}
#JsonIgnoreProperties(ignoreUnknown = true)
public final class Child {
#JsonProperty("property1")
private int property1;
#JsonProperty("property2")
private boolean property2;
}
It seems that the mapping was fine, I only had to initialize the Arraylist. The main issue was that the endpoint was returning empty because of a parameter that I forgot.