Skip to content

Commit 1ae7efd

Browse files
committed
Properly handle associations in nested entities.
Nested entities that contain a reference to an aggregate root get a link to that attached to their representation. Previously, the creation of those links assumed that the reference is a materialized instance of the remote aggregate. That's now altered to be able to deal with associations, use identifiers directly or materialize to an intermediate aggregate instance to potentially use a custom lookup.
1 parent ba2a341 commit 1ae7efd

File tree

11 files changed

+98
-36
lines changed

11 files changed

+98
-36
lines changed

spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/PersistentPropertyResourceMapping.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public boolean isExported() {
9090
return false;
9191
}
9292

93-
ResourceMapping typeMapping = mappings.getMetadataFor(property.getActualType());
93+
ResourceMapping typeMapping = mappings.getMetadataFor(property.getAssociationTargetType());
9494
return !typeMapping.isExported() ? false : annotation.map(it -> it.exported()).orElse(true);
9595
}
9696

spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/DefaultSelfLinkProvider.java

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,13 @@
1717

1818
import java.util.List;
1919

20+
import org.springframework.core.convert.ConversionService;
21+
import org.springframework.data.mapping.PersistentEntity;
22+
import org.springframework.data.mapping.PersistentProperty;
2023
import org.springframework.data.mapping.context.PersistentEntities;
2124
import org.springframework.hateoas.Link;
2225
import org.springframework.hateoas.server.EntityLinks;
26+
import org.springframework.lang.Nullable;
2327
import org.springframework.plugin.core.PluginRegistry;
2428
import org.springframework.util.Assert;
2529

