Allow customization of LinkCollector.
LinkCollector is now an interface. The actual implementation has been moved to DefaultLinkCollector. RepositoryRestConfigurer now has a customizeLinkCollector(…) callback method to tweak or even completely replace the LinkCollector instance. Fixes #2042.
This commit is contained in:
@@ -43,6 +43,7 @@ import org.springframework.data.rest.webmvc.EmbeddedResourcesAssembler;
|
||||
import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module;
|
||||
import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module.LookupObjectSerializer;
|
||||
import org.springframework.data.rest.webmvc.mapping.Associations;
|
||||
import org.springframework.data.rest.webmvc.mapping.DefaultLinkCollector;
|
||||
import org.springframework.data.rest.webmvc.mapping.LinkCollector;
|
||||
import org.springframework.data.rest.webmvc.spi.BackendIdConverter.DefaultIdConverter;
|
||||
import org.springframework.data.rest.webmvc.support.ExcerptProjector;
|
||||
@@ -120,7 +121,7 @@ public class RepositoryTestsConfig {
|
||||
repositories());
|
||||
|
||||
Associations associations = new Associations(mappings, config());
|
||||
LinkCollector collector = new LinkCollector(persistentEntities(), selfLinkProvider, associations);
|
||||
LinkCollector collector = new DefaultLinkCollector(persistentEntities(), selfLinkProvider, associations);
|
||||
|
||||
return new PersistentEntityJackson2Module(associations, persistentEntities(), uriToEntityConverter, collector,
|
||||
invokerFactory, mock(LookupObjectSerializer.class),
|
||||
|
||||
@@ -43,6 +43,7 @@ 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.mapping.Associations;
|
||||
import org.springframework.data.rest.webmvc.mapping.DefaultLinkCollector;
|
||||
import org.springframework.data.rest.webmvc.mapping.LinkCollector;
|
||||
import org.springframework.data.rest.webmvc.spi.BackendIdConverter.DefaultIdConverter;
|
||||
import org.springframework.data.rest.webmvc.support.ExcerptProjector;
|
||||
@@ -127,7 +128,7 @@ public class RepositoryTestsConfig {
|
||||
repositories());
|
||||
|
||||
Associations associations = new Associations(mappings, config());
|
||||
LinkCollector collector = new LinkCollector(persistentEntities(), selfLinkProvider, associations);
|
||||
LinkCollector collector = new DefaultLinkCollector(persistentEntities(), selfLinkProvider, associations);
|
||||
|
||||
return new PersistentEntityJackson2Module(associations, persistentEntities(), uriToEntityConverter, collector,
|
||||
invokerFactory, mock(LookupObjectSerializer.class),
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.springframework.core.convert.support.ConfigurableConversionService;
|
||||
import org.springframework.data.auditing.AuditableBeanWrapperFactory;
|
||||
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
|
||||
import org.springframework.data.rest.core.event.ValidatingRepositoryEventListener;
|
||||
import org.springframework.data.rest.webmvc.mapping.LinkCollector;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
@@ -144,4 +145,15 @@ public interface RepositoryRestConfigurer {
|
||||
default AuditableBeanWrapperFactory customizeAuditableBeanWrapperFactory(AuditableBeanWrapperFactory factory) {
|
||||
return factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Customize the {@link LinkCollector} to be used.
|
||||
*
|
||||
* @param collector will never be {@literal null}.
|
||||
* @return must not be {@literal null}.
|
||||
* @since 3.5
|
||||
*/
|
||||
default LinkCollector customizeLinkCollector(LinkCollector collector) {
|
||||
return collector;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import org.springframework.core.convert.support.ConfigurableConversionService;
|
||||
import org.springframework.data.auditing.AuditableBeanWrapperFactory;
|
||||
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
|
||||
import org.springframework.data.rest.core.event.ValidatingRepositoryEventListener;
|
||||
import org.springframework.data.rest.webmvc.mapping.LinkCollector;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
@@ -136,4 +137,18 @@ class RepositoryRestConfigurerDelegate implements RepositoryRestConfigurer {
|
||||
|
||||
return factory;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer#customizeLinkCollector(org.springframework.data.rest.webmvc.mapping.LinkCollector)
|
||||
*/
|
||||
@Override
|
||||
public LinkCollector customizeLinkCollector(LinkCollector collector) {
|
||||
|
||||
for (RepositoryRestConfigurer configurer : delegates) {
|
||||
collector = configurer.customizeLinkCollector(collector);
|
||||
}
|
||||
|
||||
return collector;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ import org.springframework.data.rest.webmvc.json.*;
|
||||
import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module.LookupObjectSerializer;
|
||||
import org.springframework.data.rest.webmvc.json.PersistentEntityToJsonSchemaConverter.ValueTypeSchemaPropertyCustomizerFactory;
|
||||
import org.springframework.data.rest.webmvc.mapping.Associations;
|
||||
import org.springframework.data.rest.webmvc.mapping.DefaultLinkCollector;
|
||||
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;
|
||||
@@ -727,7 +728,9 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
|
||||
@Bean
|
||||
protected LinkCollector linkCollector(PersistentEntities persistentEntities, SelfLinkProvider selfLinkProvider,
|
||||
Associations associationLinks) {
|
||||
return new LinkCollector(persistentEntities, selfLinkProvider, associationLinks);
|
||||
|
||||
return configurerDelegate.get()
|
||||
.customizeLinkCollector(new DefaultLinkCollector(persistentEntities, selfLinkProvider, associationLinks));
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* Copyright 2021 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
|
||||
*
|
||||
* https://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 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.MappingException;
|
||||
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.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.IanaLinkRelations;
|
||||
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 Drotbohm
|
||||
* @since 3.6
|
||||
*/
|
||||
public class DefaultLinkCollector implements LinkCollector {
|
||||
|
||||
private final PersistentEntities entities;
|
||||
private final Associations associationLinks;
|
||||
private final SelfLinkProvider links;
|
||||
|
||||
/**
|
||||
* Creates a new {@link DefaultLinkCollector} for the given {@link PersistentEntities}, {@link SelfLinkProvider} and
|
||||
* {@link Associations}.
|
||||
*
|
||||
* @param entities must not be {@literal null}.
|
||||
* @param linkProvider must not be {@literal null}.
|
||||
* @param associationLinks must not be {@literal null}.
|
||||
*/
|
||||
public DefaultLinkCollector(PersistentEntities entities, SelfLinkProvider linkProvider,
|
||||
Associations 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
|
||||
*/
|
||||
@Override
|
||||
public Links getLinksFor(Object object) {
|
||||
return getLinksFor(object, Links.NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@Override
|
||||
public Links getLinksFor(Object object, Links existingLinks) {
|
||||
|
||||
Assert.notNull(object, "Object must not be null!");
|
||||
Assert.notNull(existingLinks, "Existing links must not be null!");
|
||||
|
||||
Link selfLink = createSelfLink(object, existingLinks);
|
||||
|
||||
if (selfLink == null) {
|
||||
return existingLinks;
|
||||
}
|
||||
|
||||
Path path = new Path(selfLink.expand().getHref());
|
||||
|
||||
LinkCollectingAssociationHandler handler = new LinkCollectingAssociationHandler(path, associationLinks);
|
||||
entities.getRequiredPersistentEntity(object.getClass()).doWithAssociations(handler);
|
||||
|
||||
return addSelfLinkIfNecessary(object, existingLinks.and(handler.getLinks()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Links getLinksForNested(Object object, Links existing) {
|
||||
|
||||
PersistentEntity<?, ?> entity = entities.getRequiredPersistentEntity(object.getClass());
|
||||
|
||||
NestedLinkCollectingAssociationHandler handler = new NestedLinkCollectingAssociationHandler(links,
|
||||
entity.getPropertyAccessor(object), associationLinks);
|
||||
entity.doWithAssociations(handler);
|
||||
|
||||
return existing.and(handler.getLinks());
|
||||
}
|
||||
|
||||
private Links addSelfLinkIfNecessary(Object object, Links existing) {
|
||||
return existing.andIf(!existing.hasLink(IanaLinkRelations.SELF),
|
||||
() -> links.createSelfLinkFor(object).withSelfRel());
|
||||
}
|
||||
|
||||
private Link createSelfLink(Object object, Links existing) {
|
||||
|
||||
return existing.getLink(IanaLinkRelations.SELF) //
|
||||
.orElseGet(() -> links.createSelfLinkFor(object).withSelfRel());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link SimpleAssociationHandler} that will collect {@link Link}s for all linkable associations.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @since 2.1
|
||||
*/
|
||||
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 Path basePath;
|
||||
private final Associations associationLinks;
|
||||
private final List<Link> links = new ArrayList<Link>();
|
||||
|
||||
public LinkCollectingAssociationHandler(Path basePath, Associations associationLinks) {
|
||||
|
||||
Assert.notNull(basePath, "Base Path must not be null!");
|
||||
Assert.notNull(associationLinks, "Associations must not be null!");
|
||||
|
||||
this.basePath = basePath;
|
||||
this.associationLinks = associationLinks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the links collected after the {@link Association} has been traversed.
|
||||
*
|
||||
* @return the links
|
||||
*/
|
||||
public Links getLinks() {
|
||||
return Links.of(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 = Links.of(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class NestedLinkCollectingAssociationHandler implements SimpleAssociationHandler {
|
||||
|
||||
private final SelfLinkProvider selfLinks;
|
||||
private final PersistentPropertyAccessor<?> accessor;
|
||||
private final Associations associations;
|
||||
private Links links = Links.NONE;
|
||||
|
||||
public NestedLinkCollectingAssociationHandler(SelfLinkProvider selfLinks,
|
||||
PersistentPropertyAccessor<?> accessor, Associations associations) {
|
||||
|
||||
Assert.notNull(selfLinks, "SelfLinkProvider must not be null!");
|
||||
Assert.notNull(accessor, "PersistentPropertyAccessor must not be null!");
|
||||
Assert.notNull(associations, "Associations must not be null!");
|
||||
|
||||
this.selfLinks = selfLinks;
|
||||
this.accessor = accessor;
|
||||
this.associations = associations;
|
||||
}
|
||||
|
||||
public List<Link> getLinks() {
|
||||
return this.links.toList();
|
||||
}
|
||||
|
||||
/*
|
||||
* (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)) {
|
||||
|
||||
links = links.andIf(element != null,
|
||||
() -> selfLinks.createSelfLinkFor(property.getAssociationTargetType(), element)
|
||||
.withRel(propertyMapping.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) {
|
||||
|
||||
return object instanceof Collection //
|
||||
? (Collection<Object>) object //
|
||||
: Collections.singleton(object);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,55 +15,15 @@
|
||||
*/
|
||||
package org.springframework.data.rest.webmvc.mapping;
|
||||
|
||||
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.MappingException;
|
||||
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.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.IanaLinkRelations;
|
||||
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 Associations associationLinks;
|
||||
private final SelfLinkProvider links;
|
||||
|
||||
/**
|
||||
* Creates a new {@link PersistentEntities}, {@link SelfLinkProvider} and {@link Associations}.
|
||||
*
|
||||
* @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, Associations 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;
|
||||
}
|
||||
public interface LinkCollector {
|
||||
|
||||
/**
|
||||
* Returns all {@link Links} for the given object.
|
||||
@@ -71,189 +31,16 @@ public class LinkCollector {
|
||||
* @param object must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public Links getLinksFor(Object object) {
|
||||
return getLinksFor(object, Links.NONE);
|
||||
}
|
||||
Links getLinksFor(Object object);
|
||||
|
||||
/**
|
||||
* 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}.
|
||||
* @param existing must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public Links getLinksFor(Object object, Links existingLinks) {
|
||||
Links getLinksFor(Object object, Links existing);
|
||||
|
||||
Assert.notNull(object, "Object must not be null!");
|
||||
Assert.notNull(existingLinks, "Existing links must not be null!");
|
||||
|
||||
Link selfLink = createSelfLink(object, existingLinks);
|
||||
|
||||
if (selfLink == null) {
|
||||
return existingLinks;
|
||||
}
|
||||
|
||||
Path path = new Path(selfLink.expand().getHref());
|
||||
|
||||
LinkCollectingAssociationHandler handler = new LinkCollectingAssociationHandler(path, associationLinks);
|
||||
entities.getRequiredPersistentEntity(object.getClass()).doWithAssociations(handler);
|
||||
|
||||
return addSelfLinkIfNecessary(object, existingLinks.and(handler.getLinks()));
|
||||
}
|
||||
|
||||
public Links getLinksForNested(Object object, Links existing) {
|
||||
|
||||
PersistentEntity<?, ?> entity = entities.getRequiredPersistentEntity(object.getClass());
|
||||
|
||||
NestedLinkCollectingAssociationHandler handler = new NestedLinkCollectingAssociationHandler(links,
|
||||
entity.getPropertyAccessor(object), associationLinks);
|
||||
entity.doWithAssociations(handler);
|
||||
|
||||
return existing.and(handler.getLinks());
|
||||
}
|
||||
|
||||
private Links addSelfLinkIfNecessary(Object object, Links existing) {
|
||||
return existing.andIf(!existing.hasLink(IanaLinkRelations.SELF),
|
||||
() -> links.createSelfLinkFor(object).withSelfRel());
|
||||
}
|
||||
|
||||
private Link createSelfLink(Object object, Links existing) {
|
||||
|
||||
return existing.getLink(IanaLinkRelations.SELF) //
|
||||
.orElseGet(() -> links.createSelfLinkFor(object).withSelfRel());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link SimpleAssociationHandler} that will collect {@link Link}s for all linkable associations.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @since 2.1
|
||||
*/
|
||||
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 Path basePath;
|
||||
private final Associations associationLinks;
|
||||
private final List<Link> links = new ArrayList<Link>();
|
||||
|
||||
public LinkCollectingAssociationHandler(Path basePath, Associations associationLinks) {
|
||||
|
||||
Assert.notNull(basePath, "Base Path must not be null!");
|
||||
Assert.notNull(associationLinks, "Associations must not be null!");
|
||||
|
||||
this.basePath = basePath;
|
||||
this.associationLinks = associationLinks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the links collected after the {@link Association} has been traversed.
|
||||
*
|
||||
* @return the links
|
||||
*/
|
||||
public Links getLinks() {
|
||||
return Links.of(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 = Links.of(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class NestedLinkCollectingAssociationHandler implements SimpleAssociationHandler {
|
||||
|
||||
private final SelfLinkProvider selfLinks;
|
||||
private final PersistentPropertyAccessor<?> accessor;
|
||||
private final Associations associations;
|
||||
private Links links = Links.NONE;
|
||||
|
||||
public NestedLinkCollectingAssociationHandler(SelfLinkProvider selfLinks,
|
||||
PersistentPropertyAccessor<?> accessor, Associations associations) {
|
||||
|
||||
Assert.notNull(selfLinks, "SelfLinkProvider must not be null!");
|
||||
Assert.notNull(accessor, "PersistentPropertyAccessor must not be null!");
|
||||
Assert.notNull(associations, "Associations must not be null!");
|
||||
|
||||
this.selfLinks = selfLinks;
|
||||
this.accessor = accessor;
|
||||
this.associations = associations;
|
||||
}
|
||||
|
||||
public List<Link> getLinks() {
|
||||
return this.links.toList();
|
||||
}
|
||||
|
||||
/*
|
||||
* (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)) {
|
||||
|
||||
links = links.andIf(element != null,
|
||||
() -> selfLinks.createSelfLinkFor(property.getAssociationTargetType(), element)
|
||||
.withRel(propertyMapping.getRel()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
|
||||
return object instanceof Collection //
|
||||
? (Collection<Object>) object //
|
||||
: Collections.singleton(object);
|
||||
}
|
||||
}
|
||||
Links getLinksForNested(Object object, Links existing);
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ import org.springframework.data.rest.webmvc.RepositoryLinksResource;
|
||||
import org.springframework.data.rest.webmvc.RestMediaTypes;
|
||||
import org.springframework.data.rest.webmvc.alps.AlpsJsonHttpMessageConverter;
|
||||
import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module;
|
||||
import org.springframework.data.rest.webmvc.mapping.LinkCollector;
|
||||
import org.springframework.data.util.Lazy;
|
||||
import org.springframework.data.util.Streamable;
|
||||
import org.springframework.data.web.HateoasPageableHandlerMethodArgumentResolver;
|
||||
@@ -221,6 +222,14 @@ public class RepositoryRestMvConfigurationIntegrationTests {
|
||||
assertThat(factory).isEqualTo(ExtendingConfiguration.auditableBeanWrapperFactory);
|
||||
}
|
||||
|
||||
@Test // #2042
|
||||
public void appliesLinkCollectorCustomizer() {
|
||||
|
||||
LinkCollector factory = context.getBean(LinkCollector.class);
|
||||
|
||||
assertThat(factory).isEqualTo(ExtendingConfiguration.collector);
|
||||
}
|
||||
|
||||
private static ObjectMapper getObjectMapper() {
|
||||
|
||||
AbstractJackson2HttpMessageConverter converter = context.getBean("halJacksonHttpMessageConverter",
|
||||
@@ -233,6 +242,7 @@ public class RepositoryRestMvConfigurationIntegrationTests {
|
||||
static class ExtendingConfiguration {
|
||||
|
||||
static AuditableBeanWrapperFactory auditableBeanWrapperFactory = mock(AuditableBeanWrapperFactory.class);
|
||||
static LinkCollector collector = mock(LinkCollector.class);
|
||||
|
||||
@Bean
|
||||
DefaultLinkRelationProvider relProvider() {
|
||||
@@ -265,8 +275,8 @@ public class RepositoryRestMvConfigurationIntegrationTests {
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(300) // #2040
|
||||
RepositoryRestConfigurer auditingBeanWrapperCustomizer() {
|
||||
@Order(300) // #2040, #2042
|
||||
RepositoryRestConfigurer customizer() {
|
||||
|
||||
return new RepositoryRestConfigurer() {
|
||||
|
||||
@@ -274,6 +284,11 @@ public class RepositoryRestMvConfigurationIntegrationTests {
|
||||
public AuditableBeanWrapperFactory customizeAuditableBeanWrapperFactory(AuditableBeanWrapperFactory factory) {
|
||||
return auditableBeanWrapperFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LinkCollector customizeLinkCollector(LinkCollector collector) {
|
||||
return ExtendingConfiguration.collector;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user