Expose custom endpoint to base-path in Spring Boot Data Rest - spring-boot

I am using Spring Boot Data Rest and I can list all endpoints using follling url:
http://localhost:8080/api
It lists following endpoints:
{
"_links": {
"tacos": {
"href": "http://localhost:8080/api/tacos{?page,size,sort}",
"templated": true
},
"orders": {
"href": "http://localhost:8080/api/orders"
},
"ingredients": {
"href": "http://localhost:8080/api/ingredients"
},
"profile": {
"href": "http://localhost:8080/api/profile"
}
}
}
But I have a custom end point created like below in Controller
#RepositoryRestController
public class RecentTacosController {
private TacoRepository tacoRepo;
public RecentTacosController(TacoRepository tacoRepo) {
this.tacoRepo = tacoRepo;
}
#GetMapping(path = "/tacos/recent", produces = "application/hal+json")
public ResponseEntity<Resources<TacoResource>> recentTacos() {
PageRequest page = PageRequest.of(0, 12, Sort.by("createdAt").descending());
List<Taco> tacos = tacoRepo.findAll(page).getContent();
List<TacoResource> tacoResources = new TacoResourceAssembler().toResources(tacos);
Resources<TacoResource> recentResources = new Resources<TacoResource>(tacoResources);
recentResources.add(ControllerLinkBuilder.linkTo(ControllerLinkBuilder.methodOn(RecentTacosController.class).recentTacos()).withRel("recents"));
return new ResponseEntity<>(recentResources, HttpStatus.OK);
}
}
But this endpoint (http://localhost:8080/api/tacos/recent) is not listed when doing GET on base path.

implement ResourceProcessor<RepositoryLinksResource> in one of your class (could be the controller itself), add the following method:
#Override
public RepositoryLinksResource process(RepositoryLinksResource resource) {
resource.add( ... );
return resource;
}
... and create and add the proper link to your custom method there.

Related

Spring HATEOAS RepresentationModelAssembler to generate links with Pageable parameter

I have a MemberController that has two GetMappings, one returns a paginated list of members and the other returns a member. I have a MemberModelAssembler which overrides toModel and returns a selfRel() link. How to make the toModel method in the MemberModelAssembler return a the pagination link for each member? Given I cannot pass Pageable and PagedResourcesAssembler to the MemberModelAssembler?
Expected result when calling api/v1/member/1
{
"id": 1,
"phone": "85298890006",
"profileImageUrl": null,
"displayedName": "Mak",
"salutation": "MS",
"_links": {
"self": {
"href": "http://localhost:8080/api/v1/member/1"
}
*****Want to achieve this*****
"members": {
"href": "http://localhost:8080/api/v1/memberpage=0&size=20"
*****Want to achieve this*****
}
}
My MemberController:
#RestController
#RequestMapping("api/v1/member")
class MemberController(
private val service: MemberService,
private val assembler: MemberModelAssembler
) {
#GetMapping
fun findAll(
pageable: Pageable,
pagedResourcesAssembler: PagedResourcesAssembler<Member>
): ResponseEntity<PagedModel<EntityModel<Member>>> {
val members = service.findAll(pageable)
return ResponseEntity(pagedResourcesAssembler.toModel(members, assembler), HttpStatus.OK)
}
#GetMapping("/{id}")
fun findById(#PathVariable id: Int): ResponseEntity<EntityModel<Member>> {
val member = service.findById(id) ?: throw ItemNotFoundException(this::class.simpleName!!, id)
return ResponseEntity(assembler.toModel(member), HttpStatus.OK)
}
}
My MemberModelAssembler
#Component
class MemberModelAssembler : RepresentationModelAssembler<Member, EntityModel<Member>> {
override fun toModel(member: Member) =
EntityModel.of(
member,
linkTo(methodOn(MemberController::class.java).findById(member.id)).withSelfRel(),
)
}
For anyone who still has a problem with that:
Although it's a little counteractive, you can enter a 3rd parameter as the initial link.

Entry creation returns a 201 but accessing it returns 404

I have a simple Spring Data Rest implementation of user creation using Hibernate and MongoDB.
User.java:
#Data
#Entity
#RequiredArgsConstructor
#JsonIgnoreProperties(ignoreUnknown = true)
public class User {
private #Id String username;
private String about;
}
UserRepository.java
#PreAuthorize("hasRole('ROLE_USER')")
#CrossOrigin(methods = {GET, PUT, POST})
public interface UserRepository extends MongoRepository<User, String> {
#Override
#PreAuthorize("hasRole('ROLE_ADMIN')")
<S extends User> S save(S s);
}
Then I make a POST call to /users with this body:
{
"username": "username1",
"about": "example"
}
I get a 201 Created response with the following body:
{
"about": "example",
"_links": {
"self": {
"href": "http://localhost:8080/api/users/username1"
},
"user": {
"href": "http://localhost:8080/api/users/username1"
}
}
}
I make a GET request to /users to see if the user was indeed added and this response is returned rightfully so:
{
"_embedded": {
"users": [
{
"about": "example",
"_links": {
"self": {
"href": "http://localhost:8080/api/users/username1"
},
"user": {
"href": "http://localhost:8080/api/users/username1"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/api/users{?page,size,sort}",
"templated": true
},
"profile": {
"href": "http://localhost:8080/api/profile/users"
}
},
"page": {
"size": 20,
"totalElements": 1,
"totalPages": 1,
"number": 0
}
}
THE PROBLEM
But then I access the URL of the user provided in the links, i.e., http://localhost:8080/api/users/username1 but I get a 404 Not Found response.
What am I doing wrong? I've tried looking through examples and documentation but nothing seems to do the work. If I add the #AutoGenerated annotation it works, but I obviously want the id to be provided by the request as the username.
In User.java, I changed the declaration of username to this:
#Id
#JsonProperty("username")
#NotBlank
private String id;
MongoDb requires _id as the primary key for every document by default otherwise it won't get indexed. I had to change the field name to id so that it translates to _id on MongoDB with the #Id annotation. To fix this on the consumer side, I used #JsonProperty("username") to get the value from the username property of the request's JSON body.

How to consume _embedded resources with Spring HATEOAS

I am trying to consume the following REST HAL response from a 3rd party service:
{
"id": 51780,
"name": "Lambeth",
"description": "",
"address_id": 54225,
"website": "",
"numeric_widget_id": 3602008,
"currency_code": "GBP",
"timezone": "Europe/London",
"country_code": "gb",
"live": true,
"_embedded": {
"settings": {
"has_services": true,
"has_classes": true,
"payment_tax": 0,
"currency": "GBP",
"requires_login": false,
"has_wallets": false,
"ask_address": true,
"_links": {
"self": {
"href": "https://myhost.com/api/v1/51780/settings"
}
}
}
},
"_links": {
"self": {
"href": "https://myhost.com/api/v1/company/51780"
},
"settings": {
"href": "https://myhost.com/api/v1/51780/settings"
}
}
}
Which I would like to map to a class like this:
public class Company extends ResourceSupport {
private String name;
private CompanySettings settings;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public CompanySettings getSettings() {
return settings;
}
public void setSettings(CompanySettings settings) {
this.settings = settings;
}
}
And a class for the embedded item like this:
public class CompanySettings extends ResourceSupport {
private String currency;
public String getCurrency() {
return currency;
}
public void setCurrency(String currency) {
this.currency = currency;
}
}
However I am having no luck getting the embedded item to map to the nested settings object. My code is below.
RestTemplate restTemplate = new RestTemplate();
HttpEntity<String> entity = new HttpEntity<String>("parameters", headers);
ResponseEntity<Resource<Company>> responseEntity = restTemplate.exchange("https://uk.bookingbug.com/api/v1/company/51780",
HttpMethod.GET, null, new ParameterizedTypeReference<Resource<Company>>() {
}, Collections.emptyMap());
if (responseEntity.getStatusCode() == HttpStatus.OK) {
Resource<Company> userResource = responseEntity.getBody();
Company company = userResource.getContent();
}
Any help would be greatly appreciated.

Adding more information to the HATEOAS response in Spring Boot Data Rest

I have the following REST controller.
#RepositoryRestController
#RequestMapping(value = "/booksCustom")
public class BooksController extends ResourceSupport {
#Autowired
public BooksService booksService;
#Autowired
private PagedResourcesAssembler<Books> booksAssembler;
#RequestMapping("/search")
public HttpEntity<PagedResources<Resource<Books>>> search(#RequestParam(value = "q", required = false) String query, #PageableDefault(page = 0, size = 20) Pageable pageable) {
pageable = new PageRequest(0, 20);
Page<Books> booksResult = BooksService.findBookText(query, pageable);
return new ResponseEntity<PagedResources<Resource<Books>>>(BooksAssembler.toResource(BooksResult), HttpStatus.OK);
}
My Page<Books> BooksResult = BooksService.findBookText(query, pageable); is backed by SolrCrudRepository. When it is run BookResult has several fields in it, the content field and several other fields, one being highlighted. Unfortunately the only thing I get back from the REST response is the data in the content field and the metadata information in the HATEOAS response (e.g. page information, links, etc.). What would be the proper way of adding the highlighted field to the response? I'm assuming I would need to modify the ResponseEntity, but unsure of the proper way.
Edit:
Model:
#SolrDocument(solrCoreName = "Books_Core")
public class Books {
#Field
private String id;
#Field
private String filename;
#Field("full_text")
private String fullText;
//Getters and setters omitted
...
}
When a search and the SolrRepository is called (e.g. BooksService.findBookText(query, pageable);) I get back these objects.
However, in my REST response I only see the "content". I would like to be able to add the "highlighted" object to the REST response. It just appears that HATEOAS is only sending the information in the "content" object (see below for the object).
{
"_embedded" : {
"solrBooks" : [ {
"filename" : "ABookName",
"fullText" : "ABook Text"
} ]
},
"_links" : {
"first" : {
"href" : "http://localhost:8080/booksCustom/search?q=ABook&page=0&size=20"
},
"self" : {
"href" : "http://localhost:8080/booksCustom/search?q=ABook"
},
"next" : {
"href" : "http://localhost:8080/booksCustom/search?q=ABook&page=0&size=20"
},
"last" : {
"href" : "http://localhost:8080/booksCustom/search?q=ABook&page=0&size=20"
}
},
"page" : {
"size" : 1,
"totalElements" : 1,
"totalPages" : 1,
"number" : 0
}
}
Just so you can get a full picture, this is the repository that is backing the BooksService. All the service does is call this SolrCrudRepository method.
public interface SolrBooksRepository extends SolrCrudRepository<Books, String> {
#Highlight(prefix = "<highlight>", postfix = "</highlight>", fragsize = 20, snipplets = 3)
HighlightPage<SolrTestDocuments> findBookText(#Param("fullText") String fullText, Pageable pageable);
}
Ok, here is how I did it:
I wrote mine HighlightPagedResources
public class HighlightPagedResources<R,T> extends PagedResources<R> {
private List<HighlightEntry<T>> phrases;
public HighlightPagedResources(Collection<R> content, PageMetadata metadata, List<HighlightEntry<T>> highlightPhrases, Link... links) {
super(content, metadata, links);
this.phrases = highlightPhrases;
}
#JsonProperty("highlighting")
public List<HighlightEntry<T>> getHighlightedPhrases() {
return phrases;
}
}
and HighlightPagedResourcesAssembler:
public class HighlightPagedResourcesAssembler<T> extends PagedResourcesAssembler<T> {
public HighlightPagedResourcesAssembler(HateoasPageableHandlerMethodArgumentResolver resolver, UriComponents baseUri) {
super(resolver, baseUri);
}
public <R extends ResourceSupport> HighlightPagedResources<R,T> toResource(HighlightPage<T> page, ResourceAssembler<T, R> assembler) {
final PagedResources<R> rs = super.toResource(page, assembler);
final Link[] links = new Link[rs.getLinks().size()];
return new HighlightPagedResources<R, T>(rs.getContent(), rs.getMetadata(), page.getHighlighted(), rs.getLinks().toArray(links));
}
}
I had to add to my spring RepositoryRestMvcConfiguration.java:
#Primary
#Bean
public HighlightPagedResourcesAssembler solrPagedResourcesAssembler() {
return new HighlightPagedResourcesAssembler<Object>(pageableResolver(), null);
}
In cotroller I had to change PagedResourcesAssembler for newly implemented one and also use new HighlightPagedResources in request method:
#Autowired
private HighlightPagedResourcesAssembler<Object> highlightPagedResourcesAssembler;
#RequestMapping(value = "/conversations/search", method = POST)
public HighlightPagedResources<PersistentEntityResource, Object> findAll(
#RequestBody ConversationSearch search,
#SortDefault(sort = FIELD_LATEST_SEGMENT_START_DATE_TIME, direction = DESC) Pageable pageable,
PersistentEntityResourceAssembler assembler) {
HighlightPage page = conversationRepository.findByConversationSearch(search, pageable);
return highlightPagedResourcesAssembler.toResource(page, assembler);
}
RESULT:
{
"_embedded": {
"conversations": [
..our stuff..
]
},
"_links": {
...as you know them...
},
"page": {
"size": 1,
"totalElements": 25,
"totalPages": 25,
"number": 0
},
"highlighting": [
{
"entity": {
"conversationId": "a2127d01-747e-4312-b230-01c63dacac5a",
...
},
"highlights": [
{
"field": {
"name": "textBody"
},
"snipplets": [
"Additional XXX License for YYY Servers DCL-2016-PO0422 \n  \n<em>hi</em> bodgan \n  \nwe urgently need the",
"Additional XXX License for YYY Servers DCL-2016-PO0422\n \n<em>hi</em> bodgan\n \nwe urgently need the permanent"
]
}
]
}
]
}
I was using Page<Books> instead of HighlightPage to create the response page. Page obviously doesn't contain content which was causing the highlighted portion to be truncated. I ended up creating a new page based off of HighlightPage and returning that as my result instead of Page.
#RepositoryRestController
#RequestMapping(value = "/booksCustom")
public class BooksController extends ResourceSupport {
#Autowired
public BooksService booksService;
#Autowired
private PagedResourcesAssembler<Books> booksAssembler;
#RequestMapping("/search")
public HttpEntity<PagedResources<Resource<HighlightPage>>> search(#RequestParam(value = "q", required = false) String query, #PageableDefault(page = 0, size = 20) Pageable pageable) {
HighlightPage solrBookResult = booksService.findBookText(query, pageable);
Page<Books> highlightedPages = new PageImpl(solrBookResult.getHighlighted(), pageable, solrBookResult.getTotalElements());
return new ResponseEntity<PagedResources<Resource<HighlightPage>>>(booksAssembler.toResource(highlightedPages), HttpStatus.OK);
}
Probably a better way of doing this, but I couldn't find anything that would do what I wanted it to do without having a change a ton of code. Hope this helps!

Merge Spring data rest links with controller links on same Entity

I would like to combine HATEAOS links to methods on both Controller and Repository.
#RepositoryRestController
#ResponseBody
#ExposesResourceFor(Group.class)
#RequestMapping(value = "/api/v2/groups", produces = MediaTypes.HAL_JSON_VALUE)
public class GroupController {
#Resource
private GroupService groupService;
#RequestMapping(value = "/external", method = POST)
public #ResponseBody PersistentEntityResource saveExternalGroup(
#RequestBody Group newGroup,
PersistentEntityResourceAssembler assembler) {
return assembler.toResource(groupService.saveExternalGroup(newGroup));
}
}
Repository:
#RepositoryRestResource(excerptProjection = GroupSummary.class)
public interface GroupDao extends DefaultDao<Group, Long> {
#NotNull
List<Group> findByState(#Nullable GroupState state);
...other methods...
I would like to achieve to have possibility to go to /api/v2/groups and have there also link to /external. Currently, only links from repository are returned:
"_links": {
"first": {
"href": "http://localhost:8300/api/v2/groups?page=0&size=20"
},
"self": {
"href": "http://localhost:8300/api/v2/groups"
},
"next": {
"href": "http://localhost:8300/api/v2/groups?page=1&size=20"
},
"last": {
"href": "http://localhost:8300/api/v2/groups?page=1&size=20"
},
"profile": {
"href": "http://localhost:8300/api/v2/profile/groups"
},
"search": {
"href": "http://localhost:8300/api/v2/groups/search"
}
},
What should I implement to get there all as above plus something like this:
"external": {
"href": "http://localhost:8300/api/v2/groups/external"
}
Or is there problem with that "/external" is POST? If so, please comment and consider this question with "method=GET".
Option 1:
If it's a one-off, you could add the links in the controller method using the Resource class.
#RequestMapping(value = "/external", method = POST)
public #ResponseBody PersistentEntityResource saveExternalGroup(
#RequestBody Group newGroup,
PersistentEntityResourceAssembler assembler) {
PersistentEntityResource resource = assembler.toResource(groupService.saveExternalGroup(newGroup));
// Replace with ControllerLinkBuilder call, or EntityLinks as you see fit.
resource.add(new Link("http://localhost:8300/api/v2/groups/external","search"));
return resource;
}
Option 2:
If you would like this link to be added to every rendered Resource<Group>, then create a ResourceProcessor component to add it.
#Component
public class GroupResourceProcessor implements ResourceProcessor<Resource<Group>> {
#Override
public Resource<Group> process(Resource<Group> groupResource) {
// Replace with ControllerLinkBuilder call, or EntityLinks as you see fit.
groupResource.add(new Link("http://localhost:8300/api/v2/groups/external","search"));
return groupResource;
}
}

Resources