Skip to content

Commit 04f98ad

Browse files
Artur-claude
andauthored
feat: Extend @ClientCallable to support beans and collections for all RPC calls (#22410)
Extend `@ClientCallable` to support custom beans and generic collections Co-authored-by: Claude <noreply@anthropic.com>
1 parent 143b9e3 commit 04f98ad

File tree

6 files changed

+478
-5
lines changed

6 files changed

+478
-5
lines changed

flow-server/src/main/java/com/vaadin/flow/server/communication/rpc/DefaultRpcDecoder.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@
2020
import com.vaadin.flow.internal.JacksonCodec;
2121

2222
/**
23-
* Decodes the standard basic types from their JSON representation.
23+
* Decodes JSON values to Java objects using Jackson deserialization.
2424
* <p>
25-
* Delegates to the standard JSON deserializer method
25+
* Supports a wide range of types including basic types, custom bean classes,
26+
* and generic collections. Delegates to the enhanced JSON deserializer method
2627
* {@link JacksonCodec#decodeAs(JsonNode, Class)}.
2728
* <p>
2829
* For internal use only. May be renamed or removed in a future release.
@@ -36,7 +37,10 @@ public class DefaultRpcDecoder implements RpcDecoder {
3637

3738
@Override
3839
public boolean isApplicable(JsonNode value, Class<?> type) {
39-
return JacksonCodec.canEncodeWithoutTypeInfo(type);
40+
// This decoder handles all types that JacksonCodec.decodeAs can
41+
// process,
42+
// which includes basic types, custom beans, and collections
43+
return true;
4044
}
4145

4246
@Override

flow-server/src/main/java/com/vaadin/flow/server/communication/rpc/PublishedServerEventHandlerRpcHandler.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@
2323
import java.util.Collection;
2424
import java.util.List;
2525
import java.util.Optional;
26+
import java.util.Set;
2627
import java.util.stream.Collectors;
2728
import java.util.stream.Stream;
2829

2930
import tools.jackson.databind.JsonNode;
31+
import tools.jackson.databind.ObjectMapper;
3032
import tools.jackson.databind.node.ArrayNode;
3133
import tools.jackson.databind.node.JsonNodeType;
3234
import org.slf4j.Logger;
@@ -305,6 +307,8 @@ private static Object decodeArg(Component instance, Method method,
305307
return JacksonCodec.decodeAs(argValue, type);
306308
} else if (type.isArray()) {
307309
return decodeArray(method, type, index, argValue);
310+
} else if (isGenericCollection(type, argValue)) {
311+
return decodeGenericCollection(method, type, index, argValue);
308312
} else {
309313
Class<?> convertedType = ReflectTools.convertPrimitiveType(type);
310314

@@ -378,6 +382,36 @@ private static Collection<RpcDecoder> loadDecoders() {
378382
return decoders;
379383
}
380384

385+
private static boolean isGenericCollection(Class<?> type,
386+
JsonNode argValue) {
387+
// Check if it's a collection type and the JSON value is an array
388+
return Collection.class.isAssignableFrom(type)
389+
&& argValue.getNodeType() == JsonNodeType.ARRAY;
390+
}
391+
392+
private static Object decodeGenericCollection(Method method, Class<?> type,
393+
int index, JsonNode argValue) {
394+
try {
395+
// Get the generic type information
396+
java.lang.reflect.Type genericType = method
397+
.getGenericParameterTypes()[index];
398+
399+
// Use Jackson's advanced type system to handle generic collections
400+
tools.jackson.databind.ObjectMapper mapper = JacksonUtils
401+
.getMapper();
402+
tools.jackson.databind.JavaType javaType = mapper.getTypeFactory()
403+
.constructType(genericType);
404+
405+
return mapper.treeToValue(argValue, javaType);
406+
} catch (Exception e) {
407+
throw new IllegalArgumentException(
408+
"Failed to deserialize generic collection for parameter "
409+
+ index + " of method " + method.getName() + ": "
410+
+ e.getMessage(),
411+
e);
412+
}
413+
}
414+
381415
private static Logger getLogger() {
382416
return LoggerFactory.getLogger(
383417
PublishedServerEventHandlerRpcHandler.class.getName());

flow-server/src/test/java/com/vaadin/flow/internal/JacksonCodecTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,7 @@ public void testSetOfBeansSerialization() {
485485

486486
JsonNode encoded = JacksonCodec.encodeWithTypeInfo(beanSet);
487487

488-
// Should be direct array
488+
// With the new approach, sets are directly serialized as JSON arrays
489489
Assert.assertTrue("Should be array", encoded.isArray());
490490
Assert.assertEquals("Should have 2 elements", 2, encoded.size());
491491

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
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.communication.rpc;
17+
18+
import java.util.Arrays;
19+
import java.util.List;
20+
21+
import tools.jackson.databind.JsonNode;
22+
import tools.jackson.databind.node.ArrayNode;
23+
import tools.jackson.databind.node.ObjectNode;
24+
import org.junit.After;
25+
import org.junit.Assert;
26+
import org.junit.Before;
27+
import org.junit.Test;
28+
29+
import com.vaadin.flow.component.ClientCallable;
30+
import com.vaadin.flow.component.Component;
31+
import com.vaadin.flow.component.EventData;
32+
import com.vaadin.flow.component.Tag;
33+
import com.vaadin.flow.component.UI;
34+
import com.vaadin.flow.internal.JacksonUtils;
35+
import com.vaadin.flow.server.MockServletServiceSessionSetup;
36+
import com.vaadin.flow.server.VaadinService;
37+
import com.vaadin.flow.server.VaadinSession;
38+
import com.vaadin.flow.shared.JsonConstants;
39+
import com.vaadin.tests.util.MockDeploymentConfiguration;
40+
import com.vaadin.tests.util.MockUI;
41+
42+
/**
43+
* Tests for @ClientCallable method support with bean and collection parameters
44+
* and return values.
45+
*/
46+
public class ClientCallableBeanSupportTest {
47+
48+
private MockServletServiceSessionSetup mocks;
49+
private UI ui;
50+
private PublishedServerEventHandlerRpcHandler handler;
51+
52+
// Test bean classes
53+
public static class SimpleBean {
54+
public String name;
55+
public int value;
56+
public boolean active;
57+
58+
public SimpleBean() {
59+
}
60+
61+
public SimpleBean(String name, int value, boolean active) {
62+
this.name = name;
63+
this.value = value;
64+
this.active = active;
65+
}
66+
}
67+
68+
public static class NestedBean {
69+
public String title;
70+
public SimpleBean simple;
71+
72+
public NestedBean() {
73+
}
74+
75+
public NestedBean(String title, SimpleBean simple) {
76+
this.title = title;
77+
this.simple = simple;
78+
}
79+
}
80+
81+
// Test component with @ClientCallable methods
82+
@Tag(Tag.DIV)
83+
public static class ComponentWithClientCallableMethods extends Component {
84+
private SimpleBean receivedBean;
85+
private List<SimpleBean> receivedList;
86+
private NestedBean receivedNestedBean;
87+
private List<Integer> receivedIntegerList;
88+
89+
@ClientCallable
90+
public void handleSimpleBean(@EventData("bean") SimpleBean bean) {
91+
this.receivedBean = bean;
92+
}
93+
94+
@ClientCallable
95+
public void handleBeanList(@EventData("list") List<SimpleBean> list) {
96+
this.receivedList = list;
97+
}
98+
99+
@ClientCallable
100+
public void handleNestedBean(@EventData("nested") NestedBean nested) {
101+
this.receivedNestedBean = nested;
102+
}
103+
104+
@ClientCallable
105+
public void handleIntegerList(
106+
@EventData("integers") List<Integer> integers) {
107+
this.receivedIntegerList = integers;
108+
}
109+
110+
@ClientCallable
111+
public SimpleBean returnSimpleBean() {
112+
return new SimpleBean("returned", 42, true);
113+
}
114+
115+
@ClientCallable
116+
public List<SimpleBean> returnBeanList() {
117+
return Arrays.asList(new SimpleBean("first", 1, true),
118+
new SimpleBean("second", 2, false));
119+
}
120+
121+
@ClientCallable
122+
public NestedBean returnNestedBean() {
123+
return new NestedBean("outer", new SimpleBean("inner", 100, false));
124+
}
125+
126+
@ClientCallable
127+
public List<Integer> returnIntegerList() {
128+
return Arrays.asList(10, 20, 30);
129+
}
130+
131+
// Getters for test verification
132+
public SimpleBean getReceivedBean() {
133+
return receivedBean;
134+
}
135+
136+
public List<SimpleBean> getReceivedList() {
137+
return receivedList;
138+
}
139+
140+
public NestedBean getReceivedNestedBean() {
141+
return receivedNestedBean;
142+
}
143+
144+
public List<Integer> getReceivedIntegerList() {
145+
return receivedIntegerList;
146+
}
147+
}
148+
149+
@Before
150+
public void setUp() throws Exception {
151+
mocks = new MockServletServiceSessionSetup();
152+
VaadinService service = mocks.getService();
153+
service.init();
154+
155+
ui = new MockUI();
156+
VaadinSession.setCurrent(mocks.getSession());
157+
UI.setCurrent(ui);
158+
159+
handler = new PublishedServerEventHandlerRpcHandler();
160+
}
161+
162+
@After
163+
public void tearDown() {
164+
mocks.cleanup();
165+
}
166+
167+
@Test
168+
public void testSimpleBeanParameter() throws Exception {
169+
ComponentWithClientCallableMethods component = new ComponentWithClientCallableMethods();
170+
ui.add(component);
171+
172+
// Create JSON for SimpleBean parameter
173+
ObjectNode beanJson = JacksonUtils.createObjectNode();
174+
beanJson.put("name", "TestBean");
175+
beanJson.put("value", 123);
176+
beanJson.put("active", true);
177+
178+
// Create parameters array
179+
ArrayNode params = JacksonUtils.createArrayNode();
180+
params.add(beanJson);
181+
182+
// Invoke the method directly
183+
PublishedServerEventHandlerRpcHandler.invokeMethod(component,
184+
component.getClass(), "handleSimpleBean", params, -1);
185+
186+
// Verify the bean was properly deserialized
187+
SimpleBean received = component.getReceivedBean();
188+
Assert.assertNotNull("Bean should be received", received);
189+
Assert.assertEquals("TestBean", received.name);
190+
Assert.assertEquals(123, received.value);
191+
Assert.assertTrue(received.active);
192+
}
193+
194+
@Test
195+
public void testBeanListParameter() throws Exception {
196+
ComponentWithClientCallableMethods component = new ComponentWithClientCallableMethods();
197+
ui.add(component);
198+
199+
// Create JSON array with bean objects
200+
ArrayNode beanArray = JacksonUtils.createArrayNode();
201+
202+
ObjectNode bean1 = JacksonUtils.createObjectNode();
203+
bean1.put("name", "First");
204+
bean1.put("value", 1);
205+
bean1.put("active", true);
206+
beanArray.add(bean1);
207+
208+
ObjectNode bean2 = JacksonUtils.createObjectNode();
209+
bean2.put("name", "Second");
210+
bean2.put("value", 2);
211+
bean2.put("active", false);
212+
beanArray.add(bean2);
213+
214+
// Create parameters array
215+
ArrayNode params = JacksonUtils.createArrayNode();
216+
params.add(beanArray);
217+
218+
// Invoke the method directly
219+
PublishedServerEventHandlerRpcHandler.invokeMethod(component,
220+
component.getClass(), "handleBeanList", params, -1);
221+
222+
// Verify the list was properly deserialized
223+
List<SimpleBean> received = component.getReceivedList();
224+
Assert.assertNotNull("List should be received", received);
225+
Assert.assertEquals("Should have 2 beans", 2, received.size());
226+
227+
Assert.assertEquals("First", received.get(0).name);
228+
Assert.assertEquals(1, received.get(0).value);
229+
Assert.assertTrue(received.get(0).active);
230+
231+
Assert.assertEquals("Second", received.get(1).name);
232+
Assert.assertEquals(2, received.get(1).value);
233+
Assert.assertFalse(received.get(1).active);
234+
}
235+
236+
@Test
237+
public void testNestedBeanParameter() throws Exception {
238+
ComponentWithClientCallableMethods component = new ComponentWithClientCallableMethods();
239+
ui.add(component);
240+
241+
// Create JSON for nested bean
242+
ObjectNode innerBean = JacksonUtils.createObjectNode();
243+
innerBean.put("name", "InnerBean");
244+
innerBean.put("value", 456);
245+
innerBean.put("active", false);
246+
247+
ObjectNode nestedBean = JacksonUtils.createObjectNode();
248+
nestedBean.put("title", "OuterBean");
249+
nestedBean.set("simple", innerBean);
250+
251+
// Create parameters array
252+
ArrayNode params = JacksonUtils.createArrayNode();
253+
params.add(nestedBean);
254+
255+
// Invoke the method directly
256+
PublishedServerEventHandlerRpcHandler.invokeMethod(component,
257+
component.getClass(), "handleNestedBean", params, -1);
258+
259+
// Verify the nested bean was properly deserialized
260+
NestedBean received = component.getReceivedNestedBean();
261+
Assert.assertNotNull("Nested bean should be received", received);
262+
Assert.assertEquals("OuterBean", received.title);
263+
Assert.assertNotNull("Inner bean should be present", received.simple);
264+
Assert.assertEquals("InnerBean", received.simple.name);
265+
Assert.assertEquals(456, received.simple.value);
266+
Assert.assertFalse(received.simple.active);
267+
}
268+
269+
@Test
270+
public void testIntegerListParameter() throws Exception {
271+
ComponentWithClientCallableMethods component = new ComponentWithClientCallableMethods();
272+
ui.add(component);
273+
274+
// Create JSON array with integers
275+
ArrayNode intArray = JacksonUtils.createArrayNode();
276+
intArray.add(10);
277+
intArray.add(20);
278+
intArray.add(30);
279+
280+
// Create parameters array
281+
ArrayNode params = JacksonUtils.createArrayNode();
282+
params.add(intArray);
283+
284+
// Invoke the method directly
285+
PublishedServerEventHandlerRpcHandler.invokeMethod(component,
286+
component.getClass(), "handleIntegerList", params, -1);
287+
288+
// Verify the integer list was properly deserialized
289+
List<Integer> received = component.getReceivedIntegerList();
290+
Assert.assertNotNull("Integer list should be received", received);
291+
Assert.assertEquals("Should have 3 integers", 3, received.size());
292+
Assert.assertEquals(Integer.valueOf(10), received.get(0));
293+
Assert.assertEquals(Integer.valueOf(20), received.get(1));
294+
Assert.assertEquals(Integer.valueOf(30), received.get(2));
295+
}
296+
}

0 commit comments

Comments
 (0)