Spring HATEOAS embedded resource support
Asked Answered
F

8

54

I want to use the HAL format for my REST API to include embedded resources. I'm using Spring HATEOAS for my APIs and Spring HATEOAS seems to support embedded resources; however, there's no documentation or example on how to use this.

Can someone provide an example how to use Spring HATEOAS to include embedded resources?

Featherweight answered 15/9, 2014 at 23:57 Comment(0)
R
51

Make sure to read Spring's documentation about HATEOAS, it helps to get the basics.

In this answer a core developer points out the concept of Resource, Resources and PagedResources, something essential which is is not covered by the documentation.

It took me some time to understand how it works, so let's step through some examples to make it crystal-clear.

Returning a Single Resource

the resource

import org.springframework.hateoas.ResourceSupport;


public class ProductResource extends ResourceSupport{
    final String name;

    public ProductResource(String name) {
        this.name = name;
    }
}

the controller

import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyController {
    @RequestMapping("products/{id}", method = RequestMethod.GET)
    ResponseEntity<Resource<ProductResource>> get(@PathVariable Long id) {
        ProductResource productResource = new ProductResource("Apfelstrudel");
        Resource<ProductResource> resource = new Resource<>(productResource, new Link("http://example.com/products/1"));
        return ResponseEntity.ok(resource);
    }
}

the response

{
    "name": "Apfelstrudel",
    "_links": {
        "self": { "href": "http://example.com/products/1" }
    }
}

Returning Multiple Resources

Spring HATEOAS comes with embedded support, which is used by Resources to reflect a response with multiple resources.

    @RequestMapping("products/", method = RequestMethod.GET)
    ResponseEntity<Resources<Resource<ProductResource>>> getAll() {
        ProductResource p1 = new ProductResource("Apfelstrudel");
        ProductResource p2 = new ProductResource("Schnitzel");

        Resource<ProductResource> r1 = new Resource<>(p1, new Link("http://example.com/products/1"));
        Resource<ProductResource> r2 = new Resource<>(p2, new Link("http://example.com/products/2"));

        Link link = new Link("http://example.com/products/");
        Resources<Resource<ProductResource>> resources = new Resources<>(Arrays.asList(r1, r2), link);

        return ResponseEntity.ok(resources);
    }

the response

{
    "_links": {
        "self": { "href": "http://example.com/products/" }
    },
    "_embedded": {
        "productResources": [{
            "name": "Apfelstrudel",
            "_links": {
                "self": { "href": "http://example.com/products/1" }
            }, {
            "name": "Schnitzel",
            "_links": {
                "self": { "href": "http://example.com/products/2" }
            }
        }]
    }
}

If you want to change the key productResources you need to annotate your resource:

@Relation(collectionRelation = "items")
class ProductResource ...

Returning a Resource with Embedded Resources

This is when you need to start to pimp Spring. The HALResource introduced by @chris-damour in another answer suits perfectly.

public class OrderResource extends HalResource {
    final float totalPrice;

    public OrderResource(float totalPrice) {
        this.totalPrice = totalPrice;
    }
}

the controller

    @RequestMapping(name = "orders/{id}", method = RequestMethod.GET)
    ResponseEntity<OrderResource> getOrder(@PathVariable Long id) {
        ProductResource p1 = new ProductResource("Apfelstrudel");
        ProductResource p2 = new ProductResource("Schnitzel");

        Resource<ProductResource> r1 = new Resource<>(p1, new Link("http://example.com/products/1"));
        Resource<ProductResource> r2 = new Resource<>(p2, new Link("http://example.com/products/2"));
        Link link = new Link("http://example.com/order/1/products/");

        OrderResource resource = new OrderResource(12.34f);
        resource.add(new Link("http://example.com/orders/1"));

        resource.embed("products", new Resources<>(Arrays.asList(r1, r2), link));

        return ResponseEntity.ok(resource);
    }

the response

{
    "_links": {
        "self": { "href": "http://example.com/products/1" }
    },
    "totalPrice": 12.34,
    "_embedded": {
        "products":     {
            "_links": {
                "self": { "href": "http://example.com/orders/1/products/" }
            },
            "_embedded": {
                "items": [{
                    "name": "Apfelstrudel",
                    "_links": {
                        "self": { "href": "http://example.com/products/1" }
                    }, {
                    "name": "Schnitzel",
                    "_links": {
                        "self": { "href": "http://example.com/products/2" }
                    }
                }]
            }
        }
    }
}
Rabbet answered 15/5, 2016 at 9:48 Comment(4)
@Glide, you might consider accepting this answer. Great response, @linqu!Zeena
You have resource.embed("products", new Resources<>(Arrays.asList(r1, r2), link)); . Where is this embed method coming from? Is it just a rename of the HALResource embedResource method?Gerthagerti
@Gerthagerti the class OrderResource extends HalResource, that is where the embed method came from, and you are right, that is confusing: I renamed embed to embedResource.Rabbet
Thanks. Your solution got me over the roadblock :) The hardest thing was accepting that we have to do this! ;)Gerthagerti
T
33

