Merge Spring data rest links with controller links on same Entity - spring-boot

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

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.

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

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.

Spring RepositoryRestController with excerptProjection

I have defined a #Projection for my Spring Data entity as described here
For the same reasons as described there. When I do GET request, everything is returned as expected. But when I do a POST request, the projection won't work. Following the example provided above, "Address" is shown as a URL under Links and is not exposed the way it is with GET request.
How to get it exposed the same way?
I created a class with #RepositoryRestController where I can catch the POST method. If I simply return the entity, it is without links. If I return it as a resource, the links are there, but "Address" is also a link. If I remove the GET method from my controller, the default behavior is as described above.
UPDATE
My entities are same as described here A, B and SuperClass except I don't have fetch defined in my #ManyToOne
My controller looks like this:
#RepositoryRestController
public class BRepositoryRestController {
private final BRepository bRepository;
public BRepositoryRestController(BRepository bRepository) {
this.bRepository = bRepository;
}
#RequestMapping(method = RequestMethod.POST, value = "/bs")
public
ResponseEntity<?> post(#RequestBody Resource<B> bResource) {
B b= bRepository.save(bResource.getContent());
BProjection result = bRepository.findById(b.getId());
return ResponseEntity.ok(new Resource<>(result));
}
}
And my repository looks like this:
#RepositoryRestResource(excerptProjection = BProjection.class)
public interface BRepository extends BaseRepository<B, Long> {
#EntityGraph(attributePaths = {"a"})
BProjection findById(Long id);
}
And my projection looks like this:
#Projection(types = B.class)
public interface BProjection extends SuperClassProjection {
A getA();
String getSomeData();
String getOtherData();
}
And SuperClassProjection looks like this:
#Projection(types = SuperClass.class)
public interface SuperClassProjection {
Long getId();
}
In the custom #RepositoryRestController POST method you should also return the projection. For example:
#Projection(name = "inlineAddress", types = { Person.class })
public interface InlineAddress {
String getFirstName();
String getLastName();
#Value("#{target.address}")
Address getAddress();
}
public interface PersonRepo extends JpaRepository<Person, Long> {
InlineAddress findById(Long personId);
}
#PostMapping
public ResponseEntity<?> post(...) {
//... posting a person
InlineAddress inlineAddress = bookRepo.findById(person.getId());
return ResponseEntity.ok(new Resource<>(inlineAddress));
}
UPDATE
I've corrected my code above and the code from the question:
#RepositoryRestResource(excerptProjection = BProjection.class)
public interface BRepository extends CrudRepository<B, Long> {
BProjection findById(Long id);
}
#Projection(types = B.class)
public interface BProjection {
#Value("#{target.a}")
A getA();
String getSomeData();
String getOtherData();
}
Then all works fine.
POST request body:
{
"name": "b1",
"someData": "someData1",
"otherData": "otherData",
"a": {
"name": "a1"
}
}
Response body:
{
"a": {
"name": "a1"
},
"someData": "someData1",
"otherData": "otherData",
"_links": {
"self": {
"href": "http://localhost:8080/api/bs/1{?projection}",
"templated": true
}
}
}
See working example

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!

How add a new _link to an entity using spring data rest?

I want to add an extra link to a entity such as:
"_links": {
"self": {
"href": "http://localhost:8080/api/organizaciones"
},
"profile": {
"href": "http://localhost:8080/api/profile/organizaciones"
},
"search": {
"href": "http://localhost:8080/api/organizaciones/search"
},
"disable": {
"href": "http://localhost:8080/api/organizaciones/disable"
}
}
The idea behind this scenario is that I need to expose a soft delete via its own link within Organizacion entity... right now I'm only able to do:
http://localhost:8080/api/organizaciones/search/disable?id=100
in order to perform the soft delete. How can I achieve this the right way? Or is it my only alternative creating a controller?
You just need to add a class extending the ResourceProcessor interface and add it to the spring-context(http://docs.spring.io/spring-data/rest/docs/current/reference/html/#_the_resourceprocessor_interface)
For example
#Bean
public ResourceProcessor<Resource<Person>> personProcessor() {
return new ResourceProcessor<Resource<Person>>() {
#Override
public Resource<Person> process(Resource<Person> resource) {
resource.add(new Link("http://localhost:8080/people", "added-link"));
return resource;
}
};
}
Where the Person entity can be replaced with your Organizacion entity.
I finally figured it out, I did what was mentioned by Alex in a comment.
I have to give credit to the father of spring-data-rest to #olivergieke I checked one of his examples, more precise: restbucks
First created the following component
#Component
#RequiredArgsConstructor
public class OrganizacionResourceProcessor implements ResourceProcessor<Resource<Organizacion>>{
private static final String DISABLE_REL = "deshabilitar";
private static final String ENABLE_REL = "habilitar";
private final #NonNull EntityLinks entityLinks;
#Override
public Resource<Organizacion> process(Resource<Organizacion> resource) {
Organizacion organizacion = resource.getContent();
if(organizacion.isEnabled()){
resource.add(entityLinks.linkForSingleResource(Organizacion.class, organizacion.getId()).slash(DISABLE_REL).withRel(DISABLE_REL));
}
if(organizacion.isDisabled()){
resource.add(entityLinks.linkForSingleResource(Organizacion.class, organizacion.getId()).slash(ENABLE_REL).withRel(ENABLE_REL));
}
return resource;
}
}
Then created the controller to support those two operations...
#RepositoryRestController
#RequestMapping("/organizaciones")
#ExposesResourceFor(Organizacion.class)
#RequiredArgsConstructor
#Slf4j
#Transactional
public class OrganizacionController {
private final #NonNull OrganizacionRepository organizacionRepository;
private final #NonNull EntityLinks entityLinks;
#GetMapping(value="/{id}/habilitar")
public ResponseEntity<?> desactivarOrganizacion(#PathVariable("id") Long id) {
Preconditions.checkNotNull(id);
Organizacion organizacion = organizacionRepository.findOne(id);
if(organizacion == null){
return new ResponseEntity<Void>(HttpStatus.NOT_FOUND);
}
organizacion.setEstado(Estado.DESHABILITADO);
Organizacion pOrg = this.organizacionRepository.save(organizacion);
HttpHeaders header = new HttpHeaders();
header.setLocation(this.entityLinks.linkForSingleResource(Organizacion.class, pOrg.getId()).toUri());//construimos el URL
return new ResponseEntity<Void>(header,HttpStatus.CREATED);
}
#GetMapping(value="/{id}/deshabilitar")
public ResponseEntity<?> activarOrganizacion(#PathVariable("id") Long id){
Preconditions.checkNotNull(id);
Organizacion organizacion = organizacionRepository.findOne(id);
if(organizacion == null){
return new ResponseEntity<Void>(HttpStatus.NOT_FOUND);
}
organizacion.setEstado(Estado.ACTIVO);
Organizacion pOrg = this.organizacionRepository.save(organizacion);
HttpHeaders header = new HttpHeaders();
header.setLocation(this.entityLinks.linkForSingleResource(Organizacion.class, pOrg.getId()).toUri());//construimos el URL
return new ResponseEntity<Void>(header,HttpStatus.CREATED);
}
}
and that was it.
This was originally added to Revision 3 of the question.

Resources