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.
This commit is contained in:
Oliver Drotbohm
2021-04-07 16:46:31 +02:00
parent ba2a34152d
commit 1ae7efd0b8
11 changed files with 98 additions and 36 deletions

View File

@@ -90,7 +90,7 @@ class PersistentPropertyResourceMapping implements PropertyAwareResourceMapping
return false;
}
ResourceMapping typeMapping = mappings.getMetadataFor(property.getActualType());
ResourceMapping typeMapping = mappings.getMetadataFor(property.getAssociationTargetType());
return !typeMapping.isExported() ? false : annotation.map(it -> it.exported()).orElse(true);
}

View File

@@ -17,9 +17,13 @@ package org.springframework.data.rest.core.support;
import java.util.List;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.server.EntityLinks;
import org.springframework.lang.Nullable;
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.util.Assert;
@@ -36,6 +40,7 @@ public class DefaultSelfLinkProvider implements SelfLinkProvider {
private final PersistentEntities entities;
private final EntityLinks entityLinks;
private final PluginRegistry<EntityLookup<?>, Class<?>> lookups;
private final ConversionService conversionService;
/**
* Creates a new {@link DefaultSelfLinkProvider} from the {@link PersistentEntities}, {@link EntityLinks} and
@@ -46,7 +51,7 @@ public class DefaultSelfLinkProvider implements SelfLinkProvider {
* @param lookups must not be {@literal null}.
*/
public DefaultSelfLinkProvider(PersistentEntities entities, EntityLinks entityLinks,
List<? extends EntityLookup<?>> lookups) {
List<? extends EntityLookup<?>> lookups, ConversionService conversionService) {
Assert.notNull(entities, "PersistentEntities must not be null!");
Assert.notNull(entityLinks, "EntityLinks must not be null!");
@@ -55,6 +60,7 @@ public class DefaultSelfLinkProvider implements SelfLinkProvider {
this.entities = entities;
this.entityLinks = entityLinks;
this.lookups = PluginRegistry.of(lookups);
this.conversionService = conversionService;
}
/*
@@ -65,29 +71,54 @@ public class DefaultSelfLinkProvider implements SelfLinkProvider {
Assert.notNull(instance, "Domain object must not be null!");
return entityLinks.linkToItemResource(instance.getClass(), getResourceId(instance));
return createSelfLinkFor(instance.getClass(), instance);
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.support.SelfLinkProvider#createSelfLinkFor(java.lang.Class, java.lang.Object)
*/
public Link createSelfLinkFor(Class<?> type, Object reference) {
if (type.isInstance(reference)) {
return entityLinks.linkToItemResource(type, getResourceId(type, reference));
}
PersistentEntity<?, ?> entity = entities.getRequiredPersistentEntity(type);
PersistentProperty<?> idProperty = entity.getRequiredIdProperty();
Object identifier = conversionService.convert(reference, idProperty.getType());
if (lookups.hasPluginFor(type)) {
identifier = getResourceId(type, conversionService.convert(identifier, type));
}
return entityLinks.linkToItemResource(type, identifier);
}
/**
* Returns the identifier to be used to create the self link URI.
*
* @param instance must not be {@literal null}.
* @param reference must not be {@literal null}.
* @return
*/
@SuppressWarnings("unchecked")
private Object getResourceId(Object instance) {
@Nullable
private Object getResourceId(Class<?> type, Object reference) {
Class<? extends Object> instanceType = instance.getClass();
if (!lookups.hasPluginFor(type)) {
return entityIdentifierOrNull(reference);
}
return lookups.getPluginFor(instanceType)//
return lookups.getPluginFor(type)//
.map(it -> it.getClass().cast(it))//
.map(it -> it.getResourceIdentifier(instance))//
.orElseGet(() -> identifierOrNull(instance));
.map(it -> it.getResourceIdentifier(reference))//
.orElseGet(() -> entityIdentifierOrNull(reference));
}
private Object identifierOrNull(Object instance) {
private Object entityIdentifierOrNull(Object instance) {
return entities.getRequiredPersistentEntity(instance.getClass())//
.getIdentifierAccessor(instance).getIdentifier();
return entities.getRequiredPersistentEntity(instance.getClass()) //
.getIdentifierAccessor(instance) //
.getIdentifier();
}
}

View File

@@ -27,10 +27,24 @@ import org.springframework.hateoas.Link;
public interface SelfLinkProvider {
/**
* Returns the self link for the given entity instance.
* Returns the self link for the given entity instance. Only call this with an actual entity instance. Otherwise,
* prefer {@link #createSelfLinkFor(Class, Object)}.
*
* @param instance must never be {@literal null}.
* @return will never be {@literal null}.
* @see #createSelfLinkFor(Class, Object)
*/
Link createSelfLinkFor(Object instance);
/**
* Returns the self link for the entity of the given type and the given reference. The latter can be an instance of
* the former, an identifier value of the former or anything that can be converted into an identifier in the first
* place.
*
* @param type must not be {@literal null}.
* @param reference must not be {@literal null}.
* @return will never be {@literal null}.
* @since 3.5
*/
Link createSelfLinkFor(Class<?> type, Object reference);
}

View File

@@ -28,7 +28,8 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.context.PersistentEntities;
@@ -51,6 +52,7 @@ public class DefaultSelfLinkProviderUnitTests {
@Mock EntityLinks entityLinks;
PersistentEntities entities;
List<EntityLookup<?>> lookups;
ConversionService conversionService;
@Before
public void setUp() {
@@ -69,22 +71,23 @@ public class DefaultSelfLinkProviderUnitTests {
this.entities = new PersistentEntities(Arrays.asList(context));
this.lookups = Collections.emptyList();
this.provider = new DefaultSelfLinkProvider(entities, entityLinks, lookups);
this.conversionService = new DefaultConversionService();
this.provider = new DefaultSelfLinkProvider(entities, entityLinks, lookups, conversionService);
}
@Test(expected = IllegalArgumentException.class) // DATAREST-724
public void rejectsNullEntities() {
new DefaultSelfLinkProvider(null, entityLinks, lookups);
new DefaultSelfLinkProvider(null, entityLinks, lookups, conversionService);
}
@Test(expected = IllegalArgumentException.class) // DATAREST-724
public void rejectsNullEntityLinks() {
new DefaultSelfLinkProvider(entities, null, lookups);
new DefaultSelfLinkProvider(entities, null, lookups, conversionService);
}
@Test(expected = IllegalArgumentException.class) // DATAREST-724
public void rejectsNullEntityLookups() {
new DefaultSelfLinkProvider(entities, entityLinks, null);
new DefaultSelfLinkProvider(entities, entityLinks, null, conversionService);
}
@Test // DATAREST-724
@@ -104,7 +107,8 @@ public class DefaultSelfLinkProviderUnitTests {
when(lookup.supports(Profile.class)).thenReturn(true);
when(lookup.getResourceIdentifier(any(Profile.class))).thenReturn("foo");
this.provider = new DefaultSelfLinkProvider(entities, entityLinks, Collections.singletonList(lookup));
this.provider = new DefaultSelfLinkProvider(entities, entityLinks, Collections.singletonList(lookup),
conversionService);
Link link = provider.createSelfLinkFor(new Profile("Name", "Type"));
@@ -114,7 +118,8 @@ public class DefaultSelfLinkProviderUnitTests {
@Test // DATAREST-724, DATAREST-1549
public void rejectsLinkCreationForUnknownEntity() {
assertThatExceptionOfType(MappingException.class).isThrownBy(() -> provider.createSelfLinkFor(new Object())) //
assertThatExceptionOfType(MappingException.class) //
.isThrownBy(() -> provider.createSelfLinkFor(new Object())) //
.withMessageContaining(Object.class.getName()) //
.withMessageContaining("Couldn't find PersistentEntity for");
}