CHE-243: Add paging support

6.19.x
Yevhenii Voevodin 2016-04-15 18:09:29 +03:00
parent a201683578
commit fcbf25fd74
8 changed files with 901 additions and 3 deletions

View File

@ -42,6 +42,10 @@
<groupId>com.google.inject.extensions</groupId>
<artifactId>guice-multibindings</artifactId>
</dependency>
<dependency>
<groupId>com.jayway.restassured</groupId>
<artifactId>rest-assured</artifactId>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>

View File

@ -0,0 +1,312 @@
/*******************************************************************************
* Copyright (c) 2012-2016 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.api.core;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
/**
* Defines paged result of data selection, it is rather dynamic data window than
* regular page, as it refers to the specified selection region based on the items before
* and the page size, but not on the page number and page size.
*
* <p>Examples:
*
* <p>Regular page.<br>
* For page input arguments {@code skipItems = 3}, {@code pageSize = 3} and {@code totalCount = 10}
* the page is normalized and will refer to the second page which is(item4, item5, item6):
* <pre>
* item1. <- previous page start <- first page start
* item2.
* item3. <- previous page end <- first page end
* item4. <- page start
* item5.
* item6. <- page end
* item7. <- next page start
* item8.
* item9. <- next page end
* item.10 <- last page start/end
* </pre>
*
* <ul>Result page:
* <li>Non-empty</li>
* <li>Contains 3 items(item4, item5, item6)</li>
* <li>Has the previous page(item1, item2, item3)
* <li>Has the next page(item7, item8, item9)</li>
* </ul>
*
*
* <p>Data window.<br>
* For page input arguments {@code skipItems = 2}, {@code pageSize = 3} and {@code totalCount = 7}
* the page will refer to the following data window:
* <pre>
* item1. <- first page start
* item2.
* item3. <- page start <- first page end
* item4.
* item5. <- page end
* item6.
* item7. <- last page start/end
* </pre>
*
* <ul>Result page:
* <li>Non-empty</li>
* <li>Contains 3 items(item3, item4, item5)</li>
* <li>Doesn't have the previous page(because page refers to the elements which
* are partially present in the first page(item3) and the second page(item4, item5)</li>
* <li>Doesn't have the next page(the reason is the same to previous statement)</li>
* </ul>
*
* <p>Note that all {@code Page} instances perform reference
* calculations based on the given {@code itemsBefore} and {@code pageSize}
* values which means that implementor is responsible for providing correct bounds
* and data management.
*
* <p>The instances of this class are <b>NOT thread safe</b>.
*
* @param <ITEM_T>
* the type of the page items
* @author Yevhenii Voevodin
*/
public class Page<ITEM_T> {
private final int pageSize;
private final long itemsBefore;
private final long totalCount;
private final List<ITEM_T> items;
/**
* Creates a new page.
*
* @param items
* page items
* @param itemsBefore
* items count before this page
* @param pageSize
* page size
* @param totalCount
* count of all the items
* @throws NullPointerException
* when {@code items} collection is null
* @throws IllegalArgumentException
* when {@code itemsBefore} is negative
* @throws IllegalArgumentException
* when {@code pageSize} is non-positive
* @throws IllegalArgumentException
* when {@code totalCount} is negative
*/
public Page(Collection<? extends ITEM_T> items, long itemsBefore, int pageSize, long totalCount) {
requireNonNull(items, "Required non-null items");
this.items = new ArrayList<>(items);
checkArgument(itemsBefore >= 0, "Required non-negative value of items before");
this.itemsBefore = itemsBefore;
checkArgument(pageSize > 0, "Required positive value of page size");
this.pageSize = pageSize;
checkArgument(totalCount >= 0, "Required non-negative value of total items");
this.totalCount = totalCount;
}
/** Returns true whether this page doesn't contain items, returns false if it does. */
public boolean isEmpty() {
return items.isEmpty();
}
/**
* Returns true when the current page has the next page,
* otherwise when the page is the last page false will be returned.
*/
public boolean hasNextPage() {
return getNumber() != -1 && itemsBefore + pageSize < totalCount;
}
/**
* Returns true when this page has the previous page,
* otherwise when the page is the first page false will be returned.
*/
public boolean hasPreviousPage() {
return getNumber() != -1 && itemsBefore != 0;
}
/**
* Returns an {@link Optional} describing reference to the next page,
* or the empty {@code Optional} when this page is the first one.
*/
public Optional<PageRef> getNextPageRef() {
if (!hasNextPage()) {
return Optional.empty();
}
return Optional.of(new PageRef(itemsBefore + pageSize, pageSize));
}
/**
* Returns an {@link Optional} describing reference to the previous page,
* or the empty {@code Optional} when this page is the last one.
*/
public Optional<PageRef> getPreviousPageRef() {
if (!hasPreviousPage()) {
return Optional.empty();
}
final long skipItems = itemsBefore <= pageSize ? 0 : itemsBefore - pageSize;
return Optional.of(new PageRef(skipItems, pageSize));
}
/** Returns the reference to the last page. */
public PageRef getLastPageRef() {
final long lastPageItems = totalCount % pageSize;
if (lastPageItems == 0) {
return new PageRef(totalCount <= pageSize ? 0 : totalCount - pageSize, pageSize);
}
return new PageRef(totalCount - lastPageItems, pageSize);
}
/** Returns the reference to the first page. */
public PageRef getFirstPageRef() {
return new PageRef(0, pageSize);
}
/**
* Returns the size of the current page.
*
* <p>Returned value is always positive and greater or equal
* to the value returned by the {@link #getItemsCount()} method.
*/
public int getSize() {
return pageSize;
}
/**
* Returns page number starting from 1.
*
* <p>If the page is not regular page(it refers rather to the
* data window than to the certain page(e.g. skip=2, pageSize=4))
* then this method returns -1.
*/
public long getNumber() {
if (itemsBefore % pageSize != 0) {
return -1;
}
return itemsBefore / pageSize + 1;
}
/**
* Returns the size of the page items, returned value
* may be equal to 0 when the page {@link #isEmpty() is empty},
* the values is the same to {@code page.getItems().size()}.
*/
public int getItemsCount() {
return items.size();
}
/** Returns the count of all the items. */
public long getTotalItemsCount() {
return totalCount;
}
/**
* Returns page items or an empty list when page doesn't contain items.
*
* <p>Note that returned instance is modifiable list and modification applied
* on that list will affect the origin page result items, which allows
* components to modify items before propagating page.
*/
public List<ITEM_T> getItems() {
return items;
}
/**
* Gets the page items and maps them with given {@code mapper}.
*
* @param mapper
* items mapper
* @param <R>
* the type of the result items
* @return the list of mapped items
*/
public <R> List<R> getItems(Function<? super ITEM_T, ? extends R> mapper) {
requireNonNull(mapper, "Required non-null mapper for page items");
return items.stream()
.map(mapper::apply)
.collect(toList());
}
/**
* Fills the given collection with page items.
* This method may be convenient when needed collection
* different from the {@link List}.
*
* <p>The common example:
* <pre>{@code
* Set<User> user = page.fill(new TreeSet<>(comparator));
* }</pre>
*
* <p>Note that this method uses {@code container.addAll(items)}
* so be aware of putting modifiable collection.
*
* @param container
* collection which is used to fill result into
* @param <COL_T>
* collection type
* @return given collection instance {@code container} with items filled
* @throws NullPointerException
* when {@code container} is null
*/
public <COL_T extends Collection<? super ITEM_T>> COL_T fill(COL_T container) {
requireNonNull(container, "Required non-null items container");
container.addAll(items);
return container;
}
/** Represents page reference as a combination of {@code skipItems & pageSize}. */
public static class PageRef {
private final long skipItems;
private final int pageSize;
private PageRef(long skipItems, int pageSize) {
this.skipItems = skipItems;
this.pageSize = pageSize;
}
public long getItemsBefore() {
return skipItems;
}
public int getPageSize() {
return pageSize;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof PageRef)) {
return false;
}
final PageRef that = (PageRef)obj;
return skipItems == that.skipItems && pageSize == that.pageSize;
}
@Override
public int hashCode() {
int hash = 7;
hash = 31 * hash + Long.hashCode(skipItems);
hash = 31 * hash + pageSize;
return hash;
}
}
}

