diff --git a/articles/flow/advanced/downloads.adoc b/articles/flow/advanced/downloads.adoc
new file mode 100644
index 0000000000..5047963b69
--- /dev/null
+++ b/articles/flow/advanced/downloads.adoc
@@ -0,0 +1,437 @@
+---
+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:
+
+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.
+
+This documentation covers the main features of the `DownloadHandler` API, including:
+
+* <<#common,Common download scenarios>>
+* <<#custom,Creating custom download handlers>>
+* <<#progress,Download progress tracking>>
+* <<#low-level,Low-level API features>>
+
+[#common]
+== 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.
+
+=== Using DownloadEvent And Lambda Expression
+
+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]
+----
+Image logoImage = new Image(DownloadHandler.forClassResource(
+ MainView.class, "vaadin.jpeg"), "Vaadin Logo");
+----
+
+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.
+
+=== 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.
+
+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");
+----
+
+[#custom]
+== Custom Download Handlers
+
+For more complex download scenarios, you can create custom download handlers by implementing the `DownloadHandler` interface or extending existing implementations.
+
+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 header to the response, write data to `OutputStream`, updates the UI, and tracks the number of downloads per session:
+
+[source,java]
+----
+String filename = getFileName();
+String contentType = getContentType();
+AtomicInteger numberOfDownloads = new AtomicInteger(0);
+Anchor link = new Anchor(event -> {
+ try {
+ event.setFileName(filename);
+ event.setContentType(contentType);
+ 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()));
+ event.getSession().lock();
+ try {
+ event.getSession().setAttribute("downloads-number-" + fileName,
+ numberOfDownloads.get());
+ } finally {
+ event.getSession().unlock();
+ }
+ } catch (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;
+}
+----
+
+This example shows how to:
+
+* Set file meta-data with helpers in [classname]`DownloadEvent`
+* 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
+
+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 a svg image and create an input stream:
+
+[source,java]
+----
+private InputStream getImageInputStream(TextField name) {
+ String value = name.getValue();
+ if (value == null) {
+ value = "";
+ }
+ String svg = ""
+ + "";
+ return new ByteArrayInputStream(svg.getBytes(StandardCharsets.UTF_8));
+}
+----
+
+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
+
+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 be 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
+* `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.
+
+[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` objects are the same as in the fluent API.
+
+[#low-level]
+== Low-Level DownloadHandler 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 [methodname]`isAllowInert()` 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 [methodname]`getDisabledUpdateMode()` method.
+
+=== URL Postfix
+
+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() {
+ @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");
+----
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 = ""
- + "";
- 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 = ""
- + "";
- return new ByteArrayInputStream(svg.getBytes(StandardCharsets.UTF_8));
-}
-----
-
-
-[discussion-id]`DF78C6F1-4DFC-4F65-A0D4-29CCB2CFEDD5`
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
+ }
+}