spring-hateoas icon indicating copy to clipboard operation
spring-hateoas copied to clipboard

Limitations of nested embedded resources through private HalRepresentationModel

Open R-o-la-n-d opened this issue 3 years ago • 0 comments
trafficstars

We have a REST service which is implement with SpringBoot and HATEOAS. The following method exists within the WebService

@Override
public ResponseEntity<?> getCar(String carId) {
        Car car = carService.findById(carId).orElseThrow(() -> {
                throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Car not found!");
        });
               
       // toDTO & HATEOAS Links
       CarItem carItem = carModelAssembler.toModel(car);
         
       HalModelBuilder builder = HalModelBuilder.halModelOf(carItem);
       builder.embed(garageModelAssembler.toModel(car.getGarage()), LinkRelation.of("garage"));
            
       return ResponseEntity.ok(builder.build());
}
 

The response looks like this (abbreviated):

{
   "id":"xyu",
   "garageId":"096a07ea-3970-4968-9cd9-4756ae6a9cb1",
   "_embedded": {
       "garage":{
         "id":"abc",
         "location":"XY",
         "_links":{
            "self":{
               "href":"https://localhost:8443/api/v1/garages/096a07ea-3970-4968-9cd9-4756ae6a9cb1"
            },
        }
     }
  },
  "_links": {
    "self": {
      "href":"https://localhost:8443/api/v1/cars/d589852f-0b3f-4025-86cb-2fa3b35a1339"
    }
  }
}

We would also like to output the embedded objects in our paged responses. We came up with the following as a solution:

@Component
public class CarModelAssembler implements RepresentationModelAssembler<Car, RepresentationModel<CarItem>> {
        private GarageModelAssembler garageModelAssembler;
        private CarMapper carMapper;
 
        public BuildingModelAssembler(CarMapper carMapper, GarageModelAssembler garageModelAssembler) {
                this.carMapper = carMapper;
                this.garageModelAssembler = garageModelAssembler;
        }
 
        @Override
        public RepresentationModel<CarItem> toModel(Car car) {
                return toModel(car, true);
        }
       
        public RepresentationModel<CarItem> toModel(Car car, boolean embedded) {
                CarItem carItem = locationMapper.CarToCarItem(car);
                HalModelBuilder builder = HalModelBuilder.halModelOf(carItem);
               
                // add Links
                builder.link(linkTo(methodOn(CarController.class).getBuilding(carItem.getId())).withSelfRel());
                builder.link(linkTo(methodOn(GarageController.class).getGarage(carItem.getGarage.getId())).withRel("garage"));
               
                if(embedded) {
                        builder.embed(garageModelAssembler.toModel(car.getGarage(), false));
                }
               
                return builder.<CarItem>build();
        }
}

The call in the controller is then as follows: CollectionModel<CarItem> carItems = pagedResourcesAssembler.toModel(pCars, carModelAssembler);

The whole thing works in principle, but I don't like the fact that: a. - I have to pack another RepresentationModel (RepresentationModel<CarItem>) around my CarItem (extends RepresentationModel<CarItem>). b. - I still have to install the embedded switch in the assembler so that there are no infinite nestings c. - I have to inject the required assemblers for the embedded objects everywhere, which means that sooner or later I have to work with @Lazy

Unfortunately, I cannot use the HalRepresentationModel directly because it is private

I'm hoping someone here has a better idea.

R-o-la-n-d avatar Nov 04 '22 16:11 R-o-la-n-d