View File

@ -10,6 +10,9 @@
*******************************************************************************/
package org.eclipse.che.api.core.rest;
import com.google.common.collect.ListMultimap;
import org.eclipse.che.api.core.Page;
import org.eclipse.che.api.core.rest.annotations.Description;
import org.eclipse.che.api.core.rest.annotations.GenerateLink;
import org.eclipse.che.api.core.rest.annotations.OPTIONS;
@ -20,6 +23,7 @@ import org.eclipse.che.api.core.rest.shared.dto.Link;
import org.eclipse.che.api.core.rest.shared.dto.LinkParameter;
import org.eclipse.che.api.core.rest.shared.dto.RequestBodyDescriptor;
import org.eclipse.che.api.core.rest.shared.dto.ServiceDescriptor;
import org.eclipse.che.api.core.util.PagingUtil;
import org.eclipse.che.dto.server.DtoFactory;
import javax.ws.rs.Consumes;
@ -39,14 +43,19 @@ import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import static java.lang.String.format;
import static java.util.Collections.emptyMap;
/**
* Base class for all API services.
*
@ -70,7 +79,66 @@ public abstract class Service {
return DtoFactory.getInstance().createDto(ServiceDescriptor.class);
}
//
/**
* Generates link header value based on given {@code page}
* and uri returned by {@code uriInfo.getRequestUri()}.
*
* @param page
* page to create link header
* @return link header value
*/
protected String createLinkHeader(Page<?> page) {
return PagingUtil.createLinkHeader(page, uriInfo.getRequestUri());
}
/**
* Creates uri from the given parameters and
* delegates execution to the {@link PagingUtil#createLinkHeader(Page, URI)} method.
*
* @param page
* page to create link header
* @param method
* rest service method name like {@link UriBuilder#path(Class, String) path} argument
* @param queryParams
* query parameters map, if multiple query params needed then
* {@link #createLinkHeader(Page, String, ListMultimap, Object...)}
* method should be used instead
* @param pathParams
* path param values like {f@link UriBuilder#build(Object...)} method arguments
*/
protected String createLinkHeader(Page<?> page,
String method,
Map<String, Object> queryParams,
Object... pathParams) {
final UriBuilder ub = getServiceContext().getServiceUriBuilder().path(getClass(), method);
for (Map.Entry<String, Object> queryParam : queryParams.entrySet()) {
ub.queryParam(queryParam.getKey(), queryParam.getValue());
}
return PagingUtil.createLinkHeader(page, ub.build(pathParams));
}
/**
* This method is the same to the {@link #createLinkHeader(Page, String, Map, Object...)}
* except of receiving query parameters.
*/
protected String createLinkHeader(Page<?> page, String method, Object... pathParams) {
return createLinkHeader(page, method, emptyMap(), pathParams);
}
/**
* This method is the same to {@link #createLinkHeader(Page, String, Map, Object...)}
* except of receiving multivalued query parameters.
*/
protected String createLinkHeader(Page<?> page,
String method,
ListMultimap<String, Object> queryParams,
Object... pathParams) {
final UriBuilder ub = getServiceContext().getServiceUriBuilder().path(getClass(), method);
for (Map.Entry<String, Object> queryParam : queryParams.entries()) {
ub.queryParam(queryParam.getKey(), queryParam.getValue());
}
return PagingUtil.createLinkHeader(page, ub.build(pathParams));
}
private static final Set<String> JAX_RS_ANNOTATIONS;
@ -117,7 +185,7 @@ public abstract class Service {
}
if (httpMethod == null) {
throw new IllegalArgumentException(
String.format("Method '%s' has not any HTTP method annotation and may not be used to produce link.", method.getName()));
format("Method '%s' has not any HTTP method annotation and may not be used to produce link.", method.getName()));
}
final Consumes consumes = getAnnotation(method, Consumes.class);

