Spring Data REST - collectionResourceRel vs path - java

I'm using Spring Data REST. RepositoryRestResource annotation has two different fields: path and collectionResourceRel. What's the difference between those two? I can't figure this out by reading the documentation.
path is described:
The path segment under which this resource is to be exported.
and collectionResourceRel is described:
The rel value to use when generating links to the collection resource.
In all code examples I've seen these two properties where the same. Is there any case when they differ? And what is the actual difference between them?

For example, for entity User the default values will be:
path = users
itemResourceRel = user
collectionResourceRel = users
Example:
GET /users (path: users)
"_links": {
"self": {
"href": "http://localhost:8080/api/users"
},
"users": { <-- collectionResourceRel
"href": "http://localhost:8080/api/users"
}
}
GET /users/1 (path: users)
"_links": {
"self": {
"href": "http://localhost:8080/api/users/1"
},
"user": { <-- itemResourceRel
"href": "http://localhost:8080/api/users/1"
}
}

Related

The spring data rest's behavior looks weird

I have a project with spring data rest that when I run it does not have some expected features. When I go to http://localhost:8080/rest-api/profile, the following links appear, which the repository related to each of them has extended CrudRepository.
{
"_links": {
"self": {
"href": "http://localhost:8080/rest-api/profile"
},
"roles": {
"href": "http://localhost:8080/rest-api/profile/roles"
},
"users": {
"href": "http://localhost:8080/rest-api/profile/users"
},
"offers": {
"href": "http://localhost:8080/rest-api/profile/offers"
},
"businesses": {
"href": "http://localhost:8080/rest-api/profile/businesses"
},
"apps": {
"href": "http://localhost:8080/rest-api/profile/apps"
},
"addresses": {
"href": "http://localhost:8080/rest-api/profile/addresses"
},
"contacts": {
"href": "http://localhost:8080/rest-api/profile/contacts"
}
}
}
Now, when I go to the http://localhost:8080/rest-api/users/search, the following page opens:
{
"_links": {
"getByEmail": {
"href": "http://localhost:8080/rest-api/users/search/getByEmail{?email}",
"templated": true
},
"getByRoleName": {
"href": "http://localhost:8080/rest-api/users/search/getByRoleName{?role}",
"templated": true
},
"existsByEmail": {
"href": "http://localhost:8080/rest-api/users/search/existsByEmail{?email}",
"templated": true
},
"self": {
"href": "http://localhost:8080/rest-api/users/search"
}
}
}
First question:
1-Why does not the spring data rest expose method for CrudRepository's specific methods? like findAll ro findById ?Are they not the search method?
Second question:
When I open http://localhost:8080/rest-api/apps/search it gives a 404 not found error.
Why does it give an error when I go to the search link related to apps? Don't apps have search methods? In addition, when I go to the mentioned link, the following line is logged in the program console:
2022-10-24 11:02:14.990 WARN 7560 --- [nio-8080-exec-8] .m.m.a.ExceptionHandlerExceptionResolver : Resolved [org.springframework.data.rest.webmvc.ResourceNotFoundException: EntityRepresentationModel not found!]
I would be really appreciated if you would answer me.

Configuring #RepositoryRestResource path with multi-segment for nested relationships

