#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.
This commit is contained in:
@@ -182,6 +182,7 @@ EntityModel<Person> model = EntityModel.of(person);
|
||||
----
|
||||
====
|
||||
|
||||
[[fundamentals.collection-model]]
|
||||
=== Collection resource representation model
|
||||
|
||||
For resources that are conceptually collections, a `CollectionModel` is available.
|
||||
@@ -195,3 +196,16 @@ Collection<Person> people = Collections.singleton(new Person("Dave", "Matthews")
|
||||
CollectionModel<Person> model = CollectionModel.of(people);
|
||||
----
|
||||
====
|
||||
|
||||
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.
|
||||
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.
|
||||
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:
|
||||
|
||||
====
|
||||
[source, java]
|
||||
----
|
||||
Iterable<Person> people = repository.findAll();
|
||||
var model = CollectionModel.of(people).withFallbackType(Person.class);
|
||||
----
|
||||
====
|
||||
|
||||
|
||||
@@ -548,7 +548,6 @@ include::{resource-dir}/docs/order-plain.json[]
|
||||
|
||||
You wish to add a link so the client can make payment, but don't want to mix details about your `PaymentController` into
|
||||
the `OrderController`.
|
||||
|
||||
Instead of polluting the details of your ordering system, you can write a `RepresentationModelProcessor` like this:
|
||||
|
||||
====
|
||||
@@ -590,6 +589,15 @@ This example is quite simple, but you can easily:
|
||||
Also, in this example, the `PaymentProcessor` alters the provided `EntityModel<Order>`. You also have the power to
|
||||
_replace_ it with another object. Just be advised the API requires the return type to equal the input type.
|
||||
|
||||
[[server.processors.empty-collections]]
|
||||
=== Processing empty collection models
|
||||
|
||||
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.
|
||||
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).
|
||||
That means, by default, `RepresentationModelProcessor` instances are not invoked for empty collection models.
|
||||
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(…)`.
|
||||
See <<fundamentals.collection-model>> for details.
|
||||
|
||||
[[server.rel-provider]]
|
||||
== [[spis.rel-provider]] Using the `LinkRelationProvider` API
|
||||
|
||||
|
||||
@@ -20,10 +20,17 @@ import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.ResolvableTypeProvider;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
@@ -32,9 +39,12 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
* @author Oliver Gierke
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
public class CollectionModel<T> extends RepresentationModel<CollectionModel<T>> implements Iterable<T> {
|
||||
public class CollectionModel<T> extends RepresentationModel<CollectionModel<T>>
|
||||
implements Iterable<T>, ResolvableTypeProvider {
|
||||
|
||||
private final Collection<T> content;
|
||||
private final @Nullable ResolvableType fallbackType;
|
||||
private ResolvableType fullType;
|
||||
|
||||
/**
|
||||
* Creates an empty {@link CollectionModel} instance.
|
||||
@@ -64,6 +74,10 @@ public class CollectionModel<T> extends RepresentationModel<CollectionModel<T>>
|
||||
*/
|
||||
@Deprecated
|
||||
public CollectionModel(Iterable<T> content, Iterable<Link> links) {
|
||||
this(content, links, null);
|
||||
}
|
||||
|
||||
protected CollectionModel(Iterable<T> content, Iterable<Link> links, @Nullable ResolvableType fallbackType) {
|
||||
|
||||
Assert.notNull(content, "Content must not be null!");
|
||||
|
||||
@@ -74,6 +88,7 @@ public class CollectionModel<T> extends RepresentationModel<CollectionModel<T>>
|
||||
}
|
||||
|
||||
this.add(links);
|
||||
this.fallbackType = fallbackType;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,6 +102,42 @@ public class CollectionModel<T> extends RepresentationModel<CollectionModel<T>>
|
||||
return of(Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new empty collection model with the given type defined as fallback type.
|
||||
*
|
||||
* @param <T>
|
||||
* @return
|
||||
* @since 1.4
|
||||
* @see #withFallbackType(Class, Class...)
|
||||
*/
|
||||
public static <T> CollectionModel<T> empty(Class<T> elementType, Class<?>... generics) {
|
||||
return empty(ResolvableType.forClassWithGenerics(elementType, generics));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new empty collection model with the given type defined as fallback type.
|
||||
*
|
||||
* @param <T>
|
||||
* @return
|
||||
* @since 1.4
|
||||
* @see #withFallbackType(ParameterizedTypeReference)
|
||||
*/
|
||||
public static <T> CollectionModel<T> empty(ParameterizedTypeReference<T> type) {
|
||||
return empty(ResolvableType.forType(type.getType()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new empty collection model with the given type defined as fallback type.
|
||||
*
|
||||
* @param <T>
|
||||
* @return
|
||||
* @since 1.4
|
||||
* @see #withFallbackType(ResolvableType)
|
||||
*/
|
||||
public static <T> CollectionModel<T> empty(ResolvableType elementType) {
|
||||
return new CollectionModel<>(Collections.emptyList(), Collections.emptyList(), elementType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new empty collection model with the given links.
|
||||
*
|
||||
@@ -117,6 +168,8 @@ public class CollectionModel<T> extends RepresentationModel<CollectionModel<T>>
|
||||
* @param content must not be {@literal null}.
|
||||
* @return
|
||||
* @since 1.1
|
||||
* @see #withFallbackType(Class)
|
||||
* @see #withFallbackType(ResolvableType)
|
||||
*/
|
||||
public static <T> CollectionModel<T> of(Iterable<T> content) {
|
||||
return of(content, Collections.emptyList());
|
||||
@@ -129,6 +182,8 @@ public class CollectionModel<T> extends RepresentationModel<CollectionModel<T>>
|
||||
* @param links the links to be added to the {@link CollectionModel}.
|
||||
* @return
|
||||
* @since 1.1
|
||||
* @see #withFallbackType(Class)
|
||||
* @see #withFallbackType(ResolvableType)
|
||||
*/
|
||||
public static <T> CollectionModel<T> of(Iterable<T> content, Link... links) {
|
||||
return of(content, Arrays.asList(links));
|
||||
@@ -141,6 +196,8 @@ public class CollectionModel<T> extends RepresentationModel<CollectionModel<T>>
|
||||
* @param links the links to be added to the {@link CollectionModel}.
|
||||
* @return
|
||||
* @since 1.1
|
||||
* @see #withFallbackType(Class)
|
||||
* @see #withFallbackType(ResolvableType)
|
||||
*/
|
||||
public static <T> CollectionModel<T> of(Iterable<T> content, Iterable<Link> links) {
|
||||
return new CollectionModel<>(content, links);
|
||||
@@ -177,6 +234,74 @@ public class CollectionModel<T> extends RepresentationModel<CollectionModel<T>>
|
||||
return Collections.unmodifiableCollection(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Declares the given type as fallback element type in case the underlying collection is empty. This allows client
|
||||
* components to still apply type matches at runtime.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.4
|
||||
*/
|
||||
public CollectionModel<T> withFallbackType(Class<? super T> type, Class<?>... generics) {
|
||||
|
||||
Assert.notNull(type, "Fallback type must not be null!");
|
||||
Assert.notNull(generics, "Generics must not be null!");
|
||||
|
||||
return withFallbackType(ResolvableType.forClassWithGenerics(type, generics));
|
||||
}
|
||||
|
||||
/**
|
||||
* Declares the given type as fallback element type in case the underlying collection is empty. This allows client
|
||||
* components to still apply type matches at runtime.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.4
|
||||
*/
|
||||
public CollectionModel<T> withFallbackType(ParameterizedTypeReference<?> type) {
|
||||
|
||||
Assert.notNull(type, "Fallback type must not be null!");
|
||||
|
||||
return withFallbackType(ResolvableType.forType(type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Declares the given type as fallback element type in case the underlying collection is empty. This allows client
|
||||
* components to still apply type matches at runtime.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.4
|
||||
*/
|
||||
public CollectionModel<T> withFallbackType(ResolvableType type) {
|
||||
|
||||
Assert.notNull(type, "Fallback type must not be null!");
|
||||
|
||||
return new CollectionModel<>(content, getLinks(), type);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.core.ResolvableTypeProvider#getResolvableType()
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public ResolvableType getResolvableType() {
|
||||
|
||||
if (fullType == null) {
|
||||
|
||||
ResolvableType elementType = deriveElementType(this.content, fallbackType);
|
||||
Class<?> type = this.getClass();
|
||||
|
||||
this.fullType = elementType == null || type.getTypeParameters().length == 0 //
|
||||
? ResolvableType.forClass(type) //
|
||||
: ResolvableType.forClassWithGenerics(type, elementType);
|
||||
}
|
||||
|
||||
return fullType;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Iterable#iterator()
|
||||
@@ -192,7 +317,9 @@ public class CollectionModel<T> extends RepresentationModel<CollectionModel<T>>
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("CollectionModel { content: %s, %s }", getContent(), super.toString());
|
||||
|
||||
return String.format("CollectionModel { content: %s, fallbackType: %s, %s }", //
|
||||
getContent(), fallbackType, super.toString());
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -212,8 +339,9 @@ public class CollectionModel<T> extends RepresentationModel<CollectionModel<T>>
|
||||
|
||||
CollectionModel<?> that = (CollectionModel<?>) obj;
|
||||
|
||||
boolean contentEqual = this.content == null ? that.content == null : this.content.equals(that.content);
|
||||
return contentEqual && super.equals(obj);
|
||||
return Objects.equals(this.content, that.content)
|
||||
&& Objects.equals(this.fallbackType, that.fallbackType)
|
||||
&& super.equals(obj);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -222,10 +350,28 @@ public class CollectionModel<T> extends RepresentationModel<CollectionModel<T>>
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return super.hashCode() + Objects.hash(content, fallbackType);
|
||||
}
|
||||
|
||||
int result = super.hashCode();
|
||||
result += content == null ? 0 : 17 * content.hashCode();
|
||||
/**
|
||||
* Determines the most common element type from the given elements defaulting to the given fallback type.
|
||||
*
|
||||
* @param elements must not be {@literal null}.
|
||||
* @param fallbackType can be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
@Nullable
|
||||
private static ResolvableType deriveElementType(Collection<?> elements, @Nullable ResolvableType fallbackType) {
|
||||
|
||||
return result;
|
||||
if (elements.isEmpty()) {
|
||||
return fallbackType;
|
||||
}
|
||||
|
||||
return elements.stream()
|
||||
.filter(it -> it != null)
|
||||
.<Class<?>> map(Object::getClass)
|
||||
.reduce(ClassUtils::determineCommonAncestor)
|
||||
.map(ResolvableType::forClass)
|
||||
.orElse(fallbackType);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,11 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -37,7 +40,8 @@ public class PagedModel<T> extends CollectionModel<T> {
|
||||
|
||||
public static PagedModel<?> NO_PAGE = new PagedModel<>();
|
||||
|
||||
private PageMetadata metadata;
|
||||
private final PageMetadata metadata;
|
||||
private final @Nullable ResolvableType fallbackType;
|
||||
|
||||
/**
|
||||
* Default constructor to allow instantiation by reflection.
|
||||
@@ -69,23 +73,69 @@ public class PagedModel<T> extends CollectionModel<T> {
|
||||
*/
|
||||
@Deprecated
|
||||
public PagedModel(Collection<T> content, @Nullable PageMetadata metadata, Iterable<Link> links) {
|
||||
this(content, metadata, links, null);
|
||||
}
|
||||
|
||||
super(content, links);
|
||||
protected PagedModel(Collection<T> content, @Nullable PageMetadata metadata, Iterable<Link> links,
|
||||
@Nullable ResolvableType fallbackType) {
|
||||
|
||||
super(content, links, fallbackType);
|
||||
|
||||
this.metadata = metadata;
|
||||
this.fallbackType = fallbackType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an empty {@link PagedModel}.
|
||||
*
|
||||
* @param <T>
|
||||
* @return
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.1
|
||||
*/
|
||||
public static <T> PagedModel<T> empty() {
|
||||
return empty(Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an empty {@link PagedModel} with the given fallback type.
|
||||
*
|
||||
* @param <T>
|
||||
* @param fallbackElementType must not be {@literal null}.
|
||||
* @param generics must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.4
|
||||
* @see #withFallbackType(Class, Class...)
|
||||
*/
|
||||
public static <T> PagedModel<T> empty(Class<T> fallbackElementType, Class<?> generics) {
|
||||
return empty(ResolvableType.forClassWithGenerics(fallbackElementType, generics));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an empty {@link PagedModel} with the given fallback type.
|
||||
*
|
||||
* @param <T>
|
||||
* @param fallbackElementType must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.4
|
||||
* @see #withFallbackType(ParameterizedTypeReference)
|
||||
*/
|
||||
public static <T> PagedModel<T> empty(ParameterizedTypeReference<T> fallbackElementType) {
|
||||
return empty(ResolvableType.forType(fallbackElementType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an empty {@link PagedModel} with the given fallback type.
|
||||
*
|
||||
* @param <T>
|
||||
* @param fallbackElementType must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.4
|
||||
* @see #withFallbackType(ResolvableType)
|
||||
*/
|
||||
public static <T> PagedModel<T> empty(ResolvableType fallbackElementType) {
|
||||
return new PagedModel<>(Collections.emptyList(), null, Collections.emptyList(), fallbackElementType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an empty {@link PagedModel} with the given links.
|
||||
*
|
||||
@@ -122,6 +172,58 @@ public class PagedModel<T> extends CollectionModel<T> {
|
||||
return empty(metadata, Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an empty {@link PagedModel} with the given {@link PageMetadata} and fallback type.
|
||||
*
|
||||
* @param <T>
|
||||
* @param metadata can be {@literal null}.
|
||||
* @param fallbackType must not be {@literal null}.
|
||||
* @param generics must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.4
|
||||
* @see #withFallbackType(Class, Class...)
|
||||
*/
|
||||
public static <T> PagedModel<T> empty(@Nullable PageMetadata metadata, Class<?> fallbackType, Class<?>... generics) {
|
||||
|
||||
Assert.notNull(fallbackType, "Fallback type must not be null!");
|
||||
Assert.notNull(generics, "Generics must not be null!");
|
||||
|
||||
return empty(metadata, ResolvableType.forClassWithGenerics(fallbackType, generics));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an empty {@link PagedModel} with the given {@link PageMetadata} and fallback type.
|
||||
*
|
||||
* @param <T>
|
||||
* @param metadata can be {@literal null}.
|
||||
* @return
|
||||
* @since 1.4
|
||||
* @see #withFallbackType(ParameterizedTypeReference)
|
||||
*/
|
||||
public static <T> PagedModel<T> empty(@Nullable PageMetadata metadata, ParameterizedTypeReference<T> fallbackType) {
|
||||
|
||||
Assert.notNull(fallbackType, "Fallback type must not be null!");
|
||||
|
||||
return empty(metadata, ResolvableType.forType(fallbackType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an empty {@link PagedModel} with the given {@link PageMetadata} and fallback type.
|
||||
*
|
||||
* @param <T>
|
||||
* @param metadata can be {@literal null}.
|
||||
* @param fallbackType must not be {@literal null}.
|
||||
* @return
|
||||
* @since 1.4
|
||||
* @see #withFallbackType(ResolvableType)
|
||||
*/
|
||||
public static <T> PagedModel<T> empty(@Nullable PageMetadata metadata, ResolvableType fallbackType) {
|
||||
|
||||
Assert.notNull(fallbackType, "Fallback type must not be null!");
|
||||
|
||||
return new PagedModel<>(Collections.emptyList(), metadata, Collections.emptyList(), fallbackType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an empty {@link PagedModel} with the given {@link PageMetadata} and links.
|
||||
*
|
||||
@@ -232,13 +334,41 @@ public class PagedModel<T> extends CollectionModel<T> {
|
||||
return getLink(IanaLinkRelations.PREV);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.hateoas.CollectionModel#withFallbackType(java.lang.Class, java.lang.Class[])
|
||||
*/
|
||||
@Override
|
||||
public PagedModel<T> withFallbackType(Class<? super T> type, Class<?>... generics) {
|
||||
return withFallbackType(ResolvableType.forClassWithGenerics(type, generics));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.hateoas.CollectionModel#withFallbackType(org.springframework.core.ParameterizedTypeReference)
|
||||
*/
|
||||
@Override
|
||||
public PagedModel<T> withFallbackType(ParameterizedTypeReference<?> type) {
|
||||
return withFallbackType(ResolvableType.forType(type));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.hateoas.CollectionModel#withFallbackType(org.springframework.core.ResolvableType)
|
||||
*/
|
||||
@Override
|
||||
public PagedModel<T> withFallbackType(ResolvableType type) {
|
||||
return new PagedModel<>(getContent(), metadata, getLinks(), type);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.hateoas.RepresentationModel#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("PagedModel { content: %s, metadata: %s, links: %s }", getContent(), metadata, getLinks());
|
||||
return String.format("PagedModel { content: %s, fallbackType: %s, metadata: %s, links: %s }", //
|
||||
getContent(), fallbackType, metadata, getLinks());
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -257,9 +387,9 @@ public class PagedModel<T> extends CollectionModel<T> {
|
||||
}
|
||||
|
||||
PagedModel<?> that = (PagedModel<?>) obj;
|
||||
boolean metadataEquals = this.metadata == null ? that.metadata == null : this.metadata.equals(that.metadata);
|
||||
|
||||
return metadataEquals && super.equals(obj);
|
||||
return Objects.equals(this.metadata, that.metadata) //
|
||||
&& super.equals(obj);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -268,10 +398,7 @@ public class PagedModel<T> extends CollectionModel<T> {
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
|
||||
int result = super.hashCode();
|
||||
result += this.metadata == null ? 0 : 31 * this.metadata.hashCode();
|
||||
return result;
|
||||
return super.hashCode() + Objects.hash(metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -370,9 +370,8 @@ public class RepresentationModelProcessorInvoker {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean defaultSupports = super.supports(type, value);
|
||||
boolean valueTypeMatch = isValueTypeMatch((CollectionModel<?>) value, getTargetType());
|
||||
return defaultSupports && valueTypeMatch;
|
||||
return super.supports(type, value) //
|
||||
&& isValueTypeMatch((CollectionModel<?>) value, getTargetType());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -392,7 +391,7 @@ public class RepresentationModelProcessorInvoker {
|
||||
Collection<?> content = collectionModel.getContent();
|
||||
|
||||
if (content.isEmpty()) {
|
||||
return false;
|
||||
return collectionModel.getResolvableType().isAssignableFrom(target);
|
||||
}
|
||||
|
||||
ResolvableType superType = null;
|
||||
|
||||
@@ -17,14 +17,21 @@ package org.springframework.hateoas;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.DynamicTest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestFactory;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link CollectionModel}.
|
||||
*
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
class CollectionModelUnitTest {
|
||||
@@ -69,4 +76,42 @@ class CollectionModelUnitTest {
|
||||
assertThat(left).isNotEqualTo(right);
|
||||
assertThat(right).isNotEqualTo(left);
|
||||
}
|
||||
|
||||
@TestFactory // #1590
|
||||
Stream<DynamicTest> exposesElementTypeForCollection() {
|
||||
return DynamicTest.stream(Fixture.probes(), Fixture::toString, Fixture::verify);
|
||||
}
|
||||
|
||||
@Value(staticConstructor = "$")
|
||||
static class Fixture {
|
||||
|
||||
CollectionModel<?> model;
|
||||
@Nullable Class<?> expectedElementType;
|
||||
|
||||
static Stream<Fixture> probes() {
|
||||
|
||||
return Stream.of(
|
||||
$(CollectionModel.empty(), null),
|
||||
$(CollectionModel.empty(String.class), String.class),
|
||||
$(CollectionModel.of(Arrays.asList(new Person())).withFallbackType(Contact.class), Person.class),
|
||||
$(CollectionModel.of(Arrays.asList(new Person(), new Company())).withFallbackType(Object.class),
|
||||
Contact.class));
|
||||
}
|
||||
|
||||
void verify() {
|
||||
assertThat(model.getResolvableType().getGeneric(0).resolve()).isEqualTo(expectedElementType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("Expect element type %s for collection model %s.", expectedElementType, model);
|
||||
}
|
||||
}
|
||||
|
||||
static class Contact {}
|
||||
|
||||
static class Person extends Contact {}
|
||||
|
||||
static class Company extends Contact {}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,11 +21,12 @@ import java.util.Collections;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.hateoas.PagedModel.PageMetadata;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link PagedModel}.
|
||||
*
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
class PagedModelUnitTest {
|
||||
@@ -114,4 +115,13 @@ class PagedModelUnitTest {
|
||||
void calculatesTotalPagesCorrectly() {
|
||||
assertThat(new PageMetadata(5, 0, 16).getTotalPages()).isEqualTo(4L);
|
||||
}
|
||||
|
||||
@Test // #1590
|
||||
void exposesElementTypeForEmpty() {
|
||||
|
||||
ResolvableType fallbackType = ResolvableType.forClassWithGenerics(EntityModel.class, String.class);
|
||||
PagedModel<String> model = PagedModel.empty(fallbackType);
|
||||
|
||||
assertThat(model.getResolvableType().getGeneric(0).resolve()).isEqualTo(EntityModel.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,18 @@ public class RepresentationModelProcessorInvokerUnitTests {
|
||||
assertThat(processor.invoked).isTrue();
|
||||
}
|
||||
|
||||
@Test // #1590
|
||||
void invokesProcessorForEmptyCollectionModelIfFallbackTypeDefined() {
|
||||
|
||||
FirstEntityProcessor processor = new FirstEntityProcessor();
|
||||
RepresentationModelProcessorInvoker invoker = new RepresentationModelProcessorInvoker(singletonList(processor));
|
||||
CollectionModel<?> model = CollectionModel.empty(EntityModel.class, FirstEntity.class);
|
||||
|
||||
invoker.invokeProcessorsFor(model);
|
||||
|
||||
assertThat(processor.invoked).isTrue();
|
||||
}
|
||||
|
||||
// #1280
|
||||
|
||||
static class GenericPostProcessor<T extends GenericModel<T>> implements RepresentationModelProcessor<T> {
|
||||
|
||||
Reference in New Issue
Block a user