Skip to content

Commit c734a7f

Browse files
committed
feat: add DownloadHandler
Add DownloadHandler interface and factory methods. Closes #21166
1 parent 9d08e57 commit c734a7f

File tree

15 files changed

+1160
-2
lines changed

15 files changed

+1160
-2
lines changed
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
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+
42+
private Component owningComponent;
43+
44+
/**
45+
* Create a new download event with required data.
46+
*
47+
* @param request
48+
* current request
49+
* @param response
50+
* current response to write response data to
51+
* @param session
52+
* current session
53+
* @param fileName
54+
* defined download file name
55+
*/
56+
public DownloadEvent(VaadinRequest request, VaadinResponse response,
57+
VaadinSession session, String fileName) {
58+
this.request = request;
59+
this.response = response;
60+
this.session = session;
61+
this.fileName = fileName;
62+
}
63+
64+
/**
65+
* Set the owning component for the download event from the element instance
66+
* if a component is available.
67+
*
68+
* @param owningElement
69+
* owning element for the event
70+
* @return this Event instance
71+
*/
72+
public DownloadEvent withOwningComponent(Element owningElement) {
73+
if (owningElement != null) {
74+
Optional<Component> component = owningElement.getComponent();
75+
component.ifPresent(value -> owningComponent = value);
76+
}
77+
return this;
78+
}
79+
80+
/**
81+
* Set the owning component for the download event.
82+
*
83+
* @param owningComponent
84+
* owning component for the event
85+
* @return this Event instance
86+
*/
87+
public DownloadEvent withOwningComponent(Component owningComponent) {
88+
this.owningComponent = owningComponent;
89+
return this;
90+
}
91+
92+
/**
93+
* Set the DownloadEvent content type.
94+
*
95+
* @param contentType
96+
* content type of the event content
97+
* @return this Event instance
98+
*/
99+
public DownloadEvent withContentType(String contentType) {
100+
this.contentType = contentType;
101+
return this;
102+
}
103+
104+
/**
105+
* Returns a <code>OutputStream</code> for writing binary data in the
106+
* response.
107+
* <p>
108+
* Either this method or getWriter() may be called to write the response,
109+
* not both.
110+
*
111+
* @return a <code>OutputStream</code> for writing binary data
112+
* @throws IOException
113+
* if an input or output exception occurred
114+
*/
115+
public OutputStream getOutputStream() throws IOException {
116+
return response.getOutputStream();
117+
}
118+
119+
/**
120+
* Returns a <code>PrintWriter</code> object that can send character text to
121+
* the client. The PrintWriter uses the character encoding defined using
122+
* setContentType.
123+
* <p>
124+
* Either this method or getOutputStream() may be called to write the
125+
* response, not both.
126+
*
127+
* @return a <code>PrintWriter</code> for writing character text
128+
* @throws IOException
129+
* if an input or output exception occurred
130+
*/
131+
public PrintWriter getWriter() throws IOException {
132+
return response.getWriter();
133+
}
134+
135+
/**
136+
* Get {@link VaadinRequest} for download event.
137+
*
138+
* @return vaadin request
139+
*/
140+
public VaadinRequest getRequest() {
141+
return request;
142+
}
143+
144+
/**
145+
* Get {@link VaadinResponse} for download event.
146+
*
147+
* @return vaadin response
148+
*/
149+
public VaadinResponse getResponse() {
150+
return response;
151+
}
152+
153+
/**
154+
* Get {@link VaadinSession} for download event.
155+
*
156+
* @return vaadin session
157+
*/
158+
public VaadinSession getSession() {
159+
return session;
160+
}
161+
162+
/**
163+
* Get the set file name.
164+
*
165+
* @return file name
166+
*/
167+
public String getFileName() {
168+
return fileName;
169+
}
170+
171+
/**
172+
* Get the content type for the data to download.
173+
*
174+
* @return set content type
175+
*/
176+
public String getContentType() {
177+
return contentType;
178+
}
179+
180+
/**
181+
* Get owner {@link Component} for this event.
182+
*
183+
* @return owning component or null in none defined
184+
*/
185+
public Component getComponent() {
186+
return owningComponent;
187+
}
188+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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+
import java.util.function.Function;
22+
23+
import com.vaadin.flow.dom.Element;
24+
import com.vaadin.flow.function.SerializableFunction;
25+
import com.vaadin.flow.server.streams.ClassDownloadHandler;
26+
import com.vaadin.flow.server.streams.DownloadResponse;
27+
import com.vaadin.flow.server.streams.FileDownloadHandler;
28+
import com.vaadin.flow.server.streams.InputStreamDownloadHandler;
29+
import com.vaadin.flow.server.streams.ServletResourceDownloadHandler;
30+
31+
/**
32+
* Interface for handling download of data from the server to the client.
33+
*
34+
* @since 24.8
35+
*/
36+
@FunctionalInterface
37+
public interface DownloadHandler extends ElementRequestHandler {
38+
39+
/**
40+
* Method that is called when the client wants to download from the url
41+
* stored for this specific handler registration.
42+
*
43+
* @param event
44+
* download event containing the necessary data for writing the
45+
* response
46+
*/
47+
void handleDownloadRequest(DownloadEvent event);
48+
49+
default void handleRequest(VaadinRequest request, VaadinResponse response,
50+
VaadinSession session, Element owner) {
51+
String fileName = getUrlPostfix() == null ? "" : getUrlPostfix();
52+
53+
DownloadEvent event = new DownloadEvent(request, response, session,
54+
fileName);
55+
event.withOwningComponent(owner)
56+
.withContentType(Optional
57+
.ofNullable(response.getService().getMimeType(fileName))
58+
.orElse("application/octet-stream"));
59+
60+
handleDownloadRequest(event);
61+
}
62+
63+
/**
64+
* Get a download handler for serving given {@link File}.
65+
*
66+
* @param file
67+
* file to server for download
68+
* @return DownloadHandler instance for file
69+
*/
70+
static DownloadHandler forFile(File file) {
71+
return new FileDownloadHandler(file);
72+
}
73+
74+
/**
75+
* Generate a download handler for class resource.
76+
* <p>
77+
* For instance for the file {@code resources/com/example/ui/MyData.json}
78+
* and class {@code com.example.ui.MyData} the definition would be
79+
* {@code forClassResource(MyData.class, "MyData.json")}
80+
*
81+
* @param clazz
82+
* class for resource module
83+
* @param name
84+
* resource name
85+
* @return DownloadHandler instance for class resource
86+
*/
87+
static DownloadHandler forClassResource(Class<?> clazz, String name) {
88+
return new ClassDownloadHandler(clazz, name);
89+
}
90+
91+
/**
92+
* Generate a download handler for a servlet resource.
93+
* <p>
94+
* For instance for the file {@code webapp/WEB-INF/servlet.json} the path
95+
* would be {@code /WEB-INF/servlet.json}
96+
*
97+
* @param path
98+
* the servlet path to the file
99+
* @return DownloadHandler instance for servlet resource
100+
*/
101+
static DownloadHandler forServletResource(String path) {
102+
return new ServletResourceDownloadHandler(path);
103+
}
104+
105+
/**
106+
* Generate a function for downloading from a generated inputStream.
107+
*
108+
* @param handler
109+
* handler function that will be called on download
110+
* @return DownloadHandler instance for inputStream
111+
*/
112+
static DownloadHandler fromInputStream(
113+
SerializableFunction<DownloadEvent, DownloadResponse> handler) {
114+
return new InputStreamDownloadHandler(handler);
115+
}
116+
}

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

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.Map;
2323
import java.util.Optional;
2424

