27

I'm trying to do something I think should be really simple. I have a Question object, setup with spring-boot, spring-data-rest and spring-hateoas. All the basics work fine. I would like to add a custom controller that returns a List<Question> in exactly the same format that a GET to my Repository's /questions url does, so that the responses between the two are compatible.

Here is my controller:

@Controller
public class QuestionListController {

    @Autowired private QuestionRepository questionRepository;

    @Autowired private PagedResourcesAssembler<Question> pagedResourcesAssembler;

    @Autowired private QuestionResourceAssembler questionResourceAssembler;

    @RequestMapping(
            value = "/api/questions/filter", method = RequestMethod.GET,
            consumes = MediaType.APPLICATION_JSON_VALUE,
            produces = MediaType.APPLICATION_JSON_VALUE)
    public @ResponseBody PagedResources<QuestionResource> filter(
            @RequestParam(value = "filter", required = false) String filter,
            Pageable p) {

        // Using queryDSL here to get a paged list of Questions
        Page<Question> page = 
            questionRepository.findAll(
                QuestionPredicate.findWithFilter(filter), p);

        // Option 1 - default resource assembler
        return pagedResourcesAssembler.toResource(page);

        // Option 2 - custom resource assembler
        return pagedResourcesAssembler.toResource(page, questionResourceAssembler);
    }

}

Option 1: Rely on the provided SimplePagedResourceAssembler

The problem with this option is none of the necessary _links are rendered. If there was a fix for this, it would be the easiest solution.

Option 2: Implement my open resource assembler

The problem with this option is that implementing QuestionResourceAssembler according to the Spring-Hateoas documentation leads down a path where the QuestionResource ends up being a near-duplicate of Question, and then the assembler needs to manually copy data between the two objects, and I need to build all the relevant _links by hand. This seems like a lot of wasted effort.

What to do?

I know Spring has already generated the code to do all this when it exports the QuestionRepository. Is there any way I can tap into that code and use it, to ensure the output from my controller is seamless and interchangeable with the generated responses?

JBCP
  • 13,109
  • 9
  • 73
  • 111

3 Answers3

26

I've found a way to imitate the behavior of Spring Data Rest completely. The trick lies in using a combination of the PagedResourcesAssembler and an argument-injected instance of PersistentEntityResourceAssembler. Simply define your controller as follows...

@RepositoryRestController
@RequestMapping("...")
public class ThingController {

    @Autowired
    private PagedResourcesAssembler pagedResourcesAssembler;

    @SuppressWarnings("unchecked") // optional - ignores warning on return statement below...
    @RequestMapping(value = "...", method = RequestMethod.GET)
    @ResponseBody
    public PagedResources<PersistentEntityResource> customMethod(
            ...,
            Pageable pageable,
            // this gets automatically injected by Spring...
            PersistentEntityResourceAssembler resourceAssembler) {

        Page<MyEntity> page = ...;
        ...
        return pagedResourcesAssembler.toResource(page, resourceAssembler);
    }
}

This works thanks to the existence of PersistentEntityResourceAssemblerArgumentResolver, which Spring uses to inject the PersistentEntityResourceAssembler for you. The result is exactly what you'd expect from one of your repository query methods!

Ryan
  • 261
  • 3
  • 2
  • Nice, I'll give try it out. – JBCP Apr 29 '15 at 17:00
  • See also http://stackoverflow.com/questions/31758862/enable-hal-serialization-in-spring-boot-for-custom-controller-method#31782016. –  Oct 11 '15 at 00:50
  • Small addition: You can prevent the unchecked warning, when your method directly returns `PagedResource` this is what (paged)resourceAssembler.toResource returns – Robert Jan 06 '17 at 12:55
  • 3
    This worked except when no results were found. In that case, no `_embedded` key was in the REST response, unlike in an auto-generated REST/HATEOAS controller. I had to manually call `pagedResourcesAssembler.toEmptyResource` in this instance. – alalonde Feb 27 '17 at 19:47
  • 1
    @alalonde maybe the Spring Data REST behaviour changed, but I also get no `_embedded` if a collection is empty in generated HATEOAS repositories. – Hubert Grzeskowiak Jun 08 '17 at 02:38
  • @Robert Sorry but I'm not able to remove the uncheked warning in my code. I tried what you suggested but I've always a warning on toResource() method. Thanks! – drenda Nov 14 '17 at 18:26
  • SDR is failing to inject the `PersistentEntityResourceAssembler` with Spring Boot 2.1.0. It gives `java.lang.IllegalArgumentException: entities is marked @NonNull but is null`. – Jefferson Lima Jan 03 '19 at 18:38
9

Updated answer on this old question: You can now do that with a PersistentEntityResourceAssembler

Inside your @RepositoryRestController:

