I have a project based on spring-data-rest and also it has some custom endpoints.
For sending POST data I'm using json like
{
"action": "REMOVE",
"customer": "http://localhost:8080/api/rest/customers/7"
}
That is fine for spring-data-rest, but does not work with a custom controller.
for example:
public class Action {
public ActionType action;
public Customer customer;
}
@RestController
public class ActionController(){
@Autowired
private ActionService actionService;
@RestController
public class ActionController {
@Autowired
private ActionService actionService;
@RequestMapping(value = "/customer/action", method = RequestMethod.POST)
public ResponseEntity<ActionResult> doAction(@RequestBody Action action){
ActionType actionType = action.action;
Customer customer = action.customer;//<------There is a problem
ActionResult result = actionService.doCustomerAction(actionType, customer);
return ResponseEntity.ok(result);
}
}
When I call
curl -v -X POST -H "Content-Type: application/json" -d '{"action": "REMOVE","customer": "http://localhost:8080/api/rest/customers/7"}' http://localhost:8080/customer/action
I have an answer
{
"timestamp" : "2016-05-12T11:55:41.237+0000",
"status" : 400,
"error" : "Bad Request",
"exception" : "org.springframework.http.converter.HttpMessageNotReadableException",
"message" : "Could not read document: Can not instantiate value of type [simple type, class model.user.Customer] from String value ('http://localhost:8080/api/rest/customers/7'); no single-String constructor/factory method\n at [Source: java.io.PushbackInputStream@73af10c6; line: 1, column: 33] (through reference chain: api.controller.Action[\"customer\"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Can not instantiate value of type [simple type, class logic.model.user.Customer] from String value ('http://localhost:8080/api/rest/customers/7'); no single-String constructor/factory method\n at [Source: java.io.PushbackInputStream@73af10c6; line: 1, column: 33] (through reference chain: api.controller.Action[\"customer\"])",
"path" : "/customer/action"
* Closing connection 0
}
bacause case spring can not convert a URI to a Customer entity.
Is there any way to use spring-data-rest mechanism for resolving entities by their URIs?
I have only one idea - to use custom JsonDeserializer with parsing URI for extracting entityId and making a request to a repository. But this strategy does not help me if I have URI like "http://localhost:8080/api/rest/customers/8/product" in that case I do not have product.Id value.
I have been having the same problem too for really long time now and solved it the following way. @Florian was on the right track and thanks to his suggestion I found a way to make the conversion work automatically. There are several pieces needed:
For point 1 the implementation can be narrowed to the following
import org.springframework.context.ApplicationContext;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.repository.support.DomainClassConverter;
import org.springframework.data.rest.core.UriToEntityConverter;
import org.springframework.format.support.DefaultFormattingConversionService;
public class UriToEntityConversionService extends DefaultFormattingConversionService {
private UriToEntityConverter converter;
public UriToEntityConversionService(ApplicationContext applicationContext, PersistentEntities entities) {
new DomainClassConverter<>(this).setApplicationContext(applicationContext);
converter = new UriToEntityConverter(entities, this);
addConverter(converter);
}
public UriToEntityConverter getConverter() {
return converter;
}
}
For point 2 this is my solution
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.ValueInstantiator;
import com.fasterxml.jackson.databind.deser.std.StdValueInstantiator;
import your.domain.RootEntity; // <-- replace this with the import of the root class (or marker interface) of your domain
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.rest.core.UriToEntityConverter;
import org.springframework.util.Assert;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Optional;
public class RootEntityFromUriDeserializer extends BeanDeserializerModifier {
private final UriToEntityConverter converter;
private final PersistentEntities repositories;
public RootEntityFromUriDeserializer(PersistentEntities repositories, UriToEntityConverter converter) {
Assert.notNull(repositories, "Repositories must not be null!");
Assert.notNull(converter, "UriToEntityConverter must not be null!");
this.repositories = repositories;
this.converter = converter;
}
@Override
public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanDescription beanDesc, BeanDeserializerBuilder builder) {
PersistentEntity<?, ?> entity = repositories.getPersistentEntity(beanDesc.getBeanClass());
boolean deserializingARootEntity = entity != null && RootEntity.class.isAssignableFrom(entity.getType());
if (deserializingARootEntity) {
replaceValueInstantiator(builder, entity);
}
return builder;
}
private void replaceValueInstantiator(BeanDeserializerBuilder builder, PersistentEntity<?, ?> entity) {
ValueInstantiator currentValueInstantiator = builder.getValueInstantiator();
if (currentValueInstantiator instanceof StdValueInstantiator) {
EntityFromUriInstantiator entityFromUriInstantiator =
new EntityFromUriInstantiator((StdValueInstantiator) currentValueInstantiator, entity.getType(), converter);
builder.setValueInstantiator(entityFromUriInstantiator);
}
}
private class EntityFromUriInstantiator extends StdValueInstantiator {
private final Class entityType;
private final UriToEntityConverter converter;
private EntityFromUriInstantiator(StdValueInstantiator src, Class entityType, UriToEntityConverter converter) {
super(src);
this.entityType = entityType;
this.converter = converter;
}
@Override
public Object createFromString(DeserializationContext ctxt, String value) throws IOException {
URI uri;
try {
uri = new URI(value);
} catch (URISyntaxException e) {
return super.createFromString(ctxt, value);
}
return converter.convert(uri, TypeDescriptor.valueOf(URI.class), TypeDescriptor.valueOf(entityType));
}
}
}
Then for point 3, in the custom RepositoryRestConfigurerAdapter,
public class MyRepositoryRestConfigurer extends RepositoryRestConfigurerAdapter {
@Override
public void configureJacksonObjectMapper(ObjectMapper objectMapper) {
objectMapper.registerModule(new SimpleModule("URIDeserializationModule"){
@Override
public void setupModule(SetupContext context) {
UriToEntityConverter converter = conversionService.getConverter();
RootEntityFromUriDeserializer rootEntityFromUriDeserializer = new RootEntityFromUriDeserializer(persistentEntities, converter);
context.addBeanDeserializerModifier(rootEntityFromUriDeserializer);
}
});
}
}
This works smoothly for me and does not interfere with any conversion from the framework (we have many custom endpoints). In the point 2 the intent was to enable the instantiation from a URI only in cases where:
This is more of an side note instead of a real answer, but a while ago I managed to copy&paste myself a class to resolve entities from an URL by using the methods used in SDR (just more crude). There probably is a much better way, but until then, perhaps this helps...
@Service
public class EntityConverter {
@Autowired
private MappingContext<?, ?> mappingContext;
@Autowired
private ApplicationContext applicationContext;
@Autowired(required = false)
private List<RepositoryRestConfigurer> configurers = Collections.emptyList();
public <T> T convert(Link link, Class<T> target) {
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
PersistentEntities entities = new PersistentEntities(Arrays.asList(mappingContext));
UriToEntityConverter converter = new UriToEntityConverter(entities, conversionService);
conversionService.addConverter(converter);
addFormatters(conversionService);
for (RepositoryRestConfigurer configurer : configurers) {
configurer.configureConversionService(conversionService);
}
URI uri = convert(link);
T object = target.cast(conversionService.convert(uri, TypeDescriptor.valueOf(target)));
if (object == null) {
throw new IllegalArgumentException(String.format("%s '%s' was not found.", target.getSimpleName(), uri));
}
return object;
}
private URI convert(Link link) {
try {
return new URI(link.getHref());
} catch (Exception e) {
throw new IllegalArgumentException("URI from link is invalid", e);
}
}
private void addFormatters(FormatterRegistry registry) {
registry.addFormatter(DistanceFormatter.INSTANCE);
registry.addFormatter(PointFormatter.INSTANCE);
if (!(registry instanceof FormattingConversionService)) {
return;
}
FormattingConversionService conversionService = (FormattingConversionService) registry;
DomainClassConverter<FormattingConversionService> converter = new DomainClassConverter<FormattingConversionService>(
conversionService);
converter.setApplicationContext(applicationContext);
}
}
And yes, it's likely that parts of this class are simply useless. In my defense, it was just a short hack and I never got around to actually need it, because I found other problems first ;-)
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