Skip to content

Commit 7393e00

Browse files
committed
feat: add DownloadHandler
Add DownloadHandler interface and factory method for use with a File. Closes #21229
1 parent 8b5562c commit 7393e00

File tree

8 files changed

+499
-13
lines changed

8 files changed

+499
-13
lines changed
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
/*
2+
* Copyright 2000-2025 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
17+
package com.vaadin.flow.server;
18+
19+
import java.io.IOException;
20+
import java.io.OutputStream;
21+
import java.io.PrintWriter;
22+
import java.io.Serializable;
23+
import java.util.Optional;
24+
25+
import com.vaadin.flow.component.Component;
26+
import com.vaadin.flow.dom.Element;
27+
28+
/**
29+
* Class containing data on requested client download.
30+
*
31+
* @since 24.8
32+
*/
33+
public class DownloadEvent implements Serializable {
34+
35+
private VaadinRequest request;
36+
private VaadinResponse response;
37+
private VaadinSession session;
38+
39+
private String fileName;
40+
private String contentType;
41+
private String fileSize;
42+
43+
private Component owningComponent;
44+
45+
/**
46+
* Create a new download event with required data.
47+
*
48+
* @param request
49+
* current request
50+
* @param response
51+
* current response to write response data to
52+
* @param session
53+
* current session
54+
* @param fileName
55+
* defined download file name
56+
*/
57+
public DownloadEvent(VaadinRequest request, VaadinResponse response,
58+
VaadinSession session, String fileName) {
59+
this.request = request;
60+
this.response = response;
61+
this.session = session;
62+
this.fileName = fileName;
63+
}
64+
65+
/**
66+
* Set the owning component for the download event from the element instance
67+
* if a component is available.
68+
*
69+
* @param owningElement
70+
* owning element for the event
71+
* @return this Event instance
72+
*/
73+
public DownloadEvent withOwningComponent(Element owningElement) {
74+
if (owningElement != null) {
75+
Optional<Component> component = owningElement.getComponent();
76+
component.ifPresent(value -> owningComponent = value);
77+
}
78+
return this;
79+
}
80+
81+
/**
82+
* Set the owning component for the download event.
83+
*
84+
* @param owningComponent
85+
* owning component for the event
86+
* @return this Event instance
87+
*/
88+
public DownloadEvent withOwningComponent(Component owningComponent) {
89+
this.owningComponent = owningComponent;
90+
return this;
91+
}
92+
93+
/**
94+
* Set the DownloadEvent content type.
95+
*
96+
* @param contentType
97+
* content type of the event content
98+
* @return this Event instance
99+
*/
100+
public DownloadEvent withContentType(String contentType) {
101+
this.contentType = contentType;
102+
return this;
103+
}
104+
105+
/**
106+
* Set the file size for the Download event.
107+
*
108+
* @param fileSize
109+
* size of the download
110+
* @return this Event instance
111+
*/
112+
public DownloadEvent withFileSize(String fileSize) {
113+
this.fileSize = fileSize;
114+
return this;
115+
}
116+
117+
/**
118+
* Returns a <code>OutputStream</code> for writing binary data in the
119+
* response.
120+
* <p>
121+
* Either this method or getWriter() may be called to write the response,
122+
* not both.
123+
*
124+
* @return a <code>OutputStream</code> for writing binary data
125+
* @throws IOException
126+
* if an input or output exception occurred
127+
*/
128+
public OutputStream getOutputStream() throws IOException {
129+
return response.getOutputStream();
130+
}
131+
132+
/**
133+
* Returns a <code>PrintWriter</code> object that can send character text to
134+
* the client. The PrintWriter uses the character encoding defined using
135+
* setContentType.
136+
* <p>
137+
* Either this method or getOutputStream() may be called to write the
138+
* response, not both.
139+
*
140+
* @return a <code>PrintWriter</code> for writing character text
141+
* @throws IOException
142+
* if an input or output exception occurred
143+
*/
144+
public PrintWriter getWriter() throws IOException {
145+
return response.getWriter();
146+
}
147+
148+
/**
149+
* Get {@link VaadinRequest} for download event.
150+
*
151+
* @return vaadin request
152+
*/
153+
public VaadinRequest getRequest() {
154+
return request;
155+
}
156+
157+
/**
158+
* Get {@link VaadinResponse} for download event.
159+
*
160+
* @return vaadin response
161+
*/
162+
public VaadinResponse getResponse() {
163+
return response;
164+
}
165+
166+
/**
167+
* Get {@link VaadinSession} for download event.
168+
*
169+
* @return vaadin session
170+
*/
171+
public VaadinSession getSession() {
172+
return session;
173+
}
174+
175+
/**
176+
* Get the set file name.
177+
*
178+
* @return file name
179+
*/
180+
public String getFileName() {
181+
return fileName;
182+
}
183+
184+
/**
185+
* Get the content type for the data to download.
186+
*
187+
* @return set content type
188+
*/
189+
public String getContentType() {
190+
return contentType;
191+
}
192+
193+
/**
194+
* Get the file size for the download event if defined
195+
*
196+
* @return size if defined
197+
*/
198+
public String getFileSize() {
199+
return fileSize;
200+
}
201+
202+
/**
203+
* Get owner {@link Component} for this event.
204+
*
205+
* @return owning component or null in none defined
206+
*/
207+
public Component getComponent() {
208+
return owningComponent;
209+
}
210+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2000-2025 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
17+
package com.vaadin.flow.server;
18+
19+
import java.io.File;
20+
import java.util.Optional;
21+
22+
import com.vaadin.flow.component.Component;
23+
import com.vaadin.flow.dom.Element;
24+
import com.vaadin.flow.server.streams.FileDownloadHandler;
25+
26+
/**
27+
* Interface for handling download of data from the server to the client.
28+
*
29+
* @since 24.8
30+
*/
31+
@FunctionalInterface
32+
public interface DownloadHandler extends ElementRequestHandler {
33+
34+
/**
35+
* Method that is called when the client wants to download from the url
36+
* stored for this specific handler registration.
37+
*
38+
* @param event
39+
* download event containing the necessary data for writing the
40+
* response
41+
*/
42+
void handleDownloadRequest(DownloadEvent event);
43+
44+
default void handleRequest(VaadinRequest request, VaadinResponse response,
45+
VaadinSession session, Element owner) {
46+
String fileName = getFileName();
47+
if (fileName == null) {
48+
throw new IllegalStateException(
49+
"Download can not be without a defined file name");
50+
}
51+
52+
DownloadEvent event = new DownloadEvent(request, response, session,
53+
fileName);
54+
event.withOwningComponent(owner)
55+
.withContentType(Optional
56+
.ofNullable(response.getService().getMimeType(fileName))
57+
.orElse("application/octet-stream"));
58+
59+
handleDownloadRequest(event);
60+
}
61+
62+
/**
63+
* Get file name for download.
64+
* <p>
65+
* Default value is {@link #getUrlPostfix}
66+
*
67+
* @return file name for this download handler
68+
*/
69+
default String getFileName() {
70+
return getUrlPostfix() == null ? "" : getUrlPostfix();
71+
}
72+
73+
/**
74+
* Get a download handler for serving given {@link File}.
75+
*
76+
* @param file
77+
* file to server for download
78+
* @return DownloadHandler instance for file
79+
*/
80+
static DownloadHandler forFile(File file) {
81+
return new FileDownloadHandler(file);
82+
}
83+
}

flow-server/src/main/java/com/vaadin/flow/server/StreamResourceRegistry.java

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,8 @@ public StreamRegistration registerResource(
118118
*/
119119
public StreamRegistration registerResource(
120120
ElementRequestHandler elementRequestHandler) {
121-
AbstractStreamResource wrappedResource = wrap(elementRequestHandler);
121+
AbstractStreamResource wrappedResource = new ElementStreamResource(
122+
elementRequestHandler);
122123
session.checkHasLock(
123124
"Session needs to be locked when registering stream resources.");
124125
StreamRegistration registration = new Registration(this,
@@ -127,15 +128,23 @@ public StreamRegistration registerResource(
127128
return registration;
128129
}
129130

130-
private AbstractStreamResource wrap(
131-
ElementRequestHandler elementRequestHandler) {
132-
return new AbstractStreamResource() {
133-
@Override
134-
public String getName() {
135-
return elementRequestHandler.getUrlPostfix();
136-
}
131+
public static class ElementStreamResource extends AbstractStreamResource {
132+
ElementRequestHandler elementRequestHandler;
133+
134+
public ElementStreamResource(
135+
ElementRequestHandler elementRequestHandler) {
136+
this.elementRequestHandler = elementRequestHandler;
137+
}
137138

138-
};
139+
public ElementRequestHandler getElementRequestHandler() {
140+
return elementRequestHandler;
141+
}
142+
143+
@Override
144+
public String getName() {
145+
return elementRequestHandler.getUrlPostfix() == null ? ""
146+
: elementRequestHandler.getUrlPostfix();
147+
}
139148
}
140149

141150
/**

flow-server/src/main/java/com/vaadin/flow/server/communication/StreamRequestHandler.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@
2626
import com.vaadin.flow.component.UI;
2727
import com.vaadin.flow.internal.UrlUtil;
2828
import com.vaadin.flow.server.AbstractStreamResource;
29+
import com.vaadin.flow.server.ElementRequestHandler;
2930
import com.vaadin.flow.server.HttpStatusCode;
3031
import com.vaadin.flow.server.RequestHandler;
3132
import com.vaadin.flow.server.StreamReceiver;
3233
import com.vaadin.flow.server.StreamResource;
34+
import com.vaadin.flow.server.StreamResourceRegistry;
3335
import com.vaadin.flow.server.VaadinRequest;
3436
import com.vaadin.flow.server.VaadinResponse;
3537
import com.vaadin.flow.server.VaadinSession;
@@ -107,11 +109,13 @@ public boolean handleRequest(VaadinSession session, VaadinRequest request,
107109

108110
if (abstractStreamResource.isPresent()) {
109111
AbstractStreamResource resource = abstractStreamResource.get();
110-
if (resource instanceof StreamResource) {
112+
if (resource instanceof StreamResourceRegistry.ElementStreamResource elementRequest) {
113+
elementRequest.getElementRequestHandler().handleRequest(request,
114+
response, session, null);
115+
} else if (resource instanceof StreamResource) {
111116
resourceHandler.handleRequest(session, request, response,
112117
(StreamResource) resource);
113-
} else if (resource instanceof StreamReceiver) {
114-
StreamReceiver streamReceiver = (StreamReceiver) resource;
118+
} else if (resource instanceof StreamReceiver streamReceiver) {
115119
String[] parts = parsePath(pathInfo);
116120

117121
receiverHandler.handleRequest(session, request, response,

0 commit comments

Comments
 (0)