Configure HATEOAS Link Generation for Objects with Compound Identifiers

I configured RepositoryRestResource to PageAndSortingRepository , which accesses an object that contains a composite identifier:

 @Entity @IdClass(CustomerId.class) public class Customer { @Id BigInteger id; @Id int startVersion; ... } public class CustomerId { BigInteger id; int startVersion; ... } @RepositoryRestResource(collectionResourceRel = "customers", path = "customers", itemResourceRel = "customers/{id}_{startVersion}") public interface CustomerRepository extends PagingAndSortingRepository<Customer, CustomerId> {} 

When, for example, I access the server in "http://<server>/api/customers/1_1" , I believe the correct resource as json, but the href in the _links section for self is incorrect, and also the same for any other request I client: "http://<server>/api/customer/1"

i.e:.

 { "id" : 1, "startVersion" : 1, ... "firstname" : "BOB", "_links" : { "self" : { "href" : "http://localhost:9081/reps/api/reps/1" <-- This should be /1_1 } } } 

I assume this is due to my composite identifier, but it bothers me how I can change this default behavior.

I looked at the ResourceSupport and ResourceProcessor class, but not sure how much I need to change to fix this problem.

Can anyone who knows spring lend me a hand?

+7
spring spring-data-jpa spring-hateoas spring-mvc
source share
4 answers

Unfortunately, all versions of Spring Data JPA / Rest prior to 2.1.0.RELEASE cannot satisfy your needs out of the box. Source buried inside Spring Data Commons / JPA. Spring JPA data only supports Id and EmbeddedId .

