Skip to content

Conversation

@mlopezFC
Copy link
Member

@mlopezFC mlopezFC commented Nov 25, 2025

This PR introduces support for the new DownloadHandler API while maintaining full backward compatibility with existing code. The deprecated StreamResource API methods are preserved but marked for removal in version 3.0.0, allowing users to migrate at their own pace. All export formats (Excel, DOCX, PDF, CSV) now have corresponding DownloadHandler-based methods alongside the existing StreamResource methods.

Closes #179

Summary by CodeRabbit

  • New Features

    • DownloadHandler-based export API for Excel, PDF, DOCX and CSV; export links can use new handlers
    • Concurrency-aware export controls with configurable limits, costs and timeouts
  • Documentation

    • Detailed Migration Guide and README section with examples, compatibility matrix, timelines and troubleshooting
    • Adapter preserves backward compatibility with existing stream-based exports
  • Chores

    • Vaadin bumped to 24.8.0; project version updated to 2.6.0-SNAPSHOT

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Nov 25, 2025

Walkthrough

Adds DownloadHandler-based export APIs, a semaphore-backed concurrency core, a StreamResourceWriter→DownloadHandler adapter, GridExporter download handler methods, version bumps, and documentation (migration guide + README) for migrating from StreamResource to DownloadHandler.

Changes

Cohort / File(s) Summary
Documentation
MIGRATION_GUIDE.md, README.md
Add migration guide and README section describing transition from StreamResource to DownloadHandler, examples, Vaadin requirement, timelines, and compatibility notes.
Build
pom.xml
Bump project version to 2.6.0-SNAPSHOT and vaadin.version to 24.8.0.
Concurrency core
src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentOperationBase.java, src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentDownloadHandler.java
Introduce ConcurrentOperationBase (semaphore/limit, lifecycle hooks, runWithSemaphore) and package-private ConcurrentDownloadHandler that delegates download handling under semaphore control.
Stream writer updates
src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentStreamResourceWriter.java
Change to extend ConcurrentOperationBase; delegate limit/getLimit to base and use runWithSemaphore for delegate writes.
Adapter
src/main/java/com/flowingcode/vaadin/addons/gridexporter/StreamResourceWriterAdapter.java
New adapter to wrap a StreamResourceWriter as a DownloadHandler, applying filename/content-type and delegating write.
Public API (GridExporter)
src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java
Add public getExcelDownloadHandler(...), getPdfDownloadHandler(...), getDocxDownloadHandler(...), getCsvDownloadHandler(); add helper makeConcurrentDownloadHandler(...), private GridExporterConcurrentDownloadHandler, and concurrency strategy interface.
Tests / Demos
src/test/java/com/flowingcode/vaadin/addons/gridexporter/GridExporterCustomLinkDemo.java
Demo updated to use getExcelDownloadHandler() instead of getExcelStreamResource(); minor formatting tweaks.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas needing close review:
    • Semaphore and permit math in ConcurrentOperationBase (costToPermits, setPermits semantics).
    • Timeout, InterruptedException handling, and UI-attachment/failOnUiChange behavior in runWithSemaphore.
    • Integration of ConcurrentDownloadHandler with GridExporter lifecycle hooks and UI/component state updates.
    • Backward-compatibility adapter StreamResourceWriterAdapter and newly exposed public methods on GridExporter.

Possibly related PRs

Suggested reviewers

  • paodb

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.28% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: migration from StreamResource to DownloadHandler API for Vaadin 25 compatibility.
Linked Issues check ✅ Passed The PR successfully addresses issue #179 by introducing DownloadHandler-based methods for all export formats while maintaining backward compatibility with deprecated StreamResource methods.
Out of Scope Changes check ✅ Passed All changes align with the migration objective: new DownloadHandler API methods, supporting infrastructure (ConcurrentDownloadHandler, adapter, base class), version bumps, and documentation.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch issue-179

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
src/main/java/com/flowingcode/vaadin/addons/gridexporter/StreamResourceWriterAdapter.java (1)

50-54: Add null check for writer parameter.

The writer parameter should be validated to prevent potential NPE. While the adapter pattern delegates resource management appropriately, defensive validation improves robustness.

Apply this diff to add validation:

 public StreamResourceWriterAdapter(StreamResourceWriter writer, String filename, String contentType) {
+    this.writer = Objects.requireNonNull(writer, "writer cannot be null");
-    this.writer = writer;
     this.filename = filename;
     this.contentType = contentType;
 }
src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java (1)

617-621: Use pattern matching for instanceof.

Since the project targets Java 17, you can simplify the instanceof check using pattern matching.

