35
35
import com .vaadin .flow .function .SerializableSupplier ;
36
36
import com .vaadin .flow .function .ValueProvider ;
37
37
import com .vaadin .flow .server .StreamResource ;
38
+ import com .vaadin .flow .server .StreamResourceWriter ;
39
+ import com .vaadin .flow .server .VaadinSession ;
38
40
import java .io .Serializable ;
39
41
import java .lang .reflect .Field ;
40
42
import java .lang .reflect .InvocationTargetException ;
47
49
import java .util .List ;
48
50
import java .util .Map ;
49
51
import java .util .Optional ;
52
+ import java .util .concurrent .TimeUnit ;
50
53
import java .util .stream .Collectors ;
51
54
import org .slf4j .Logger ;
52
55
import org .slf4j .LoggerFactory ;
@@ -62,6 +65,18 @@ public class GridExporter<T> implements Serializable {
62
65
private boolean csvExportEnabled = true ;
63
66
private boolean autoSizeColumns = true ;
64
67
68
+ /** Represents all the permits available to the semaphore. */
69
+ public static final float MAX_COST = ConcurrentStreamResourceWriter .MAX_COST ;
70
+
71
+ /** A fractional cost that acquires only one permit. */
72
+ public static final float MIN_COST = ConcurrentStreamResourceWriter .MIN_COST ;
73
+
74
+ /** The standard unit of resource usage for concurrent downloads. */
75
+ public static final float DEFAULT_COST = 1.0f ;
76
+
77
+ private static long concurrentDownloadTimeoutNanos = 0L ;
78
+ private float concurrentDownloadCost = DEFAULT_COST ;
79
+
65
80
static final String COLUMN_VALUE_PROVIDER_DATA = "column-value-provider-data" ;
66
81
static final String COLUMN_EXPORTED_PROVIDER_DATA = "column-value-exported-data" ;
67
82
static final String COLUMN_PARSING_FORMAT_PATTERN_DATA = "column-parsing-format-pattern-data" ;
@@ -268,15 +283,17 @@ public StreamResource getDocxStreamResource() {
268
283
}
269
284
270
285
public StreamResource getDocxStreamResource (String template ) {
271
- return new StreamResource (fileName + ".docx" , new DocxStreamResourceWriter <>(this , template ));
286
+ return new StreamResource (fileName + ".docx" ,
287
+ makeConcurrentWriter (new DocxStreamResourceWriter <>(this , template )));
272
288
}
273
289
274
290
public StreamResource getPdfStreamResource () {
275
291
return getPdfStreamResource (null );
276
292
}
277
293
278
294
public StreamResource getPdfStreamResource (String template ) {
279
- return new StreamResource (fileName + ".pdf" , new PdfStreamResourceWriter <>(this , template ));
295
+ return new StreamResource (fileName + ".pdf" ,
296
+ makeConcurrentWriter (new PdfStreamResourceWriter <>(this , template )));
280
297
}
281
298
282
299
public StreamResource getCsvStreamResource () {
@@ -288,7 +305,101 @@ public StreamResource getExcelStreamResource() {
288
305
}
289
306
290
307
public StreamResource getExcelStreamResource (String template ) {
291
- return new StreamResource (fileName + ".xlsx" , new ExcelStreamResourceWriter <>(this , template ));
308
+ return new StreamResource (fileName + ".xlsx" ,
309
+ makeConcurrentWriter (new ExcelStreamResourceWriter <>(this , template )));
310
+ }
311
+
312
+ private StreamResourceWriter makeConcurrentWriter (StreamResourceWriter writer ) {
313
+ return new ConcurrentStreamResourceWriter (writer ) {
314
+ @ Override
315
+ public float getCost (VaadinSession session ) {
316
+ return concurrentDownloadCost ;
317
+ }
318
+
319
+ @ Override
320
+ public long getTimeout () {
321
+ // It would have been possible to specify a different timeout for each instance but I cannot
322
+ // figure out a good use case for that. The timeout returned herebecomes relevant when the
323
+ // semaphore has been acquired by any other download, so the timeout must reflect how long
324
+ // it is reasonable to wait for "any other download" to complete and release the semaphore.
325
+ //
326
+ // Since the reasonable timeout would depend on the duration of "any other download", it
327
+ // makes sense that it's a global setting instead of a per-instance setting.
328
+ return concurrentDownloadTimeoutNanos ;
329
+ }
330
+
331
+ };
332
+ }
333
+
334
+ /**
335
+ * Sets the limit for the {@linkplain #setConcurrentDownloadCost(float) cost of concurrent
336
+ * downloads}. If all the downloads have a cost of {@link #DEFAULT_COST}, the limit represents the
337
+ * number of concurrent downloads that are allowed.
338
+ * <p>
339
+ * Finite limits are capped to {@link #MAX_COST} (32767). If the limit is
340
+ * {@link Float#POSITIVE_INFINITY POSITIVE_INFINITY}, concurrent downloads will not be limited.
341
+ *
342
+ * @param limit the maximum cost of concurrent downloads allowed
343
+ * @throws IllegalArgumentException if the limit is zero or negative.
344
+ */
345
+ public static void setConcurrentDownloadLimit (float limit ) {
346
+ ConcurrentStreamResourceWriter .setLimit (limit );
347
+ }
348
+
349
+ /**
350
+ * Returns the limit for the number of concurrent downloads.
351
+ *
352
+ * @return the limit for the number of concurrent downloads, or {@link Float#POSITIVE_INFINITY} if
353
+ * concurrent downloads are not limited.
354
+ */
355
+ public static float getConcurrentDownloadLimit () {
356
+ return ConcurrentStreamResourceWriter .getLimit ();
357
+ }
358
+
359
+ /**
360
+ * Sets the timeout for acquiring a permit to start a download when the
361
+ * {@linkplain #setConcurrentDownloadLimit(int) maximum number of concurrent downloads} is
362
+ * reached. If the timeout is less than or equal to zero, the downloads will fail immediately if
363
+ * no enough permits can be acquired.
364
+ *
365
+ * This timeout is crucial for preventing the system from hanging indefinitely while waiting for
366
+ * available resources. If the timeout expires before a permit can be acquired, the download is
367
+ * cancelled.
368
+ *
369
+ * @param timeout the maximum time to wait for a permit
370
+ * @param unit the time unit of the {@code timeout} argument
371
+ */
372
+ public static void setConcurrentDownloadTimeout (long timeout , TimeUnit unit ) {
373
+ GridExporter .concurrentDownloadTimeoutNanos = unit .toNanos (timeout );
374
+ }
375
+
376
+ /**
377
+ * Sets the cost for concurrent downloads. This cost is used to determine the number of permits
378
+ * required for downloads to proceed, thereby controlling the concurrency level. At any given
379
+ * time, the sum of the costs of all concurrent downloads will not exceed the limit set by
380
+ * {@link #setConcurrentDownloadLimit(float)}.
381
+ * <p>
382
+ *
383
+ * The cost is represented as a float to allow for more granular control over resource usage. By
384
+ * using a floating-point number, fractional costs can be expressed, providing flexibility in
385
+ * determining the resource consumption for different downloads.
386
+ * <p>
387
+ *
388
+ * The cost is converted to a number of permits by capping it to stay within the limit. A cost of
389
+ * 1.0 ({@link #DEFAULT_COST}) represents a standard unit of resource usage, while a cost of 0.5
390
+ * represents half a unit, and a cost above 1.0 indicates higher than normal resource usage.
391
+ * <p>
392
+ *
393
+ * If the cost is zero or negative, no permits are needed. However, any positive cost, no matter
394
+ * how small, will require at least one permit to prevent downloads with very low costs from
395
+ * bypassing the semaphore. {@link #MIN_COST} represents the minimal fractional cost that acquires
396
+ * only one permit (hence {@code 2*MIN_COST} acquires two permits and so on). A cost of
397
+ * {@link #MAX_COST} prevents any other downloads from acquiring permits simultaneously.
398
+ *
399
+ * @param concurrentDownloadCost the cost associated with concurrent downloads for this instance.
400
+ */
401
+ public void setConcurrentDownloadCost (float concurrentDownloadCost ) {
402
+ this .concurrentDownloadCost = concurrentDownloadCost ;
292
403
}
293
404
294
405
public String getTitle () {
0 commit comments