View File

@ -0,0 +1,106 @@
/*******************************************************************************
* Copyright (c) 2012-2016 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.api.core.util;
import org.eclipse.che.api.core.Page;
import org.eclipse.che.commons.lang.Pair;
import javax.ws.rs.core.UriBuilder;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.google.common.base.Strings.isNullOrEmpty;
import static java.lang.String.format;
import static java.util.Collections.emptyMap;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.joining;
/**
* Provides useful methods for working with {@link Page} instances
* and pageable uris management.
*
* @author Yevhenii Voevodin
*/
public final class PagingUtil {
private static final String LINK_HEADER_SEPARATOR = ", ";
/**
* Helps to retrieve href along with rel from link header value part.
* Value format is {@literal <http://host:port/path?query=value>; rel="next"},
* so the first group is href while the second group is rel.
*/
private static final Pattern LINK_HEADER_REGEX = Pattern.compile("<(?<href>.+)>;.*rel=\"(?<rel>.+)\".*");
/**
* Generates link header value from the page object and base uri.
* <a href="https://tools.ietf.org/html/rfc5988">The Link header spec</a>
*
* @param page
* the page used to generate link
* @param uri
* the uri which is used for adding {@code skipCount} & {@code maxItems} query parameters
* @return 'Link' header value
* @throws NullPointerException
* when either {@code page} or {@code uri} is null
*/
public static String createLinkHeader(Page<?> page, URI uri) {
requireNonNull(page, "Required non-null page");
requireNonNull(uri, "Required non-null uri");
final ArrayList<Pair<String, Page.PageRef>> pageRefs = new ArrayList<>(4);
pageRefs.add(Pair.of("first", page.getFirstPageRef()));
pageRefs.add(Pair.of("last", page.getLastPageRef()));
if (page.hasPreviousPage()) {
pageRefs.add(Pair.of("prev", page.getPreviousPageRef().get()));
}
if (page.hasNextPage()) {
pageRefs.add(Pair.of("next", page.getNextPageRef().get()));
}
final UriBuilder ub = UriBuilder.fromUri(uri);
return pageRefs.stream()
.map(refPair -> format("<%s>; rel=\"%s\"",
ub.clone()
.replaceQueryParam("skipCount", refPair.second.getItemsBefore())
.replaceQueryParam("maxItems", refPair.second.getPageSize())
.build()
.toString(),
refPair.first))
.collect(joining(LINK_HEADER_SEPARATOR));
}
/**
* Returns REL to URI map based on the given {@code linkHeader} value.
* If the {@code linkHeader} is null or empty then an empty map will be returned.
*
* <p>Note that link header is parsed due to the {@link #createLinkHeader(Page, URI)} method strategy.
*
* @param linkHeader
* link header value
*/
public static Map<String, String> parseLinkHeader(String linkHeader) {
if (isNullOrEmpty(linkHeader)) {
return emptyMap();
}
final Map<String, String> res = new HashMap<>();
for (String part : linkHeader.split(LINK_HEADER_SEPARATOR)) {
final Matcher matcher = LINK_HEADER_REGEX.matcher(part);
if (matcher.matches()) {
res.put(matcher.group("rel"), matcher.group("href"));
}
}
return res;
}
private PagingUtil() {}
}

