diff --git a/core/che-core-api-core/pom.xml b/core/che-core-api-core/pom.xml index f9373ee84e..a5934385c8 100644 --- a/core/che-core-api-core/pom.xml +++ b/core/che-core-api-core/pom.xml @@ -42,6 +42,10 @@ com.google.inject.extensions guice-multibindings + + com.jayway.restassured + rest-assured + 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
+ * 
+ * + * + * + * + *

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
+ * 
+ * + * + * + *

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 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 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"); + } +}