From 2e1d2de4e6ca22533874109ce6abd90ff7d52155 Mon Sep 17 00:00:00 2001 From: Javier Godoy <11554739+javier-godoy@users.noreply.github.com> Date: Mon, 10 Jun 2024 16:16:02 -0300 Subject: [PATCH 1/4] feat: support cancelling queued downloads if the UI is closed Close #127 --- .../ConcurrentStreamResourceWriter.java | 71 +++++++++++++------ .../addons/gridexporter/GridExporter.java | 58 +++++++++------ ...gurableConcurrentStreamResourceWriter.java | 11 +++ .../VaadinServiceInitListenerImpl.java | 2 + 4 files changed, 101 insertions(+), 41 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 4bfd1ff..76b7574 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentStreamResourceWriter.java +++ b/src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentStreamResourceWriter.java @@ -1,11 +1,13 @@ package com.flowingcode.vaadin.addons.gridexporter; +import com.vaadin.flow.component.UI; import com.vaadin.flow.server.StreamResourceWriter; import com.vaadin.flow.server.VaadinSession; import java.io.IOException; import java.io.InterruptedIOException; import java.io.OutputStream; import java.nio.channels.InterruptedByTimeoutException; +import java.util.Optional; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.function.IntFunction; @@ -30,6 +32,8 @@ abstract class ConcurrentStreamResourceWriter implements StreamResourceWriter { private static volatile boolean enabled; + private static volatile boolean failOnUiChange; + private final StreamResourceWriter delegate; private static final class ConfigurableSemaphore extends Semaphore { @@ -89,6 +93,10 @@ public static void setLimit(float limit) { } } + static void setFailOnUiChange(boolean failOnUiChange) { + ConcurrentStreamResourceWriter.failOnUiChange = failOnUiChange; + } + /** * Returns the limit for the number of concurrent downloads. * @@ -147,6 +155,22 @@ public float getCost(VaadinSession session) { return DEFAULT_COST; } + /** + * Returns the UI associated with the current download. + *
+ * This method is used to ensure that the UI is still attached to the current session when a + * download is initiated. Implementations should return the appropriate UI instance. + *
+ * + * @return the {@link UI} instance associated with the current download, or {@code null} if no UI + * is available. + */ + protected abstract UI getUI(); + + private UI getAttachedUI() { + return Optional.ofNullable(getUI()).filter(UI::isAttached).orElse(null); + } + /** * Callback method that is invoked when a timeout occurs while trying to acquire a permit for * starting a download. @@ -207,36 +231,41 @@ public float getCost(VaadinSession session) { public final void accept(OutputStream stream, VaadinSession session) throws IOException { onAccept(); try { - if (!enabled) { - delegate.accept(stream, session); - } else { + if (!enabled) { + delegate.accept(stream, session); + } else { - try { + try { + int permits; + float cost = getCost(session); + synchronized (semaphore) { + permits = costToPermits(cost, semaphore.maxPermits); + } - int permits; - float cost = getCost(session); - synchronized (semaphore) { - permits = costToPermits(cost, semaphore.maxPermits); - } + UI ui = failOnUiChange ? getAttachedUI() : null; - if (semaphore.tryAcquire(permits, getTimeout(), TimeUnit.NANOSECONDS)) { - try { - delegate.accept(stream, session); - } finally { - semaphore.release(permits); + if (semaphore.tryAcquire(permits, getTimeout(), TimeUnit.NANOSECONDS)) { + try { + if (ui != null && getAttachedUI()!=ui) { + // The UI has changed or was detached after acquirig the semaphore + throw new IOException("Detached UI"); } - } else { - onTimeout(); - throw new InterruptedByTimeoutException(); + delegate.accept(stream, session); + } finally { + semaphore.release(permits); } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw (IOException) new InterruptedIOException().initCause(e); + } else { + onTimeout(); + throw new InterruptedByTimeoutException(); } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw (IOException) new InterruptedIOException().initCause(e); } + } } finally { onFinish(); - } + } } } \ No newline at end of file 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 1650c78..3604d14 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java +++ b/src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java @@ -24,6 +24,7 @@ import com.vaadin.flow.component.Component; import com.vaadin.flow.component.ComponentUtil; import com.vaadin.flow.component.HasEnabled; +import com.vaadin.flow.component.UI; import com.vaadin.flow.component.grid.ColumnPathRenderer; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.grid.Grid.Column; @@ -41,6 +42,7 @@ import com.vaadin.flow.server.StreamResourceWriter; import com.vaadin.flow.server.VaadinSession; import com.vaadin.flow.shared.Registration; +import java.io.IOException; import java.io.Serializable; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; @@ -354,27 +356,32 @@ private class GridExporterConcurrentStreamResourceWriter extends ConcurrentStrea private Component button; - @Override - public float getCost(VaadinSession session) { - return concurrentDownloadCost; - } + @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 + 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 UI getUI() { + return grid.getUI().orElse(null); + } + + @Override + protected void onTimeout() { + fireConcurrentDownloadTimeout(); + } @Override protected void onAccept() { @@ -386,7 +393,7 @@ protected void onAccept() { @Override protected void onFinish() { setButtonEnabled(true); - } + } private void setButtonEnabled(boolean enabled) { if (button instanceof HasEnabled) { @@ -474,6 +481,17 @@ public static float getConcurrentDownloadLimit() { return ConcurrentStreamResourceWriter.getLimit(); } + /** + * Configures the behavior of the stream operation when the UI changes during execution. + * + * @param failOnUiChange If {@code true}, the operation will throw an {@link IOException} if the + * UI changes (e.g., becomes detached) after acquiring the semaphore. If {@code false}, the + * operation will proceed regardless of any UI changes. + */ + public static void setFailOnUiChange(boolean failOnUiChange) { + ConcurrentStreamResourceWriter.setFailOnUiChange(failOnUiChange); + } + /** * Sets the timeout for acquiring a permit to start a download when the * {@linkplain #setConcurrentDownloadLimit(int) maximum number of concurrent downloads} is diff --git a/src/test/java/com/flowingcode/vaadin/addons/gridexporter/ConfigurableConcurrentStreamResourceWriter.java b/src/test/java/com/flowingcode/vaadin/addons/gridexporter/ConfigurableConcurrentStreamResourceWriter.java index 8a3d685..0b32a94 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/gridexporter/ConfigurableConcurrentStreamResourceWriter.java +++ b/src/test/java/com/flowingcode/vaadin/addons/gridexporter/ConfigurableConcurrentStreamResourceWriter.java @@ -1,5 +1,6 @@ package com.flowingcode.vaadin.addons.gridexporter; +import com.vaadin.flow.component.UI; import com.vaadin.flow.server.StreamResourceWriter; import com.vaadin.flow.server.VaadinSession; @@ -13,6 +14,7 @@ public ConfigurableConcurrentStreamResourceWriter(StreamResourceWriter delegate) private float cost = GridExporter.DEFAULT_COST; private long timeout = 0L; + private UI ui; @Override public float getCost(VaadinSession session) { @@ -32,4 +34,13 @@ public void setTimeout(long timeout) { this.timeout = timeout; } + @Override + public UI getUI() { + return ui; + } + + public void setUi(UI ui) { + this.ui = ui; + } + } diff --git a/src/test/java/com/flowingcode/vaadin/addons/gridexporter/VaadinServiceInitListenerImpl.java b/src/test/java/com/flowingcode/vaadin/addons/gridexporter/VaadinServiceInitListenerImpl.java index 5f959a2..7be7e88 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/gridexporter/VaadinServiceInitListenerImpl.java +++ b/src/test/java/com/flowingcode/vaadin/addons/gridexporter/VaadinServiceInitListenerImpl.java @@ -20,6 +20,8 @@ public void serviceInit(ServiceInitEvent event) { }); // end-block + GridExporter.setFailOnUiChange(true); + } } From 7eba81a59e3f962e5a6845c3f1f6b89da1723bad Mon Sep 17 00:00:00 2001 From: Javier Godoy <11554739+javier-godoy@users.noreply.github.com> Date: Wed, 11 Sep 2024 09:45:43 -0300 Subject: [PATCH 2/4] docs: fix javadocs --- .../vaadin/addons/gridexporter/GridExporter.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) 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 3604d14..9efb925 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java +++ b/src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java @@ -494,7 +494,7 @@ public static void setFailOnUiChange(boolean failOnUiChange) { /** * Sets the timeout for acquiring a permit to start a download when the - * {@linkplain #setConcurrentDownloadLimit(int) maximum number of concurrent downloads} is + * {@linkplain #setConcurrentDownloadLimit(float) maximum number of concurrent downloads} is * reached. If the timeout is less than or equal to zero, the downloads will fail immediately if * no enough permits can be acquired. * @@ -687,7 +687,7 @@ public void setExportValue(Column