inventory-framework
inventory-framework copied to clipboard
Do some error treatment here, even if we expect to the user to handle it
https://github.com/DevNatan/inventory-framework/blob/1fae8ce006bf7f27d71a4ea71e234c30e695fb41/inventory-framework-core/src/main/java/me/devnatan/inventoryframework/component/PaginationImpl.java#L146
package me.devnatan.inventoryframework.component;
import static me.devnatan.inventoryframework.IFDebug.debug;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import me.devnatan.inventoryframework.Ref;
import me.devnatan.inventoryframework.ViewContainer;
import me.devnatan.inventoryframework.VirtualView;
import me.devnatan.inventoryframework.context.*;
import me.devnatan.inventoryframework.internal.LayoutSlot;
import me.devnatan.inventoryframework.state.State;
import me.devnatan.inventoryframework.state.StateValue;
import me.devnatan.inventoryframework.state.StateValueHost;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.UnmodifiableView;
import org.jetbrains.annotations.VisibleForTesting;
// TODO add "key" to child pagination components and check if it needs to be updated based on it
@VisibleForTesting
public class PaginationImpl extends AbstractComponent implements Pagination, StateValue {
private List<Component> components = new ArrayList<>();
// --- User provided ---
private final char layoutTarget;
private final Object sourceProvider;
private final PaginationElementFactory<Object> elementFactory;
private final BiConsumer<VirtualView, Pagination> pageSwitchHandler;
// --- Internal ---
private final long internalStateId;
private int currPageIndex;
private final boolean isLazy, isStatic, isComputed, isAsync;
private boolean pageWasChanged;
private boolean initialized;
private int pagesCount;
private LayoutSlot currentLayoutSlot;
// Number of elements that each page can have. -1 means uninitialized.
private int pageSize = -1;
// Changes when dynamic data source is used and being loaded
private boolean isLoading;
/**
* Final source factory for dynamic or asynchronous pagination converted from {@link #sourceProvider}.
* <p>
* The return parameter of this source factory can be either {@code List} in dynamic pagination
* or {@code CompletableFuture<List>} in asynchronous pagination.
*/
private Function<VirtualView, Object> _srcFactory;
// Current page source, null before first pagination render.
private List<?> currSource;
public PaginationImpl(
String key,
VirtualView root,
Ref<Component> reference,
Set<State<?>> watchingStates,
Predicate<? extends IFContext> displayCondition,
long internalStateId,
char layoutTarget,
Object sourceProvider,
PaginationElementFactory<Object> elementFactory,
BiConsumer<VirtualView, Pagination> pageSwitchHandler,
boolean isAsync,
boolean isComputed) {
super(key, root, reference, watchingStates, displayCondition);
this.internalStateId = internalStateId;
this.layoutTarget = layoutTarget;
this.sourceProvider = sourceProvider;
this.elementFactory = elementFactory;
this.pageSwitchHandler = pageSwitchHandler;
this.currSource = convertSourceProvider();
this.isComputed = isComputed;
this.isAsync = isAsync;
this.isStatic = sourceProvider instanceof Collection;
this.isLazy =
!isStatic && !isComputed && (sourceProvider instanceof Function || sourceProvider instanceof Supplier);
}
/**
* Tries to access and load the source to the current page.
* <p>
* If this pagination {@link #isLazy() is lazy} it tries to get the current data source
* dynamically or asynchronously and waits for its completion. For static pagination it returns
* immediately with the source.
* <p>
* On asynchronous pagination the source update job will be inherited by the user provided one
* and when job gets done the {@link #currSource} is updated with the result of the computation.
*
* @return A CompletableFuture with the current pagination source as result.
* @throws IllegalStateException In static pagination when the current source wasn't yet defined.
*/
private CompletableFuture<List<?>> loadSourceForTheCurrentPage() {
/*
* In lazy pagination **that was already initialized (already rendered before)** we must
* use the current data source as source of truth to ensure that page switching do not
* re-trigger pagination data factory since it will always return the source as a whole,
* the original one, and not the source for the switched page.
*/
final boolean reuseLazy = isLazy() && initialized;
debug(
"[Pagination] Loading page %d (reuseLazy = %b, isStatic = %b, isComputed = %b, forceUpdated = %b)",
currentPageIndex(), reuseLazy, isStatic(), isComputed(), wasForceUpdated());
if ((isStatic() || reuseLazy) && !isComputed() && !wasForceUpdated()) {
// For unknown reasons already initialized but source is null, external modification?
if (initialized && currSource == null)
throw new IllegalStateException("User provided pagination source cannot be null");
else {
// Lazy pagination have pages count calculated on first render as a computed flow
if (!isLazy()) pagesCount = calculatePagesCount(currSource);
}
final List<?> result =
Pagination.splitSourceForPage(currentPageIndex(), getPageSize(), getPagesCount(), currSource);
debug(
"[Pagination] Split source of %d elements (page = %d, pageSize = %d, pagesCount = %d)",
result.size(), currentPageIndex(), getPageSize(), getPagesCount());
int index = 0;
for (final Object el : result) {
debug(" | (%d): %s", index++, el);
}
return CompletableFuture.completedFuture(result);
}
isLoading = true;
simulateStateUpdate();
// TODO Do some error treatment here, even if we expect to the user to handle it
return createProvidedNewSource().handle((result, exception) -> {
if (exception != null) {
debug("[Pagination] An error occurred on data source computation: %s", exception.getMessage());
exception.printStackTrace();
return Collections.emptyList();
}
updateSource(result);
isLoading = false;
simulateStateUpdate();
if (isLazy())
return Pagination.splitSourceForPage(currentPageIndex(), getPageSize(), getPagesCount(), result);
else return result;
});
}
@SuppressWarnings("unchecked")
private CompletableFuture<List<?>> createProvidedNewSource() {
CompletableFuture<List<?>> job = new CompletableFuture<>();
final Object source = _srcFactory.apply(getRoot());
if (isAsync()) job = (CompletableFuture<List<?>>) source;
else if (isComputed() || isLazy()) job.complete((List<?>) source);
else throw new IllegalArgumentException("Unhandled pagination source");
return job;
}
/**
* Updates the current source and the number of available pages count based on that source.
*
* @param newSource The new data source.
*/
private void updateSource(@NotNull List<?> newSource) {
currSource = newSource;
pagesCount = calculatePagesCount(currSource);
debug("[Pagination] Source updated with %d elements and pages count set to %d", newSource.size(), pagesCount);
}
/**
* The total number of pages available.
*
* @return The number of pages based on the current source.
*/
private int getPagesCount() {
return pagesCount;
}
/**
* Calculates the number of pages available based on a given source.
*
* @param source The source to check.
* @return The number of pages that can have based on the specified source.
*/
private int calculatePagesCount(@NotNull List<?> source) {
return (int) Math.ceil((double) source.size() / getPageSize());
}
/**
* The current page size.
*
* @return Number of available elements position for pagination in the current page.
*/
public int getPageSize() {
if (pageSize == -1) throw new IllegalStateException("Page size need to be updated before try to get it");
return pageSize;
}
/**
* Loads pagination components using container boundaries, no constraints.
* <p>
* The position of the first paged item must be the first slot in the container, the last
* position must be the last slot in the container, and {@link #pageSize} on the current page
* must be the size of the container.
*
* @param context The render context.
* @param pageContents Elements of the current page.
*/
private void addComponentsForUnconstrainedPagination(IFRenderContext context, List<?> pageContents) {
final ViewContainer container = context.getContainer();
// TODO Investigate why page size is being updated here
if (pageSize == -1) updatePageSize(context);
final int lastSlot = Math.min(container.getLastSlot() + 1 /* inclusive */, pageContents.size());
for (int i = container.getFirstSlot(); i < lastSlot; i++) {
final Object value = pageContents.get(i);
final Component component = elementFactory.create(this, i, i, value);
getInternalComponents().add(component);
}
}
/**
* Renders the pagination using the layout positions defined from the {@link #getLayoutTarget() target layout character}.
* <p>
* The first position, last position and number of items on the page must be exactly the same as
* the layout.
*
* @param context The render context.
* @param pageContents Elements of the current page.
*/
private void addComponentsForLayeredPagination(IFRenderContext context, List<?> pageContents) {
final LayoutSlot targetLayoutSlot = getLayoutSlotForCurrentTarget(context);
final int elementsLen = pageContents.size();
debug("[Pagination] Elements count: %d elements", elementsLen);
debug("[Pagination] Iterating over '%c' layout target", targetLayoutSlot.getCharacter());
int iterationIndex = 0;
for (final int position : targetLayoutSlot.getPositions()) {
final Object value = pageContents.get(iterationIndex++);
try {
final Component component = elementFactory.create(this, iterationIndex, position, value);
debug(
() -> " @ added %d (index %d) = %s",
position,
iterationIndex,
component.getClass().getSimpleName());
getInternalComponents().add(component);
} catch (final Exception exception) {
debug(() -> " @ failed to add %d (index %d) = %s", position, iterationIndex, exception.getMessage());
exception.printStackTrace();
}
if (iterationIndex == elementsLen) break;
}
}
/**
* Updates the current page size.
* <p>
* Page size is based on the type of pagination data source; on the possible usage of layout
* in context, if a layout is configured in the layout so this property must be the count of
* {@link #getLayoutTarget() layout target} characters in the layout configured layout.
* <p>
* When without a configured layout in the root, the page size is the entire size of context's container.
*
* @param context The render context.
*/
private void updatePageSize(IFRenderContext context) {
final boolean useLayout = context.getConfig().getLayout() != null;
if (useLayout) pageSize = getLayoutSlotForCurrentTarget(context).getPositions().length;
else pageSize = context.getContainer().getSize();
debug(
"[Pagination] Page size updated to %d (page = %d, useLayout = %b)",
pageSize, currentPageIndex(), useLayout);
}
private LayoutSlot getLayoutSlotForCurrentTarget(IFRenderContext context) {
if (currentLayoutSlot != null) return currentLayoutSlot;
final Optional<LayoutSlot> layoutSlotOptional = context.getLayoutSlots().stream()
.filter(layoutSlot -> layoutSlot.getCharacter() == getLayoutTarget())
.findFirst();
if (!layoutSlotOptional.isPresent())
// TODO more detailed error message
throw new IllegalArgumentException(String.format("Layout slot target not found: %c", getLayoutTarget()));
return (currentLayoutSlot = layoutSlotOptional.get());
}
/**
* Converts the user provided source provider to a valid static source.
* <p>
* Also, assigns the {@link #_srcFactory} value if the provided source has dynamic capabilities.
*
* @return The current source.
* @throws IllegalArgumentException If the provided source is not supported.
*/
@SuppressWarnings("unchecked")
private List<?> convertSourceProvider() {
if (sourceProvider instanceof Collection) {
currSource = new ArrayList<>((Collection<?>) sourceProvider);
} else if (sourceProvider instanceof Function) {
_srcFactory = (Function<VirtualView, Object>) sourceProvider;
} else if (sourceProvider instanceof Supplier) {
_srcFactory = $ -> ((Supplier<List<?>>) sourceProvider).get();
} else {
throw new IllegalArgumentException(String.format(
"Unsupported pagination source provider: %s",
sourceProvider.getClass().getName()));
}
return currSource;
}
/**
* Loads the current page contents.
*
* @param context The render context.
* @return A CompletableFuture with the completion stage of the current page.
*/
private CompletableFuture<?> loadCurrentPage(IFRenderContext context) {
return loadSourceForTheCurrentPage().thenAccept(pageContents -> {
if (pageContents.isEmpty()) {
debug("[Pagination] Empty page contents (page %d of %d)", currentPageIndex(), getPagesCount());
return;
}
final boolean useLayout = context.getConfig().getLayout() != null;
debug("[Pagination] Adding components.. (useLayout = %b)", useLayout);
if (useLayout) addComponentsForLayeredPagination(context, pageContents);
else addComponentsForUnconstrainedPagination(context, pageContents);
});
}
@Override
public long internalId() {
return internalStateId;
}
@Override
public Object get() {
return this;
}
@Override
public void set(Object value) {
// do nothing since Pagination is not immutable but unmodifiable directly
}
private void accessStateHost(Consumer<StateValueHost> consumer) {
if (!(getRoot() instanceof StateValueHost)) return;
consumer.accept((StateValueHost) getRoot());
}
@Override
public void render(@NotNull IFComponentRenderContext context) {
final IFRenderContext root = context.getParent();
if (!initialized || pageWasChanged) {
if (!initialized) updatePageSize(root);
loadCurrentPage(root).thenRun(() -> {
renderChild(root);
simulateStateUpdate();
});
setVisible(true);
initialized = true;
return;
}
renderChild(root);
}
private void renderChild(IFRenderContext context) {
getInternalComponents().forEach(context::renderComponent);
}
@Override
public void updated(@NotNull IFComponentUpdateContext context) {
// TODO Change to context.getParent()
final IFRenderContext root = (IFRenderContext) context.getRoot();
debug(
"[Pagination] #updated(IFSlotRenderContext) called (forceUpdated = %b, pageWasChanged = %b)",
wasForceUpdated(), pageWasChanged);
// If page was changed all components will be removed, so don't trigger update on them
if (wasForceUpdated() || pageWasChanged) {
cleared(root);
components = new ArrayList<>();
root.renderComponent(this);
pageWasChanged = false;
return;
}
if (!isVisible()) return;
getInternalComponents().forEach(child -> root.updateComponent(child, context.isForceUpdate(), null));
}
/**
* Simulate state update to call listeners thus calling watches in parent components.
* <p>
* Used when something changes in pagination. It allows the end user and developers to "listen"
* for changes in {@link #isLoading()} and current page states.
*/
private void simulateStateUpdate() {
debug("[Pagination] State update simulation triggered on %d", internalStateId);
accessStateHost(host -> host.updateState(internalStateId, this));
}
@Override
public void cleared(@NotNull IFRenderContext context) {
debug("[Pagination] #clear(IFRenderContext) called (pageWasChanged = %b)", pageWasChanged);
if (!pageWasChanged) {
getInternalComponents().forEach(context::clearComponent);
return;
}
final Iterator<Component> childIterator = getInternalComponents().iterator();
while (childIterator.hasNext()) {
Component child = childIterator.next();
context.clearComponent(child);
childIterator.remove();
}
}
@Override
public @UnmodifiableView List<Component> getComponents() {
return Collections.unmodifiableList(getInternalComponents());
}
@Override
public List<Component> getInternalComponents() {
return components;
}
@Override
public boolean isContainedWithin(int position) {
if (currentLayoutSlot != null) {
for (int slot : currentLayoutSlot.getPositions()) {
if (slot == position) return true;
}
return false;
}
for (final Component component : getInternalComponents()) {
if (component.isContainedWithin(position)) return true;
}
return false;
}
@Override
public boolean intersects(@NotNull Component other) {
throw new UnsupportedOperationException("Missing #intersects(Component) implementation.");
}
@Override
public int currentPage() {
return currentPageIndex() + 1;
}
@Override
public int currentPageIndex() {
return currPageIndex;
}
@Override
public int nextPage() {
return Math.min(getPagesCount(), currentPageIndex() + 1);
}
@Override
public int nextPageIndex() {
return Math.max(0, nextPage() - 1);
}
@Override
public int lastPage() {
return getPagesCount();
}
@Override
public int lastPageIndex() {
return Math.max(0, getPagesCount() - 1);
}
@Override
public boolean isFirstPage() {
return currentPageIndex() == 0;
}
@Override
public boolean isLastPage() {
return !canAdvance();
}
@Override
public boolean hasPage(int pageIndex) {
if (isComputed()) return true;
if (pageIndex < 0) return false;
return pageIndex < getPagesCount();
}
@Override
public void switchTo(int pageIndex) {
debug("[Pagination] #switchTo(int) called (pageIndex = %d, isLoading = %b)", pageIndex, isLoading());
if (!hasPage(pageIndex))
throw new IndexOutOfBoundsException(
String.format("Page index not found (%d > %d)", pageIndex, getPagesCount()));
if (isLoading()) return;
currPageIndex = pageIndex;
pageWasChanged = true;
if (pageSwitchHandler != null) {
pageSwitchHandler.accept(getRoot(), this);
}
update();
}
@Override
public void advance() {
if (!canAdvance()) return;
switchTo(currentPageIndex() + 1);
}
@Override
public boolean canAdvance() {
return hasPage(currentPageIndex() + 1);
}
@Override
public void back() {
if (!canBack()) return;
switchTo(currentPageIndex() - 1);
}
@Override
public boolean canBack() {
return hasPage(currentPageIndex() - 1);
}
@Override
public char getLayoutTarget() {
return layoutTarget;
}
@Override
public boolean isLazy() {
return isLazy;
}
@Override
public boolean isStatic() {
return isStatic;
}
@Override
public boolean isComputed() {
return isComputed;
}
@Override
public boolean isAsync() {
return isAsync;
}
@Override
public boolean isLoading() {
return isLoading;
}
@NotNull
@Override
public Iterator<Component> iterator() {
return getComponents().iterator();
}
@Override
public void setVisible(boolean visible) {
super.setVisible(visible);
getInternalComponents().forEach(component -> component.setVisible(isVisible()));
}
@Override
public void clicked(@NotNull IFSlotClickContext context) {
// Lock child interactions while page is changing (specially for async pagination cases)
if (pageWasChanged) {
context.setCancelled(true);
return;
}
for (final Component child : getInternalComponents()) {
if (!child.isVisible()) {
continue;
}
if (child.isContainedWithin(context.getClickedSlot())) {
context.getParent()
.performClickInComponent(
child,
context.getViewer(),
context.getClickedContainer(),
context.getPlatformEvent(),
context.getClickedSlot(),
true);
break;
}
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PaginationImpl that = (PaginationImpl) o;
return getLayoutTarget() == that.getLayoutTarget()
&& currPageIndex == that.currPageIndex
&& getPageSize() == that.getPageSize()
&& isLazy() == that.isLazy()
&& pageWasChanged == that.pageWasChanged
&& Objects.equals(sourceProvider, that.sourceProvider)
&& Objects.equals(pageSwitchHandler, that.pageSwitchHandler);
}
@Override
public int hashCode() {
return Objects.hash(
getLayoutTarget(),
sourceProvider,
pageSwitchHandler,
currPageIndex,
getPageSize(),
isLazy(),
pageWasChanged);
}
@Override
public String toString() {
return "PaginationImpl{" + ", root="
+ getRoot() + ", layoutTarget="
+ layoutTarget + ", sourceProvider="
+ sourceProvider + ", elementFactory="
+ elementFactory + ", pageSwitchHandler="
+ pageSwitchHandler + ", currPageIndex="
+ currPageIndex + ", pageSize="
+ pageSize + ", dynamic="
+ isLazy + ", pageWasChanged="
+ pageWasChanged + ", _srcFactory="
+ _srcFactory + ", currSource="
+ currSource + "} "
+ super.toString();
}
}