/*******************************************************************************
* Copyright (c) 2012-2017 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.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 a reference to the next page.
*
* <p>Note: This method was designed to be used in couple with {@link #hasNextPage()}.
* Returns reference to the next page even when {@link #hasNextPage()} returns false.
*/
public PageRef getNextPageRef() {
return new PageRef(itemsBefore + pageSize, pageSize);
}
/**
* Returns a reference to the previous page.
*
* <p>Note: This method was designed to be used in couple with {@link #hasPreviousPage()}.
* Returns reference to the first page when {@link #hasPreviousPage()} returns false.
*/
public PageRef getPreviousPageRef() {
final long skipItems = itemsBefore <= pageSize ? 0 : itemsBefore - pageSize;
return 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;
}
}
}