From 3e40dacd72f7fb6d28759c433b68fca75c66d7d7 Mon Sep 17 00:00:00 2001
From: Javier Godoy <11554739+javier-godoy@users.noreply.github.com>
Date: Mon, 10 Jun 2024 23:27:00 -0300
Subject: [PATCH] feat: disable button while file is being downloaded
Close #126
---
.../ConcurrentStreamResourceWriter.java | 80 +++++++----
.../addons/gridexporter/GridExporter.java | 130 +++++++++++++-----
.../GridExporterBigDatasetDemo.java | 3 +
.../test/ConcurrentExportTests.java | 12 ++
4 files changed, 169 insertions(+), 56 deletions(-)
diff --git a/src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentStreamResourceWriter.java b/src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentStreamResourceWriter.java
index 88bcc69..4bfd1ff 100644
--- a/src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentStreamResourceWriter.java
+++ b/src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentStreamResourceWriter.java
@@ -157,6 +157,36 @@ public float getCost(VaadinSession session) {
*/
protected abstract void onTimeout();
+ /**
+ * Callback method that is invoked when a download is accepted.
+ *
+ * This method is called at the start of the download process, right after the
+ * {@link #accept(OutputStream, VaadinSession) accept} method is invoked and it has been
+ * determined that the download can proceed. Subclasses should implement this method to perform
+ * any necessary actions before the download begins, such as initializing resources, logging, or
+ * updating the UI to reflect the start of the download.
+ *
+ * Note that this method is called before any semaphore permits are acquired, so it is executed
+ * regardless of whether the semaphore is enabled or not.
+ *
+ */
+ protected abstract void onAccept();
+
+ /**
+ * Callback method that is invoked when a download finishes.
+ *
+ * This method is called at the end of the download process, right before the
+ * {@link #accept(OutputStream, VaadinSession) accept} method returns, regardless of whether the
+ * download was successful, timed out, or encountered an error. Subclasses should implement this
+ * method to perform any necessary actions after the download completes, such as releasing
+ * resources, logging, or updating the UI to reflect the completion of the download.
+ *
+ * Note that this method is always called, even if an exception is thrown during the download
+ * process, ensuring that any necessary cleanup can be performed.
+ *
+ */
+ protected abstract void onFinish();
+
/**
* Handles {@code stream} (writes data to it) using {@code session} as a context.
*
@@ -175,33 +205,37 @@ public float getCost(VaadinSession session) {
*/
@Override
public final void accept(OutputStream stream, VaadinSession session) throws IOException {
+ onAccept();
+ try {
+ if (!enabled) {
+ delegate.accept(stream, session);
+ } else {
+
+ try {
+
+ int permits;
+ float cost = getCost(session);
+ synchronized (semaphore) {
+ permits = costToPermits(cost, semaphore.maxPermits);
+ }
- if (!enabled) {
- delegate.accept(stream, session);
- } else {
-
- try {
-
- int permits;
- float cost = getCost(session);
- synchronized (semaphore) {
- permits = costToPermits(cost, semaphore.maxPermits);
- }
-
- if (semaphore.tryAcquire(permits, getTimeout(), TimeUnit.NANOSECONDS)) {
- try {
- delegate.accept(stream, session);
- } finally {
- semaphore.release(permits);
+ if (semaphore.tryAcquire(permits, getTimeout(), TimeUnit.NANOSECONDS)) {
+ try {
+ delegate.accept(stream, session);
+ } finally {
+ semaphore.release(permits);
+ }
+ } else {
+ onTimeout();
+ throw new InterruptedByTimeoutException();
}
- } else {
- onTimeout();
- throw new InterruptedByTimeoutException();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw (IOException) new InterruptedIOException().initCause(e);
}
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw (IOException) new InterruptedIOException().initCause(e);
}
+ } finally {
+ onFinish();
}
}
diff --git a/src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java b/src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java
index 0bc6829..1650c78 100644
--- a/src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java
+++ b/src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java
@@ -21,7 +21,9 @@
import com.flowingcode.vaadin.addons.fontawesome.FontAwesome;
import com.flowingcode.vaadin.addons.gridhelpers.GridHelper;
+import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.ComponentUtil;
+import com.vaadin.flow.component.HasEnabled;
import com.vaadin.flow.component.grid.ColumnPathRenderer;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.grid.Grid.Column;
@@ -50,6 +52,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
@@ -79,6 +82,9 @@ public class GridExporter implements Serializable {
public static final float DEFAULT_COST = 1.0f;
private static long concurrentDownloadTimeoutNanos = 0L;
+
+ private boolean disableOnClick;
+
private float concurrentDownloadCost = DEFAULT_COST;
private final List> instanceDownloadTimeoutListeners =
new CopyOnWriteArrayList<>();
@@ -148,21 +154,25 @@ public static GridExporter createFor(
if (exporter.autoAttachExportButtons) {
if (exporter.isExcelExportEnabled()) {
Anchor excelLink = new Anchor("", FontAwesome.Regular.FILE_EXCEL.create());
- excelLink.setHref(exporter.getExcelStreamResource(excelCustomTemplate));
+ excelLink
+ .setHref(exporter.getExcelStreamResource(excelCustomTemplate)
+ .forComponent(excelLink));
excelLink.getElement().setAttribute("download", true);
footerToolbar.add(
new FooterToolbarItem(excelLink, FooterToolbarItemPosition.EXPORT_BUTTON));
}
if (exporter.isDocxExportEnabled()) {
Anchor docLink = new Anchor("", FontAwesome.Regular.FILE_WORD.create());
- docLink.setHref(exporter.getDocxStreamResource(docxCustomTemplate));
+ docLink.setHref(
+ exporter.getDocxStreamResource(docxCustomTemplate).forComponent(docLink));
docLink.getElement().setAttribute("download", true);
footerToolbar
.add(new FooterToolbarItem(docLink, FooterToolbarItemPosition.EXPORT_BUTTON));
}
if (exporter.isPdfExportEnabled()) {
Anchor docLink = new Anchor("", FontAwesome.Regular.FILE_PDF.create());
- docLink.setHref(exporter.getPdfStreamResource(docxCustomTemplate));
+ docLink.setHref(
+ exporter.getPdfStreamResource(docxCustomTemplate).forComponent(docLink));
docLink.getElement().setAttribute("download", true);
footerToolbar
.add(new FooterToolbarItem(docLink, FooterToolbarItemPosition.EXPORT_BUTTON));
@@ -286,21 +296,21 @@ else if (r.getValueProviders().containsKey("name")) {
return value;
}
- public StreamResource getDocxStreamResource() {
+ public GridExporterStreamResource getDocxStreamResource() {
return getDocxStreamResource(null);
}
- public StreamResource getDocxStreamResource(String template) {
- return new StreamResource(fileName + ".docx",
+ public GridExporterStreamResource getDocxStreamResource(String template) {
+ return new GridExporterStreamResource(fileName + ".docx",
makeConcurrentWriter(new DocxStreamResourceWriter<>(this, template)));
}
- public StreamResource getPdfStreamResource() {
+ public GridExporterStreamResource getPdfStreamResource() {
return getPdfStreamResource(null);
}
- public StreamResource getPdfStreamResource(String template) {
- return new StreamResource(fileName + ".pdf",
+ public GridExporterStreamResource getPdfStreamResource(String template) {
+ return new GridExporterStreamResource(fileName + ".pdf",
makeConcurrentWriter(new PdfStreamResourceWriter<>(this, template)));
}
@@ -308,41 +318,81 @@ public StreamResource getCsvStreamResource() {
return new StreamResource(fileName + ".csv", new CsvStreamResourceWriter<>(this));
}
- public StreamResource getExcelStreamResource() {
+ public GridExporterStreamResource getExcelStreamResource() {
return getExcelStreamResource(null);
}
- public StreamResource getExcelStreamResource(String template) {
- return new StreamResource(fileName + ".xlsx",
+ public GridExporterStreamResource getExcelStreamResource(String template) {
+ return new GridExporterStreamResource(fileName + ".xlsx",
makeConcurrentWriter(new ExcelStreamResourceWriter<>(this, template)));
}
- private StreamResourceWriter makeConcurrentWriter(StreamResourceWriter writer) {
- return new ConcurrentStreamResourceWriter(writer) {
- @Override
- public float getCost(VaadinSession session) {
- return concurrentDownloadCost;
- }
+ private GridExporterConcurrentStreamResourceWriter makeConcurrentWriter(
+ StreamResourceWriter writer) {
+ return new GridExporterConcurrentStreamResourceWriter(writer);
+ }
- @Override
- public long getTimeout() {
- // It would have been possible to specify a different timeout for each instance but I cannot
- // figure out a good use case for that. The timeout returned herebecomes relevant when the
- // semaphore has been acquired by any other download, so the timeout must reflect how long
- // it is reasonable to wait for "any other download" to complete and release the semaphore.
- //
- // Since the reasonable timeout would depend on the duration of "any other download", it
- // makes sense that it's a global setting instead of a per-instance setting.
- return concurrentDownloadTimeoutNanos;
- }
+ public class GridExporterStreamResource extends StreamResource {
+ private final GridExporterConcurrentStreamResourceWriter writer;
+
+ GridExporterStreamResource(String name, GridExporterConcurrentStreamResourceWriter writer) {
+ super(name, writer);
+ this.writer = Objects.requireNonNull(writer);
+ }
+
+ public GridExporterStreamResource forComponent(Component component) {
+ writer.button = component;
+ return this;
+ }
+ }
+
+ private class GridExporterConcurrentStreamResourceWriter extends ConcurrentStreamResourceWriter {
+
+ GridExporterConcurrentStreamResourceWriter(StreamResourceWriter delegate) {
+ super(delegate);
+ }
+
+ private Component button;
+
+ @Override
+ public float getCost(VaadinSession session) {
+ return concurrentDownloadCost;
+ }
+
+ @Override
+ public long getTimeout() {
+ // It would have been possible to specify a different timeout for each instance but I cannot
+ // figure out a good use case for that. The timeout returned herebecomes relevant when the
+ // semaphore has been acquired by any other download, so the timeout must reflect how long
+ // it is reasonable to wait for "any other download" to complete and release the semaphore.
+ //
+ // Since the reasonable timeout would depend on the duration of "any other download", it
+ // makes sense that it's a global setting instead of a per-instance setting.
+ return concurrentDownloadTimeoutNanos;
+ }
+
+ @Override
+ protected void onTimeout() {
+ fireConcurrentDownloadTimeout();
+ }
- @Override
- protected void onTimeout() {
- fireConcurrentDownloadTimeout();
+ @Override
+ protected void onAccept() {
+ if (disableOnClick) {
+ setButtonEnabled(false);
}
+ }
+ @Override
+ protected void onFinish() {
+ setButtonEnabled(true);
+ }
- };
+ private void setButtonEnabled(boolean enabled) {
+ if (button instanceof HasEnabled) {
+ grid.getUI().ifPresent(ui -> ui.access(() -> ((HasEnabled) button).setEnabled(enabled)));
+ }
+ }
}
/**
@@ -441,6 +491,20 @@ public static void setConcurrentDownloadTimeout(long timeout, TimeUnit unit) {
GridExporter.concurrentDownloadTimeoutNanos = unit.toNanos(timeout);
}
+ /**
+ * Configures the behavior of the system when a download is in progress.
+ *
+ * When {@code disableOnClick} is set to {@code true}, the system prevents the UI from starting an
+ * additional download of the same kind while one is already in progress. Downloads from other UIs
+ * are still allowed. When set to {@code false}, concurrent downloads are permitted.
+ *
+ *
+ * @param disableOnClick Whether to prevent additional downloads during an ongoing download.
+ */
+ public void setDisableOnClick(boolean disableOnClick) {
+ this.disableOnClick = disableOnClick;
+ }
+
/**
* Sets the cost for concurrent downloads. This cost is used to determine the number of permits
* required for downloads to proceed, thereby controlling the concurrency level. At any given
diff --git a/src/test/java/com/flowingcode/vaadin/addons/gridexporter/GridExporterBigDatasetDemo.java b/src/test/java/com/flowingcode/vaadin/addons/gridexporter/GridExporterBigDatasetDemo.java
index 571e024..fd04062 100644
--- a/src/test/java/com/flowingcode/vaadin/addons/gridexporter/GridExporterBigDatasetDemo.java
+++ b/src/test/java/com/flowingcode/vaadin/addons/gridexporter/GridExporterBigDatasetDemo.java
@@ -115,5 +115,8 @@ public GridExporterBigDatasetDemo() throws EncryptedDocumentException, IOExcepti
exporter.setConcurrentDownloadCost(9);
// end-block
+ // Prevents additional downloads from starting while one is already in progress
+ exporter.setDisableOnClick(true);
}
+
}
diff --git a/src/test/java/com/flowingcode/vaadin/addons/gridexporter/test/ConcurrentExportTests.java b/src/test/java/com/flowingcode/vaadin/addons/gridexporter/test/ConcurrentExportTests.java
index 21f69ff..0e442e1 100644
--- a/src/test/java/com/flowingcode/vaadin/addons/gridexporter/test/ConcurrentExportTests.java
+++ b/src/test/java/com/flowingcode/vaadin/addons/gridexporter/test/ConcurrentExportTests.java
@@ -38,6 +38,8 @@ private class ConcurrentStreamResourceWriter
extends ConfigurableConcurrentStreamResourceWriter {
private boolean interruptedByTimeout;
+ private boolean accepted;
+ private boolean finished;
public ConcurrentStreamResourceWriter(StreamResourceWriter delegate) {
super(delegate);
@@ -48,6 +50,16 @@ protected void onTimeout() {
interruptedByTimeout = true;
}
+ @Override
+ protected void onAccept() {
+ accepted = true;
+ }
+
+ @Override
+ protected void onFinish() {
+ finished = true;
+ }
+
}
private CyclicBarrier barrier;