How do I expose a new REST method to through a Spring Data MongoDB repository with ALPS/HATEOAS metadata?
Asked Answered
G

1

3

How do I expose a new method in a MongoRepository extension class to the generated REST API and include the same ALPS/HATEOAS metadata, links etc.

I have the usual Spring Data MongoDB repository:

public interface CollectionRepository extends Repository<Collection, String> {    
    // Simple queries
    Collection findFirstByName(@Param("name") String name);   
}

Now I want to add another method and have it integrate into the generated Repository REST API so that it is included in the {repository}/collection/search response alongside the QueryDSL generated elements.

I'm stuck on 2 things 1) Using PersistentEntityResourceAssembler and PersistentEntityResource to provide the parameters and convert the response into the appropriate format. 2) Automating the generation of the HATEOAS/ALPS metadata so the parent URL shows the method with parameters in the API browser.

Here's my custom controller. Note that I had to specify a @Qualifier on the template autowiring as I have multiple databases and it was picking the wrong one be default.

@Component
@RepositoryRestController
@RequestMapping(value = "{repository}/search")
public class CollectionRepositoryController implements ResourceProcessor<RepositorySearchesResource> {

    @Autowired private EntityLinks entityLinks;
    @Autowired private PagedResourcesAssembler pagedResourcesAssembler;
    @Autowired Repositories repositories;
    @Autowired
    @Qualifier("mongoTemplateCollections")
    private MongoTemplate mongoTemplate;
    private final CollectionRepository repository;
    @Autowired
    public CollectionRepositoryController(CollectionRepository repo) {
        repository = repo;
    }
    @SuppressWarnings("unchecked")
    @RequestMapping(value = "findText", method = RequestMethod.GET,
            produces = MediaType.APPLICATION_JSON_VALUE)
    public PagedResources<PersistentEntityResource> findText(
            Pageable pageable, @RequestParam(value = "findText", required = false) String findText,
            PersistentEntityResourceAssembler resourceAssembler) {
        TextCriteria criteria = TextCriteria.forDefaultLanguage().matchingAny(findText);
        Query query = TextQuery.queryText(criteria).sortByScore();
        List<Collection> collections = mongoTemplate.find(query, Collection.class);
        Page<Collection> page = new PageImpl<Collection>(Arrays.asList(new Collection()), pageable, pageable.getOffset());

       // What goes below here to convert List<Collections> into PersistentEntityResource ?

        PersistentEntity<?, ?> persistentEntity = repositories.getPersistentEntity(Collection.class);
        for(Collection collection : collections) {
            PersistentEntityResource collectionResource = PersistentEntityResource.build(collection, persistentEntity).
                    withLink(new Link("/collection/" + collection.getId())).build();
            Log.info("collections resource: " + collectionResource);
        }

        return pagedResourcesAssembler.toResource(page, resourceAssembler);
    }

    // https://mcmap.net/q/871850/-how-to-add-custom-methods-to-spring-data-rest-jpa-implementation-and-leverage-hateos-support
    @Override
    public RepositorySearchesResource process(RepositorySearchesResource resource) {
        LinkBuilder lb = entityLinks.linkFor(Collection.class, "name");
        resource.add(new Link(lb.toString() + "/search/findText{?findText}", "findText"));
        return resource;
    }
}

I also created this, but I'm not sure how/where to wire it in:

@Component
public class CollectionResourceAssembler implements ResourceAssembler<Collection, Resource<Collection>> {

    @Autowired
    EntityLinks entityLinks;

    @Override
    public Resource<Collection> toResource(Collection collection) {
        Resource<Collection> resource = new Resource<Collection>(collection);

        final LinkBuilder lb = entityLinks.linkForSingleResource(Collection.class, collection.getId());
        resource.add(lb.withSelfRel());
        resource.add(lb.slash("answers").withRel("answers"));
        // other links

        return resource;
    }
}

The bit I'm having trouble with is the "what goes below here", such that it does as Oliver suggested:

calling the repository and using a PersistentEntityResourceAssembler injectable into the handler method to produce a PersistentEntityResource to return.

And, do I really have to manually set the ALPS/HATEOAS data using the ResourceProcessor.process() method or is there a trick to automating that?

This is where an example would be priceless. I'm pretty sure there's nothing in SO that shows how to do this, at least at the level of handholding I need. This one is also pretty close: Defining a resource assembler for a REST Spring HATEOAS controller but also assumes more than I know.

Gerald answered 22/7, 2015 at 22:39 Comment(0)
S
3

tl;dr

It will need custom implementations for the repository and the controller.

Details

We have to make sure we're not getting lost in all the different aspects you're mentioning here. I'll try to untangle the branches bottom up:

Executing MongoDB scripts

As the reference documentation on executing scripts with MongoDB states (and you already discovered), the functionality is provided by MongoTemplate's ScriptOperations. Thus how to use that API should be clear. Consult the Javadoc for more information.

Executing scripts via Spring Data repositories

The next thing you're asking for is executing those scripts through the repository abstraction. To not throw the baby out with the bath water here, make sure we understand the purpose of a repository: it simulates a collection of aggregate roots and access to it without exposing the underlying persistence mechanism. Exposing types like ExecutableMongoScript on the repository would break the latter trait. Thus the correct approach here is to craft a custom implementation for that particular functionality as described in the reference documentation on Spring Data repositories.

Exposing the functionality via REST

I am assuming you're referring to Spring Data REST's feature to expose a dedicated resource for the repository query method in your question. Spring Data REST currently only exposes declarative query methods automatically, mostly for the reason that it's hard to reason about the correct HTTP method to support for custom implementations as we cannot guess what's happening inside the method.

The recommended way to expose custom repository query methods using a custom controller with an @RequestMapping suiting your purpose, calling the repository and using a PersistentEntityResourceAssembler injectable into the handler method to produce a PersistentEntityResource to return.

Satterfield answered 23/7, 2015 at 14:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.