25+
import com.vaadin.flow.component.UI;
2526
import com.vaadin.flow.dom.Element;
2627
import com.vaadin.flow.server.communication.StreamRequestHandler;
2728

@@ -85,7 +86,7 @@ public StreamResourceRegistry(VaadinSession session) {
8586
* attribute value or property value) via the registration handler. The
8687
* registration handler should be used to unregister resource when it's not
8788
* needed anymore. Note that it is the developer's responsibility to
88-
* unregister resources. Otherwise resources won't be garbage collected
89+
* unregister resources. Otherwise, resources won't be garbage collected
8990
* until the session expires which causes memory leak.
9091
*
9192
* @param resource
@@ -102,6 +103,29 @@ public StreamRegistration registerResource(
102103
return registration;
103104
}
104105

106+
/**
107+
* Registers a stream resource in the session and returns registration
108+
* handler. Registration is done without a specific owner element and thus
109+
* bound to UI element.
110+
* <p>
111+
* You can get resource URI to use it in the application (e.g. set an
112+
* attribute value or property value) via the registration handler. The
113+
* registration handler should be used to unregister the resource when it's
114+
* not needed anymore. Note that it is the developer's responsibility to
115+
* unregister resources. Otherwise, resources won't be garbage collected
116+
* until the session expires which causes memory leak.
117+
*
118+
* @param elementRequestHandler
119+
* element request handler to register
120+
*
121+
* @return registration handler
122+
*/
123+
public StreamRegistration registerResource(
124+
ElementRequestHandler elementRequestHandler) {
125+
return registerResource(elementRequestHandler,
126+
UI.getCurrent().getElement());
127+
}
128+
105129
/**
106130
* Registers a stream resource in the session and returns registration
107131
* handler.
@@ -110,7 +134,7 @@ public StreamRegistration registerResource(
110134
* attribute value or property value) via the registration handler. The
111135
* registration handler should be used to unregister the resource when it's
112136
* not needed anymore. Note that it is the developer's responsibility to
113-
* unregister resources. Otherwise resources won't be garbage collected
137+
* unregister resources. Otherwise, resources won't be garbage collected
114138
* until the session expires which causes memory leak.
115139
*
116140
* @param elementRequestHandler
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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.streams;
18+
19+
import java.io.IOException;
20+
import java.io.InputStream;
21+
22+
import com.vaadin.flow.server.DownloadHandler;
23+
import com.vaadin.flow.server.VaadinSession;
24+
25+
public abstract class AbstractDownloadHandler implements DownloadHandler {
26+
27+
protected int read(VaadinSession session, InputStream source, byte[] buffer)
28+
throws IOException {
29+
session.lock();
30+
try {
31+
return source.read(buffer);
32+
} finally {
33+
session.unlock();
34+
}
35+
}
36+
37+
}

0 commit comments

Comments
 (0)