Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
menu search
person
Welcome To Ask or Share your Answers For Others

Categories

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
 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
 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.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
207 views
Welcome To Ask or Share your Answers For Others

1 Answer

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:

  1. A conversion service to enable the conversion from a URI to an entity (leveraging the UriToEntityConverter provided with the framework)
  2. A deserializer to detect when it is appropriate to invoke the converter (we don't want to mess up with the default SDR behavior)
  3. A custom Jackson module to push everything to SDR

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:

  1. The entity being deserialized is a root entity (so no properties)
  2. The provided string is an actual URI (otherwise it just falls back to the default behavior)

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
...