Skip to content

Commit 5ab4dc0

Browse files
authored
feat: add ElementRequestHandler (#21228)
Fixes #21164
1 parent 7347aca commit 5ab4dc0

File tree

8 files changed

+254
-8
lines changed

8 files changed

+254
-8
lines changed

flow-html-components/src/test/java/com/vaadin/flow/component/html/AnchorTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@
1717

1818
import java.util.Optional;
1919

20-
import com.vaadin.flow.component.UI;
21-
import com.vaadin.flow.server.AbstractStreamResource;
2220
import org.junit.After;
2321
import org.junit.Assert;
2422
import org.junit.Test;
2523

2624
import com.vaadin.flow.component.Text;
25+
import com.vaadin.flow.component.UI;
26+
import com.vaadin.flow.server.AbstractStreamResource;
2727

2828
public class AnchorTest extends ComponentTest {
2929

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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+
package com.vaadin.flow.server;
17+
18+
import java.io.Serializable;
19+
20+
import com.vaadin.flow.dom.DisabledUpdateMode;
21+
import com.vaadin.flow.dom.Element;
22+
23+
/**
24+
* Request handler callback for handing client-server or server-client data
25+
* transfer scoped to a specific (owner) element.
26+
*/
27+
@FunctionalInterface
28+
public interface ElementRequestHandler extends Serializable {
29+
30+
/**
31+
* Request handler callback for handing client-server or server-client data
32+
* transfer scoped to a specific (owner) element.
33+
*
34+
* Note: when handling requests via this API, you need to take care of
35+
* typical stream handling issues, e.g. exceptions yourself. However, you do
36+
* not need to close the stream yourself, Flow will handle that for you when
37+
* needed.
38+
*
39+
* @param request
40+
* VaadinRequest request to handle
41+
* @param response
42+
* VaadinResponse response to handle
43+
* @param session
44+
* VaadinSession current VaadinSession
45+
* @param owner
46+
* Element owner element
47+
*/
48+
void handleRequest(VaadinRequest request, VaadinResponse response,
49+
VaadinSession session, Element owner);
50+
51+
/**
52+
* Optional URL postfix allows appending an application-controlled string,
53+
* e.g. the logical name of the target file, to the end of the otherwise
54+
* random-looking download URL. If defined, requests that would otherwise be
55+
* routable are still rejected if the postfix is missing or invalid. Postfix
56+
* changes the last segment in the resource url.
57+
*
58+
* @return String optional URL postfix, or {@code null} for "".
59+
*/
60+
default String getUrlPostfix() {
61+
return null;
62+
}
63+
64+
/**
65+
* Whether to invoke this request handler even if the owning element is
66+
* currently inert.
67+
*
68+
* @return {@code true} to invoke for inert elements, {@code false}
69+
* otherwise. Defaults to {@code false}.
70+
*/
71+
default boolean allowInert() {
72+
return false;
73+
}
74+
75+
/**
76+
* Controls whether request handler is invoked when the owner element is
77+
* disabled.
78+
*
79+
* @return the currently set DisabledUpdateMode for this request handler.
80+
* Defaults to {@literal ONLY_WHEN_ENABLED}.
81+
*/
82+
default DisabledUpdateMode getDisabledUpdateMode() {
83+
return DisabledUpdateMode.ONLY_WHEN_ENABLED;
84+
}
85+
}

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

Lines changed: 62 additions & 0 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.dom.Element;
2526
import com.vaadin.flow.server.communication.StreamRequestHandler;
2627

2728
/**
@@ -101,6 +102,67 @@ public StreamRegistration registerResource(
101102
return registration;
102103
}
103104

105+
/**
106+
* Registers a stream resource in the session and returns registration
107+
* handler.
108+
* <p>
109+
* You can get resource URI to use it in the application (e.g. set an
110+
* attribute value or property value) via the registration handler. The
111+
* registration handler should be used to unregister the resource when it's
112+
* not needed anymore. Note that it is the developer's responsibility to
113+
* unregister resources. Otherwise resources won't be garbage collected
114+
* until the session expires which causes memory leak.
115+
*
116+
* @param elementRequestHandler
117+
* element request handler to register
118+
* @param owner
119+
* owner element this request handler is scoped to
120+
*
121+
* @return registration handler
122+
*/
123+
public StreamRegistration registerResource(
124+
ElementRequestHandler elementRequestHandler, Element owner) {
125+
AbstractStreamResource wrappedResource = new ElementStreamResource(
126+
elementRequestHandler, owner);
127+
session.checkHasLock(
128+
"Session needs to be locked when registering stream resources.");
129+
StreamRegistration registration = new Registration(this,
130+
wrappedResource.getId(), wrappedResource.getName());
131+
res.put(registration.getResourceUri(), wrappedResource);
132+
return registration;
133+
}
134+
135+
/**
136+
* Internal wrapper class for wrapping {@link ElementRequestHandler}
137+
* instances as {@link AbstractStreamResource} compatible instances.
138+
*
139+
* For internal use only. May be renamed or removed in a future release.
140+
*/
141+
public static class ElementStreamResource extends AbstractStreamResource {
142+
private final ElementRequestHandler elementRequestHandler;
143+
private final Element owner;
144+
145+
public ElementStreamResource(
146+
ElementRequestHandler elementRequestHandler, Element owner) {
147+
this.elementRequestHandler = elementRequestHandler;
148+
this.owner = owner;
149+
}
150+
151+
public ElementRequestHandler getElementRequestHandler() {
152+
return elementRequestHandler;
153+
}
154+
155+
@Override
156+
public String getName() {
157+
return elementRequestHandler.getUrlPostfix() == null ? ""
158+
: elementRequestHandler.getUrlPostfix();
159+
}
160+
161+
public Element getOwner() {
162+
return owner;
163+
}
164+
}
165+
104166
/**
105167
* Unregister a stream receiver resource.
106168
*

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

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@
2424
import org.slf4j.LoggerFactory;
2525

2626
import com.vaadin.flow.component.UI;
27+
import com.vaadin.flow.dom.Element;
2728
import com.vaadin.flow.internal.UrlUtil;
2829
import com.vaadin.flow.server.AbstractStreamResource;
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,22 @@ 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+
Element owner = elementRequest.getOwner();
114+
if (owner.getNode().isInert() && !elementRequest
115+
.getElementRequestHandler().allowInert()) {
116+
response.sendError(HttpStatusCode.FORBIDDEN.getCode(),
117+
"Resource not available");
118+
return true;
119+
} else {
120+
elementRequest.getElementRequestHandler().handleRequest(
121+
request, response, session,
122+
elementRequest.getOwner());
123+
}
124+
} else if (resource instanceof StreamResource) {
111125
resourceHandler.handleRequest(session, request, response,
112126
(StreamResource) resource);
113-
} else if (resource instanceof StreamReceiver) {
114-
StreamReceiver streamReceiver = (StreamReceiver) resource;
127+
} else if (resource instanceof StreamReceiver streamReceiver) {
115128
String[] parts = parsePath(pathInfo);
116129

117130
receiverHandler.handleRequest(session, request, response,

flow-server/src/test/java/com/vaadin/flow/server/StreamResourceRegistryTest.java

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.mockito.Mockito;
3030

3131
import com.vaadin.flow.component.UI;
32+
import com.vaadin.flow.dom.Element;
3233
import com.vaadin.flow.internal.CurrentInstance;
3334

3435
public class StreamResourceRegistryTest {
@@ -79,6 +80,27 @@ public void registerResource_registrationResultCanBeFound() {
7980
resource, registration.getResource());
8081
}
8182

83+
@Test
84+
public void registerElementResourceHandler_registrationResultCanBeFound() {
85+
StreamResourceRegistry registry = new StreamResourceRegistry(session);
86+
87+
ElementRequestHandler handler = (request, response, session, owner) -> {
88+
// nop
89+
};
90+
Element owner = Mockito.mock(Element.class);
91+
StreamRegistration registration = registry.registerResource(handler,
92+
owner);
93+
Assert.assertNotNull(registration);
94+
95+
URI uri = registration.getResourceUri();
96+
AbstractStreamResource generatedResource = registration.getResource();
97+
98+
Optional<AbstractStreamResource> stored = registry.getResource(uri);
99+
Assert.assertSame(
100+
"Unexpected stored resource is returned for registered URI",
101+
generatedResource, stored.get());
102+
}
103+
82104
@Test
83105
public void unregisterResource_resourceIsRemoved() {
84106
StreamResourceRegistry registry = new StreamResourceRegistry(session);
@@ -97,9 +119,35 @@ public void unregisterResource_resourceIsRemoved() {
97119
Assert.assertFalse(
98120
"Unexpected stored resource is found after unregister()",
99121
stored.isPresent());
122+
Assert.assertNull(
123+
"Unexpected resource is returned by the registration instance",
124+
registration.getResource());
125+
}
126+
127+
@Test
128+
public void unregisterElementResourceHandler_resourceIsRemoved() {
129+
StreamResourceRegistry registry = new StreamResourceRegistry(session);
130+
131+
ElementRequestHandler handler = (request, response, session, owner) -> {
132+
// nop
133+
};
134+
Element owner = Mockito.mock(Element.class);
135+
StreamRegistration registration = registry.registerResource(handler,
136+
owner);
137+
138+
Assert.assertNotNull(registration);
139+
140+
URI uri = registration.getResourceUri();
141+
142+
registration.unregister();
143+
144+
Optional<AbstractStreamResource> stored = registry.getResource(uri);
100145
Assert.assertFalse(
146+
"Unexpected stored resource is found after unregister()",
147+
stored.isPresent());
148+
Assert.assertNull(
101149
"Unexpected resource is returned by the registration instance",
102-
registration.getResource() != null);
150+
registration.getResource());
103151
}
104152

105153
@Test

flow-server/src/test/java/com/vaadin/flow/server/communication/StreamRequestHandlerTest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import jakarta.servlet.ServletContext;
44
import jakarta.servlet.ServletException;
55
import jakarta.servlet.ServletOutputStream;
6-
76
import java.io.ByteArrayInputStream;
87
import java.io.IOException;
98

flow-tests/test-root-context/src/main/java/com/vaadin/flow/uitest/ui/StreamResourceView.java

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,51 @@
1616
package com.vaadin.flow.uitest.ui;
1717

1818
import java.io.ByteArrayInputStream;
19+
import java.io.IOException;
1920
import java.nio.charset.StandardCharsets;
2021

2122
import com.vaadin.flow.component.html.Anchor;
2223
import com.vaadin.flow.component.html.Div;
2324
import com.vaadin.flow.component.html.NativeButton;
25+
import com.vaadin.flow.dom.Element;
2426
import com.vaadin.flow.router.Route;
27+
import com.vaadin.flow.server.ElementRequestHandler;
2528
import com.vaadin.flow.server.StreamResource;
29+
import com.vaadin.flow.server.StreamResourceRegistry;
30+
import com.vaadin.flow.server.VaadinRequest;
31+
import com.vaadin.flow.server.VaadinResponse;
32+
import com.vaadin.flow.server.VaadinSession;
2633
import com.vaadin.flow.uitest.servlet.ViewTestLayout;
2734

2835
@Route(value = "com.vaadin.flow.uitest.ui.StreamResourceView", layout = ViewTestLayout.class)
2936
public class StreamResourceView extends Div {
3037

3138
public StreamResourceView() {
39+
Anchor esrAnchor = new Anchor();
40+
esrAnchor.setText("esr anchor");
41+
esrAnchor.setId("esrAnchor");
42+
StreamResourceRegistry.ElementStreamResource elementStreamResource = new StreamResourceRegistry.ElementStreamResource(
43+
new ElementRequestHandler() {
44+
@Override
45+
public void handleRequest(VaadinRequest request,
46+
VaadinResponse response, VaadinSession session,
47+
Element owner) {
48+
response.setContentType("text/plain");
49+
try {
50+
response.getOutputStream().write(
51+
"foo".getBytes(StandardCharsets.UTF_8));
52+
} catch (IOException e) {
53+
throw new RuntimeException(e);
54+
}
55+
}
56+
57+
@Override
58+
public String getUrlPostfix() {
59+
return "esr-filename.txt";
60+
}
61+
}, esrAnchor.getElement());
62+
esrAnchor.setHref(elementStreamResource);
63+
3264
StreamResource resource = new StreamResource("file name",
3365
() -> new ByteArrayInputStream(
3466
"foo".getBytes(StandardCharsets.UTF_8)));
@@ -50,7 +82,7 @@ public StreamResourceView() {
5082
percentDownload.setHref(percentResource);
5183
percentDownload.setId("percent-link");
5284

53-
add(download, plusDownload, percentDownload);
85+
add(esrAnchor, download, plusDownload, percentDownload);
5486

5587
NativeButton reattach = new NativeButton("Remove and add back",
5688
event -> {

flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/StreamResourceIT.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@
3232

3333
public class StreamResourceIT extends AbstractStreamResourceIT {
3434

35+
@Test
36+
public void getElementStreamResource() throws IOException {
37+
open();
38+
39+
assertDownloadedContent("esrAnchor", "esr-filename.txt");
40+
}
41+
3542
@Test
3643
public void getDynamicVaadinResource() throws IOException {
3744
open();

0 commit comments

Comments
 (0)