diff --git a/src/main/java/org/springframework/hateoas/Link.java b/src/main/java/org/springframework/hateoas/Link.java index 6cd925ec..2f99b454 100755 --- a/src/main/java/org/springframework/hateoas/Link.java +++ b/src/main/java/org/springframework/hateoas/Link.java @@ -42,7 +42,7 @@ public class Link implements Serializable { public static final String REL_SELF = "self"; public static final String REL_FIRST = "first"; - public static final String REL_PREVIOUS = "previous"; + public static final String REL_PREVIOUS = "prev"; public static final String REL_NEXT = "next"; public static final String REL_LAST = "last"; @@ -101,6 +101,25 @@ public class Link implements Serializable { return rel; } + /** + * Returns a {@link Link} pointing to the same URI but with the given relation. + * + * @param rel must not be {@literal null} or empty. + * @return + */ + public Link withRel(String rel) { + return new Link(href, rel); + } + + /** + * Returns a {@link Link} pointing to the same URI but with the {@code self} relation. + * + * @return + */ + public Link withSelfRel() { + return withRel(Link.REL_SELF); + } + /* * (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) diff --git a/src/main/java/org/springframework/hateoas/Links.java b/src/main/java/org/springframework/hateoas/Links.java index 51a27c07..178a90cd 100644 --- a/src/main/java/org/springframework/hateoas/Links.java +++ b/src/main/java/org/springframework/hateoas/Links.java @@ -87,6 +87,16 @@ public class Links implements Iterable { return result; } + /** + * Returns whether the {@link Links} container contains a {@link Link} with the given rel. + * + * @param rel + * @return + */ + public boolean hasLink(String rel) { + return getLink(rel) != null; + } + /** * Creates a {@link Links} instance from the given RFC5988-compatible link format. * @@ -114,6 +124,15 @@ public class Links implements Iterable { return new Links(links); } + /** + * Returns whether the {@link Links} containter is empty. + * + * @return + */ + public boolean isEmpty() { + return links.isEmpty(); + } + /* * (non-Javadoc) * @see java.lang.Object#toString() diff --git a/src/main/java/org/springframework/hateoas/PagedResources.java b/src/main/java/org/springframework/hateoas/PagedResources.java index 2aa03649..c027237f 100644 --- a/src/main/java/org/springframework/hateoas/PagedResources.java +++ b/src/main/java/org/springframework/hateoas/PagedResources.java @@ -34,6 +34,8 @@ import org.springframework.util.Assert; @com.fasterxml.jackson.annotation.JsonAutoDetect(fieldVisibility = com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY) public class PagedResources extends Resources { + public static PagedResources NO_PAGE = new PagedResources(); + @org.codehaus.jackson.annotate.JsonProperty("page") @com.fasterxml.jackson.annotation.JsonProperty("page") private PageMetadata metadata; @@ -97,13 +99,37 @@ public class PagedResources extends Resources { return new PagedResources(resources, metadata); } + /** + * Returns the Link pointing to the next page (if set). + * + * @see #addPaginationLinks(Link) + * @return + */ + @com.fasterxml.jackson.annotation.JsonIgnore + @org.codehaus.jackson.annotate.JsonIgnore + public Link getNextLink() { + return getLink(Link.REL_NEXT); + } + + /** + * Returns the Link pointing to the previous page (if set). + * + * @see #addPaginationLinks(Link) + * @return + */ + @com.fasterxml.jackson.annotation.JsonIgnore + @org.codehaus.jackson.annotate.JsonIgnore + public Link getPreviousLink() { + return getLink(Link.REL_PREVIOUS); + } + /* * (non-Javadoc) * @see org.springframework.hateoas.ResourceSupport#toString() */ @Override public String toString() { - return String.format("PagedResource { content: %s, metadata: %s }", getContent(), metadata); + return String.format("PagedResource { content: %s, metadata: %s, links: %s }", getContent(), metadata, getLinks()); } /* @@ -173,19 +199,35 @@ public class PagedResources extends Resources { } /** + * Creates a new {@link PageMetadata} from the given size, number, total elements and total pages. + * * @param size - * @param number + * @param number zero-indexed, must be less than totalPages * @param totalElements * @param totalPages */ public PageMetadata(long size, long number, long totalElements, long totalPages) { + Assert.isTrue(number < totalPages, + "The current page number cannot be greater or equal to the total number of pages!"); + this.size = size; this.number = number; this.totalElements = totalElements; this.totalPages = totalPages; } + /** + * Creates a new {@link PageMetadata} from the given size, numer and total elements. + * + * @param size the size of the page + * @param number the number of the page + * @param totalElements the total number of elements available + */ + public PageMetadata(long size, long number, long totalElements) { + this(size, number, totalElements, totalElements / size); + } + /** * Returns the requested size of the page. * diff --git a/src/test/java/org/springframework/hateoas/PagedResourcesUnitTest.java b/src/test/java/org/springframework/hateoas/PagedResourcesUnitTest.java new file mode 100644 index 00000000..512b806f --- /dev/null +++ b/src/test/java/org/springframework/hateoas/PagedResourcesUnitTest.java @@ -0,0 +1,58 @@ +/* + * Copyright 2013 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 + * + * http://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.hateoas; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import java.util.Collections; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.hateoas.PagedResources.PageMetadata; + +/** + * Unit tests for {@link PagedResources}. + * + * @author Oliver Gierke + */ +public class PagedResourcesUnitTest { + + static final PageMetadata metadata = new PagedResources.PageMetadata(10, 1, 200); + + PagedResources resources; + + @Before + public void setUp() { + resources = new PagedResources(Collections.emptyList(), metadata); + } + + @Test + public void discoversNextLink() { + + resources.add(new Link("foo", Link.REL_NEXT)); + + assertThat(resources.getNextLink(), is(notNullValue())); + } + + @Test + public void discoversPreviousLink() { + + resources.add(new Link("custom", Link.REL_PREVIOUS)); + + assertThat(resources.getPreviousLink(), is(notNullValue())); + } +} diff --git a/src/test/java/org/springframework/hateoas/hal/Jackson1HalIntegrationTest.java b/src/test/java/org/springframework/hateoas/hal/Jackson1HalIntegrationTest.java index 441b72ca..6bd6edf7 100644 --- a/src/test/java/org/springframework/hateoas/hal/Jackson1HalIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/hal/Jackson1HalIntegrationTest.java @@ -25,6 +25,9 @@ import org.junit.Before; import org.junit.Test; import org.springframework.hateoas.AbstractMarshallingIntegrationTests; import org.springframework.hateoas.Link; +import org.springframework.hateoas.Links; +import org.springframework.hateoas.PagedResources; +import org.springframework.hateoas.PagedResources.PageMetadata; import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.Resources; @@ -49,6 +52,10 @@ public class Jackson1HalIntegrationTest extends AbstractMarshallingIntegrationTe static final String ANNOTATED_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"pojo\":{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}}}}"; static final String ANNOTATED_EMBEDDED_RESOURCES_REFERENCE = "{\"_embedded\":{\"pojos\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}},{\"text\":\"test2\",\"number\":2,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]}}"; + static final String ANNOTATED_PAGED_RESOURCES = "{\"_links\":{\"next\":{\"href\":\"foo\"},\"prev\":{\"href\":\"bar\"}},\"_embedded\":{\"pojos\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}},{\"text\":\"test2\",\"number\":2,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]},\"page\":{\"size\":2,\"totalElements\":4,\"totalPages\":2,\"number\":0}}"; + + static final Links PAGINATION_LINKS = new Links(new Link("foo", Link.REL_NEXT), new Link("bar", Link.REL_PREVIOUS)); + @Before public void setUpModule() { @@ -232,6 +239,30 @@ public class Jackson1HalIntegrationTest extends AbstractMarshallingIntegrationTe assertThat(result, is(setupAnnotatedResources())); } + @Test + public void serializesPagedResource() throws Exception { + assertThat(write(setupAnnotatedPagedResources()), is(ANNOTATED_PAGED_RESOURCES)); + } + + @Test + public void deserializesPagedResource() throws Exception { + PagedResources> result = mapper.readValue( + ANNOTATED_PAGED_RESOURCES, + mapper.getTypeFactory().constructParametricType(PagedResources.class, + mapper.getTypeFactory().constructParametricType(Resource.class, SimpleAnnotatedPojo.class))); + + assertThat(result, is(setupAnnotatedPagedResources())); + } + + private static Resources> setupAnnotatedPagedResources() { + + List> content = new ArrayList>(); + content.add(new Resource(new SimpleAnnotatedPojo("test1", 1), new Link("localhost"))); + content.add(new Resource(new SimpleAnnotatedPojo("test2", 2), new Link("localhost"))); + + return new PagedResources>(content, new PageMetadata(2, 0, 4), PAGINATION_LINKS); + } + private static Resources> setupAnnotatedResources() { List> content = new ArrayList>(); diff --git a/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java b/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java index 6fe7497b..b9213cef 100644 --- a/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java @@ -25,6 +25,9 @@ import org.junit.Before; import org.junit.Test; import org.springframework.hateoas.AbstractJackson2MarshallingIntegrationTests; import org.springframework.hateoas.Link; +import org.springframework.hateoas.Links; +import org.springframework.hateoas.PagedResources; +import org.springframework.hateoas.PagedResources.PageMetadata; import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.Resources; @@ -49,6 +52,10 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg static final String ANNOTATED_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"pojo\":{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}}}}"; static final String ANNOTATED_EMBEDDED_RESOURCES_REFERENCE = "{\"_links\":{},\"_embedded\":{\"pojos\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}},{\"text\":\"test2\",\"number\":2,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]}}"; + static final String ANNOTATED_PAGED_RESOURCES = "{\"_links\":{\"next\":{\"href\":\"foo\"},\"prev\":{\"href\":\"bar\"}},\"_embedded\":{\"pojos\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}},{\"text\":\"test2\",\"number\":2,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]},\"page\":{\"size\":2,\"totalElements\":4,\"totalPages\":2,\"number\":0}}"; + + static final Links PAGINATION_LINKS = new Links(new Link("foo", Link.REL_NEXT), new Link("bar", Link.REL_PREVIOUS)); + @Before public void setUpModule() { @@ -238,6 +245,30 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg assertThat(result, is(setupAnnotatedResources())); } + @Test + public void serializesPagedResource() throws Exception { + assertThat(write(setupAnnotatedPagedResources()), is(ANNOTATED_PAGED_RESOURCES)); + } + + @Test + public void deserializesPagedResource() throws Exception { + PagedResources> result = mapper.readValue( + ANNOTATED_PAGED_RESOURCES, + mapper.getTypeFactory().constructParametricType(PagedResources.class, + mapper.getTypeFactory().constructParametricType(Resource.class, SimpleAnnotatedPojo.class))); + + assertThat(result, is(setupAnnotatedPagedResources())); + } + + private static Resources> setupAnnotatedPagedResources() { + + List> content = new ArrayList>(); + content.add(new Resource(new SimpleAnnotatedPojo("test1", 1), new Link("localhost"))); + content.add(new Resource(new SimpleAnnotatedPojo("test2", 2), new Link("localhost"))); + + return new PagedResources>(content, new PageMetadata(2, 0, 4), PAGINATION_LINKS); + } + private static Resources> setupAnnotatedResources() { List> content = new ArrayList>();