Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring HATEOAS embedded resource support

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?

like image 757
Glide Avatar asked Sep 15 '14 23:09

Glide


People also ask

What is Hal in HATEOAS?

HAL is a simple format that gives a consistent and easy way to hyperlink between resources in your API. The HAL format is strictly coupled to HATEOAS. The main target of HATEOAS is to decouple the API Consumer from the paths used in the API.

What is the use of spring HATEOAS?

Spring HATEOAS provides some APIs to ease creating REST representations that follow the HATEOAS principle when working with Spring and especially Spring MVC. The core problem it tries to address is link creation and representation assembly.

In which Spring Library is HATEOAS implemented?

Spring-HATEOAS is the library of APIs. We can use these APIs for creating REST representations that follow the HATEOAS principle while working with Spring MVC.


5 Answers

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"
   }
  ]
 }
}
like image 21
Chris DaMour Avatar answered Oct 26 '22 09:10

Chris DaMour


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" }
                    }
                }]
            }
        }
    }
}
like image 99
linqu Avatar answered Oct 26 '22 10:10

linqu


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

like image 27
Dmitriy Mayboroda Avatar answered Oct 26 '22 10:10

Dmitriy Mayboroda


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;
    }
}
like image 5
Peter Szanto Avatar answered Oct 26 '22 09:10

Peter Szanto


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"
            },...
        ]
    }
}
like image 3
Sam Avatar answered Oct 26 '22 08:10

Sam