diff --git a/src/main/java/org/springframework/hateoas/SlicedModel.java b/src/main/java/org/springframework/hateoas/SlicedModel.java index 875f698b..f3f4ff4e 100644 --- a/src/main/java/org/springframework/hateoas/SlicedModel.java +++ b/src/main/java/org/springframework/hateoas/SlicedModel.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2023 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. @@ -15,7 +15,13 @@ */ package org.springframework.hateoas; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; @@ -29,8 +35,11 @@ import com.fasterxml.jackson.annotation.JsonProperty; * DTO to implement binding response representations of Slice collections. * * @author Michael Schout + * @author Oliver Drotbohm + * @since 2.1 */ public class SlicedModel extends CollectionModel { + public static SlicedModel NO_SLICE = new SlicedModel<>(); private final SliceMetadata metadata; @@ -53,6 +62,7 @@ public class SlicedModel extends CollectionModel { protected SlicedModel(Collection content, @Nullable SliceMetadata metadata, Iterable links, @Nullable ResolvableType fallbackType) { + super(content, links, fallbackType); this.metadata = metadata; @@ -62,7 +72,7 @@ public class SlicedModel extends CollectionModel { /** * Creates an empty {@link SlicedModel}. * - * @param + * @param the payload type. * @return will never be {@literal null}. */ public static SlicedModel empty() { @@ -72,7 +82,7 @@ public class SlicedModel extends CollectionModel { /** * Creates an empty {@link SlicedModel} with the given fallback type. * - * @param + * @param the payload type. * @param fallbackElementType must not be {@literal null}. * @param generics must not be {@literal null}. * @return will never be {@literal null}. @@ -85,7 +95,7 @@ public class SlicedModel extends CollectionModel { /** * Creates an empty {@link SlicedModel} with the given fallback type. * - * @param + * @param the payload type. * @param fallbackElementType must not be {@literal null}. * @return will never be {@literal null}. * @see #withFallbackType(ParameterizedTypeReference) @@ -97,7 +107,7 @@ public class SlicedModel extends CollectionModel { /** * Creates an empty {@link SlicedModel} with the given fallback type. * - * @param + * @param the payload type. * @param fallbackElementType must not be {@literal null}. * @return will never be {@literal null}. * @see #withFallbackType(ResolvableType) @@ -109,9 +119,9 @@ public class SlicedModel extends CollectionModel { /** * Creates an empty {@link SlicedModel} with the given links. * - * @param + * @param the payload type. * @param links must not be {@literal null}. - * @return + * @return will never be {@literal null}. */ public static SlicedModel empty(Link... links) { return empty(null, links); @@ -120,9 +130,9 @@ public class SlicedModel extends CollectionModel { /** * Creates an empty {@link SlicedModel} with the given links. * - * @param + * @param the payload type. * @param links must not be {@literal null}. - * @return + * @return will never be {@literal null}. */ public static SlicedModel empty(Iterable links) { return empty(null, links); @@ -131,19 +141,18 @@ public class SlicedModel extends CollectionModel { /** * Creates an empty {@link SlicedModel} with the given {@link SliceMetadata}. * - * @param + * @param the payload type. * @param metadata can be {@literal null}. - * @return + * @return will never be {@literal null}. */ public static SlicedModel empty(@Nullable SliceMetadata metadata) { return empty(metadata, Collections.emptyList()); } /** - * Creates an empty {@link SlicedModel} with the given {@link SliceMetadata} and fallback - * type. + * Creates an empty {@link SlicedModel} with the given {@link SliceMetadata} and fallback type. * - * @param + * @param the payload type. * @param metadata can be {@literal null}. * @param fallbackType must not be {@literal null}. * @param generics must not be {@literal null}. @@ -160,12 +169,11 @@ public class SlicedModel extends CollectionModel { } /** - * Creates an empty {@link SlicedModel} with the given {@link SliceMetadata} and fallback - * type. + * Creates an empty {@link SlicedModel} with the given {@link SliceMetadata} and fallback type. * - * @param + * @param the payload type. * @param metadata can be {@literal null}. - * @return + * @return will never be {@literal null}. * @see #withFallbackType(ParameterizedTypeReference) */ public static SlicedModel empty(@Nullable SliceMetadata metadata, @@ -177,8 +185,7 @@ public class SlicedModel extends CollectionModel { } /** - * Creates an empty {@link SlicedModel} with the given {@link SliceMetadata} and fallback - * type. + * Creates an empty {@link SlicedModel} with the given {@link SliceMetadata} and fallback type. * * @param * @param metadata can be {@literal null}. @@ -208,62 +215,68 @@ public class SlicedModel extends CollectionModel { /** * Creates an empty {@link SlicedModel} with the given {@link SliceMetadata} and links. * - * @param + * @param the payload type. * @param metadata can be {@literal null}. * @param links must not be {@literal null}. - * @return + * @return will never be {@literal null}. */ public static SlicedModel empty(@Nullable SliceMetadata metadata, Iterable links) { return of(Collections.emptyList(), metadata, links); } /** - * Creates a new {@link SlicedModel} from the given content, {@link SliceMetadata} and - * {@link Link}s (optional). + * Creates a new {@link SlicedModel} from the given content, {@link SliceMetadata} and {@link Link}s (optional). * + * @param the payload type. * @param content must not be {@literal null}. * @param metadata can be {@literal null}. + * @return will never be {@literal null}. */ public static SlicedModel of(Collection content, @Nullable SliceMetadata metadata) { return new SlicedModel<>(content, metadata); } /** - * Creates a new {@link SlicedModel} from the given content, {@link SliceMetadata} and - * {@link Link}s (optional). + * Creates a new {@link SlicedModel} from the given content, {@link SliceMetadata} and {@link Link}s (optional). * + * @param the payload type. * @param content must not be {@literal null}. * @param metadata can be {@literal null}. - * @param links + * @param links must not be {@literal null}. + * @return will never be {@literal null}. */ public static SlicedModel of(Collection content, @Nullable SliceMetadata metadata, Link... links) { return new SlicedModel<>(content, metadata, Arrays.asList(links)); } /** - * Creates a new {@link SlicedModel} from the given content {@link SliceMetadata} and - * {@link Link}s. + * Creates a new {@link SlicedModel} from the given content {@link SliceMetadata} and {@link Link}s. * + * @param the payload type. * @param content must not be {@literal null}. * @param metadata can be {@literal null}. - * @param links + * @param links must not be {@literal null}. + * @return will never be {@literal null}. */ public static SlicedModel of(Collection content, @Nullable SliceMetadata metadata, Iterable links) { return new SlicedModel<>(content, metadata, links); } /** - * Factory method to easily create a {@link SlicedModel} instance from a set of entities - * and pagination metadata. + * Factory method to easily create a {@link SlicedModel} instance from a set of entities and pagination metadata. * + * @param the nested {@link EntityModel} type. + * @param the actual payload type. * @param content must not be {@literal null}. * @param metadata - * @return + * @return will never be {@literal null}. */ @SuppressWarnings("unchecked") public static , S> SlicedModel wrap(Iterable content, SliceMetadata metadata) { + Assert.notNull(content, "Content must not be null!"); - ArrayList resources = new ArrayList<>(); + + List resources = new ArrayList<>(); for (S element : content) { resources.add((T) EntityModel.of(element)); @@ -275,18 +288,18 @@ public class SlicedModel extends CollectionModel { /** * Returns the pagination metadata. * - * @return the metadata + * @return the metadata can be {@literal null}. */ - @JsonProperty("page") @Nullable + @JsonProperty("page") public SliceMetadata getMetadata() { return metadata; } /** - * Returns the Link pointing to the next slice (if set). + * Returns the {@link Link} pointing to the next slice (if set). * - * @return + * @return will never be {@literal null}. */ @JsonIgnore public Optional getNextLink() { @@ -294,9 +307,9 @@ public class SlicedModel extends CollectionModel { } /** - * Returns the Link pointing to the previous slice (if set). + * Returns the {@link Link} pointing to the previous slice (if set). * - * @return + * @return will never be {@literal null}. */ @JsonIgnore public Optional getPreviousLink() { @@ -336,6 +349,10 @@ public class SlicedModel extends CollectionModel { return new SlicedModel<>(getContent(), metadata, getLinks(), type); } + /* + * (non-Javadoc) + * @see org.springframework.hateoas.CollectionModel#toString() + */ @Override public String toString() { return String.format("SlicedModel { content: %s, fallbackType: %s, metadata: %s, links: %s }", // @@ -344,7 +361,6 @@ public class SlicedModel extends CollectionModel { /* * (non-Javadoc) - * * @see org.springframework.hateoas.CollectionModel#equals(java.lang.Object) */ @Override @@ -380,22 +396,23 @@ public class SlicedModel extends CollectionModel { * @author Michael Schout */ public static class SliceMetadata { - @JsonProperty + + @JsonProperty // private long size; - @JsonProperty + @JsonProperty // private long number; - protected SliceMetadata() { - } + protected SliceMetadata() {} /** * Creates a new {@link SliceMetadata} from the given size, and slice number. * - * @param size - * @param number zero-indexed slice number + * @param size must be greater or equal to zero. + * @param number zero-indexed slice number, greater or equal to zero. */ public SliceMetadata(long size, long number) { + Assert.isTrue(size > -1, "Size must not be negative!"); Assert.isTrue(number > -1, "Number must not be negative!"); @@ -421,6 +438,10 @@ public class SlicedModel extends CollectionModel { return number; } + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ @Override public String toString() { return String.format("Metadata: { number: %d, size %d )", number, size); @@ -428,11 +449,11 @@ public class SlicedModel extends CollectionModel { /* * (non-Javadoc) - * * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(@Nullable Object obj) { + if (this == obj) { return true; } @@ -443,21 +464,18 @@ public class SlicedModel extends CollectionModel { SliceMetadata that = (SliceMetadata) obj; - return this.number == that.number && this.size == that.size; + return super.equals(that) // + && Objects.equals(this.number, that.number) // + && Objects.equals(this.size, that.size); } /* * (non-Javadoc) - * * @see java.lang.Object#hashCode() */ @Override public int hashCode() { - int result = 17; - result += 31 * (int) (this.number ^ this.number >>> 32); - result += 31 * (int) (this.size ^ this.size >>> 32); - - return result; + return Objects.hash(super.hashCode(), number, size); } } } diff --git a/src/test/java/org/springframework/hateoas/SlicedModelUnitTest.java b/src/test/java/org/springframework/hateoas/SlicedModelUnitTest.java index 8f312073..29168473 100644 --- a/src/test/java/org/springframework/hateoas/SlicedModelUnitTest.java +++ b/src/test/java/org/springframework/hateoas/SlicedModelUnitTest.java @@ -15,8 +15,7 @@ */ package org.springframework.hateoas; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.*; import java.util.Collections; @@ -29,8 +28,11 @@ import org.springframework.hateoas.SlicedModel.SliceMetadata; * Unit tests for SlicedModel * * @author Michael Schout + * @author Oliver Drotbohm + * @since 2.1 */ class SlicedModelUnitTest { + static final SliceMetadata metadata = new SliceMetadata(10, 1); SlicedModel resources; @@ -40,38 +42,41 @@ class SlicedModelUnitTest { resources = SlicedModel.of(Collections.emptyList(), metadata); } - @Test + @Test // #1856 void discoversNextLink() { + resources.add(Link.of("foo", IanaLinkRelations.NEXT.value())); assertThat(resources.getNextLink()).isNotNull(); } - @Test + @Test // #1856 void discoversPreviousLink() { + resources.add(Link.of("custom", IanaLinkRelations.PREV.value())); assertThat(resources.getPreviousLink()).isNotNull(); } - @Test + @Test // #1856 void preventsNegativeSliceSize() { - assertThatIllegalArgumentException().isThrownBy(() -> { - new SliceMetadata(-1, 0); - }); + + assertThatIllegalArgumentException() + .isThrownBy(() -> new SliceMetadata(-1, 0)); } - @Test + @Test // #1856 void preventsNegativeSliceNumber() { - assertThatIllegalArgumentException().isThrownBy(() -> { - new SliceMetadata(0, -1); - }); + + assertThatIllegalArgumentException() + .isThrownBy(() -> new SliceMetadata(0, -1)); } - @Test + @Test // #1856 void exposesElementTypeForEmpty() { - ResolvableType fallbackType = ResolvableType.forClassWithGenerics(EntityModel.class, String.class); - SlicedModel model = SlicedModel.empty(fallbackType); + + var fallbackType = ResolvableType.forClassWithGenerics(EntityModel.class, String.class); + var model = SlicedModel.empty(fallbackType); assertThat(model.getResolvableType().getGeneric(0).resolve()).isEqualTo(EntityModel.class); }