0
votes

I'm developing Spring HATEOAS REST api. Now I encountered a need to hide some fields of Employee class (I want to hide some of them). As far as i understand DTO (data transfer objects) must be used. So i created new class (EmployeeDTO) with only this fields, that I need. I am using ModelMapper to map appropriate fields.

So now, I have a problem with data types. Do i have to change all return values in my Controller from Employee to EmployeeDTO? And then change everything in ModelAssembler?

Or maybe DTO class should extends RepresentationModel? (This forces to extends it in Entity too). Looking forward for your help, all of this looks kinda messy (changing all return types) and I feel like there should be smarter solution.

That's how am I doing it right now:

Methods in EmployeeController:

@GetMapping(value = "/{id}", produces = "application/hal+json")
    public EntityModel<Employee> getEmployeeById(@PathVariable Long id) {
        Optional<Employee> employee = employeeService.findById(id);
        return employeeModelAssembler.toModel(employee.get());
}

@GetMapping(value = "", produces = "application/hal+json")
    public CollectionModel<EntityModel<Employee>> getAllEmployees() {
        List<Employee> employeesList = EmployeeService.findall();
        return employeeModelAssembler.toCollectionModel(EmployeesList);
}

EmployeeModelAssembler (autowired in Controller):

public class EmployeeModelAssembler implements RepresentationModelAssembler<Employee, EntityModel<Employee>> {

    @Override
    public EntityModel<Employee> toModel(Employee employee) {
        EntityModel<Employee> employeeEntityModel = EntityModel.of(employee);

        Link selfLink = linkTo(methodOn(EmployeeController.class).getEmployeeById(employee.getId())).withSelfRel();
        employeeEntityModel.add(selfLink);

        return employeeEntityModel;
    }

    @Override
    public CollectionModel<EntityModel<Employee>> toCollectionModel(Iterable<? extends Employee> entities) {

        List<Employee> employeesList = (List<Employee>) entities;
        List<EntityModel<Employee>> employeeEML = employeesList.stream().map(this::toModel).collect(Collectors.toList());

        Link selfLink = linkTo(methodOn(EmployeeController.class).getAllEmployees()).withSelfRel();

        return CollectionModel.of(employeeEML, selfLink);
    }
}

EDIT

This is the way I'm accessing this hal+json response in another controller. The goal is to get List of EmployeeDTO objects:

String uri = "http://localhost:8080/api/employees";

Traverson traverson = new Traverson(URI.create(uri), MediaTypes.HAL_JSON);
Traverson.TraversalBuilder tb = traverson.follow("href");

ParameterizedTypeReference<CollectionModel<EmployeeDTO>> typeReference = new ParameterizedTypeReference<CollectionModel<EmployeeDTO>>() {};
CollectionModel<EmployeeDTO> resEmployees = tb.toObject(typeReference);
Collection<EmployeeDTO> employees = resEmployees.getContent();

ArrayList<EmployeesDTO> employeesList = new ArrayList<>(employees);

for(EmployeesDTO x : employeesList) {
    System.out.println(x);
    System.out.println(x.getLinks());
}
1

1 Answers

3
votes

If you want to decouple the resource exposed by your API from your JPA entity, which is a good idea, you will have to update your controller to handle the appropriate types.

See the example below:

Firstly, let's suppose you have the following DTO:

public class EmployeeDTO extends RepresentationModel<EmployeeDTO> {
      private long id;
      private String surname;
      private String departmentId;

      // getters and setters
}

Note how I'm not mixing your JPA entity (Employee) with the DTO.

Now your controller should be updated to return the appropriate type.

@GetMapping(value = "/{id}", produces = "application/hal+json")
public EmployeeDTO getEmployeeById(@PathVariable long id) {
       Optional<Employee> employee = employeeService.findById(id);
       return employeeModelAssembler.toModel(employee.get());
}

@GetMapping(value = "", produces = "application/hal+json")
public CollectionModel<EmployeeDTO> getAllEmployees() {
    List<Employee> employeesList = employeeService.findAll();
    return employeeModelAssembler.toCollectionModel(employeesList);
}

And an example of a ModelAssembler

@Component
public class EmployeeModelAssembler implements RepresentationModelAssembler<Employee, EmployeeDTO> {

    @Override
    public EmployeeDTO toModel(Employee employee) {
        ModelMapper modelMapper = new ModelMapper();
        EmployeeDTO employeeDto = modelMapper.map(employee, EmployeeDTO.class);

        Link selfLink = linkTo(methodOn(EmployeeController.class).getEmployeeById(employee.getId())).withSelfRel();
        employeeDto.add(selfLink);

        return employeeDto;
    }

    @Override
    public CollectionModel<EmployeeDTO> toCollectionModel(Iterable<? extends Employee> employeesList) {
       ModelMapper modelMapper = new ModelMapper();
       List<EmployeeDTO> employeeDTOS = new ArrayList<>();

       for (Employee employee : employeesList){
           EmployeeDTO employeeDto = modelMapper.map(employee, EmployeeDTO.class);
           employeeDto.add(linkTo(methodOn(EmployeeController.class)
                                .getEmployeeById(employeeDto.getId())).withSelfRel());
           employeeDTOS.add(employeeDto);
        }

        return new CollectionModel<>(employeeDTOS);
    }
}

I tested it and got the following response, assuming I have an employees endpoint

get employees/1

{
"id": 1,
"surname": "teste",
"departmentId": "1",
"_links": {
    "self": {
        "href": "http://localhost:8080/employees/1"
    }
}
}

get employees

{
    "_embedded": {
        "employeeDtoList": [
            {
                "id": 1,
                "surname": "teste",
                "departmentId": "1",
                "_links": {
                    "self": {
                        "href": "http://localhost:8080/employees/1"
                    }
                }
            },
            {
                "id": 2,
                "surname": "teste",
                "departmentId": "2",
                "_links": {
                    "self": {
                        "href": "http://localhost:8080/employees/2"
                    }
                }
            }
        ]
    }
}