-
Notifications
You must be signed in to change notification settings - Fork 210
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 all commits
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`. | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would not list any components specifically and only mention that Vaadin Components support DownloadHandler where applicable. |
||||||||
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
|
||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
|
||||||||
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 | ||||||||
Comment on lines
+28
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These should be direct links to the respective parts of the document |
||||||||
|
||||||||
== 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
|
||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I think it doesn't want A Classpath , but a Classpath instead. |
||||||||
|
||||||||
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. | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could have the note that if you start the resource name with |
||||||||
|
||||||||
=== 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) -> { | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
As no other place uses |
||||||||
try { | ||||||||
Attachment attachment = attachmentsRepository.findById(attachmentId); | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The documentation should be copyable, and this introduces a non existent attachmentsRepository and a non known Attrachment object that is a cause for confusion when having low knowledge about this. Probable questions would most likely include at least: Perhaps it would be better to have something else as a sample for input stream instead. Perhaps to show just use a String -> InputStream
|
||||||||
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
|
||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
* 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
|
||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TransferProgressListener example for Customized DownloadHandler as the reading from stream you should do is not apparent from the text. |
||||||||
|
||||||||
== Custom Download Handlers | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would probably be better before progress listeners. |
||||||||
|
||||||||
For more complex download scenarios, you can create custom download handlers by implementing the `DownloadHandler` interface or extending existing implementations. | ||||||||
|
||||||||
=== Implementing DownloadHandler Interface | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
|
||||||||
You can implement the `DownloadHandler` interface to create a custom download handler or use a lambda. | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The [interfacename] |
||||||||
|
||||||||
[source,java] | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Samples for using a lambda expression and also for writing to the response using both |
||||||||
---- | ||||||||
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 -> { | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
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); | ||||||||
Comment on lines
+215
to
+218
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This example with calculating MD5 checksum and checking it on the client isn't perhaps a realistic case. I'd think of a better case for when setting a header is useful in download handler, feel free to share a better example if you have any. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looking at this gives the feeling that we should perhaps have a |
||||||||
event.getResponse().getOutputStream().write(data); | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
We should use the DownloadEvent api and not the response api. |
||||||||
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); | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should probably have a |
||||||||
} | ||||||||
}, "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
|
||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
|
||||||||
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 | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
|
||||||||
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. | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
|
||||||||
=== 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. | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
|
||||||||
=== 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. | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
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] | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could have a sample with the new DownloadHandler for the exact same case to see difference between the approaches. |
||||||||
---- | ||||||||
@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); | ||||||||
} | ||||||||
} | ||||||||
---- | ||||||||
Comment on lines
+315
to
+341
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is from the old article, I'd keep it just in case if someone is interested in a low level servlet way of downloading a content. |
||||||||
|
||||||||
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()); | ||||||||
---- |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't push people to have an extra servlet for dynamic content generation.
The custom URL feels like it shouldn't be in the downloads document (even if it was from the old documentation)
At the minimum this should have a link to
== Using Custom Servlet and Request Parameters