Skip to content

Commit 28e8b6d

Browse files
committed
#1590 - Support for explicit type information on CollectionModel.
We now allow explicit definition of a fallback collection element type on CollectionModel to be used in cases of an empty model, so that the RepresentationModelProcessor infrastructure can still reason about the element type and also invoke the processor for empty collection models. Fixes #1590.
1 parent db1cd5d commit 28e8b6d

File tree

8 files changed

+384
-24
lines changed

8 files changed

+384
-24
lines changed

src/main/asciidoc/fundamentals.adoc

+13
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,16 @@ Collection<Person> people = Collections.singleton(new Person("Dave", "Matthews")
195195
CollectionModel<Person> model = CollectionModel.of(people);
196196
----
197197
====
198+
199+
While an `EntityModel` is constrained to always contain a payload and thus allows to reason about the type arrangement on the sole instance, a ``CollectionModel``'s underlying collection can be empty.
200+
Due to Java's type erasure, we cannot actually detect that a `CollectionModel<Person> model = CollectionModel.empty()` is actually a `CollectionModel<Person>` because all we see is the runtime instance and an empty collection.
201+
That missing type information can be added to the model by either adding it to the empty instance on construction via `CollectionModel.empty(Person.class)` or as fallback in case the underlying collection might be empty:
202+
203+
====
204+
[source, java]
205+
----
206+
Iterable<Person> people = repository.findAll();
207+
var model = CollectionModel.of(people).withFallbackType(Person.class);
208+
----
209+
====
210+

src/main/asciidoc/server.adoc

+9-1
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,6 @@ include::{resource-dir}/docs/order-plain.json[]
548548

549549
You wish to add a link so the client can make payment, but don't want to mix details about your `PaymentController` into
550550
the `OrderController`.
551-
552551
Instead of polluting the details of your ordering system, you can write a `RepresentationModelProcessor` like this:
553552

554553
====
@@ -590,6 +589,15 @@ This example is quite simple, but you can easily:
590589
Also, in this example, the `PaymentProcessor` alters the provided `EntityModel<Order>`. You also have the power to
591590
_replace_ it with another object. Just be advised the API requires the return type to equal the input type.
592591

592+
[[server.processors.empty-collections]]
593+
=== Processing empty collection models
594+
595+
To find the right set of ``RepresentationModelProcessor`` instance to invoke for a `RepresentationModel` instance, the invoking infrastructure performs a detailed analysis of the generics declaration of the ``RepresentationModelProcessor``s registered.
596+
For `CollectionModel` instances, this includes inspecting the elements of the underlying collection, as at runtime, the sole model instance does not expose generics information (due to Java's type erasure).
597+
That means, by default, `RepresentationModelProcessor` instances are not invoked for empty collection models.
598+
To still allow the infrastructure to deduce the payload types correctly, you can initialize empty `CollectionModel` instances with an explicit fallback payload type right from the start, or register it by calling `CollectionModel.withFallbackType(…)`.
599+
See <<fundamentals.collection-model>> for details.
600+
593601
[[server.rel-provider]]
594602
== [[spis.rel-provider]] Using the `LinkRelationProvider` API
595603

src/main/java/org/springframework/hateoas/CollectionModel.java

+153-7
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,17 @@
2020
import java.util.Collection;
2121
import java.util.Collections;
2222
import java.util.Iterator;
23+
import java.util.Objects;
2324

25+
import org.springframework.core.ParameterizedTypeReference;
26+
import org.springframework.core.ResolvableType;
27+
import org.springframework.core.ResolvableTypeProvider;
28+
import org.springframework.lang.NonNull;
2429
import org.springframework.lang.Nullable;
2530
import org.springframework.util.Assert;
31+
import org.springframework.util.ClassUtils;
2632

33+
import com.fasterxml.jackson.annotation.JsonIgnore;
2734
import com.fasterxml.jackson.annotation.JsonProperty;
2835

2936
/**
@@ -32,9 +39,12 @@
3239
* @author Oliver Gierke
3340
* @author Greg Turnquist
3441
*/
35-
public class CollectionModel<T> extends RepresentationModel<CollectionModel<T>> implements Iterable<T> {
42+
public class CollectionModel<T> extends RepresentationModel<CollectionModel<T>>
43+
implements Iterable<T>, ResolvableTypeProvider {
3644

3745
private final Collection<T> content;
46+
private final @Nullable ResolvableType fallbackType;
47+
private ResolvableType fullType;
3848

3949
/**
4050
* Creates an empty {@link CollectionModel} instance.
@@ -64,6 +74,10 @@ public CollectionModel(Iterable<T> content, Link... links) {
6474
*/
6575
@Deprecated
6676
public CollectionModel(Iterable<T> content, Iterable<Link> links) {
77+
this(content, links, null);
78+
}
79+
80+
protected CollectionModel(Iterable<T> content, Iterable<Link> links, @Nullable ResolvableType fallbackType) {
6781

6882
Assert.notNull(content, "Content must not be null!");
6983

@@ -74,6 +88,7 @@ public CollectionModel(Iterable<T> content, Iterable<Link> links) {
7488
}
7589

7690
this.add(links);
91+
this.fallbackType = fallbackType;
7792
}
7893

7994
/**
@@ -87,6 +102,42 @@ public static <T> CollectionModel<T> empty() {
87102
return of(Collections.emptyList());
88103
}
89104

105+
/**
106+
* Creates a new empty collection model with the given type defined as fallback type.
107+
*
108+
* @param <T>
109+
* @return
110+
* @since 1.4
111+
* @see #withFallbackType(Class, Class...)
112+
*/
113+
public static <T> CollectionModel<T> empty(Class<T> elementType, Class<?>... generics) {
114+
return empty(ResolvableType.forClassWithGenerics(elementType, generics));
115+
}
116+
117+
/**
118+
* Creates a new empty collection model with the given type defined as fallback type.
119+
*
120+
* @param <T>
121+
* @return
122+
* @since 1.4
123+
* @see #withFallbackType(ParameterizedTypeReference)
124+
*/
125+
public static <T> CollectionModel<T> empty(ParameterizedTypeReference<T> type) {
126+
return empty(ResolvableType.forType(type.getType()));
127+
}
128+
129+
/**
130+
* Creates a new empty collection model with the given type defined as fallback type.
131+
*
132+
* @param <T>
133+
* @return
134+
* @since 1.4
135+
* @see #withFallbackType(ResolvableType)
136+
*/
137+
public static <T> CollectionModel<T> empty(ResolvableType elementType) {
138+
return new CollectionModel<>(Collections.emptyList(), Collections.emptyList(), elementType);
139+
}
140+
90141
/**
91142
* Creates a new empty collection model with the given links.
92143
*
@@ -117,6 +168,8 @@ public static <T> CollectionModel<T> empty(Iterable<Link> links) {
117168
* @param content must not be {@literal null}.
118169
* @return
119170
* @since 1.1
171+
* @see #withFallbackType(Class)
172+
* @see #withFallbackType(ResolvableType)
120173
*/
121174
public static <T> CollectionModel<T> of(Iterable<T> content) {
122175
return of(content, Collections.emptyList());
@@ -129,6 +182,8 @@ public static <T> CollectionModel<T> of(Iterable<T> content) {
129182
* @param links the links to be added to the {@link CollectionModel}.
130183
* @return
131184
* @since 1.1
185+
* @see #withFallbackType(Class)
186+
* @see #withFallbackType(ResolvableType)
132187
*/
133188
public static <T> CollectionModel<T> of(Iterable<T> content, Link... links) {
134189
return of(content, Arrays.asList(links));
@@ -141,6 +196,8 @@ public static <T> CollectionModel<T> of(Iterable<T> content, Link... links) {
141196
* @param links the links to be added to the {@link CollectionModel}.
142197
* @return
143198
* @since 1.1
199+
* @see #withFallbackType(Class)
200+
* @see #withFallbackType(ResolvableType)
144201
*/
145202
public static <T> CollectionModel<T> of(Iterable<T> content, Iterable<Link> links) {
146203
return new CollectionModel<>(content, links);
@@ -177,6 +234,74 @@ public Collection<T> getContent() {
177234
return Collections.unmodifiableCollection(content);
178235
}
179236

237+
/**
238+
* Declares the given type as fallback element type in case the underlying collection is empty. This allows client
239+
* components to still apply type matches at runtime.
240+
*
241+
* @param type must not be {@literal null}.
242+
* @return will never be {@literal null}.
243+
* @since 1.4
244+
*/
245+
public CollectionModel<T> withFallbackType(Class<? super T> type, Class<?>... generics) {
246+
247+
Assert.notNull(type, "Fallback type must not be null!");
248+
Assert.notNull(generics, "Generics must not be null!");
249+
250+
return withFallbackType(ResolvableType.forClassWithGenerics(type, generics));
251+
}
252+
253+
/**
254+
* Declares the given type as fallback element type in case the underlying collection is empty. This allows client
255+
* components to still apply type matches at runtime.
256+
*
257+
* @param type must not be {@literal null}.
258+
* @return will never be {@literal null}.
259+
* @since 1.4
260+
*/
261+
public CollectionModel<T> withFallbackType(ParameterizedTypeReference<?> type) {
262+
263+
Assert.notNull(type, "Fallback type must not be null!");
264+
265+
return withFallbackType(ResolvableType.forType(type));
266+
}
267+
268+
/**
269+
* Declares the given type as fallback element type in case the underlying collection is empty. This allows client
270+
* components to still apply type matches at runtime.
271+
*
272+
* @param type must not be {@literal null}.
273+
* @return will never be {@literal null}.
274+
* @since 1.4
275+
*/
276+
public CollectionModel<T> withFallbackType(ResolvableType type) {
277+
278+
Assert.notNull(type, "Fallback type must not be null!");
279+
280+
return new CollectionModel<>(content, getLinks(), type);
281+
}
282+
283+
/*
284+
* (non-Javadoc)
285+
* @see org.springframework.core.ResolvableTypeProvider#getResolvableType()
286+
*/
287+
@NonNull
288+
@Override
289+
@JsonIgnore
290+
public ResolvableType getResolvableType() {
291+
292+
if (fullType == null) {
293+
294+
ResolvableType elementType = deriveElementType(this.content, fallbackType);
295+
Class<?> type = this.getClass();
296+
297+
this.fullType = elementType == null || type.getTypeParameters().length == 0 //
298+
? ResolvableType.forClass(type) //
299+
: ResolvableType.forClassWithGenerics(type, elementType);
300+
}
301+
302+
return fullType;
303+
}
304+
180305
/*
181306
* (non-Javadoc)
182307
* @see java.lang.Iterable#iterator()
@@ -192,7 +317,9 @@ public Iterator<T> iterator() {
192317
*/
193318
@Override
194319
public String toString() {
195-
return String.format("CollectionModel { content: %s, %s }", getContent(), super.toString());
320+
321+
return String.format("CollectionModel { content: %s, fallbackType: %s, %s }", //
322+
getContent(), fallbackType, super.toString());
196323
}
197324

198325
/*
@@ -212,8 +339,9 @@ public boolean equals(@Nullable Object obj) {
212339

213340
CollectionModel<?> that = (CollectionModel<?>) obj;
214341

215-
boolean contentEqual = this.content == null ? that.content == null : this.content.equals(that.content);
216-
return contentEqual && super.equals(obj);
342+
return Objects.equals(this.content, that.content)
343+
&& Objects.equals(this.fallbackType, that.fallbackType)
344+
&& super.equals(obj);
217345
}
218346

219347
/*
@@ -222,10 +350,28 @@ public boolean equals(@Nullable Object obj) {
222350
*/
223351
@Override
224352
public int hashCode() {
353+
return super.hashCode() + Objects.hash(content, fallbackType);
354+
}
225355

226-
int result = super.hashCode();
227-
result += content == null ? 0 : 17 * content.hashCode();
356+
/**
357+
* Determines the most common element type from the given elements defaulting to the given fallback type.
358+
*
359+
* @param elements must not be {@literal null}.
360+
* @param fallbackType can be {@literal null}.
361+
* @return
362+
*/
363+
@Nullable
364+
private static ResolvableType deriveElementType(Collection<?> elements, @Nullable ResolvableType fallbackType) {
365+
366+
if (elements.isEmpty()) {
367+
return fallbackType;
368+
}
228369

229-
return result;
370+
return elements.stream()
371+
.filter(it -> it != null)
372+
.<Class<?>> map(Object::getClass)
373+
.reduce(ClassUtils::determineCommonAncestor)
374+
.map(ResolvableType::forClass)
375+
.orElse(fallbackType);
230376
}
231377
}

0 commit comments

Comments
 (0)