diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/DefaultSelfLinkProvider.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/DefaultSelfLinkProvider.java index c5983b663..e053f228f 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/DefaultSelfLinkProvider.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/DefaultSelfLinkProvider.java @@ -124,8 +124,26 @@ private Object getResourceId(Class type, @Nullable Object reference) { private @Nullable Object entityIdentifierOrNull(Object instance) { - return entities.getRequiredPersistentEntity(instance.getClass()) // - .getIdentifierAccessor(instance) // - .getIdentifier(); + PersistentEntity entity = entities.getRequiredPersistentEntity(instance.getClass()); + + // First try the standard IdentifierAccessor path (works for simple @Id) + Object identifier = entity.getIdentifierAccessor(instance).getIdentifier(); + + if (identifier != null) { + return identifier; + } + + // Fall back to reading the ID property directly via PersistentPropertyAccessor. + // This handles composite keys (@EmbeddedId / @IdClass) where the IdentifierAccessor + // may return null because the mapping layer cannot resolve the composite key through + // its standard path (e.g. when the key type contains non-primitive fields such as + // Joda-Time DateTime that are not registered in the mapping context). + PersistentProperty idProperty = entity.getIdProperty(); + + if (idProperty == null) { + return null; + } + + return entity.getPropertyAccessor(instance).getProperty(idProperty); } } diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/support/DefaultSelfLinkProviderUnitTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/support/DefaultSelfLinkProviderUnitTests.java index 6e1ca067b..577ff6fbc 100755 --- a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/support/DefaultSelfLinkProviderUnitTests.java +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/support/DefaultSelfLinkProviderUnitTests.java @@ -19,6 +19,7 @@ import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import java.io.Serializable; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -30,8 +31,13 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.data.annotation.Id; import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext; +import org.springframework.data.mapping.IdentifierAccessor; import org.springframework.data.mapping.MappingException; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.rest.core.domain.Profile; import org.springframework.hateoas.Link; @@ -129,4 +135,68 @@ void rejectsLinkCreationForUnknownEntity() { .withMessageContaining(Object.class.getName()) // .withMessageContaining("Couldn't find PersistentEntity for"); } + + @Test // DATAREST-846 + @SuppressWarnings({ "unchecked", "rawtypes" }) + void fallsBackToIdPropertyAccessorWhenIdentifierAccessorReturnsNullForCompositeKey() { + + // Simulate an entity whose IdentifierAccessor returns null (as happens with + // @EmbeddedId / @IdClass in Spring Data JPA when the mapping context cannot + // resolve the composite key via its standard path), but whose ID property + // is still readable via PersistentPropertyAccessor. + CompositeKey compositeKey = new CompositeKey(1L, "part2"); + + IdentifierAccessor identifierAccessor = mock(IdentifierAccessor.class); + when(identifierAccessor.getIdentifier()).thenReturn(null); + + PersistentProperty idProperty = mock(PersistentProperty.class); + + PersistentPropertyAccessor propertyAccessor = mock(PersistentPropertyAccessor.class); + when(propertyAccessor.getProperty(idProperty)).thenReturn(compositeKey); + + PersistentEntity persistentEntity = mock(PersistentEntity.class); + when(persistentEntity.getIdentifierAccessor(any())).thenReturn(identifierAccessor); + when(persistentEntity.getIdProperty()).thenReturn(idProperty); + when(persistentEntity.getPropertyAccessor(any())).thenReturn(propertyAccessor); + + PersistentEntities mockEntities = mock(PersistentEntities.class); + when(mockEntities.getRequiredPersistentEntity(CompositeKeyEntity.class)).thenReturn(persistentEntity); + + SelfLinkProvider providerUnderTest = new DefaultSelfLinkProvider(mockEntities, entityLinks, lookups, + conversionService); + + CompositeKeyEntity entity = new CompositeKeyEntity(compositeKey); + Link link = providerUnderTest.createSelfLinkFor(entity); + + assertThat(link.getHref()).endsWith(compositeKey.toString()); + } + + // --------------------------------------------------------------------------- + // Helper types for the composite-key test + // --------------------------------------------------------------------------- + + static class CompositeKey implements Serializable { + + final Long part1; + final String part2; + + CompositeKey(Long part1, String part2) { + this.part1 = part1; + this.part2 = part2; + } + + @Override + public String toString() { + return part1 + "_" + part2; + } + } + + static class CompositeKeyEntity { + + @Id CompositeKey id; + + CompositeKeyEntity(CompositeKey id) { + this.id = id; + } + } }