Apply this diff:

 private void setButtonEnabled(boolean enabled) {
-  if (button instanceof HasEnabled) {
-    grid.getUI().ifPresent(ui -> ui.access(() -> ((HasEnabled) button).setEnabled(enabled)));
+  if (button instanceof HasEnabled hasEnabled) {
+    grid.getUI().ifPresent(ui -> ui.access(() -> hasEnabled.setEnabled(enabled)));
   }
 }
src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentDownloadHandler.java (1)

28-28: Remove unused import.

The java.io.OutputStream import is not used in this file.

Apply this diff:

 import java.io.IOException;
 import java.io.InterruptedIOException;
-import java.io.OutputStream;
 import java.nio.channels.InterruptedByTimeoutException;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4b82e6d and d9b1b9f.

📒 Files selected for processing (7)
  • MIGRATION_GUIDE.md (1 hunks)
  • README.md (1 hunks)
  • pom.xml (1 hunks)
  • src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentDownloadHandler.java (1 hunks)
  • src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java (5 hunks)
  • src/main/java/com/flowingcode/vaadin/addons/gridexporter/StreamResourceWriterAdapter.java (1 hunks)
  • src/test/java/com/flowingcode/vaadin/addons/gridexporter/GridExporterCustomLinkDemo.java (2 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: javier-godoy
Repo: FlowingCode/GridExporterAddon PR: 119
File: src/main/java/com/flowingcode/vaadin/addons/gridexporter/CsvStreamResourceWriter.java:77-93
Timestamp: 2024-07-27T22:14:37.039Z
Learning: User javier-godoy requested to open a follow-up GitHub issue to discuss resource management in the refactored code of CsvStreamResourceWriter, highlighting the absence of try-with-resources in the previous implementation.
Learnt from: javier-godoy
Repo: FlowingCode/GridExporterAddon PR: 119
File: src/main/java/com/flowingcode/vaadin/addons/gridexporter/CsvStreamResourceWriter.java:77-93
Timestamp: 2024-10-08T21:28:24.972Z
Learning: User javier-godoy requested to open a follow-up GitHub issue to discuss resource management in the refactored code of CsvStreamResourceWriter, highlighting the absence of try-with-resources in the previous implementation.
Learnt from: javier-godoy
Repo: FlowingCode/GridExporterAddon PR: 143
File: src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java:315-315
Timestamp: 2024-10-08T21:28:24.972Z
Learning: In the `GridExporter` class, CSV exports are not subject to concurrent controls, so `getCsvStreamResource` returns a `StreamResource` instead of a `GridExporterStreamResource`.
Learnt from: javier-godoy
Repo: FlowingCode/GridExporterAddon PR: 143
File: src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java:315-315
Timestamp: 2024-09-26T18:26:03.345Z
Learning: In the `GridExporter` class, CSV exports are not subject to concurrent controls, so `getCsvStreamResource` returns a `StreamResource` instead of a `GridExporterStreamResource`.
📚 Learning: 2024-10-08T21:28:24.972Z
Learnt from: javier-godoy
Repo: FlowingCode/GridExporterAddon PR: 143
File: src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java:315-315
Timestamp: 2024-10-08T21:28:24.972Z
Learning: In the `GridExporter` class, CSV exports are not subject to concurrent controls, so `getCsvStreamResource` returns a `StreamResource` instead of a `GridExporterStreamResource`.

Applied to files:

  • MIGRATION_GUIDE.md
  • src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java
  • src/main/java/com/flowingcode/vaadin/addons/gridexporter/StreamResourceWriterAdapter.java
  • src/test/java/com/flowingcode/vaadin/addons/gridexporter/GridExporterCustomLinkDemo.java
📚 Learning: 2024-06-09T06:20:09.702Z
Learnt from: javier-godoy
Repo: FlowingCode/GridExporterAddon PR: 125
File: src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java:338-402
Timestamp: 2024-06-09T06:20:09.702Z
Learning: Zero or negative costs in the `setConcurrentDownloadCost` method do not require any permits, allowing such downloads to bypass the semaphore mechanism.

Applied to files:

  • src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java
  • src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentDownloadHandler.java
📚 Learning: 2024-10-28T17:35:31.077Z
Learnt from: javier-godoy
Repo: FlowingCode/GridExporterAddon PR: 154
File: src/main/java/com/flowingcode/vaadin/addons/gridexporter/BaseStreamResourceWriter.java:98-103
Timestamp: 2024-10-28T17:35:31.077Z
Learning: `BaseStreamResourceWriter` and its extending classes are not public API currently, but the return type must be refactored as we are going to expose `BaseStreamResourceWriter` in the near future.

Applied to files:

  • src/main/java/com/flowingcode/vaadin/addons/gridexporter/StreamResourceWriterAdapter.java
🧬 Code graph analysis (1)
src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java (1)
src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporterConcurrentSettings.java (1)
  • GridExporterConcurrentSettings (37-123)
🪛 GitHub Check: SonarCloud Code Analysis
src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java

[warning] 333-333: Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=FlowingCode_GridExporterAddon&issues=AZq84AEOvK_9d0Mfzo2a&open=AZq84AEOvK_9d0Mfzo2a&pullRequest=189


[warning] 388-388: Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=FlowingCode_GridExporterAddon&issues=AZq84AEOvK_9d0Mfzo2e&open=AZq84AEOvK_9d0Mfzo2e&pullRequest=189


[warning] 347-347: Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=FlowingCode_GridExporterAddon&issues=AZq84AEOvK_9d0Mfzo2b&open=AZq84AEOvK_9d0Mfzo2b&pullRequest=189


[warning] 402-402: Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=FlowingCode_GridExporterAddon&issues=AZq84AEOvK_9d0Mfzo2f&open=AZq84AEOvK_9d0Mfzo2f&pullRequest=189


[warning] 319-319: Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=FlowingCode_GridExporterAddon&issues=AZq84AEOvK_9d0Mfzo2Z&open=AZq84AEOvK_9d0Mfzo2Z&pullRequest=189


[warning] 375-375: Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=FlowingCode_GridExporterAddon&issues=AZq84AEOvK_9d0Mfzo2d&open=AZq84AEOvK_9d0Mfzo2d&pullRequest=189


[warning] 361-361: Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=FlowingCode_GridExporterAddon&issues=AZq84AEOvK_9d0Mfzo2c&open=AZq84AEOvK_9d0Mfzo2c&pullRequest=189


[warning] 618-618: Replace this instanceof check and cast with 'instanceof HasEnabled hasenabled'

See more on https://sonarcloud.io/project/issues?id=FlowingCode_GridExporterAddon&issues=AZq84AEOvK_9d0Mfzo2g&open=AZq84AEOvK_9d0Mfzo2g&pullRequest=189

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

[warning] 28-28: Remove this unused import 'java.io.OutputStream'.

See more on https://sonarcloud.io/project/issues?id=FlowingCode_GridExporterAddon&issues=AZq84AHAvK_9d0Mfzo2h&open=AZq84AHAvK_9d0Mfzo2h&pullRequest=189

🪛 LanguageTool
MIGRATION_GUIDE.md

[style] ~146-~146: Try using a more formal synonym for ‘check’.
Context: ...any issues during migration, please: 1. Check that you're using Vaadin 24.8.0 or late...

(VERIFY)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-vaadin24
🔇 Additional comments (14)
pom.xml (1)

7-7: LGTM!

The version updates correctly reflect the new feature set (DownloadHandler API) and required Vaadin dependency.

Also applies to: 13-13

README.md (1)

57-83: LGTM!

The migration documentation is clear, comprehensive, and provides helpful quick-start guidance for users upgrading to the new API.

src/test/java/com/flowingcode/vaadin/addons/gridexporter/GridExporterCustomLinkDemo.java (1)

79-79: LGTM!

The test correctly demonstrates migration to the new getExcelDownloadHandler() API.

MIGRATION_GUIDE.md (1)

1-189: LGTM!

The migration guide is comprehensive, well-structured, and provides clear before/after examples for all export formats.

src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java (5)

226-232: LGTM!

Formatting improvement with no functional changes.


310-405: LGTM!

The deprecation strategy is well-documented with clear migration paths to the new DownloadHandler-based methods. The removal timeline (version 3.0.0) provides adequate time for users to migrate.


406-489: LGTM!

The new DownloadHandler-based methods are correctly implemented. CSV export appropriately uses StreamResourceWriterAdapter directly without concurrent controls, consistent with the existing design where CSV exports are not throttled. Based on learnings, CSV exports are intentionally not subject to concurrent controls.


496-500: LGTM!

The helper method cleanly composes the adapter and concurrent handler layers.


570-634: LGTM!

The GridExporterConcurrentDownloadHandler correctly implements the concurrency framework with appropriate lifecycle hooks and UI association.

src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentDownloadHandler.java (5)

46-58: LGTM!

The constants and static fields are well-defined for the global concurrency control mechanism.


60-90: LGTM!

The ConfigurableSemaphore correctly manages dynamic permit allocation with proper synchronization.


103-137: LGTM!

The limit management correctly handles enabling/disabling the semaphore and validates input parameters.


139-148: LGTM!

The cost-to-permits conversion correctly implements fixed-point arithmetic for fractional costs and properly handles zero/negative costs to bypass the semaphore. Based on learnings, this aligns with the documented behavior where non-positive costs require no permits.


277-315: LGTM!

The download handling logic correctly implements:

  • Lifecycle callbacks (onAccept/onFinish)
  • Permit acquisition with timeout
  • UI change detection
  • Proper resource cleanup in finally blocks

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentStreamResourceWriter.java (1)

176-185: Fix indentation and spacing consistency.

The if statement on line 178 has inconsistent indentation (appears to be misaligned with the surrounding code) and missing spaces around the != operator.

           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");
-              }
+              if (ui != null && getAttachedUI() != ui) {
+                // The UI has changed or was detached after acquiring the semaphore
+                throw new IOException("Detached UI");
+              }
               delegate.accept(stream, session);
             } finally {
               semaphore.release(permits);

Note: There's also a typo "acquirig" → "acquiring" in the comment.

src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentDownloadHandler.java (1)

56-64: Minor Javadoc inconsistency: "Sets" should be "Returns".

The Javadoc at line 57 says "Sets the timeout" but this is a getter method that returns the timeout value.

     /**
-     * Sets the timeout for acquiring a permit to start a download when there are
+     * Returns the timeout for acquiring a permit to start a download when there are
      * not enough permits
      * available in the semaphore.
      *
      * @see GridExporter#setConcurrentDownloadTimeout(long, TimeUnit)
      * @return the timeout in nanoseconds.
      */
     public abstract long getTimeout();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d9b1b9f and 41a3440.

📒 Files selected for processing (3)
  • src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentDownloadHandler.java (1 hunks)
  • src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentOperationBase.java (1 hunks)
  • src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentStreamResourceWriter.java (2 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: javier-godoy
Repo: FlowingCode/GridExporterAddon PR: 119
File: src/main/java/com/flowingcode/vaadin/addons/gridexporter/CsvStreamResourceWriter.java:77-93
Timestamp: 2024-07-27T22:14:37.039Z
Learning: User javier-godoy requested to open a follow-up GitHub issue to discuss resource management in the refactored code of CsvStreamResourceWriter, highlighting the absence of try-with-resources in the previous implementation.
Learnt from: javier-godoy
Repo: FlowingCode/GridExporterAddon PR: 119
File: src/main/java/com/flowingcode/vaadin/addons/gridexporter/CsvStreamResourceWriter.java:77-93
Timestamp: 2024-10-08T21:28:24.972Z
Learning: User javier-godoy requested to open a follow-up GitHub issue to discuss resource management in the refactored code of CsvStreamResourceWriter, highlighting the absence of try-with-resources in the previous implementation.
Learnt from: javier-godoy
Repo: FlowingCode/GridExporterAddon PR: 143
File: src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java:315-315
Timestamp: 2024-10-08T21:28:24.972Z
Learning: In the `GridExporter` class, CSV exports are not subject to concurrent controls, so `getCsvStreamResource` returns a `StreamResource` instead of a `GridExporterStreamResource`.
Learnt from: javier-godoy
Repo: FlowingCode/GridExporterAddon PR: 143
File: src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java:315-315
Timestamp: 2024-09-26T18:26:03.345Z
Learning: In the `GridExporter` class, CSV exports are not subject to concurrent controls, so `getCsvStreamResource` returns a `StreamResource` instead of a `GridExporterStreamResource`.
📚 Learning: 2024-10-08T21:28:24.972Z
Learnt from: javier-godoy
Repo: FlowingCode/GridExporterAddon PR: 143
File: src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java:315-315
Timestamp: 2024-10-08T21:28:24.972Z
Learning: In the `GridExporter` class, CSV exports are not subject to concurrent controls, so `getCsvStreamResource` returns a `StreamResource` instead of a `GridExporterStreamResource`.

Applied to files:

  • src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentStreamResourceWriter.java
  • src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentOperationBase.java
📚 Learning: 2024-10-28T17:35:31.077Z
Learnt from: javier-godoy
Repo: FlowingCode/GridExporterAddon PR: 154
File: src/main/java/com/flowingcode/vaadin/addons/gridexporter/BaseStreamResourceWriter.java:98-103
Timestamp: 2024-10-28T17:35:31.077Z
Learning: `BaseStreamResourceWriter` and its extending classes are not public API currently, but the return type must be refactored as we are going to expose `BaseStreamResourceWriter` in the near future.

Applied to files:

  • src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentStreamResourceWriter.java
📚 Learning: 2024-06-09T06:20:09.702Z
Learnt from: javier-godoy
Repo: FlowingCode/GridExporterAddon PR: 125
File: src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java:338-402
Timestamp: 2024-06-09T06:20:09.702Z
Learning: Zero or negative costs in the `setConcurrentDownloadCost` method do not require any permits, allowing such downloads to bypass the semaphore mechanism.

Applied to files:

  • src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentStreamResourceWriter.java
  • src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentOperationBase.java
  • src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentDownloadHandler.java
🧬 Code graph analysis (1)
src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentOperationBase.java (2)
src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentDownloadHandler.java (1)
  • SuppressWarnings (40-212)
src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentStreamResourceWriter.java (1)
  • SuppressWarnings (42-200)
🪛 GitHub Check: SonarCloud Code Analysis
src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentOperationBase.java

[warning] 35-35: Add a private constructor to hide the implicit public one.

See more on https://sonarcloud.io/project/issues?id=FlowingCode_GridExporterAddon&issues=AZq9YdoTZqhFnrDkgnFV&open=AZq9YdoTZqhFnrDkgnFV&pullRequest=189

🔇 Additional comments (8)
src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentStreamResourceWriter.java (1)

43-53: LGTM! Clean delegation to the base class.

The refactoring to extend ConcurrentOperationBase and delegate the static setLimit/getLimit methods centralizes the concurrency control logic effectively. This aligns well with the new ConcurrentDownloadHandler that will share the same base.

src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentOperationBase.java (5)

35-44: Abstract class - SonarCloud warning is a false positive.

Since ConcurrentOperationBase is declared abstract, it cannot be instantiated directly, and subclasses will provide their own constructors. The SonarCloud suggestion to add a private constructor doesn't apply here.


113-121: Minor: Potential TOCTOU between enabled check and synchronized read.

The enabled volatile check on line 114 and the synchronized block on lines 115-117 are not atomic. If another thread calls setLimit(POSITIVE_INFINITY) between these, the returned value may be stale. However, since getLimit() is a status/monitoring method and not used for critical decisions, this is acceptable.


45-74: LGTM! Well-designed configurable semaphore.

The ConfigurableSemaphore correctly uses reducePermits for decreasing and release for increasing, maintaining consistency. The toString() provides helpful debugging output with the fixed-point representation.


123-132: LGTM! Cost-to-permits conversion correctly handles edge cases.

The implementation properly handles zero/negative costs (returning 0 permits to bypass the semaphore), caps at MAX_COST, and ensures any positive cost requires at least 1 permit. This aligns with the established behavior where zero or negative costs bypass the semaphore mechanism. Based on learnings, this is the expected behavior.


87-100: LGTM! Proper limit configuration with infinity handling.

The method correctly validates input, handles POSITIVE_INFINITY to disable the semaphore, and uses synchronization when updating permits. The guard against zero/negative values prevents misconfiguration.

src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentDownloadHandler.java (2)

40-54: LGTM! Clean parallel implementation to ConcurrentStreamResourceWriter.

The class structure mirrors ConcurrentStreamResourceWriter appropriately, extending ConcurrentOperationBase and wrapping a delegate DownloadHandler. This provides consistent concurrency control for the new DownloadHandler API.


171-210: LGTM! Correct concurrency-controlled download handling.

The handleDownloadRequest implementation correctly:

  • Invokes onAccept() before acquiring permits
  • Bypasses semaphore when enabled is false
  • Computes permits from cost within synchronized block
  • Checks for UI detachment after permit acquisition (when failOnUiChange is set)
  • Guarantees onFinish() via finally block
  • Properly handles InterruptedException by preserving interrupt status

The implementation is consistent with ConcurrentStreamResourceWriter.accept(), ensuring uniform behavior across both APIs.

@mlopezFC mlopezFC marked this pull request as draft November 26, 2025 11:16
Extract common semaphore logic into ConcurrentOperationBase for reuse in concurrent handlers.
@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
8.6% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentDownloadHandler.java (1)

22-27: Remove unused import.

The VaadinSession import on line 24 is not used in this file. The session is obtained from event.getSession() directly.

 import com.vaadin.flow.server.streams.DownloadHandler;
 import com.vaadin.flow.server.streams.DownloadEvent;
-import com.vaadin.flow.server.VaadinSession;
 import java.io.IOException;
 import java.io.InterruptedIOException;
 import java.nio.channels.InterruptedByTimeoutException;
src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentOperationBase.java (1)

44-50: Consider documenting the volatile fields.

The enabled and failOnUiChange fields are volatile, which provides visibility guarantees but not atomicity for compound operations. The current usage appears safe since enabled is only set within synchronized blocks or checked before synchronized access. However, adding brief comments explaining the concurrency semantics would improve maintainability.

src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java (1)

1042-1079: Consider addressing the wildcard type if feasible.

The GridExporter<?> return type from getExporter() (line 1043) triggers a static analysis warning. While this is a private interface and the impact is limited, the wildcard is used because inner classes reference the enclosing GridExporter<T> which could potentially be typed more specifically.

Since this is internal to the class and the wildcard doesn't escape to public API, this is a minor concern that can be deferred.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 41a3440 and e608b7c.

📒 Files selected for processing (4)
  • src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentDownloadHandler.java (1 hunks)
  • src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentOperationBase.java (1 hunks)
  • src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentStreamResourceWriter.java (2 hunks)
  • src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java (5 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: javier-godoy
Repo: FlowingCode/GridExporterAddon PR: 119
File: src/main/java/com/flowingcode/vaadin/addons/gridexporter/CsvStreamResourceWriter.java:77-93
Timestamp: 2024-10-08T21:28:24.972Z
Learning: User javier-godoy requested to open a follow-up GitHub issue to discuss resource management in the refactored code of CsvStreamResourceWriter, highlighting the absence of try-with-resources in the previous implementation.
Learnt from: javier-godoy
Repo: FlowingCode/GridExporterAddon PR: 119
File: src/main/java/com/flowingcode/vaadin/addons/gridexporter/CsvStreamResourceWriter.java:77-93
Timestamp: 2024-07-27T22:14:37.039Z
Learning: User javier-godoy requested to open a follow-up GitHub issue to discuss resource management in the refactored code of CsvStreamResourceWriter, highlighting the absence of try-with-resources in the previous implementation.
Learnt from: javier-godoy
Repo: FlowingCode/GridExporterAddon PR: 143
File: src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java:315-315
Timestamp: 2024-09-26T18:26:03.345Z
Learning: In the `GridExporter` class, CSV exports are not subject to concurrent controls, so `getCsvStreamResource` returns a `StreamResource` instead of a `GridExporterStreamResource`.
Learnt from: javier-godoy
Repo: FlowingCode/GridExporterAddon PR: 143
File: src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java:315-315
Timestamp: 2024-10-08T21:28:24.972Z
Learning: In the `GridExporter` class, CSV exports are not subject to concurrent controls, so `getCsvStreamResource` returns a `StreamResource` instead of a `GridExporterStreamResource`.
📚 Learning: 2024-09-26T18:26:03.345Z
Learnt from: javier-godoy
Repo: FlowingCode/GridExporterAddon PR: 143
File: src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java:315-315
Timestamp: 2024-09-26T18:26:03.345Z
Learning: In the `GridExporter` class, CSV exports are not subject to concurrent controls, so `getCsvStreamResource` returns a `StreamResource` instead of a `GridExporterStreamResource`.

Applied to files:

  • src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentStreamResourceWriter.java
  • src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentOperationBase.java
  • src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java
  • src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentDownloadHandler.java
📚 Learning: 2024-10-28T17:35:31.077Z
Learnt from: javier-godoy
Repo: FlowingCode/GridExporterAddon PR: 154
File: src/main/java/com/flowingcode/vaadin/addons/gridexporter/BaseStreamResourceWriter.java:98-103
Timestamp: 2024-10-28T17:35:31.077Z
Learning: `BaseStreamResourceWriter` and its extending classes are not public API currently, but the return type must be refactored as we are going to expose `BaseStreamResourceWriter` in the near future.

Applied to files:

  • src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentStreamResourceWriter.java
📚 Learning: 2024-07-27T22:14:37.039Z
Learnt from: javier-godoy
Repo: FlowingCode/GridExporterAddon PR: 119
File: src/main/java/com/flowingcode/vaadin/addons/gridexporter/CsvStreamResourceWriter.java:77-93
Timestamp: 2024-07-27T22:14:37.039Z
Learning: User javier-godoy requested to open a follow-up GitHub issue to discuss resource management in the refactored code of CsvStreamResourceWriter, highlighting the absence of try-with-resources in the previous implementation.

Applied to files:

  • src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentStreamResourceWriter.java
📚 Learning: 2024-10-08T21:28:24.972Z
Learnt from: javier-godoy
Repo: FlowingCode/GridExporterAddon PR: 125
File: src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java:338-402
Timestamp: 2024-10-08T21:28:24.972Z
Learning: Zero or negative costs in the `setConcurrentDownloadCost` method do not require any permits, allowing such downloads to bypass the semaphore mechanism.

Applied to files:

  • src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentStreamResourceWriter.java
  • src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentOperationBase.java
  • src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentDownloadHandler.java
🧬 Code graph analysis (1)
src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java (1)
src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporterConcurrentSettings.java (1)
  • GridExporterConcurrentSettings (37-123)
🪛 GitHub Check: SonarCloud Code Analysis
src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java

[warning] 319-319: Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=FlowingCode_GridExporterAddon&issues=AZq84AEOvK_9d0Mfzo2Z&open=AZq84AEOvK_9d0Mfzo2Z&pullRequest=189


[warning] 375-375: Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=FlowingCode_GridExporterAddon&issues=AZq84AEOvK_9d0Mfzo2d&open=AZq84AEOvK_9d0Mfzo2d&pullRequest=189


[warning] 402-402: Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=FlowingCode_GridExporterAddon&issues=AZq84AEOvK_9d0Mfzo2f&open=AZq84AEOvK_9d0Mfzo2f&pullRequest=189


[warning] 361-361: Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=FlowingCode_GridExporterAddon&issues=AZq84AEOvK_9d0Mfzo2c&open=AZq84AEOvK_9d0Mfzo2c&pullRequest=189


[warning] 388-388: Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=FlowingCode_GridExporterAddon&issues=AZq84AEOvK_9d0Mfzo2e&open=AZq84AEOvK_9d0Mfzo2e&pullRequest=189


[warning] 347-347: Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=FlowingCode_GridExporterAddon&issues=AZq84AEOvK_9d0Mfzo2b&open=AZq84AEOvK_9d0Mfzo2b&pullRequest=189


[warning] 333-333: Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=FlowingCode_GridExporterAddon&issues=AZq84AEOvK_9d0Mfzo2a&open=AZq84AEOvK_9d0Mfzo2a&pullRequest=189


[failure] 1043-1043: Remove usage of generic wildcard type.

See more on https://sonarcloud.io/project/issues?id=FlowingCode_GridExporterAddon&issues=AZrBFOArZzgyS1d0Prb-&open=AZrBFOArZzgyS1d0Prb-&pullRequest=189

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

[warning] 24-24: Remove this unused import 'com.vaadin.flow.server.VaadinSession'.

See more on https://sonarcloud.io/project/issues?id=FlowingCode_GridExporterAddon&issues=AZrBFODLZzgyS1d0Prb_&open=AZrBFODLZzgyS1d0Prb_&pullRequest=189

🔇 Additional comments (11)
src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentDownloadHandler.java (1)

38-77: LGTM!

The class structure and delegation pattern are well-designed. The handleDownloadRequest method correctly wraps the delegate's handling within the semaphore-controlled runWithSemaphore method, ensuring consistent concurrency behavior with the existing ConcurrentStreamResourceWriter.

src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentStreamResourceWriter.java (1)

38-89: LGTM!

The refactoring to extend ConcurrentOperationBase is well-executed. The class now properly delegates semaphore logic to the base class while maintaining its public API contract for setLimit/getLimit. The accept method correctly uses runWithSemaphore for concurrency control.

src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentOperationBase.java (4)

52-81: LGTM!

The ConfigurableSemaphore implementation is well-designed. The setPermits method correctly handles both increasing and decreasing permits atomically through synchronization, and the toString method provides useful debugging output.


94-128: LGTM!

The setLimit and getLimit methods properly handle edge cases including infinite limits and provide thread-safe access to the semaphore configuration.


130-139: LGTM!

The costToPermits method correctly implements the fixed-point arithmetic for permit calculation. The behavior of returning 0 permits for zero or negative costs (allowing bypass of the semaphore) is consistent with the documented design. Based on learnings, this is the intended behavior.


220-257: LGTM!

The runWithSemaphore method has sound concurrency logic:

  • onAccept() is correctly called before any semaphore interaction (appropriate for UI state management)
  • Permits are properly released in the inner finally block only when acquired
  • InterruptedException correctly restores the thread's interrupt status
  • The outer finally ensures onFinish() is always called

The UI attachment check after semaphore acquisition provides a safeguard against processing requests for detached UIs when failOnUiChange is enabled.

src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java (5)

45-46: LGTM!

The new DownloadHandler import is correctly added to support the new API.


310-405: LGTM!

The deprecation of StreamResource-based methods is properly implemented with:

  • @Deprecated(since = "2.6.0", forRemoval = true) annotations
  • Javadoc pointing to the new DownloadHandler alternatives
  • Clear indication of removal timeline (version 3.0.0)

This provides a smooth migration path as stated in the PR objectives. The SonarCloud warnings are expected reminders for future cleanup.


406-489: LGTM!

The new DownloadHandler methods are well-implemented:

  • DOCX, PDF, and Excel exports correctly use makeConcurrentDownloadHandler for concurrency control
  • CSV export correctly uses StreamResourceWriterAdapter directly without concurrency wrapper, consistent with the existing behavior where CSV exports are not subject to concurrent controls (based on learnings)
  • MIME types are correctly specified for each format

496-500: LGTM!

The makeConcurrentDownloadHandler helper properly composes the writer into a concurrent-aware download handler by first adapting it to DownloadHandler and then wrapping with concurrency control.


226-232: LGTM!

Minor formatting adjustment with no semantic changes.

Comment on lines +566 to 633
super(delegate);
}

private Component button;

@Override
public GridExporter<?> getExporter() {
return GridExporter.this;
}

@Override
public Component getButton() {
return button;
}

@Override
public float getCost(VaadinSession session) {
return GridExporterConcurrentStrategy.super.getCost(session);
}

@Override
public long getTimeout() {
return GridExporterConcurrentStrategy.super.getTimeout();
}

@Override
public UI getUI() {
return GridExporterConcurrentStrategy.super.getUI();
}

@Override
public void onTimeout() {
GridExporterConcurrentStrategy.super.onTimeout();
}

@Override
public void onAccept() {
GridExporterConcurrentStrategy.super.onAccept();
}

@Override
public void onFinish() {
GridExporterConcurrentStrategy.super.onFinish();
}

/**
* Associates this download handler with a component (typically a button).
* This allows the handler to enable/disable the component during download.
*
* @param component the component to associate with this handler
* @return this handler for method chaining
*/
public GridExporterConcurrentDownloadHandler forComponent(Component component) {
this.button = component;
return this;
}
}

/**
* Handles the timeout event by notifying all registered listeners.
* <p>
* This method is called when a timeout occurs during a concurrent download. It creates a
* {@link ConcurrentDownloadTimeoutEvent} and notifies all instance and global listeners. If any
* listener stops the event propagation, subsequent listeners will not be notified.
*/
private void fireConcurrentDownloadTimeout() {
var globalListeners = GridExporterConcurrentSettings.getGlobalDownloadTimeoutListeners();
if (!instanceDownloadTimeoutListeners.isEmpty() || !globalListeners.isEmpty()) {
grid.getUI().ifPresent(ui -> ui.access(() -> {
ConcurrentDownloadTimeoutEvent ev = new ConcurrentDownloadTimeoutEvent(GridExporter.this);
Stream.concat(instanceDownloadTimeoutListeners.stream(),
globalListeners.stream()).forEach(listener -> {
if (!ev.isPropagationStopped()) {
listener.accept(ev);
}
});
}));
}
}

/**
* Adds a listener for concurrent download timeout events specific to this instance.
* <p>
* The listener will be called whenever a concurrent download timeout event occurs.
*
* @param listener the listener to be added
* @return a {@link Registration} object that can be used to remove the listener
*/
public Registration addConcurrentDownloadTimeoutEvent(
SerializableConsumer<ConcurrentDownloadTimeoutEvent> listener) {
instanceDownloadTimeoutListeners.add(0, listener);
return () -> instanceDownloadTimeoutListeners.remove(listener);
}

/**
* Configures the behavior of the system when a download is in progress.
* <p>
* 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.
* </p>
*
* @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
* time, the sum of the costs of all concurrent downloads will not exceed the limit set by
* {@link GridExporterConcurrentSettings#setConcurrentDownloadLimit(float)}.
* <p>
*
* The cost is represented as a float to allow for more granular control over resource usage. By
* using a floating-point number, fractional costs can be expressed, providing flexibility in
* determining the resource consumption for different downloads.
* <p>
*
* The cost is converted to a number of permits by capping it to stay within the limit. A cost of
* 1.0 ({@link #DEFAULT_COST}) represents a standard unit of resource usage, while a cost of 0.5
* represents half a unit, and a cost above 1.0 indicates higher than normal resource usage.
* <p>
*
* If the cost is zero or negative, no permits are needed. However, any positive cost, no matter
* how small, will require at least one permit to prevent downloads with very low costs from
* bypassing the semaphore. {@link #MIN_COST} represents the minimal fractional cost that acquires
* only one permit (hence {@code 2*MIN_COST} acquires two permits and so on). A cost of
* {@link #MAX_COST} prevents any other downloads from acquiring permits simultaneously.
*
* @param concurrentDownloadCost the cost associated with concurrent downloads for this instance.
*/
public void setConcurrentDownloadCost(float concurrentDownloadCost) {
this.concurrentDownloadCost = concurrentDownloadCost;
}

public String getTitle() {
return title;
}

/**
* Sets the title of the exported file
*
* @param title
*/
public void setTitle(String title) {
this.title = title;
}

public String getFileName() {
return fileNameSupplier.get();
}

private String getFileName(String extension) {
return Objects.requireNonNull(getFileName()) + "." + extension;
}

/**
* Sets the filename of the exported file
*
* @param fileName
*/
public void setFileName(String fileName) {
Objects.requireNonNull(fileName, "File name cannot be null");
fileNameSupplier = () -> fileName;
}

/**
* Sets a dynamic filename for the exported file.
*
* @param fileNameSupplier a supplier that returns the name of the exported file, without
* extension.
*/
public void setFileName(SerializableSupplier<String> fileNameSupplier) {
this.fileNameSupplier =
Objects.requireNonNull(fileNameSupplier, "File name supplier cannot be null");
}

public boolean isAutoAttachExportButtons() {
return autoAttachExportButtons;
}

/**
* If true, it will automatically generate export buttons in the asociated grid
*
* @param autoAttachExportButtons
*/
public void setAutoAttachExportButtons(boolean autoAttachExportButtons) {
this.autoAttachExportButtons = autoAttachExportButtons;
}

public Map<String, String> getAdditionalPlaceHolders() {
return additionalPlaceHolders;
}

/**
* Sets a map that will contain additional place holders that will be replaced with values when
* processing the exported file
*
* @param additionalPlaceHolders
*/
public void setAdditionalPlaceHolders(Map<String, String> additionalPlaceHolders) {
this.additionalPlaceHolders = additionalPlaceHolders;
}

public int getSheetNumber() {
return sheetNumber;
}

/**
* Configures the excel sheet that will be inspected for placeholders to export the data
*
* @param sheetNumber
*/
public void setSheetNumber(int sheetNumber) {
this.sheetNumber = sheetNumber;
}

public boolean isAutoMergeTitle() {
return autoMergeTitle;
}

/**
* If true the title cell will be merged with the next ones to create a single title cell that
* will span across the columns
*
* @param autoMergeTitle
*/
public void setAutoMergeTitle(boolean autoMergeTitle) {
this.autoMergeTitle = autoMergeTitle;
}

public boolean isExcelExportEnabled() {
return excelExportEnabled;
}

public void setExcelExportEnabled(boolean excelExportEnabled) {
this.excelExportEnabled = excelExportEnabled;
}

public boolean isDocxExportEnabled() {
return docxExportEnabled;
}

public void setDocxExportEnabled(boolean docxExportEnabled) {
this.docxExportEnabled = docxExportEnabled;
}

public boolean isPdfExportEnabled() {
return pdfExportEnabled;
}

public void setPdfExportEnabled(boolean pdfExportEnabled) {
this.pdfExportEnabled = pdfExportEnabled;
}

public boolean isCsvExportEnabled() {
return csvExportEnabled;
}

public void setCsvExportEnabled(boolean csvExportEnabled) {
this.csvExportEnabled = csvExportEnabled;
}

public boolean isAutoSizeColumns() {
return autoSizeColumns;
}

public void setAutoSizeColumns(boolean autoSizeColumns) {
this.autoSizeColumns = autoSizeColumns;
}

/**
* Configure a value provider for a given column. If there is a value provider, that will be taken
* into account when exporting the column
*
* @param column
* @param vp
*/
public void setExportValue(Column<T> column, ValueProvider<T, ?> vp) {
ComponentUtil.setData(column, COLUMN_VALUE_PROVIDER_DATA, vp);
}

/**
* Configure if the column is exported or not
*
* @param column
* @param export true will be included in the exported file, false will not be included
*/
public void setExportColumn(Column<T> column, boolean export) {
ComponentUtil.setData(column, COLUMN_EXPORTED_PROVIDER_DATA, export);
}

public void setNullValueHandler(SerializableSupplier<String> nullValueSupplier) {
this.nullValueSupplier = nullValueSupplier;
}

/**
* If the column is based on a String, it configures a DecimalFormat to parse a number from the
* value of the column so it can be converted to a Double, and then allows to specify the excel
* format to be applied to the cell when exported to excel, so the resulting cell is not a string
* but a number that can be used in formulas.
*
* @param column
* @param decimalFormat
* @param excelFormat
*/
public void setNumberColumnFormat(
Column<T> column, DecimalFormat decimalFormat, String excelFormat) {
ComponentUtil.setData(column, COLUMN_PARSING_FORMAT_PATTERN_DATA, decimalFormat);
ComponentUtil.setData(column, COLUMN_EXCEL_FORMAT_DATA, excelFormat);
ComponentUtil.setData(column, COLUMN_TYPE_DATA, COLUMN_TYPE_NUMBER);
}

/**
* If the column is based on a String, it configures a DecimalFormat to parse a number from the
* value of the column so it can be converted to a Double, and then allows to specify the excel
* format to be applied to the cell when exported to excel with a provider, so the resulting cell
* is not a string but a number that can be used in formulas.
*
* @param column
* @param decimalFormat
* @param excelFormatProvider
*/
public void setNumberColumnFormatProvider(Column<T> column, DecimalFormat decimalFormat,
ValueProvider<T, String> excelFormatProvider) {
ComponentUtil.setData(column, COLUMN_PARSING_FORMAT_PATTERN_DATA, decimalFormat);
ComponentUtil.setData(column, COLUMN_EXCEL_FORMAT_DATA_PROVIDER, excelFormatProvider);
ComponentUtil.setData(column, COLUMN_TYPE_DATA, COLUMN_TYPE_NUMBER);
}

/**
* If the column is based on a String, it configures a DateFormat to parse a date from the value
* of the column so it can be converted to a java.util.Date, and then allows to specify the excel
* format to be applied to the cell when exported to excel, so the resulting cell is not a string
* but a date that can be used in formulas.
*
* @param column
* @param dateFormat
* @param excelFormat
*/
public void setDateColumnFormat(Column<T> column, DateFormat dateFormat, String excelFormat) {
ComponentUtil.setData(column, COLUMN_PARSING_FORMAT_PATTERN_DATA, dateFormat);
ComponentUtil.setData(column, COLUMN_EXCEL_FORMAT_DATA, excelFormat);
ComponentUtil.setData(column, COLUMN_TYPE_DATA, COLUMN_TYPE_DATE);
}

/**
* If the column is based on a String, it configures a DateFormat to parse a date from the value
* of the column so it can be converted to a java.util.Date, and then allows to specify the excel
* format to be applied to the cell when exported to excel, so the resulting cell is not a string
* but a date that can be used in formulas.
*
* @param column
* @param dateFormat
* @param excelFormatProvider
*/
public void setDateColumnFormatProvider(Column<T> column, DateFormat dateFormat,
ValueProvider<T, String> excelFormatProvider) {
ComponentUtil.setData(column, COLUMN_PARSING_FORMAT_PATTERN_DATA, dateFormat);
ComponentUtil.setData(column, COLUMN_EXCEL_FORMAT_DATA_PROVIDER, excelFormatProvider);
ComponentUtil.setData(column, COLUMN_TYPE_DATA, COLUMN_TYPE_DATE);
}

/**
* If the column is based on a number attribute of the item, rendered with a NumberRenderer, it
* configures the excel format to be applied to the cell when exported to excel, so the resulting
* cell is not a string but a number that can be used in formulas.
*
* @param column
* @param excelFormat
*/
public void setNumberColumnFormat(Column<T> column, String excelFormat) {
ComponentUtil.setData(column, COLUMN_EXCEL_FORMAT_DATA, excelFormat);
ComponentUtil.setData(column, COLUMN_TYPE_DATA, COLUMN_TYPE_NUMBER);
}

/**
* If the column is based on a LocalDate attribute of the item, rendered with a LocalDateRenderer,
* it configures the excel format to be applied to the cell when exported to excel, so the
* resulting cell is not a string but a date that can be used in formulas.
*
* @param column
* @param excelFormat
*/
public void setDateColumnFormat(Column<T> column, String excelFormat) {
ComponentUtil.setData(column, COLUMN_EXCEL_FORMAT_DATA, excelFormat);
ComponentUtil.setData(column, COLUMN_TYPE_DATA, COLUMN_TYPE_DATE);
}

/**
* Configures the exporter to use a custom string for a specific column's header. Usefull when the
* header is a custom component.
*
* @param column
* @param header
*/
public void setCustomHeader(Column<T> column, String header) {
ComponentUtil.setData(column, COLUMN_HEADER, header);
}

/**
* Configures the exporter to use a custom string for a specific column's footer. Usefull when the
* footer is a custom component.
*
* @param column
* @param header
*/
public void setCustomFooter(Column<T> column, String header) {
ComponentUtil.setData(column, COLUMN_FOOTER, header);
}

/**
* Assigns the position of the column in the exported file.
*
* @param column
* @param position
*/
public void setColumnPosition(Column<T> column, int position) {
ComponentUtil.setData(column, COLUMN_POSITION, position);
}

private int getColumnPosition(Column<T> column) {
return Optional.ofNullable(ComponentUtil.getData(column, COLUMN_POSITION))
.map(value -> Integer.class.cast(value))
.orElse(Integer.MAX_VALUE);
}

public void setColumns(List<Grid.Column<T>> columns) {
this.columns = columns;
}

public List<Column<T>> getColumns() {
return columns;
}

/**
* Get columns in the positions specified by {@link GridExporter#setColumnPosition(Column, int)}
*/
public List<Column<T>> getColumnsOrdered() {
return columns == null
? columns
: columns.stream()
.sorted(Comparator.comparing(this::getColumnPosition))
.collect(Collectors.toList());
}

public void setFooterToolbarItems(List<FooterToolbarItem> footerToolbarItems) {
this.footerToolbarItems = footerToolbarItems;
}

/**
* Charset to use when exporting the CSV file.
*
* @return CSV file charset or default one.
*/
public Charset getCsvCharset() {
return csvCharset == null ? Charset.defaultCharset() : csvCharset.get();
}

public void setCsvCharset(SerializableSupplier<Charset> charset) {
csvCharset = charset;
}

private interface GridExporterConcurrentStrategy {
GridExporter<?> getExporter();

Component getButton();

default float getCost(VaadinSession session) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The forComponent method is not accessible from the public API.

The getExcelDownloadHandler() (and similar methods) return DownloadHandler, but forComponent(Component) is defined on the private GridExporterConcurrentDownloadHandler class. This means users cannot call forComponent to associate a button with the download handler, unlike the existing GridExporterStreamResource.forComponent() pattern.

This creates an API asymmetry between the deprecated and new methods. Consider either:

  1. Making the return type of getExcelDownloadHandler() etc. return a public type with forComponent, or
  2. Creating a public interface/class that exposes forComponent while implementing DownloadHandler

For comparison, the deprecated API allows:

exporter.getExcelStreamResource().forComponent(button)

But the new API doesn't support:

exporter.getExcelDownloadHandler().forComponent(button) // forComponent not visible
🤖 Prompt for AI Agents
In src/main/java/com/flowingcode/vaadin/addons/gridexporter/GridExporter.java
around lines 566 to 633, the nested class GridExporterConcurrentDownloadHandler
defines forComponent(Component) but is private so callers receiving a
DownloadHandler can't access it; change the API so callers can chain
forComponent like the deprecated StreamResource API. Fix by extracting a public
interface (e.g. PublicConcurrentDownloadHandler) that extends DownloadHandler
and declares forComponent(Component) (and any needed
GridExporterConcurrentStrategy methods), have
GridExporterConcurrentDownloadHandler implement that interface, and update the
public factory/getter methods (getExcelDownloadHandler() etc.) to return the new
public interface type instead of DownloadHandler; alternatively, make
GridExporterConcurrentDownloadHandler a public/static top-level class with the
forComponent method exposed and update the return types accordingly. Ensure
imports and method signatures are updated so existing callers can call
.forComponent(button).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

StreamResource is deprecated in favor of DownloadHandler

2 participants