Spring HATEOAS recursive representation model processors?
Asked Answered
C

1

6

I have a question concerning the representation model processors of Spring HATEOAS. We are experimenting to process models before serializing them to the client. Our use case is to enrich the imageUrl field of UserModel objects at runtime, as we have to build the URL based on values from a config bean (AWS S3 bucket URL differs for DEV / PROD setup).

@Data
public class UserModel {
    // ...
    private String imageUrl;
}

Therefore, we create a UserProcessor to implement this:

public class UserProcessor implements RepresentationModelProcessor<EntityModel<UserModel>> {

    private final ConfigAccessor configAccessor;

    public UserProcessor(ConfigAccessor configAccessor) {
        this.configAccessor = configAccessor;
    }

    @Override
    public EntityModel<UserModel> process(EntityModel<UserModel> model) {
        if (model.getContent() != null)
            // do the enrichment and set "imageUrl" field
        }
        return model;
    }
}

This works perfectly if we have a controller method like this:

@ResponseBody
@GetMapping("/me")
public EntityModel<UserModel> getCurrentUser(@AuthenticationPrincipal Principal principal) {
    UserModel user = ... // get user model
    return EntityModel.of(user);
}

However, we are struggling now with the enrichment whenever a UserModel is referenced in another model class, e.g., the BookModel:

@Data
public class BookModel {
    private String isbn;
    // ...
    private EntityModel<UserModel> user;  // or "private UserModel user;"
}

A controller method returning type EntityModel<BookModel> only applies the processor for its type, but not for types that are referenced. It seems the processors are not applied recursively.

Is this intentional or are we doing something wrong?

Thanks for any input and help, Michael

Centro answered 6/7, 2020 at 10:31 Comment(0)
A
1

I encountered the same issue and I resolved it by manually assembling resources, in your case that would be implementing RepresentationModelAssembler of the BookModel and then manually invoking the processor on the userModel object that is inside the book.

Make the outer resource a representation model

First consider the BookModel to extend RepresentationModel so that you can manually add links and assemble inner resources (which you would like for the EntityModel<UserModel> object)

@Data
public class BookModel extends RepresentationModel<BookModel> {...}

Write a model assembler

Now write the assembler that takes your book entity and transforms it into a representation model or a collection of these models. You will implement here what EntityModel.of(...) does for you automagically.

@Component
public class BookModelAssembler implements RepresentationModelAssembler<Book, BookModel> {
    
    @Autowired
    private UserProcessor userProcessor;

    @Override
    public BookModel toModel(Book entity) {
        var bookModel = new BookModel(entity) // map fields from entity to model
        // Transform the user entity to an entity model of user
        var user = entity.getUser();       
        EntityModel<UserModel> userModel = EntityModel.of(user);
        userModel = userProcessor.process(userModel);

        bookModel.setUserModel(userModel);

        return bookModel;
    }
}

I might be going out on a limb but I suppose the reason for this is that the processors get invoked when an MVC endpoint returns a type that has a registered processor, which in the case of embedded types is not invoked. My reasoning is based on the docs for RepresentationModelProcessor, which states that processor processes representation models returned from Spring MVC controllers.

Armorial answered 23/7, 2020 at 13:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.