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
Related
Jackson is serializing lists with no type erasure. When a GET method returns a Resources object,Is it possible to override to remove the type from Resources
{
"_embedded": {
"customObjList": [
{
"fld1": "a",
"_links": {
"self": {
"href": "http://..."
}
}
}
]
}
}
expected
{
"_embedded": {
"customObj": [
{
"fld1": "a",
"_links": {
"self": {
"href": "http://..."
}
}
}
]
}
}
Use #Relation on Resource class to change the default collection name
#Relation(collectionRelation = "customObj")
class CustomObjResource{
...
}
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.
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"
}
}
Basically i have the same issue like the member who posted this question
When i request a single user in my Application, i get the response in the HAL-format, like i wish
http://localhost:8080/api/v1/users/25 with GET:
{
"userId": "25",
"firstname": "Beytullah",
"lastname": "Güneyli",
"username": "gueneylb",
"_links": {
"self": {
"href": "http://localhost:8080/api/v1/users/25"
},
"roles": [
{
"href": "http://localhost:8080/api/v1/roles/33"
},
{
"href": "http://localhost:8080/api/v1/roles/34"
}
]
}
}
But, when i request all users , i get the response in non-HAL-Format, like this:
http://localhost:8080/api/v1/users with GET:
[...
{
"userId": "25",
"firstname": "Beytullah",
"lastname": "Güneyli",
"username": "gueneylb",
"links": [
{
"rel": "self",
"href": "http://localhost:8080/api/v1/users/25"
},
{
"rel": "roles",
"href": "http://localhost:8080/api/v1/roles/33"
},
{
"rel": "roles",
"href": "http://localhost:8080/api/v1/roles/34"
}
]
},
...]
Here are my methods:
#RequestMapping(value = "users", method = RequestMethod.GET, produces = MediaTypes.HAL_JSON_VALUE)
public List<UserResource> list() throws MyException, NotFoundException {
List<User> userList= userRepository.findAll();
List<UserResource> resources = new ArrayList<UserResource>();
for (User user : userList) {
resources.add(getUserResource(user));
}
if(userList == null)
throw new MyException("List is empty");
else
return resources;
}
#RequestMapping(value = "users/{id}", method = RequestMethod.GET)
public UserResource get(#PathVariable Long id) throws NotFoundException {
User findOne = userRepository.findOne(id);
if (findOne == null){
log.error("Unexpected error, User with ID " + id + " not found");
throw new NotFoundException("User with ID " + id + " not found");
}
return getUserResource(findOne);
}
private UserResource getUserResource(User user) throws NotFoundException {
resource.add(linkTo(UserController.class).slash("users").slash(user.getId()).withSelfRel());
for(Role role : user.getRoles()){
resource.add(linkTo(RoleController.class).slash("roles").slash(role.getId()).withRel("roles"));
}
return resource;
}
You can see that both methods invoke thegetUserResource(User user) method.
But when i get all users in my database, the format of the _linksis not like i want. I think it must be something about the List of resources i return. Maybe because of that it has no HAL-Format. I also tried a Set instead of List but it gave me the same response
You should return Resources<UserResource> in your list() method.
return new Resources(resources);
You could also add a self-link to the resources itself to point to the list resource.
Furthermore, I would suggest using a RessourceAssembler to create the resource instance - see http://docs.spring.io/spring-hateoas/docs/0.23.0.RELEASE/reference/html/#fundamentals.resource-assembler.
Additionally, you could add paging to your list resource.
For this you need to:
use the findAll method in your repository that returns a Page<User>.
autowire PagedResourcesAssembler<User> in your controller
return PagedResources<UserResource> in your list method
use the PagedResourcesAssembler to convert the Page into PagedResources
This would result in something like this:
private final PagedResourcesAssembler<User> pagedResourcesAssembler;
#RequestMapping(value = "users", method = RequestMethod.GET)
public ResponseEntity<PagedResources<UserResource>> list(Pageable pageable) {
final Page<User> page = repository.findAll(pageable);
final Link link = ControllerLinkBuilder.linkTo(ControllerLinkBuilder.methodOn(this.getClass()).list(null)).withSelfRel();
PagedResources<UserResource> resources = page.getContent().isEmpty() ?
(PagedResources<UserResource>) pagedResourcesAssembler.toEmptyResource(page, ShippingZoneResource.class, link)
: pagedResourcesAssembler.toResource(page, resourceAssembler, link);
return ResponseEntity.ok(resources);
}
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.