I've created 2 HATEOS #RepositoryRestResource for a Customer that has many Address :
class Customer {
...
Long id;
#OneToMany(
Set<Address> addresses;
...
}
class Address {
...
Long id;
...
}
#RepositoryRestResource(path = "customers")
public interface CustomerRepository extends PagingAndSortingRepository<Customer, Long> {
}
#RepositoryRestResource(path = "addresses")
public interface AddressRepository extends PagingAndSortingRepository<Address, Long> {
}
I am able to get an Address entity with the following multi-segment path
Get Address 1 of Customer 1: GET /customers/1/adresses/1
BUT ... I'm not able to PATCH, PUT or DELETE an address using the same multi-segment path
Update Address 1 of Customer 1: PATCH, PUT or DELETE /customers/1/adresses/1
I always get a 404 - Not Found. It only works if I use the address endpoint
Update Address 1 : PATCH, PUT or DELETE /adresses/1
Is there a way to configure the #RepositoryRestResource so that I can update/delete an address (or any entities related to customer via the customer endpoint (/customers/{customerId}/addresses/{addressId}) ?
Regards
The code is available at https://github.com/fdlessard/spring-boot-repository-rest-resource
When you GET /customers/1/addresses/1, the response JSON should be
{
"country": "XYZ",
"_links": {
"self": {
"href": "http://localhost:8888/addresses/1" <-- here
},
"address": {
"href": "http://localhost:8888/addresses/1"
}
}
}
Use the link self, that is, http://localhost:8888/addresses/1 for update.
Actually, when you GET /customers/1/addresses, the array addresses in response JSON already gives you the true URI /addresses/1.
{
"_embedded": {
"addresses": [
{
"country": "XYZ",
"_links": {
"self": {
"href": "http://localhost:8888/addresses/1" <-- here
},
"address": {
"href": "http://localhost:8888/addresses/1"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8888/customers/1/addresses"
}
}
}
Remember, when play with HATEOAS, discover link and follow the href, rather than hardcode the URI. See https://docs.spring.io/spring-hateoas/docs/current/reference/html/#client.link-discoverer

Reading JSON object with jackson

I would like to deserialize the following json object with jackson:
{
"_embedded": {
"endpoints": [
{
"name": "Tester",
"id": "48aba1b3-3585-4327-a20f-627a1749611b",
"componentId": "Darwin2",
"_links": {
"self": {
"href": "www.google.com"
},
"network": {
"href": "www.google.com"
},
"appWans": {
"href": "www.google.com"
},
"services": {
"href": "www.google.com"
},
"endpointGroups": {
"href": "www.google.com"
},
"geoRegion": {
"href": "www.google.com"
},
"dataCenter": {
"href": "www.google.com"
}
}
}
]
},
"_links": {
"first": {
"href": "www.google.com"
},
"last": {
"href": "www.google.com"
}
},
"page": {
"size": 2000,
"totalElements": 1,
"totalPages": 1,
"number": 1
}
}
My goal is to implement an Embedded object then within this object add another object called Endpoints. Ideally, I'd be able to access the id property off of the endpoints object. However, I keep getting deserialization errors. For the moment I am using this class:
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
#lombok.Value
public class Endpoints {
#JsonProperty("_embedded")
private Map<String, Object> embedded = new HashMap<>();
}
This at least affords me the opportunity to do the following:
Endpoints result = apiRequest.get();
if (result != null) System.out.println(result.getEmbedded().get("endpoints"));
Which prints out the array of endpoints, but I can't use this. I must implement a java object. Any help would be greatly appreciated with this issue.
So I have no idea what frameworks you use, or where this JSON data comes from, but you mentioned Spring HATEOS, so here is a solution using Jackson and Spring HATEOAS:
#Data
public class Endpoint extends RepresentationModel<Endpoint> {
private String name;
private String id; // Could be a UUID type instead??
private String componentId;
}
#Service
public class MyService {
#Autowired
private ObjectMapper objectMapper;
public void foo() {
String mysteryString = apiRequest.get();
PagedModel<Endpoint> endpointsPage = objectMapper.readValue(mysteryString, new TypeReference<PagedModel<Endpoint>>);
for (Endpoint e : endpointsPage) {
System.out.println(e.getName());
}
}
}
Spring HATEOAS docs and a guide. Also look at the Javadoc of the classes I've used.
If you want Endpoint to be a Lombok Value (i.e., all final), you need a constructor with appropriate ´#JsonCreator´ and ´#JsonProperty´ annotations, so Jackson knows how to build your Object (see 'guide' link).
And some more reading on Jackson.
Then again, if you are using Spring and you could just use Spring RestTemplate to fetch the Data from the remote API, you don't even need to manually use Jackson ObjectMapper:
PagedModel<Endpoint> endpointsPage =
restTemplate.exchange(apiUrl,
HttpMethod.GET,
null, // RequestEntity (body of request)
new ParameterizedTypeReference<PagedModel<Endpoint>>() {}).getBody();
The whole TypeReference and ParameterizedTypeReference business is only needed bc. we are dealing with generics.

Spring REST response is different in a custom controller

I have several controllers that are automatically creating REST endpoints.
#RepositoryRestResource(collectionResourceRel = "books", path = "books")
public interface BooksRepository extends CrudRepository<Books, Integer> {
public Page<Books> findTopByNameOrderByFilenameDesc(String name);
}
When I visit: http://localhost:8080/Books
I get back:
{
"_embedded": {
"Books": [{
"id": ,
"filename": "Test123",
"name": "test123",
"_links": {
"self": {
"href": "http://localhost:8080/books/123"
},
"Books": {
"href": "http://localhost:8080/books/123"
}
}
}]
},
"_links": {
"self": {
"href": "http://localhost:8080/books"
},
"profile": {
"href": "http://localhost:8080/profile/books"
},
"search": {
"href": "http://localhost:8080/books/search"
},
"page": {
"size": 20,
"totalElements": 81,
"totalPages": 5,
"number": 0
}
}
}
When I create my own controller:
#Controller
#RequestMapping(value = "/CustomBooks")
public class CustomBooksController {
#Autowired
public CustomBookService customBookService;
#RequestMapping("/search")
#ResponseBody
public Page<Book> search(#RequestParam(value = "q", required = false) String query,
#PageableDefault(page = 0, size = 20) Pageable pageable) {
return customBookService.findAll();
}
}
I'll get a response back that looks nothing like the automatically generated controller response:
{
"content": [{
"filename": "Test123",
"name" : "test123"
}],
"totalPages": 5,
"totalElements": 81,
"size": 20,
"number": 0,
}
What do I need to do to make my response look like the automatically generated response? I want to keep it consistent, so I don't have to rewrite code for a different response. Should I be doing it a different way?
Edit: Found this: Enable HAL serialization in Spring Boot for custom controller method
But I don't understand what I need to change in my REST Controller to enable: PersistentEntityResourceAssembler. I've searched on Google for PersistentEntityResourceAssembler, but it keeps leading me back to similar pages without much of an example (or the example doesn't seem to work for me).
As #chrylis suggested you should replace your #Controller annotation with #RepositoryRestController for spring-data-rest to invoke it's ResourceProcessors for customizing the given resource.
For you resource to follow the HATEOAS specification (like your spring-data-rest BooksRepository) your method declaration return type should be like HttpEntity<PagedResources<Resource<Books>>>
For converting your Page object to PagedResources:
You need to autowire this object.
#Autowired
private PagedResourcesAssembler<Books> bookAssembler;
Your return statement should be like
return new ResponseEntity<>(bookAssembler.toResource(customBookService.findAll()), HttpStatus.OK);
These changes should help you to get a org.springframework.hateoas.Resources compliant response containing the "_embedded" and "_links" attribute.

Parsing _embedded items from JSON HAL response

We are trying to parse the below JSON to get a list of people.
JSON Response:
{
"_embedded": {
"people": [
{
"id": 35356,
"name": "Jon",
"description": "Test",
"type": "person",
"_links": {
"self": {
"href": "http://localhost/api/v1/50452/people/35356"
},
"items": {
"href": "http://localhost/api/v1/50452/items?person_id=35356"
},
"enabled_services": [
{
"title": "Water Company",
"href": "http://localhost/api/v1/50452/services/103890"
}
]
}
},
{
"id": 46363,
"name": "Kevin",
"description": "",
"type": "person",
"_links": {
"self": {
"href": "http://localhost/api/v1/50452/people/46363"
},
"items": {
"href": "http://localhost/api/v1/50452/items?person_id=46363"
},
"enabled_services": [
{
"title": "Water Company",
"href": "http://localhost/api/v1/50452/services/103890"
}
]
}
}
]
},
"_links": {
"self": {
"href": "http://localhost/api/v1/50452/people"
}
}
}
Our code:
ParameterizedTypeReference<Resources<Person>> resource = new ParameterizedTypeReference<Resources<Person>>() {};
Traverson traverson = new Traverson(new URI("http://localhost/api/v1/people"), MediaType.APPLICATION_JSON_UTF8);
// Create our own LinkDiscoverer as our service returns application/json instead of application/json+hal
List<LinkDiscoverer> linkDiscoverers = new ArrayList<>();
linkDiscoverers.add(new JsonPathLinkDiscoverer("$._links..['%s']..href", MediaType.APPLICATION_JSON_UTF8));
traverson.setLinkDiscoverers(linkDiscoverers);
HttpHeaders headers = new HttpHeaders();
headers.add("App-Key", Globals.Appkey);
headers.add("App-Id", Globals.AppId);
Resources<Person> personResources = traverson.follow("people").withHeaders(headers).toObject(resource);
However we are getting the following error:
java.lang.IllegalStateException: Expected to find link with rel 'people' in response {"_embedded":{"people":[{"id":31350,"name":"Jon Blue","description":"Developer","type":"person","deleted":false,"disabled":false,"company_id":50452,"order":31350,"phone_prefix":"44","_links":{"self":{"href":"http://localhost/api/v1/50452/people/31350"},"items":{"href":"http://localhost/api/v1/50452/items?person_id=31350"},"enabled_services":[{"title":"Water ...
Based on the (very limited) client docs this seems to be the correct way to do things. Does anyone know what we might be missing here?
Thanks
The Traverson is meant to be used to follow links:
Component to ease traversing hypermedia APIs by following links with
relation types. Highly inspired by the equally named JavaScript
library.
There is no link with rel people in your response, so it cannot be followed.
I could imagine you wanted to call the top-level resource that has a link to people:
Traverson traverson = new Traverson(new URI("http://localhost/api/v1/"), MediaType.APPLICATION_JSON_UTF8);
Otherwise I would suggest to use a RestTemplate to get the Resource:
restTemplate.exchange(
URI.create("http://localhost/api/v1/people"),
HttpMethod.GET,
new HttpEntity<Void>(headers),
resource).getBody();

Categories