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.
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.
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.
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.
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.
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];
}
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));
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With