Mapping HAL URI in @RequestBody to a Spring Data Rest managed entity
Asked Answered
E

3

9

I am using Spring Data JPA to manage a JPA entity and Spring Data Rest exports the repository into a REST API.

I am also building a custom controller where I want to take the URI of the entity (HAL link) and automatically map it to entity object via the CrudRepository.

CustomController

@RequestMapping(path = MeLinks.ELEVATE, method = RequestMethod.PUT, consumes = RestMediaTypes.TEXT_URI_LIST_VALUE)
HttpEntity<?> elevate(@RequestBody @Valid CollectionModel<Contact> contact) {
    ...
}

When trying to PUT a contact link like http://localhost:8080/contacts/1 with Content-Type text/uri-list, I am able to access the link using contact.getLinks() but not the Contact object itself.

Is it possible for Spring Data Rest to automatically infer the entity from the URI? I am aware of an in-built UriToEntityConverter but how do I use it?

Edit

Here is an attempt that works but doesn't really solve the problem gracefully.

Below code initializes a UriToEntityConverter and converts the incoming URI to domain object.

@Autowired
private ApplicationContext applicationContext;

@Autowired
private MappingContext<?, ?> mappingContext;

@RequestMapping(path = MeLinks.ELEVATE, method = RequestMethod.PUT, consumes = RestMediaTypes.TEXT_URI_LIST_VALUE)
HttpEntity<?> elevate(@RequestBody @Valid CollectionModel<Contact> uris) {

    URI uri = URI.create(uris.getLinks().get(0).getHref());

    Repositories repositories = new Repositories(applicationContext);

    PersistentEntity<?,?> contactPersistentEntity = repositories.getPersistentEntity(Contact.class);

    UriToEntityConverter uriToEntityConverter = new UriToEntityConverter(
        new PersistentEntities(Collections.singleton(mappingContext)), 
        new DefaultRepositoryInvokerFactory(repositories), 
        repositories);

    Contact t = (Contact) uriToEntityConverter.convert(uri, TypeDescriptor.valueOf(URI.class), TypeDescriptor.valueOf(Contact.class));
}

As you can imagine, fetching the domain object from the Repository would be much easier than doing above. Also this works assuming the URI uses the ID as unique part of the link. In my case I have customized it to use a UUID instead. So default behaviour of UriToEntityConverter would not work.

NOTE: Resources class has been renamed to CollectionModel with HATEOAS first release.

Eb answered 23/3, 2018 at 21:33 Comment(6)
Works for me, but too bad it doesn't work for child entities (e.g. http://localhost:8080/contacts/1/childEntity) I searched around and found nothing so far.Breathe
@Breathe what doesn't work for child entities exactly? Does child entity also have a repository or is it a custom link? In case of custom link, are you using a RepositoryRestController?Eb
In the example link above, it tries to parse childEntity as a Long, as if it were a contact id I suppose. childEntity does have a repository but no custom link.Breathe
Ah I see. This could be because UriToEntityConverter is meant to parse only root URIs. So in your case the URI to parse should be http://localhost:8080/child_entities/1 or similar.Eb
Im trying to use your code, but how do you get @Autowired private MappingContext<?, ?> mappingContext;?Boatbill
Autowiring MappingContext<?, ?> works for meHaphazard
G
1

If you want to keep using the UriToEntityConverter I recommend checking out this part of the Spring Data REST documentation to customize the resource URI to a different property by using the EntityLookupSupport<T> class. The UriToEntityConverter should then return the entity correctly.

If you wish to use the repository directly as you stated consider the following:

Looking at the source code of UriToEntityConverter, it uses the last part of the URI as the identifier of the object. You could do this too. (In this code snippet I have just modified your code from above, assuming that you correctly retrieve the URI object)

private ContactRepository contactRepository;

// You should use constructor injection, as it is preferred over field injection.
@Autowired
public CustomController(ContactRepository contactRepository){
    this.contactRepository = contactRepository;
}


@RequestMapping(path = MeLinks.ELEVATE, method = RequestMethod.PUT, consumes = RestMediaTypes.TEXT_URI_LIST_VALUE)
HttpEntity<?> elevate(@RequestBody @Valid CollectionModel<Contact> uris) {
    URI uri = URI.create(uris.getLinks().get(0).getHref());

    Optional<Contact> optionalContact = contactRepository.findByName(getLastUriPart(uri));

    if (optionalContact.isPresent()){
        return optionalContact.get();
    } else {
        // Error handling
    }
}

private String getLastUriPart(URI uri) {
    String[] uriParts = uri.getPath().split("/");

    if (uriParts.length < 2) {
        // Error handling
    }

    return uriParts[uriParts.length - 1];
}
Guthrie answered 22/1, 2020 at 16:49 Comment(0)
D
1

I've simplified the instantiating of UriToEntityConverter as part of a service with auto-wired dependencies.

This worked for me:

@Service
public class UriToEntityConversionService {

    private UriToEntityConverter converter;

    @Autowired
    public UriToEntityConversionService(PersistentEntities entities, RepositoryInvokerFactory invokerFactory,
                                        Repositories repositories) {
        converter = new UriToEntityConverter(entities, invokerFactory, repositories);
    }

    public <T> T convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType){
        return (T) converter.convert(source, sourceType, targetType);
    }

}

calling it in my custom controller:

@RequestMapping(path = "/processes/{id}/protocols", method = { PUT, POST }, consumes = {TEXT_URI_LIST_VALUE})
    HttpEntity<?> overrideLinkProtocolsDefaultEndpoint(@PathVariable("id") Process process,
                                             @RequestBody Resources<Object> incoming,
                                             PersistentEntityResourceAssembler assembler) throws URISyntaxException {
    // TODO handle both PUT and POST and all links
    URI uri = new URI(incoming.getLinks().get(0).getHref());
    Protocol protocol = uriToEntityConversionService.convert(uri, TypeDescriptor.valueOf(URI.class), TypeDescriptor.valueOf(Protocol.class));

Darkle answered 8/12, 2021 at 15:57 Comment(0)
C
0

You can use (@RequestBody EntityModel) in parameters of @RepositoryRestController

For example

fun createPendingPayment(@RequestBody bookingEntityModel: EntityModel<Booking>): ResponseEntity<PendingPayment>

then you can use links in the requestBody like this

{
    "restaurant": "http://localhost:8080/api/v1/restaurants/1",
    "date":"2022-02-26",
    "time":"11:00",
    "member": "http://localhost:8080/api/v1/members/1",
    "persons": 3,
    "status": "CONFIRMED",
    "payment": "FULL"
}
Central answered 23/2, 2022 at 11:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.