View File

@ -0,0 +1,253 @@
/*******************************************************************************
* Copyright (c) 2012-2016 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.api.core;
import org.testng.annotations.Test;
import java.util.ArrayList;
import java.util.TreeSet;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.reverseOrder;
import static java.util.Collections.singleton;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
/**
* Tests for {@link Page}.
*
* @author Yevhenii Voevodin
*/
public class PageTest {
@Test(expectedExceptions = IllegalArgumentException.class,
expectedExceptionsMessageRegExp = "Required non-negative value of items before")
public void shouldThrowIllegalArgumentWhenItemsBeforeIsNegative() throws Exception {
new Page<>(emptyList(), -1, 1, 10);
}
@Test(expectedExceptions = IllegalArgumentException.class,
expectedExceptionsMessageRegExp = "Required positive value of page size")
public void shouldThrowIllegalArgumentWhenPageSizeIsNotPositive() throws Exception {
new Page<>(emptyList(), 1, 0, 10);
}
@Test(expectedExceptions = IllegalArgumentException.class,
expectedExceptionsMessageRegExp = "Required non-negative value of total items")
public void shouldThrowIllegalArgumentWhenTotalCountIsNegative() throws Exception {
new Page<>(emptyList(), 1, 1, -1);
}
@Test(expectedExceptions = NullPointerException.class,
expectedExceptionsMessageRegExp = "Required non-null items")
public void shouldThrownNPEWhenItemsListIsNull() throws Exception {
new Page<>(null, 1, 1, 1);
}
@Test
public void pageShouldBeEmptyWhenItIsCreatedWithAneEmptyItemsCollection() throws Exception {
assertTrue(new Page<>(emptyList(), 1, 1, 1).isEmpty());
}
@Test
public void testMiddleDataWindowPage() throws Exception {
// item1. <= skipped <- first page start
// item2. <= skipped
// item3. <- page start <- first page end
// item4.
// item5. <- page end <- last page start
// item6.
// item7. <- last page end
final Page<String> page = new Page<>(asList("item3", "item4", "item5"), 2, 3, 7);
assertFalse(page.isEmpty(), "non-empty page");
assertEquals(page.getItemsCount(), 3, "items size ");
assertEquals(page.getSize(), 3, "page size");
assertEquals(page.getTotalItemsCount(), 7, "total elements count");
final Page.PageRef firstRef = page.getFirstPageRef();
assertEquals(firstRef.getPageSize(), 3, "first page size");
assertEquals(firstRef.getItemsBefore(), 0, "first page skip items");
final Page.PageRef lastRef = page.getLastPageRef();
assertEquals(lastRef.getPageSize(), 3, "last page size");
assertEquals(lastRef.getItemsBefore(), 6, "last page skip items");
assertEquals(page.getNumber(), -1, "page number");
assertFalse(page.hasPreviousPage(), "has previous page");
assertFalse(page.getPreviousPageRef().isPresent(), "has previous page ref");
assertFalse(page.hasNextPage(), "page has next page");
assertFalse(page.getNextPageRef().isPresent(), "page has next page ref");
assertEquals(page.getItems(), asList("item3", "item4", "item5"));
assertEquals(page.getItems(i -> i.substring(4)), asList("3", "4", "5"));
assertEquals(new ArrayList<>(page.fill(new TreeSet<>(reverseOrder()))), asList("item5", "item4", "item3"));
}
@Test
public void testMiddlePage() throws Exception {
// item1. <- previous page start <- first page start
// item2.
// item3. <- previous page end <- first page end
// item4. <- page start
// item5.
// item6. <- page end
// item7. <- next page start
// item8.
// item9. <- next page end
// item.10 <- last page start/end
final Page<String> page = new Page<>(asList("item4", "item5", "item6"), 3, 3, 10);
assertFalse(page.isEmpty(), "non-empty page");
assertEquals(page.getItemsCount(), 3, "items size");
assertEquals(page.getSize(), 3, "page size");
assertEquals(page.getTotalItemsCount(), 10, "total elements count");
final Page.PageRef firstRef = page.getFirstPageRef();
assertEquals(firstRef.getPageSize(), 3, "first page size");
assertEquals(firstRef.getItemsBefore(), 0, "first page skip items");
final Page.PageRef lastRef = page.getLastPageRef();
assertEquals(lastRef.getPageSize(), 3, "last page size");
assertEquals(lastRef.getItemsBefore(), 9, "last page skip items");
assertEquals(page.getNumber(), 2, "page number");
assertTrue(page.hasPreviousPage(), "has previous page");
assertTrue(page.getPreviousPageRef().isPresent(), "has previous page ref");
final Page.PageRef prevRef = page.getPreviousPageRef().get();
assertEquals(prevRef.getItemsBefore(), 0, "items before prev page");
assertEquals(prevRef.getPageSize(), 3, "prev page size");
assertTrue(page.hasNextPage(), "page has next page");
assertTrue(page.getNextPageRef().isPresent(), "page has next page ref");
final Page.PageRef nextRef = page.getNextPageRef().get();
assertEquals(nextRef.getItemsBefore(), 6, "items before next page");
assertEquals(nextRef.getPageSize(), 3, "next page size");
assertEquals(page.getItems(), asList("item4", "item5", "item6"));
assertEquals(page.getItems(i -> i.substring(4)), asList("4", "5", "6"));
assertEquals(new ArrayList<>(page.fill(new TreeSet<>(reverseOrder()))), asList("item6", "item5", "item4"));
}
@Test
public void testFirstPage() throws Exception {
// item1. <- page start
// item2.
// item3.
// item4.
// item5. <- page end
// item6. <- last page start
// item7. <- last page end
final Page<String> page = new Page<>(asList("item1", "item2", "item3", "item4", "item5"), 0, 5, 7);
assertFalse(page.isEmpty(), "page is empty");
assertEquals(page.getItemsCount(), 5, "items items count");
assertEquals(page.getSize(), 5, "page size");
assertEquals(page.getTotalItemsCount(), 7, "total items");
final Page.PageRef firstRef = page.getFirstPageRef();
assertEquals(firstRef.getPageSize(), 5, "first page size");
assertEquals(firstRef.getItemsBefore(), 0, "first page skip items");
final Page.PageRef lastRef = page.getLastPageRef();
assertEquals(lastRef.getPageSize(), 5, "last page size");
assertEquals(lastRef.getItemsBefore(), 5, "last page skip items");
assertEquals(page.getNumber(), 1, "page number");
assertFalse(page.hasPreviousPage(), "has previous page");
assertFalse(page.getPreviousPageRef().isPresent(), "page has previous page ref");
assertTrue(page.hasNextPage(), "page has next page");
assertTrue(page.getNextPageRef().isPresent(), "page has next page ref");
final Page.PageRef nextRef = page.getNextPageRef().get();
assertEquals(nextRef.getPageSize(), 5, "next page size");
assertEquals(nextRef.getItemsBefore(), 5, "next page skip items");
assertEquals(page.getItems(), asList("item1", "item2", "item3", "item4", "item5"));
}
@Test
public void testLastPage() throws Exception {
// item1. <- first page start
// item2.
// item3. <- first page end
// item4. <- prev page start
// item5.
// item6. <- prev page end
// item7. <- page start
// item8. <- page end
final Page<String> page = new Page<>(asList("item7", "item8"), 6, 3, 8);
assertFalse(page.isEmpty(), "page is empty");
assertEquals(page.getItemsCount(), 2, "items count");
assertEquals(page.getSize(), 3, "page size");
assertEquals(page.getTotalItemsCount(), 8, "total items");
final Page.PageRef firstRef = page.getFirstPageRef();
assertEquals(firstRef.getPageSize(), 3, "first page size");
assertEquals(firstRef.getItemsBefore(), 0, "first page skip items");
final Page.PageRef lastRef = page.getLastPageRef();
assertEquals(lastRef.getPageSize(), 3, "last page size");
assertEquals(lastRef.getItemsBefore(), 6, "last page skip items");
assertEquals(page.getNumber(), 3, "page number");
assertTrue(page.hasPreviousPage(), "has previous page");
assertTrue(page.getPreviousPageRef().isPresent(), "page has previous page ref");
final Page.PageRef prevRef = page.getPreviousPageRef().get();
assertEquals(prevRef.getPageSize(), 3, "prev page size");
assertEquals(prevRef.getItemsBefore(), 3, "prev page skip items");
assertFalse(page.hasNextPage(), "has next page");
assertFalse(page.getNextPageRef().isPresent(), "page has next page ref");
assertEquals(page.getItems(), asList("item7", "item8"));
}
@Test
public void testSmallPage() throws Exception {
final Page<String> page = new Page<>(singleton("item1"), 0, 1, 1);
assertFalse(page.isEmpty(), "page is empty");
assertEquals(page.getItemsCount(), 1, "items count");
assertEquals(page.getSize(), 1, "page size");
assertEquals(page.getTotalItemsCount(), 1, "total items");
final Page.PageRef firstRef = page.getFirstPageRef();
assertEquals(firstRef.getPageSize(), 1, "first page size");
assertEquals(firstRef.getItemsBefore(), 0, "first page skip items");
final Page.PageRef lastRef = page.getLastPageRef();
assertEquals(lastRef.getPageSize(), 1, "last page size");
assertEquals(lastRef.getItemsBefore(), 0, "last page skip items");
assertEquals(page.getNumber(), 1, "page number");
assertFalse(page.hasPreviousPage(), "has previous page");
assertFalse(page.getPreviousPageRef().isPresent(), "page has previous page ref");
assertFalse(page.hasNextPage(), "page has next page");
assertFalse(page.getNextPageRef().isPresent(), "page has next page ref");
assertEquals(page.getItems(), singleton("item1"));
}
}

