DATAREST-775 - Support for nested association links.

Tweaked custom Jackson serialization to make sure nested entities are rendered as resources so that links to related resources can be collected on nested levels as well.

Extracted EmbeddedResourcesAssembler from PersistentEntityResourceAssembler so that embedded resources can also be build for nested entities. Extracted a ResourceProcessorInvoker from ResourceProcessorHandlerMethodReturnValueHandler to allow ResourceSupport instances created for nested entities get the ResourceProcessor instance registered for them invoked as well.

ProjectionDefinitionRegistrar now also allows the domain type of a repository being registered as excerpt, too.

Related tickets: DATAREST-776.
This commit is contained in:
Oliver Gierke
2016-02-19 08:42:48 +01:00
parent 6b9f274388
commit 897bc88d69
38 changed files with 1713 additions and 975 deletions

View File

@@ -36,7 +36,6 @@
<springdata.cassandra>1.4.0.BUILD-SNAPSHOT</springdata.cassandra>
<hibernate.version>4.3.10.Final</hibernate.version>
<bundlor.enabled>false</bundlor.enabled>
</properties>

View File

@@ -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<Object, Serializable, Repository<? extends Object, ?>>> lookupInformation = new ArrayList<LookupInformation<Object, Serializable, Repository<?, ?>>>();
private final List<Class<?>> lookupTypes = new ArrayList<Class<?>>();
/*
* (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 <T, ID extends Serializable, R extends Repository<T, ?>> IdMappingRegistrar<T, R> forValueRepository(
Class<R> 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<T, ID, R>(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 <T, ID extends Serializable, R extends Repository<T, ?>> EntityLookupRegistrar forValueRepository(
Class<R> type, Converter<T, ID> identifierMapping, Lookup<R, ID> 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}.
*

View File

@@ -38,15 +38,7 @@ public interface EntityLookupRegistrar {
*/
<T, ID extends Serializable, R extends Repository<T, ?>> IdMappingRegistrar<T, R> forRepository(Class<R> 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}.
*/
<T, ID extends Serializable, R extends Repository<T, ?>> EntityLookupRegistrar forRepository(Class<R> type,
Converter<T, ID> identifierMapping, Lookup<R, ID> lookup);
<T, ID extends Serializable, R extends Repository<T, ?>> IdMappingRegistrar<T, R> forValueRepository(Class<R> type);
interface IdMappingRegistrar<T, R extends Repository<T, ?>> {
@@ -59,6 +51,19 @@ public interface EntityLookupRegistrar {
<ID extends Serializable> LookupRegistrar<T, ID, R> withIdMapping(Converter<T, ID> 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}.
*/
<T, ID extends Serializable, R extends Repository<T, ?>> EntityLookupRegistrar forRepository(Class<R> type,
Converter<T, ID> identifierMapping, Lookup<R, ID> lookup);
<T, ID extends Serializable, R extends Repository<T, ?>> EntityLookupRegistrar forValueRepository(Class<R> type,
Converter<T, ID> identifierMapping, Lookup<R, ID> lookup);
interface LookupRegistrar<T, ID extends Serializable, R extends Repository<T, ?>> {
/**

View File

@@ -61,6 +61,7 @@ public class RepositoryRestConfiguration {
private final ProjectionDefinitionConfiguration projectionConfiguration;
private final MetadataConfiguration metadataConfiguration;
private final EntityLookupConfiguration entityLookupConfiguration;
private final List<Class<?>> valueTypes = new ArrayList<Class<?>>();
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);
}
}

View File

@@ -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<EmbeddedWrapper> getEmbeddedResources(Object instance) {
Assert.notNull(instance, "Entity instance must not be null!");
PersistentEntity<?, ?> entity = entities.getPersistentEntity(instance.getClass());
final List<EmbeddedWrapper> associationProjections = new ArrayList<EmbeddedWrapper>();
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<? extends PersistentProperty<?>> 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<Object> nestedCollection = new ArrayList<Object>();
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;
}
}

View File

@@ -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<Object> {
private final PersistentEntity<?, ?> entity;
private final Iterable<EmbeddedWrapper> 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<Object> {
* @param embeddeds can be {@literal null}.
*/
private PersistentEntityResource(PersistentEntity<?, ?> entity, Object content, Iterable<Link> links,
Iterable<EmbeddedWrapper> embeddeds, boolean isNew) {
Iterable<EmbeddedWrapper> embeddeds, boolean isNew, boolean nested) {
super(content, links);
@@ -64,6 +74,7 @@ public class PersistentEntityResource extends Resource<Object> {
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<Object> {
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<Object> {
return this;
}
public Builder withLinks(List<Link> 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<Object> {
* @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);
}
}

View File

@@ -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<Object, PersistentEntityResource> {
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<Obje
* @return
*/
private Iterable<EmbeddedWrapper> getEmbeddedResources(Object instance) {
Assert.notNull(instance, "Entity instance must not be null!");
PersistentEntity<?, ?> entity = entities.getPersistentEntity(instance.getClass());
final List<EmbeddedWrapper> associationProjections = new ArrayList<EmbeddedWrapper>();
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<? extends PersistentProperty<?>> 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<Object> nestedCollection = new ArrayList<Object>();
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);
}
/**

View File

@@ -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<HandlerMethodArgumentResolver> argumentResolvers,
List<ResourceProcessor<?>> resourceProcessors) {
ResourceProcessorInvoker invoker) {
super(resourceProcessors);
super(invoker);
this.argumentResolvers = argumentResolvers;
}

View File

@@ -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<ProcessorWrapper> 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<ResourceProcessor<?>> processors) {
Assert.notNull(delegate, "Delegate must not be null!");
Assert.notNull(processors, "ResourceProcessors must not be null!");
this.delegate = delegate;
this.processors = new ArrayList<ProcessorWrapper>();
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<Object> result = new ArrayList<Object>(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<ResourceSupport>) 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.<Class<?>> 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);
}
}
}

View File

@@ -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<ProcessorWrapper> 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<ResourceProcessor<?>> processors) {
Assert.notNull(processors, "ResourceProcessors must not be null!");
this.processors = new ArrayList<ProcessorWrapper>();
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 extends ResourceSupport> 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 extends ResourceSupport> 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<Object> result = new ArrayList<Object>(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<ResourceSupport>) 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.<Class<?>> 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);
}
}
}

View File

@@ -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<ResourceProcessor<?>> 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<ResourceProcessor<?>> 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<HandlerMethodReturnValueHandler> newHandlers = new ArrayList<HandlerMethodReturnValueHandler>();
newHandlers.add(new ResourceProcessorHandlerMethodReturnValueHandler(oldHandlers, resourcesProcessors));
newHandlers.add(new ResourceProcessorHandlerMethodReturnValueHandler(oldHandlers, invoker));
// Configure the new handler to be used
this.setReturnValueHandlers(newHandlers);

View File

@@ -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<HttpMethod> 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<Descriptor> nestedDescriptors = new ArrayList<Descriptor>();
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.<Descriptor> emptyList();
}
@@ -316,8 +293,7 @@ public class RootResourceInformationToAlpsDescriptorConverter {
final PersistentEntity<?, ?> entity = persistentEntities.getPersistentEntity(type);
final List<Descriptor> propertyDescriptors = new ArrayList<Descriptor>();
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<Descriptor> buildSearchResourceDescriptors(PersistentEntity<?, ?> entity) {
ResourceMetadata metadata = mappings.getMetadataFor(entity.getType());
ResourceMetadata metadata = associations.getMetadataFor(entity.getType());
List<Descriptor> descriptors = new ArrayList<Descriptor>();
for (MethodResourceMapping methodMapping : metadata.getSearchResourceMappings()) {

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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<ResourceProcessor> 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<EntityLookup<?>> getEntityLookups() {
List<EntityLookup<?>> lookups = new ArrayList<EntityLookup<?>>();
@@ -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());
}

View File

@@ -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.

View File

@@ -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<Object> resourceToRender = new Resource<Object>(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<Object> 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<Object> {
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<Object> resources = new ArrayList<Object>();
for (Object element : source) {
resources.add(toResource(element));
}
provider.defaultSerializeValue(resources, gen);
} else {
provider.defaultSerializeValue(toResource(value), gen);
}
}
private Resource<Object> 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<SettableBeanProperty> 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<Object> 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<TargetAware> {
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<TargetAware> 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.<Link> 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<Link> 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<Link> result = new ArrayList<Link>(existingLinks);
result.addAll(handler.getLinks());
return addSelfLinkIfNecessary(object, result);
}
private Links addSelfLinkIfNecessary(Object object, List<Link> existing) {
Links result = new Links(existing);
if (result.hasLink(Link.REL_SELF)) {
return result;
}
List<Link> list = new ArrayList<Link>();
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<Object> {
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<EntityLookup<?>, 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<Object> lookup = (EntityLookup<Object>) lookups.getPluginFor(value.getClass());
return lookup.getResourceIdentifier(value).toString();
}
}
}

View File

@@ -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<ConvertiblePair> convertiblePairs = new HashSet<ConvertiblePair>();
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<AbstractJsonSchemaProperty<?>> 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<AbstractJsonSchemaProperty<?>> 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.<AbstractJsonSchemaProperty<?>> 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<AbstractJsonSchemaProperty<?>> 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<AbstractJsonSchemaProperty<?>> 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<String> result = new ArrayList<String>();
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.

View File

@@ -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<? extends PersistentProperty<?>> 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();
}
}

View File

@@ -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<Link> 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<Link>();
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);
}
}
}

View File

@@ -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.<Link> 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<Link> 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<Link> result = new ArrayList<Link>(existingLinks);
result.addAll(handler.getLinks());
return addSelfLinkIfNecessary(object, result);
}
public Links getLinksForNested(Object object, List<Link> existing) {
PersistentEntity<?, ?> entity = entities.getPersistentEntity(object.getClass());
NestedLinkCollectingAssociationHandler handler = new NestedLinkCollectingAssociationHandler(links,
entity.getPropertyAccessor(object), associationLinks);
entity.doWithAssociations(handler);
List<Link> links = new ArrayList<Link>();
links.addAll(existing);
links.addAll(handler.getLinks());
return new Links(links);
}
private Links addSelfLinkIfNecessary(Object object, List<Link> existing) {
Links result = new Links(existing);
if (result.hasLink(Link.REL_SELF)) {
return result;
}
List<Link> list = new ArrayList<Link>();
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<Link> links = new ArrayList<Link>();
/**
* Returns the links collected after the {@link Association} has been traversed.
*
* @return the links
*/
public List<Link> getLinks() {
return links;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.SimpleAssociationHandler#doWithAssociation(org.springframework.data.mapping.Association)
*/
@Override
public void doWithAssociation(final Association<? extends PersistentProperty<?>> 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<Link> links = new ArrayList<Link>();
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.SimpleAssociationHandler#doWithAssociation(org.springframework.data.mapping.Association)
*/
@Override
public void doWithAssociation(Association<? extends PersistentProperty<?>> 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<Object> asCollection(Object object) {
if (object instanceof Collection) {
return (Collection<Object>) object;
}
return Collections.singleton(object);
}
}
}

View File

@@ -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<Link> links = new ArrayList<Link>();
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.SimpleAssociationHandler#doWithAssociation(org.springframework.data.mapping.Association)
*/
@Override
public void doWithAssociation(Association<? extends PersistentProperty<?>> 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()));
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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<BackendIdConverter, Class<?>> 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<BackendIdConverter, Class<?>> 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<BackendIdConverter, Class<?>> idConverters;
/*
* (non-Javadoc)

View File

@@ -62,8 +62,8 @@ public abstract class AbstractControllerIntegrationTests {
SelfLinkProvider selfLinkProvider = new DefaultSelfLinkProvider(persistentEntities(), entityLinks(),
Collections.<EntityLookup<?>> emptyList());
return new PersistentEntityResourceAssembler(persistentEntities(), selfLinkProvider, StubProjector.INSTANCE,
resourceMappings());
return new PersistentEntityResourceAssembler(persistentEntities(), StubProjector.INSTANCE, associationLinks(),
selfLinkProvider);
}
}

View File

@@ -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;

View File

@@ -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.<EntityLookup<?>> emptyList()), projector,
mappings);
PersistentEntityResourceAssembler assembler = new PersistentEntityResourceAssembler(entities, projector,
associations, new DefaultSelfLinkProvider(entities, entityLinks, Collections.<EntityLookup<?>> 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.<Link> iterableWithSize(2)));
assertThat(links.getLink("self").getVariables(), is(Matchers.empty()));

View File

@@ -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));
}

View File

@@ -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";

View File

@@ -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();

View File

@@ -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<Person> 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<Person> siblings = new ArrayList<Person>();
@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<Person>();
}
siblings.add(p);
return this;
}
@ManyToMany
public List<Person> 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;
}

View File

@@ -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)));
}
/**

View File

@@ -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.<ResourceProcessor<?>> emptyList());
module.setDeserializerModifier(new PersistentEntityJackson2Module.AssociationUriResolvingDeserializerModifier(
persistentEntities, converter, associationLinks));
NestedEntitySerializer nestedEntitySerializer = new NestedEntitySerializer(persistentEntities,
new EmbeddedResourcesAssembler(persistentEntities, associations, mock(ExcerptProjector.class)), invoker);
OrderAwarePluginRegistry<EntityLookup<?>, 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());

View File

@@ -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);
}
/**

View File

@@ -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.<EntityLookup<?>> 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.<ResourceProcessor<?>> emptyList()));
return new PersistentEntityJackson2Module(associations, persistentEntities(), uriToEntityConverter, collector,
invokerFactory, nestedEntitySerializer, mock(LookupObjectSerializer.class));
}
@Bean