Spring Boot: Get required columns from API based on user request - java

I am trying to implement an API endpoint where a user can request required columns from the API
Basically, this is what I want:
This is my products table entity
#Entity
#Table(name ="products")
class product{
private Long id;
private String itemName;
private String itemDesc;
private double quantity;
private double purchaseRate;
private double saleRate;
private double onlineSaleRate;
.
.
.
constructor()
getter & setter
}
***And my endpoint is:
localhost:8080/api/v1/products
Requirement:
I want to write an api endpoint where i request columns based on requirementsa and get those as response
Example: If i only need - itemName, itemPrice and quantity i'll return those as response only.
if some user has requirement of itemName, purchaseRate, saleRate, quantity he will get those as a response only.
Right now i am writing new endpoints as per requirements, but i think there is some way to do this.
I want to implement this in my application, i tried google for this but there is no search query that is resulting me as per my requirement.

Create a class with all the fields of your entity with the field types of nullable boxed Boolean (for the purpose of the request json).
class ProductColumns {
private Boolean itemName;
private Boolean itemDesc;
...
// setters and getters
}
Then, to construct a custom response, you can use a java Map to acheive this:
public ResponseEntity<Object> getFilteredColumns(ProductColumns columns) {
Map<String, Object> map = new HashMap<String, Object>();
if (columns.getItemName() == true) {
map.put("itemName", [your repo/service method for getting the particular value]);
}
if (columns.getItemDesc() == true) {
map.put("itemDesc", your repo/service method for getting the particular value]);
}
...
return ResponseEntity<Object>(map, HttpStatus.OK);
}
Of course you should wrap it in some try-catch to your liking.

Related

Spring WebFlux - Inconvertible types; cannot cast 'reactor.core.publisher.Mono'

I am using Spring Boot for my API. I am rewriting my API, to adopt the microservices architecture.
I have 2 classes:
1) Product
2) Ingredient
My Code:
Here is my Product class:
public class Product{
private String name;
#OneToMany
private List<Ingredient> productIngredients; //Ingredient
private Double quantity = 0.0;
private Double productCost = 0.0;
public void addIngredient(Ingredient myIngredient){
this.productComponents.add(myIngredient);
}
}
Here is my Ingredient class:
public class Ingredient{
private String name;
private String unit;
private Double quantity = 0.0;
}
In the Product microservice, I am doing an API call to the Ingredient microservice:
// Making a call to the Ingredients microservice from the product microservice
WebClient myWebClient = WebClient.create("http://localhost:8090/api");
#PostMapping("/component/list/add/{id}")
public Mono<Ingredient> addProductComponent(#PathVariable Long id){
Product myProduct = new Product();
Mono<Ingredient> myProductComponentMono =
myWebClient
.get()
.uri("/components/component/get/{"+ id +'}')
.retrieve()
.bodyToMono(Ingredient.class);
return myProduct.addProductIngredient((Ingredient) myIngredientMono); //this is the line where I am getting the error.
}
Here is the line where I get the error in the above method:
return myProduct.addProductIngredient((Ingredient) myProductComponentMono);
I am getting the following error:
Inconvertible types; cannot cast 'reactor.core.publisher.Mono<com.product.product.Entity.Ingredient>' to 'com.product.product.Entity.Ingredient'
What is a possible solution to fix the above problem ?
Just use myProductComponentMono.block() instead of casting the retrieved mono to Ingredient, since your code in method myProduct.addProductIngredient(...) accept only an object of Ingredient.

Nested json structure

