How can i customize the dafault behavior of jackson when returning a ResponseEntity<> with PagedResources<>? - spring

Im using spring boot on my proyect. Basically, all i want is to include inline entities in a response of a GET method while still using the advantages of spring-hateoas (_embedded and links).
Having this class:
#Entity
#Table(name = "sub_specialty", schema = "public", catalog = "icorelab")
public class SubSpecialty {
private Integer id;
private String name;
private Boolean active;
private Date createdAt;
private Date deletedAt;
private Specialty specialty;
private String description;
#ManyToOne
#JoinColumn(name = "specialty_id", referencedColumnName = "id", nullable = false)
public Specialty getSpecialtyBySpecialtyId() {
return specialty;
}
I set up repository with PagingAndSorting capabilities, also it extends CustomRepository to provide custom methods. This repository uses a projection so an iternal entity gets serialized as mentioned here
https://docs.spring.io/spring-data/rest/docs/current/reference/html/#projections-excerpts.excerpts
#RepositoryRestResource(collectionResourceRel = "sub_specialties", path = "sub_specialties",
excerptProjection = InlineSpecialty.class)
public interface SubSpecialtyRepository extends PagingAndSortingRepository<SubSpecialty, Integer>,SubSpecialtyRepositoryCustom {
The in a controller, i return the response by using Page and PageResourcesAssembler
Page<SubSpecialty> page = subSpecialtyRepository.filter(name,description,active,specialty,limit,offset,pageable.getSort());
PagedResources<SubSpecialty> pagedResources = pagedResourcesAssembler.toResource(page,subSpecialtyResourceAssembler);
HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(page, "/sub_specialties/filter", offset, limit);
ResponseEntity<?> response = new ResponseEntity<>(pagedResources, headers, HttpStatus.OK);
return response;
The problem is that the message does not include Specialty info, just the link
{
"_embedded": {
"sub_specialties": [
{
"id": 107,
"name": "PHARMACOLOGY",
"active": false,
"createdAt": "2017-09-30",
"deletedAt": null,
"description": null,
"_links": {
"self": {
"href": "http://localhost:8080/sub_specialties/107{?projection}",
"templated": true
},
"subSpecialty": {
"href": "http://localhost:8080/sub_specialties/107{?projection}",
"templated": true
},
"specialty": {
"href": "http://localhost:8080/specialties/16"
}
}
},
{
"id": 104,
"name": "PHARMACOLOGY",
"active": false,
"createdAt": "2017-09-30",
"deletedAt": null,
"description": null,
"_links": {
"self": {
"href": "http://localhost:8080/sub_specialties/104{?projection}",
"templated": true
},
"subSpecialty": {
"href": "http://localhost:8080/sub_specialties/104{?projection}",
"templated": true
},
"specialty": {
"href": "http://localhost:8080/specialties/16"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/sub_specialties/filter?page=0&size=20"
}
},
"page": {
"size": 20,
"totalElements": 2,
"totalPages": 1,
"number": 0
}
}
Is there anyway to achieve this? I thought i could use an annotation on the entity domain class or a custom jackson serializer.The Specialty entity is included in the response, however is not included in the message that receives the client. I suppose there is some default configuration for jackson that detects a repository of type Specialty, and the just includes the link in the response.

Related

Spring boot mongodb #DBRef lazy loading not working for hashsets

Issue: Don't want to fetch the full referenced child objects just the Id.
Partner.java
#Data
#Document
#Builder
public class Partner {
#Id
private String id;
#DBRef(lazy = true) // working as expected
private User user;
#DBRef(lazy = true) // not working, still loads full objects
#Builder.Default
private HashSet<Product> products = new HashSet<>();
}
Product.java
#Data
#Document
#Builder
public class Product {
#Id
private String id;
private Name name;
}
I know that #DBRef is not recommended for such cases. But the project is already built that way and is expensive to change at the moment.
So far tried:
excluding the child array field from the query but then I do not get the Ids as well.
excluding the products.$ref from the query.
#Query(value="{ '_id' : ?0 }", fields="{ products.$ref: 0 }")
Optional<Partner> findByIdWithoutProducts(String id);
Expected:
{
"_id": {
"$oid": "634692559f31e246984436c8"
},
"user": {
"$ref": "user",
"$id": "f689bae8-7442-4355-ae9c-65d22a96ear4"
},
"products": [
{
"$id": {
"$oid": "633d92d2310687496fb852e6"
}
},
{
"$id": {
"$oid": "633d92d2310687496fb852d5"
}
}
}
}
Actual:
{
"_id": {
"$oid": "634692559f31e246984436c8"
},
"user": {
"$ref": "user",
"$id": "f689bae8-7442-4355-ae9c-65d22a96ear4"
},
"products": [
{
"$id": {
"$oid": "633d92d2310687496fb852e6"
},
"name": "abcd" // not wanted
},
{
"$id": {
"$oid": "633d92d2310687496fb852d5"
},
"name": "xyz" // not wanted
}
}
}
My goal is to fetch the parent and just list of Ids of the child objects. Any other way is also appreciated.

Show only object id in OneToMany List

I have these two classes:
Quota Class
#Entity
#Data
#EqualsAndHashCode(callSuper=false)
#Table(name="quotas")
#Relation(collectionRelation = "quotas", itemRelation = "quota")
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class Quota extends RepresentationModel<Quota> {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "quota")
#JsonIdentityReference(alwaysAsId = true)
private List<Customer> customers;
private String type;
private boolean flatrate;
#CreatedDate
private long createdDate;
#LastModifiedDate
private long lastModifiedDate;
And Customer:
#Entity
#Data
#EqualsAndHashCode(callSuper=false)
#Table(name = "customers")
#Relation(collectionRelation = "customers", itemRelation = "customer")
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class Customer extends RepresentationModel<Customer> {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
private String customerNumber;
#CreatedDate
private long createdDate;
#LastModifiedDate
private long lastModifiedDate;
#ManyToOne
#JoinColumn(name = "quota_id", referencedColumnName = "id")
#JsonProperty("quotaId")
#JsonIdentityReference(alwaysAsId = true)
private Quota quota;
}
What i get when i make a GET request on all Customers:
{
"_embedded": {
"customers": [
{
"id": 3,
"name": "Customer Test3",
"customerNumber": "45678",
"createdDate": 1596117132045,
"lastModifiedDate": 1596117132045,
"_links": {
"self": {
"href": "http://localhost:2502/seminars/3"
}
},
"quotaId": 1
},
{
"id": 1,
"name": "test3",
"customerNumber": "12345",
"createdDate": 1596111304535,
"lastModifiedDate": 1596186450456,
"_links": {
"self": {
"href": "http://localhost:2502/seminars/1"
}
},
"quotaId": 1
},
{
"id": 2,
"name": "Customer Test2",
"customerNumber": "23456",
"createdDate": 1596112131934,
"lastModifiedDate": 1596112131934,
"_links": {
"self": {
"href": "http://localhost:2502/seminars/2"
}
},
"quotaId": 2
},
{
"id": 4,
"name": "Customer Test4",
"customerNumber": "34567",
"createdDate": 1596117145795,
"lastModifiedDate": 1596117145795,
"_links": {
"self": {
"href": "http://localhost:2502/seminars/4"
}
},
"quotaId": 2
},
{
"id": 6,
"name": "Customer Test6",
"customerNumber": "12345",
"createdDate": 1596187250598,
"lastModifiedDate": 1596187250598,
"_links": {
"self": {
"href": "http://localhost:2502/seminars/6"
}
},
"quotaId": null
}
]
},
"_links": {
"self": {
"href": "http://localhost:2502/seminars?page=0&size=20&sort=quota,asc"
}
},
"page": {
"size": 20,
"totalElements": 5,
"totalPages": 1,
"number": 0
}
}
What i get when i make a GET request in all Quotas:
{
"_embedded": {
"quotas": [
{
"id": 1,
"customers": [
{
"id": 3,
"name": "Customer Test3",
"customerNumber": "45678",
"createdDate": 1596117132045,
"lastModifiedDate": 1596117132045,
"quotaId": 1
},
{
"id": 1,
"name": "test3",
"customerNumber": "12345",
"createdDate": 1596111304535,
"lastModifiedDate": 1596186450456,
"quotaId": 1
}
],
"type": "paid",
"createdDate": 1596111304535,
"lastModifiedDate": 1596111304535,
"flatrate": true,
"_links": {
"self": {
"href": "http://localhost:2502/quotas/1"
}
}
},
{
"id": 2,
"customers": [
{
"id": 2,
"name": "Customer Test2",
"customerNumber": "23456",
"createdDate": 1596112131934,
"lastModifiedDate": 1596112131934,
"quotaId": 2
},
{
"id": 4,
"name": "Customer Test4",
"customerNumber": "34567",
"createdDate": 1596117145795,
"lastModifiedDate": 1596117145795,
"quotaId": 2
}
],
"type": "demo",
"createdDate": 1596111304535,
"lastModifiedDate": 1596111304535,
"flatrate": true,
"_links": {
"self": {
"href": "http://localhost:2502/quotas/2"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:2502/quotas?page=0&size=10&sort=id,asc"
}
},
"page": {
"size": 10,
"totalElements": 2,
"totalPages": 1,
"number": 0
}
}
I am happy with the result from Customers, but the result from quotas i would want to look like this:
{
"_embedded": {
"quotas": [
{
"id": 1,
"customers": [3,1],
"type": "paid",
"createdDate": 1596111304535,
"lastModifiedDate": 1596111304535,
"flatrate": true,
"_links": {
"self": {
"href": "http://localhost:2502/quotas/1"
}
}
},
{
"id": 2,
"customers": [2,4],
"type": "demo",
"createdDate": 1596111304535,
"lastModifiedDate": 1596111304535,
"flatrate": true,
"_links": {
"self": {
"href": "http://localhost:2502/quotas/2"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:2502/quotas?page=0&size=10&sort=id,asc"
}
},
"page": {
"size": 10,
"totalElements": 2,
"totalPages": 1,
"number": 0
}
}
For json serialization i use 'com.fasterxml.jackson.annotation.*'
I already tried it with using the #JsonBackRefference, #JsonIgnore, #JsonIngoreProperties and #JsonIdentityReference annotations but never got the wanted result.
I would recommend you introduce DTOs to model this rather than trying to annotate the entity classes with JSON annotations. At some point you will probably have conflicting needs i.e. one use case needs a field and another doesn't and then you will have to think about abstracting this anyway.
Having said that, this is a perfect use case for Blaze-Persistence Entity Views.
Blaze-Persistence is a query builder on top of JPA which supports many of the advanced DBMS features on top of the JPA model. I created Entity Views on top of it to allow easy mapping between JPA models and custom interface defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure the way you like and map attributes(getters) via JPQL expressions to the entity model. Since the attribute name is used as default mapping, you mostly don't need explicit mappings as 80% of the use cases is to have DTOs that are a subset of the entity model.
A DTO mapping for your model could look as simple as the following
#EntityView(Quota.class)
public abstract class QuotaDto extends RepresentationModel<QuotaDto> {
public abstract Integer getId();
#Mapping("customers.id")
public abstract List<Integer> getCustomers();
public abstract String getType();
public abstract boolean isFlatrate();
public abstract long getCreatedDate();
public abstract long getLastModifiedDate();
}
#EntityView(Customer.class)
public abstract class CustomerDto extends RepresentationModel<CustomerDto> {
public abstract Integer getId();
public abstract String getName();
public abstract String getCustomerNumber();
public abstract long getCreatedDate();
public abstract long getLastModifiedDate();
#Mapping("quota.id")
public abstract Integer getQuotaId();
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
CustomerDto dto = entityViewManager.find(entityManager, CustomerDto.class, id);
But the Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features
You are in control of the structure and can map anything to the properties. It will only fetch the mappings that you tell it to fetch.

Spring Boot - Flatten JSON Entity returned by JPA specification for exporting to excel

I am creating a Spring Boot project for inventory management. I have an Entity called InwardInventory that has one to many relationship with another entity called InwardOutwardList. I am using JPA Specification to filter the Entity InwardInventory and it is working fine. Response I receive after filter is
{
"inwardInventory": {
"content": [
{
"inwardid": 19497,
"date": "2019-05-28",
"vehicleNo": "TRUCK",
"supplierSlipNo": "",
"ourSlipNo": "",
"inwardOutwardList": [
{
"entryid": 19499,
"product": {
"productName": "Cement",
},
"quantity": 100.0
},
{
"entryid": 19500,
"product": {
"productName": "Iron",
},
"quantity": 30.0
}
],
"warehouse": {
"warehouseName": "war2"
},
"supplier": {
"name": "Bright Traders"
}
}
]
}
}
Now, I want to export this data to excel. So, I need to flatten this response to something like this.
{
"inwardInventory": {
"content": [
{
"inwardid": 19497,
"date": "2019-05-28",
"vehicleNo": "TRUCK",
"supplierSlipNo": "",
"ourSlipNo": "",
"entryid": 19499,
"productName": "Cement",
"quantity": 100.0,
"warehouseName": "war2",
"name": "Bright Traders"
},
{
"inwardid": 19497,
"date": "2019-05-28",
"vehicleNo": "TRUCK",
"supplierSlipNo": "",
"ourSlipNo": "",
"entryid": 19500,
"productName": "Iron",
"quantity": 30.0,
"warehouseName": "war2",
"name": "Bright Traders"
}
]
}
}
I know that I can do this by iterating over each inward inventory and then nested iteration over each product and creating custom DAO. However, that doesn't seem to be optimised solution.
I also cannot use projections or native queries with custom select columns because they cannot be integrated with jpa specification. Can someone suggest me best approach that can be used to achieve this in most optimal way.
Thank You.
Handled this through custom serializer. Sample code below
#Component
public class OutwardExportSerializer extends StdSerializer<OutwardInventoryExportDAO>
{
private static final long serialVersionUID = 1L;
protected OutwardExportSerializer(Class<OutwardInventoryExportDAO> t)
{
super(t);
}
protected OutwardExportSerializer()
{
this(null);
}
#Override
public void serialize(OutwardInventoryExportDAO value, JsonGenerator gen, SerializerProvider provider) throws IOException
{
DateFormat df = new SimpleDateFormat("dd/MM/yyyy");
for(InwardOutwardList ioList:value.getInwardOutwardList())
{
System.out.println("Serialization starts for OutWardID"+value.getOutwardid());
gen.writeStartObject();
gen.writeNumberField("outwardid", value.getOutwardid());
gen.writeStringField("date",df.format(value.getDate()));
gen.writeStringField("purpose",value.getPurpose()!=null?value.getPurpose():"");
gen.writeStringField("slipNo",value.getSlipNo()!=null?value.getSlipNo():"");
gen.writeStringField("product",ioList.getProduct().getProductName()!=null?ioList.getProduct().getProductName():"");
gen.writeStringField("measurement unit",ioList.getProduct().getMeasurementUnit()!=null?ioList.getProduct().getMeasurementUnit():"");
gen.writeNumberField("quantity",ioList.getQuantity()!=null?ioList.getQuantity():0.0);
gen.writeNumberField("closing stock",ioList.getClosingStock()!=null?ioList.getClosingStock():0.0);
gen.writeStringField("warehouse",value.getWarehouse()!=null?value.getWarehouse():"");
gen.writeStringField("contractor",value.getContractor()!=null?value.getContractor():"");
gen.writeStringField("additionalInfo",value.getAdditionalInfo()!=null?value.getAdditionalInfo():"");
gen.writeStringField("usageLocation",value.getUsageLocation()!=null?value.getUsageLocation():"");
gen.writeStringField("usageArea",value.getUsageArea()!=null?value.getUsageArea():"");
gen.writeEndObject();
System.out.println("Serialization ends for OutWardID"+value.getOutwardid());
}
}
}

Pageable REST API

I'm using Pageable to return json results with pages. One example of my controller
#GetMapping("/nome/{nome}")
public ResponseEntity<Page<Cidadao>> consultaPorNome(
#PathVariable(value="nome",required=true) String nome,
#RequestParam(value="pagina", defaultValue="0") Integer page,
#RequestParam(value="quantidade", defaultValue="10") Integer linesPerPage,
#RequestParam(value="ordem", defaultValue="nome") String orderBy,
#RequestParam(value="direcao", defaultValue="ASC") String direction){
Page<Cidadao> lista = cidadaoService.findByNome(nome,page,linesPerPage,orderBy, direction);
return ResponseEntity.ok(lista);
}
And i get json like
{
"content": [
{
"id": 1,
"name": "Lucas"
},
{
"id": 2,
"name": "Erick"
},
],
"totalElements": 23,
"totalPages": 3,
"last": false,
"numberOfElements": 10,
"first": true,
"sort": [
{
"direction": "ASC",
"property": "nome",
"ignoreCase": false,
"nullHandling": "NATIVE",
"ascending": true,
"descending": false
}
],
"size": 10,
"number": 0
}
but i would like to return this way
{
"data":{
"content" : [
{
"id": 1,
"name": "Lucas"
},
{
"id": 1,
"name": "Lucas"
}
],
"totalElements": 23,
"totalPages": 3,
"last": false,
"numberOfElements": 10,
"first": true,
"sort": [
{
"direction": "ASC",
"property": "nome",
"ignoreCase": false,
"nullHandling": "NATIVE",
"ascending": true,
"descending": false
}
],
"size": 10,
"number": 0
},
errors:[]
}
It is possible implements anything like this to change or add attributes like errors ? Or if possible change the name attributes from Page like totalElements for numberofTotalElementBySomething.... ??
When I was looking for, I didn't find a beautiful solution to do this. So, I did this way:
Create a class called PageAdapter (or whatever you like)
public class PageAdapter<T extends Serializable> implements Serializable {
private long totalElements;
private long totalPages;
private long size;
private List<T> content;
public PageAdapter(Page<T> page){
this.totalElements = page.getTotalElements();
this.totalPages = page.getTotalPages();
this.size = page.getSize();
this.content = page.getContent();
}
}
And receive within constructor the original Pageable.
You can customize your properties, these properties were used in my case.
After this, you can return the PageAdapter in your controller/resource.
if there is a better solution, I would like to know too.
A suggestion for you, spring can receive an instance of Pageable, so, you don't need to receive each parameter for size, page, order and etc.
Just do this:
public ResponseEntity<Page<Cidadao>> consultaPorNome(
#PathVariable(value="nome",required=true) String nome, Pageable pageable){
You will have all information about size, page, order...
I'm not a native English speaker, so, may contains grammar errors.

How to add templated selfrel to Spring HATEOAS PagedResourcesAssembler?

I'm currently building an API using spring hateoas. Most of my controllers provide a list method which returns a PagedResources<>. For some reason the selfrel does not contain the {?page,size,sort} template which is found in all examples. Instead I only get the base URI.
So my e.g. my ProjectContoller looks like
#GetMapping
public PagedResources<ProjectResource> list(Pageable pageable, PagedResourcesAssembler<Project> pagedResourcesAssembler){
Page<Project> projects = service.findAll(pageable);
return pagedResourcesAssembler.toResource(projects, assembler);
}
and returns
{"_embedded":{
"projectResourceList":[
{
"begin":1462053600000,
"end":1469829600000,
"name":"Cool Big Project",
"_links":{"self":{"href":"http://localhost/projects/1"}}
}
]
},
"_links":{"self":{"href":"http://localhost/projects"}},
"page":{
"size":20,
"totalElements":1,
"totalPages":1,
"number":0
}
}
I guess I'm missing something trivial but can't find out what :-/
After reading how HAL should behave in Spring Data Rest it seems there could be some bug.
This is the standard Spring Data Rest links output for a tasks collection resource:
"_links": {
"first": {
"href": "http://localhost:8080/tasks?page=0&size=20"
},
"self": {
"href": "http://localhost:8080/tasks"
},
"next": {
"href": "http://localhost:8080/tasks?page=1&size=20"
},
"last": {
"href": "http://localhost:8080/tasks?page=2&size=20"
},
"profile": {
"href": "http://localhost:8080/profile/tasks"
},
"search": {
"href": "http://localhost:8080/tasks/search"
}
No link is being templated in out of the box HAL responses rendered by Spring Data Rest, contrary to what is shown in docs.
If I follow next link, the self link is not correct:
"_links": {
"first": {
"href": "http://localhost:8080/tasks?page=0&size=20"
},
"prev": {
"href": "http://localhost:8080/tasks?page=0&size=20"
},
"self": {
"href": "http://localhost:8080/tasks"
},
"next": {
"href": "http://localhost:8080/tasks?page=2&size=20"
},
"last": {
"href": "http://localhost:8080/tasks?page=2&size=20"
},
"profile": {
"href": "http://localhost:8080/profile/tasks"
},
"search": {
"href": "http://localhost:8080/tasks/search"
}
}
If I override the Controller:
#RequestMapping(method = RequestMethod.GET, path = "/tasks")
public ResponseEntity<Page<Task>> read(Pageable pageRequest, PersistentEntityResourceAssembler assembler) {
Page<Task> pendingTasks = taskService.read(pageRequest);
return new ResponseEntity(pageAssembler.toResource(pendingTasks, (ResourceAssembler) assembler),
HttpStatus.OK);
}
And add a service between the controller and repository that initializes a Pageable instance to default values even if none is specified:
public Page<Task> read(Pageable pageRequest) {
Pageable effectivePageRequest = pageRequest;
if (effectivePageRequest == null) {
effectivePageRequest = new PageRequest(0, 20, DEFAULT_SORT);
}
if (effectivePageRequest.getSort() == null) {
//override Sort
effectivePageRequest = new PageRequest(effectivePageRequest.getPageNumber(),
effectivePageRequest.getPageSize(), DEFAULT_SORT);
}
return taskRepository.findByStatus(Task.Status.PENDING, effectivePageRequest);
}
I can overcome the problem with the self link. But have seen no way of generating the templlated URIs. These are the links for the second page:
"_links": {
"first": {
"href": "http://localhost:8080/tasks?page=0&size=20&sort=created,desc"
},
"prev": {
"href": "http://localhost:8080/tasks?page=0&size=20&sort=created,desc"
},
"self": {
"href": "http://localhost:8080/tasks?page=1&size=20&sort=created,desc"
},
"next": {
"href": "http://localhost:8080/tasks?page=2&size=20&sort=created,desc"
},
"last": {
"href": "http://localhost:8080/tasks?page=2&size=20&sort=created,desc"
}
Hope this helps discarding possibilities, but default behavior shown here suggests that there may be some problem with Spring HATEOAS/Spring Data Rest when generating collection links.
I'm using org.springframework.boot:spring-boot-starter-data-rest:jar:1.4.0.RELEASE, org.springframework.data:spring-data-rest-webmvc:jar:2.5.2.RELEASE, org.springframework.hateoas:spring-hateoas:jar:0.20.0.RELEASE

Resources