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 ;-)
对于带有@RequestBody
HAL,使用Resource<T>
作为方法参数而不是实体Action
以允许转换相关资源 URI
public ResponseEntity<ActionResult> doAction(@RequestBody Resource<Action> action){
I can't believe it. After wrapping my head around this for MONTH(!) I managed to SOLVE THIS !
Some words of introduction:
Spring HATEOAS uses URIs as references to entities. And it provides great support to obtain these URI Links for a given entity . For example, when a client requests an entity that references other child entities, then the client will receive those URIs. Nice to work with.
GET /users/1
{
"username": "foobar",
"_links": {
"self": {
"href": "http://localhost:8080/user/1" //<<<== HATEOAS Link
}
}
}
The REST client only works with those uris. The REST client MUST NOT know the structure of these URIs. The REST client does not know, that there is a DB internal ID at the end of the URI string.
So far so good. BUT spring data HATEOAS does not offer any functionality to convert an URI back to the corresponding entity (loaded from the DB). Everyone needs that in custom REST controllers. (See question above)
Think of an example where you want to work with a user in a custom REST controller. The client would send this request
POST /checkAdress
{
user: "/users/1"
someMoreOtherParams: "...",
[...]
}
How shall the custom REST controller deserialize from the (String) uri to the UserModel? I found a way: You must configure the Jackson deserialization in your RepositoryRestConfigurer:
RepositoryRestConfigurer.java
public class RepositoryRestConfigurer extends RepositoryRestConfigurerAdapter {
@Autowired
UserRepo userRepo;
@Override
public void configureJacksonObjectMapper(ObjectMapper objectMapper) {
SimpleModule module = new SimpleModule();
module.addDeserializer(UserModel.class, new JsonDeserializer<UserModel>() {
@Override
public UserModel deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
String uri = p.getValueAsString();
//extract ID from URI, with regular expression (1)
Pattern regex = Pattern.compile(".*\\/" + entityName + "\\/(\\d+)");
Matcher matcher = regex.matcher(uri);
if (!matcher.matches()) throw new RuntimeException("This does not seem to be an URI for an '"+entityName+"': "+uri);
String userId = matcher.group(1);
UserModel user = userRepo.findById(userId)
.orElseThrow(() -> new RuntimeException("User with id "+userId+" does not exist."))
return user;
}
});
objectMapper.registerModule(module);
}
}
(1) This string parsing is ugly. I know. But it's just the inverse of org.springframework.hateoas.EntityLinks and its implementations. And the author of spring-hateos stubbornly refuses to offer utility methods for both directions.
I arrived at the following solution. It's a bit hackish, but works.
First, the service to convert URIs into entities.
EntityConverter
import java.net.URI;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.geo.format.DistanceFormatter;
import org.springframework.data.geo.format.PointFormatter;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.repository.support.DefaultRepositoryInvokerFactory;
import org.springframework.data.repository.support.DomainClassConverter;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.UriToEntityConverter;
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer;
import org.springframework.format.FormatterRegistry;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.hateoas.Link;
import org.springframework.stereotype.Service;
@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();
Repositories repositories = new Repositories(applicationContext);
UriToEntityConverter converter = new UriToEntityConverter(
new PersistentEntities(Collections.singleton(mappingContext)),
new DefaultRepositoryInvokerFactory(repositories),
repositories);
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("registerNotFound", target.getSimpleName(), uri));
}
return object;
}
private URI convert(Link link) {
try {
return new URI(link.getHref().replace("{?projection}", ""));
} catch (Exception e) {
throw new IllegalArgumentException("invalidURI", 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);
}
}
Second, a component to be able to use the EntityConverter
outside of the Spring context.
ApplicationContextHolder
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class ApplicationContextHolder implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
public static ApplicationContext getContext() {
return context;
}
}
Third, entity constructor that takes another entity as input.
MyEntity
public MyEntity(MyEntity entity) {
property1 = entity.property1;
property2 = entity.property2;
property3 = entity.property3;
// ...
}
Fourth, entity constructor that takes a String
as input, which should be the URI.
MyEntity
public MyEntity(String URI) {
this(ApplicationContextHolder.getContext().getBean(EntityConverter.class).convert(new Link(URI.replace("{?projection}", "")), MyEntity.class));
}
Optionally, I have moved part of the code above to an Utils
class.
I arrived at this solution by looking at the error message from the question post, which I was getting as well. Spring doesn't know how to construct an object from a String
? I'll show it how...
Like a said in a comment, however, doesn't work for nested entities' URIs.
My solution will be some compact. Not sure that it can be useful for all cases but for simple relation like .../entity/{id}
it could parse. I've tested it on SDR & Spring Boot 2.0.3.RELEASE
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.repository.support.RepositoryInvokerFactory;
import org.springframework.data.rest.core.UriToEntityConverter;
import org.springframework.hateoas.Link;
import org.springframework.stereotype.Service;
import java.net.URI;
import java.util.Collections;
@Service
public class UriToEntityConversionService {
@Autowired
private MappingContext<?, ?> mappingContext; // OOTB
@Autowired
private RepositoryInvokerFactory invokerFactory; // OOTB
@Autowired
private Repositories repositories; // OOTB
public <T> T convert(Link link, Class<T> target) {
PersistentEntities entities = new PersistentEntities(Collections.singletonList(mappingContext));
UriToEntityConverter converter = new UriToEntityConverter(entities, invokerFactory, repositories);
URI uri = convert(link);
Object o = converter.convert(uri, TypeDescriptor.valueOf(URI.class), TypeDescriptor.valueOf(target));
T object = target.cast(o);
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);
}
}
}
Usage:
@Component
public class CategoryConverter implements Converter<CategoryForm, Category> {
private UriToEntityConversionService conversionService;
@Autowired
public CategoryConverter(UriToEntityConversionService conversionService) {
this.conversionService = conversionService;
}
@Override
public Category convert(CategoryForm source) {
Category category = new Category();
category.setId(source.getId());
category.setName(source.getName());
category.setOptions(source.getOptions());
if (source.getParent() != null) {
Category parent = conversionService.convert(new Link(source.getParent()), Category.class);
category.setParent(parent);
}
return category;
}
}
Request JSON like:
{
...
"parent": "http://localhost:8080/categories/{id}",
...
}
Unfortunately UriToEntityConverter (A Generic Converter that can convert a URI into an entity) which use Spring Data REST are not exported as a Bean or as a Service .
So we can not @Autowired
it directly but it registered as Converter in Default Formatting Conversion Service .
Thus we manage to @Autowired
Default Formatting Conversion Service and use them to convert a URI into an entity, for example:
@RestController
@RequiredArgsConstructor
public class InstanceController {
private final DefaultFormattingConversionService formattingConversionService;
@RequestMapping(path = "/api/instances", method = {RequestMethod.POST})
public ResponseEntity<?> create(@RequestBody @Valid InstanceDTO instanceDTO) { // get something what you want from request
// ...
// extract URI from any string what you want to process
final URI uri = "..."; // http://localhost:8080/api/instances/1
// convert URI to Entity
final Instance instance = formattingConversionService.convert(uri, Instance.class); // Instance(id=1, ...)
// ...
}
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.