Pre HATEOAS 1.0.0M1: I couldn't find an official way to do this...here's what we did

public abstract class HALResource extends ResourceSupport {

    private final Map<String, ResourceSupport> embedded = new HashMap<String, ResourceSupport>();

    @JsonInclude(Include.NON_EMPTY)
    @JsonProperty("_embedded")
    public Map<String, ResourceSupport> getEmbeddedResources() {
        return embedded;
    }

    public void embedResource(String relationship, ResourceSupport resource) {

        embedded.put(relationship, resource);
    }  
}

then made our resources extend HALResource

UPDATE: in HATEOAS 1.0.0M1 the EntityModel (and really anything extending RepresentationalModel) this is natively supported now as long as the embedded resource is exposed via a getContent (or however you make jackson serialize a content property). like:

    public class Result extends RepresentationalModel<Result> {
        private final List<Object> content;

        public Result(

            List<Object> content
        ){

            this.content = content;
        }

        public List<Object> getContent() {
            return content;
        }
    };

    EmbeddedWrappers wrappers = new EmbeddedWrappers(false);
    List<Object> elements = new ArrayList<>();

    elements.add(wrappers.wrap(new Product("Product1a"), LinkRelation.of("all")));
    elements.add(wrappers.wrap(new Product("Product2a"), LinkRelation.of("purchased")));
    elements.add(wrappers.wrap(new Product("Product1b"), LinkRelation.of("all")));

    return new Result(elements);

you'll get

{
 _embedded: {
   purchased: {
    name: "Product2a"
   },
  all: [
   {
    name: "Product1a"
   },
   {
    name: "Product1b"
   }
  ]
 }
}
Tapestry answered 16/9, 2014 at 17:7 Comment(9)
Same here, we couldn't find anything built-in to support this so we added an _embedded property on our resourcesUnwinking
That's what I ended up doing too so I upvoted the answer. But was hoping for an official way so won't mark the answer is accepted :(Featherweight
This is what I ended up using as well, with a minor modification. My map is of type Map<String, List<ResourceSupport>>, since a rel can have multiple instances of a particular resource (i.e., basically representing a collection of resources). The solution as it stands does not account for this.Babbette
A problem with this is that you have to create a resource class for each entity that you want to return.Beeman
Please tell me this is not the case anymore after years, doing custom stuff for the most basic standard HAL thing! Why spring HATEOAS pretends it can manage HAL and make it default, if it can't even do this automatically?Lemon
@Lemon I am afraid even in the latest HATEOAS reference documentation these issues have not been addressedSlaphappy
Where does Include come from?Gerthagerti
import com.fasterxml.jackson.annotation.JsonInclude.Include;Gerthagerti
I found it helpful, but it does not supports CurieProvider.Indoor
H
29

here is a small example what we've found. First of all we use spring-hateoas-0.16

Imaging we have GET /profile that should return user profile with embedded emails list.

We have email resource.

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@Relation(value = "email", collectionRelation = "emails")
public class EmailResource {
    private final String email;
    private final String type;
}

two emails that we want to embedded into profile response

Resource primary = new Resource(new Email("[email protected]", "primary"));
Resource home = new Resource(new Email("[email protected]", "home"));

To indicate that these resources are embedded we need an instance of EmbeddedWrappers:

import org.springframework.hateoas.core.EmbeddedWrappers
EmbeddedWrappers wrappers = new EmbeddedWrappers(true);

With the help of wrappers we can create EmbeddedWrapper instance for each email and put them into a list.

List<EmbeddedWrapper> embeddeds = Arrays.asList(wrappers.wrap(primary), wrappers.wrap(home))

The only thing is left to do is to construct our profile resource with these embeddeds. In the example below I use lombok to short the code.

@Data
@Relation(value = "profile")
public class ProfileResource {
    private final String firstName;
    private final String lastName;
    @JsonUnwrapped
    private final Resources<EmbeddedWrapper> embeddeds;
}

Keep in mind annotation @JsonUnwrapped on embeddeds field

And we are ready to return all this from controller

...
Resources<EmbeddedWrapper> embeddedEmails = new Resources(embeddeds, linkTo(EmailAddressController.class).withSelfRel());
return ResponseEntity.ok(new Resource(new ProfileResource("Thomas", "Anderson", embeddedEmails), linkTo(ProfileController.class).withSelfRel()));
}

