Skip to content

Commit 1d18c52

Browse files
committed
feat: disable button while file is being downloaded
1 parent 4cdf668 commit 1d18c52

File tree

3 files changed

+123
-34
lines changed

3 files changed

+123
-34
lines changed

src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentStreamResourceWriter.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,36 @@ public float getCost(VaadinSession session) {
160160
*/
161161
protected abstract void onTimeout();
162162

163+
/**
164+
* Callback method that is invoked when a download is accepted.
165+
* <p>
166+
* This method is called at the start of the download process, right after the
167+
* {@link #accept(OutputStream, VaadinSession) accept} method is invoked and it has been
168+
* determined that the download can proceed. Subclasses should implement this method to perform
169+
* any necessary actions before the download begins, such as initializing resources, logging, or
170+
* updating the UI to reflect the start of the download.
171+
* <p>
172+
* Note that this method is called before any semaphore permits are acquired, so it is executed
173+
* regardless of whether the semaphore is enabled or not.
174+
* </p>
175+
*/
176+
protected abstract void onAccept();
177+
178+
/**
179+
* Callback method that is invoked when a download finishes.
180+
* <p>
181+
* This method is called at the end of the download process, right before the
182+
* {@link #accept(OutputStream, VaadinSession) accept} method returns, regardless of whether the
183+
* download was successful, timed out, or encountered an error. Subclasses should implement this
184+
* method to perform any necessary actions after the download completes, such as releasing
185+
* resources, logging, or updating the UI to reflect the completion of the download.
186+
* <p>
187+
* Note that this method is always called, even if an exception is thrown during the download
188+
* process, ensuring that any necessary cleanup can be performed.
189+
* </p>
190+
*/
191+
protected abstract void onFinish();
192+
163193
/**
164194
* Handles {@code stream} (writes data to it) using {@code session} as a context.
165195
* <p>
@@ -183,6 +213,7 @@ public final void accept(OutputStream stream, VaadinSession session) throws IOEx
183213
}
184214

185215
try {
216+
onAccept();
186217
if (!enabled) {
187218
delegate.accept(stream, session);
188219
} else {
@@ -212,6 +243,7 @@ public final void accept(OutputStream stream, VaadinSession session) throws IOEx
212243
}
213244
} finally {
214245
downloading.set(false);
246+
onFinish();
215247
}
216248
}
217249

src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java

Lines changed: 79 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121

2222
import com.flowingcode.vaadin.addons.fontawesome.FontAwesome;
2323
import com.flowingcode.vaadin.addons.gridhelpers.GridHelper;
24+
import com.vaadin.flow.component.Component;
2425
import com.vaadin.flow.component.ComponentUtil;
26+
import com.vaadin.flow.component.HasEnabled;
2527
import com.vaadin.flow.component.grid.ColumnPathRenderer;
2628
import com.vaadin.flow.component.grid.Grid;
2729
import com.vaadin.flow.component.grid.Grid.Column;
@@ -50,6 +52,7 @@
5052
import java.util.HashMap;
5153
import java.util.List;
5254
import java.util.Map;
55+
import java.util.Objects;
5356
import java.util.Optional;
5457
import java.util.concurrent.CopyOnWriteArrayList;
5558
import java.util.concurrent.TimeUnit;
@@ -148,21 +151,25 @@ public static <T> GridExporter<T> createFor(
148151
if (exporter.autoAttachExportButtons) {
149152
if (exporter.isExcelExportEnabled()) {
150153
Anchor excelLink = new Anchor("", FontAwesome.Regular.FILE_EXCEL.create());
151-
excelLink.setHref(exporter.getExcelStreamResource(excelCustomTemplate));
154+
excelLink
155+
.setHref(exporter.getExcelStreamResource(excelCustomTemplate)
156+
.forComponent(excelLink));
152157
excelLink.getElement().setAttribute("download", true);
153158
footerToolbar.add(
154159
new FooterToolbarItem(excelLink, FooterToolbarItemPosition.EXPORT_BUTTON));
155160
}
156161
if (exporter.isDocxExportEnabled()) {
157162
Anchor docLink = new Anchor("", FontAwesome.Regular.FILE_WORD.create());
158-
docLink.setHref(exporter.getDocxStreamResource(docxCustomTemplate));
163+
docLink.setHref(
164+
exporter.getDocxStreamResource(docxCustomTemplate).forComponent(docLink));
159165
docLink.getElement().setAttribute("download", true);
160166
footerToolbar
161167
.add(new FooterToolbarItem(docLink, FooterToolbarItemPosition.EXPORT_BUTTON));
162168
}
163169
if (exporter.isPdfExportEnabled()) {
164170
Anchor docLink = new Anchor("", FontAwesome.Regular.FILE_PDF.create());
165-
docLink.setHref(exporter.getPdfStreamResource(docxCustomTemplate));
171+
docLink.setHref(
172+
exporter.getPdfStreamResource(docxCustomTemplate).forComponent(docLink));
166173
docLink.getElement().setAttribute("download", true);
167174
footerToolbar
168175
.add(new FooterToolbarItem(docLink, FooterToolbarItemPosition.EXPORT_BUTTON));
@@ -286,63 +293,101 @@ else if (r.getValueProviders().containsKey("name")) {
286293
return value;
287294
}
288295

289-
public StreamResource getDocxStreamResource() {
296+
public GridExporterStreamResource getDocxStreamResource() {
290297
return getDocxStreamResource(null);
291298
}
292299

293-
public StreamResource getDocxStreamResource(String template) {
294-
return new StreamResource(fileName + ".docx",
300+
public GridExporterStreamResource getDocxStreamResource(String template) {
301+
return new GridExporterStreamResource(fileName + ".docx",
295302
makeConcurrentWriter(new DocxStreamResourceWriter<>(this, template)));
296303
}
297304

298-
public StreamResource getPdfStreamResource() {
305+
public GridExporterStreamResource getPdfStreamResource() {
299306
return getPdfStreamResource(null);
300307
}
301308

302-
public StreamResource getPdfStreamResource(String template) {
303-
return new StreamResource(fileName + ".pdf",
309+
public GridExporterStreamResource getPdfStreamResource(String template) {
310+
return new GridExporterStreamResource(fileName + ".pdf",
304311
makeConcurrentWriter(new PdfStreamResourceWriter<>(this, template)));
305312
}
306313

307314
public StreamResource getCsvStreamResource() {
308315
return new StreamResource(fileName + ".csv", new CsvStreamResourceWriter<>(this));
309316
}
310317

311-
public StreamResource getExcelStreamResource() {
318+
public GridExporterStreamResource getExcelStreamResource() {
312319
return getExcelStreamResource(null);
313320
}
314321

315-
public StreamResource getExcelStreamResource(String template) {
316-
return new StreamResource(fileName + ".xlsx",
322+
public GridExporterStreamResource getExcelStreamResource(String template) {
323+
return new GridExporterStreamResource(fileName + ".xlsx",
317324
makeConcurrentWriter(new ExcelStreamResourceWriter<>(this, template)));
318325
}
319326

320-
private StreamResourceWriter makeConcurrentWriter(StreamResourceWriter writer) {
321-
return new ConcurrentStreamResourceWriter(writer) {
322-
@Override
323-
public float getCost(VaadinSession session) {
324-
return concurrentDownloadCost;
325-
}
327+
private GridExporterConcurrentStreamResourceWriter makeConcurrentWriter(
328+
StreamResourceWriter writer) {
329+
return new GridExporterConcurrentStreamResourceWriter(writer);
330+
}
326331

327-
@Override
328-
public long getTimeout() {
329-
// It would have been possible to specify a different timeout for each instance but I cannot
330-
// figure out a good use case for that. The timeout returned herebecomes relevant when the
331-
// semaphore has been acquired by any other download, so the timeout must reflect how long
332-
// it is reasonable to wait for "any other download" to complete and release the semaphore.
333-
//
334-
// Since the reasonable timeout would depend on the duration of "any other download", it
335-
// makes sense that it's a global setting instead of a per-instance setting.
336-
return concurrentDownloadTimeoutNanos;
337-
}
332+
public class GridExporterStreamResource extends StreamResource {
333+
private final GridExporterConcurrentStreamResourceWriter writer;
338334

339-
@Override
340-
protected void onTimeout() {
341-
fireConcurrentDownloadTimeout();
342-
}
335+
GridExporterStreamResource(String name, GridExporterConcurrentStreamResourceWriter writer) {
336+
super(name, writer);
337+
this.writer = Objects.requireNonNull(writer);
338+
}
339+
340+
public GridExporterStreamResource forComponent(Component component) {
341+
writer.button = component;
342+
return this;
343+
}
344+
}
345+
346+
private class GridExporterConcurrentStreamResourceWriter extends ConcurrentStreamResourceWriter {
343347

348+
GridExporterConcurrentStreamResourceWriter(StreamResourceWriter delegate) {
349+
super(delegate);
350+
}
351+
352+
private Component button;
344353

345-
};
354+
@Override
355+
public float getCost(VaadinSession session) {
356+
return concurrentDownloadCost;
357+
}
358+
359+
@Override
360+
public long getTimeout() {
361+
// It would have been possible to specify a different timeout for each instance but I cannot
362+
// figure out a good use case for that. The timeout returned herebecomes relevant when the
363+
// semaphore has been acquired by any other download, so the timeout must reflect how long
364+
// it is reasonable to wait for "any other download" to complete and release the semaphore.
365+
//
366+
// Since the reasonable timeout would depend on the duration of "any other download", it
367+
// makes sense that it's a global setting instead of a per-instance setting.
368+
return concurrentDownloadTimeoutNanos;
369+
}
370+
371+
@Override
372+
protected void onTimeout() {
373+
fireConcurrentDownloadTimeout();
374+
}
375+
376+
@Override
377+
protected void onAccept() {
378+
setButtonEnabled(false);
379+
}
380+
381+
@Override
382+
protected void onFinish() {
383+
setButtonEnabled(true);
384+
}
385+
386+
private void setButtonEnabled(boolean enabled) {
387+
if (button instanceof HasEnabled) {
388+
grid.getUI().ifPresent(ui -> ui.access(() -> ((HasEnabled) button).setEnabled(enabled)));
389+
}
390+
}
346391
}
347392

348393
/**

src/test/java/com/flowingcode/vaadin/addons/gridexporter/test/ConcurrentExportTests.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ private class ConcurrentStreamResourceWriter
3838
extends ConfigurableConcurrentStreamResourceWriter {
3939

4040
private boolean interruptedByTimeout;
41+
private boolean accepted;
42+
private boolean finished;
4143

4244
public ConcurrentStreamResourceWriter(StreamResourceWriter delegate) {
4345
super(delegate);
@@ -48,6 +50,16 @@ protected void onTimeout() {
4850
interruptedByTimeout = true;
4951
}
5052

53+
@Override
54+
protected void onAccept() {
55+
accepted = true;
56+
}
57+
58+
@Override
59+
protected void onFinish() {
60+
finished = true;
61+
}
62+
5163
}
5264

5365
private CyclicBarrier barrier;

0 commit comments

Comments
 (0)