View File

@ -0,0 +1,78 @@
/*******************************************************************************
* Copyright (c) 2012-2016 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.api.core.rest;
import com.jayway.restassured.response.Response;
import org.eclipse.che.api.core.util.PagingUtil;
import org.everrest.assured.EverrestJetty;
import org.testng.ITestContext;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
import java.net.URI;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.google.common.collect.Sets.symmetricDifference;
import static com.jayway.restassured.RestAssured.given;
import static java.util.Arrays.asList;
import static org.eclipse.che.commons.lang.UrlUtils.getQueryParameters;
import static org.everrest.assured.JettyHttpServer.ADMIN_USER_NAME;
import static org.everrest.assured.JettyHttpServer.ADMIN_USER_PASSWORD;
import static org.everrest.assured.JettyHttpServer.SECURE_PATH;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
/**
* Tests of {@link Service#createLinkHeader} methods.
*
* @author Yevhenii Voevodin
*/
@Listeners(EverrestJetty.class)
public class LinkHeaderGenerationTest {
@SuppressWarnings("unused") // used by EverrestJetty
private static final TestService TEST_SERVICE = new TestService();
@Test
public void linksHeaderShouldBeCorrectlyGenerated(ITestContext ctx) throws Exception {
final Response response = given().auth()
.basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
.contentType("application/json")
.when()
.get(SECURE_PATH + "/test/paging/test-path-param?query-param=test-query-param");
assertEquals(response.getStatusCode(), 200);
final String headerValue = response.getHeader("Link");
assertNotNull(headerValue, "Link header is missing in the response");
final Map<String, String> relToLinkMap = PagingUtil.parseLinkHeader(headerValue);
final Set<String> expectedRels = new HashSet<>(asList("first", "last", "prev", "next"));
assertEquals(relToLinkMap.keySet(), expectedRels,
"Rels are different " + symmetricDifference(expectedRels, relToLinkMap.keySet()));
final String expectedUri = "http://localhost:" + ctx.getAttribute(EverrestJetty.JETTY_PORT) +
"/rest/private/test/paging/test-path-param";
for (String link : relToLinkMap.values()) {
final URI uri = URI.create(link);
final Map<String, List<String>> params = getQueryParameters(uri.toURL());
assertEquals(params.size(), 3);
assertNotNull(params.get("skipCount"));
assertNotNull(params.get("maxItems"));
assertEquals(params.get("query-param").get(0), "test-query-param");
assertEquals(link, expectedUri + '?' + uri.getQuery());
}
}
}