Now in the response we'll have

{
"firstName": "Thomas",
"lastName": "Anderson",
"_links": {
    "self": {
        "href": "http://localhost:8080/profile"
    }
},
"_embedded": {
    "emails": [
        {
            "email": "[email protected]",
            "type": "primary"
        },
        {
            "email": "[email protected]",
            "type": "home"
        }
    ]
}
}

Interesting part in using Resources<EmbeddedWrapper> embeddeds is that you can put different resources in it and it will automatically group them by relations. For this we use annotation @Relation from org.springframework.hateoas.core package.

Also there is a good article about embedded resources in HAL

Humorous answered 13/3, 2015 at 18:7 Comment(2)
Thanks! Where did you find a documentation about how to use EmbeddedWrapper?Burke
Maybe this could be updated. The current version of the library doesn't have Resources class anymore. It's been replaced by CollectionModel, I believe.Sheena
D
5

Usually HATEOAS requires to create a POJO that represents the REST output and extends HATEOAS provided ResourceSupport. It is possible do this without creating the extra POJO and use the Resource, Resources and Link classes directly as shown in the code below :

@RestController
class CustomerController {

    List<Customer> customers;

    public CustomerController() {
        customers = new LinkedList<>();
        customers.add(new Customer(1, "Peter", "Test"));
        customers.add(new Customer(2, "Peter", "Test2"));
    }

    @RequestMapping(value = "/customers", method = RequestMethod.GET, produces = "application/hal+json")
    public Resources<Resource> getCustomers() {

        List<Link> links = new LinkedList<>();
        links.add(linkTo(methodOn(CustomerController.class).getCustomers()).withSelfRel());
        List<Resource> resources = customerToResource(customers.toArray(new Customer[0]));

        return new Resources<>(resources, links);

    }

    @RequestMapping(value = "/customer/{id}", method = RequestMethod.GET, produces = "application/hal+json")
    public Resources<Resource> getCustomer(@PathVariable int id) {

        Link link = linkTo(methodOn(CustomerController.class).getCustomer(id)).withSelfRel();

        Optional<Customer> customer = customers.stream().filter(customer1 -> customer1.getId() == id).findFirst();

        List<Resource> resources = customerToResource(customer.get());

        return new Resources<Resource>(resources, link);

    }

    private List<Resource> customerToResource(Customer... customers) {

        List<Resource> resources = new ArrayList<>(customers.length);

        for (Customer customer : customers) {
            Link selfLink = linkTo(methodOn(CustomerController.class).getCustomer(customer.getId())).withSelfRel();
            resources.add(new Resource<Customer>(customer, selfLink));
        }

        return resources;
    }
}
Douglass answered 29/9, 2015 at 15:37 Comment(0)
C
3

Combining the answers above I've made a much easier approach:

return resWrapper(domainObj, embeddedRes(domainObj.getSettings(), "settings"))

This is a custom utility class (see below). Note:

  • Second argument of resWrapper accepts ... of embeddedRes calls.
  • You may create another method that omits the relation String inside resWrapper.
  • First argument of embeddedRes is Object, so you may also supply an instance of ResourceSupport
  • The result of the expression is of the type that extends Resource<DomainObjClass>. So, it will be processed by all Spring Data REST ResourceProcessor<Resource<DomainObjClass>>. You may create a collection of them and also wrap around new Resources<>().

Create the utility class:

import com.fasterxml.jackson.annotation.JsonUnwrapped;
import java.util.Arrays;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.core.EmbeddedWrapper;
import org.springframework.hateoas.core.EmbeddedWrappers;

public class ResourceWithEmbeddable<T> extends Resource<T> {

    @SuppressWarnings("FieldCanBeLocal")
    @JsonUnwrapped
    private Resources<EmbeddedWrapper> wrappers;

    private ResourceWithEmbeddable(final T content, final Iterable<EmbeddedWrapper> wrappers, final Link... links) {

        super(content, links);
        this.wrappers = new Resources<>(wrappers);
    }


    public static <T> ResourceWithEmbeddable<T> resWrapper(final T content,
                                                           final EmbeddedWrapper... wrappers) {

        return new ResourceWithEmbeddable<>(content, Arrays.asList(wrappers));

    }

    public static EmbeddedWrapper embeddedRes(final Object source, final String rel) {
        return new EmbeddedWrappers(false).wrap(source, rel);
    }
}

You only need to include import static package.ResourceWithEmbeddable.* to your service class to use it.

JSON looks like this:

