-
Notifications
You must be signed in to change notification settings - Fork 209
feat: Add docs for DownloadHandler #4309
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: latest
Are you sure you want to change the base?
Changes from 3 commits
ced77ca
8312221
3949d9b
8b78601
33b2e15
51d026d
8e6ed8a
3a73712
4aacf0b
9f0fae5
b3e8edb
58fb126
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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`. | ||
caalador marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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. | ||
Check warning on line 26 in articles/flow/advanced/downloads.adoc
|
||
caalador marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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 | ||
caalador marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
== Common Download Scenarios | ||
|
||
The `DownloadHandler` API provides several static helper methods to simplify common download scenarios. | ||
|
||
=== Download A Classpath Resource | ||
Check warning on line 39 in articles/flow/advanced/downloads.adoc
|
||
caalador marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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. | ||
caalador marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
=== 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) -> { | ||
caalador marked this conversation as resolved.
Show resolved
Hide resolved
|
||
try { | ||
Attachment attachment = attachmentsRepository.findById(attachmentId); | ||
caalador marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
Check failure on line 83 in articles/flow/advanced/downloads.adoc
|
||
caalador marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* 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 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<Long, Long>)`: Called during the download with transferred and total bytes | ||
* `whenComplete(Consumer<Boolean>)`: 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. | ||
Check warning on line 181 in articles/flow/advanced/downloads.adoc
|
||
caalador marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
== Custom Download Handlers | ||
caalador marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
For more complex download scenarios, you can create custom download handlers by implementing the `DownloadHandler` interface or extending existing implementations. | ||
|
||
=== Implementing DownloadHandler Interface | ||
caalador marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
You can implement the `DownloadHandler` interface to create a custom download handler or use a lambda. | ||
mshabarov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
[source,java] | ||
caalador marked this conversation as resolved.
Show resolved
Hide resolved
|
||
---- | ||
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 -> { | ||
caalador marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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); | ||
mshabarov marked this conversation as resolved.
Show resolved
Hide resolved
caalador marked this conversation as resolved.
Show resolved
Hide resolved
|
||
event.getResponse().getOutputStream().write(data); | ||
caalador marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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); | ||
caalador marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
}, "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 <a> 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. | ||
Check warning on line 254 in articles/flow/advanced/downloads.adoc
|
||
caalador marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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 | ||
mshabarov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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. | ||
caalador marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
=== 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. | ||
caalador marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
=== 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. | ||
caalador marked this conversation as resolved.
Show resolved
Hide resolved
|
||
If defined, requests that would otherwise be servable are still rejected if the postfix is missing or invalid. | ||
Check failure on line 283 in articles/flow/advanced/downloads.adoc
|
||
|
||
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] | ||
caalador marked this conversation as resolved.
Show resolved
Hide resolved
|
||
---- | ||
@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 = "<?xml version='1.0' encoding='UTF-8' standalone='no'?>" | ||
+ "<svg xmlns='http://www.w3.org/2000/svg' " | ||
+ "xmlns:xlink='http://www.w3.org/1999/xlink'>" | ||
+ "<rect x='10' y='10' height='100' width='100' " | ||
+ "style=' fill: #90C3D4'/><text x='30' y='30' fill='red'>" | ||
+ name + "</text>" + "</svg>"; | ||
resp.getWriter().write(svg); | ||
} | ||
} | ||
---- | ||
mshabarov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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()); | ||
---- |
Uh oh!
There was an error while loading. Please reload this page.