From ced77cad6d21c671528f88d15da4259baa0c2bb2 Mon Sep 17 00:00:00 2001 From: Mikhail Shabarov <61410877+mshabarov@users.noreply.github.com> Date: Wed, 14 May 2025 16:42:13 +0300 Subject: [PATCH 01/13] feat: Add docs for DownloadHandler fixes https://github.com/vaadin/docs/issues/4303 --- articles/flow/advanced/downloads.adoc | 363 ++++++++++++++++++++ articles/flow/advanced/dynamic-content.adoc | 119 ------- 2 files changed, 363 insertions(+), 119 deletions(-) create mode 100644 articles/flow/advanced/downloads.adoc delete mode 100644 articles/flow/advanced/dynamic-content.adoc diff --git a/articles/flow/advanced/downloads.adoc b/articles/flow/advanced/downloads.adoc new file mode 100644 index 0000000000..1886100b21 --- /dev/null +++ b/articles/flow/advanced/downloads.adoc @@ -0,0 +1,363 @@ +--- +title: Downloads +page-title: How to download from server to browser in Vaadin +description: Download a file or arbitrary content from server to browser. +meta-description: Learn how to handle download requests on server and transfer a content to browser in Vaadin Flow applications. +order: 110 +--- + += Handle Downloads +:toc: + +To generate content dynamically and download it, there are two options: + +* You can use a [classname]`DownloadHandler`, which handles HTTP requests automatically. +* You can build a custom URL using [classname]`String` type parameters. +In this case, you need one more servlet, which handles the URL. + +The first option is preferable, since it doesn't require an additional servlet and allows you to use data of any type from the application state. + +== Introduction + +The [classname]`DownloadHandler` API provides a flexible high-level abstraction to implement file and arbitrary contents downloads from server to browser in Vaadin applications. +This API supports various download scenarios, from simple file downloads to complex streaming with progress tracking. + +Downloading is supported by multiple different Vaadin components such as `Anchor`, `Image`, `IFrame`, `Avatar`, `AvatarGroup`, `SvgIcon`, `MessageListItem`. +Note that "download" here doesn't refer to only downloading a file to the user's file system, but it also covers cases where an HTML element downloads a file into the browser's cache and renders it from there, e.g. `Image` component. + +This documentation covers the main features of the `DownloadHandler` API, including: + +* Static helper methods for common download scenarios +* Download progress tracking +* Creating custom download handlers +* Low-level API features + +== Common Download Scenarios + +The `DownloadHandler` API provides several static helper methods to simplify common download scenarios. + +=== Download A Classpath Resource + +The `forClassResource` method allows you to serve resources from the classpath. +For instance, for the file [filename]`src/main/resources/com/example/ui/vaadin.jpeg` and class [classname]`com.example.ui.MainView` the code would be: + +[source,java] +---- +Image logoImage = new Image(DownloadHandler.forClassResource( + MainView.class, "vaadin.jpeg"), "Vaadin Logo"); +---- + +This method is useful for serving static resources like images, CSS, or JavaScript files that are packaged with your application. + +=== Download A File From File System + +The `forFile` method allows you to serve files from the server's file system. + +[source,java] +---- +Anchor download = new Anchor(DownloadHandler.forFile(new File("/path/to/terms-and-conditions.md")), "Download Terms and Conditions"); +---- + +This method is useful for serving files that are stored on the server's file system. + +=== Download Content from InputStream + +The `fromInputStream` method allows you to serve content from any [classname]`InputStream`. +This is the most flexible helper method as it can be used with any data source that can provide an `InputStream`. + +[source,java] +---- +var downloadAttachment = new Anchor(DownloadHandler.fromInputStream((event) -> { + try { + Attachment attachment = attachmentsRepository.findById(attachmentId); + return new DownloadResponse(attachment.getData().getBinaryStream(), + attachment.getName(), attachment.getMime(), attachment.getSize()); + } catch (Exception e) { + return DownloadResponse.error(500); + } +}, "attachment.txt"), "Download attachment"); +---- + +This method is particularly useful for: + +* Serving content from databases or file storages +* Generating dynamic content +* Streaming large files + +== Download Progress Listeners + +The `DownloadHandler` API provides two ways to track download progress: + +1. Using the fluent API with shorthand methods +2. Implementing the [classname]`TransferProgressListener` interface + +Asynchronous UI updates in progress listeners are automatically wrapped into `UI.access()` calls by Vaadin, thus you don't need to call it manually. +Vaadin `@Push` should be enabled in your application to being able to see UI updates while download is in progress. + +=== Fluent API for Progress Tracking + +The fluent API provides a concise way to track download progress using method chaining. + +[source,java] +---- +InputStreamDownloadHandler handler = DownloadHandler.fromInputStream(event -> + new DownloadResponse(getInputStream(), "download.bin", + "application/octet-stream", contentSize)) + .whenStart(() -> { + Notification.show("Download started", 3000, Notification.Position.BOTTOM_START); + progressBar.setVisible(true); + }) + .onProgress((transferred, total) -> progressBar.setValue((double) transferred / total)) + .whenComplete(success -> { + progressBar.setVisible(false); + if (success) { + Notification.show("Download completed", 3000, Notification.Position.BOTTOM_START) + .addThemeVariants(NotificationVariant.LUMO_SUCCESS); + } else { + Notification.show("Download failed", 3000, Notification.Position.BOTTOM_START) + .addThemeVariants(NotificationVariant.LUMO_ERROR); + } + }); +---- + +The fluent API provides the following methods: + +* `whenStart(Runnable)`: Called when the download starts +* `onProgress(BiConsumer)`: Called during the download with transferred and total bytes +* `whenComplete(Consumer)`: Called when the download completes successfully or with a failure + +=== TransferProgressListener Interface + +For more control over download progress tracking, you can implement the `TransferProgressListener` interface. + +[source,java] +---- +InputStreamDownloadHandler handler = DownloadHandler.fromInputStream(event -> + new DownloadResponse(getInputStream(), "download.bin", + "application/octet-stream", contentSize), + "download.bin", new TransferProgressListener() { + @Override + public void onStart(TransferContext context) { + Notification.show("Download started for file " + context.fileName(), + 3000, Notification.Position.BOTTOM_START); + progressBar.setVisible(true); + } + + @Override + public void onProgress(TransferContext context, long transferredBytes, + long totalBytes) { + progressBar.setValue((double) transferredBytes / totalBytes); + } + + @Override + public void onError(TransferContext context, IOException reason) { + progressBar.setVisible(false); + Notification.show("Download failed, reason: " + reason.getMessage(), + 3000, Notification.Position.BOTTOM_START); + } + + @Override + public void onComplete(TransferContext context, long transferredBytes) { + progressBar.setVisible(false); + Notification.show("Download completed, total bytes " + transferredBytes, + 3000, Notification.Position.BOTTOM_START); + } + + @Override + public long progressReportInterval() { + return 1024 * 1024 * 2; // 2 MB + } +}); +---- + +The `TransferProgressListener` interface provides the following methods: + +* `onStart(TransferContext)`: Called when the download starts +* `onProgress(TransferContext, long, long)`: Called during the download with transferred and total bytes +* `onError(TransferContext, IOException)`: Called when the download fails with an exception +* `onComplete(TransferContext, long)`: Called when the download completes with the total transferred bytes +* `progressReportInterval()`: Defines how often progress updates are sent (in bytes) + +The [classname]`TransferContext` provides information about the download, such as the file name, content length (if known), a reference to an owner component and Vaadin request, response and session. + +== Custom Download Handlers + +For more complex download scenarios, you can create custom download handlers by implementing the `DownloadHandler` interface or extending existing implementations. + +=== Implementing DownloadHandler Interface + +You can implement the `DownloadHandler` interface to create a custom download handler or use a lambda. + +[source,java] +---- +Anchor downloadLink = new Anchor(new DownloadHandler() { + @Override + public void handleDownloadRequest(DownloadEvent event) { + // Custom download handling logic + } + + @Override + public String getUrlPostfix() { + return "custom-download.txt"; + } +}, "Download me!"); +---- + +=== Custom Download Handler Example + +Here's an example of a custom download handler that adds a checksum header, updates the UI and tracks the number of downloads per session: + +[source,java] +---- +LinkWithM5Validation link = new LinkWithM5Validation(event -> { + try { + var data = loadFileFromS3(event.getFileName(), event.getContentType()); + MessageDigest md5 = MessageDigest.getInstance("MD5"); + byte[] digest = md5.digest(data); + String base64Md5 = Base64.getEncoder().encodeToString(digest); + event.getResponse().setHeader("Content-MD5", base64Md5); + event.getResponse().getOutputStream().write(data); + event.getUI().access(() -> Notification.show( + "Download completed, number of downloads: " + + numberOfDownloads.incrementAndGet())); + event.getSession().lock(); + try { + event.getSession().setAttribute("downloads-number-" + event.getFileName(), + numberOfDownloads.get()); + } finally { + event.getSession().unlock(); + } + } catch (NoSuchAlgorithmException | IOException e) { + event.getResponse().setStatus(500); + } +}, "Download from S3"); + +private byte[] loadFileFromS3(String fileName, String contentType) { + byte[] bytes = new byte[1024 * 1024 * 10]; // 10 MB buffer + // load from file storage by file name and content type + return bytes; +} + +private static class LinkWithM5Validation extends Anchor { + // JS customizations in for checksum checking on the client-side +} +---- + +This example shows how to: + +* Get file meta-data from [classname]`DownloadEvent` to load data from an external source (S3) +* Set the MD5 checksum header to the response +* Write data directly to the response output stream +* Update the UI after the download completes +* Store download statistics in the session + +Note that `UI.access` is needed for updating the UI and also session locking if you want to access session. + +The [classname]`DownloadEvent` provides information about the download, such as the file name, content length (if known), a reference to an owner component and Vaadin request, response and session. + +== Low-Level API + +The `DownloadHandler` API provides several low-level features for advanced use cases. + +=== Inert Property + +The `inert` property controls whether the download should be handled when the owner component is in an inert state, e.g. when a modal dialog is opened while the owner component is on the underlined page. +See the <<../advanced/server-side-modality.adoc#,Server-Side Modality>> for details. + +[classname]`DownloadHandler` allows to handle download request from inert component by overriding the `allowInert()` method. + +=== Disabled Update Mode + +The [classname]`DisabledUpdateMode` controls whether downloads are allowed when the owner component is disabled. + +The available modes are: + +* `ONLY_WHEN_ENABLED`: Download handling is rejected when the owner component is disabled (default) +* `ALWAYS`: Download handling is allowed even when the owner component is disabled + +[classname]`DownloadHandler` allows to override this mode by overriding the `getDisabledUpdateMode()` method. + +=== URL Postfix + +The `getUrlPostfix()` method allows you to specify an optional URL postfix that appends application-controlled string, e.g. the logical name of the target file, to the end of the otherwise random-looking download URL. +If defined, requests that would otherwise be servable are still rejected if the postfix is missing or invalid. + +This is useful for: + +* Providing a meaningful filename into the download handler callback +* Making the download request URL look more user-friendly as otherwise it is a random-looking URL + +[source,java] +---- +Anchor downloadLink = new Anchor(new DownloadHandler() { + @Override + public void handleDownloadRequest(DownloadEvent event) { + // download handling... + } + + @Override + public boolean allowInert() { + return true; // default is false + } + + @Override + public DisabledUpdateMode getDisabledUpdateMode() { + return DisabledUpdateMode.ALWAYS; // the default is ONLY_WHEN_ENABLED + } + + @Override + public String getUrlPostfix() { + return "meeting-notes.txt"; + } +}, "Download meeting notes"); +---- + +== Using Custom Servlet and Request Parameters + +You can create a custom servlet which handles "image" as a relative URL: + +[source,java] +---- +@WebServlet(urlPatterns = "/image", name = "DynamicContentServlet") +public class DynamicContentServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("image/svg+xml"); + String name = req.getParameter("name"); + if (name == null) { + name = ""; + } + String svg = "" + + "" + + "" + + name + "" + ""; + resp.getWriter().write(svg); + } +} +---- + +The following code should be used in the application (which has its own servlet). +It generates the resource URL on the fly, based on the current application state. +The property value of the input component is used here as a state: + +[source,java] +---- +Input name = new Input(); + +Element image = new Element("object"); +image.setAttribute("type", "image/svg+xml"); +image.getStyle().set("display", "block"); + +NativeButton button = new NativeButton("Generate Image"); +button.addClickListener(event -> { + String url = "image?name=" + name.getValue(); + image.setAttribute("data", url); +}); + +UI.getCurrent().getElement().appendChild(name.getElement(), image, + button.getElement()); +---- diff --git a/articles/flow/advanced/dynamic-content.adoc b/articles/flow/advanced/dynamic-content.adoc deleted file mode 100644 index 68f8a3ea16..0000000000 --- a/articles/flow/advanced/dynamic-content.adoc +++ /dev/null @@ -1,119 +0,0 @@ ---- -title: Dynamic Content -page-title: How to create dynamic content in Vaadin -description: Generating content dynamically based on the application state. -meta-description: Learn how to manage and display dynamic content in Vaadin Flow applications. -order: 110 ---- - - -= Dynamic Content -:toc: - -To generate content dynamically based on data provided by the current application state, there are two options: - -* You can use a [classname]`StreamResource`, which handles URLs automatically. -* You can build a custom URL using [classname]`String` type parameters. -In this case, you need one more servlet, which handles the URL. - -The first option is preferable, since it doesn't require an additional servlet and allows you to use data of any type from the application state. - -== Using Custom Servlet and Request Parameters - -You can create a custom servlet which handles "image" as a relative URL: - -[source,java] ----- -@WebServlet(urlPatterns = "/image", name = "DynamicContentServlet") -public class DynamicContentServlet extends HttpServlet { - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) - throws ServletException, IOException { - resp.setContentType("image/svg+xml"); - String name = req.getParameter("name"); - if (name == null) { - name = ""; - } - String svg = "" - + "" - + "" - + name + "" + ""; - resp.getWriter().write(svg); - } -} ----- - -The following code should be used in the application (which has its own servlet). -It generates the resource URL on the fly, based on the current application state. -The property value of the input component is used here as a state: - -[source,java] ----- -Input name = new Input(); - -Element image = new Element("object"); -image.setAttribute("type", "image/svg+xml"); -image.getStyle().set("display", "block"); - -NativeButton button = new NativeButton("Generate Image"); -button.addClickListener(event -> { - String url = "image?name=" + name.getValue(); - image.setAttribute("data", url); -}); - -UI.getCurrent().getElement().appendChild(name.getElement(), image, - button.getElement()); ----- - -=== Using StreamResource - -Use [classname]`StreamResource` to generate dynamic content within the same servlet. -In this case, the application generates the URL transparently for you and registers an internal handler for this URL. -The code below shows how to implement the same functionality as above, using [classname]`StreamResource`. - -[source,java] ----- -Input name = new Input(); - -Element image = new Element("object"); -image.setAttribute("type", "image/svg+xml"); -image.getStyle().set("display", "block"); - -NativeButton button = new NativeButton("Generate Image"); -button.addClickListener(event -> { - StreamResource resource = new StreamResource("image.svg", - () -> getImageInputStream(name)); - image.setAttribute("data", resource); -}); - -UI.getCurrent().getElement().appendChild(name.getElement(), image, - button.getElement()); ----- - -The `data` attribute value is set to the [classname]`StreamResource`, which is automatically converted into a URL. -A [classname]`StreamResource` uses a dynamic data provider to produce the data. -The file name given to a [classname]`StreamResource` is used as a part of the URL and also becomes the filename, if the user chooses to download the resource. -And here is an example of how to create a data provider: - -[source,java] ----- -private InputStream getImageInputStream(Input name) { - String value = name.getValue(); - if (value == null) { - value = ""; - } - String svg = "" - + "" - + "" - + value + "" + ""; - return new ByteArrayInputStream(svg.getBytes(StandardCharsets.UTF_8)); -} ----- - - -[discussion-id]`DF78C6F1-4DFC-4F65-A0D4-29CCB2CFEDD5` From 3949d9bbf12b0b985a88a1c58590d91d8c695831 Mon Sep 17 00:00:00 2001 From: Mikhail Shabarov <61410877+mshabarov@users.noreply.github.com> Date: Thu, 15 May 2025 08:50:32 +0300 Subject: [PATCH 02/13] Update articles/flow/advanced/downloads.adoc Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- articles/flow/advanced/downloads.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/articles/flow/advanced/downloads.adoc b/articles/flow/advanced/downloads.adoc index 1886100b21..e1fa3ea5f6 100644 --- a/articles/flow/advanced/downloads.adoc +++ b/articles/flow/advanced/downloads.adoc @@ -92,7 +92,7 @@ The `DownloadHandler` API provides two ways to track download progress: 2. Implementing the [classname]`TransferProgressListener` interface Asynchronous UI updates in progress listeners are automatically wrapped into `UI.access()` calls by Vaadin, thus you don't need to call it manually. -Vaadin `@Push` should be enabled in your application to being able to see UI updates while download is in progress. +Vaadin `@Push` should be enabled in your application to be able to see UI updates while download is in progress. === Fluent API for Progress Tracking From 8b786013378bf22155959a283c069bc67b8ee628 Mon Sep 17 00:00:00 2001 From: Mikhail Shabarov <61410877+mshabarov@users.noreply.github.com> Date: Thu, 15 May 2025 15:11:35 +0300 Subject: [PATCH 03/13] fix review comments part 1 --- articles/flow/advanced/downloads.adoc | 84 +++++---------------------- 1 file changed, 15 insertions(+), 69 deletions(-) diff --git a/articles/flow/advanced/downloads.adoc b/articles/flow/advanced/downloads.adoc index 1886100b21..da23c4ee2c 100644 --- a/articles/flow/advanced/downloads.adoc +++ b/articles/flow/advanced/downloads.adoc @@ -9,34 +9,27 @@ order: 110 = Handle Downloads :toc: -To generate content dynamically and download it, there are two options: - -* You can use a [classname]`DownloadHandler`, which handles HTTP requests automatically. -* You can build a custom URL using [classname]`String` type parameters. -In this case, you need one more servlet, which handles the URL. - -The first option is preferable, since it doesn't require an additional servlet and allows you to use data of any type from the application state. - -== Introduction - The [classname]`DownloadHandler` API provides a flexible high-level abstraction to implement file and arbitrary contents downloads from server to browser in Vaadin applications. This API supports various download scenarios, from simple file downloads to complex streaming with progress tracking. -Downloading is supported by multiple different Vaadin components such as `Anchor`, `Image`, `IFrame`, `Avatar`, `AvatarGroup`, `SvgIcon`, `MessageListItem`. -Note that "download" here doesn't refer to only downloading a file to the user's file system, but it also covers cases where an HTML element downloads a file into the browser's cache and renders it from there, e.g. `Image` component. +Downloading is supported by multiple Vaadin Components where applicable. + +[NOTE] +"Download" here doesn't refer to only downloading a file to the user's file system, but it also covers cases where an HTML element downloads a file into the browser's cache and renders it from there, e.g. `Image` component. This documentation covers the main features of the `DownloadHandler` API, including: -* Static helper methods for common download scenarios -* Download progress tracking -* Creating custom download handlers -* Low-level API features +* <<#helpers,Static helper methods for common download scenarios>> +* <<#progress,Download progress tracking>> +* <<#custom,Creating custom download handlers>> +* <<#low-level,Low-level API features>> +[#helpers] == Common Download Scenarios The `DownloadHandler` API provides several static helper methods to simplify common download scenarios. -=== Download A Classpath Resource +=== Download a Classpath Resource The `forClassResource` method allows you to serve resources from the classpath. For instance, for the file [filename]`src/main/resources/com/example/ui/vaadin.jpeg` and class [classname]`com.example.ui.MainView` the code would be: @@ -84,6 +77,7 @@ This method is particularly useful for: * Generating dynamic content * Streaming large files +[#progress] == Download Progress Listeners The `DownloadHandler` API provides two ways to track download progress: @@ -180,6 +174,7 @@ The `TransferProgressListener` interface provides the following methods: The [classname]`TransferContext` provides information about the download, such as the file name, content length (if known), a reference to an owner component and Vaadin request, response and session. +[#custom] == Custom Download Handlers For more complex download scenarios, you can create custom download handlers by implementing the `DownloadHandler` interface or extending existing implementations. @@ -209,7 +204,7 @@ Here's an example of a custom download handler that adds a checksum header, upda [source,java] ---- -LinkWithM5Validation link = new LinkWithM5Validation(event -> { +LinkWithMD5Validation link = new LinkWithMD5Validation(event -> { try { var data = loadFileFromS3(event.getFileName(), event.getContentType()); MessageDigest md5 = MessageDigest.getInstance("MD5"); @@ -238,7 +233,7 @@ private byte[] loadFileFromS3(String fileName, String contentType) { return bytes; } -private static class LinkWithM5Validation extends Anchor { +private static class LinkWithMD5Validation extends Anchor { // JS customizations in for checksum checking on the client-side } ---- @@ -255,6 +250,7 @@ Note that `UI.access` is needed for updating the UI and also session locking if The [classname]`DownloadEvent` provides information about the download, such as the file name, content length (if known), a reference to an owner component and Vaadin request, response and session. +[#low-level] == Low-Level API The `DownloadHandler` API provides several low-level features for advanced use cases. @@ -311,53 +307,3 @@ Anchor downloadLink = new Anchor(new DownloadHandler() { } }, "Download meeting notes"); ---- - -== Using Custom Servlet and Request Parameters - -You can create a custom servlet which handles "image" as a relative URL: - -[source,java] ----- -@WebServlet(urlPatterns = "/image", name = "DynamicContentServlet") -public class DynamicContentServlet extends HttpServlet { - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) - throws ServletException, IOException { - resp.setContentType("image/svg+xml"); - String name = req.getParameter("name"); - if (name == null) { - name = ""; - } - String svg = "" - + "" - + "" - + name + "" + ""; - resp.getWriter().write(svg); - } -} ----- - -The following code should be used in the application (which has its own servlet). -It generates the resource URL on the fly, based on the current application state. -The property value of the input component is used here as a state: - -[source,java] ----- -Input name = new Input(); - -Element image = new Element("object"); -image.setAttribute("type", "image/svg+xml"); -image.getStyle().set("display", "block"); - -NativeButton button = new NativeButton("Generate Image"); -button.addClickListener(event -> { - String url = "image?name=" + name.getValue(); - image.setAttribute("data", url); -}); - -UI.getCurrent().getElement().appendChild(name.getElement(), image, - button.getElement()); ----- From 8e6ed8ac08b85d45566ae13105d137e0f07e9fc8 Mon Sep 17 00:00:00 2001 From: Mikhail Shabarov <61410877+mshabarov@users.noreply.github.com> Date: Tue, 20 May 2025 16:09:40 +0300 Subject: [PATCH 04/13] fix review comments part 2 --- articles/flow/advanced/downloads.adoc | 47 ++++++++++++++++++--------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/articles/flow/advanced/downloads.adoc b/articles/flow/advanced/downloads.adoc index eb8bb84e07..4da85cad1f 100644 --- a/articles/flow/advanced/downloads.adoc +++ b/articles/flow/advanced/downloads.adoc @@ -12,10 +12,7 @@ order: 110 The [classname]`DownloadHandler` API provides a flexible high-level abstraction to implement file and arbitrary contents downloads from server to browser in Vaadin applications. This API supports various download scenarios, from simple file downloads to complex streaming with progress tracking. -Downloading is supported by multiple Vaadin Components where applicable. - -[NOTE] -"Download" here doesn't refer to only downloading a file to the user's file system, but it also covers cases where an HTML element downloads a file into the browser's cache and renders it from there, e.g. `Image` component. +[classname]`DownloadHandler` is supported by multiple Vaadin Components where applicable. This documentation covers the main features of the `DownloadHandler` API, including: @@ -118,8 +115,16 @@ The fluent API provides the following methods: * `whenStart(Runnable)`: Called when the download starts * `onProgress(BiConsumer)`: Called during the download with transferred and total bytes +* `onProgress(BiConsumer, Long)`: Called during the download with transferred and total bytes and with the given progress interval in bytes * `whenComplete(Consumer)`: Called when the download completes successfully or with a failure +These methods have overloads that accept also the [classname]`TransferContext` object that gives more information and references: +* [classname]`VaadinRequest`, [classname]`VaadinResponse`, [classname]`VaadinSession` instances +* The owner component and element of the data transfer that you can change when transfer is in progress, e.g. disable the component, or get attributes or properties +* [classname]`UI` instance that you can use to call `UI.access()` for asynchronous updates in threads +* The name of the file being transferred, might be null if the file name is not known +* The content length of the file being transferred, might be -1 if the content length is not known + === TransferProgressListener Interface For more control over download progress tracking, you can implement the `TransferProgressListener` interface. @@ -172,16 +177,17 @@ The `TransferProgressListener` interface provides the following methods: * `onComplete(TransferContext, long)`: Called when the download completes with the total transferred bytes * `progressReportInterval()`: Defines how often progress updates are sent (in bytes) -The [classname]`TransferContext` provides information about the download, such as the file name, content length (if known), a reference to an owner component and Vaadin request, response and session. +The [classname]`TransferContext` objects are the same as in the fluent API. [#custom] == Custom Download Handlers For more complex download scenarios, you can create custom download handlers by implementing the `DownloadHandler` interface or extending existing implementations. -=== Implementing DownloadHandler Interface +=== Implementing DownloadHandler Interface Using Lambda Expression -You can implement the `DownloadHandler` interface to create a custom download handler or use a lambda. +The [interfacename]`DownloadHandler` is a [annotationname]`FunctionalInterface` and can be created using a lambda expression or by creating an implementation. +Creating an implementation is needed only when overriding some of the default methods from the interface, e.g. [methodname]`getUrlPostfix`, [methodname]`isAllowInert` or [methodname]`getDisabledUpdateMode`: [source,java] ---- @@ -204,14 +210,18 @@ Here's an example of a custom download handler that adds a checksum header, upda [source,java] ---- +var filename = getFileName(); +var contentType = getContentType(); LinkWithMD5Validation link = new LinkWithMD5Validation(event -> { try { - var data = loadFileFromS3(event.getFileName(), event.getContentType()); + event.setFileName(filename); + event.setContentType(contentType); + var data = loadFileFromS3(filename, contentType); MessageDigest md5 = MessageDigest.getInstance("MD5"); byte[] digest = md5.digest(data); String base64Md5 = Base64.getEncoder().encodeToString(digest); event.getResponse().setHeader("Content-MD5", base64Md5); - event.getResponse().getOutputStream().write(data); + event.getOutputStream().write(data); event.getUI().access(() -> Notification.show( "Download completed, number of downloads: " + numberOfDownloads.incrementAndGet())); @@ -246,12 +256,19 @@ This example shows how to: * Update the UI after the download completes * Store download statistics in the session -Note that `UI.access` is needed for updating the UI and also session locking if you want to access session. +The [classname]`DownloadEvent` gives the access to the following information and helper methods: +* [classname]`VaadinRequest`, [classname]`VaadinResponse`, [classname]`VaadinSession` instances +* The owner component and element of the download that you can change when download is in progress, e.g. disable the component, or get attributes or properties +* [classname]`UI` instance that you can use to call `UI.access()` for asynchronous updates +* The helper [methodname]`setFileName` method sets the file name for the download, empty name gives a default name and `null` value doesn't set anything +* The helper [methodname]`setContentType` method sets the content type for the download +* The helper [methodname]`setContentLength` method sets the content length for the download or does nothing if the `-1` value is given -The [classname]`DownloadEvent` provides information about the download, such as the file name, content length (if known), a reference to an owner component and Vaadin request, response and session. +[NOTE] +`UI.access` is needed for updating the UI and also session locking if you want to access the session. [#low-level] -== Low-Level API +== Low-Level DownloadHandler API The `DownloadHandler` API provides several low-level features for advanced use cases. @@ -260,7 +277,7 @@ The `DownloadHandler` API provides several low-level features for advanced use c The `inert` property controls whether the download should be handled when the owner component is in an inert state, e.g. when a modal dialog is opened while the owner component is on the underlined page. See the <<../advanced/server-side-modality.adoc#,Server-Side Modality>> for details. -[classname]`DownloadHandler` allows to handle download request from inert component by overriding the `allowInert()` method. +[classname]`DownloadHandler` allows to handle download request from inert component by overriding the [methodname]`isAllowInert()` method. === Disabled Update Mode @@ -271,11 +288,11 @@ The available modes are: * `ONLY_WHEN_ENABLED`: Download handling is rejected when the owner component is disabled (default) * `ALWAYS`: Download handling is allowed even when the owner component is disabled -[classname]`DownloadHandler` allows to override this mode by overriding the `getDisabledUpdateMode()` method. +[classname]`DownloadHandler` allows to override this mode by overriding the [methodname]`getDisabledUpdateMode()` method. === URL Postfix -The `getUrlPostfix()` method allows you to specify an optional URL postfix that appends application-controlled string, e.g. the logical name of the target file, to the end of the otherwise random-looking download URL. +The [methodname]`getUrlPostfix()` method allows you to specify an optional URL postfix that appends application-controlled string, e.g. the logical name of the target file, to the end of the otherwise random-looking download URL. If defined, requests that would otherwise be servable are still rejected if the postfix is missing or invalid. This is useful for: From 4aacf0b8e5d0a7e4ea78f07cd8e50e63b4759016 Mon Sep 17 00:00:00 2001 From: Mikhail Shabarov <61410877+mshabarov@users.noreply.github.com> Date: Tue, 20 May 2025 20:18:20 +0300 Subject: [PATCH 05/13] fix review comments part 3 --- articles/flow/advanced/downloads.adoc | 257 +++++++++++++++++--------- 1 file changed, 165 insertions(+), 92 deletions(-) diff --git a/articles/flow/advanced/downloads.adoc b/articles/flow/advanced/downloads.adoc index 4da85cad1f..9b7552b696 100644 --- a/articles/flow/advanced/downloads.adoc +++ b/articles/flow/advanced/downloads.adoc @@ -12,13 +12,13 @@ order: 110 The [classname]`DownloadHandler` API provides a flexible high-level abstraction to implement file and arbitrary contents downloads from server to browser in Vaadin applications. This API supports various download scenarios, from simple file downloads to complex streaming with progress tracking. -[classname]`DownloadHandler` is supported by multiple Vaadin Components where applicable. +[classname]`DownloadHandler` is supported by multiple Vaadin components where applicable. This documentation covers the main features of the `DownloadHandler` API, including: * <<#helpers,Static helper methods for common download scenarios>> -* <<#progress,Download progress tracking>> * <<#custom,Creating custom download handlers>> +* <<#progress,Download progress tracking>> * <<#low-level,Low-level API features>> [#helpers] @@ -39,6 +39,8 @@ Image logoImage = new Image(DownloadHandler.forClassResource( This method is useful for serving static resources like images, CSS, or JavaScript files that are packaged with your application. +If the resource name starts with `/`, it will then look from `/src/main/resources` without the class path prepended. + === Download A File From File System The `forFile` method allows you to serve files from the server's file system. @@ -57,7 +59,7 @@ This is the most flexible helper method as it can be used with any data source t [source,java] ---- -var downloadAttachment = new Anchor(DownloadHandler.fromInputStream((event) -> { +Anchor downloadAttachment = new Anchor(DownloadHandler.fromInputStream((event) -> { try { Attachment attachment = attachmentsRepository.findById(attachmentId); return new DownloadResponse(attachment.getData().getBinaryStream(), @@ -66,14 +68,173 @@ var downloadAttachment = new Anchor(DownloadHandler.fromInputStream((event) -> { return DownloadResponse.error(500); } }, "attachment.txt"), "Download attachment"); + +@Entity +@Table(name = "attachment") +public class Attachment { + @Lob + @Column(name = "data", nullable = false) + private Blob data; + + @Column(name = "size", nullable = false) + private Integer size; + + @Column(name = "name", nullable = false) + private String name; + + @Column(name = "mime", nullable = false) + private String mime; + + public Blob getData() { return data; } + public Integer getSize() { return size; } + public String getName() { return name; } + public String getMime() { return mime; } + + // other class fields and methods are omitted +} + +public interface AttachmentRepository extends + JpaRepository, JpaSpecificationExecutor { + Attachment findById(long id); + + // other class fields and methods are omitted +} ---- This method is particularly useful for: -* Serving content from databases or file storages +* Serving content from databases or file storage * Generating dynamic content * Streaming large files +[#custom] +== Custom Download Handlers + +For more complex download scenarios, you can create custom download handlers by implementing the `DownloadHandler` interface or extending existing implementations. + +=== Implementing DownloadHandler Interface Using Lambda Expression + +The [interfacename]`DownloadHandler` is a [annotationname]`FunctionalInterface` and can be created using a lambda expression or by creating an implementation. +Creating an implementation is needed only when overriding some of the default methods from the interface, e.g. [methodname]`getUrlPostfix`, [methodname]`isAllowInert` or [methodname]`getDisabledUpdateMode`: + +[source,java] +---- +Anchor downloadLink = new Anchor(new DownloadHandler() { + @Override + public void handleDownloadRequest(DownloadEvent event) { + // Custom download handling logic + } + + @Override + public String getUrlPostfix() { + return "custom-download.txt"; + } +}, "Download me!"); +---- + +=== Custom Download Handler Examples + +Here's an example of how a custom download handler can be written with lambda. +It adds a checksum header, updates the UI and tracks the number of downloads per session: + +[source,java] +---- +var filename = getFileName(); +var contentType = getContentType(); +LinkWithMD5Validation link = new LinkWithMD5Validation(event -> { + try { + event.setFileName(filename); + event.setContentType(contentType); + var data = loadFileFromS3(filename, contentType); + MessageDigest md5 = MessageDigest.getInstance("MD5"); + byte[] digest = md5.digest(data); + String base64Md5 = Base64.getEncoder().encodeToString(digest); + event.getResponse().setHeader("Content-MD5", base64Md5); + event.getOutputStream().write(data); + event.getUI().access(() -> Notification.show( + "Download completed, number of downloads: " + + numberOfDownloads.incrementAndGet())); + event.getSession().lock(); + try { + event.getSession().setAttribute("downloads-number-" + event.getFileName(), + numberOfDownloads.get()); + } finally { + event.getSession().unlock(); + } + } catch (NoSuchAlgorithmException | IOException e) { + event.getResponse().setStatus(500); + } +}, "Download from S3"); + +private byte[] loadFileFromS3(String fileName, String contentType) { + byte[] bytes = new byte[1024 * 1024 * 10]; // 10 MB buffer + // load from file storage by file name and content type + return bytes; +} + +private static class LinkWithMD5Validation extends Anchor { + // JS customizations in for checksum checking on the client-side +} +---- + +This example shows how to: + +* Set file meta-data with helpers in [classname]`DownloadEvent` +* Set the MD5 checksum header to the response +* Write data directly to the response output stream +* Update the UI after the download completes +* Store download statistics in the session + +The [classname]`DownloadEvent` gives the access to the following information and helper methods: +* [classname]`VaadinRequest`, [classname]`VaadinResponse`, [classname]`VaadinSession` instances +* [methodname]`getOutputStream` method to write the download content represented as a stream of bytes to response +* [methodname]`getWriter` method to write the download content represented as a formatted text to response +* The owner component and element of the download that you can change when download is in progress, e.g. disable the component, or get attributes or properties +* [classname]`UI` instance that you can use to call `UI.access()` for asynchronous updates +* The helper [methodname]`setFileName` method sets the file name for the download, empty name gives a default name and `null` value doesn't set anything +* The helper [methodname]`setContentType` method sets the content type for the download +* The helper [methodname]`setContentLength` method sets the content length for the download or does nothing if the `-1` value is given + +[NOTE] +`UI.access` is needed for updating the UI and also session locking if you want to access the session. + +[NOTE] +Methods [methodname]`getOutputStream` and [methodname]`getWriter` cannot be used simultaneously for the same response, either one or the other. + +Another example is how to generate and render a dynamic content using a [classname]`DownloadHandler`. + +[source,java] +---- +TextField name = new TextField("Input a name..."); +HtmlObject image = new HtmlObject(); +image.setType("image/svg+xml"); +image.getStyle().set("display", "block"); +Button button = new Button("Generate Image", click -> image.setData( + DownloadHandler.fromInputStream(event -> new DownloadResponse( + getImageInputStream(name), "image.svg", "image/svg+xml", -1)))); +---- + +The `HtmlObject` component is used to render the SVG image in the browser that is generated dynamically based on the input from the `TextField`. +On a button click the [classname]`DownloadHandler` is created with the [methodname]`fromInputStream` method that is set to `HtmlObject` component and that sends content to a client. +And here is an example of how to generate an svg image and create an input stream: + +[source,java] +---- +private InputStream getImageInputStream(TextField name) { + String value = name.getValue(); + if (value == null) { + value = ""; + } + String svg = "" + + "" + + "" + + value + "" + ""; + return new ByteArrayInputStream(svg.getBytes(StandardCharsets.UTF_8)); +} +---- + [#progress] == Download Progress Listeners @@ -179,94 +340,6 @@ The `TransferProgressListener` interface provides the following methods: The [classname]`TransferContext` objects are the same as in the fluent API. -[#custom] -== Custom Download Handlers - -For more complex download scenarios, you can create custom download handlers by implementing the `DownloadHandler` interface or extending existing implementations. - -=== Implementing DownloadHandler Interface Using Lambda Expression - -The [interfacename]`DownloadHandler` is a [annotationname]`FunctionalInterface` and can be created using a lambda expression or by creating an implementation. -Creating an implementation is needed only when overriding some of the default methods from the interface, e.g. [methodname]`getUrlPostfix`, [methodname]`isAllowInert` or [methodname]`getDisabledUpdateMode`: - -[source,java] ----- -Anchor downloadLink = new Anchor(new DownloadHandler() { - @Override - public void handleDownloadRequest(DownloadEvent event) { - // Custom download handling logic - } - - @Override - public String getUrlPostfix() { - return "custom-download.txt"; - } -}, "Download me!"); ----- - -=== Custom Download Handler Example - -Here's an example of a custom download handler that adds a checksum header, updates the UI and tracks the number of downloads per session: - -[source,java] ----- -var filename = getFileName(); -var contentType = getContentType(); -LinkWithMD5Validation link = new LinkWithMD5Validation(event -> { - try { - event.setFileName(filename); - event.setContentType(contentType); - var data = loadFileFromS3(filename, contentType); - MessageDigest md5 = MessageDigest.getInstance("MD5"); - byte[] digest = md5.digest(data); - String base64Md5 = Base64.getEncoder().encodeToString(digest); - event.getResponse().setHeader("Content-MD5", base64Md5); - event.getOutputStream().write(data); - event.getUI().access(() -> Notification.show( - "Download completed, number of downloads: " + - numberOfDownloads.incrementAndGet())); - event.getSession().lock(); - try { - event.getSession().setAttribute("downloads-number-" + event.getFileName(), - numberOfDownloads.get()); - } finally { - event.getSession().unlock(); - } - } catch (NoSuchAlgorithmException | IOException e) { - event.getResponse().setStatus(500); - } -}, "Download from S3"); - -private byte[] loadFileFromS3(String fileName, String contentType) { - byte[] bytes = new byte[1024 * 1024 * 10]; // 10 MB buffer - // load from file storage by file name and content type - return bytes; -} - -private static class LinkWithMD5Validation extends Anchor { - // JS customizations in for checksum checking on the client-side -} ----- - -This example shows how to: - -* Get file meta-data from [classname]`DownloadEvent` to load data from an external source (S3) -* Set the MD5 checksum header to the response -* Write data directly to the response output stream -* Update the UI after the download completes -* Store download statistics in the session - -The [classname]`DownloadEvent` gives the access to the following information and helper methods: -* [classname]`VaadinRequest`, [classname]`VaadinResponse`, [classname]`VaadinSession` instances -* The owner component and element of the download that you can change when download is in progress, e.g. disable the component, or get attributes or properties -* [classname]`UI` instance that you can use to call `UI.access()` for asynchronous updates -* The helper [methodname]`setFileName` method sets the file name for the download, empty name gives a default name and `null` value doesn't set anything -* The helper [methodname]`setContentType` method sets the content type for the download -* The helper [methodname]`setContentLength` method sets the content length for the download or does nothing if the `-1` value is given - -[NOTE] -`UI.access` is needed for updating the UI and also session locking if you want to access the session. - [#low-level] == Low-Level DownloadHandler API From 9f0fae508a7732aceb74b64c6033a4ecdb53cc0a Mon Sep 17 00:00:00 2001 From: Mikhail Shabarov <61410877+mshabarov@users.noreply.github.com> Date: Tue, 20 May 2025 20:21:21 +0300 Subject: [PATCH 06/13] fix lists --- articles/flow/advanced/downloads.adoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/articles/flow/advanced/downloads.adoc b/articles/flow/advanced/downloads.adoc index 9b7552b696..498d30e7b7 100644 --- a/articles/flow/advanced/downloads.adoc +++ b/articles/flow/advanced/downloads.adoc @@ -186,6 +186,7 @@ This example shows how to: * Store download statistics in the session The [classname]`DownloadEvent` gives the access to the following information and helper methods: + * [classname]`VaadinRequest`, [classname]`VaadinResponse`, [classname]`VaadinSession` instances * [methodname]`getOutputStream` method to write the download content represented as a stream of bytes to response * [methodname]`getWriter` method to write the download content represented as a formatted text to response @@ -280,6 +281,7 @@ The fluent API provides the following methods: * `whenComplete(Consumer)`: Called when the download completes successfully or with a failure These methods have overloads that accept also the [classname]`TransferContext` object that gives more information and references: + * [classname]`VaadinRequest`, [classname]`VaadinResponse`, [classname]`VaadinSession` instances * The owner component and element of the data transfer that you can change when transfer is in progress, e.g. disable the component, or get attributes or properties * [classname]`UI` instance that you can use to call `UI.access()` for asynchronous updates in threads From b3e8edbcbc9dfa331eebd03ff7390165f09feb8e Mon Sep 17 00:00:00 2001 From: Mikhail Shabarov <61410877+mshabarov@users.noreply.github.com> Date: Thu, 22 May 2025 15:07:04 +0300 Subject: [PATCH 07/13] add more advanced examples, simplify existing --- articles/flow/advanced/downloads.adoc | 125 ++++++++++-------- .../download/InputStreamDownloadView.java | 73 ++++++++++ 2 files changed, 142 insertions(+), 56 deletions(-) create mode 100644 src/main/java/com/vaadin/demo/flow/advanced/download/InputStreamDownloadView.java diff --git a/articles/flow/advanced/downloads.adoc b/articles/flow/advanced/downloads.adoc index 498d30e7b7..dfef54c211 100644 --- a/articles/flow/advanced/downloads.adoc +++ b/articles/flow/advanced/downloads.adoc @@ -59,46 +59,7 @@ This is the most flexible helper method as it can be used with any data source t [source,java] ---- -Anchor downloadAttachment = new Anchor(DownloadHandler.fromInputStream((event) -> { - try { - Attachment attachment = attachmentsRepository.findById(attachmentId); - return new DownloadResponse(attachment.getData().getBinaryStream(), - attachment.getName(), attachment.getMime(), attachment.getSize()); - } catch (Exception e) { - return DownloadResponse.error(500); - } -}, "attachment.txt"), "Download attachment"); - -@Entity -@Table(name = "attachment") -public class Attachment { - @Lob - @Column(name = "data", nullable = false) - private Blob data; - - @Column(name = "size", nullable = false) - private Integer size; - - @Column(name = "name", nullable = false) - private String name; - - @Column(name = "mime", nullable = false) - private String mime; - - public Blob getData() { return data; } - public Integer getSize() { return size; } - public String getName() { return name; } - public String getMime() { return mime; } - - // other class fields and methods are omitted -} - -public interface AttachmentRepository extends - JpaRepository, JpaSpecificationExecutor { - Attachment findById(long id); - - // other class fields and methods are omitted -} +include::{root}/src/main/java/com/vaadin/demo/flow/advanced/download/InputStreamDownloadView.java[render,tags=snippet,indent=0] ---- This method is particularly useful for: @@ -114,7 +75,15 @@ For more complex download scenarios, you can create custom download handlers by === Implementing DownloadHandler Interface Using Lambda Expression -The [interfacename]`DownloadHandler` is a [annotationname]`FunctionalInterface` and can be created using a lambda expression or by creating an implementation. +The [interfacename]`DownloadHandler` is a [annotationname]`FunctionalInterface` and can be created using a lambda expression or by creating an implementation: + +[source,java] +---- +Anchor downloadLink = new Anchor(event -> { + // Custom download handling logic +}, "Download me!"); +---- + Creating an implementation is needed only when overriding some of the default methods from the interface, e.g. [methodname]`getUrlPostfix`, [methodname]`isAllowInert` or [methodname]`getDisabledUpdateMode`: [source,java] @@ -135,21 +104,19 @@ Anchor downloadLink = new Anchor(new DownloadHandler() { === Custom Download Handler Examples Here's an example of how a custom download handler can be written with lambda. -It adds a checksum header, updates the UI and tracks the number of downloads per session: +It adds a header to the response, write data to `OutputStream`, updates the UI, and tracks the number of downloads per session: [source,java] ---- -var filename = getFileName(); -var contentType = getContentType(); -LinkWithMD5Validation link = new LinkWithMD5Validation(event -> { +String filename = getFileName(); +String contentType = getContentType(); +AtomicInteger numberOfDownloads = new AtomicInteger(0); +Anchor link = new Anchor(event -> { try { event.setFileName(filename); event.setContentType(contentType); - var data = loadFileFromS3(filename, contentType); - MessageDigest md5 = MessageDigest.getInstance("MD5"); - byte[] digest = md5.digest(data); - String base64Md5 = Base64.getEncoder().encodeToString(digest); - event.getResponse().setHeader("Content-MD5", base64Md5); + byte[] data = loadFileFromS3(filename, contentType); + event.getResponse().setHeader("Cache-Control", "public, max-age=3600"); event.getOutputStream().write(data); event.getUI().access(() -> Notification.show( "Download completed, number of downloads: " + @@ -171,16 +138,12 @@ private byte[] loadFileFromS3(String fileName, String contentType) { // load from file storage by file name and content type return bytes; } - -private static class LinkWithMD5Validation extends Anchor { - // JS customizations in for checksum checking on the client-side -} ---- This example shows how to: * Set file meta-data with helpers in [classname]`DownloadEvent` -* Set the MD5 checksum header to the response +* Set a header to the response * Write data directly to the response output stream * Update the UI after the download completes * Store download statistics in the session @@ -217,7 +180,7 @@ Button button = new Button("Generate Image", click -> image.setData( The `HtmlObject` component is used to render the SVG image in the browser that is generated dynamically based on the input from the `TextField`. On a button click the [classname]`DownloadHandler` is created with the [methodname]`fromInputStream` method that is set to `HtmlObject` component and that sends content to a client. -And here is an example of how to generate an svg image and create an input stream: +And here is an example of how to generate a svg image and create an input stream: [source,java] ---- @@ -236,6 +199,49 @@ private InputStream getImageInputStream(TextField name) { } ---- +When [classname]`DownloadHandler` is used with `Anchor` component, the data is downloaded as a file. +This can be changed to render the data in the browser by using [methodname]`inline()` method: + +[source,java] +---- +Anchor downloadLink = new Anchor(DownloadHandler.fromFile( + new File("/path/to/document.pdf"), "report.pdf").inline(), "Download Report"); +---- + +The second parameter of the `fromFile` method is the file name that will be used for download. +This name is also used as a URL postfix. + +Finally, the [classname]`DownloadHandler` can be created from an abstract base class [classname]`AbstractDownloadHandler`: + +[source,java] +---- +public class MyDownloadHandler extends AbstractDownloadHandler { + @Override + public void handleDownloadRequest(DownloadEvent downloadEvent) { + byte[] data; + // load data from backend... + try (OutputStream outputStream = downloadEvent.getOutputStream(); + ByteArrayInputStream inputStream = new ByteArrayInputStream(data)) { + TransferUtil.transfer(inputStream, outputStream, + getTransferContext(downloadEvent), getListeners()); + } catch (IOException ioe) { + downloadEvent.getResponse().setStatus( + HttpStatusCode.INTERNAL_SERVER_ERROR.getCode()); + notifyError(downloadEvent, ioe); + } + } +} +---- + +This example shows how to: + +* Extend the [classname]`AbstractDownloadHandler` class +* Override the [methodname]`handleDownloadRequest` method to implement custom download handling logic +* Use the [classname]`TransferUtil` class to transfer data from an `InputStream` to an `OutputStream`. +This helper method also fires progress events to the listeners so no need to implement this logic manually, see also <<#progress,Download progress tracking>> +* Notify progress listeners about errors by calling the [methodname]`notifyError` method + + [#progress] == Download Progress Listeners @@ -369,12 +375,19 @@ The available modes are: The [methodname]`getUrlPostfix()` method allows you to specify an optional URL postfix that appends application-controlled string, e.g. the logical name of the target file, to the end of the otherwise random-looking download URL. If defined, requests that would otherwise be servable are still rejected if the postfix is missing or invalid. +[classname]`DownloadHandler` factory methods have overloads that accept the postfix as a parameter. This is useful for: * Providing a meaningful filename into the download handler callback * Making the download request URL look more user-friendly as otherwise it is a random-looking URL +The request URL looks like when the postfix is set: +`/VAADIN/dynamic/resource/0/5298ee8b-9686-4a5a-ae1d-b38c62767d6a/meeting-notes.txt`. + +By default, the postfix is not set and the request URL looks like: +`/VAADIN/dynamic/resource/0/5298ee8b-9686-4a5a-ae1d-b38c62767d6a`. + [source,java] ---- Anchor downloadLink = new Anchor(new DownloadHandler() { diff --git a/src/main/java/com/vaadin/demo/flow/advanced/download/InputStreamDownloadView.java b/src/main/java/com/vaadin/demo/flow/advanced/download/InputStreamDownloadView.java new file mode 100644 index 0000000000..8bc44eea48 --- /dev/null +++ b/src/main/java/com/vaadin/demo/flow/advanced/download/InputStreamDownloadView.java @@ -0,0 +1,73 @@ +package com.vaadin.demo.flow.advanced.download; + +import java.sql.Blob; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +import com.vaadin.flow.component.html.Anchor; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.streams.DownloadHandler; +import com.vaadin.flow.server.streams.DownloadResponse; + +@Route("/download-attachment") +public class InputStreamDownloadView extends Div { + public InputStreamDownloadView(AttachmentRepository attachmentsRepository) { + long attachmentId = 1L; // Example attachment ID + // tag::snippet[] + Anchor downloadAttachment = new Anchor( + DownloadHandler.fromInputStream((event) -> { + try { + Attachment attachment = attachmentsRepository.findById(attachmentId); + return new DownloadResponse(attachment.getData().getBinaryStream(), + attachment.getName(), attachment.getMime(), attachment.getSize()); + } catch (Exception e) { + return DownloadResponse.error(500); + } + }), "Download attachment"); + // end::snippet[] + add(downloadAttachment); + } + + @Entity + @Table(name = "attachment") + public static class Attachment { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Lob + @Column(name = "data", nullable = false) + private Blob data; + + @Column(name = "size", nullable = false) + private Integer size; + + @Column(name = "name", nullable = false) + private String name; + + @Column(name = "mime", nullable = false) + private String mime; + + public Blob getData() { return data; } + public Integer getSize() { return size; } + public String getName() { return name; } + public String getMime() { return mime; } + + // other class fields and methods are omitted + } + + public interface AttachmentRepository extends + JpaRepository, JpaSpecificationExecutor { + Attachment findById(long id); + // other class fields and methods are omitted + } +} From 58fb1268f8e675080beeae0ed7e917a6e269f755 Mon Sep 17 00:00:00 2001 From: Mikhail Shabarov <61410877+mshabarov@users.noreply.github.com> Date: Thu, 22 May 2025 15:32:03 +0300 Subject: [PATCH 08/13] remove CSS and JS --- articles/flow/advanced/downloads.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/articles/flow/advanced/downloads.adoc b/articles/flow/advanced/downloads.adoc index dfef54c211..542052acb1 100644 --- a/articles/flow/advanced/downloads.adoc +++ b/articles/flow/advanced/downloads.adoc @@ -37,7 +37,7 @@ Image logoImage = new Image(DownloadHandler.forClassResource( MainView.class, "vaadin.jpeg"), "Vaadin Logo"); ---- -This method is useful for serving static resources like images, CSS, or JavaScript files that are packaged with your application. +This method is useful for serving static resources like images, templates, fonts, and other types of files that are packaged with your application. If the resource name starts with `/`, it will then look from `/src/main/resources` without the class path prepended. From 6e2a480dc8233605ac8d3f7db184940001b47514 Mon Sep 17 00:00:00 2001 From: Mikhail Shabarov <61410877+mshabarov@users.noreply.github.com> Date: Mon, 26 May 2025 13:31:52 +0300 Subject: [PATCH 09/13] Update articles/flow/advanced/downloads.adoc Co-authored-by: caalador --- articles/flow/advanced/downloads.adoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/articles/flow/advanced/downloads.adoc b/articles/flow/advanced/downloads.adoc index 542052acb1..75a6e33d22 100644 --- a/articles/flow/advanced/downloads.adoc +++ b/articles/flow/advanced/downloads.adoc @@ -52,6 +52,10 @@ Anchor download = new Anchor(DownloadHandler.forFile(new File("/path/to/terms-an This method is useful for serving files that are stored on the server's file system. +[NOTE] +When using a download handler extending [classname]`AbstractDownloadHandler`, pre-built or custom [classname]`DownloadHandler`, if not specified as inline the anchor will get the `download` attribute set. +Inline will only be taken into account on setting the handler and not if set after creating anchor. + === Download Content from InputStream The `fromInputStream` method allows you to serve content from any [classname]`InputStream`. From dd1de916b4b4742c77d69765f40cdf06aac62d49 Mon Sep 17 00:00:00 2001 From: Mikhail Shabarov <61410877+mshabarov@users.noreply.github.com> Date: Mon, 2 Jun 2025 16:03:04 +0300 Subject: [PATCH 10/13] update example --- articles/flow/advanced/downloads.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/articles/flow/advanced/downloads.adoc b/articles/flow/advanced/downloads.adoc index 75a6e33d22..a9a4e5ae7b 100644 --- a/articles/flow/advanced/downloads.adoc +++ b/articles/flow/advanced/downloads.adoc @@ -127,12 +127,12 @@ Anchor link = new Anchor(event -> { numberOfDownloads.incrementAndGet())); event.getSession().lock(); try { - event.getSession().setAttribute("downloads-number-" + event.getFileName(), + event.getSession().setAttribute("downloads-number-" + fileName, numberOfDownloads.get()); } finally { event.getSession().unlock(); } - } catch (NoSuchAlgorithmException | IOException e) { + } catch (IOException e) { event.getResponse().setStatus(500); } }, "Download from S3"); From 388a267fa38ee40ea4b5bd7bc38c2e875347f180 Mon Sep 17 00:00:00 2001 From: Mikhail Shabarov <61410877+mshabarov@users.noreply.github.com> Date: Mon, 2 Jun 2025 16:34:49 +0300 Subject: [PATCH 11/13] update example --- articles/flow/advanced/downloads.adoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/articles/flow/advanced/downloads.adoc b/articles/flow/advanced/downloads.adoc index a9a4e5ae7b..cb2fa46a34 100644 --- a/articles/flow/advanced/downloads.adoc +++ b/articles/flow/advanced/downloads.adoc @@ -122,6 +122,8 @@ Anchor link = new Anchor(event -> { byte[] data = loadFileFromS3(filename, contentType); event.getResponse().setHeader("Cache-Control", "public, max-age=3600"); event.getOutputStream().write(data); + // Remember to enable @Push + // Use event.getUI().push() if push is manual event.getUI().access(() -> Notification.show( "Download completed, number of downloads: " + numberOfDownloads.incrementAndGet())); From 0e1ec273df92cd5f802a3fb20941859a2fa2a223 Mon Sep 17 00:00:00 2001 From: Mikhail Shabarov <61410877+mshabarov@users.noreply.github.com> Date: Tue, 17 Jun 2025 14:28:44 +0300 Subject: [PATCH 12/13] add attachment type --- articles/flow/advanced/downloads.adoc | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/articles/flow/advanced/downloads.adoc b/articles/flow/advanced/downloads.adoc index cb2fa46a34..ef03502f74 100644 --- a/articles/flow/advanced/downloads.adoc +++ b/articles/flow/advanced/downloads.adoc @@ -52,9 +52,13 @@ Anchor download = new Anchor(DownloadHandler.forFile(new File("/path/to/terms-an This method is useful for serving files that are stored on the server's file system. -[NOTE] -When using a download handler extending [classname]`AbstractDownloadHandler`, pre-built or custom [classname]`DownloadHandler`, if not specified as inline the anchor will get the `download` attribute set. -Inline will only be taken into account on setting the handler and not if set after creating anchor. +The [classname]`Anchor` component sets the `download` attribute by default and download handlers extending [classname]`AbstractDownloadHandler` also set the `Content-Disposition` to `attachment`. +If the content should be inlined to the page, this have to be set explicitly by calling the [methodname]`inline()` method on the `DownloadHandler` instance and using `AttachmentType.INLINE`: + +[source,java] +---- +Anchor download = new Anchor(DownloadHandler.forFile(new File("/path/to/terms-and-conditions.md")).inline(), AttachmentType.INLINE, "View Terms and Conditions"); +---- === Download Content from InputStream From 42e4c54b90d118383049e9150a46c221f96abb13 Mon Sep 17 00:00:00 2001 From: Mikhail Shabarov <61410877+mshabarov@users.noreply.github.com> Date: Tue, 17 Jun 2025 15:04:12 +0300 Subject: [PATCH 13/13] place the DownloadEvent and lambda example first --- articles/flow/advanced/downloads.adoc | 77 ++++++++++++++++----------- 1 file changed, 45 insertions(+), 32 deletions(-) diff --git a/articles/flow/advanced/downloads.adoc b/articles/flow/advanced/downloads.adoc index ef03502f74..5047963b69 100644 --- a/articles/flow/advanced/downloads.adoc +++ b/articles/flow/advanced/downloads.adoc @@ -16,19 +16,59 @@ This API supports various download scenarios, from simple file downloads to comp This documentation covers the main features of the `DownloadHandler` API, including: -* <<#helpers,Static helper methods for common download scenarios>> +* <<#common,Common download scenarios>> * <<#custom,Creating custom download handlers>> * <<#progress,Download progress tracking>> * <<#low-level,Low-level API features>> -[#helpers] +[#common] == Common Download Scenarios -The `DownloadHandler` API provides several static helper methods to simplify common download scenarios. +The `DownloadHandler` can be in a form of lambda, where you can control the data transfer and UI updates thanks to [classname]`DownloadEvent` API, or can use several provided static helper methods to simplify common download scenarios. -=== Download a Classpath Resource +=== Using DownloadEvent And Lambda Expression -The `forClassResource` method allows you to serve resources from the classpath. +The [interfacename]`DownloadHandler` is a [annotationname]`FunctionalInterface` and can be created using a lambda expression: + +[source,java] +---- +Anchor downloadLink = new Anchor((DownloadEvent event) -> { + event.setFileName("readme.md"); + var anchor = event.getOwningComponent(); + event.getResponse().setHeader("Cache-Control", "public, max-age=3600"); + try (OutputStream outputStream = event.getOutputStream()) { + // Write data to the output stream + } + event.getUI().access(() -> { /* UI updates */}); +}, "Download me!"); +---- + +Using [classname]`DownloadEvent` and lambda is particularly useful for: + +* Writing arbitrary data to the response output stream +* Setting file meta-data like file name, content type, and content length +* Update UI or owner component during the download +* Having access to the [classname]`VaadinRequest`, [classname]`VaadinResponse`, and [classname]`VaadinSession` instances + +=== Download Content from InputStream + +The `fromInputStream` method allows you to serve content from any [classname]`InputStream`. +This is the most flexible helper method as it can be used with any data source that can provide an `InputStream`. + +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/flow/advanced/download/InputStreamDownloadView.java[render,tags=snippet,indent=0] +---- + +This method is particularly useful for: + +* Serving content from databases or file storage +* Generating dynamic content +* Streaming large files + +=== Render Or Download Static Resource + +The [methodname]`forClassResource` and [methodname]`forServletResource` methods allows you to serve resources from the classpath or servlet context. For instance, for the file [filename]`src/main/resources/com/example/ui/vaadin.jpeg` and class [classname]`com.example.ui.MainView` the code would be: [source,java] @@ -60,38 +100,11 @@ If the content should be inlined to the page, this have to be set explicitly by Anchor download = new Anchor(DownloadHandler.forFile(new File("/path/to/terms-and-conditions.md")).inline(), AttachmentType.INLINE, "View Terms and Conditions"); ---- -=== Download Content from InputStream - -The `fromInputStream` method allows you to serve content from any [classname]`InputStream`. -This is the most flexible helper method as it can be used with any data source that can provide an `InputStream`. - -[source,java] ----- -include::{root}/src/main/java/com/vaadin/demo/flow/advanced/download/InputStreamDownloadView.java[render,tags=snippet,indent=0] ----- - -This method is particularly useful for: - -* Serving content from databases or file storage -* Generating dynamic content -* Streaming large files - [#custom] == Custom Download Handlers For more complex download scenarios, you can create custom download handlers by implementing the `DownloadHandler` interface or extending existing implementations. -=== Implementing DownloadHandler Interface Using Lambda Expression - -The [interfacename]`DownloadHandler` is a [annotationname]`FunctionalInterface` and can be created using a lambda expression or by creating an implementation: - -[source,java] ----- -Anchor downloadLink = new Anchor(event -> { - // Custom download handling logic -}, "Download me!"); ----- - Creating an implementation is needed only when overriding some of the default methods from the interface, e.g. [methodname]`getUrlPostfix`, [methodname]`isAllowInert` or [methodname]`getDisabledUpdateMode`: [source,java]