I want to create a dynamodb which has following features
PK: orderId
RK: date
shipped: Y|N
details: <nested json structure>
Point 4 is the one which i am really confused about. If i keep details field as a string and try to store json as string, AWS escape " characters i.e., {"onlineStore" : "283"} becomes {\"onlineStore\": \"283\"}
This get's retrieved properly from dynamodb with details as string mappings but if i have to convert it to a pojo using jackson, I have to take care of those \.
So as an alternative, I thought that i could create details a POJO i.e.,
public class OrderDetail{
private int onlineStore;
// rest of the JSON properties
#JsonCreator
public OrderDetail (#JsonProperty("onlineStore") int onlineStore, ...){
this.onlineStore = onlineStore;
}
}
With the above implementation, i get error that DynamoDBMappingException: Couldn't convert attribte.
The OrderDetail type is a common type which is being used between my JSON REST Response as well so i want to avoid putting DynamoDB specific annotation here.
Now the question is what should be proper way to implement it.
If you are using DynamoDB Mapper class to perform the CRUD operation, you can use the annotation #DynamoDBTypeConvertedJson to save the order details.
DynamoDBTypeConvertedJson - Check this link for more details
DynamoDBMapper class:-
The AWS SDK for Java provides a DynamoDBMapper class, allowing you to
map your client-side classes to DynamoDB tables. To use
DynamoDBMapper, you define the relationship between items in a
DynamoDB table and their corresponding object instances in your code.
The DynamoDBMapper class enables you to access your tables, perform
various create, read, update and delete (CRUD) operations, and execute
queries.
Sample Implementation:-
#DynamoDBTable(tableName = "Order")
public class Order implements Serializable {
private static final long serialVersionUID = -3534650012619938612L;
private String orderId;
private OrderDetail orderDetail;
#DynamoDBHashKey(attributeName = "orderId")
#DynamoDBAutoGeneratedKey
public String getOrderId() {
return orderId;
}
public void setOrderId(String orderId) {
this.orderId = orderId;
}
#DynamoDBTypeConvertedJson
public OrderDetail getOrderDetail() {
return orderDetail;
}
public void setOrderDetail(OrderDetail orderDetail) {
this.orderDetail = orderDetail;
}
}
public class OrderDetail implements Serializable {
private static final long serialVersionUID = 7312390212027563305L;
private Integer onlineStore;
public Integer getOnlineStore() {
return onlineStore;
}
public void setOnlineStore(Integer onlineStore) {
this.onlineStore = onlineStore;
}
#Override
public String toString() {
return "OrderDetail [onlineStore=" + onlineStore + "]";
}
}
public Order loadOrder(String orderId) {
DynamoDBMapper dynamoDBMapper = new DynamoDBMapper(dynamoDBClient);
Order order = dynamoDBMapper.load(Order.class, orderId,
new DynamoDBMapperConfig(DynamoDBMapperConfig.ConsistentReads.CONSISTENT));
System.out.println("Order : " + order.toString());
System.out.println("Order Id : " + order.getOrderId());
System.out.println("Order Detail : " + order.getOrderDetail());
System.out.println("Online store : " + order.getOrderDetail().getOnlineStore());
return order;
}
Output:-
Order Id : 0beced28-f1de-4c44-8094-6de687d25e97
Order Detail : OrderDetail [onlineStore=1]
Online store : 1
Data in DDB:-
As you mentioned, the order detail will be stored with escape characters. However, when you get the data using DynamoDB mapper, it will be in deserialized form (i.e. as POJO object).

A custom domain object isn't being shown while using #ModelAttribute

I am using Springfox and Swagger to generate swagger files. Right now I'm using #ModelAttribute to pull the variables from an object (NetworkCmd) to show as query params in the swagger doc.
I currently have the following controller:
#RequestMapping(value = "/{product_id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseHeader()
public ResponseEntity<?> networkResponse(
#RequestHeader HttpHeaders headers,
#PathVariable("product_id")String productId,
#Valid #ModelAttribute NetworkCmd cmd,
BindingResult result)
throws Exception {
...
}
And here is a sample of NetworkCmd:
#ItemId
#NotNull(message = "product cannot be null")
#ApiModelProperty(
value = "testing")
private String product_id;
#ApiModelProperty(
value = "key",
private String key;
#ApiModelProperty(
value = "parent")
private Boolean is_parent_id;
#Min(0)
#ApiModelProperty(
value = "radius")
private double radius = 10d;
One of the variables in this class is a custom domain object Nearby.
private Nearby nearby = null;
public Nearby getNearby() {
return nearby;
}
public void setNearby(String nearby) throws ParseException {
this.nearby = Nearby.parse(nearby);
}
This is kind of a special variable because it takes in a String, and then parses that string and turns it into the Nearby object.
My problem is that this Nearby variable isn't showing up on the generated swagger document through #ModelAttribute. I'm happy to provide any more information.
One way to get around this problem is to create an alternate type rule in your docket. This way anytime we encounter the nearby type we treat it as a string.
new Docket(...)
.directModelSubstitute(Nearby.class, String.class)