{
    "myField1": "1field",
    "myField2": "2field",
    "_embedded": {
        "settings": [
            {
                "settingName": "mySetting",
                "value": "1337",
                "description": "umh"
            },
            {
                "settingName": "other",
                "value": "1488",
                "description": "a"
            },...
        ]
    }
}
Caliper answered 24/1, 2018 at 5:26 Comment(3)
Could you please also add the example of usage ResourceWithEmbeddable class?Muscle
@Muscle have a look at this issue, Spring will provide a proper builder github.com/spring-projects/spring-hateoas/issues/864Caliper
I don't use hateoas in my recent projects so don't have this code on my hands and don't wanna spend time when Spring already does something. But I copy pasted it from production code so it should work. The context was Spring Data RESTCaliper
C
0

Spring will provide a builder https://github.com/spring-projects/spring-hateoas/issues/864

Caliper answered 27/3, 2019 at 14:8 Comment(0)
S
0

This is how I've built such json with spring-boot-starter-hateoas 2.1.1:

{
    "total": 2,
    "count": 2,
    "_embedded": {
        "contacts": [
            {
                "id": "1-1CW-303",
                "role": "ASP",
                "_links": {
                    "self": {
                        "href": "http://localhost:8080/accounts/2700098669/contacts/1-1CW-303"
                    }
                }
            },
            {
                "id": "1-1D0-267",
                "role": "HSP",
                "_links": {
                    "self": {
                        "href": "http://localhost:8080/accounts/2700098669/contacts/1-1D0-267"
                    }
                }
            }
        ]
    },
    "_links": {
        "self": {
            "href": "http://localhost:8080/accounts/2700098669/contacts?limit=2&page=1"
        },
        "first": {
            "href": "http://localhost:8080/accounts/2700098669/contacts?limit=2&page=1"
        },
        "last": {
            "href": "http://localhost:8080/accounts/2700098669/contacts?limit=2&page=1"
        }
    }
}

The main class that encapsulates all this fields is

public class ContactsResource extends ResourceSupport{
    private long count;
    private long total;
    private final Resources<Resource<SimpleContact>> contacts;

    public long getTotal() {
        return total;
    }

    public ContactsResource(long total, long count, Resources<Resource<SimpleContact>> contacts){
        this.contacts = contacts;
        this.total = total;
        this.count = count;
    }

    public long getCount() {
        return count;
    }

    @JsonUnwrapped
    public Resources<Resource<SimpleContact>> getContacts() {
        return contacts;
    }
}

SimpleContact has info about single contact and it's just pojo

@Relation(value = "contact", collectionRelation = "contacts")
public class SimpleContact {
    private String id;
    private String role;

    public String getId() {
        return id;
    }

    public SimpleContact id(String id) {
        this.id = id;
        return this;
    }

    public String getRole() {
        return role;
    }

    public SimpleContact role(String role) {
        this.role = role;
        return this;
    }
}

And creating ContactsResource:

public class ContactsResourceConverter {

    public static ContactsResource toResources(Page<SimpleContact> simpleContacts, Long accountId){

        List<Resource<SimpleContact>> embeddeds = simpleContacts.stream().map(contact -> {
            Link self = linkTo(methodOn(AccountController.class).getContactById(accountId, contact.getId())).
                    withSelfRel();
            return new Resource<>(contact, self);
        }
        ).collect(Collectors.toList());

        List<Link> listOfLinks = new ArrayList<>();
        //self link
        Link selfLink = linkTo(methodOn(AccountController.class).getContactsForAccount(
                accountId,
                simpleContacts.getPageable().getPageSize(),
                simpleContacts.getPageable().getPageNumber() + 1)) // +1 because of 0 first index
                .withSelfRel();
        listOfLinks.add(selfLink);

        ... another links           

        Resources<Resource<SimpleContact>> resources = new Resources<>(embeddeds);
        ContactsResource contactsResource = new ContactsResource(simpleContacts.getTotalElements(), simpleContacts.getNumberOfElements(), resources);
        contactsResource.add(listOfLinks);

        return contactsResource;
    }
}

And I'm just calling this in this way from controller:

return new ResponseEntity<>(ContactsResourceConverter.toResources(simpleContacts, accountId), HttpStatus.OK);
Springing answered 29/3, 2019 at 14:19 Comment(0)
J
0

Add this dependency in your pom. Check this link: https://www.baeldung.com/spring-rest-hal

<dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-rest-hal-browser</artifactId>
</dependency>

It will change your response like this.

"_links": {
    "next": {
        "href": "http://localhost:8082/mbill/user/listUser?extra=ok&page=11"
    }
}
Jesselyn answered 23/4, 2019 at 17:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.