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