Fix mapping URIs to @JsonProperty annotated associations

Normally, when creating and updating (POST/PUT) an entity via the REST endpoints, you can use the URL of the relation target. (e.g.: send `{"package": "/packages/1"}` when `package` is a JPA `@OneToOne` relation).

Now this also takes into account when the JPA relation is annotated with `@JsonProperty` to change the serialized name.

Add unit tests for linkable associations.

Issue: #2165
This commit is contained in:
Lars Vierbergen
2022-07-27 10:03:26 +02:00
committed by Oliver Drotbohm
parent 8d9855b57e
commit 5335fe61e9
2 changed files with 62 additions and 1 deletions

View File

@@ -24,6 +24,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import org.slf4j.Logger;
@@ -82,6 +83,8 @@ import com.fasterxml.jackson.databind.deser.std.CollectionDeserializer;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer;
import com.fasterxml.jackson.databind.deser.std.StdValueInstantiator;
import com.fasterxml.jackson.databind.introspect.AnnotatedField;
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
@@ -410,6 +413,7 @@ public class PersistentEntityJackson2Module extends SimpleModule {
* non-optional associations can be populated on resource creation.
*
* @author Oliver Gierke
* @author Lars Vierbergen
*/
public static class AssociationUriResolvingDeserializerModifier extends BeanDeserializerModifier {
@@ -444,7 +448,18 @@ public class PersistentEntityJackson2Module extends SimpleModule {
while (properties.hasNext()) {
SettableBeanProperty property = properties.next();
PersistentProperty<?> persistentProperty = entity.getPersistentProperty(property.getName());
// To find the PersistentProperty name in case there is a @JsonProperty annotation
// on the field. Both BeanPropertyDefinition#getName() and BeanPropertyDefinition#getInternalName()
// don't return the actual name of the field, so we look up the AnnotatedField itself to retrieve
// the real name from, so it can be used for PersistentProperty lookup
String persistentPropertyName = beanDesc.findProperties().stream()
.filter(propertyDefinition -> property.getName().equals(propertyDefinition.getName()))
.map(BeanPropertyDefinition::getField).filter(Objects::nonNull).map(AnnotatedField::getName).findFirst()
// Fall back to the JSON name in case we can't find a BeanPropertyDefinition,
// so things can be mapped by convention in case they are immutable objects and are
// using constructor injection
.orElse(property.getName());
PersistentProperty<?> persistentProperty = entity.getPersistentProperty(persistentPropertyName);
if (persistentProperty == null) {
continue;

View File

@@ -19,6 +19,7 @@ import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Getter;
@@ -96,6 +97,7 @@ class PersistentEntityJackson2ModuleUnitTests {
KeyValueMappingContext<?, ?> mappingContext = new KeyValueMappingContext<>();
mappingContext.getPersistentEntity(Sample.class);
mappingContext.getPersistentEntity(Package.class);
mappingContext.getPersistentEntity(SampleWithAdditionalGetters.class);
mappingContext.getPersistentEntity(PersistentEntityJackson2ModuleUnitTests.PetOwner.class);
mappingContext.getPersistentEntity(Immutable.class);
@@ -157,6 +159,41 @@ class PersistentEntityJackson2ModuleUnitTests {
assertThat(petOwner.getPet()).isNotNull();
}
@Test
void allowsUrlsForLinkableAssociation() throws Exception {
when(converter.convert(UriTemplate.of("/homes/1").expand(), TypeDescriptor.valueOf(URI.class),
TypeDescriptor.valueOf(Home.class))).thenReturn(new Home());
PersistentProperty<?> property = persistentEntities.getRequiredPersistentEntity(PetOwner.class)
.getRequiredPersistentProperty("home");
when(associations.isLinkableAssociation(property)).thenReturn(true);
PetOwner petOwner = mapper.readValue("{\"home\": \"/homes/1\" }", PetOwner.class);
assertThat(petOwner).isNotNull();
assertThat(petOwner.getHome()).isInstanceOf(Home.class);
}
@Test
void allowsUrlsForRenamedLinkableAssociation() throws IOException {
when(converter.convert(UriTemplate.of("/packages/1").expand(), TypeDescriptor.valueOf(URI.class),
TypeDescriptor.valueOf(Package.class))).thenReturn(new Package());
PersistentProperty<?> property = persistentEntities.getRequiredPersistentEntity(PetOwner.class)
.getRequiredPersistentProperty("_package");
when(associations.isLinkableAssociation(property)).thenReturn(true);
PetOwner petOwner = mapper.readValue("{\"package\":\"/packages/1\"}", PetOwner.class);
assertThat(petOwner).isNotNull();
assertThat(petOwner.getPackage()).isNotNull();
}
@Test // DATAREST-1321
void allowsNumericIdsForLookupTypes() throws Exception {
@@ -260,8 +297,17 @@ class PersistentEntityJackson2ModuleUnitTests {
Pet pet;
Home home;
@Getter(value = AccessLevel.NONE)
@JsonProperty("package") Package _package;
public Package getPackage() {
return _package;
}
}
static class Package {}
@JsonTypeInfo(include = JsonTypeInfo.As.PROPERTY, use = JsonTypeInfo.Id.MINIMAL_CLASS)
static class Pet {}