Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Resolving entity URI in custom controller (Spring HATEOAS)

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.

like image 951
Serg Avatar asked May 12 '16 12:05

Serg


2 Answers

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)
like image 53
Luigi Cristalli Avatar answered Nov 15 '22 16:11

Luigi Cristalli


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

like image 22
Florian Schaetz Avatar answered Nov 15 '22 18:11

Florian Schaetz