@@ -36,6 +40,7 @@ public class DefaultSelfLinkProvider implements SelfLinkProvider {
3640
private final PersistentEntities entities;
3741
private final EntityLinks entityLinks;
3842
private final PluginRegistry<EntityLookup<?>, Class<?>> lookups;
43+
private final ConversionService conversionService;
3944

4045
/**
4146
* Creates a new {@link DefaultSelfLinkProvider} from the {@link PersistentEntities}, {@link EntityLinks} and
@@ -46,7 +51,7 @@ public class DefaultSelfLinkProvider implements SelfLinkProvider {
4651
* @param lookups must not be {@literal null}.
4752
*/
4853
public DefaultSelfLinkProvider(PersistentEntities entities, EntityLinks entityLinks,
49-
List<? extends EntityLookup<?>> lookups) {
54+
List<? extends EntityLookup<?>> lookups, ConversionService conversionService) {
5055

5156
Assert.notNull(entities, "PersistentEntities must not be null!");
5257
Assert.notNull(entityLinks, "EntityLinks must not be null!");
@@ -55,6 +60,7 @@ public DefaultSelfLinkProvider(PersistentEntities entities, EntityLinks entityLi
5560
this.entities = entities;
5661
this.entityLinks = entityLinks;
5762
this.lookups = PluginRegistry.of(lookups);
63+
this.conversionService = conversionService;
5864
}
5965

6066
/*
@@ -65,29 +71,54 @@ public Link createSelfLinkFor(Object instance) {
6571

6672
Assert.notNull(instance, "Domain object must not be null!");
6773

68-
return entityLinks.linkToItemResource(instance.getClass(), getResourceId(instance));
74+
return createSelfLinkFor(instance.getClass(), instance);
75+
}
76+
77+
/*
78+
* (non-Javadoc)
79+
* @see org.springframework.data.rest.core.support.SelfLinkProvider#createSelfLinkFor(java.lang.Class, java.lang.Object)
80+
*/
81+
public Link createSelfLinkFor(Class<?> type, Object reference) {
82+
83+
if (type.isInstance(reference)) {
84+
return entityLinks.linkToItemResource(type, getResourceId(type, reference));
85+
}
86+
87+
PersistentEntity<?, ?> entity = entities.getRequiredPersistentEntity(type);
88+
PersistentProperty<?> idProperty = entity.getRequiredIdProperty();
89+
90+
Object identifier = conversionService.convert(reference, idProperty.getType());
91+
92+
if (lookups.hasPluginFor(type)) {
93+
identifier = getResourceId(type, conversionService.convert(identifier, type));
94+
}
95+
96+
return entityLinks.linkToItemResource(type, identifier);
6997
}
7098

7199
/**
72100
* Returns the identifier to be used to create the self link URI.
73101
*
74-
* @param instance must not be {@literal null}.
102+
* @param reference must not be {@literal null}.
75103
* @return
76104
*/
77-
@SuppressWarnings("unchecked")
78-
private Object getResourceId(Object instance) {
105+
@Nullable
106+
private Object getResourceId(Class<?> type, Object reference) {
79107

80-
Class<? extends Object> instanceType = instance.getClass();
108+
if (!lookups.hasPluginFor(type)) {
109+
return entityIdentifierOrNull(reference);
110+
}
81111

82-
return lookups.getPluginFor(instanceType)//
112+
return lookups.getPluginFor(type)//
83113
.map(it -> it.getClass().cast(it))//
84-
.map(it -> it.getResourceIdentifier(instance))//
85-
.orElseGet(() -> identifierOrNull(instance));
114+
.map(it -> it.getResourceIdentifier(reference))//
115+
.orElseGet(() -> entityIdentifierOrNull(reference));
86116
}
87117

88-
private Object identifierOrNull(Object instance) {
118+
private Object entityIdentifierOrNull(Object instance) {
89119

90-
return entities.getRequiredPersistentEntity(instance.getClass())//
91-
.getIdentifierAccessor(instance).getIdentifier();
120+
return entities.getRequiredPersistentEntity(instance.getClass()) //
121+
.getIdentifierAccessor(instance) //
122+
.getIdentifier();
92123
}
93124
}

spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/SelfLinkProvider.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,24 @@
2727
public interface SelfLinkProvider {
2828

2929
/**
30-
* Returns the self link for the given entity instance.
30+
* Returns the self link for the given entity instance. Only call this with an actual entity instance. Otherwise,
31+
* prefer {@link #createSelfLinkFor(Class, Object)}.
3132
*
3233
* @param instance must never be {@literal null}.
3334
* @return will never be {@literal null}.
35+
* @see #createSelfLinkFor(Class, Object)
3436
*/
3537
Link createSelfLinkFor(Object instance);
38+
39+
/**
40+
* Returns the self link for the entity of the given type and the given reference. The latter can be an instance of
41+
* the former, an identifier value of the former or anything that can be converted into an identifier in the first
42+
* place.
43+
*
44+
* @param type must not be {@literal null}.
45+
* @param reference must not be {@literal null}.
46+
* @return will never be {@literal null}.
47+
* @since 3.5
48+
*/
49+
Link createSelfLinkFor(Class<?> type, Object reference);
3650
}

spring-data-rest-core/src/test/java/org/springframework/data/rest/core/support/DefaultSelfLinkProviderUnitTests.java

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
import org.junit.runner.RunWith;
2929
import org.mockito.Mock;
3030
import org.mockito.junit.MockitoJUnitRunner;
31-
31+
import org.springframework.core.convert.ConversionService;
32+
import org.springframework.core.convert.support.DefaultConversionService;
3233
import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext;
3334
import org.springframework.data.mapping.MappingException;
3435
import org.springframework.data.mapping.context.PersistentEntities;
@@ -51,6 +52,7 @@ public class DefaultSelfLinkProviderUnitTests {
5152
@Mock EntityLinks entityLinks;
5253
PersistentEntities entities;
5354
List<EntityLookup<?>> lookups;
55+
ConversionService conversionService;
5456

5557
@Before
5658
public void setUp() {
@@ -69,22 +71,23 @@ public void setUp() {
6971

7072
this.entities = new PersistentEntities(Arrays.asList(context));
7173
this.lookups = Collections.emptyList();
72-
this.provider = new DefaultSelfLinkProvider(entities, entityLinks, lookups);
74+
this.conversionService = new DefaultConversionService();
75+
this.provider = new DefaultSelfLinkProvider(entities, entityLinks, lookups, conversionService);
7376
}
7477

7578
@Test(expected = IllegalArgumentException.class) // DATAREST-724
7679
public void rejectsNullEntities() {
77-
new DefaultSelfLinkProvider(null, entityLinks, lookups);
80+
new DefaultSelfLinkProvider(null, entityLinks, lookups, conversionService);
7881
}
7982

8083
@Test(expected = IllegalArgumentException.class) // DATAREST-724
8184
public void rejectsNullEntityLinks() {
82-
new DefaultSelfLinkProvider(entities, null, lookups);
85+
new DefaultSelfLinkProvider(entities, null, lookups, conversionService);
8386
}
8487

8588
@Test(expected = IllegalArgumentException.class) // DATAREST-724
8689
public void rejectsNullEntityLookups() {
87-
new DefaultSelfLinkProvider(entities, entityLinks, null);
90+
new DefaultSelfLinkProvider(entities, entityLinks, null, conversionService);
8891
}
8992

9093
@Test // DATAREST-724
@@ -104,7 +107,8 @@ public void usesEntityLookupIfDefined() {
104107
when(lookup.supports(Profile.class)).thenReturn(true);
105108
when(lookup.getResourceIdentifier(any(Profile.class))).thenReturn("foo");
106109

107-
this.provider = new DefaultSelfLinkProvider(entities, entityLinks, Collections.singletonList(lookup));
110+
this.provider = new DefaultSelfLinkProvider(entities, entityLinks, Collections.singletonList(lookup),
111+
conversionService);
108112

109113
Link link = provider.createSelfLinkFor(new Profile("Name", "Type"));
110114

@@ -114,7 +118,8 @@ public void usesEntityLookupIfDefined() {
114118
@Test // DATAREST-724, DATAREST-1549
115119
public void rejectsLinkCreationForUnknownEntity() {
116120

117-
assertThatExceptionOfType(MappingException.class).isThrownBy(() -> provider.createSelfLinkFor(new Object())) //
121+
assertThatExceptionOfType(MappingException.class) //
122+
.isThrownBy(() -> provider.createSelfLinkFor(new Object())) //
118123
.withMessageContaining(Object.class.getName()) //
119124
.withMessageContaining("Couldn't find PersistentEntity for");
120125
}

spring-data-rest-tests/spring-data-rest-tests-core/src/test/java/org/springframework/data/rest/tests/AbstractControllerIntegrationTests.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.springframework.context.annotation.Bean;
2424
import org.springframework.context.annotation.Configuration;
2525
import org.springframework.context.annotation.Import;
26+
import org.springframework.core.convert.support.DefaultConversionService;
2627
import org.springframework.data.mapping.PersistentEntity;
2728
import org.springframework.data.mapping.context.PersistentEntities;
2829
import org.springframework.data.repository.support.Repositories;
@@ -66,7 +67,8 @@ public static class TestConfiguration {
6667
public PersistentEntityResourceAssembler persistentEntityResourceAssembler(PersistentEntities entities,
6768
EntityLinks entityLinks, Associations associations) {
6869

69-
SelfLinkProvider selfLinkProvider = new DefaultSelfLinkProvider(entities, entityLinks, Collections.emptyList());
70+
SelfLinkProvider selfLinkProvider = new DefaultSelfLinkProvider(entities, entityLinks, Collections.emptyList(),
71+
new DefaultConversionService());
7072

7173
return new PersistentEntityResourceAssembler(entities, StubProjector.INSTANCE, associations,
7274
selfLinkProvider);

spring-data-rest-tests/spring-data-rest-tests-core/src/test/java/org/springframework/data/rest/tests/RepositoryTestsConfig.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.springframework.context.ApplicationContext;
2525
import org.springframework.context.annotation.Bean;
2626
import org.springframework.context.annotation.Configuration;
27+
import org.springframework.core.convert.support.DefaultConversionService;
2728
import org.springframework.data.mapping.context.MappingContext;
2829
import org.springframework.data.mapping.context.PersistentEntities;
2930
import org.springframework.data.repository.support.DefaultRepositoryInvokerFactory;
@@ -112,7 +113,7 @@ public Module persistentEntityModule() {
112113
EntityLinks entityLinks = new RepositoryEntityLinks(repositories(), mappings, config(),
113114
mock(PagingAndSortingTemplateVariables.class), PluginRegistry.of(DefaultIdConverter.INSTANCE));
114115
SelfLinkProvider selfLinkProvider = new DefaultSelfLinkProvider(persistentEntities(), entityLinks,
115-
Collections.<EntityLookup<?>> emptyList());
116+
Collections.<EntityLookup<?>> emptyList(), new DefaultConversionService());
116117

117118
DefaultRepositoryInvokerFactory invokerFactory = new DefaultRepositoryInvokerFactory(repositories());
118119
UriToEntityConverter uriToEntityConverter = new UriToEntityConverter(persistentEntities(), invokerFactory,

spring-data-rest-tests/spring-data-rest-tests-jpa/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.springframework.context.ApplicationContext;
2525
import org.springframework.context.annotation.Bean;
2626
import org.springframework.context.annotation.Configuration;
27+
import org.springframework.core.convert.support.DefaultConversionService;
2728
import org.springframework.data.mapping.context.MappingContext;
2829
import org.springframework.data.mapping.context.PersistentEntities;
2930
import org.springframework.data.repository.support.DefaultRepositoryInvokerFactory;
@@ -36,7 +37,6 @@
3637
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
3738
import org.springframework.data.rest.core.mapping.RepositoryResourceMappings;
3839
import org.springframework.data.rest.core.support.DefaultSelfLinkProvider;
39-
import org.springframework.data.rest.core.support.EntityLookup;
4040
import org.springframework.data.rest.core.support.SelfLinkProvider;
4141
import org.springframework.data.rest.webmvc.EmbeddedResourcesAssembler;
4242
import org.springframework.data.rest.webmvc.jpa.Person;
@@ -120,7 +120,7 @@ public Module persistentEntityModule() {
120120
EntityLinks entityLinks = new RepositoryEntityLinks(repositories(), mappings, config(),
121121
mock(PagingAndSortingTemplateVariables.class), PluginRegistry.of(DefaultIdConverter.INSTANCE));
122122
SelfLinkProvider selfLinkProvider = new DefaultSelfLinkProvider(persistentEntities(), entityLinks,
123-
Collections.<EntityLookup<?>> emptyList());
123+
Collections.emptyList(), new DefaultConversionService());
124124

125125
DefaultRepositoryInvokerFactory invokerFactory = new DefaultRepositoryInvokerFactory(repositories());
126126
UriToEntityConverter uriToEntityConverter = new UriToEntityConverter(persistentEntities(), invokerFactory,

spring-data-rest-tests/spring-data-rest-tests-mongodb/src/test/java/org/springframework/data/rest/webmvc/PersistentEntityResourceAssemblerIntegrationTests.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import org.junit.Test;
2626
import org.mockito.internal.stubbing.answers.ReturnsArgumentAt;
2727
import org.springframework.beans.factory.annotation.Autowired;
28+
import org.springframework.core.convert.ConversionService;
29+
import org.springframework.core.convert.support.DefaultConversionService;
2830
import org.springframework.data.mapping.context.PersistentEntities;
2931
import org.springframework.data.rest.core.support.DefaultSelfLinkProvider;
3032
import org.springframework.data.rest.tests.AbstractControllerIntegrationTests;
@@ -33,9 +35,9 @@
3335
import org.springframework.data.rest.tests.mongodb.User;
3436
import org.springframework.data.rest.webmvc.mapping.Associations;
3537
import org.springframework.data.rest.webmvc.support.Projector;
36-
import org.springframework.hateoas.server.EntityLinks;
3738
import org.springframework.hateoas.IanaLinkRelations;
3839
import org.springframework.hateoas.Links;
40+
import org.springframework.hateoas.server.EntityLinks;
3941
import org.springframework.test.context.ContextConfiguration;
4042

4143
/**
@@ -57,8 +59,9 @@ public void addsSelfAndSingleResourceLinkToResourceByDefault() throws Exception
5759

5860
when(projector.projectExcerpt(any())).thenAnswer(new ReturnsArgumentAt(0));
5961

62+
ConversionService conversionService = new DefaultConversionService();
6063
PersistentEntityResourceAssembler assembler = new PersistentEntityResourceAssembler(entities, projector,
61-
associations, new DefaultSelfLinkProvider(entities, entityLinks, Collections.emptyList()));
64+
associations, new DefaultSelfLinkProvider(entities, entityLinks, Collections.emptyList(), conversionService));
6265

6366
User user = new User();
6467
user.id = BigInteger.valueOf(4711);

spring-data-rest-tests/spring-data-rest-tests-mongodb/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntitySerializationTests.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151

5252
import com.fasterxml.jackson.databind.ObjectMapper;
5353
import com.jayway.jsonpath.JsonPath;
54+
import com.jayway.jsonpath.ReadContext;
5455

5556
/**
5657
* Integration tests for entity (de)serialization.
@@ -129,7 +130,9 @@ public void createsNestedResourceForMap() throws Exception {
129130
PersistentEntityResource resource = PersistentEntityResource
130131
.build(dave, repositories.getPersistentEntity(User.class)).build();
131132

132-
assertThat(JsonPath.parse(mapper.writeValueAsString(resource)).read("$.colleaguesMap.carter._links.user.href",
133-
String.class)).isNotNull();
133+
String result = mapper.writeValueAsString(resource);
134+
ReadContext document = JsonPath.parse(result);
135+
136+
assertThat(document.read("$.colleaguesMap.carter._links.user.href", String.class)).isNotNull();
134137
}
135138
}

spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -829,8 +829,10 @@ public HttpHeadersPreparer httpHeadersPreparer(AuditableBeanWrapperFactory audit
829829
}
830830

831831
@Bean
832-
public SelfLinkProvider selfLinkProvider(PersistentEntities persistentEntities, RepositoryEntityLinks entityLinks) {
833-
return new DefaultSelfLinkProvider(persistentEntities, entityLinks, getEntityLookups());
832+
public SelfLinkProvider selfLinkProvider(PersistentEntities persistentEntities, RepositoryEntityLinks entityLinks,
833+
@Qualifier("mvcConversionService") ObjectProvider<ConversionService> conversionService) {
834+
return new DefaultSelfLinkProvider(persistentEntities, entityLinks, getEntityLookups(),
835+
conversionService.getIfUnique(() -> defaultConversionService));
834836
}
835837

836838
@Bean

0 commit comments

Comments
 (0)