@RequestMapping(value = "somePath", method = POST)
public @ResponseBody PersistentEntityResource postEntity(@RequestBody Resource<EntityModel> newEntityResource, PersistentEntityResourceAssembler resourceAssembler)
{
  EntityModel newEntity = newEntityResource.getContent();
  // ... do something additional with new Entity if you want here ...  
  EntityModel savedEntity = entityRepo.save(newEntity);

  return resourceAssembler.toResource(savedEntity);  // this will create the complete HATEOAS response
}
Robert
  • 1,579
  • 1
  • 21
  • 36
  • Is it possible to accept a link to an existing resource in the controller method arguments and convert it to an entity automagically? – aycanadal Feb 15 '17 at 05:49
  • Yes, if your entity links to another "child" entity, then you can simply POST a URI to create that link. e.g HTTP POST /path/to/parent/entity Payload is for ecample `{someAttr: "example Value", linkToChildEntity: "/path/to/child/entity/" }` Hope this helps. See [this stackoverflow question](http://stackoverflow.com/questions/25311978/posting-a-onetomany-sub-resource-association-in-spring-data-rest?rq=1) for far more details – Robert Feb 17 '17 at 19:10
  • In a custom controller I mean. Spring data rest does it, I know. Question is, how can I write a controller that can do the same? – aycanadal Feb 18 '17 at 05:14
  • 1
    how to deal with null values? ... "PersistentEntity must not be null!" – Rafael Mar 30 '17 at 11:21
  • Also i am not sure how to deal with collections as e.g. GET / findAll operations. Can you provide some woking example, please? – Rafael Mar 30 '17 at 11:40
  • @Rafael IMHO a HATEOAS REST WebService should never return just simply null. It should always at least return an empty JSON array: { } – Robert Mar 31 '17 at 11:53
  • My working example is checked in here: https://github.com/Doogiemuc/liquido-backend-spring/blob/master/src/main/java/org/doogie/liquido/rest/BallotRestController.java – Robert Mar 31 '17 at 11:54
  • @Robert i totally agree ... i get that error even when the collection is not null nor empty ... maybe i have somethinf wrong. Thanks. – Rafael Mar 31 '17 at 15:46
  • See https://jira.spring.io/browse/DATAREST-657 before using this approach – Miguel Pereira Oct 16 '17 at 20:25
  • @aycanadal regarding your question, if you take your entity as a parameter using the Resource<> to encapsulate the entity, SDR should translate the association links to their actual entities. In the example above: `@RequestBody Resource` – Davi Cavalcanti Oct 23 '18 at 00:50
4

I believe I've solved this problem in a fairly straightforward way, although it could have been better documented.

After reading the implementation of SimplePagedResourceAssembler I realized a hybrid solution might work. The provided Resource<?> class renders entities correctly, but doesn't include links, so all you need to do is add them.

My QuestionResourceAssembler implementation looks like this:

@Component
public class QuestionResourceAssembler implements ResourceAssembler<Question, Resource<Question>> {

    @Autowired EntityLinks entityLinks;

    @Override
    public Resource<Question> toResource(Question question) {
        Resource<Question> resource = new Resource<Question>(question);

        final LinkBuilder lb = 
            entityLinks.linkForSingleResource(Question.class, question.getId());

        resource.add(lb.withSelfRel());
        resource.add(lb.slash("answers").withRel("answers"));
        // other links

        return resource;
    }
}

Once that's done, in my controller I used Option 2 above:

    return pagedResourcesAssembler.toResource(page, questionResourceAssembler);

This works well, and isn't too much code. The only hassle is you need to manually add links for each reference you need.

JBCP
  • 13,109
  • 9
  • 73
  • 111
  • Sure seems like Spring could've made this a lot easier. Doesn't the fact that you have to add your own links still lead to a potentially disjointed API (like you pointed out elsewhere)? And do you have to use Pages everywhere? When I just try to return a List> I get recursion because of bi-directional hibernate mappings. So I either have to add @JsonIgnore's or use a PagedResourcesAssembler instead. – gyoder Jan 02 '15 at 13:46
  • I think you want to use PagedResources and Pageable any time you are returning more than one object anyway, just for safety. Definitely it could be much easier, but you don't really end up with a disjointed API if you rely on Spring's own linkbuilder. It is possible you don't end up with links to useful destinations, but since what is relevant is up to the Controller it makes some sense. Its true Spring could probably provide useful links based on the object type returned. – JBCP Jan 02 '15 at 19:02
  • 1
    In 2016, is this still the best/only way to get spring-data-rest esque behavior when using a controller / custom repository? It's quite an annoyance. – Casey Oct 30 '16 at 09:54
  • Sorry, I no longer work with Spring, so I can't answer what the latest technique is. – JBCP Oct 30 '16 at 13:44