Skip to content

Commit 161a3a9

Browse files
committed
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.
1 parent 0c24113 commit 161a3a9

File tree

8 files changed

+311
-223
lines changed

8 files changed

+311
-223
lines changed

spring-data-rest-tests/spring-data-rest-tests-core/src/test/java/org/springframework/data/rest/tests/RepositoryTestsConfig.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module;
4444
import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module.LookupObjectSerializer;
4545
import org.springframework.data.rest.webmvc.mapping.Associations;
46+
import org.springframework.data.rest.webmvc.mapping.DefaultLinkCollector;
4647
import org.springframework.data.rest.webmvc.mapping.LinkCollector;
4748
import org.springframework.data.rest.webmvc.spi.BackendIdConverter.DefaultIdConverter;
4849
import org.springframework.data.rest.webmvc.support.ExcerptProjector;
@@ -120,7 +121,7 @@ public Module persistentEntityModule() {
120121
repositories());
121122

122123
Associations associations = new Associations(mappings, config());
123-
LinkCollector collector = new LinkCollector(persistentEntities(), selfLinkProvider, associations);
124+
LinkCollector collector = new DefaultLinkCollector(persistentEntities(), selfLinkProvider, associations);
124125

125126
return new PersistentEntityJackson2Module(associations, persistentEntities(), uriToEntityConverter, collector,
126127
invokerFactory, mock(LookupObjectSerializer.class),

spring-data-rest-tests/spring-data-rest-tests-jpa/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import org.springframework.data.rest.webmvc.jpa.PersonRepository;
4444
import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module.LookupObjectSerializer;
4545
import org.springframework.data.rest.webmvc.mapping.Associations;
46+
import org.springframework.data.rest.webmvc.mapping.DefaultLinkCollector;
4647
import org.springframework.data.rest.webmvc.mapping.LinkCollector;
4748
import org.springframework.data.rest.webmvc.spi.BackendIdConverter.DefaultIdConverter;
4849
import org.springframework.data.rest.webmvc.support.ExcerptProjector;
@@ -127,7 +128,7 @@ public Module persistentEntityModule() {
127128
repositories());
128129

129130
Associations associations = new Associations(mappings, config());
130-
LinkCollector collector = new LinkCollector(persistentEntities(), selfLinkProvider, associations);
131+
LinkCollector collector = new DefaultLinkCollector(persistentEntities(), selfLinkProvider, associations);
131132

132133
return new PersistentEntityJackson2Module(associations, persistentEntities(), uriToEntityConverter, collector,
133134
invokerFactory, mock(LookupObjectSerializer.class),

spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestConfigurer.java

+12
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.springframework.data.auditing.AuditableBeanWrapperFactory;
2424
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
2525
import org.springframework.data.rest.core.event.ValidatingRepositoryEventListener;
26+
import org.springframework.data.rest.webmvc.mapping.LinkCollector;
2627
import org.springframework.http.converter.HttpMessageConverter;
2728
import org.springframework.util.Assert;
2829
import org.springframework.web.servlet.config.annotation.CorsRegistry;
@@ -144,4 +145,15 @@ default void configureJacksonObjectMapper(ObjectMapper objectMapper) {}
144145
default AuditableBeanWrapperFactory customizeAuditableBeanWrapperFactory(AuditableBeanWrapperFactory factory) {
145146
return factory;
146147
}
148+
149+
/**
150+
* Customize the {@link LinkCollector} to be used.
151+
*
152+
* @param collector will never be {@literal null}.
153+
* @return must not be {@literal null}.
154+
* @since 3.5
155+
*/
156+
default LinkCollector customizeLinkCollector(LinkCollector collector) {
157+
return collector;
158+
}
147159
}

spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestConfigurerDelegate.java

+15
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.springframework.data.auditing.AuditableBeanWrapperFactory;
2222
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
2323
import org.springframework.data.rest.core.event.ValidatingRepositoryEventListener;
24+
import org.springframework.data.rest.webmvc.mapping.LinkCollector;
2425
import org.springframework.http.converter.HttpMessageConverter;
2526
import org.springframework.util.Assert;
2627
import org.springframework.web.servlet.config.annotation.CorsRegistry;
@@ -136,4 +137,18 @@ public AuditableBeanWrapperFactory customizeAuditableBeanWrapperFactory(Auditabl
136137

137138
return factory;
138139
}
140+
141+
/*
142+
* (non-Javadoc)
143+
* @see org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer#customizeLinkCollector(org.springframework.data.rest.webmvc.mapping.LinkCollector)
144+
*/
145+
@Override
146+
public LinkCollector customizeLinkCollector(LinkCollector collector) {
147+
148+
for (RepositoryRestConfigurer configurer : delegates) {
149+
collector = configurer.customizeLinkCollector(collector);
150+
}
151+
152+
return collector;
153+
}
139154
}

spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module.LookupObjectSerializer;
7575
import org.springframework.data.rest.webmvc.json.PersistentEntityToJsonSchemaConverter.ValueTypeSchemaPropertyCustomizerFactory;
7676
import org.springframework.data.rest.webmvc.mapping.Associations;
77+
import org.springframework.data.rest.webmvc.mapping.DefaultLinkCollector;
7778
import org.springframework.data.rest.webmvc.mapping.LinkCollector;
7879
import org.springframework.data.rest.webmvc.spi.BackendIdConverter;
7980
import org.springframework.data.rest.webmvc.spi.BackendIdConverter.DefaultIdConverter;
@@ -727,7 +728,9 @@ protected Module persistentEntityJackson2Module(LinkCollector linkCollector) {
727728
@Bean
728729
protected LinkCollector linkCollector(PersistentEntities persistentEntities, SelfLinkProvider selfLinkProvider,
729730
Associations associationLinks) {
730-
return new LinkCollector(persistentEntities, selfLinkProvider, associationLinks);
731+
732+
return configurerDelegate.get()
733+
.customizeLinkCollector(new DefaultLinkCollector(persistentEntities, selfLinkProvider, associationLinks));
731734
}
732735

733736
@Bean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.rest.webmvc.mapping;
17+
18+
import java.util.ArrayList;
19+
import java.util.Collection;
20+
import java.util.Collections;
21+
import java.util.List;
22+
23+
import org.springframework.data.mapping.Association;
24+
import org.springframework.data.mapping.MappingException;
25+
import org.springframework.data.mapping.PersistentEntity;
26+
import org.springframework.data.mapping.PersistentProperty;
27+
import org.springframework.data.mapping.PersistentPropertyAccessor;
28+
import org.springframework.data.mapping.SimpleAssociationHandler;
29+
import org.springframework.data.mapping.context.PersistentEntities;
30+
import org.springframework.data.rest.core.Path;
31+
import org.springframework.data.rest.core.mapping.ResourceMapping;
32+
import org.springframework.data.rest.core.mapping.ResourceMetadata;
33+
import org.springframework.data.rest.core.support.SelfLinkProvider;
34+
import org.springframework.hateoas.IanaLinkRelations;
35+
import org.springframework.hateoas.Link;
36+
import org.springframework.hateoas.Links;
37+
import org.springframework.util.Assert;
38+
39+
/**
40+
* A service to collect all standard links that need to be added to a certain object.
41+
*
42+
* @author Oliver Drotbohm
43+
* @since 3.6
44+
*/
45+
public class DefaultLinkCollector implements LinkCollector {
46+
47+
private final PersistentEntities entities;
48+
private final Associations associationLinks;
49+
private final SelfLinkProvider links;
50+
51+
/**
52+
* Creates a new {@link DefaultLinkCollector} for the given {@link PersistentEntities}, {@link SelfLinkProvider} and
53+
* {@link Associations}.
54+
*
55+
* @param entities must not be {@literal null}.
56+
* @param linkProvider must not be {@literal null}.
57+
* @param associationLinks must not be {@literal null}.
58+
*/
59+
public DefaultLinkCollector(PersistentEntities entities, SelfLinkProvider linkProvider,
60+
Associations associationLinks) {
61+
62+
Assert.notNull(entities, "PersistentEntities must not be null!");
63+
Assert.notNull(linkProvider, "SelfLinkProvider must not be null!");
64+
Assert.notNull(associationLinks, "AssociationLinks must not be null!");
65+
66+
this.links = linkProvider;
67+
this.entities = entities;
68+
this.associationLinks = associationLinks;
69+
}
70+
71+
/**
72+
* Returns all {@link Links} for the given object.
73+
*
74+
* @param object must not be {@literal null}.
75+
* @return
76+
*/
77+
@Override
78+
public Links getLinksFor(Object object) {
79+
return getLinksFor(object, Links.NONE);
80+
}
81+
82+
/**
83+
* Returns all {@link Links} for the given object and already existing {@link Link}.
84+
*
85+
* @param object must not be {@literal null}.
86+
* @param existingLinks must not be {@literal null}.
87+
* @return
88+
*/
89+
@Override
90+
public Links getLinksFor(Object object, Links existingLinks) {
91+
92+
Assert.notNull(object, "Object must not be null!");
93+
Assert.notNull(existingLinks, "Existing links must not be null!");
94+
95+
Link selfLink = createSelfLink(object, existingLinks);
96+
97+
if (selfLink == null) {
98+
return existingLinks;
99+
}
100+
101+
Path path = new Path(selfLink.expand().getHref());
102+
103+
LinkCollectingAssociationHandler handler = new LinkCollectingAssociationHandler(path, associationLinks);
104+
entities.getRequiredPersistentEntity(object.getClass()).doWithAssociations(handler);
105+
106+
return addSelfLinkIfNecessary(object, existingLinks.and(handler.getLinks()));
107+
}
108+
109+
@Override
110+
public Links getLinksForNested(Object object, Links existing) {
111+
112+
PersistentEntity<?, ?> entity = entities.getRequiredPersistentEntity(object.getClass());
113+
114+
NestedLinkCollectingAssociationHandler handler = new NestedLinkCollectingAssociationHandler(links,
115+
entity.getPropertyAccessor(object), associationLinks);
116+
entity.doWithAssociations(handler);
117+
118+
return existing.and(handler.getLinks());
119+
}
120+
121+
private Links addSelfLinkIfNecessary(Object object, Links existing) {
122+
return existing.andIf(!existing.hasLink(IanaLinkRelations.SELF),
123+
() -> links.createSelfLinkFor(object).withSelfRel());
124+
}
125+
126+
private Link createSelfLink(Object object, Links existing) {
127+
128+
return existing.getLink(IanaLinkRelations.SELF) //
129+
.orElseGet(() -> links.createSelfLinkFor(object).withSelfRel());
130+
}
131+
132+
/**
133+
* {@link SimpleAssociationHandler} that will collect {@link Link}s for all linkable associations.
134+
*
135+
* @author Oliver Gierke
136+
* @since 2.1
137+
*/
138+
private static class LinkCollectingAssociationHandler implements SimpleAssociationHandler {
139+
140+
private static final String AMBIGUOUS_ASSOCIATIONS = "Detected multiple association links with same relation type! Disambiguate association %s using @RestResource!";
141+
142+
private final Path basePath;
143+
private final Associations associationLinks;
144+
private final List<Link> links = new ArrayList<Link>();
145+
146+
public LinkCollectingAssociationHandler(Path basePath, Associations associationLinks) {
147+
148+
Assert.notNull(basePath, "Base Path must not be null!");
149+
Assert.notNull(associationLinks, "Associations must not be null!");
150+
151+
this.basePath = basePath;
152+
this.associationLinks = associationLinks;
153+
}
154+
155+
/**
156+
* Returns the links collected after the {@link Association} has been traversed.
157+
*
158+
* @return the links
159+
*/
160+
public Links getLinks() {
161+
return Links.of(links);
162+
}
163+
164+
/*
165+
* (non-Javadoc)
166+
* @see org.springframework.data.mapping.SimpleAssociationHandler#doWithAssociation(org.springframework.data.mapping.Association)
167+
*/
168+
@Override
169+
public void doWithAssociation(final Association<? extends PersistentProperty<?>> association) {
170+
171+
if (associationLinks.isLinkableAssociation(association)) {
172+
173+
PersistentProperty<?> property = association.getInverse();
174+
Links existingLinks = Links.of(links);
175+
176+
for (Link link : associationLinks.getLinksFor(association, basePath)) {
177+
if (existingLinks.hasLink(link.getRel())) {
178+
throw new MappingException(String.format(AMBIGUOUS_ASSOCIATIONS, property.toString()));
179+
} else {
180+
links.add(link);
181+
}
182+
}
183+
}
184+
}
185+
}
186+
187+
private static class NestedLinkCollectingAssociationHandler implements SimpleAssociationHandler {
188+
189+
private final SelfLinkProvider selfLinks;
190+
private final PersistentPropertyAccessor<?> accessor;
191+
private final Associations associations;
192+
private Links links = Links.NONE;
193+
194+
public NestedLinkCollectingAssociationHandler(SelfLinkProvider selfLinks,
195+
PersistentPropertyAccessor<?> accessor, Associations associations) {
196+
197+
Assert.notNull(selfLinks, "SelfLinkProvider must not be null!");
198+
Assert.notNull(accessor, "PersistentPropertyAccessor must not be null!");
199+
Assert.notNull(associations, "Associations must not be null!");
200+
201+
this.selfLinks = selfLinks;
202+
this.accessor = accessor;
203+
this.associations = associations;
204+
}
205+
206+
public List<Link> getLinks() {
207+
return this.links.toList();
208+
}
209+
210+
/*
211+
* (non-Javadoc)
212+
* @see org.springframework.data.mapping.SimpleAssociationHandler#doWithAssociation(org.springframework.data.mapping.Association)
213+
*/
214+
@Override
215+
public void doWithAssociation(Association<? extends PersistentProperty<?>> association) {
216+
217+
if (!associations.isLinkableAssociation(association)) {
218+
return;
219+
}
220+
221+
PersistentProperty<?> property = association.getInverse();
222+
Object value = accessor.getProperty(property);
223+
224+
if (value == null) {
225+
return;
226+
}
227+
228+
ResourceMetadata metadata = associations.getMappings().getMetadataFor(property.getOwner().getType());
229+
ResourceMapping propertyMapping = metadata.getMappingFor(property);
230+
231+
for (Object element : asCollection(value)) {
232+
233+
links = links.andIf(element != null,
234+
() -> selfLinks.createSelfLinkFor(property.getAssociationTargetType(), element)
235+
.withRel(propertyMapping.getRel()));
236+
}
237+
}
238+
239+
/**
240+
* Returns the given object as {@link Collection}, i.e. the object as is if it's a collection already or wrapped
241+
* into a single-element collection otherwise.
242+
*
243+
* @param object can be {@literal null}.
244+
* @return
245+
*/
246+
@SuppressWarnings("unchecked")
247+
private static Collection<Object> asCollection(Object object) {
248+
249+
return object instanceof Collection //
250+
? (Collection<Object>) object //
251+
: Collections.singleton(object);
252+
}
253+
}
254+
}

0 commit comments

Comments
 (0)