How to distinguish between null and not provided values for partial updates in Spring Rest Controller

I'm trying to distinguish between null values and not provided values when partially updating an entity with PUT request method in Spring Rest Controller.
Consider the following entity, as an example:
#Entity
private class Person {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/* let's assume the following attributes may be null */
private String firstName;
private String lastName;
/* getters and setters ... */
}
My Person repository (Spring Data):
#Repository
public interface PersonRepository extends CrudRepository<Person, Long> {
}
The DTO I use:
private class PersonDTO {
private String firstName;
private String lastName;
/* getters and setters ... */
}
My Spring RestController:
#RestController
#RequestMapping("/api/people")
public class PersonController {
#Autowired
private PersonRepository people;
#Transactional
#RequestMapping(path = "/{personId}", method = RequestMethod.PUT)
public ResponseEntity<?> update(
#PathVariable String personId,
#RequestBody PersonDTO dto) {
// get the entity by ID
Person p = people.findOne(personId); // we assume it exists
// update ONLY entity attributes that have been defined
if(/* dto.getFirstName is defined */)
p.setFirstName = dto.getFirstName;
if(/* dto.getLastName is defined */)
p.setLastName = dto.getLastName;
return ResponseEntity.ok(p);
}
}
Request with missing property
{"firstName": "John"}
Expected behaviour: update firstName= "John" (leave lastName unchanged).
Request with null property
{"firstName": "John", "lastName": null}
Expected behaviour: update firstName="John" and set lastName=null.
I cannot distinguish between these two cases, sincelastName in the DTO is always set to null by Jackson.
Note:
I know that REST best practices (RFC 6902) recommend using PATCH instead of PUT for partial updates, but in my particular scenario I need to use PUT.
Another option is to use java.util.Optional.
import com.fasterxml.jackson.annotation.JsonInclude;
import java.util.Optional;
#JsonInclude(JsonInclude.Include.NON_NULL)
private class PersonDTO {
private Optional<String> firstName;
private Optional<String> lastName;
/* getters and setters ... */
}
If firstName is not set, the value is null, and would be ignored by the #JsonInclude annotation. Otherwise, if implicitly set in the request object, firstName would not be null, but firstName.get() would be. I found this browsing the solution #laffuste linked to a little lower down in a different comment (garretwilson's initial comment saying it didn't work turns out to work).
You can also map the DTO to the Entity with Jackson's ObjectMapper, and it will ignore properties that were not passed in the request object:
import com.fasterxml.jackson.databind.ObjectMapper;
class PersonController {
// ...
#Autowired
ObjectMapper objectMapper
#Transactional
#RequestMapping(path = "/{personId}", method = RequestMethod.PUT)
public ResponseEntity<?> update(
#PathVariable String personId,
#RequestBody PersonDTO dto
) {
Person p = people.findOne(personId);
objectMapper.updateValue(p, dto);
personRepository.save(p);
// return ...
}
}
Validating a DTO using java.util.Optional is a little different as well. It's documented here, but took me a while to find:
// ...
import javax.validation.constraints.NotNull;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
// ...
private class PersonDTO {
private Optional<#NotNull String> firstName;
private Optional<#NotBlank #Pattern(regexp = "...") String> lastName;
/* getters and setters ... */
}
In this case, firstName may not be set at all, but if set, may not be set to null if PersonDTO is validated.
//...
import javax.validation.Valid;
//...
public ResponseEntity<?> update(
#PathVariable String personId,
#RequestBody #Valid PersonDTO dto
) {
// ...
}
Also might be worth mentioning the use of Optional seems to be highly debated, and as of writing Lombok's maintainer(s) won't support it (see this question for example). This means using lombok.Data/lombok.Setter on a class with Optional fields with constraints doesn't work (it attempts to create setters with the constraints intact), so using #Setter/#Data causes an exception to be thrown as both the setter and the member variable have constraints set. It also seems better form to write the Setter without an Optional parameter, for example:
//...
import lombok.Getter;
//...
#Getter
private class PersonDTO {
private Optional<#NotNull String> firstName;
private Optional<#NotBlank #Pattern(regexp = "...") String> lastName;
public void setFirstName(String firstName) {
this.firstName = Optional.ofNullable(firstName);
}
// etc...
}
There is a better option, that does not involve changing your DTO's or to customize your setters.
It involves letting Jackson merge data with an existing data object, as follows:
MyData existingData = ...
ObjectReader readerForUpdating = objectMapper.readerForUpdating(existingData);
MyData mergedData = readerForUpdating.readValue(newData);
Any fields not present in newData will not overwrite data in existingData, but if a field is present it will be overwritten, even if it contains null.
Demo code:
ObjectMapper objectMapper = new ObjectMapper();
MyDTO dto = new MyDTO();
dto.setText("text");
dto.setAddress("address");
dto.setCity("city");
String json = "{\"text\": \"patched text\", \"city\": null}";
ObjectReader readerForUpdating = objectMapper.readerForUpdating(dto);
MyDTO merged = readerForUpdating.readValue(json);
Results in {"text": "patched text", "address": "address", "city": null}
Note that text and city were patched (city is now null) and that address was left alone.
In a Spring Rest Controller you will need to get the original JSON data instead of having Spring deserialize it in order to do this. So change your endpoint like this:
#Autowired ObjectMapper objectMapper;
#RequestMapping(path = "/{personId}", method = RequestMethod.PATCH)
public ResponseEntity<?> update(
#PathVariable String personId,
#RequestBody JsonNode jsonNode) {
RequestDTO existingData = getExistingDataFromSomewhere();
ObjectReader readerForUpdating = objectMapper.readerForUpdating(existingData);
RequestDTO mergedData = readerForUpdating.readValue(jsonNode);
...
}
Use boolean flags as jackson's author recommends.
class PersonDTO {
private String firstName;
private boolean isFirstNameDirty;
public void setFirstName(String firstName){
this.firstName = firstName;
this.isFirstNameDirty = true;
}
public String getFirstName() {
return firstName;
}
public boolean hasFirstName() {
return isFirstNameDirty;
}
}
Actually,if ignore the validation,you can solve your problem like this.
public class BusDto {
private Map<String, Object> changedAttrs = new HashMap<>();
/* getter and setter */
}
First, write a super class for your dto,like BusDto.
Second, change your dto to extend the super class, and change the
dto's set method,to put the attribute name and value to the
changedAttrs(beacause the spring would invoke the set when the
attribute has value no matter null or not null).
Third,traversal the map.
I have tried to solve the same problem. I found it quite easy to use JsonNode as the DTOs. This way you only get what is submitted.
You will need to write a MergeService yourself that does the actual work, similar to the BeanWrapper. I haven't found an existing framework that can do exactly what is needed. (If you use only Json requests you might be able to use Jacksons readForUpdate method.)
We actually use another node type as we need the same functionality from "standard form submits" and other service calls. Additionally the modifications should be applied within a transaction inside something called EntityService.
This MergeService will unfortunately become quite complex, as you will need to handle properties, lists, sets and maps yourself :)
The most problematic piece for me was to distinguish between changes within an element of a list/set and modifications or replacements of lists/sets.
And also validation will not be easy as you need to validate some properties against another model (the JPA entities in my case)
EDIT - Some mapping code (pseudo-code):
class SomeController {
#RequestMapping(value = { "/{id}" }, method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
#ResponseBody
public void save(
#PathVariable("id") final Integer id,
#RequestBody final JsonNode modifications) {
modifierService.applyModifications(someEntityLoadedById, modifications);
}
}
class ModifierService {
public void applyModifications(Object updateObj, JsonNode node)
throws Exception {
BeanWrapperImpl bw = new BeanWrapperImpl(updateObj);
Iterator<String> fieldNames = node.fieldNames();
while (fieldNames.hasNext()) {
String fieldName = fieldNames.next();
Object valueToBeUpdated = node.get(fieldName);
Class<?> propertyType = bw.getPropertyType(fieldName);
if (propertyType == null) {
if (!ignoreUnkown) {
throw new IllegalArgumentException("Unkown field " + fieldName + " on type " + bw.getWrappedClass());
}
} else if (Map.class.isAssignableFrom(propertyType)) {
handleMap(bw, fieldName, valueToBeUpdated, ModificationType.MODIFY, createdObjects);
} else if (Collection.class.isAssignableFrom(propertyType)) {
handleCollection(bw, fieldName, valueToBeUpdated, ModificationType.MODIFY, createdObjects);
} else {
handleObject(bw, fieldName, valueToBeUpdated, propertyType, createdObjects);
}
}
}
}
Maybe too late for an answer, but you could:
By default, don't unset 'null' values. Provide an explicit list via query params what fields you want to unset. In such a way you can still send JSON that corresponds to your entity and have flexibility to unset fields when you need.
Depending on your use case, some endpoints may explicitly treat all null values as unset operations. A little bit dangerous for patching, but in some circumstances might be an option.
Another solution would be to imperatively deserialize the request body. By doing it, you will be able to collect user provided fields and selectively validate them.
So your DTO might look like this:
public class CatDto {
#NotBlank
private String name;
#Min(0)
#Max(100)
private int laziness;
#Max(3)
private int purringVolume;
}
And your controller can be something like this:
#RestController
#RequestMapping("/api/cats")
#io.swagger.v3.oas.annotations.parameters.RequestBody(
content = #Content(schema = #Schema(implementation = CatDto.class)))
// ^^ this passes your CatDto model to swagger (you must use springdoc to get it to work!)
public class CatController {
#Autowired
SmartValidator validator; // we'll use this to validate our request
#PatchMapping(path = "/{id}", consumes = "application/json")
public ResponseEntity<String> updateCat(
#PathVariable String id,
#RequestBody Map<String, Object> body
// ^^ no Valid annotation, no declarative DTO binding here!
) throws MethodArgumentNotValidException {
CatDto catDto = new CatDto();
WebDataBinder binder = new WebDataBinder(catDto);
BindingResult bindingResult = binder.getBindingResult();
List<String> patchFields = new ArrayList<>();
binder.bind(new MutablePropertyValues(body));
// ^^ imperatively bind to DTO
body.forEach((k, v) -> {
patchFields.add(k);
// ^^ collect user provided fields if you need
validator.validateValue(CatDto.class, k, v, bindingResult);
// ^^ imperatively validate user input
});
if (bindingResult.hasErrors()) {
throw new MethodArgumentNotValidException(null, bindingResult);
// ^^ this can be handled by your regular exception handler
}
// Here you can do normal stuff with your catDto.
// Map it to cat model, send to cat service, whatever.
return ResponseEntity.ok("cat updated");
}
}
No need for Optional's, no extra dependencies, your normal validation just works, your swagger looks good. The only problem is, you don't get proper merge patch on nested objects, but in many use cases that's not even required.
Probably to late but following code works for me to distinguish between null and not provided values
if(dto.getIban() == null){
log.info("Iban value is not provided");
}else if(dto.getIban().orElse(null) == null){
log.info("Iban is provided and has null value");
}else{
log.info("Iban value is : " + dto.getIban().get());
}