Excerpt JpaPersistentPropertyImpl :

 static { // [...] annotations = new HashSet<Class<? extends Annotation>>(); annotations.add(Id.class); annotations.add(EmbeddedId.class); ID_ANNOTATIONS = annotations; } 

Spring Data Commons does not support the concept of combined properties. It considers each property of a class independently of each other.

Of course you can hack Spring Data Rest. But this is cumbersome, does not solve the problem in your heart and reduces the flexibility of the structure.

Here's a hack. This should give you an idea of โ€‹โ€‹how to solve your problem.

In your configuration, override repositoryExporterHandlerAdapter and return CustomPersistentEntityResourceAssemblerArgumentResolver . Also, override the backendIdConverterRegistry and add CustomBackendIdConverter to the list of known id converter :

 import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.data.rest.core.projection.ProxyProjectionFactory; import org.springframework.data.rest.webmvc.RepositoryRestHandlerAdapter; import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration; import org.springframework.data.rest.webmvc.spi.BackendIdConverter; import org.springframework.data.rest.webmvc.support.HttpMethodHandlerMethodArgumentResolver; import org.springframework.data.web.config.EnableSpringDataWebSupport; import org.springframework.hateoas.ResourceProcessor; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.plugin.core.OrderAwarePluginRegistry; import org.springframework.plugin.core.PluginRegistry; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @Configuration @Import(RepositoryRestMvcConfiguration.class) @EnableSpringDataWebSupport public class RestConfig extends RepositoryRestMvcConfiguration { @Autowired(required = false) List<ResourceProcessor<?>> resourceProcessors = Collections.emptyList(); @Autowired ListableBeanFactory beanFactory; @Override @Bean public PluginRegistry<BackendIdConverter, Class<?>> backendIdConverterRegistry() { List<BackendIdConverter> converters = new ArrayList<BackendIdConverter>(3); converters.add(new CustomBackendIdConverter()); converters.add(BackendIdConverter.DefaultIdConverter.INSTANCE); return OrderAwarePluginRegistry.create(converters); } @Bean public RequestMappingHandlerAdapter repositoryExporterHandlerAdapter() { List<HttpMessageConverter<?>> messageConverters = defaultMessageConverters(); configureHttpMessageConverters(messageConverters); RepositoryRestHandlerAdapter handlerAdapter = new RepositoryRestHandlerAdapter(defaultMethodArgumentResolvers(), resourceProcessors); handlerAdapter.setMessageConverters(messageConverters); return handlerAdapter; } private List<HandlerMethodArgumentResolver> defaultMethodArgumentResolvers() { CustomPersistentEntityResourceAssemblerArgumentResolver peraResolver = new CustomPersistentEntityResourceAssemblerArgumentResolver( repositories(), entityLinks(), config().projectionConfiguration(), new ProxyProjectionFactory(beanFactory)); return Arrays.asList(pageableResolver(), sortResolver(), serverHttpRequestMethodArgumentResolver(), repoRequestArgumentResolver(), persistentEntityArgumentResolver(), resourceMetadataHandlerMethodArgumentResolver(), HttpMethodHandlerMethodArgumentResolver.INSTANCE, peraResolver, backendIdHandlerMethodArgumentResolver()); } } 

Create a CustomBackendIdConverter . This class is responsible for providing your custom object identifiers:

 import org.springframework.data.rest.webmvc.spi.BackendIdConverter; import java.io.Serializable; public class CustomBackendIdConverter implements BackendIdConverter { @Override public Serializable fromRequestId(String id, Class<?> entityType) { return id; } @Override public String toRequestId(Serializable id, Class<?> entityType) { if(entityType.equals(Customer.class)) { Customer c = (Customer) id; return c.getId() + "_" +c.getStartVersion(); } return id.toString(); } @Override public boolean supports(Class<?> delimiter) { return true; } } 

CustomPersistentEntityResourceAssemblerArgumentResolver in turn should return a CustomPersistentEntityResourceAssembler :

 import org.springframework.core.MethodParameter; import org.springframework.data.repository.support.Repositories; import org.springframework.data.rest.core.projection.ProjectionDefinitions; import org.springframework.data.rest.core.projection.ProjectionFactory; import org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler; import org.springframework.data.rest.webmvc.config.PersistentEntityResourceAssemblerArgumentResolver; import org.springframework.data.rest.webmvc.support.PersistentEntityProjector; import org.springframework.hateoas.EntityLinks; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.ModelAndViewContainer; public class CustomPersistentEntityResourceAssemblerArgumentResolver extends PersistentEntityResourceAssemblerArgumentResolver { private final Repositories repositories; private final EntityLinks entityLinks; private final ProjectionDefinitions projectionDefinitions; private final ProjectionFactory projectionFactory; public CustomPersistentEntityResourceAssemblerArgumentResolver(Repositories repositories, EntityLinks entityLinks, ProjectionDefinitions projectionDefinitions, ProjectionFactory projectionFactory) { super(repositories, entityLinks,projectionDefinitions,projectionFactory); this.repositories = repositories; this.entityLinks = entityLinks; this.projectionDefinitions = projectionDefinitions; this.projectionFactory = projectionFactory; } public boolean supportsParameter(MethodParameter parameter) { return PersistentEntityResourceAssembler.class.isAssignableFrom(parameter.getParameterType()); } public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { String projectionParameter = webRequest.getParameter(projectionDefinitions.getParameterName()); PersistentEntityProjector projector = new PersistentEntityProjector(projectionDefinitions, projectionFactory, projectionParameter); return new CustomPersistentEntityResourceAssembler(repositories, entityLinks, projector); } } 

CustomPersistentEntityResourceAssembler must be overridden by getSelfLinkFor . As you can see, entity.getIdProperty() returns the id or startVersion property of your Customer class, which, in turn, is used to retrieve the real value using BeanWrapper . Here we short-circuit the entire structure using the instanceof operator. Therefore, your Customer class must implement Serializable for further processing.

 import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.model.BeanWrapper; import org.springframework.data.repository.support.Repositories; import org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler; import org.springframework.data.rest.webmvc.support.Projector; import org.springframework.hateoas.EntityLinks; import org.springframework.hateoas.Link; import org.springframework.util.Assert; public class CustomPersistentEntityResourceAssembler extends PersistentEntityResourceAssembler { private final Repositories repositories; private final EntityLinks entityLinks; public CustomPersistentEntityResourceAssembler(Repositories repositories, EntityLinks entityLinks, Projector projector) { super(repositories, entityLinks, projector); this.repositories = repositories; this.entityLinks = entityLinks; } public Link getSelfLinkFor(Object instance) { Assert.notNull(instance, "Domain object must not be null!"); Class<? extends Object> instanceType = instance.getClass(); PersistentEntity<?, ?> entity = repositories.getPersistentEntity(instanceType); if (entity == null) { throw new IllegalArgumentException(String.format("Cannot create self link for %s! No persistent entity found!", instanceType)); } Object id; //this is a hack for demonstration purpose. don't do this at home! if(instance instanceof Customer) { id = instance; } else { BeanWrapper<Object> wrapper = BeanWrapper.create(instance, null); id = wrapper.getProperty(entity.getIdProperty()); } Link resourceLink = entityLinks.linkToSingleResource(entity.getType(), id); return new Link(resourceLink.getHref(), Link.REL_SELF); } } 

What is it! You should see these URIs:

 { "_embedded" : { "customers" : [ { "name" : "test", "_links" : { "self" : { "href" : "http://localhost:8080/demo/customers/1_1" } } } ] } } 

Imho, if you are working on a project with a green field, I would suggest to push through IdClass completely and go with technical simple identifiers based on Long class. This has been tested with Spring Data Rest 2.1.0.RELEASE, Spring Data JPA 1.6.0.RELEASE and Spring Framework 4.0.3.RELEASE.

+10
source share

Although this is undesirable, I worked on this issue using @EmbeddedId instead of the IdClass annotation for my JPA object.

Same:

 @Entity public class Customer { @EmbeddedId private CustomerId id; ... } public class CustomerId { @Column(...) BigInteger key; @Column(...) int startVersion; ... } 

Now I see correctly generated 1_1 links in my returned entities.

If someone can still guide me towards a solution that does not require a change in my view of the model, it would be highly appreciated. Fortunately, I did not go far in developing applications so that this would cause serious concern when changing, but I believe that for others it would be significant overhead when making such changes: (for example, changing all the queries that reference this model in JPQL queries )

+4
source share

I had a similar problem when complex scripts for storing data did not work. A detailed explanation of @ksokol provided the necessary materials to solve the problem. changed my pom primarily for data-rest-webmvc and data-jpa as

  <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-rest-webmvc</artifactId> <version>2.2.1.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-jpa</artifactId> <version>1.7.1.RELEASE</version> </dependency> 

which solved all the problems associated with the composite key, and I do not need to configure. Thanks to xokol for the detailed explanation.

0
source share

First create SpringUtil to get the bean from spring.

 import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; @Component public class SpringUtil implements ApplicationContextAware { private static ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { if(SpringUtil.applicationContext == null) { SpringUtil.applicationContext = applicationContext; } } public static ApplicationContext getApplicationContext() { return applicationContext; } public static Object getBean(String name){ return getApplicationContext().getBean(name); } public static <T> T getBean(Class<T> clazz){ return getApplicationContext().getBean(clazz); } public static <T> T getBean(String name,Class<T> clazz){ return getApplicationContext().getBean(name, clazz); } } 

Then we implement BackendIdConverter.

 import com.alibaba.fastjson.JSON; import com.example.SpringUtil; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.rest.webmvc.spi.BackendIdConverter; import org.springframework.stereotype.Component; import javax.persistence.EmbeddedId; import javax.persistence.Id; import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.lang.reflect.Method; import java.net.URLDecoder; import java.net.URLEncoder; @Component public class CustomBackendIdConverter implements BackendIdConverter { @Override public boolean supports(Class<?> delimiter) { return true; } @Override public Serializable fromRequestId(String id, Class<?> entityType) { if (id == null) { return null; } //first decode url string if (!id.contains(" ") && id.toUpperCase().contains("%7B")) { try { id = URLDecoder.decode(id, "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } //deserialize json string to ID object Object idObject = null; for (Method method : entityType.getDeclaredMethods()) { if (method.isAnnotationPresent(Id.class) || method.isAnnotationPresent(EmbeddedId.class)) { idObject = JSON.parseObject(id, method.getGenericReturnType()); break; } } //get dao class from spring Object daoClass = null; try { daoClass = SpringUtil.getBean(Class.forName("com.example.db.dao." + entityType.getSimpleName() + "DAO")); } catch (ClassNotFoundException e) { e.printStackTrace(); } //get the entity with given primary key JpaRepository simpleJpaRepository = (JpaRepository) daoClass; Object entity = simpleJpaRepository.findOne((Serializable) idObject); return (Serializable) entity; } @Override public String toRequestId(Serializable id, Class<?> entityType) { if (id == null) { return null; } String jsonString = JSON.toJSONString(id); String encodedString = ""; try { encodedString = URLEncoder.encode(jsonString, "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return encodedString; } } 

After that. you can do what you want.

The following is an example.

  • If the object has a single pk property, you can use localhost: 8080 / demo / 1 as usual. According to my code, suppose pk has the annotation "@Id".
  • If the object compiled pk, suppose pk is a demo type and has the annotation "@EmbeddedId", you can use localhost: 8080 / demo / {demoId json} to get / put / delete. And your personal link will be the same.
0
source share

All Articles