19

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.

Serg
  • 938
  • 8
  • 14
  • 4
    I couldn't find it, but I've posted on this topic before. Unfortunately, there's currently no automatic way to extract an entity from a link in Spring HATEOAS, and for at least two years the maintainers have ignored the need to understand incoming URLs in the request. – chrylis -cautiouslyoptimistic- May 12 '16 at 12:35
  • Request and controller don't seem to be RESTful. And `http://localhost:8080/api/rest/customers/8/product` is not a URI. Instead of trying to solve this particular problem I strongly recommend to redesign your API. – a better oliver May 12 '16 at 12:56
  • @zeroflagL, you are right, but we already have workable service with more than 20 rest resources that obey hateoas rules, we have clients that use it, and now we have to add two more custom endpoints. So I think it is logically to use same rules for all resources in our service. – Serg May 12 '16 at 14:01
  • I hope (because we already have spring-data-rest) find a way to use SDR flow for entity resolving – Serg May 12 '16 at 14:08
  • I fully agree that it's a bad idea to completely rewrite a working application. This particular endpoint, however, does **not** (seem to) _"obey hateoas rules"_. The approach you suggested is a good one and, as I said, you don't have to worry about `http://localhost:8080/api/rest/customers/8/product`, because it's not a URI. You never will use that as a reference to a product. – a better oliver May 13 '16 at 07:12

7 Answers7

8

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)
Luigi Cristalli
  • 131
  • 1
  • 5
  • Hi, could you post the entire code of those 3 classes? I've some issues trying to compile that code and fixes imports. Thanks – drenda Feb 15 '18 at 22:19
  • Hi, I'm not sure I can post the full code (if there actually is more code), I need to double-check. In the meantime, do I understand correctly that having the correct imports (and maybe dependency versions) will likely fix your problem? – Luigi Cristalli Feb 22 '18 at 11:00
  • Yes I think so. I have the same problem in my application – drenda Feb 23 '18 at 12:19
  • I edited the post again, please check if this works for you. I realized I had forgotten to mention that RootEntity is whatever is at the root of your domain structure. We used to have IDs and other stuff used by Hibernate in it but, for the way I used it, it might be a marker interface. Please let me know if I need to improve anything else and if it worked for you as well, otherwise I can help. I bet we can tame it again :) – Luigi Cristalli Feb 23 '18 at 17:35
  • This answer does not appear to validate that the URI being supplied matches the root uri of the api server or that the uri is a valid path for the class of entity you're trying to convert into? – adam p Feb 27 '18 at 21:09
  • @adamp Shouldn't the request fail anyway if the URI is somehow invalid? If I remember correctly (I moved to another project a year ago), this should be taken care of by the UriToEntityConverter and this code provides a way to hook into the same mechanism. Otherwise can you please rephrase your question? Because I'm not sure i understand the problem – Luigi Cristalli Feb 28 '18 at 02:36
  • @LuigiCristalli Based on the code above, it does not appear to do so, at least in the current version of SDR. It will happily accept any junk valid uri that is in the /{anything}/{id} format and look for an entity with that id in your table, or just return null. – adam p Feb 28 '18 at 17:07
  • In MyRepositoryRestConfigurer I don't get what is persistentEntities and how to get conversionService. Someone has an advice on that? Thanks – drenda Jul 11 '18 at 10:19
  • Does anyone have a full example of this? There are many things I don't get to work in these snippets. – user3235738 Apr 14 '20 at 07:25
2

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

