All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.dellroad.stuff.vaadin24.data.AsyncDataProvider Maven / Gradle / Ivy

The newest version!

/*
 * Copyright (C) 2022 Archie L. Cobbs. All rights reserved.
 */

package org.dellroad.stuff.vaadin24.data;

import com.google.common.base.Preconditions;
import com.vaadin.flow.data.provider.DataProvider;
import com.vaadin.flow.data.provider.ListDataProvider;
import com.vaadin.flow.data.provider.Query;
import com.vaadin.flow.server.VaadinSession;

import java.util.ArrayList;
import java.util.Collection;
import java.util.stream.Stream;

import org.dellroad.stuff.vaadin24.util.AsyncTaskManager;
import org.dellroad.stuff.vaadin24.util.VaadinUtil;

/**
 * A {@link ListDataProvider} whose contents are gathered in an asynchronous query.
 *
 * 

* Instances are just a {@link ListDataProvider} with support for asynchronous (re)loading of the underlying data * in a background thread using an {@link AsyncTaskManager}. As a result, the UI never "locks up" while a backend * data query executes. * *

* The associated {@link AsyncTaskManager} provides status change notifications when a load operation starts, * completes, fails, or is canceled. These updates can be used to drive GUI loading spinners, etc. * *

* Load operations are initiated via {@link #load load()}, and may be canceled in progress via {@link #cancel}. * Initiating a new load operation will cancel and replace any previous load operation still in progress. * *

* All operations are atomic and race free. See {@link AsyncTaskManager} for details. * *

Queries * *

* The {@link DataProvider}'s {@link #fetch fetch()} and {@link #size size()} methods take a {@link Query} parameter, * which includes basic filtering, sorting, and offset/limit information for the query. However, because these methods * are synchronous, they not compatible with this class' aynchronous loading model. That's why this class extends * {@link ListDataProvider}, which applies the {@link Query} to a list of objects already loaded into memory by other means. * The purpose of this class is therefore simply to refresh that list, aynchronously, {@linkplain #load when requested}. * *

* However, there may be scenarios where the aynchronous load operation can benefit from knowing the {@link Query} currently * being used, i.e., most recently passed to {@link #fetch fetch()} or {@link #size size()}. For such scenarios, subclasses * could override these methods and trigger a new load operation if the {@link Query} has changed, etc. */ @SuppressWarnings("serial") public class AsyncDataProvider extends ListDataProvider { private final AsyncTaskManager> taskManager; // Constructors /** * Default constructor. * *

* Caller still must configure an async executor via * {@link #getAsyncTaskManager getAsyncTaskManager}{@code ().}{@link AsyncTaskManager#setAsyncExecutor setAsyncExecutor()}. * * @throws IllegalStateException if there is no {@link VaadinSession} associated with the current thread */ public AsyncDataProvider() { this(new AsyncTaskManager<>()); } /** * Constructor. * * @param taskManager loading task manager * @throws IllegalStateException if there is no {@link VaadinSession} associated with the current thread */ public AsyncDataProvider(AsyncTaskManager> taskManager) { super(new ArrayList<>()); Preconditions.checkArgument(taskManager != null, "null taskManager"); this.taskManager = taskManager; this.taskManager.setResultConsumer(this::updateFromLoad); } // Public Methods /** * Get the {@link AsyncTaskManager} that manages background load operations. * * @return this instance's {@link AsyncTaskManager}, never null */ public AsyncTaskManager> getAsyncTaskManager() { return this.taskManager; } /** * Trigger a new asynchronous load of this instance. * *

* If/when the given load task completes successfully, its results will completely replace the contents * of this data provider. * *

* See {@link Loader#load Loader} for important requirements for the behavior of {@code loader}. * * @param loader performs the load operation * @return unique ID for this load attempt * @throws IllegalStateException if the current thread is not associated with the task manager's session * @throws IllegalStateException if this instance's {@link AsyncTaskManager} has no executor configured * @throws IllegalArgumentException if {@code loader} is null * @see AsyncTaskManager#startTask */ public long load(Loader loader) { Preconditions.checkArgument(loader != null, "null loader"); return this.taskManager.startTask(id -> { final Stream result = loader.load(id); if (result == null) throw new IllegalArgumentException("loader returned null Stream"); return result; }); } /** * Cancel any outstanding asynchronous load of this instance. * * @return the unique ID of the canceled task, if any, or zero if there is no task outstanding * @throws IllegalStateException if the current thread is not associated with the task manager's session * @see AsyncTaskManager#cancelTask */ public long cancel() { return this.taskManager.cancelTask(); } // Internal Methods /** * Update this data provider's internal list of items using the new data from a successful load operation * and fire a refresh notification. * *

* This is invoked with the task manager's session locked. * * @param id task ID * @param stream load results * @throws IllegalStateException if the current thread is not associated with the task manager's session * @throws IllegalArgumentException if {@code stream} is null */ protected void updateFromLoad(long id, final Stream stream) { VaadinUtil.assertCurrentSession(this.taskManager.getVaadinSession()); final Collection items = this.getItems(); items.clear(); stream.forEach(items::add); this.refreshAll(); } // Loader /** * Callback interface for asynchronous loading of data for a {@link AsyncDataProvider}. * * @param data provider model type */ @FunctionalInterface public interface Loader { /** * Perform a load operation. * *

* This method runs in a background thread; in particular, no {@link VaadinSession} will be locked. * *

* Note: This method returns a {@link Stream} but the {@link Stream} is not actually consumed in the * background thread; that operation occurs later, in a different thread with the {@link VaadinSession} locked. * In cases where this matters (e.g., the data is gathered in a per-thread transaction), this method * may need to proactively pull the data into memory ahead of time. * *

* This method should be prepared to handle an {@linkplain Thread#interrupt interrupt} if/when * {@link AsyncDataProvider#cancel} is invoked; in that case this method may throw {@link InterruptedException}. * This method may also cancel itself by throwing an unprompted {@link InterruptedException}. * *

* If this method returns null, the load fails with an {@link IllegalArgumentException}. * * @param id unique ID for this load attempt * @return stream of data items * @throws InterruptedException if the current thread is interrupted * @throws RuntimeException if an error occurs during loading */ Stream load(long id) throws InterruptedException; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy