Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 574
xsreality Avatar asked Mar 23 '18 21:03

xsreality


People also ask

What does the @RepositoryRestResource annotation do?

The @RepositoryRestResource annotation is optional and is used to customize the REST endpoint. If we decided to omit it, Spring would automatically create an endpoint at “/websiteUsers” instead of “/users“. That's it! We now have a fully-functional REST API.

Why is Spring Data REST not recommended in real world applications?

It is not recommended in real-world applications as you are exposing your database entities directly as REST Services. While designing RESTful services, the two most important things that we consider are the domain model and the consumers. But, while using Spring Data REST, none of these parameters are considered.

What is used for exposing spring data repositories over REST using Spring Data REST?

Spring Data REST can be used to expose HATEOAS RESTful resources around Spring Data repositories. Without writing a lot of code, we can expose RESTful API around Spring Data Repositories.

What is @RepositoryRestController?

Annotation Type RepositoryRestControllerAnnotation to demarcate Spring MVC controllers provided by Spring Data REST. Allows to easily detect them and exclude them from standard Spring MVC handling.


2 Answers

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];
}
like image 66
Daniel Avatar answered Jan 03 '23 01:01

Daniel


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

like image 34
alegria Avatar answered Jan 03 '23 02:01

alegria