View File

@ -10,6 +10,7 @@
*******************************************************************************/
package org.eclipse.che.api.core.rest;
import org.eclipse.che.api.core.Page;
import org.eclipse.che.api.core.UnauthorizedException;
import org.eclipse.che.api.core.rest.shared.dto.Link;
import org.eclipse.che.api.core.rest.shared.dto.ServiceError;
@ -34,7 +35,9 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.MediaType.TEXT_PLAIN;
@ -44,7 +47,7 @@ import static javax.ws.rs.core.MediaType.TEXT_PLAIN;
* @author Yevhenii Voevodin
*/
@Path("/test")
public class TestService {
public class TestService extends Service {
public static final String JSON_OBJECT = new JsonArrayImpl<>(singletonList("element")).toJson();
@ -111,4 +114,16 @@ public class TestService {
@Context UriInfo uriInfo) {
return URLDecoder.decode(uriInfo.getRequestUri().toString());
}
@GET
@Path("/paging/{value}")
@Produces(APPLICATION_JSON)
public Response getStringList(@PathParam("value") String value, @QueryParam("query-param") String param) {
final Page<String> page = new Page<>(asList("item3", "item4", "item5"), 3, 3, 7);
return Response.ok()
.entity(page.getItems())
.header("Link", createLinkHeader(page, "getStringList", singletonMap("query-param", param), value))
.build();
}
}

View File

@ -0,0 +1,62 @@
/*******************************************************************************
* Copyright (c) 2012-2016 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.api.core.util;
import org.eclipse.che.api.core.Page;
import org.testng.annotations.Test;
import java.net.URI;
import java.util.Map;
import static java.util.Arrays.asList;
import static org.eclipse.che.api.core.util.PagingUtil.createLinkHeader;
import static org.eclipse.che.api.core.util.PagingUtil.parseLinkHeader;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertEqualsNoOrder;
/**
* Tests for {@link PagingUtil}.
*
* @author Yevhenii Voevodin
*/
public class PagingUtilTest {
@Test
public void testCreatingLinksHeader() throws Exception {
final Page<String> page = new Page<>(asList("item3", "item4", "item5"), 3, 3, 7);
final URI srcUri = URI.create("http://localhost:8080/path?qp=test");
final String linkHeader = createLinkHeader(page, srcUri);
final String[] expLinks = ("<http://localhost:8080/path?qp=test&skipCount=0&maxItems=3>; rel=\"first\", " +
"<http://localhost:8080/path?qp=test&skipCount=6&maxItems=3>; rel=\"last\", " +
"<http://localhost:8080/path?qp=test&skipCount=0&maxItems=3>; rel=\"prev\", " +
"<http://localhost:8080/path?qp=test&skipCount=6&maxItems=3>; rel=\"next\"").split(", ");
assertEqualsNoOrder(linkHeader.split(", "), expLinks);
}
@Test
public void testParsingLinksHeader() throws Exception {
final Map<String, String> relToLinks =
parseLinkHeader("<http://localhost:8080/path?qp=test&skipCount=0&maxItems=3>; rel=\"first\", " +
"<http://localhost:8080/path?qp=test&skipCount=4&maxItems=3>; rel=\"last\", " +
"<http://localhost:8080/path?qp=test&skipCount=0&maxItems=3>; rel=\"prev\", " +
"<http://localhost:8080/path?qp=test&skipCount=5&maxItems=3>; rel=\"next\"");
assertEquals(relToLinks.size(), 4);
assertEquals(relToLinks.get("first"), "http://localhost:8080/path?qp=test&skipCount=0&maxItems=3");
assertEquals(relToLinks.get("last"), "http://localhost:8080/path?qp=test&skipCount=4&maxItems=3");
assertEquals(relToLinks.get("next"), "http://localhost:8080/path?qp=test&skipCount=5&maxItems=3");
assertEquals(relToLinks.get("prev"), "http://localhost:8080/path?qp=test&skipCount=0&maxItems=3");
}
}