Parsing JSON with Retrofit and GSON, error when trying to parse and obtain callback.

I am using to Retrofit to handle Calls to my API for an Android Application. I am trying to get Retrofit to handle the parsing of the JSON, and creating a list of Objects in accordance with the POJO i have created.
The error i receive is "com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected a string but was BEGIN_OBJECT at line 1 column 176".
I used JsonSchema2Pojo to generate my java classes. The classes and associated JSON are as follows.
{"status":"success","data":[{"sort_key":1,"event_id":1947357,"title":"2014 US Open Tennis Session 15 (Mens\/Womens Round of 16)","datetime_utc":"2014-09-01T15:00:00","venue":{"city":"Flushing","name":"Louis Armstrong Stadium","extended_address":"Flushing, NY 11368","url":"https:\/\/seatgeek.com\/venues\/louis-armstrong-stadium\/tickets\/?aid=10918","country":"US","display_location":"Flushing, NY","links":[],"slug":"louis-armstrong-stadium","state":"NY","score":0.73523,"postal_code":"11368","location":{"lat":40.7636,"lon":-73.83},"address":"1 Flushing Meadows Corona Park Road","timezone":"America\/New_York","id":2979},"images":["https:\/\/chairnerd.global.ssl.fastly.net\/images\/performers-landscape\/us-open-tennis-45e2d9\/5702\/huge.jpg","https:\/\/chairnerd.global.ssl.fastly.net\/images\/performers\/5702\/us-open-tennis-c1ccf7\/medium.jpg","https:\/\/chairnerd.global.ssl.fastly.net\/images\/performers\/5702\/us-open-tennis-01f513\/large.jpg","https:\/\/chairnerd.global.ssl.fastly.net\/images\/performers\/5702\/us-open-tennis-4e07f2\/small.jpg"]}
From this i believe i need to generate 3 POJO's, my higher level "EventObject" Class, A Location Class, and a Venue Class. These classes and their variables follow:
EventObject Class:
public class EventObject {
private Integer sortKey;
private Integer eventId;
private String title;
private String datetimeUtc;
private Venue venue;
private List<String> images = new ArrayList<String>();
private Map<String, Object> additionalProperties = new HashMap<String, Object>();
Location Class:
public class Location {
private Float lat;
private Float lon;
private Map<String, Object> additionalProperties = new HashMap<String, Object>();
Venue Class:
public class Venue {
private String city;
private String name;
private String extendedAddress;
private String url;
private String country;
private String displayLocation;
private List<Object> links = new ArrayList<Object>();
private String slug;
private String state;
private Float score;
private String postalCode;
private Location location;
private String address;
private String timezone;
private Integer id;
private Map<String, Object> additionalProperties = new HashMap<String, Object>();
My interface for the Api Call is as follows:
public interface UserEvents {
#GET("/user/get_events")
void getEvents(#Header("Authorization")String token_id,
#Query("event_type")String event_type,
#Query("postal_code")int postalCode,
#Query("per_page") int perPage ,
#Query("lat") int lat,
#Query("lon") int lon,
#Query("month")int month,
#Query("page")int page,
Callback<List<EventObject>>callback) ;
}
Here is its implementation in my code :
UserEvents mUserEvents = mRestAdapter.create(UserEvents.class);
mUserEvents.getEvents(token_Id, "sports",11209,25,0, 0, 9, 2, new Callback <List<EventObject>>() {
#Override
public void success(List<EventObject> eventObjects, retrofit.client.Response response) {
Log.d(TAG,"Success");
}
There is alot going on here, but i believe that i am probably going wrong with how i am handling the JSON. When i copied and pasted in my JSON to the Pojo generator, i did not include "status:success, " data:{
I essentially just used the entire entry of an element in the Array ( everything from {sort_key onward until the next sort key ) and pushed that through the converter.
This is my first try at Retrofit and API work, and parsing anything this complicated.
I am hoping its something that someone else will be able to point out. I have googled as well i could to sort this out with no luck.
Thanks for looking.
The main problem is that you are not getting the root element of the response. You need to create an entity "response" that gets the items status and data. It would look something like this:
public class RootObject {
#Expose
private String status;
#Expose
private EventObject data;
//getters and setters here
}
Then when you make the callback you should point to your RootObject, mUserEvents.getEvents(token_Id, "sports",11209,25,0, 0, 9, 2, new Callback <RootObject>()
One more thing, Retrofit uses GSON to parse your json reponse. It means that when you create the entities, the variables need to match the name of the objects coming in the response. If it doesn't you need to tell GSON how to map the variables, like this:
#SerializedName("extended_address")
#Expose
private String extendedAddress;
In that case the value coming in the json is "extended_address" and will be mapped to the String extendedAddress. If you don't put that #SerializedName line the parsing will fail. If you want to skip that line then you can call your variable "extended_address" so it matches the response.
The #Expose is needed by GSON to parse the variable below it. If a variable doesn't have it then GSON will ignore that parsing. So you need to fix both the #Expose and #SerializedName on your entities so GSON works correctly.
Hope it helps.

Categories