From 898b36851085083eddbb8a5f3f8185d0970db2d9 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Fri, 15 Dec 2023 12:18:27 +0100 Subject: [PATCH] Fix potential NullPointerException in ProjectionSerializer. Fixes GH-1947. --- .../rest/core/mapping/ResourceMappings.java | 2 ++ .../json/PersistentEntityJackson2Module.java | 11 +++-------- .../data/rest/webmvc/mapping/Associations.java | 2 ++ ...ersistentEntityJackson2ModuleUnitTests.java | 18 ++++++++++++++++++ 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ResourceMappings.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ResourceMappings.java index c8a482940..0c5a57b4d 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ResourceMappings.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ResourceMappings.java @@ -16,6 +16,7 @@ package org.springframework.data.rest.core.mapping; import org.springframework.data.util.Streamable; +import org.springframework.lang.Nullable; /** * @author Oliver Gierke @@ -28,6 +29,7 @@ public interface ResourceMappings extends Streamable { * @param type must not be {@literal null}. * @return the {@link ResourceMetadata} if available or {@literal null} otherwise. */ + @Nullable ResourceMetadata getMetadataFor(Class type); /** diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java index 6a72a1355..1cd71ecd2 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java @@ -34,15 +34,11 @@ import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.projection.TargetAware; -import org.springframework.data.repository.support.Repositories; import org.springframework.data.repository.support.RepositoryInvoker; import org.springframework.data.repository.support.RepositoryInvokerFactory; import org.springframework.data.rest.core.UriToEntityConverter; -import org.springframework.data.rest.core.config.RepositoryRestConfiguration; -import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.core.mapping.ResourceMetadata; import org.springframework.data.rest.core.support.EntityLookup; -import org.springframework.data.rest.core.support.SelfLinkProvider; import org.springframework.data.rest.webmvc.EmbeddedResourcesAssembler; import org.springframework.data.rest.webmvc.PersistentEntityResource; import org.springframework.data.rest.webmvc.mapping.Associations; @@ -649,7 +645,7 @@ public class PersistentEntityJackson2Module extends SimpleModule { * @param invoker must not be {@literal null}. * @param unwrapping */ - private ProjectionSerializer(LinkCollector collector, Associations mappings, + ProjectionSerializer(LinkCollector collector, Associations mappings, RepresentationModelProcessorInvoker invoker, boolean unwrapping) { super(TargetAware.class); @@ -694,11 +690,11 @@ public class PersistentEntityJackson2Module extends SimpleModule { * @param value must not be {@literal null}. * @return */ - private ProjectionResource toModel(TargetAware value) { + ProjectionResource toModel(TargetAware value) { Object target = value.getTarget(); ResourceMetadata metadata = associations.getMetadataFor(value.getTargetClass()); - Links links = metadata.isExported() ? collector.getLinksFor(target) : Links.NONE; + Links links = metadata != null && metadata.isExported() ? collector.getLinksFor(target) : Links.NONE; EntityModel resource = invoker.invokeProcessorsFor(EntityModel.of(value, links)); @@ -708,7 +704,6 @@ public class PersistentEntityJackson2Module extends SimpleModule { static class ProjectionResource extends EntityModel { - @SuppressWarnings("deprecation") ProjectionResource(TargetAware projection, Iterable links) { super(new ProjectionResourceContent(projection, projection.getClass().getInterfaces()[0]), links); } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/mapping/Associations.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/mapping/Associations.java index 7803c6f59..d95bef61c 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/mapping/Associations.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/mapping/Associations.java @@ -33,6 +33,7 @@ import org.springframework.hateoas.Link; import org.springframework.hateoas.TemplateVariable; import org.springframework.hateoas.TemplateVariables; import org.springframework.hateoas.UriTemplate; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -94,6 +95,7 @@ public class Associations { * @param type must not be {@literal null}. * @return */ + @Nullable public ResourceMetadata getMetadataFor(Class type) { Assert.notNull(type, "Type must not be null"); diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2ModuleUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2ModuleUnitTests.java index 7028068cd..fabf73a5e 100755 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2ModuleUnitTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2ModuleUnitTests.java @@ -39,6 +39,7 @@ import org.springframework.core.convert.TypeDescriptor; import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.context.PersistentEntities; +import org.springframework.data.projection.TargetAware; import org.springframework.data.repository.support.RepositoryInvoker; import org.springframework.data.repository.support.RepositoryInvokerFactory; import org.springframework.data.rest.core.UriToEntityConverter; @@ -52,8 +53,10 @@ import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module. import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module.LookupObjectSerializer; import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module.NestedEntitySerializer; import org.springframework.data.rest.webmvc.mapping.Associations; +import org.springframework.data.rest.webmvc.mapping.DefaultLinkCollector; import org.springframework.data.rest.webmvc.support.ExcerptProjector; import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.RepresentationModel; import org.springframework.hateoas.UriTemplate; import org.springframework.hateoas.server.EntityLinks; import org.springframework.hateoas.server.mvc.RepresentationModelProcessorInvoker; @@ -263,6 +266,19 @@ class PersistentEntityJackson2ModuleUnitTests { assertThatNoException().isThrownBy(() -> mapper.writeValueAsString(model)); } + @Test // GH-1947 + void projectionsHandleMissingMetadata() { + + var collector = new DefaultLinkCollector(persistentEntities, selfLinks, associations); + var invoker = mock(RepresentationModelProcessorInvoker.class); + when(invoker.invokeProcessorsFor(any(RepresentationModel.class))) + .then(invocation -> invocation.getArgument(0)); + + var serializer = new PersistentEntityJackson2Module.ProjectionSerializer(collector, associations, invoker, false); + + assertThatNoException().isThrownBy(() -> serializer.toModel(mock(SampleProjection.class))); + } + /** * @author Oliver Gierke */ @@ -365,4 +381,6 @@ class PersistentEntityJackson2ModuleUnitTests { gen.writeEndObject(); } } + + interface SampleProjection extends TargetAware {} }