Canonical _links with Spring HATEOAS
Asked Answered
B

3

6

We're building a RESTful web service similiar to the spring.io guide "Accessing JPA Data with REST". To reproduce the sample outputs below, it suffices to add a ManyToOne-Relation to Person as follows:

// ...

@Entity
public class Person {

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private long id;

  private String firstName;
  private String lastName;

  @ManyToOne
  private Person father;

  // getters and setters
}

A GET request after adding some sample data yields:

{
  "firstName" : "Paul",
  "lastName" : "Mustermann",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/people/1"
    },
    "father" : {
      "href" : "http://localhost:8080/people/1/father"
    }
  }
}

But, given Paul's father is stored with ID 2, our desired result would be the canonical url for its relation:

// ...
    "father" : {
      "href" : "http://localhost:8080/people/2"
    }
// ...

This of course will cause problems if father is null on some persons (OK, this does not make much sense here... ;)), but in this case we would like to not render the link in JSON at all.

We already tried to implement a ResourceProcessor to achieve this, but it seems that by the time the processor is called the links are not populated yet. We managed to add additional links pointing to the desired canonical url, but failed to modify the somehow later added links.

Question: Is there a generic approach to customize the link generation for all resources?

To clarify our need for canonical URLs: We use the SproutCore Javascript framework to access the RESTful web service. It uses an "ORM-like" abstraction of data sources for which we've implemented a generic handler of the JSON output Spring produces. When querying for all persons, we would need to send n*(1+q) requests (instead of just n) for n persons with q relations to other persons to sync them to the client side data source. This is because the default "non-canonical" link contains absolutely no information about a father being set or the father's id. It just seems that this causes a huge amount of unnecessary requests which could be easily avoided if the initial response would contain a little more information in the first place.

Another solution would be to add the father's id to the relation, e.g.:

"father" : {
  "href" : "http://localhost:8080/people/1/father",
  "id" : 2
}
Blevins answered 4/7, 2014 at 8:51 Comment(0)
D
2

There is a discussion somewhere Spring Data Rest team explained why properties are rendered as links that way. Having said you could still achieve what you like by suppressing link generated by SDR and implementing ResourceProcessor. So your Person class would look like below. Note the annotation @RestResource(exported = false) to suppress link

@Entity
public class Person {

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private long id;

  private String firstName;
  private String lastName;

  @ManyToOne
  @RestResource(exported = false)
  private Person father;

  // getters and setters
}

Your ResourceProcessor class would look like

public class EmployeeResourceProcessor implements
        ResourceProcessor<Resource<Person>> {

    @Autowired
    private EntityLinks entityLinks;

    @Override
    public Resource<Person> process(Resource<Person> resource) {
        Person person = resource.getContent();
        if (person.getFather() != null) {
            resource.add(entityLinks.linkForSingleResour(Person.class, person.getFather().getId())
                .withRel("father"));
        }
        return resource;
    }

}

The above solution works only if father value is eagerly fetched along with Person. Otherwise you need to have property fatherId and use it instead of father property. Don't forget to use Jackson @ignore... to hide fatherId in response JSON.

Note: I haven't tested this myself but guessing it would work

Dailey answered 9/7, 2014 at 17:54 Comment(4)
i wasn't sure how to do it without implementing a controller...that's interesting stuff. Do you have to register EmployeeResourceProcessor as a bean?Skerl
Yes, the ResourceProcessor needs to be registered as a bean. I got this working already. Unfortunately the @RestResource annotation causes a StackOverflowError (infinite recursion) - it seems it is not applicable to single attributes, but to repositories?Blevins
Yes you need register EmployeeResourceProcessor. Also make sure latest version of SDR (2.1.1)Dailey
This works, except the @RestResource(exported = false) part. This doesn't do what you (well I) expect. Rather than hiding the field from the links, it actually includes all the fields of the non-exported field directly into the serialized object. I think you need to suppress the link in the ResourceProcessorMcclenon
S
2

Since I had the same problem, I created a Jira issue at spring-data-rest: https://jira.spring.io/browse/DATAREST-682

If enough people vote for it, perhaps we can convince some of the developers to implement it :-).

Somme answered 26/11, 2015 at 8:19 Comment(1)
I've added some explanation to the issue: there is another way to force SDR to include canonical links of related resources using additional projections: jira.spring.io/browse/…Functional
S
0

It's weird that you're pushing to display the canonical link. Once that resource at /father is retrieved the self link should be canonical...but there's really not a good reason to force the father relationship to be canonical...maybe some cacheing scheme?

To your specific question...you're relying on auto-generated controllers so you've given up the right to make decisions about a lot of your links. If you were to have your own PersonController than you would be more in charge of the link structure. If you created your own controller you could then use EntityLinks https://github.com/spring-projects/spring-hateoas#entitylinks with the Father's ID..IE

@Controller
@ExposesResourceFor(Person.class)
@RequestMapping("/people")
class PersonController {

  @RequestMapping
  ResponseEntity people(…) { … }

  @RequestMapping("/{id}")
  ResponseEntity person(@PathVariable("id") … ) {
    PersonResource p = ....
    if(p.father){
      p.addLink(entityLinks.linkToSingleResource(Person.class, orderId).withRel("father");
    }
  }
}

But that seems like a lot of work to just change the URL

Skerl answered 8/7, 2014 at 22:59 Comment(2)
Thank you for your answer!To clarify our need for canonical URLs: We use the SproutCore Javascript framework to access the RESTful web service. It uses an "ORM-like" abstraction of data sources for which we've implemented a generic handler of the JSON output Spring produces.Blevins
OK, I'm not very familiar with StackOverlow yet ;). The above comment was saved accidently by pressing return and I missed the 5 minute timeframe to edit it. I will update the question to clarify our need for canonical URLs (or a similar solution) and we will consider writing controllers for each entity, although it seems cumbersome for this task.Blevins

© 2022 - 2024 — McMap. All rights reserved.