Spring HATEOAS embedded resource support

JavaSpringSpring Hateoas

Java Problem Overview


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?

Java Solutions


Solution 1 - Java

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" }
                    }
                }]
            }
        }
    }
}

Solution 2 - Java

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"
   }
  ]
 }
}

Solution 3 - Java

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

Solution 4 - Java

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;
    }
}

Solution 5 - Java

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"
            },...
        ]
    }
}

Solution 6 - Java

Solution 7 - Java

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);

Solution 8 - Java

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"
    }
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionGlideView Question on Stackoverflow
Solution 1 - JavalinquView Answer on Stackoverflow
Solution 2 - JavaChris DaMourView Answer on Stackoverflow
Solution 3 - JavaDmitriy MayborodaView Answer on Stackoverflow
Solution 4 - JavaPeter SzantoView Answer on Stackoverflow
Solution 5 - JavaSamView Answer on Stackoverflow
Solution 6 - JavaSamView Answer on Stackoverflow
Solution 7 - JavaEugeneView Answer on Stackoverflow
Solution 8 - Javaaditya lathView Answer on Stackoverflow