javax.annotation
javax.annotation-api
diff --git a/core/che-core-api-core/src/main/java/org/eclipse/che/api/core/Page.java b/core/che-core-api-core/src/main/java/org/eclipse/che/api/core/Page.java
new file mode 100644
index 0000000000..b4382b5e54
--- /dev/null
+++ b/core/che-core-api-core/src/main/java/org/eclipse/che/api/core/Page.java
@@ -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.
+ *
+ * Examples:
+ *
+ *
Regular page.
+ * 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):
+ *
+ * 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
+ *
+ *
+ * Result page:
+ * - Non-empty
+ * - Contains 3 items(item4, item5, item6)
+ * - Has the previous page(item1, item2, item3)
+ *
- Has the next page(item7, item8, item9)
+ *
+ *
+ *
+ * Data window.
+ * For page input arguments {@code skipItems = 2}, {@code pageSize = 3} and {@code totalCount = 7}
+ * the page will refer to the following data window:
+ *
+ * item1. <- first page start
+ * item2.
+ * item3. <- page start <- first page end
+ * item4.
+ * item5. <- page end
+ * item6.
+ * item7. <- last page start/end
+ *
+ *
+ * Result page:
+ * - Non-empty
+ * - Contains 3 items(item3, item4, item5)
+ * - 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)
+ * - Doesn't have the next page(the reason is the same to previous statement)
+ *
+ *
+ * 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.
+ *
+ *
The instances of this class are NOT thread safe.
+ *
+ * @param
+ * the type of the page items
+ * @author Yevhenii Voevodin
+ */
+public class Page {
+
+ private final int pageSize;
+ private final long itemsBefore;
+ private final long totalCount;
+ private final List 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 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 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.
+ *
+ * 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.
+ *
+ *
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.
+ *
+ *
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 getItems() {
+ return items;
+ }
+
+ /**
+ * Gets the page items and maps them with given {@code mapper}.
+ *
+ * @param mapper
+ * items mapper
+ * @param
+ * the type of the result items
+ * @return the list of mapped items
+ */
+ public List 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}.
+ *
+ * The common example:
+ *
{@code
+ * Set user = page.fill(new TreeSet<>(comparator));
+ * }
+ *
+ * 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
+ * collection type
+ * @return given collection instance {@code container} with items filled
+ * @throws NullPointerException
+ * when {@code container} is null
+ */
+ public > 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;
+ }
+ }
+}
diff --git a/core/che-core-api-core/src/main/java/org/eclipse/che/api/core/rest/Service.java b/core/che-core-api-core/src/main/java/org/eclipse/che/api/core/rest/Service.java
index 524a08dfc3..08a4066b71 100644
--- a/core/che-core-api-core/src/main/java/org/eclipse/che/api/core/rest/Service.java
+++ b/core/che-core-api-core/src/main/java/org/eclipse/che/api/core/rest/Service.java
@@ -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 queryParams,
+ Object... pathParams) {
+ final UriBuilder ub = getServiceContext().getServiceUriBuilder().path(getClass(), method);
+ for (Map.Entry 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 queryParams,
+ Object... pathParams) {
+ final UriBuilder ub = getServiceContext().getServiceUriBuilder().path(getClass(), method);
+ for (Map.Entry queryParam : queryParams.entries()) {
+ ub.queryParam(queryParam.getKey(), queryParam.getValue());
+ }
+ return PagingUtil.createLinkHeader(page, ub.build(pathParams));
+ }
private static final Set 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);
diff --git a/core/che-core-api-core/src/main/java/org/eclipse/che/api/core/util/PagingUtil.java b/core/che-core-api-core/src/main/java/org/eclipse/che/api/core/util/PagingUtil.java
new file mode 100644
index 0000000000..c5ba6c7e3d
--- /dev/null
+++ b/core/che-core-api-core/src/main/java/org/eclipse/che/api/core/util/PagingUtil.java
@@ -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 ; rel="next"},
+ * so the first group is href while the second group is rel.
+ */
+ private static final Pattern LINK_HEADER_REGEX = Pattern.compile("<(?.+)>;.*rel=\"(?.+)\".*");
+
+ /**
+ * Generates link header value from the page object and base uri.
+ * The Link header spec
+ *
+ * @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> 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.
+ *
+ * Note that link header is parsed due to the {@link #createLinkHeader(Page, URI)} method strategy.
+ *
+ * @param linkHeader
+ * link header value
+ */
+ public static Map parseLinkHeader(String linkHeader) {
+ if (isNullOrEmpty(linkHeader)) {
+ return emptyMap();
+ }
+ final Map 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() {}
+}
diff --git a/core/che-core-api-core/src/test/java/org/eclipse/che/api/core/PageTest.java b/core/che-core-api-core/src/test/java/org/eclipse/che/api/core/PageTest.java
new file mode 100644
index 0000000000..f2638a294d
--- /dev/null
+++ b/core/che-core-api-core/src/test/java/org/eclipse/che/api/core/PageTest.java
@@ -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 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 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 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 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 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"));
+ }
+}
diff --git a/core/che-core-api-core/src/test/java/org/eclipse/che/api/core/rest/LinkHeaderGenerationTest.java b/core/che-core-api-core/src/test/java/org/eclipse/che/api/core/rest/LinkHeaderGenerationTest.java
new file mode 100644
index 0000000000..d969a02a7d
--- /dev/null
+++ b/core/che-core-api-core/src/test/java/org/eclipse/che/api/core/rest/LinkHeaderGenerationTest.java
@@ -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 relToLinkMap = PagingUtil.parseLinkHeader(headerValue);
+ final Set 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> 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());
+ }
+ }
+}
diff --git a/core/che-core-api-core/src/test/java/org/eclipse/che/api/core/rest/TestService.java b/core/che-core-api-core/src/test/java/org/eclipse/che/api/core/rest/TestService.java
index 42aa041d55..1833d83bc8 100644
--- a/core/che-core-api-core/src/test/java/org/eclipse/che/api/core/rest/TestService.java
+++ b/core/che-core-api-core/src/test/java/org/eclipse/che/api/core/rest/TestService.java
@@ -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 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();
+ }
}
diff --git a/core/che-core-api-core/src/test/java/org/eclipse/che/api/core/util/PagingUtilTest.java b/core/che-core-api-core/src/test/java/org/eclipse/che/api/core/util/PagingUtilTest.java
new file mode 100644
index 0000000000..878749591d
--- /dev/null
+++ b/core/che-core-api-core/src/test/java/org/eclipse/che/api/core/util/PagingUtilTest.java
@@ -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 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 = ("; rel=\"first\", " +
+ "; rel=\"last\", " +
+ "; rel=\"prev\", " +
+ "; rel=\"next\"").split(", ");
+ assertEqualsNoOrder(linkHeader.split(", "), expLinks);
+ }
+
+ @Test
+ public void testParsingLinksHeader() throws Exception {
+ final Map relToLinks =
+ parseLinkHeader("; rel=\"first\", " +
+ "; rel=\"last\", " +
+ "; rel=\"prev\", " +
+ "; 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");
+ }
+}