Skip to content

Commit ea123dd

Browse files
committed
feat: add DownloadHandler
Add DownloadHandler interface and factory methods. Closes #21166
1 parent 7a87f4f commit ea123dd

File tree

16 files changed

+1158
-12
lines changed

16 files changed

+1158
-12
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: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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.server.streams.ClassDownloadHandler;
25+
import com.vaadin.flow.server.streams.DownloadResponse;
26+
import com.vaadin.flow.server.streams.FileDownloadHandler;
27+
import com.vaadin.flow.server.streams.InputStreamDownloadHandler;
28+
import com.vaadin.flow.server.streams.ServletResourceDownloadHandler;
29+
30+
/**
31+
* Interface for handling download of data from the server to the client.
32+
*
33+
* @since 24.8
34+
*/
35+
@FunctionalInterface
36+
public interface DownloadHandler extends ElementRequestHandler {
37+
38+
/**
39+
* Method that is called when the client wants to download from the url
40+
* stored for this specific handler registration.
41+
*
42+
* @param event
43+
* download event containing the necessary data for writing the
44+
* response
45+
*/
46+
void handleDownloadRequest(DownloadEvent event);
47+
48+
default void handleRequest(VaadinRequest request, VaadinResponse response,
49+
VaadinSession session, Element owner) {
50+
String fileName = getUrlPostfix() == null ? "" : getUrlPostfix();
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 a download handler for serving given {@link File}.
64+
*
65+
* @param file
66+
* file to server for download
67+
* @return DownloadHandler instance for file
68+
*/
69+
static DownloadHandler forFile(File file) {
70+
return new FileDownloadHandler(file);
71+
}
72+
73+
/**
74+
* Generate a download handler for class resource.
75+
* <p>
76+
* For instance for the file {@code resources/com/example/ui/MyData.json}
77+
* and class {@code com.example.ui.MyData} the definition would be
78+
* {@code forClassResource(MyData.class, "MyData.json")}
79+
*
80+
* @param clazz
81+
* class for resource module
82+
* @param name
83+
* resource name
84+
* @return DownloadHandler instance for class resource
85+
*/
86+
static DownloadHandler forClassResource(Class<?> clazz, String name) {
87+
return new ClassDownloadHandler(clazz, name);
88+
}
89+
90+
/**
91+
* Generate a download handler for a servlet resource.
92+
* <p>
93+
* For instance for the file {@code webapp/WEB-INF/servlet.json} the path
94+
* would be {@code /WEB-INF/servlet.json}
95+
*
96+
* @param path
97+
* the servlet path to the file
98+
* @return DownloadHandler instance for servlet resource
99+
*/
100+
static DownloadHandler forServletResource(String path) {
101+
return new ServletResourceDownloadHandler(path);
102+
}
103+
104+
/**
105+
* Generate a function for downloading from a generated inputStream.
106+
*
107+
* @param handler
108+
* handler function that will be called on download
109+
* @return DownloadHandler instance for inputStream
110+
*/
111+
static DownloadHandler fromInputStream(
112+
Function<DownloadEvent, DownloadResponse> handler) {
113+
return new InputStreamDownloadHandler(handler);
114+
}
115+
}

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)