Florian Schaetz
  • 10,454
  • 5
  • 32
  • 58
  • 1
    Works for me, but too bad it doesn't work for child entities (e.g. `http://localhost:8080/contacts/1/childEntity`) I searched around and found nothing so far. Also, I had to change the `UriToEntityConverter` instantiation to: `Repositories repositories = new Repositories(applicationContext); UriToEntityConverter converter = new UriToEntityConverter( new PersistentEntities(Collections.singleton(mappingContext)), new DefaultRepositoryInvokerFactory(repositories), repositories);` – GuiRitter Apr 06 '18 at 12:45
  • @GuiRitter how did you manage to make it work? I added the service but how do you interconnect it with Spring? Thanks – drenda Jul 11 '18 at 10:25
  • @drenda Just autowire it. `@Autowired private EntityConverter entityConverter;` – GuiRitter Jul 11 '18 at 10:49
  • @GuiRitter ok. But how do you use that in Spring custom controller? – drenda Jul 11 '18 at 10:49
  • @drenda Forget about what I said earlier. That was for using it manually. For using it in controllers, I'll have to post an answer because it's a bit more complex. Check again in a few minutes. – GuiRitter Jul 11 '18 at 10:52
  • @GuiRitter Thanks very much – drenda Jul 11 '18 at 10:57
  • How do you get MappingContext? I get bean not found? – erotsppa Nov 07 '19 at 03:35
2

For HAL with @RequestBody use Resource<T> as method parameter instead entity Action to allow convert related resources URIs

public ResponseEntity<ActionResult> doAction(@RequestBody Resource<Action> action){
pdorgambide
  • 1,787
  • 19
  • 33
  • What do you return from that method? – Hubert Grzeskowiak Jun 02 '17 at 15:11
  • Sorry!, It was a mistake. I fixed it to return as the original sample. – pdorgambide Jun 03 '17 at 09:15
  • If a pass only the URI, in a text/uri-list it do not convert to entity. To be more clear `@RequestBody Resource` or `Resources` passing in body `http://localhost:8080/api/entityType/1` result in empty resources. – Giovanni Silva Jun 23 '17 at 09:39
  • _"Resources is for a collection while Resource is for a single item. These types can be combined. If you know the links for each item in a collection, use Resources> (or whatever the core domain type is). This lets you assemble links for each item as well as for the whole collection."_ See more at [Spring Data REST Reference Doc](https://docs.spring.io/spring-data/rest/docs/current/reference/html/#customizing-sdr.overriding-sdr-response-handlers) – pdorgambide Jun 24 '17 at 14:04
  • 1
    This only seems to work for me if the passed class (Action in the example above) is an Entity, if it's just a POJO I still get the error. – Adam Jones Jan 26 '18 at 23:09
2

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

        // ...

    }

}
1

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.

Robert
  • 1,579
  • 1
  • 21
  • 36
  • One funny side remark: If you just simply provide a constructor from String (as the error message would suppose, then spring-data-rest actually calls that constructor. But how to load an entity from within that constructor in your model layer. That would be even more ugly. public UserModle(String uri) { ... userRepo.findById(parsedId) ... } – Robert Oct 22 '18 at 22:55
  • Just for reference: Can also be implemented with @JsonComonent https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-developing-web-applications.html#boot-features-json-components – Robert Oct 23 '18 at 18:50
  • Be careful with @JsonComponent. When you anotate your `EntityDeserializer` with @JsonComponent it will ALWAYS be used. This means it will also be used for the standard spring data rest endpoints. If you want custom deserialization only for your custom Rest Controllers then you can add `@JsonDeserialize(using=MyCustomModelDeserializer.class)` to the attribute in your entitty. – Robert Oct 29 '18 at 13:58
0

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.

GuiRitter
  • 697
  • 1
  • 11
  • 20
0

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}",
    ...
}
Bogdan Samondros
  • 138
  • 1
  • 10
  • How do you add/register/configure your CategoryConverter? So that it used by your REST controller? – Robert Oct 22 '18 at 21:22
  • Hi, @Robert, I've replaced OOTB DataRestController by my own implementation by using a set of `@BasePathAwareController` & `@RepositoryRestController` annotated RestCategoryController, and placed my converter there – Bogdan Samondros Oct 28 '18 at 11:56
  • The drawback there is you must specify "/categories/..." prefix in all methods in the given controller like `@RequestMapping(path = "categories", method = POST, consumes = MediaType.APPLICATION_JSON_VALUE)` to create entity for example. Non-overriden SDR mappings will still work aside. – Bogdan Samondros Oct 28 '18 at 12:03
  • How do you get MappingContext? It says bean not found? – erotsppa Nov 07 '19 at 04:02