From 897bc88d6922e755038a6a8e34ca25ace6a27689 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Fri, 19 Feb 2016 08:42:48 +0100 Subject: [PATCH] DATAREST-775 - Support for nested association links. Tweaked custom Jackson serialization to make sure nested entities are rendered as resources so that links to related resources can be collected on nested levels as well. Extracted EmbeddedResourcesAssembler from PersistentEntityResourceAssembler so that embedded resources can also be build for nested entities. Extracted a ResourceProcessorInvoker from ResourceProcessorHandlerMethodReturnValueHandler to allow ResourceSupport instances created for nested entities get the ResourceProcessor instance registered for them invoked as well. ProjectionDefinitionRegistrar now also allows the domain type of a repository being registered as excerpt, too. Related tickets: DATAREST-776. --- pom.xml | 1 - .../config/EntityLookupConfiguration.java | 32 +- .../core/config/EntityLookupRegistrar.java | 23 +- .../config/RepositoryRestConfiguration.java | 7 +- .../webmvc/EmbeddedResourcesAssembler.java | 119 +++++ .../rest/webmvc/PersistentEntityResource.java | 41 +- .../PersistentEntityResourceAssembler.java | 107 +---- .../webmvc/RepositoryRestHandlerAdapter.java | 9 +- ...cessorHandlerMethodReturnValueHandler.java | 362 +-------------- .../rest/webmvc/ResourceProcessorInvoker.java | 410 +++++++++++++++++ ...sourceProcessorInvokingHandlerAdapter.java | 18 +- ...eInformationToAlpsDescriptorConverter.java | 64 +-- ...tityResourceAssemblerArgumentResolver.java | 47 +- .../config/ProjectionDefinitionRegistar.java | 8 +- .../RepositoryRestMvcConfiguration.java | 80 +++- .../rest/webmvc/json/DomainObjectReader.java | 27 +- .../json/PersistentEntityJackson2Module.java | 412 ++++++++++-------- ...PersistentEntityToJsonSchemaConverter.java | 85 +++- .../rest/webmvc/mapping/AssociationLinks.java | 70 ++- .../LinkCollectingAssociationHandler.java | 16 +- .../rest/webmvc/mapping/LinkCollector.java | 263 +++++++++++ ...estedLinkCollectingAssociationHandler.java | 83 ++++ .../support/DefaultExcerptProjector.java | 64 +++ .../rest/webmvc/support/ExcerptProjector.java | 43 ++ .../support/PersistentEntityProjector.java | 43 +- .../data/rest/webmvc/support/Projector.java | 20 +- .../webmvc/support/RepositoryEntityLinks.java | 41 +- .../AbstractControllerIntegrationTests.java | 4 +- .../webmvc/AssociationLinksUnitTests.java | 34 +- ...tityResourceAssemblerIntegrationTests.java | 18 +- ...dlerMethodReturnValueHandlerUnitTests.java | 10 +- .../config/JsonPatchHandlerUnitTests.java | 7 +- .../rest/webmvc/jpa/DataRest262Tests.java | 2 + .../data/rest/webmvc/jpa/Person.java | 34 +- .../json/DomainObjectReaderUnitTests.java | 6 +- ...rsistentEntityJackson2ModuleUnitTests.java | 44 +- ...tEntityToJsonSchemaConverterUnitTests.java | 11 +- .../webmvc/json/RepositoryTestsConfig.java | 23 +- 38 files changed, 1713 insertions(+), 975 deletions(-) create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/EmbeddedResourcesAssembler.java create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceProcessorInvoker.java create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/mapping/LinkCollector.java create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/mapping/NestedLinkCollectingAssociationHandler.java create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/DefaultExcerptProjector.java create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/ExcerptProjector.java diff --git a/pom.xml b/pom.xml index c0ed261c5..a04484ee5 100644 --- a/pom.xml +++ b/pom.xml @@ -36,7 +36,6 @@ 1.4.0.BUILD-SNAPSHOT 4.3.10.Final - false diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/EntityLookupConfiguration.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/EntityLookupConfiguration.java index 64e082b30..52f5a2fcd 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/EntityLookupConfiguration.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/EntityLookupConfiguration.java @@ -26,6 +26,7 @@ import java.util.List; import org.springframework.core.convert.converter.Converter; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; import org.springframework.data.repository.support.Repositories; import org.springframework.data.rest.core.config.EntityLookupRegistrar.LookupRegistrar.Lookup; import org.springframework.data.rest.core.support.EntityLookup; @@ -33,15 +34,15 @@ import org.springframework.util.Assert; /** * Configuration instance to implement {@link EntityLookupRegistrar}. Exposed via - * {@link RepositoryRestConfiguration#withCustomEntityLookup()}. + * {@link RepositoryRestConfiguration#withEntityLookup()}. * * @author Oliver Gierke * @since 2.5 */ -@RequiredArgsConstructor class EntityLookupConfiguration implements EntityLookupRegistrar { private final List>> lookupInformation = new ArrayList>>(); + private final List> lookupTypes = new ArrayList>(); /* * (non-Javadoc) @@ -55,6 +56,17 @@ class EntityLookupConfiguration implements EntityLookupRegistrar { return this; } + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.config.EntityLookupRegistrar#forValueRepository(java.lang.Class) + */ + @Override + public > IdMappingRegistrar forValueRepository( + Class type) { + this.lookupTypes.add(AbstractRepositoryMetadata.getMetadata(type).getDomainType()); + return forRepository(type); + } + /* * (non-Javadoc) * @see org.springframework.data.rest.core.config.EntityLookupRegistrar#forRepository(java.lang.Class) @@ -65,6 +77,18 @@ class EntityLookupConfiguration implements EntityLookupRegistrar { return new MappingBuilder(type); } + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.config.EntityLookupRegistrar#forValueRepository(java.lang.Class, org.springframework.core.convert.converter.Converter, org.springframework.data.rest.core.config.EntityLookupRegistrar.LookupRegistrar.Lookup) + */ + @Override + public > EntityLookupRegistrar forValueRepository( + Class type, Converter identifierMapping, Lookup lookup) { + + this.lookupTypes.add(AbstractRepositoryMetadata.getMetadata(type).getDomainType()); + return forRepository(type, identifierMapping, lookup); + } + /** * Custom builder implementation to back {@link LookupRegistrar} and {@link IdMappingRegistrar}. * @@ -136,6 +160,10 @@ class EntityLookupConfiguration implements EntityLookupRegistrar { return lookups; } + public boolean isLookupType(Class type) { + return this.lookupTypes.contains(type); + } + /** * An {@link EntityLookup} backed by a repository instance and a {@link LookupInformation}. * diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/EntityLookupRegistrar.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/EntityLookupRegistrar.java index cea2b4a3d..2cec18c28 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/EntityLookupRegistrar.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/EntityLookupRegistrar.java @@ -38,15 +38,7 @@ public interface EntityLookupRegistrar { */ > IdMappingRegistrar forRepository(Class type); - /** - * Registers an {@link EntityLookup} for the given repository type, identifier mapping and lookup operation. - * - * @param type must not be {@literal null}. - * @param identifierMapping must not be {@literal null}. - * @param lookup must not be {@literal null}. - */ - > EntityLookupRegistrar forRepository(Class type, - Converter identifierMapping, Lookup lookup); + > IdMappingRegistrar forValueRepository(Class type); interface IdMappingRegistrar> { @@ -59,6 +51,19 @@ public interface EntityLookupRegistrar { LookupRegistrar withIdMapping(Converter mapping); } + /** + * Registers an {@link EntityLookup} for the given repository type, identifier mapping and lookup operation. + * + * @param type must not be {@literal null}. + * @param identifierMapping must not be {@literal null}. + * @param lookup must not be {@literal null}. + */ + > EntityLookupRegistrar forRepository(Class type, + Converter identifierMapping, Lookup lookup); + + > EntityLookupRegistrar forValueRepository(Class type, + Converter identifierMapping, Lookup lookup); + interface LookupRegistrar> { /** diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java index 97c785dfe..406a834fb 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java @@ -61,6 +61,7 @@ public class RepositoryRestConfiguration { private final ProjectionDefinitionConfiguration projectionConfiguration; private final MetadataConfiguration metadataConfiguration; private final EntityLookupConfiguration entityLookupConfiguration; + private final List> valueTypes = new ArrayList>(); private final EnumTranslationConfiguration enumTranslationConfiguration; private boolean enableEnumTranslation = false; @@ -555,7 +556,7 @@ public class RepositoryRestConfiguration { * @return the {@link EntityLookupRegistrar} to build custom {@link EntityLookup}s. * @since 2.5 */ - public EntityLookupRegistrar withCustomEntityLookup() { + public EntityLookupRegistrar withEntityLookup() { return entityLookupConfiguration; } @@ -571,4 +572,8 @@ public class RepositoryRestConfiguration { return entityLookupConfiguration.getEntityLookups(repositories); } + + public boolean isLookupType(Class type) { + return this.entityLookupConfiguration.isLookupType(type); + } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/EmbeddedResourcesAssembler.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/EmbeddedResourcesAssembler.java new file mode 100644 index 000000000..e87ba4ea0 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/EmbeddedResourcesAssembler.java @@ -0,0 +1,119 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.SimpleAssociationHandler; +import org.springframework.data.mapping.context.PersistentEntities; +import org.springframework.data.rest.core.mapping.ResourceMetadata; +import org.springframework.data.rest.webmvc.mapping.AssociationLinks; +import org.springframework.data.rest.webmvc.support.ExcerptProjector; +import org.springframework.hateoas.core.EmbeddedWrapper; +import org.springframework.hateoas.core.EmbeddedWrappers; +import org.springframework.util.Assert; + +/** + * @author Oliver Gierke + */ +@RequiredArgsConstructor +public class EmbeddedResourcesAssembler { + + private final @NonNull PersistentEntities entities; + private final @NonNull AssociationLinks associations; + private final @NonNull ExcerptProjector projector; + private final @NonNull EmbeddedWrappers wrappers = new EmbeddedWrappers(false); + + /** + * Returns the embedded resources to render. This will add an {@link RelatedResource} for linkable associations if + * they have an excerpt projection registered. + * + * @param instance must not be {@literal null}. + * @return + */ + public Iterable getEmbeddedResources(Object instance) { + + Assert.notNull(instance, "Entity instance must not be null!"); + + PersistentEntity entity = entities.getPersistentEntity(instance.getClass()); + + final List associationProjections = new ArrayList(); + final PersistentPropertyAccessor accessor = entity.getPropertyAccessor(instance); + final ResourceMetadata metadata = associations.getMetadataFor(entity.getType()); + + entity.doWithAssociations(new SimpleAssociationHandler() { + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.SimpleAssociationHandler#doWithAssociation(org.springframework.data.mapping.Association) + */ + @Override + public void doWithAssociation(Association> association) { + + PersistentProperty property = association.getInverse(); + + if (!associations.isLinkableAssociation(property)) { + return; + } + + if (!projector.hasExcerptProjection(property.getActualType())) { + return; + } + + Object value = accessor.getProperty(association.getInverse()); + + if (value == null) { + return; + } + + String rel = metadata.getMappingFor(property).getRel(); + + if (value instanceof Collection) { + + Collection collection = (Collection) value; + + if (collection.isEmpty()) { + return; + } + + List nestedCollection = new ArrayList(); + + for (Object element : collection) { + if (element != null) { + nestedCollection.add(projector.projectExcerpt(element)); + } + } + + associationProjections.add(wrappers.wrap(nestedCollection, rel)); + + } else { + associationProjections.add(wrappers.wrap(projector.projectExcerpt(value), rel)); + } + } + }); + + return associationProjections; + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResource.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResource.java index f7960634d..a88462beb 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResource.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResource.java @@ -15,6 +15,8 @@ */ package org.springframework.data.rest.webmvc; +import lombok.Getter; + import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -43,7 +45,15 @@ public class PersistentEntityResource extends Resource { private final PersistentEntity entity; private final Iterable embeddeds; - private final boolean isNew; + + /** + * Returns whether the content of the resource is a new entity about to be created. Used to distinguish between + * creation and updates for incoming requests. + * + * @return + */ + private final @Getter boolean isNew; + private final @Getter boolean nested; /** * Creates a new {@link PersistentEntityResource} for the given {@link PersistentEntity}, content, embedded @@ -55,7 +65,7 @@ public class PersistentEntityResource extends Resource { * @param embeddeds can be {@literal null}. */ private PersistentEntityResource(PersistentEntity entity, Object content, Iterable links, - Iterable embeddeds, boolean isNew) { + Iterable embeddeds, boolean isNew, boolean nested) { super(content, links); @@ -64,6 +74,7 @@ public class PersistentEntityResource extends Resource { this.entity = entity; this.embeddeds = embeddeds == null ? NO_EMBEDDEDS : embeddeds; this.isNew = isNew; + this.nested = nested; } /** @@ -93,16 +104,6 @@ public class PersistentEntityResource extends Resource { return embeddeds; } - /** - * Returns whether the content of the resource is a new entity about to be created. Used to distinguish between - * creation and updates for incoming requests. - * - * @return - */ - public boolean isNew() { - return isNew; - } - /** * Creates a new {@link Builder} to create {@link PersistentEntityResource}s eventually. * @@ -169,13 +170,21 @@ public class PersistentEntityResource extends Resource { return this; } + public Builder withLinks(List links) { + + Assert.notNull(links, "Links must not be null!"); + + this.links.addAll(links); + return this; + } + /** * Finally creates the {@link PersistentEntityResource} instance. * * @return */ public PersistentEntityResource build() { - return new PersistentEntityResource(entity, content, links, embeddeds, false); + return new PersistentEntityResource(entity, content, links, embeddeds, false, false); } /** @@ -185,7 +194,11 @@ public class PersistentEntityResource extends Resource { * @return */ public PersistentEntityResource forCreation() { - return new PersistentEntityResource(entity, content, links, embeddeds, true); + return new PersistentEntityResource(entity, content, links, embeddeds, true, false); + } + + public PersistentEntityResource buildNested() { + return new PersistentEntityResource(entity, content, links, embeddeds, false, true); } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResourceAssembler.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResourceAssembler.java index 28c84185a..9787a5a4b 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResourceAssembler.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResourceAssembler.java @@ -15,18 +15,11 @@ */ package org.springframework.data.rest.webmvc; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; -import org.springframework.data.mapping.Association; import org.springframework.data.mapping.PersistentEntity; -import org.springframework.data.mapping.PersistentProperty; -import org.springframework.data.mapping.PersistentPropertyAccessor; -import org.springframework.data.mapping.SimpleAssociationHandler; import org.springframework.data.mapping.context.PersistentEntities; -import org.springframework.data.rest.core.mapping.ResourceMappings; -import org.springframework.data.rest.core.mapping.ResourceMetadata; import org.springframework.data.rest.core.support.SelfLinkProvider; import org.springframework.data.rest.webmvc.PersistentEntityResource.Builder; import org.springframework.data.rest.webmvc.mapping.AssociationLinks; @@ -42,35 +35,14 @@ import org.springframework.util.Assert; * * @author Oliver Gierke */ +@RequiredArgsConstructor public class PersistentEntityResourceAssembler implements ResourceAssembler { - private final PersistentEntities entities; - private final Projector projector; - private final ResourceMappings mappings; - private final EmbeddedWrappers wrappers = new EmbeddedWrappers(false); - private final SelfLinkProvider linkProvider; - - /** - * Creates a new {@link PersistentEntityResourceAssembler}. - * - * @param entities must not be {@literal null}. - * @param linkProvider must not be {@literal null}. - * @param projector must not be {@literal null}. - * @param mappings must not be {@literal null}. - */ - public PersistentEntityResourceAssembler(PersistentEntities entities, SelfLinkProvider linkProvider, - Projector projector, ResourceMappings mappings) { - - Assert.notNull(entities, "PersistentEntities must not be null!"); - Assert.notNull(linkProvider, "SelfLinkProvider must not be null!"); - Assert.notNull(projector, "PersistentEntityProjector must not be be null!"); - Assert.notNull(mappings, "ResourceMappings must not be null!"); - - this.entities = entities; - this.linkProvider = linkProvider; - this.projector = projector; - this.mappings = mappings; - } + private final @NonNull PersistentEntities entities; + private final @NonNull Projector projector; + private final @NonNull AssociationLinks associations; + private final @NonNull SelfLinkProvider linkProvider; + private final @NonNull EmbeddedWrappers wrappers = new EmbeddedWrappers(false); /* * (non-Javadoc) @@ -113,68 +85,7 @@ public class PersistentEntityResourceAssembler implements ResourceAssembler getEmbeddedResources(Object instance) { - - Assert.notNull(instance, "Entity instance must not be null!"); - - PersistentEntity entity = entities.getPersistentEntity(instance.getClass()); - - final List associationProjections = new ArrayList(); - final PersistentPropertyAccessor accessor = entity.getPropertyAccessor(instance); - final AssociationLinks associationLinks = new AssociationLinks(mappings); - final ResourceMetadata metadata = mappings.getMetadataFor(entity.getType()); - - entity.doWithAssociations(new SimpleAssociationHandler() { - - /* - * (non-Javadoc) - * @see org.springframework.data.mapping.SimpleAssociationHandler#doWithAssociation(org.springframework.data.mapping.Association) - */ - @Override - public void doWithAssociation(Association> association) { - - PersistentProperty property = association.getInverse(); - - if (!associationLinks.isLinkableAssociation(property)) { - return; - } - - if (!projector.hasExcerptProjection(property.getActualType())) { - return; - } - - Object value = accessor.getProperty(association.getInverse()); - - if (value == null) { - return; - } - - String rel = metadata.getMappingFor(property).getRel(); - - if (value instanceof Collection) { - - Collection collection = (Collection) value; - - if (collection.isEmpty()) { - return; - } - - List nestedCollection = new ArrayList(); - - for (Object element : collection) { - if (element != null) { - nestedCollection.add(projector.projectExcerpt(element)); - } - } - - associationProjections.add(wrappers.wrap(nestedCollection, rel)); - - } else { - associationProjections.add(wrappers.wrap(projector.projectExcerpt(value), rel)); - } - } - }); - - return associationProjections; + return new EmbeddedResourcesAssembler(entities, associations, projector).getEmbeddedResources(instance); } /** diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerAdapter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerAdapter.java index b0bd89351..ad70cd53f 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerAdapter.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerAdapter.java @@ -19,7 +19,6 @@ import java.util.List; import org.springframework.core.Ordered; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.hateoas.ResourceProcessor; import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; @@ -38,15 +37,15 @@ public class RepositoryRestHandlerAdapter extends ResourceProcessorInvokingHandl /** * Creates a new {@link RepositoryRestHandlerAdapter} using the given {@link HandlerMethodArgumentResolver} and - * {@link ResourceProcessor}s. + * {@link ResourceProcessorInvoker}. * * @param argumentResolvers must not be {@literal null}. - * @param resourceProcessors must not be {@literal null}. + * @param invoker must not be {@literal null}. */ public RepositoryRestHandlerAdapter(List argumentResolvers, - List> resourceProcessors) { + ResourceProcessorInvoker invoker) { - super(resourceProcessors); + super(invoker); this.argumentResolvers = argumentResolvers; } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceProcessorHandlerMethodReturnValueHandler.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceProcessorHandlerMethodReturnValueHandler.java index 5aa9e8865..3e5e172f1 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceProcessorHandlerMethodReturnValueHandler.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceProcessorHandlerMethodReturnValueHandler.java @@ -15,26 +15,20 @@ */ package org.springframework.data.rest.webmvc; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; import org.springframework.core.MethodParameter; -import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceProcessor; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.Resources; -import org.springframework.hateoas.core.EmbeddedWrapper; import org.springframework.hateoas.mvc.HeaderLinksResponseEntity; import org.springframework.http.HttpEntity; import org.springframework.http.ResponseEntity; -import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; @@ -46,59 +40,24 @@ import org.springframework.web.method.support.ModelAndViewContainer; * * @author Oliver Gierke */ +@RequiredArgsConstructor public class ResourceProcessorHandlerMethodReturnValueHandler implements HandlerMethodReturnValueHandler { - private static final ResolvableType RESOURCE_TYPE = ResolvableType.forClass(Resource.class); - private static final ResolvableType RESOURCES_TYPE = ResolvableType.forClass(Resources.class); + static final ResolvableType RESOURCE_TYPE = ResolvableType.forClass(Resource.class); + static final ResolvableType RESOURCES_TYPE = ResolvableType.forClass(Resources.class); private static final ResolvableType HTTP_ENTITY_TYPE = ResolvableType.forClass(HttpEntity.class); - private static final Field CONTENT_FIELD = ReflectionUtils.findField(Resources.class, "content"); + static final Field CONTENT_FIELD = ReflectionUtils.findField(Resources.class, "content"); static { ReflectionUtils.makeAccessible(CONTENT_FIELD); } - private final HandlerMethodReturnValueHandler delegate; - private final List processors; + private final @NonNull HandlerMethodReturnValueHandler delegate; + private final @NonNull ResourceProcessorInvoker invoker; private boolean rootLinksAsHeaders = false; - /** - * Creates a new {@link ResourceProcessorHandlerMethodReturnValueHandler} using the given delegate to eventually - * delegate calls to {@link #handleReturnValue(Object, MethodParameter, ModelAndViewContainer, NativeWebRequest)} to. - * Will consider the given {@link ResourceProcessor} to post-process the controller methods return value to before - * invoking the delegate. - * - * @param delegate the {@link HandlerMethodReturnValueHandler} to evenually delegate calls to, must not be - * {@literal null}. - * @param processors the {@link ResourceProcessor}s to be considered, must not be {@literal null}. - */ - public ResourceProcessorHandlerMethodReturnValueHandler(HandlerMethodReturnValueHandler delegate, - List> processors) { - - Assert.notNull(delegate, "Delegate must not be null!"); - Assert.notNull(processors, "ResourceProcessors must not be null!"); - - this.delegate = delegate; - this.processors = new ArrayList(); - - for (ResourceProcessor processor : processors) { - - ResolvableType processorType = ResolvableType.forClass(ResourceProcessor.class, processor.getClass()); - Class rawType = processorType.getGeneric(0).resolve(); - - if (Resource.class.isAssignableFrom(rawType)) { - this.processors.add(new ResourceProcessorWrapper(processor)); - } else if (Resources.class.isAssignableFrom(rawType)) { - this.processors.add(new ResourcesProcessorWrapper(processor)); - } else { - this.processors.add(new DefaultProcessorWrapper(processor)); - } - } - - Collections.sort(this.processors, AnnotationAwareOrderComparator.INSTANCE); - } - /** * @param rootLinksAsHeaders the rootLinksAsHeaders to set */ @@ -150,53 +109,10 @@ public class ResourceProcessorHandlerMethodReturnValueHandler implements Handler targetType = returnValueType; } - // For Resources implementations, process elements first - if (RESOURCES_TYPE.isAssignableFrom(targetType)) { - - Resources resources = (Resources) value; - ResolvableType elementTargetType = ResolvableType.forClass(Resources.class, targetType.getRawClass()) - .getGeneric(0); - List result = new ArrayList(resources.getContent().size()); - - for (Object element : resources) { - - ResolvableType elementType = ResolvableType.forClass(element.getClass()); - - if (!getRawType(elementTargetType).equals(elementType.getRawClass())) { - elementTargetType = elementType; - } - - result.add(invokeProcessorsFor(element, elementTargetType)); - } - - ReflectionUtils.setField(CONTENT_FIELD, resources, result); - } - - ResourceSupport result = (ResourceSupport) invokeProcessorsFor(value, targetType); + ResourceSupport result = invoker.invokeProcessorsFor((ResourceSupport) value, targetType); delegate.handleReturnValue(rewrapResult(result, returnValue), returnType, mavContainer, webRequest); } - /** - * Invokes all registered {@link ResourceProcessor}s registered for the given {@link ResolvableType}. - * - * @param value the object to process - * @param type - * @return - */ - private Object invokeProcessorsFor(Object value, ResolvableType type) { - - Object currentValue = value; - - // Process actual value - for (ProcessorWrapper wrapper : this.processors) { - if (wrapper.supports(type, currentValue)) { - currentValue = wrapper.invokeProcessor(currentValue); - } - } - - return currentValue; - } - /** * Re-wraps the result of the post-processing work into an {@link HttpEntity} or {@link ResponseEntity} if the * original value was one of those two types. Copies headers and status code from the original value but uses the new @@ -229,267 +145,9 @@ public class ResourceProcessorHandlerMethodReturnValueHandler implements Handler return rootLinksAsHeaders ? HeaderLinksResponseEntity.wrap(entity) : entity; } - private static boolean isRawTypeAssignable(ResolvableType left, Class right) { - return getRawType(left).isAssignableFrom(right); - } - private static Class getRawType(ResolvableType type) { Class rawType = type.getRawClass(); return rawType == null ? Object.class : rawType; } - - private static ResolvableType findGenericType(ResolvableType source, Class type) { - - Class rawType = getRawType(source); - - if (Object.class.equals(rawType)) { - return null; - } - - if (rawType.equals(type)) { - return source; - } - - return findGenericType(source.getSuperType(), type); - } - - /** - * Interface to unify interaction with {@link ResourceProcessor}s. The {@link Ordered} rank should be determined by - * the underlying processor. - * - * @author Oliver Gierke - */ - private interface ProcessorWrapper extends Ordered { - - /** - * Returns whether the underlying processor supports the given {@link ResolvableType}. It might also additionally - * inspect the object that would eventually be handed to the processor. - * - * @param type the type of object to be post processed, will never be {@literal null}. - * @param value the object that would be passed into the processor eventually, can be {@literal null}. - * @return - */ - boolean supports(ResolvableType type, Object value); - - /** - * Performs the actual invocation of the processor. Implementations can be sure - * {@link #supports(ResolvableType, Object)} has been called before and returned {@literal true}. - * - * @param object - */ - Object invokeProcessor(Object object); - } - - /** - * Default implementation of {@link ProcessorWrapper} to generically deal with {@link ResourceSupport} types. - * - * @author Oliver Gierke - */ - private static class DefaultProcessorWrapper implements ProcessorWrapper { - - private final ResourceProcessor processor; - private final ResolvableType targetType; - - /** - * Creates a new {@link DefaultProcessorWrapper} with the given {@link ResourceProcessor}. - * - * @param processor must not be {@literal null}. - */ - public DefaultProcessorWrapper(ResourceProcessor processor) { - - Assert.notNull(processor); - - this.processor = processor; - this.targetType = ResolvableType.forClass(ResourceProcessor.class, processor.getClass()).getGeneric(0); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.rest.webmvc.ResourceProcessorHandlerMethodReturnValueHandler.ProcessorWrapper#supports(org.springframework.core.ResolvableType, java.lang.Object) - */ - @Override - public boolean supports(ResolvableType type, Object value) { - return isRawTypeAssignable(targetType, getRawType(type)); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.rest.webmvc.ResourceProcessorHandlerMethodReturnValueHandler.PostProcessorWrapper#invokeProcessor(java.lang.Object) - */ - @Override - @SuppressWarnings("unchecked") - public Object invokeProcessor(Object object) { - return ((ResourceProcessor) processor).process((ResourceSupport) object); - } - - /* - * (non-Javadoc) - * @see org.springframework.core.Ordered#getOrder() - */ - @Override - public int getOrder() { - return CustomOrderAwareComparator.INSTANCE.getOrder(processor); - } - - /** - * Returns the target type the underlying {@link ResourceProcessor} wants to get invoked for. - * - * @return the targetType - */ - public ResolvableType getTargetType() { - return targetType; - } - } - - /** - * {@link ProcessorWrapper} to deal with {@link ResourceProcessor}s for {@link Resource}s. Will fall back to peeking - * into the {@link Resource}'s content for type resolution. - * - * @author Oliver Gierke - */ - private static class ResourceProcessorWrapper extends DefaultProcessorWrapper { - - /** - * Creates a new {@link ResourceProcessorWrapper} for the given {@link ResourceProcessor}. - * - * @param processor must not be {@literal null}. - */ - public ResourceProcessorWrapper(ResourceProcessor processor) { - super(processor); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.rest.webmvc.ResourceProcessorHandlerMethodReturnValueHandler.DefaultProcessorWrapper#supports(org.springframework.core.ResolvableType, java.lang.Object) - */ - @Override - public boolean supports(ResolvableType type, Object value) { - - if (!RESOURCE_TYPE.isAssignableFrom(type)) { - return false; - } - - return super.supports(type, value) && isValueTypeMatch((Resource) value, getTargetType()); - } - - /** - * Returns whether the given {@link Resource} matches the given target {@link ResolvableType}. We inspect the - * {@link Resource}'s value to determine the match. - * - * @param resource - * @param target must not be {@literal null}. - * @return whether the given {@link Resource} can be assigned to the given target {@link ResolvableType} - */ - private static boolean isValueTypeMatch(Resource resource, ResolvableType target) { - - if (resource == null || !isRawTypeAssignable(target, resource.getClass())) { - return false; - } - - Object content = resource.getContent(); - - if (content == null) { - return false; - } - - ResolvableType type = findGenericType(target, Resource.class); - return type != null && type.getGeneric(0).isAssignableFrom(ResolvableType.forClass(content.getClass())); - } - } - - /** - * {@link ProcessorWrapper} for {@link ResourceProcessor}s targeting {@link Resources}. Will peek into the content of - * the {@link Resources} for type matching decisions if needed. - * - * @author Oliver Gierke - */ - static class ResourcesProcessorWrapper extends DefaultProcessorWrapper { - - /** - * Creates a new {@link ResourcesProcessorWrapper} for the given {@link ResourceProcessor}. - * - * @param processor must not be {@literal null}. - */ - public ResourcesProcessorWrapper(ResourceProcessor processor) { - super(processor); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.rest.webmvc.ResourceProcessorHandlerMethodReturnValueHandler.DefaultProcessorWrapper#supports(org.springframework.core.ResolvableType, java.lang.Object) - */ - @Override - public boolean supports(ResolvableType type, Object value) { - - if (!RESOURCES_TYPE.isAssignableFrom(type)) { - return false; - } - - return super.supports(type, value) && isValueTypeMatch((Resources) value, getTargetType()); - } - - /** - * Returns whether the given {@link Resources} instance matches the given {@link ResolvableType}. We predict this by - * inspecting the first element of the content of the {@link Resources}. - * - * @param resources the {@link Resources} to inspect. - * @param target that target {@link ResolvableType}. - * @return - */ - static boolean isValueTypeMatch(Resources resources, ResolvableType target) { - - if (resources == null) { - return false; - } - - Collection content = resources.getContent(); - - if (content.isEmpty()) { - return false; - } - - ResolvableType superType = null; - - for (Class resourcesType : Arrays.> asList(resources.getClass(), Resources.class)) { - - superType = ResolvableType.forClass(resourcesType, getRawType(target)); - - if (superType != null) { - break; - } - } - - if (superType == null) { - return false; - } - - Object element = content.iterator().next(); - ResolvableType resourceType = superType.getGeneric(0); - - if (element instanceof Resource) { - return ResourceProcessorWrapper.isValueTypeMatch((Resource) element, resourceType); - } else if (element instanceof EmbeddedWrapper) { - return isRawTypeAssignable(resourceType, ((EmbeddedWrapper) element).getRelTargetType()); - } - - return false; - } - } - - /** - * Helper extension of {@link AnnotationAwareOrderComparator} to make {@link #getOrder(Object)} public to allow it - * being used in a standalone fashion. - * - * @author Oliver Gierke - */ - private static class CustomOrderAwareComparator extends AnnotationAwareOrderComparator { - - public static CustomOrderAwareComparator INSTANCE = new CustomOrderAwareComparator(); - - @Override - protected int getOrder(Object obj) { - return super.getOrder(obj); - } - } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceProcessorInvoker.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceProcessorInvoker.java new file mode 100644 index 000000000..0dceb8a02 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceProcessorInvoker.java @@ -0,0 +1,410 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.springframework.core.Ordered; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.hateoas.Resource; +import org.springframework.hateoas.ResourceProcessor; +import org.springframework.hateoas.ResourceSupport; +import org.springframework.hateoas.Resources; +import org.springframework.hateoas.core.EmbeddedWrapper; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +/** + * Component to easily invoke all {@link ResourceProcessor} instances registered for values of type + * {@link ResourceSupport}. + * + * @author Oliver Gierke + * @since 2.5 + */ +public class ResourceProcessorInvoker { + + private final List processors; + + /** + * Creates a new {@link ResourceProcessorInvoker} to consider the given {@link ResourceProcessor} to post-process the + * controller methods return value to before invoking the delegate. + * + * @param processors the {@link ResourceProcessor}s to be considered, must not be {@literal null}. + */ + public ResourceProcessorInvoker(Collection> processors) { + + Assert.notNull(processors, "ResourceProcessors must not be null!"); + + this.processors = new ArrayList(); + + for (ResourceProcessor processor : processors) { + + ResolvableType processorType = ResolvableType.forClass(ResourceProcessor.class, processor.getClass()); + Class rawType = processorType.getGeneric(0).resolve(); + + if (Resource.class.isAssignableFrom(rawType)) { + this.processors.add(new ResourceProcessorWrapper(processor)); + } else if (Resources.class.isAssignableFrom(rawType)) { + this.processors.add(new ResourcesProcessorWrapper(processor)); + } else { + this.processors.add(new DefaultProcessorWrapper(processor)); + } + } + + Collections.sort(this.processors, AnnotationAwareOrderComparator.INSTANCE); + } + + /** + * Invokes all {@link ResourceProcessor} instances registered for the type of the given value. + * + * @param value must not be {@literal null}. + * @return + */ + public T invokeProcessorsFor(T value) { + + Assert.notNull(value, "Value must not be null!"); + + return invokeProcessorsFor(value, ResolvableType.forClass(value.getClass())); + } + + /** + * Invokes all {@link ResourceProcessor} instances registered for the type of the given value and reference type. + * + * @param value must not be {@literal null}. + * @param referenceType must not be {@literal null}. + * @return + */ + @SuppressWarnings("unchecked") + public T invokeProcessorsFor(T value, ResolvableType referenceType) { + + Assert.notNull(value, "Value must not be null!"); + Assert.notNull(referenceType, "Reference type must not be null!"); + + // For Resources implementations, process elements first + if (ResourceProcessorHandlerMethodReturnValueHandler.RESOURCES_TYPE.isAssignableFrom(referenceType)) { + + Resources resources = (Resources) value; + ResolvableType elementTargetType = ResolvableType.forClass(Resources.class, referenceType.getRawClass()) + .getGeneric(0); + List result = new ArrayList(resources.getContent().size()); + + for (Object element : resources) { + + ResolvableType elementType = ResolvableType.forClass(element.getClass()); + + if (!getRawType(elementTargetType).equals(elementType.getRawClass())) { + elementTargetType = elementType; + } + + result.add(invokeProcessorsFor(element, elementTargetType)); + } + + ReflectionUtils.setField(ResourceProcessorHandlerMethodReturnValueHandler.CONTENT_FIELD, resources, result); + } + + return (T) invokeProcessorsFor((Object) value, referenceType); + } + + /** + * Invokes all registered {@link ResourceProcessor}s registered for the given {@link ResolvableType}. + * + * @param value the object to process + * @param type + * @return + */ + private Object invokeProcessorsFor(Object value, ResolvableType type) { + + Object currentValue = value; + + // Process actual value + for (ResourceProcessorInvoker.ProcessorWrapper wrapper : this.processors) { + if (wrapper.supports(type, currentValue)) { + currentValue = wrapper.invokeProcessor(currentValue); + } + } + + return currentValue; + } + + private static boolean isRawTypeAssignable(ResolvableType left, Class right) { + return getRawType(left).isAssignableFrom(right); + } + + private static Class getRawType(ResolvableType type) { + + Class rawType = type.getRawClass(); + return rawType == null ? Object.class : rawType; + } + + /** + * Interface to unify interaction with {@link ResourceProcessor}s. The {@link Ordered} rank should be determined by + * the underlying processor. + * + * @author Oliver Gierke + */ + private interface ProcessorWrapper extends Ordered { + + /** + * Returns whether the underlying processor supports the given {@link ResolvableType}. It might also additionally + * inspect the object that would eventually be handed to the processor. + * + * @param type the type of object to be post processed, will never be {@literal null}. + * @param value the object that would be passed into the processor eventually, can be {@literal null}. + * @return + */ + boolean supports(ResolvableType type, Object value); + + /** + * Performs the actual invocation of the processor. Implementations can be sure + * {@link #supports(ResolvableType, Object)} has been called before and returned {@literal true}. + * + * @param object + */ + Object invokeProcessor(Object object); + } + + /** + * Default implementation of {@link ProcessorWrapper} to generically deal with {@link ResourceSupport} types. + * + * @author Oliver Gierke + */ + private static class DefaultProcessorWrapper implements ResourceProcessorInvoker.ProcessorWrapper { + + private final ResourceProcessor processor; + private final ResolvableType targetType; + + /** + * Creates a new {@link DefaultProcessorWrapper} with the given {@link ResourceProcessor}. + * + * @param processor must not be {@literal null}. + */ + public DefaultProcessorWrapper(ResourceProcessor processor) { + + Assert.notNull(processor); + + this.processor = processor; + this.targetType = ResolvableType.forClass(ResourceProcessor.class, processor.getClass()).getGeneric(0); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.webmvc.ResourceProcessorHandlerMethodReturnValueHandler.ProcessorWrapper#supports(org.springframework.core.ResolvableType, java.lang.Object) + */ + @Override + public boolean supports(ResolvableType type, Object value) { + return isRawTypeAssignable(targetType, getRawType(type)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.webmvc.ResourceProcessorHandlerMethodReturnValueHandler.PostProcessorWrapper#invokeProcessor(java.lang.Object) + */ + @Override + @SuppressWarnings("unchecked") + public Object invokeProcessor(Object object) { + return ((ResourceProcessor) processor).process((ResourceSupport) object); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.Ordered#getOrder() + */ + @Override + public int getOrder() { + return CustomOrderAwareComparator.INSTANCE.getOrder(processor); + } + + /** + * Returns the target type the underlying {@link ResourceProcessor} wants to get invoked for. + * + * @return the targetType + */ + public ResolvableType getTargetType() { + return targetType; + } + } + + /** + * {@link ProcessorWrapper} to deal with {@link ResourceProcessor}s for {@link Resource}s. Will fall back to peeking + * into the {@link Resource}'s content for type resolution. + * + * @author Oliver Gierke + */ + private static class ResourceProcessorWrapper extends ResourceProcessorInvoker.DefaultProcessorWrapper { + + /** + * Creates a new {@link ResourceProcessorWrapper} for the given {@link ResourceProcessor}. + * + * @param processor must not be {@literal null}. + */ + public ResourceProcessorWrapper(ResourceProcessor processor) { + super(processor); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.webmvc.ResourceProcessorHandlerMethodReturnValueHandler.DefaultProcessorWrapper#supports(org.springframework.core.ResolvableType, java.lang.Object) + */ + @Override + public boolean supports(ResolvableType type, Object value) { + + if (!ResourceProcessorHandlerMethodReturnValueHandler.RESOURCE_TYPE.isAssignableFrom(type)) { + return false; + } + + return super.supports(type, value) && isValueTypeMatch((Resource) value, getTargetType()); + } + + /** + * Returns whether the given {@link Resource} matches the given target {@link ResolvableType}. We inspect the + * {@link Resource}'s value to determine the match. + * + * @param resource + * @param target must not be {@literal null}. + * @return whether the given {@link Resource} can be assigned to the given target {@link ResolvableType} + */ + private static boolean isValueTypeMatch(Resource resource, ResolvableType target) { + + if (resource == null || !isRawTypeAssignable(target, resource.getClass())) { + return false; + } + + Object content = resource.getContent(); + + if (content == null) { + return false; + } + + ResolvableType type = findGenericType(target, Resource.class); + return type != null && type.getGeneric(0).isAssignableFrom(ResolvableType.forClass(content.getClass())); + } + + private static ResolvableType findGenericType(ResolvableType source, Class type) { + + Class rawType = getRawType(source); + + if (Object.class.equals(rawType)) { + return null; + } + + if (rawType.equals(type)) { + return source; + } + + return findGenericType(source.getSuperType(), type); + } + } + + /** + * {@link ProcessorWrapper} for {@link ResourceProcessor}s targeting {@link Resources}. Will peek into the content of + * the {@link Resources} for type matching decisions if needed. + * + * @author Oliver Gierke + */ + static class ResourcesProcessorWrapper extends ResourceProcessorInvoker.DefaultProcessorWrapper { + + /** + * Creates a new {@link ResourcesProcessorWrapper} for the given {@link ResourceProcessor}. + * + * @param processor must not be {@literal null}. + */ + public ResourcesProcessorWrapper(ResourceProcessor processor) { + super(processor); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.webmvc.ResourceProcessorHandlerMethodReturnValueHandler.DefaultProcessorWrapper#supports(org.springframework.core.ResolvableType, java.lang.Object) + */ + @Override + public boolean supports(ResolvableType type, Object value) { + + if (!ResourceProcessorHandlerMethodReturnValueHandler.RESOURCES_TYPE.isAssignableFrom(type)) { + return false; + } + + return super.supports(type, value) && isValueTypeMatch((Resources) value, getTargetType()); + } + + /** + * Returns whether the given {@link Resources} instance matches the given {@link ResolvableType}. We predict this by + * inspecting the first element of the content of the {@link Resources}. + * + * @param resources the {@link Resources} to inspect. + * @param target that target {@link ResolvableType}. + * @return + */ + static boolean isValueTypeMatch(Resources resources, ResolvableType target) { + + if (resources == null) { + return false; + } + + Collection content = resources.getContent(); + + if (content.isEmpty()) { + return false; + } + + ResolvableType superType = null; + + for (Class resourcesType : Arrays.> asList(resources.getClass(), Resources.class)) { + + superType = ResolvableType.forClass(resourcesType, getRawType(target)); + + if (superType != null) { + break; + } + } + + if (superType == null) { + return false; + } + + Object element = content.iterator().next(); + ResolvableType resourceType = superType.getGeneric(0); + + if (element instanceof Resource) { + return ResourceProcessorWrapper.isValueTypeMatch((Resource) element, resourceType); + } else if (element instanceof EmbeddedWrapper) { + return isRawTypeAssignable(resourceType, ((EmbeddedWrapper) element).getRelTargetType()); + } + + return false; + } + } + + /** + * Helper extension of {@link AnnotationAwareOrderComparator} to make {@link #getOrder(Object)} public to allow it + * being used in a standalone fashion. + * + * @author Oliver Gierke + */ + private static class CustomOrderAwareComparator extends AnnotationAwareOrderComparator { + + public static ResourceProcessorInvoker.CustomOrderAwareComparator INSTANCE = new CustomOrderAwareComparator(); + + @Override + protected int getOrder(Object obj) { + return super.getOrder(obj); + } + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceProcessorInvokingHandlerAdapter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceProcessorInvokingHandlerAdapter.java index ac40b4698..92575a388 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceProcessorInvokingHandlerAdapter.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceProcessorInvokingHandlerAdapter.java @@ -41,21 +41,21 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandl */ public class ResourceProcessorInvokingHandlerAdapter extends RequestMappingHandlerAdapter { - private static final Method RETURN_VALUE_HANDLER_METHOD = ReflectionUtils.findMethod( - ResourceProcessorInvokingHandlerAdapter.class, "getReturnValueHandlers"); + private static final Method RETURN_VALUE_HANDLER_METHOD = ReflectionUtils + .findMethod(ResourceProcessorInvokingHandlerAdapter.class, "getReturnValueHandlers"); - private final List> resourcesProcessors; + private final ResourceProcessorInvoker invoker; /** - * Creates a new {@link ResourceProcessorInvokingHandlerAdapter} with the given {@link ResourceProcessor}s. + * Creates a new {@link ResourceProcessorInvokingHandlerAdapter} with the given {@link ResourceProcessorInvoker}. * - * @param resourcesProcessors must not be {@literal null}. + * @param invoker must not be {@literal null}. */ @Autowired(required = false) - public ResourceProcessorInvokingHandlerAdapter(List> resourcesProcessors) { + public ResourceProcessorInvokingHandlerAdapter(ResourceProcessorInvoker invoker) { - Assert.notNull(resourcesProcessors); - this.resourcesProcessors = resourcesProcessors; + Assert.notNull(invoker); + this.invoker = invoker; } /* @@ -72,7 +72,7 @@ public class ResourceProcessorInvokingHandlerAdapter extends RequestMappingHandl // Set up ResourceProcessingHandlerMethodResolver to delegate to originally configured ones List newHandlers = new ArrayList(); - newHandlers.add(new ResourceProcessorHandlerMethodReturnValueHandler(oldHandlers, resourcesProcessors)); + newHandlers.add(new ResourceProcessorHandlerMethodReturnValueHandler(oldHandlers, invoker)); // Configure the new handler to be used this.setReturnValueHandlers(newHandlers); diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/RootResourceInformationToAlpsDescriptorConverter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/RootResourceInformationToAlpsDescriptorConverter.java index 385643eff..4cf157093 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/RootResourceInformationToAlpsDescriptorConverter.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/RootResourceInformationToAlpsDescriptorConverter.java @@ -17,6 +17,9 @@ package org.springframework.data.rest.webmvc.alps; import static org.springframework.hateoas.alps.Alps.*; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -43,7 +46,6 @@ import org.springframework.data.rest.core.mapping.MethodResourceMapping; import org.springframework.data.rest.core.mapping.ParameterMetadata; import org.springframework.data.rest.core.mapping.ResourceDescription; import org.springframework.data.rest.core.mapping.ResourceMapping; -import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.core.mapping.ResourceMetadata; import org.springframework.data.rest.core.mapping.ResourceType; import org.springframework.data.rest.core.mapping.SimpleResourceDescription; @@ -75,44 +77,19 @@ import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; * @author Oliver Gierke * @author Greg Turnquist */ +@RequiredArgsConstructor public class RootResourceInformationToAlpsDescriptorConverter { private static final List UNDOCUMENTED_METHODS = Arrays.asList(HttpMethod.OPTIONS, HttpMethod.HEAD); - private final Repositories repositories; - private final PersistentEntities persistentEntities; - private final ResourceMappings mappings; - private final EntityLinks entityLinks; - private final MessageSourceAccessor messageSource; - private final RepositoryRestConfiguration configuration; - private final ObjectMapper mapper; - private final EnumTranslator translator; - - /** - * Creates a new {@link RootResourceInformationToAlpsDescriptorConverter} instance. - * - * @param mappings must not be {@literal null}. - * @param repositories must not be {@literal null}. - * @param entities must not be {@literal null}. - * @param entityLinks must not be {@literal null}. - * @param messageSource must not be {@literal null}. - * @param configuration must not be {@literal null}. - * @param mapper must not be {@literal null}. - * @param translator must not be {@literal null}. - */ - public RootResourceInformationToAlpsDescriptorConverter(ResourceMappings mappings, Repositories repositories, - PersistentEntities entities, EntityLinks entityLinks, MessageSourceAccessor messageSource, - RepositoryRestConfiguration configuration, ObjectMapper mapper, EnumTranslator translator) { - - this.mappings = mappings; - this.persistentEntities = entities; - this.repositories = repositories; - this.entityLinks = entityLinks; - this.messageSource = messageSource; - this.configuration = configuration; - this.mapper = mapper; - this.translator = translator; - } + private final @NonNull AssociationLinks associations; + private final @NonNull Repositories repositories; + private final @NonNull PersistentEntities persistentEntities; + private final @NonNull EntityLinks entityLinks; + private final @NonNull MessageSourceAccessor messageSource; + private final @NonNull RepositoryRestConfiguration configuration; + private final @NonNull ObjectMapper mapper; + private final @NonNull EnumTranslator translator; /* * (non-Javadoc) @@ -150,7 +127,7 @@ public class RootResourceInformationToAlpsDescriptorConverter { private Descriptor buildRepresentationDescriptor(Class type) { - ResourceMetadata metadata = mappings.getMetadataFor(type); + ResourceMetadata metadata = associations.getMetadataFor(type); String href = ProfileController.getPath(this.configuration, metadata); @@ -165,7 +142,7 @@ public class RootResourceInformationToAlpsDescriptorConverter { private Descriptor buildCollectionResourceDescriptor(Class type, RootResourceInformation resourceInformation, Descriptor representationDescriptor, HttpMethod method) { - ResourceMetadata metadata = mappings.getMetadataFor(type); + ResourceMetadata metadata = associations.getMetadataFor(type); List nestedDescriptors = new ArrayList(); nestedDescriptors.addAll(getPaginationDescriptors(type, method)); @@ -246,7 +223,7 @@ public class RootResourceInformationToAlpsDescriptorConverter { Descriptor representationDescriptor, HttpMethod method) { PersistentEntity entity = resourceInformation.getPersistentEntity(); - ResourceMetadata metadata = mappings.getMetadataFor(entity.getType()); + ResourceMetadata metadata = associations.getMetadataFor(entity.getType()); return descriptor().// id(prefix(method).concat(metadata.getItemResourceRel())).// @@ -267,7 +244,7 @@ public class RootResourceInformationToAlpsDescriptorConverter { ProjectionDefinitionConfiguration projectionConfiguration = configuration.getProjectionConfiguration(); return projectionConfiguration.hasProjectionFor(type) - ? Arrays.asList(buildProjectionDescriptor(mappings.getMetadataFor(type))) + ? Arrays.asList(buildProjectionDescriptor(associations.getMetadataFor(type))) : Collections. emptyList(); } @@ -316,8 +293,7 @@ public class RootResourceInformationToAlpsDescriptorConverter { final PersistentEntity entity = persistentEntities.getPersistentEntity(type); final List propertyDescriptors = new ArrayList(); final JacksonMetadata jackson = new JacksonMetadata(mapper, type); - final AssociationLinks associationLinks = new AssociationLinks(mappings); - final ResourceMetadata metadata = mappings.getMetadataFor(entity.getType()); + final ResourceMetadata metadata = associations.getMetadataFor(entity.getType()); entity.doWithProperties(new SimplePropertyHandler() { @@ -350,7 +326,7 @@ public class RootResourceInformationToAlpsDescriptorConverter { PersistentProperty property = association.getInverse(); - if (!jackson.isExported(property) || !associationLinks.isLinkableAssociation(property)) { + if (!jackson.isExported(property) || !associations.isLinkableAssociation(property)) { return; } @@ -359,7 +335,7 @@ public class RootResourceInformationToAlpsDescriptorConverter { DescriptorBuilder builder = descriptor().// name(mapping.getRel()).doc(getDocFor(mapping.getDescription())); - ResourceMetadata targetTypeMetadata = mappings.getMetadataFor(property.getActualType()); + ResourceMetadata targetTypeMetadata = associations.getMetadataFor(property.getActualType()); String href = ProfileController.getPath(configuration, targetTypeMetadata) + "#" + getRepresentationDescriptorId(targetTypeMetadata); @@ -379,7 +355,7 @@ public class RootResourceInformationToAlpsDescriptorConverter { private Collection buildSearchResourceDescriptors(PersistentEntity entity) { - ResourceMetadata metadata = mappings.getMetadataFor(entity.getType()); + ResourceMetadata metadata = associations.getMetadataFor(entity.getType()); List descriptors = new ArrayList(); for (MethodResourceMapping methodMapping : metadata.getSearchResourceMappings()) { diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/PersistentEntityResourceAssemblerArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/PersistentEntityResourceAssemblerArgumentResolver.java index 54a447080..256f056d3 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/PersistentEntityResourceAssemblerArgumentResolver.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/PersistentEntityResourceAssemblerArgumentResolver.java @@ -15,17 +15,17 @@ */ package org.springframework.data.rest.webmvc.config; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + import org.springframework.core.MethodParameter; import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.projection.ProjectionFactory; -import org.springframework.data.repository.support.Repositories; -import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.core.projection.ProjectionDefinitions; -import org.springframework.data.rest.core.support.DefaultSelfLinkProvider; import org.springframework.data.rest.core.support.SelfLinkProvider; import org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler; +import org.springframework.data.rest.webmvc.mapping.AssociationLinks; import org.springframework.data.rest.webmvc.support.PersistentEntityProjector; -import org.springframework.util.Assert; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; @@ -36,37 +36,14 @@ import org.springframework.web.method.support.ModelAndViewContainer; * * @author Oliver Gierke */ +@RequiredArgsConstructor public class PersistentEntityResourceAssemblerArgumentResolver implements HandlerMethodArgumentResolver { - private final PersistentEntities entities; - private final SelfLinkProvider linkProvider; - private final ProjectionDefinitions projectionDefinitions; - private final ProjectionFactory projectionFactory; - private final ResourceMappings mappings; - - /** - * Creates a new {@link PersistentEntityResourceAssemblerArgumentResolver} for the given {@link Repositories}, - * {@link DefaultSelfLinkProvider}, {@link ProjectionDefinitions} and {@link ProjectionFactory}. - * - * @param entities must not be {@literal null}. - * @param linkProvider must not be {@literal null}. - * @param projectionDefinitions must not be {@literal null}. - * @param projectionFactory must not be {@literal null}. - */ - public PersistentEntityResourceAssemblerArgumentResolver(PersistentEntities entities, SelfLinkProvider linkProvider, - ProjectionDefinitions projectionDefinitions, ProjectionFactory projectionFactory, ResourceMappings mappings) { - - Assert.notNull(entities, "PersistentEntities must not be null!"); - Assert.notNull(linkProvider, "EntityLinks must not be null!"); - Assert.notNull(projectionDefinitions, "ProjectionDefinitions must not be null!"); - Assert.notNull(projectionFactory, "ProjectionFactory must not be null!"); - - this.entities = entities; - this.linkProvider = linkProvider; - this.projectionDefinitions = projectionDefinitions; - this.projectionFactory = projectionFactory; - this.mappings = mappings; - } + private final @NonNull PersistentEntities entities; + private final @NonNull SelfLinkProvider linkProvider; + private final @NonNull ProjectionDefinitions projectionDefinitions; + private final @NonNull ProjectionFactory projectionFactory; + private final @NonNull AssociationLinks links; /* * (non-Javadoc) @@ -87,8 +64,8 @@ public class PersistentEntityResourceAssemblerArgumentResolver implements Handle String projectionParameter = webRequest.getParameter(projectionDefinitions.getParameterName()); PersistentEntityProjector projector = new PersistentEntityProjector(projectionDefinitions, projectionFactory, - projectionParameter, mappings); + projectionParameter, links.getMappings()); - return new PersistentEntityResourceAssembler(entities, linkProvider, projector, mappings); + return new PersistentEntityResourceAssembler(entities, projector, links, linkProvider); } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/ProjectionDefinitionRegistar.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/ProjectionDefinitionRegistar.java index 0ccf8ab7f..a30e22f17 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/ProjectionDefinitionRegistar.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/ProjectionDefinitionRegistar.java @@ -19,6 +19,8 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.data.rest.core.config.Projection; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.core.mapping.RepositoryResourceMappings; import org.springframework.data.rest.core.mapping.ResourceMappings; @@ -69,7 +71,11 @@ public class ProjectionDefinitionRegistar extends InstantiationAwareBeanPostProc Class projection = resourceMetadata.getExcerptProjection(); if (projection != null) { - config.getObject().getProjectionConfiguration().addProjection(projection); + + Projection annotation = AnnotationUtils.findAnnotation(projection, Projection.class); + Class[] target = annotation == null ? new Class[] { resourceMetadata.getDomainType() } : annotation.types(); + + config.getObject().getProjectionConfiguration().addProjection(projection, target); } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java index a386066c8..98cc87540 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java @@ -40,7 +40,6 @@ import org.springframework.context.annotation.ImportResource; import org.springframework.context.support.MessageSourceAccessor; import org.springframework.context.support.ReloadableResourceBundleMessageSource; import org.springframework.core.Ordered; -import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.core.io.ClassPathResource; @@ -76,11 +75,13 @@ import org.springframework.data.rest.core.support.UnwrappingRepositoryInvokerFac import org.springframework.data.rest.webmvc.BasePathAwareController; import org.springframework.data.rest.webmvc.BasePathAwareHandlerMapping; import org.springframework.data.rest.webmvc.BaseUri; +import org.springframework.data.rest.webmvc.EmbeddedResourcesAssembler; import org.springframework.data.rest.webmvc.ProfileResourceProcessor; import org.springframework.data.rest.webmvc.RepositoryRestController; import org.springframework.data.rest.webmvc.RepositoryRestExceptionHandler; import org.springframework.data.rest.webmvc.RepositoryRestHandlerAdapter; import org.springframework.data.rest.webmvc.RepositoryRestHandlerMapping; +import org.springframework.data.rest.webmvc.ResourceProcessorInvoker; import org.springframework.data.rest.webmvc.RestMediaTypes; import org.springframework.data.rest.webmvc.ServerHttpRequestMethodArgumentResolver; import org.springframework.data.rest.webmvc.alps.AlpsJsonHttpMessageConverter; @@ -91,13 +92,20 @@ import org.springframework.data.rest.webmvc.json.EnumTranslator; import org.springframework.data.rest.webmvc.json.Jackson2DatatypeHelper; import org.springframework.data.rest.webmvc.json.JacksonSerializers; 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.json.PersistentEntityToJsonSchemaConverter; +import org.springframework.data.rest.webmvc.json.PersistentEntityToJsonSchemaConverter.ValueTypeSchemaPropertyCustomizerFactory; +import org.springframework.data.rest.webmvc.mapping.AssociationLinks; +import org.springframework.data.rest.webmvc.mapping.LinkCollector; import org.springframework.data.rest.webmvc.spi.BackendIdConverter; import org.springframework.data.rest.webmvc.spi.BackendIdConverter.DefaultIdConverter; import org.springframework.data.rest.webmvc.support.BackendIdHandlerMethodArgumentResolver; +import org.springframework.data.rest.webmvc.support.DefaultExcerptProjector; import org.springframework.data.rest.webmvc.support.DefaultedPageableHandlerMethodArgumentResolver; import org.springframework.data.rest.webmvc.support.DelegatingHandlerMapping; import org.springframework.data.rest.webmvc.support.ETagArgumentResolver; +import org.springframework.data.rest.webmvc.support.ExcerptProjector; import org.springframework.data.rest.webmvc.support.HttpMethodHandlerMethodArgumentResolver; import org.springframework.data.rest.webmvc.support.JpaHelper; import org.springframework.data.rest.webmvc.support.PagingAndSortingTemplateVariables; @@ -377,7 +385,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon return new PersistentEntityResourceHandlerMethodArgumentResolver(defaultMessageConverters(), repoRequestArgumentResolver(), backendIdHandlerMethodArgumentResolver(), - new DomainObjectReader(persistentEntities(), resourceMappings())); + new DomainObjectReader(persistentEntities(), associationLinks())); } /** @@ -387,8 +395,10 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon */ @Bean public PersistentEntityToJsonSchemaConverter jsonSchemaConverter() { - return new PersistentEntityToJsonSchemaConverter(persistentEntities(), resourceMappings(), - resourceDescriptionMessageSourceAccessor(), objectMapper(), config()); + + return new PersistentEntityToJsonSchemaConverter(persistentEntities(), associationLinks(), + resourceDescriptionMessageSourceAccessor(), objectMapper(), config(), + new ValueTypeSchemaPropertyCustomizerFactory(repositoryInvokerFactory(defaultConversionService()))); } /** @@ -512,16 +522,9 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon return new UriListHttpMessageConverter(); } - /** - * Special {@link org.springframework.web.servlet.HandlerAdapter} that only recognizes handler methods defined in the - * provided controller classes. - * - * @param resourceProcessors {@link ResourceProcessor}s available in the {@link ApplicationContext}. - * @return - */ @Bean @SuppressWarnings("rawtypes") - public RequestMappingHandlerAdapter repositoryExporterHandlerAdapter() { + public ResourceProcessorInvoker resourceProcessorInvoker() { Collection beans = applicationContext.getBeansOfType(ResourceProcessor.class, false, false) .values(); @@ -531,14 +534,25 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon processors.add(bean); } - AnnotationAwareOrderComparator.sort(processors); + return new ResourceProcessorInvoker(processors); + } + + /** + * Special {@link org.springframework.web.servlet.HandlerAdapter} that only recognizes handler methods defined in the + * provided controller classes. + * + * @param resourceProcessors {@link ResourceProcessor}s available in the {@link ApplicationContext}. + * @return + */ + @Bean + public RequestMappingHandlerAdapter repositoryExporterHandlerAdapter() { // Forward conversion service to handler adapter ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer(); initializer.setConversionService(defaultConversionService()); RepositoryRestHandlerAdapter handlerAdapter = new RepositoryRestHandlerAdapter(defaultMethodArgumentResolvers(), - processors); + resourceProcessorInvoker()); handlerAdapter.setWebBindingInitializer(initializer); handlerAdapter.setMessageConverters(defaultMessageConverters()); @@ -591,15 +605,40 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon protected Module persistentEntityJackson2Module() { PersistentEntities entities = persistentEntities(); + ConversionService conversionService = defaultConversionService(); - return new PersistentEntityJackson2Module(resourceMappings(), entities, config(), - uriToEntityConverter(defaultConversionService()), selfLinkProvider()); + UriToEntityConverter uriToEntityConverter = uriToEntityConverter(conversionService); + RepositoryInvokerFactory repositoryInvokerFactory = repositoryInvokerFactory(conversionService); + + EmbeddedResourcesAssembler assembler = new EmbeddedResourcesAssembler(entities, associationLinks(), + excerptProjector()); + NestedEntitySerializer serializer = new NestedEntitySerializer(entities, assembler, resourceProcessorInvoker()); + LookupObjectSerializer lookupObjectSerializer = new LookupObjectSerializer( + OrderAwarePluginRegistry.create(getEntityLookups())); + + return new PersistentEntityJackson2Module(associationLinks(), entities, uriToEntityConverter, linkCollector(), + repositoryInvokerFactory, serializer, lookupObjectSerializer); + } + + @Bean + protected LinkCollector linkCollector() { + return new LinkCollector(persistentEntities(), selfLinkProvider(), associationLinks()); } protected UriToEntityConverter uriToEntityConverter(ConversionService conversionService) { return new UriToEntityConverter(persistentEntities(), repositoryInvokerFactory(conversionService), repositories()); } + @Bean + public ExcerptProjector excerptProjector() { + + SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory(); + projectionFactory.setBeanFactory(applicationContext); + projectionFactory.setResourceLoader(applicationContext); + + return new DefaultExcerptProjector(projectionFactory, resourceMappings()); + } + /** * Bean for looking up methods annotated with {@link org.springframework.web.bind.annotation.ExceptionHandler}. * @@ -715,6 +754,11 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon return new DefaultSelfLinkProvider(persistentEntities(), entityLinks(), getEntityLookups()); } + @Bean + public AssociationLinks associationLinks() { + return new AssociationLinks(resourceMappings(), config()); + } + protected List> getEntityLookups() { List> lookups = new ArrayList>(); @@ -732,7 +776,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon PersistentEntityResourceAssemblerArgumentResolver peraResolver = new PersistentEntityResourceAssemblerArgumentResolver( persistentEntities(), selfLinkProvider(), config().getProjectionConfiguration(), projectionFactory, - resourceMappings()); + associationLinks()); HateoasPageableHandlerMethodArgumentResolver pageableResolver = pageableResolver(); HandlerMethodArgumentResolver defaultedPageableResolver = new DefaultedPageableHandlerMethodArgumentResolver( @@ -802,7 +846,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon RepositoryRestConfiguration config = config(); ResourceMappings resourceMappings = resourceMappings(); - return new RootResourceInformationToAlpsDescriptorConverter(resourceMappings, repositories, persistentEntities, + return new RootResourceInformationToAlpsDescriptorConverter(associationLinks(), repositories, persistentEntities, entityLinks, messageSourceAccessor, config, objectMapper(), enumTranslator()); } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/DomainObjectReader.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/DomainObjectReader.java index c423e26a3..d48b3a87f 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/DomainObjectReader.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/DomainObjectReader.java @@ -15,6 +15,9 @@ */ package org.springframework.data.rest.webmvc.json; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + import java.io.InputStream; import java.util.HashMap; import java.util.Iterator; @@ -26,7 +29,6 @@ import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.SimplePropertyHandler; import org.springframework.data.mapping.context.PersistentEntities; -import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.webmvc.mapping.AssociationLinks; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.util.Assert; @@ -47,27 +49,12 @@ import com.fasterxml.jackson.databind.node.ObjectNode; * @author Oliver Gierke * @since 2.2 */ +@RequiredArgsConstructor public class DomainObjectReader { - private final PersistentEntities entities; - private final AssociationLinks associationLinks; - private final ClassIntrospector introspector; - - /** - * Creates a new {@link DomainObjectReader} using the given {@link PersistentEntities} and {@link ResourceMappings}. - * - * @param entities must not be {@literal null}. - * @param mappings must not be {@literal null}. - */ - public DomainObjectReader(PersistentEntities entities, ResourceMappings mappings) { - - Assert.notNull(entities, "PersistentEntites must not be null!"); - Assert.notNull(mappings, "ResourceMappings must not be null!"); - - this.entities = entities; - this.associationLinks = new AssociationLinks(mappings); - this.introspector = new BasicClassIntrospector(); - } + private final @NonNull PersistentEntities entities; + private final @NonNull AssociationLinks associationLinks; + private final @NonNull ClassIntrospector introspector = new BasicClassIntrospector(); /** * Reads the given input stream into an {@link ObjectNode} and applies that to the given existing instance. 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 9873e4b15..cebb62a80 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 @@ -15,10 +15,13 @@ */ package org.springframework.data.rest.webmvc.json; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + import java.io.IOException; import java.net.URI; import java.util.ArrayList; -import java.util.Collections; +import java.util.Collection; import java.util.Iterator; import java.util.List; @@ -31,18 +34,23 @@ 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.rest.core.Path; +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.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.ResourceProcessorInvoker; import org.springframework.data.rest.webmvc.mapping.AssociationLinks; -import org.springframework.data.rest.webmvc.mapping.LinkCollectingAssociationHandler; +import org.springframework.data.rest.webmvc.mapping.LinkCollector; import org.springframework.hateoas.Link; import org.springframework.hateoas.Links; import org.springframework.hateoas.Resource; import org.springframework.hateoas.UriTemplate; +import org.springframework.plugin.core.PluginRegistry; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -56,6 +64,7 @@ import com.fasterxml.jackson.databind.BeanDescription; import com.fasterxml.jackson.databind.DeserializationConfig; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializationConfig; import com.fasterxml.jackson.databind.SerializerProvider; @@ -65,13 +74,16 @@ import com.fasterxml.jackson.databind.deser.SettableBeanProperty; import com.fasterxml.jackson.databind.deser.ValueInstantiator; 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.introspect.BeanPropertyDefinition; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.BeanPropertyWriter; import com.fasterxml.jackson.databind.ser.BeanSerializerBuilder; import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; +import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer; import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.fasterxml.jackson.databind.type.CollectionLikeType; import com.fasterxml.jackson.databind.util.NameTransformer; @@ -92,32 +104,31 @@ public class PersistentEntityJackson2Module extends SimpleModule { * Creates a new {@link PersistentEntityJackson2Module} using the given {@link ResourceMappings}, {@link Repositories} * , {@link RepositoryRestConfiguration}, {@link UriToEntityConverter} and {@link SelfLinkProvider}. * - * @param mappings must not be {@literal null}. + * @param associations must not be {@literal null}. * @param entities must not be {@literal null}. * @param config must not be {@literal null}. * @param converter must not be {@literal null}. * @param linkProvider must not be {@literal null}. */ - public PersistentEntityJackson2Module(ResourceMappings mappings, PersistentEntities entities, - RepositoryRestConfiguration config, UriToEntityConverter converter, SelfLinkProvider linkProvider) { + public PersistentEntityJackson2Module(AssociationLinks associations, PersistentEntities entities, + UriToEntityConverter converter, LinkCollector collector, RepositoryInvokerFactory factory, + NestedEntitySerializer serializer, LookupObjectSerializer lookupObjectSerializer) { super(new Version(2, 0, 0, null, "org.springframework.data.rest", "jackson-module")); - Assert.notNull(mappings, "ResourceMappings must not be null!"); + Assert.notNull(associations, "AssociationLinks must not be null!"); Assert.notNull(entities, "Repositories must not be null!"); - Assert.notNull(config, "RepositoryRestConfiguration must not be null!"); Assert.notNull(converter, "UriToEntityConverter must not be null!"); - Assert.notNull(linkProvider, "SelfLinkProvider must not be null!"); - - AssociationLinks associationLinks = new AssociationLinks(mappings); - LinkCollector collector = new LinkCollector(entities, linkProvider, associationLinks); + Assert.notNull(collector, "LinkCollector must not be null!"); addSerializer(new PersistentEntityResourceSerializer(collector)); - addSerializer(new ProjectionSerializer(collector, mappings, false)); + addSerializer(new ProjectionSerializer(collector, associations, false)); addSerializer(new ProjectionResourceContentSerializer(false)); - setSerializerModifier(new AssociationOmittingSerializerModifier(entities, associationLinks, config)); - setDeserializerModifier(new AssociationUriResolvingDeserializerModifier(entities, converter, associationLinks)); + setSerializerModifier( + new AssociationOmittingSerializerModifier(entities, associations, serializer, lookupObjectSerializer)); + setDeserializerModifier( + new AssociationUriResolvingDeserializerModifier(entities, associations, converter, factory)); } /** @@ -136,12 +147,11 @@ public class PersistentEntityJackson2Module extends SimpleModule { * {@link AssociationLinks}. * * @param entities must not be {@literal null}. - * @param links must not be {@literal null}. */ @SuppressWarnings({ "unchecked", "rawtypes" }) private PersistentEntityResourceSerializer(LinkCollector collector) { - super((Class) PersistentEntityResource.class); + super(PersistentEntityResource.class); this.collector = collector; } @@ -154,22 +164,24 @@ public class PersistentEntityJackson2Module extends SimpleModule { public void serialize(final PersistentEntityResource resource, final JsonGenerator jgen, final SerializerProvider provider) throws IOException, JsonGenerationException { - if (LOG.isDebugEnabled()) { - LOG.debug("Serializing PersistentEntity " + resource.getPersistentEntity()); - } + LOG.debug("Serializing PersistentEntity {}.", resource.getPersistentEntity()); Object content = resource.getContent(); + if (hasScalarSerializer(content, provider)) { + provider.defaultSerializeValue(content, jgen); + return; + } + + Links links = getLinks(resource); + if (TargetAware.class.isInstance(content)) { TargetAware targetAware = (TargetAware) content; - Links links = collector.getLinksFor(targetAware.getTarget(), resource.getLinks()); provider.defaultSerializeValue(new ProjectionResource(targetAware, links), jgen); return; } - Links links = collector.getLinksFor(resource.getContent(), resource.getLinks()); - Resource resourceToRender = new Resource(resource.getContent(), links) { @JsonUnwrapped @@ -180,6 +192,24 @@ public class PersistentEntityJackson2Module extends SimpleModule { provider.defaultSerializeValue(resourceToRender, jgen); } + + private Links getLinks(PersistentEntityResource resource) { + + Object source = getLinkSource(resource.getContent()); + + return resource.isNested() ? collector.getLinksForNested(source, resource.getLinks()) + : collector.getLinksFor(source, resource.getLinks()); + } + + private Object getLinkSource(Object object) { + return TargetAware.class.isInstance(object) ? ((TargetAware) object).getTarget() : object; + } + + private static boolean hasScalarSerializer(Object source, SerializerProvider provider) throws JsonMappingException { + + JsonSerializer serializer = provider.findValueSerializer(source.getClass()); + return serializer instanceof ToStringSerializer || serializer instanceof StdScalarSerializer; + } } /** @@ -187,31 +217,13 @@ public class PersistentEntityJackson2Module extends SimpleModule { * * @author Oliver Gierke */ + @RequiredArgsConstructor static class AssociationOmittingSerializerModifier extends BeanSerializerModifier { - private final PersistentEntities entities; - private final RepositoryRestConfiguration configuration; - private final AssociationLinks associationLinks; - - /** - * Creates a new {@link AssociationOmittingSerializerModifier} for the given {@link PersistentEntities}, - * {@link AssociationLinks} and {@link RepositoryRestConfiguration}. - * - * @param entities must not be {@literal null}. - * @param associationLinks must not be {@literal null}. - * @param configuration must not be {@literal null}. - */ - public AssociationOmittingSerializerModifier(PersistentEntities entities, AssociationLinks associationLinks, - RepositoryRestConfiguration configuration) { - - Assert.notNull(entities, "PersistentEntities must not be null!"); - Assert.notNull(associationLinks, "AssociationLinks must not be null!"); - Assert.notNull(configuration, "RepositoryRestConfiguration must not be null!"); - - this.entities = entities; - this.configuration = configuration; - this.associationLinks = associationLinks; - } + private final @NonNull PersistentEntities entities; + private final @NonNull AssociationLinks associations; + private final @NonNull NestedEntitySerializer nestedEntitySerializer; + private final @NonNull LookupObjectSerializer lookupObjectSerializer; /* * (non-Javadoc) @@ -239,14 +251,22 @@ public class PersistentEntityJackson2Module extends SimpleModule { continue; } + if (associations.isLookupType(persistentProperty)) { + + LOG.debug("Assigning lookup object serializer for {}.", persistentProperty); + writer.assignSerializer(lookupObjectSerializer); + result.add(writer); + continue; + } + // Is there a default projection? - if (associationLinks.isLinkableAssociation(persistentProperty)) { + if (associations.isLinkableAssociation(persistentProperty)) { continue; } // Skip ids unless explicitly configured to expose - if (persistentProperty.isIdProperty() && !configuration.isIdExposedFor(entity.getType())) { + if (persistentProperty.isIdProperty() && !associations.isIdExposed(entity)) { continue; } @@ -254,6 +274,13 @@ public class PersistentEntityJackson2Module extends SimpleModule { continue; } + if (persistentProperty.isEntity()) { + + LOG.debug("Assigning nested entity serializer for {}.", persistentProperty); + + writer.assignSerializer(nestedEntitySerializer); + } + result.add(writer); } @@ -284,6 +311,62 @@ public class PersistentEntityJackson2Module extends SimpleModule { } } + /** + * Serializer to wrap values into an {@link Resource} instance and collecting all association links. + * + * @author Oliver Gierke + * @since 2.5 + */ + public static class NestedEntitySerializer extends StdSerializer { + + private static final long serialVersionUID = -2327469118972125954L; + + private final PersistentEntities entities; + private final EmbeddedResourcesAssembler assembler; + private final ResourceProcessorInvoker invoker; + + public NestedEntitySerializer(PersistentEntities entities, EmbeddedResourcesAssembler assembler, + ResourceProcessorInvoker invoker) { + + super(Object.class); + this.entities = entities; + this.assembler = assembler; + this.invoker = invoker; + } + + /* + * (non-Javadoc) + * @see com.fasterxml.jackson.databind.ser.std.StdSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) + */ + @Override + public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException { + + if (value instanceof Collection) { + + Collection source = (Collection) value; + List resources = new ArrayList(); + + for (Object element : source) { + resources.add(toResource(element)); + } + + provider.defaultSerializeValue(resources, gen); + + } else { + provider.defaultSerializeValue(toResource(value), gen); + } + } + + private Resource toResource(Object value) { + + PersistentEntity entity = entities.getPersistentEntity(value.getClass()); + + return invoker.invokeProcessorsFor(PersistentEntityResource.build(value, entity).// + withEmbedded(assembler.getEmbeddedResources(value)).// + buildNested()); + } + } + /** * A {@link BeanDeserializerModifier} that registers a custom {@link UriStringDeserializer} for association properties * of {@link PersistentEntity}s. This allows to submit URIs for those properties in request payloads, so that @@ -291,31 +374,13 @@ public class PersistentEntityJackson2Module extends SimpleModule { * * @author Oliver Gierke */ + @RequiredArgsConstructor public static class AssociationUriResolvingDeserializerModifier extends BeanDeserializerModifier { - private final UriToEntityConverter converter; - private final PersistentEntities repositories; - private final AssociationLinks associationLinks; - - /** - * Creates a new {@link AssociationUriResolvingDeserializerModifier} using the given {@link Repositories}, - * {@link UriToEntityConverter} and {@link AssociationLinks}. - * - * @param repositories must not be {@literal null}. - * @param converter must not be {@literal null}. - * @param mappings must not be {@literal null}. - */ - public AssociationUriResolvingDeserializerModifier(PersistentEntities repositories, UriToEntityConverter converter, - AssociationLinks associationLinks) { - - Assert.notNull(repositories, "Repositories must not be null!"); - Assert.notNull(converter, "UriToEntityConverter must not be null!"); - Assert.notNull(associationLinks, "AssociationLinks must not be null!"); - - this.repositories = repositories; - this.converter = converter; - this.associationLinks = associationLinks; - } + private final @NonNull PersistentEntities entities; + private final @NonNull AssociationLinks associationLinks; + private final @NonNull UriToEntityConverter converter; + private final @NonNull RepositoryInvokerFactory factory; /* * (non-Javadoc) @@ -326,7 +391,7 @@ public class PersistentEntityJackson2Module extends SimpleModule { BeanDeserializerBuilder builder) { Iterator properties = builder.getProperties(); - PersistentEntity entity = repositories.getPersistentEntity(beanDesc.getBeanClass()); + PersistentEntity entity = entities.getPersistentEntity(beanDesc.getBeanClass()); if (entity == null) { return builder; @@ -337,29 +402,42 @@ public class PersistentEntityJackson2Module extends SimpleModule { SettableBeanProperty property = properties.next(); PersistentProperty persistentProperty = entity.getPersistentProperty(property.getName()); + if (associationLinks.isLookupType(persistentProperty)) { + + RepositoryInvokingDeserializer repositoryInvokingDeserializer = new RepositoryInvokingDeserializer(factory, + persistentProperty); + JsonDeserializer deserializer = wrapIfCollection(persistentProperty, repositoryInvokingDeserializer, + config); + + builder.addOrReplaceProperty(property.withValueDeserializer(deserializer), false); + continue; + } + if (!associationLinks.isLinkableAssociation(persistentProperty)) { continue; } UriStringDeserializer uriStringDeserializer = new UriStringDeserializer(persistentProperty, converter); + JsonDeserializer deserializer = wrapIfCollection(persistentProperty, uriStringDeserializer, config); - if (persistentProperty.isCollectionLike()) { - - CollectionLikeType collectionType = config.getTypeFactory() - .constructCollectionLikeType(persistentProperty.getType(), persistentProperty.getActualType()); - CollectionValueInstantiator instantiator = new CollectionValueInstantiator(persistentProperty); - CollectionDeserializer collectionDeserializer = new CollectionDeserializer(collectionType, - uriStringDeserializer, null, instantiator); - - builder.addOrReplaceProperty(property.withValueDeserializer(collectionDeserializer), false); - - } else { - builder.addOrReplaceProperty(property.withValueDeserializer(uriStringDeserializer), false); - } + builder.addOrReplaceProperty(property.withValueDeserializer(deserializer), false); } return builder; } + + private static JsonDeserializer wrapIfCollection(PersistentProperty property, + JsonDeserializer elementDeserializer, DeserializationConfig config) { + + if (!property.isCollectionLike()) { + return elementDeserializer; + } + + CollectionLikeType collectionType = config.getTypeFactory().constructCollectionLikeType(property.getType(), + property.getActualType()); + CollectionValueInstantiator instantiator = new CollectionValueInstantiator(property); + return new CollectionDeserializer(collectionType, elementDeserializer, null, instantiator); + } } /** @@ -434,7 +512,7 @@ public class PersistentEntityJackson2Module extends SimpleModule { static class ProjectionSerializer extends StdSerializer { private final LinkCollector collector; - private final ResourceMappings mappings; + private final AssociationLinks associations; private final boolean unwrapping; /** @@ -445,12 +523,12 @@ public class PersistentEntityJackson2Module extends SimpleModule { * @param mappings must not be {@literal null}. * @param unwrapping */ - private ProjectionSerializer(LinkCollector collector, ResourceMappings mappings, boolean unwrapping) { + private ProjectionSerializer(LinkCollector collector, AssociationLinks mappings, boolean unwrapping) { super(TargetAware.class); this.collector = collector; - this.mappings = mappings; + this.associations = mappings; this.unwrapping = unwrapping; } @@ -463,7 +541,7 @@ public class PersistentEntityJackson2Module extends SimpleModule { throws IOException, JsonGenerationException { Object target = value.getTarget(); - Links links = mappings.getMetadataFor(value.getTargetClass()).isExported() ? collector.getLinksFor(target) + Links links = associations.getMetadataFor(value.getTargetClass()).isExported() ? collector.getLinksFor(target) : new Links(); if (!unwrapping) { @@ -495,7 +573,7 @@ public class PersistentEntityJackson2Module extends SimpleModule { */ @Override public JsonSerializer unwrappingSerializer(NameTransformer unwrapper) { - return new ProjectionSerializer(collector, mappings, true); + return new ProjectionSerializer(collector, associations, true); } } @@ -578,103 +656,6 @@ public class PersistentEntityJackson2Module extends SimpleModule { } } - /** - * A service to collect all standard links that need to be added to a certain object. - * - * @author Oliver Gierke - */ - private static class LinkCollector { - - private final PersistentEntities entities; - private final AssociationLinks associationLinks; - private final SelfLinkProvider links; - - /** - * Creates a new {@link PersistentEntities}, {@link SelfLinkProvider} and {@link AssociationLinks}. - * - * @param entities must not be {@literal null}. - * @param linkProvider must not be {@literal null}. - * @param associationLinks must not be {@literal null}. - */ - public LinkCollector(PersistentEntities entities, SelfLinkProvider linkProvider, - AssociationLinks associationLinks) { - - Assert.notNull(entities, "PersistentEntities must not be null!"); - Assert.notNull(linkProvider, "SelfLinkProvider must not be null!"); - Assert.notNull(associationLinks, "AssociationLinks must not be null!"); - - this.links = linkProvider; - this.entities = entities; - this.associationLinks = associationLinks; - } - - /** - * Returns all {@link Links} for the given object. - * - * @param object must not be {@literal null}. - * @return - */ - public Links getLinksFor(Object object) { - return getLinksFor(object, Collections. emptyList()); - } - - /** - * Returns all {@link Links} for the given object and already existing {@link Link}. - * - * @param object must not be {@literal null}. - * @param existingLinks must not be {@literal null}. - * @return - */ - public Links getLinksFor(Object object, List existingLinks) { - - Assert.notNull(object, "Object must not be null!"); - Assert.notNull(existingLinks, "Existing links must not be null!"); - - PersistentEntity entity = entities.getPersistentEntity(object.getClass()); - - Links links = new Links(existingLinks); - Link selfLink = createSelfLink(object, links); - - if (selfLink == null) { - return links; - } - - Path path = new Path(selfLink.expand().getHref()); - - LinkCollectingAssociationHandler handler = new LinkCollectingAssociationHandler(entities, path, associationLinks); - entity.doWithAssociations(handler); - - List result = new ArrayList(existingLinks); - result.addAll(handler.getLinks()); - - return addSelfLinkIfNecessary(object, result); - } - - private Links addSelfLinkIfNecessary(Object object, List existing) { - - Links result = new Links(existing); - - if (result.hasLink(Link.REL_SELF)) { - return result; - } - - List list = new ArrayList(); - list.add(createSelfLink(object, result)); - list.addAll(existing); - - return new Links(list); - } - - private Link createSelfLink(Object object, Links existing) { - - if (existing.hasLink(Link.REL_SELF)) { - return existing.getLink(Link.REL_SELF); - } - - return links.createSelfLinkFor(object).withSelfRel(); - } - } - /** * {@link ValueInstantiator} to create collection or map instances based on the type of the configured * {@link PersistentProperty}. @@ -720,4 +701,61 @@ public class PersistentEntityJackson2Module extends SimpleModule { : CollectionFactory.createCollection(collectionOrMapType, 0); } } + + private static class RepositoryInvokingDeserializer extends StdScalarDeserializer { + + private static final long serialVersionUID = -3033458643050330913L; + private final RepositoryInvoker invoker; + + private RepositoryInvokingDeserializer(RepositoryInvokerFactory factory, PersistentProperty property) { + + super(property.getActualType()); + this.invoker = factory.getInvokerFor(_valueClass); + } + + /* + * (non-Javadoc) + * @see com.fasterxml.jackson.databind.JsonDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext) + */ + @Override + public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { + return invoker.invokeFindOne(p.getValueAsString()); + } + } + + @RequiredArgsConstructor + public static class LookupObjectSerializer extends ToStringSerializer { + + private static final long serialVersionUID = -3033458643050330913L; + private final PluginRegistry, Class> lookups; + + /* + * (non-Javadoc) + * @see com.fasterxml.jackson.databind.ser.std.ToStringSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) + */ + @Override + public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException { + + if (value instanceof Collection) { + + gen.writeStartArray(); + + for (Object element : (Collection) value) { + gen.writeString(getLookupKey(element)); + } + + gen.writeEndArray(); + + } else { + gen.writeString(getLookupKey(value)); + } + } + + @SuppressWarnings("unchecked") + private String getLookupKey(Object value) { + + EntityLookup lookup = (EntityLookup) lookups.getPluginFor(value.getClass()); + return lookup.getResourceIdentifier(value).toString(); + } + } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverter.java index d214d3ee7..aeab8db67 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverter.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverter.java @@ -15,6 +15,9 @@ */ package org.springframework.data.rest.webmvc.json; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -32,9 +35,11 @@ import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.context.support.MessageSourceAccessor; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.data.domain.Sort; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.context.PersistentEntities; +import org.springframework.data.repository.support.RepositoryInvokerFactory; import org.springframework.data.rest.core.config.JsonSchemaFormat; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.core.mapping.ResourceDescription; @@ -71,11 +76,12 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric private static final TypeInformation STRING_TYPE_INFORMATION = ClassTypeInformation.from(String.class); private final Set convertiblePairs = new HashSet(); - private final ResourceMappings mappings; + private final AssociationLinks associations; private final PersistentEntities entities; private final MessageSourceAccessor accessor; private final ObjectMapper objectMapper; private final RepositoryRestConfiguration configuration; + private final ValueTypeSchemaPropertyCustomizerFactory customizerFactory; /** * Creates a new {@link PersistentEntityToJsonSchemaConverter} for the given {@link PersistentEntities} and @@ -87,20 +93,22 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric * @param objectMapper must not be {@literal null}. * @param configuration must not be {@literal null}. */ - public PersistentEntityToJsonSchemaConverter(PersistentEntities entities, ResourceMappings mappings, - MessageSourceAccessor accessor, ObjectMapper objectMapper, RepositoryRestConfiguration configuration) { + public PersistentEntityToJsonSchemaConverter(PersistentEntities entities, AssociationLinks associations, + MessageSourceAccessor accessor, ObjectMapper objectMapper, RepositoryRestConfiguration configuration, + ValueTypeSchemaPropertyCustomizerFactory customizerFactory) { Assert.notNull(entities, "PersistentEntities must not be null!"); - Assert.notNull(mappings, "ResourceMappings must not be null!"); + Assert.notNull(associations, "AssociationLinks must not be null!"); Assert.notNull(accessor, "MessageSourceAccessor must not be null!"); Assert.notNull(objectMapper, "ObjectMapper must not be null!"); Assert.notNull(configuration, "RepositoryRestConfiguration must not be null!"); this.entities = entities; - this.mappings = mappings; + this.associations = associations; this.accessor = accessor; this.objectMapper = objectMapper; this.configuration = configuration; + this.customizerFactory = customizerFactory; for (TypeInformation domainType : entities.getManagedTypes()) { convertiblePairs.add(new ConvertiblePair(domainType.getType(), JsonSchema.class)); @@ -144,23 +152,22 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric public JsonSchema convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { final PersistentEntity persistentEntity = entities.getPersistentEntity((Class) source); - final ResourceMetadata metadata = mappings.getMetadataFor(persistentEntity.getType()); + final ResourceMetadata metadata = associations.getMappings().getMetadataFor(persistentEntity.getType()); - Definitions descriptors = new Definitions(); + Definitions definitions = new Definitions(); List> propertiesFor = getPropertiesFor(persistentEntity.getType(), metadata, - descriptors); + definitions); String title = resolveMessageWithDefault(new ResolvableType(persistentEntity.getType())); - return new JsonSchema(title, resolveMessage(metadata.getItemResourceDescription()), propertiesFor, descriptors); + return new JsonSchema(title, resolveMessage(metadata.getItemResourceDescription()), propertiesFor, definitions); } private List> getPropertiesFor(Class type, final ResourceMetadata metadata, - final Definitions descriptors) { + final Definitions definitions) { final PersistentEntity entity = entities.getPersistentEntity(type); final JacksonMetadata jackson = new JacksonMetadata(objectMapper, type); - final AssociationLinks associationLinks = new AssociationLinks(mappings); if (entity == null) { return Collections.> emptyList(); @@ -236,15 +243,17 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric continue; } - if (associationLinks.isLinkableAssociation(persistentProperty)) { + if (configuration.isLookupType(persistentProperty.getActualType())) { + registrar.register(property.with(propertyType), actualPropertyType); + } else if (associations.isLinkableAssociation(persistentProperty)) { registrar.register(property.asAssociation(), null); } else { if (persistentProperty.isEntity()) { - if (!descriptors.hasDefinitionFor(propertyType)) { - descriptors.addDefinition(propertyType, - new Item(propertyType, getNestedPropertiesFor(persistentProperty, descriptors))); + if (!definitions.hasDefinitionFor(propertyType)) { + definitions.addDefinition(propertyType, + new Item(propertyType, getNestedPropertiesFor(persistentProperty, definitions))); } registrar.register(property.with(propertyType, Definitions.getReference(propertyType)), actualPropertyType); @@ -266,7 +275,8 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric return Collections.emptyList(); } - return getPropertiesFor(property.getActualType(), mappings.getMetadataFor(property.getActualType()), descriptors); + return getPropertiesFor(property.getActualType(), + associations.getMappings().getMetadataFor(property.getActualType()), descriptors); } private JsonSchemaProperty getSchemaProperty(BeanPropertyDefinition definition, TypeInformation type, @@ -322,7 +332,7 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric * @author Oliver Gierke * @since 2.4 */ - private static class JsonSchemaPropertyRegistrar { + private class JsonSchemaPropertyRegistrar { private final JacksonMetadata metadata; private final List> properties; @@ -349,12 +359,17 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric JsonSerializer serializer = metadata.getTypeSerializer(type.getType()); - if (!(serializer instanceof JsonSchemaPropertyCustomizer)) { - properties.add(property); + if ((serializer instanceof JsonSchemaPropertyCustomizer)) { + properties.add(((JsonSchemaPropertyCustomizer) serializer).customize(property, type)); return; } - properties.add(((JsonSchemaPropertyCustomizer) serializer).customize(property, type)); + if (configuration.isLookupType(type.getType())) { + properties.add(customizerFactory.getCustomizerFor(type.getType()).customize(property, type)); + return; + } + + properties.add(property); } public List> getProperties() { @@ -362,6 +377,36 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric } } + @RequiredArgsConstructor + public static class ValueTypeSchemaPropertyCustomizerFactory { + + private final @NonNull RepositoryInvokerFactory factory; + + public JsonSchemaPropertyCustomizer getCustomizerFor(final Class type) { + + return new JsonSchemaPropertyCustomizer() { + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.webmvc.json.JsonSchemaPropertyCustomizer#customize(org.springframework.data.rest.webmvc.json.JsonSchema.JsonSchemaProperty, org.springframework.data.util.TypeInformation) + */ + @Override + public JsonSchemaProperty customize(JsonSchemaProperty property, TypeInformation type) { + + List result = new ArrayList(); + + for (Object element : factory.getInvokerFor(type.getType()).invokeFindAll((Sort) null)) { + result.add(element.toString()); + } + + Collections.sort(result); + + return new EnumProperty(property.getName(), property.getTitle(), result, property.description, true); + } + }; + } + } + /** * Message source resolvable that defaults the messages to the last segment of the dot-separated code in case the * configured delegate doesn't return a default message itself. diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/mapping/AssociationLinks.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/mapping/AssociationLinks.java index f22d3a173..c8a139cc9 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/mapping/AssociationLinks.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/mapping/AssociationLinks.java @@ -15,12 +15,18 @@ */ package org.springframework.data.rest.webmvc.mapping; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + import java.util.Collections; import java.util.List; import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.rest.core.Path; +import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.core.mapping.ResourceMapping; import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.core.mapping.ResourceMetadata; @@ -34,21 +40,11 @@ import org.springframework.util.Assert; * @author Greg Turnquist * @since 2.1 */ +@RequiredArgsConstructor public class AssociationLinks { - private final ResourceMappings mappings; - - /** - * Creates a new {@link AssociationLinks} using the given {@link ResourceMappings}. - * - * @param mappings must not be {@literal null}. - */ - public AssociationLinks(ResourceMappings mappings) { - - Assert.notNull(mappings, "ResourceMappings must not be null!"); - - this.mappings = mappings; - } + private final @NonNull @Getter ResourceMappings mappings; + private final @NonNull RepositoryRestConfiguration config; /** * Returns the links to render for the given {@link Association}. @@ -62,10 +58,9 @@ public class AssociationLinks { Assert.notNull(association, "Association must not be null!"); Assert.notNull(path, "Base path must not be null!"); - PersistentProperty property = association.getInverse(); - - if (isLinkableAssociation(property)) { + if (isLinkableAssociation(association)) { + PersistentProperty property = association.getInverse(); ResourceMetadata metadata = mappings.getMetadataFor(property.getOwner().getType()); ResourceMapping propertyMapping = metadata.getMappingFor(property); @@ -78,6 +73,46 @@ public class AssociationLinks { return Collections.emptyList(); } + /** + * Returns the {@link ResourceMetadata} for the given type. + * + * @param type must not be {@literal null}. + * @return + */ + public ResourceMetadata getMetadataFor(Class type) { + + Assert.notNull(type, "Type must not be null!"); + + return mappings.getMetadataFor(type); + } + + /** + * Returns whether the type of the given {@link PersistentProperty} is configured as lookup type. + * + * @param property can be {@literal null}. + * @return + */ + public boolean isLookupType(PersistentProperty property) { + return property == null ? false : config.isLookupType(property.getActualType()); + } + + public boolean isIdExposed(PersistentEntity entity) { + return config.isIdExposedFor(entity.getType()); + } + + /** + * Returns whether the given {@link Association} is linkable. + * + * @param association must not be {@literal null}. + * @return + */ + public boolean isLinkableAssociation(Association> association) { + + Assert.notNull(association, "Association must not be null!"); + + return isLinkableAssociation(association.getInverse()); + } + /** * Returns whether the given property is an association that is linkable. * @@ -86,7 +121,7 @@ public class AssociationLinks { */ public boolean isLinkableAssociation(PersistentProperty property) { - if (property == null || !property.isAssociation()) { + if (property == null || !property.isAssociation() || config.isLookupType(property.getActualType())) { return false; } @@ -99,5 +134,4 @@ public class AssociationLinks { metadata = mappings.getMetadataFor(property.getActualType()); return metadata == null ? false : metadata.isExported(); } - } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/mapping/LinkCollectingAssociationHandler.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/mapping/LinkCollectingAssociationHandler.java index 63f2bdd33..096d6cfac 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/mapping/LinkCollectingAssociationHandler.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/mapping/LinkCollectingAssociationHandler.java @@ -19,7 +19,6 @@ import java.util.ArrayList; import java.util.List; import org.springframework.data.mapping.Association; -import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.SimpleAssociationHandler; import org.springframework.data.mapping.context.PersistentEntities; @@ -42,6 +41,7 @@ public class LinkCollectingAssociationHandler implements SimpleAssociationHandle private final PersistentEntities entities; private final AssociationLinks associationLinks; private final Path basePath; + private final boolean nested; private final List links; @@ -54,6 +54,11 @@ public class LinkCollectingAssociationHandler implements SimpleAssociationHandle * @param associationLinks must not be {@literal null}. */ public LinkCollectingAssociationHandler(PersistentEntities entities, Path path, AssociationLinks associationLinks) { + this(entities, path, associationLinks, false); + } + + private LinkCollectingAssociationHandler(PersistentEntities entities, Path path, AssociationLinks associationLinks, + boolean nested) { Assert.notNull(entities, "PersistentEntities must not be null!"); Assert.notNull(path, "Path must not be null!"); @@ -64,6 +69,11 @@ public class LinkCollectingAssociationHandler implements SimpleAssociationHandle this.basePath = path; this.links = new ArrayList(); + this.nested = nested; + } + + public LinkCollectingAssociationHandler nested() { + return nested ? this : new LinkCollectingAssociationHandler(entities, basePath, associationLinks, true); } /** @@ -95,10 +105,6 @@ public class LinkCollectingAssociationHandler implements SimpleAssociationHandle links.add(link); } } - - } else { - PersistentEntity associationEntity = entities.getPersistentEntity(property.getActualType()); - associationEntity.doWithAssociations(this); } } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/mapping/LinkCollector.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/mapping/LinkCollector.java new file mode 100644 index 000000000..77e6a7df4 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/mapping/LinkCollector.java @@ -0,0 +1,263 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.mapping; + +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.SimpleAssociationHandler; +import org.springframework.data.mapping.context.PersistentEntities; +import org.springframework.data.mapping.model.MappingException; +import org.springframework.data.rest.core.Path; +import org.springframework.data.rest.core.mapping.ResourceMapping; +import org.springframework.data.rest.core.mapping.ResourceMetadata; +import org.springframework.data.rest.core.support.SelfLinkProvider; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.Links; +import org.springframework.util.Assert; + +/** + * A service to collect all standard links that need to be added to a certain object. + * + * @author Oliver Gierke + */ +public class LinkCollector { + + private final PersistentEntities entities; + private final AssociationLinks associationLinks; + private final SelfLinkProvider links; + + /** + * Creates a new {@link PersistentEntities}, {@link SelfLinkProvider} and {@link AssociationLinks}. + * + * @param entities must not be {@literal null}. + * @param linkProvider must not be {@literal null}. + * @param associationLinks must not be {@literal null}. + */ + public LinkCollector(PersistentEntities entities, SelfLinkProvider linkProvider, AssociationLinks associationLinks) { + + Assert.notNull(entities, "PersistentEntities must not be null!"); + Assert.notNull(linkProvider, "SelfLinkProvider must not be null!"); + Assert.notNull(associationLinks, "AssociationLinks must not be null!"); + + this.links = linkProvider; + this.entities = entities; + this.associationLinks = associationLinks; + } + + /** + * Returns all {@link Links} for the given object. + * + * @param object must not be {@literal null}. + * @return + */ + public Links getLinksFor(Object object) { + return getLinksFor(object, Collections. emptyList()); + } + + /** + * Returns all {@link Links} for the given object and already existing {@link Link}. + * + * @param object must not be {@literal null}. + * @param existingLinks must not be {@literal null}. + * @return + */ + public Links getLinksFor(Object object, List existingLinks) { + + Assert.notNull(object, "Object must not be null!"); + Assert.notNull(existingLinks, "Existing links must not be null!"); + + PersistentEntity entity = entities.getPersistentEntity(object.getClass()); + + Links links = new Links(existingLinks); + Link selfLink = createSelfLink(object, links); + + if (selfLink == null) { + return links; + } + + Path path = new Path(selfLink.expand().getHref()); + + LinkCollectingAssociationHandler handler = new LinkCollectingAssociationHandler(entities, path, associationLinks); + entity.doWithAssociations(handler); + + List result = new ArrayList(existingLinks); + result.addAll(handler.getLinks()); + + return addSelfLinkIfNecessary(object, result); + } + + public Links getLinksForNested(Object object, List existing) { + + PersistentEntity entity = entities.getPersistentEntity(object.getClass()); + + NestedLinkCollectingAssociationHandler handler = new NestedLinkCollectingAssociationHandler(links, + entity.getPropertyAccessor(object), associationLinks); + entity.doWithAssociations(handler); + + List links = new ArrayList(); + links.addAll(existing); + links.addAll(handler.getLinks()); + + return new Links(links); + } + + private Links addSelfLinkIfNecessary(Object object, List existing) { + + Links result = new Links(existing); + + if (result.hasLink(Link.REL_SELF)) { + return result; + } + + List list = new ArrayList(); + list.add(createSelfLink(object, result)); + list.addAll(existing); + + return new Links(list); + } + + private Link createSelfLink(Object object, Links existing) { + + if (existing.hasLink(Link.REL_SELF)) { + return existing.getLink(Link.REL_SELF); + } + + return links.createSelfLinkFor(object).withSelfRel(); + } + + /** + * {@link SimpleAssociationHandler} that will collect {@link Link}s for all linkable associations. + * + * @author Oliver Gierke + * @since 2.1 + */ + @RequiredArgsConstructor + private static class LinkCollectingAssociationHandler implements SimpleAssociationHandler { + + private static final String AMBIGUOUS_ASSOCIATIONS = "Detected multiple association links with same relation type! Disambiguate association %s using @RestResource!"; + + private final @NonNull PersistentEntities entities; + private final @NonNull Path basePath; + private final @NonNull AssociationLinks associationLinks; + private final @NonNull List links = new ArrayList(); + + /** + * Returns the links collected after the {@link Association} has been traversed. + * + * @return the links + */ + public List getLinks() { + return links; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.SimpleAssociationHandler#doWithAssociation(org.springframework.data.mapping.Association) + */ + @Override + public void doWithAssociation(final Association> association) { + + if (associationLinks.isLinkableAssociation(association)) { + + PersistentProperty property = association.getInverse(); + Links existingLinks = new Links(links); + + for (Link link : associationLinks.getLinksFor(association, basePath)) { + if (existingLinks.hasLink(link.getRel())) { + throw new MappingException(String.format(AMBIGUOUS_ASSOCIATIONS, property.toString())); + } else { + links.add(link); + } + } + } + } + } + + @RequiredArgsConstructor + private static class NestedLinkCollectingAssociationHandler implements SimpleAssociationHandler { + + private final SelfLinkProvider selfLinks; + private final PersistentPropertyAccessor accessor; + private final AssociationLinks associations; + private final @Getter List links = new ArrayList(); + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.SimpleAssociationHandler#doWithAssociation(org.springframework.data.mapping.Association) + */ + @Override + public void doWithAssociation(Association> association) { + + if (!associations.isLinkableAssociation(association)) { + return; + } + + PersistentProperty property = association.getInverse(); + Object value = accessor.getProperty(property); + + if (value == null) { + return; + } + + ResourceMetadata metadata = associations.getMappings().getMetadataFor(property.getOwner().getType()); + ResourceMapping propertyMapping = metadata.getMappingFor(property); + + for (Object element : asCollection(value)) { + if (element != null) + links.add(getLinkFor(element, propertyMapping)); + } + } + + /** + * Returns a link pointing to the given entity using the given {@link ResourceMapping} to detect the link relation. + * + * @param entity must not be {@literal null}. + * @param mapping must not be {@literal null}. + * @return + */ + private Link getLinkFor(Object entity, ResourceMapping mapping) { + return selfLinks.createSelfLinkFor(entity).withRel(mapping.getRel()); + } + + /** + * Returns the given object as {@link Collection}, i.e. the object as is if it's a collection already or wrapped + * into a single-element collection otherwise. + * + * @param object can be {@literal null}. + * @return + */ + @SuppressWarnings("unchecked") + private static Collection asCollection(Object object) { + + if (object instanceof Collection) { + return (Collection) object; + } + + return Collections.singleton(object); + } + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/mapping/NestedLinkCollectingAssociationHandler.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/mapping/NestedLinkCollectingAssociationHandler.java new file mode 100644 index 000000000..2f6b15741 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/mapping/NestedLinkCollectingAssociationHandler.java @@ -0,0 +1,83 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.mapping; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.IdentifierAccessor; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.SimpleAssociationHandler; +import org.springframework.data.mapping.context.PersistentEntities; +import org.springframework.data.rest.core.mapping.ResourceMapping; +import org.springframework.data.rest.core.mapping.ResourceMappings; +import org.springframework.data.rest.core.mapping.ResourceMetadata; +import org.springframework.hateoas.EntityLinks; +import org.springframework.hateoas.Link; + +/** + * @author Oliver Gierke + */ +@RequiredArgsConstructor +public class NestedLinkCollectingAssociationHandler implements SimpleAssociationHandler { + + private final EntityLinks entityLinks; + private final PersistentEntities entities; + private final PersistentPropertyAccessor accessor; + private final ResourceMappings mappings; + + private final @Getter List links = new ArrayList(); + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.SimpleAssociationHandler#doWithAssociation(org.springframework.data.mapping.Association) + */ + @Override + public void doWithAssociation(Association> association) { + + PersistentProperty property = association.getInverse(); + Object propertyValue = accessor.getProperty(property); + + ResourceMetadata metadata = mappings.getMetadataFor(property.getOwner().getType()); + ResourceMapping propertyMapping = metadata.getMappingFor(property); + + if (property.isCollectionLike()) { + + for (Object element : (Collection) propertyValue) { + + IdentifierAccessor identifierAccessor = entities.getPersistentEntity(element.getClass()) + .getIdentifierAccessor(element); + + links.add(entityLinks.linkForSingleResource(element.getClass(), identifierAccessor.getIdentifier()) + .withRel(propertyMapping.getRel())); + + } + + } else { + IdentifierAccessor identifierAccessor = entities.getPersistentEntity(propertyValue.getClass()) + .getIdentifierAccessor(propertyValue); + + links.add(entityLinks.linkForSingleResource(propertyValue.getClass(), identifierAccessor.getIdentifier()) + .withRel(propertyMapping.getRel())); + } + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/DefaultExcerptProjector.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/DefaultExcerptProjector.java new file mode 100644 index 000000000..ef92b2822 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/DefaultExcerptProjector.java @@ -0,0 +1,64 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.support; + +import lombok.RequiredArgsConstructor; + +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.rest.core.mapping.ResourceMappings; +import org.springframework.data.rest.core.mapping.ResourceMetadata; +import org.springframework.util.Assert; + +/** + * {@link DefaultedPageable} implementation of {@link ExcerptProjector} that uses the given {@link ProjectionFactory} + * and considers the given {@link ResourceMappings}. + * + * @author Oliver Gierke + * @since 2.5 + */ +@RequiredArgsConstructor +public class DefaultExcerptProjector implements ExcerptProjector { + + private final ProjectionFactory factory; + private final ResourceMappings mappings; + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.webmvc.support.ExcerptProjector#projectExcerpt(java.lang.Object) + */ + @Override + public Object projectExcerpt(Object source) { + + Assert.notNull(source, "Projection source must not be null!"); + + ResourceMetadata metadata = mappings.getMetadataFor(source.getClass()); + Class projection = metadata == null ? null : metadata.getExcerptProjection(); + + return projection == null || projection.equals(source.getClass()) ? source + : factory.createProjection(projection, source); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.webmvc.support.ExcerptProjector#hasExcerptProjection(java.lang.Class) + */ + @Override + public boolean hasExcerptProjection(Class type) { + + ResourceMetadata metadata = mappings.getMetadataFor(type); + return metadata == null ? false : metadata.getExcerptProjection() != null; + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/ExcerptProjector.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/ExcerptProjector.java new file mode 100644 index 000000000..54753025a --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/ExcerptProjector.java @@ -0,0 +1,43 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.support; + +/** + * Interface for a component that can provide excerpt projections. + * + * @author Oliver Gierke + * @since 2.5 + */ +public interface ExcerptProjector { + + /** + * Creates a excerpt projection for the given source. If no excerpt projection is available, the call will fall back + * to the behavior of {@link #project(Object)}. If you completely wish to skip handling the object, check for the + * presence of an excerpt projection using {@link #hasExcerptProjection(Class)}. + * + * @param source must not be {@literal null}. + * @return + */ + Object projectExcerpt(Object source); + + /** + * Returns whether an excerpt projection has been registered for the given type. + * + * @param type must not be {@literal null}. + * @return + */ + boolean hasExcerptProjection(Class type); +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/PersistentEntityProjector.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/PersistentEntityProjector.java index 5c971c164..504a8fe83 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/PersistentEntityProjector.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/PersistentEntityProjector.java @@ -17,7 +17,6 @@ package org.springframework.data.rest.webmvc.support; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.rest.core.mapping.ResourceMappings; -import org.springframework.data.rest.core.mapping.ResourceMetadata; import org.springframework.data.rest.core.projection.ProjectionDefinitions; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -27,12 +26,11 @@ import org.springframework.util.StringUtils; * * @author Oliver Gierke */ -public class PersistentEntityProjector implements Projector { +public class PersistentEntityProjector extends DefaultExcerptProjector implements Projector { - private final ProjectionDefinitions projectionDefinitions; + private final ProjectionDefinitions definitions; private final ProjectionFactory factory; private final String projection; - private final ResourceMappings mappings; /** * Creates a new {@link PersistentEntityProjector} using the given {@link ProjectionDefinitions}, @@ -45,13 +43,14 @@ public class PersistentEntityProjector implements Projector { public PersistentEntityProjector(ProjectionDefinitions projectionDefinitions, ProjectionFactory factory, String projection, ResourceMappings mappings) { + super(factory, mappings); + Assert.notNull(projectionDefinitions, "ProjectionDefinitions must not be null!"); Assert.notNull(factory, "ProjectionFactory must not be null!"); - this.projectionDefinitions = projectionDefinitions; this.factory = factory; + this.definitions = projectionDefinitions; this.projection = projection; - this.mappings = mappings; } /* @@ -66,37 +65,7 @@ public class PersistentEntityProjector implements Projector { return source; } - Class projectionType = projectionDefinitions.getProjectionType(source.getClass(), projection); + Class projectionType = definitions.getProjectionType(source.getClass(), projection); return projectionType == null ? source : factory.createProjection(projectionType, source); } - - /* - * (non-Javadoc) - * @see org.springframework.data.rest.webmvc.support.Projector#projectExcerpt(java.lang.Object) - */ - @Override - public Object projectExcerpt(Object source) { - - Assert.notNull(source, "Projection source must not be null!"); - - ResourceMetadata metadata = mappings.getMetadataFor(source.getClass()); - Class projection = metadata == null ? null : metadata.getExcerptProjection(); - - if (projection == null) { - return project(source); - } - - return projection == null ? source : factory.createProjection(projection, source); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.rest.webmvc.support.Projector#hasExcerptProjection(java.lang.Class) - */ - @Override - public boolean hasExcerptProjection(Class type) { - - ResourceMetadata metadata = mappings.getMetadataFor(type); - return metadata == null ? false : metadata.getExcerptProjection() != null; - } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/Projector.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/Projector.java index f5c6fb5b6..550fd8b2c 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/Projector.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/Projector.java @@ -20,7 +20,7 @@ package org.springframework.data.rest.webmvc.support; * * @author Oliver Gierke */ -public interface Projector { +public interface Projector extends ExcerptProjector { /** * Returns the projection object for the given source. This may result in the same object being returned or a @@ -30,22 +30,4 @@ public interface Projector { * @return */ Object project(Object source); - - /** - * Creates a excerpt projection for the given source. If no excerpt projection is available, the call will fall back - * to the behavior of {@link #project(Object)}. If you completely wish to skip handling the object, check for the - * presence of an excerpt projection using {@link #hasExcerptProjection(Class)}. - * - * @param source must not be {@literal null}. - * @return - */ - Object projectExcerpt(Object source); - - /** - * Returns whether an excerpt projection has been registered for the given type. - * - * @param type must not be {@literal null}. - * @return - */ - boolean hasExcerptProjection(Class type); } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java index 2fe6c2d74..7c0b3440e 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java @@ -17,11 +17,13 @@ package org.springframework.data.rest.webmvc.support; import static org.springframework.hateoas.TemplateVariable.VariableType.*; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + import java.io.Serializable; import java.util.ArrayList; import java.util.List; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.repository.support.Repositories; @@ -57,39 +59,14 @@ import org.springframework.web.util.UriComponentsBuilder; * @author Jon Brisbin * @author Oliver Gierke */ +@RequiredArgsConstructor public class RepositoryEntityLinks extends AbstractEntityLinks { - private final Repositories repositories; - private final ResourceMappings mappings; - private final RepositoryRestConfiguration config; - private final PagingAndSortingTemplateVariables templateVariables; - private final PluginRegistry> idConverters; - - /** - * Creates a new {@link RepositoryEntityLinks}. - * - * @param repositories must not be {@literal null}. - * @param mappings must not be {@literal null}. - * @param config must not be {@literal null}. - * @param pagingAndSortingTemplateVariables must not be {@literal null}. - * @param idConverters must not be {@literal null}. - */ - @Autowired - public RepositoryEntityLinks(Repositories repositories, ResourceMappings mappings, RepositoryRestConfiguration config, - PagingAndSortingTemplateVariables templateVariables, PluginRegistry> idConverters) { - - Assert.notNull(repositories, "Repositories must not be null!"); - Assert.notNull(mappings, "ResourceMappings must not be null!"); - Assert.notNull(config, "RepositoryRestConfiguration must not be null!"); - Assert.notNull(templateVariables, "PagingAndSortingTemplateVariables must not be null!"); - Assert.notNull(idConverters, "Id converter registry must not be null!"); - - this.repositories = repositories; - this.mappings = mappings; - this.config = config; - this.templateVariables = templateVariables; - this.idConverters = idConverters; - } + private final @NonNull Repositories repositories; + private final @NonNull ResourceMappings mappings; + private final @NonNull RepositoryRestConfiguration config; + private final @NonNull PagingAndSortingTemplateVariables templateVariables; + private final @NonNull PluginRegistry> idConverters; /* * (non-Javadoc) diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractControllerIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractControllerIntegrationTests.java index a7a163ef0..4b48ef0ff 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractControllerIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractControllerIntegrationTests.java @@ -62,8 +62,8 @@ public abstract class AbstractControllerIntegrationTests { SelfLinkProvider selfLinkProvider = new DefaultSelfLinkProvider(persistentEntities(), entityLinks(), Collections.> emptyList()); - return new PersistentEntityResourceAssembler(persistentEntities(), selfLinkProvider, StubProjector.INSTANCE, - resourceMappings()); + return new PersistentEntityResourceAssembler(persistentEntities(), StubProjector.INSTANCE, associationLinks(), + selfLinkProvider); } } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AssociationLinksUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AssociationLinksUnitTests.java index c52aa4986..f9edce3de 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AssociationLinksUnitTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AssociationLinksUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.data.rest.webmvc; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; +import static org.mockito.Mockito.*; import java.util.Arrays; import java.util.List; @@ -24,6 +25,7 @@ import java.util.List; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.data.annotation.Reference; import org.springframework.data.mapping.PersistentProperty; @@ -33,6 +35,7 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.rest.core.Path; import org.springframework.data.rest.core.annotation.RestResource; +import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.core.mapping.PersistentEntitiesResourceMappings; import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.core.mapping.ResourceMetadata; @@ -40,6 +43,8 @@ import org.springframework.data.rest.webmvc.mapping.AssociationLinks; import org.springframework.hateoas.Link; /** + * Unit tests for {@link AssociationLinks}. + * * @author Oliver Gierke */ @RunWith(MockitoJUnitRunner.class) @@ -48,18 +53,19 @@ public class AssociationLinksUnitTests { AssociationLinks links; ResourceMappings mappings; - MongoMappingContext mappingContext; MongoPersistentEntity entity; ResourceMetadata sampleResourceMetadata; + @Mock RepositoryRestConfiguration config; + @Before public void setUp() { this.mappingContext = new MongoMappingContext(); this.entity = mappingContext.getPersistentEntity(Sample.class); this.mappings = new PersistentEntitiesResourceMappings(new PersistentEntities(Arrays.asList(mappingContext))); - this.links = new AssociationLinks(mappings); + this.links = new AssociationLinks(mappings, config); } /** @@ -67,7 +73,12 @@ public class AssociationLinksUnitTests { */ @Test(expected = IllegalArgumentException.class) public void rejectsNullMappings() { - new AssociationLinks(null); + new AssociationLinks(null, mock(RepositoryRestConfiguration.class)); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullConfiguration() { + new AssociationLinks(mappings, null); } /** @@ -75,7 +86,7 @@ public class AssociationLinksUnitTests { */ @Test public void considersNullPropertyUnlinkable() { - assertThat(links.isLinkableAssociation(null), is(false)); + assertThat(links.isLinkableAssociation((PersistentProperty) null), is(false)); } /** @@ -119,6 +130,19 @@ public class AssociationLinksUnitTests { assertThat(links.getLinksFor(property.getAssociation(), new Path("/sample")), hasSize(0)); } + @Test + public void detectsLookupTypes() { + + doReturn(true).when(config).isLookupType(Property.class); + + assertThat(links.isLookupType(entity.getPersistentProperty("hiddenProperty")), is(true)); + } + + @Test + public void delegatesResourceMetadataLookupToMappings() { + assertThat(links.getMetadataFor(Property.class), is(mappings.getMetadataFor(Property.class))); + } + public static class Sample { @Reference Property property; diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/PersistentEntityResourceAssemblerIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/PersistentEntityResourceAssemblerIntegrationTests.java index 215570171..17d8246e2 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/PersistentEntityResourceAssemblerIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/PersistentEntityResourceAssemblerIntegrationTests.java @@ -27,10 +27,12 @@ import org.hamcrest.Matchers; import org.junit.Test; import org.mockito.internal.stubbing.answers.ReturnsArgumentAt; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.rest.core.support.DefaultSelfLinkProvider; import org.springframework.data.rest.core.support.EntityLookup; import org.springframework.data.rest.webmvc.AbstractControllerIntegrationTests.TestConfiguration; +import org.springframework.data.rest.webmvc.mapping.AssociationLinks; import org.springframework.data.rest.webmvc.mongodb.MongoDbRepositoryConfig; import org.springframework.data.rest.webmvc.mongodb.User; import org.springframework.data.rest.webmvc.support.Projector; @@ -39,6 +41,8 @@ import org.springframework.hateoas.Link; import org.springframework.hateoas.Links; import org.springframework.test.context.ContextConfiguration; +import com.fasterxml.jackson.databind.ObjectMapper; + /** * Integration tests for {@link PersistentEntityResourceAssembler}. * @@ -49,25 +53,29 @@ public class PersistentEntityResourceAssemblerIntegrationTests extends AbstractC @Autowired PersistentEntities entities; @Autowired EntityLinks entityLinks; + @Autowired @Qualifier("objectMapper") ObjectMapper objectMapper; + @Autowired AssociationLinks associations; /** * @see DATAREST-609 */ @Test - public void addsSelfAndSingleResourceLinkToResourceByDefault() { + public void addsSelfAndSingleResourceLinkToResourceByDefault() throws Exception { Projector projector = mock(Projector.class); when(projector.projectExcerpt(anyObject())).thenAnswer(new ReturnsArgumentAt(0)); - PersistentEntityResourceAssembler assembler = new PersistentEntityResourceAssembler(entities, - new DefaultSelfLinkProvider(entities, entityLinks, Collections.> emptyList()), projector, - mappings); + PersistentEntityResourceAssembler assembler = new PersistentEntityResourceAssembler(entities, projector, + associations, new DefaultSelfLinkProvider(entities, entityLinks, Collections.> emptyList())); User user = new User(); user.id = BigInteger.valueOf(4711); - Links links = new Links(assembler.toResource(user).getLinks()); + PersistentEntityResource resource = assembler.toResource(user); + + System.out.println(objectMapper.writeValueAsString(resource)); + Links links = new Links(resource.getLinks()); assertThat(links, is(Matchers. iterableWithSize(2))); assertThat(links.getLink("self").getVariables(), is(Matchers.empty())); diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/ResourceProcessorHandlerMethodReturnValueHandlerUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/ResourceProcessorHandlerMethodReturnValueHandlerUnitTests.java index 218d6486b..b08b0a78b 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/ResourceProcessorHandlerMethodReturnValueHandlerUnitTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/ResourceProcessorHandlerMethodReturnValueHandlerUnitTests.java @@ -41,7 +41,7 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; -import org.springframework.data.rest.webmvc.ResourceProcessorHandlerMethodReturnValueHandler.ResourcesProcessorWrapper; +import org.springframework.data.rest.webmvc.ResourceProcessorInvoker.ResourcesProcessorWrapper; import org.springframework.hateoas.Link; import org.springframework.hateoas.PagedResources; import org.springframework.hateoas.PagedResources.PageMetadata; @@ -217,7 +217,7 @@ public class ResourceProcessorHandlerMethodReturnValueHandlerUnitTests { MethodParameter parameter = METHOD_PARAMS.get("resource"); ResourceProcessorHandlerMethodReturnValueHandler handler = new ResourceProcessorHandlerMethodReturnValueHandler( - delegate, resourceProcessors); + delegate, new ResourceProcessorInvoker(resourceProcessors)); handler.setRootLinksAsHeaders(true); handler.handleReturnValue(resource, parameter, null, null); @@ -295,7 +295,7 @@ public class ResourceProcessorHandlerMethodReturnValueHandlerUnitTests { resourceProcessors.add((ResourceProcessor) factory.getProxy()); - new ResourceProcessorHandlerMethodReturnValueHandler(delegate, resourceProcessors); + new ResourceProcessorHandlerMethodReturnValueHandler(delegate, new ResourceProcessorInvoker(resourceProcessors)); } // Helpers ---------------------------------------------------------// @@ -307,7 +307,7 @@ public class ResourceProcessorHandlerMethodReturnValueHandlerUnitTests { } HandlerMethodReturnValueHandler handler = new ResourceProcessorHandlerMethodReturnValueHandler(delegate, - resourceProcessors); + new ResourceProcessorInvoker(resourceProcessors)); handler.handleReturnValue(returnValue, methodParam, null, null); } @@ -317,7 +317,7 @@ public class ResourceProcessorHandlerMethodReturnValueHandlerUnitTests { when(delegate.supportsReturnType(Mockito.any(MethodParameter.class))).thenReturn(value); HandlerMethodReturnValueHandler handler = new ResourceProcessorHandlerMethodReturnValueHandler(delegate, - resourceProcessors); + new ResourceProcessorInvoker(resourceProcessors)); assertThat(handler.supportsReturnType(parameter), is(value)); } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/config/JsonPatchHandlerUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/config/JsonPatchHandlerUnitTests.java index 0fab114e8..e29fbbb75 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/config/JsonPatchHandlerUnitTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/config/JsonPatchHandlerUnitTests.java @@ -17,6 +17,7 @@ package org.springframework.data.rest.webmvc.config; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; +import static org.mockito.Mockito.*; import static org.springframework.data.rest.webmvc.util.TestUtils.*; import java.util.Arrays; @@ -30,9 +31,11 @@ import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.webmvc.RestMediaTypes; import org.springframework.data.rest.webmvc.json.DomainObjectReader; +import org.springframework.data.rest.webmvc.mapping.AssociationLinks; import org.springframework.data.rest.webmvc.mongodb.Address; import org.springframework.data.rest.webmvc.mongodb.User; import org.springframework.http.converter.HttpMessageNotReadableException; @@ -61,7 +64,9 @@ public class JsonPatchHandlerUnitTests { PersistentEntities entities = new PersistentEntities(Arrays.asList(context)); - this.handler = new JsonPatchHandler(new ObjectMapper(), new DomainObjectReader(entities, mappings)); + AssociationLinks associations = new AssociationLinks(mappings, mock(RepositoryRestConfiguration.class)); + + this.handler = new JsonPatchHandler(new ObjectMapper(), new DomainObjectReader(entities, associations)); Address address = new Address(); address.street = "Foo"; diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/DataRest262Tests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/DataRest262Tests.java index cd3a887bc..48d7376f8 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/DataRest262Tests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/DataRest262Tests.java @@ -27,6 +27,7 @@ import javax.persistence.ManyToOne; import javax.validation.constraints.NotNull; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -93,6 +94,7 @@ public class DataRest262Tests { * @see DATAREST-262 */ @Test + @Ignore public void serializesLinksToNestedAssociations() throws Exception { Airport first = new Airport(); diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/Person.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/Person.java index f13f1a29c..a3060a5a8 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/Person.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/Person.java @@ -43,13 +43,28 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @JsonIgnoreProperties({ "height", "weight" }) public class Person { - private Long id; - @Description("A person's first name") private String firstName; - @Description("A person's last name") private String lastName; - @Description("A person's siblings") private List siblings = Collections.emptyList(); + @Id @GeneratedValue private Long id; + + @Description("A person's first name") // + private String firstName; + + @Description("A person's last name") // + private String lastName; + + @Description("A person's siblings") // + @ManyToMany // + private List siblings = new ArrayList(); + + @ManyToOne // private Person father; - @Description("Timestamp this person object was created") private Date created; - private int age, height, weight; + + @Description("Timestamp this person object was created") // + private Date created; + + @JsonIgnore // + private int age; + + private int height, weight; private Gender gender; public Person() {} @@ -59,8 +74,6 @@ public class Person { this.lastName = lastName; } - @Id - @GeneratedValue public Long getId() { return id; } @@ -87,14 +100,15 @@ public class Person { } public Person addSibling(Person p) { + if (siblings == Collections.EMPTY_LIST) { siblings = new ArrayList(); } + siblings.add(p); return this; } - @ManyToMany public List getSiblings() { return siblings; } @@ -103,7 +117,6 @@ public class Person { this.siblings = siblings; } - @ManyToOne public Person getFather() { return father; } @@ -123,7 +136,6 @@ public class Person { this.created = Calendar.getInstance().getTime(); } - @JsonIgnore public int getAge() { return age; } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/DomainObjectReaderUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/DomainObjectReaderUnitTests.java index 08f6a32ce..c1047165f 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/DomainObjectReaderUnitTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/DomainObjectReaderUnitTests.java @@ -17,6 +17,7 @@ package org.springframework.data.rest.webmvc.json; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; +import static org.mockito.Mockito.*; import java.util.Collections; import java.util.HashMap; @@ -31,7 +32,9 @@ import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Version; import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.core.mapping.ResourceMappings; +import org.springframework.data.rest.webmvc.mapping.AssociationLinks; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; @@ -65,7 +68,8 @@ public class DomainObjectReaderUnitTests { PersistentEntities entities = new PersistentEntities(Collections.singleton(mappingContext)); - this.reader = new DomainObjectReader(entities, mappings); + this.reader = new DomainObjectReader(entities, + new AssociationLinks(mappings, mock(RepositoryRestConfiguration.class))); } /** 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 465b3f4d9..a56857179 100644 --- 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 @@ -22,6 +22,7 @@ import static org.mockito.Mockito.*; import java.io.IOException; import java.net.URI; import java.util.Arrays; +import java.util.Collections; import org.junit.Before; import org.junit.Test; @@ -32,13 +33,23 @@ import org.springframework.core.convert.TypeDescriptor; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.repository.support.RepositoryInvokerFactory; import org.springframework.data.rest.core.UriToEntityConverter; -import org.springframework.data.rest.core.config.EnumTranslationConfiguration; -import org.springframework.data.rest.core.config.MetadataConfiguration; -import org.springframework.data.rest.core.config.ProjectionDefinitionConfiguration; -import org.springframework.data.rest.core.config.RepositoryRestConfiguration; +import org.springframework.data.rest.core.mapping.ResourceMappings; +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.ResourceProcessorInvoker; +import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module.AssociationOmittingSerializerModifier; +import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module.AssociationUriResolvingDeserializerModifier; +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.AssociationLinks; +import org.springframework.data.rest.webmvc.support.ExcerptProjector; +import org.springframework.hateoas.EntityLinks; +import org.springframework.hateoas.ResourceProcessor; import org.springframework.hateoas.UriTemplate; +import org.springframework.plugin.core.OrderAwarePluginRegistry; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeInfo; @@ -55,8 +66,11 @@ import com.jayway.jsonpath.JsonPath; @RunWith(MockitoJUnitRunner.class) public class PersistentEntityJackson2ModuleUnitTests { - @Mock AssociationLinks associationLinks; + @Mock AssociationLinks associations; @Mock UriToEntityConverter converter; + @Mock EntityLinks entityLinks; + @Mock ResourceMappings mappings; + @Mock SelfLinkProvider selfLinks; PersistentEntities persistentEntities; ObjectMapper mapper; @@ -71,17 +85,21 @@ public class PersistentEntityJackson2ModuleUnitTests { this.persistentEntities = new PersistentEntities(Arrays.asList(mappingContext)); - SimpleModule module = new SimpleModule(); - module.setSerializerModifier(new PersistentEntityJackson2Module.AssociationOmittingSerializerModifier( - persistentEntities, associationLinks, new RepositoryRestConfiguration(new ProjectionDefinitionConfiguration(), - new MetadataConfiguration(), mock(EnumTranslationConfiguration.class)))); + ResourceProcessorInvoker invoker = new ResourceProcessorInvoker(Collections.> emptyList()); - module.setDeserializerModifier(new PersistentEntityJackson2Module.AssociationUriResolvingDeserializerModifier( - persistentEntities, converter, associationLinks)); + NestedEntitySerializer nestedEntitySerializer = new NestedEntitySerializer(persistentEntities, + new EmbeddedResourcesAssembler(persistentEntities, associations, mock(ExcerptProjector.class)), invoker); + OrderAwarePluginRegistry, Class> lookups = OrderAwarePluginRegistry.create(); + + SimpleModule module = new SimpleModule(); + + module.setSerializerModifier(new AssociationOmittingSerializerModifier(persistentEntities, associations, + nestedEntitySerializer, new LookupObjectSerializer(lookups))); + module.setDeserializerModifier(new AssociationUriResolvingDeserializerModifier(persistentEntities, associations, + converter, mock(RepositoryInvokerFactory.class))); this.mapper = new ObjectMapper(); this.mapper.registerModule(module); - } /** @@ -119,7 +137,7 @@ public class PersistentEntityJackson2ModuleUnitTests { PersistentProperty property = persistentEntities.getPersistentEntity(PetOwner.class) .getPersistentProperty("pet"); - when(associationLinks.isLinkableAssociation(property)).thenReturn(true); + when(associations.isLinkableAssociation(property)).thenReturn(true); when(converter.convert(new UriTemplate("/pets/1").expand(), TypeDescriptor.valueOf(URI.class), TypeDescriptor.valueOf(Pet.class))).thenReturn(new Cat()); diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverterUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverterUnitTests.java index 73128c568..03ef4e77c 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverterUnitTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverterUnitTests.java @@ -17,6 +17,7 @@ package org.springframework.data.rest.webmvc.json; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; +import static org.mockito.Mockito.*; import java.util.ArrayList; import java.util.List; @@ -33,11 +34,12 @@ import org.springframework.context.support.MessageSourceAccessor; import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.rest.core.config.JsonSchemaFormat; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; -import org.springframework.data.rest.core.mapping.RepositoryResourceMappings; import org.springframework.data.rest.webmvc.TestMvcClient; import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter; import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration; +import org.springframework.data.rest.webmvc.json.PersistentEntityToJsonSchemaConverter.ValueTypeSchemaPropertyCustomizerFactory; import org.springframework.data.rest.webmvc.json.PersistentEntityToJsonSchemaConverterUnitTests.TestConfiguration; +import org.springframework.data.rest.webmvc.mapping.AssociationLinks; import org.springframework.data.rest.webmvc.mongodb.MongoDbRepositoryConfig; import org.springframework.data.rest.webmvc.mongodb.Profile; import org.springframework.data.rest.webmvc.mongodb.User; @@ -60,10 +62,10 @@ import com.jayway.jsonpath.JsonPath; public class PersistentEntityToJsonSchemaConverterUnitTests { @Autowired @Qualifier("resourceDescriptionMessageSourceAccessor") MessageSourceAccessor accessor; - @Autowired RepositoryResourceMappings mappings; @Autowired RepositoryRestConfiguration configuration; @Autowired PersistentEntities entities; @Autowired @Qualifier("objectMapper") ObjectMapper objectMapper; + @Autowired AssociationLinks associations; @Configuration @Import(RepositoryRestMvcConfiguration.class) @@ -86,7 +88,10 @@ public class PersistentEntityToJsonSchemaConverterUnitTests { TestMvcClient.initWebTest(); - converter = new PersistentEntityToJsonSchemaConverter(entities, mappings, accessor, objectMapper, configuration); + ValueTypeSchemaPropertyCustomizerFactory customizerFactory = mock(ValueTypeSchemaPropertyCustomizerFactory.class); + + converter = new PersistentEntityToJsonSchemaConverter(entities, associations, accessor, objectMapper, configuration, + customizerFactory); } /** diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java index ed45748bf..801831019 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java @@ -40,18 +40,26 @@ import org.springframework.data.rest.core.mapping.RepositoryResourceMappings; import org.springframework.data.rest.core.support.DefaultSelfLinkProvider; 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.ResourceProcessorInvoker; import org.springframework.data.rest.webmvc.jpa.JpaRepositoryConfig; import org.springframework.data.rest.webmvc.jpa.Person; import org.springframework.data.rest.webmvc.jpa.PersonRepository; +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.AssociationLinks; +import org.springframework.data.rest.webmvc.mapping.LinkCollector; import org.springframework.data.rest.webmvc.mongodb.MongoDbRepositoryConfig; import org.springframework.data.rest.webmvc.spi.BackendIdConverter; import org.springframework.data.rest.webmvc.spi.BackendIdConverter.DefaultIdConverter; +import org.springframework.data.rest.webmvc.support.ExcerptProjector; import org.springframework.data.rest.webmvc.support.PagingAndSortingTemplateVariables; import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.format.support.FormattingConversionService; import org.springframework.hateoas.EntityLinks; import org.springframework.hateoas.RelProvider; +import org.springframework.hateoas.ResourceProcessor; import org.springframework.hateoas.core.EvoInflectorRelProvider; import org.springframework.hateoas.hal.Jackson2HalModule; import org.springframework.plugin.core.OrderAwarePluginRegistry; @@ -120,8 +128,19 @@ public class RepositoryTestsConfig { SelfLinkProvider selfLinkProvider = new DefaultSelfLinkProvider(persistentEntities(), entityLinks, Collections.> emptyList()); - return new PersistentEntityJackson2Module(mappings, persistentEntities(), config(), new UriToEntityConverter( - persistentEntities(), new DefaultRepositoryInvokerFactory(repositories()), repositories()), selfLinkProvider); + DefaultRepositoryInvokerFactory invokerFactory = new DefaultRepositoryInvokerFactory(repositories()); + UriToEntityConverter uriToEntityConverter = new UriToEntityConverter(persistentEntities(), invokerFactory, + repositories()); + + AssociationLinks associations = new AssociationLinks(mappings, config()); + LinkCollector collector = new LinkCollector(persistentEntities(), selfLinkProvider, associations); + + NestedEntitySerializer nestedEntitySerializer = new NestedEntitySerializer(persistentEntities(), + new EmbeddedResourcesAssembler(persistentEntities(), associations, mock(ExcerptProjector.class)), + new ResourceProcessorInvoker(Collections.> emptyList())); + + return new PersistentEntityJackson2Module(associations, persistentEntities(), uriToEntityConverter, collector, + invokerFactory, nestedEntitySerializer, mock(LookupObjectSerializer.class)); } @Bean