Skip to content

Commit 6dff924

Browse files
committed
WebSockets Next: client endpoints
- add client endpoints and WebSocketConnector API - also includes refactoring of the server part so that we can reuse as much as possible
1 parent 7470de8 commit 6dff924

File tree

76 files changed

+3380
-1095
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+3380
-1095
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
package io.quarkus.websockets.next.deployment;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import java.util.Set;
6+
import java.util.function.Predicate;
7+
8+
import org.jboss.jandex.AnnotationInstance;
9+
import org.jboss.jandex.AnnotationValue;
10+
import org.jboss.jandex.DotName;
11+
import org.jboss.jandex.IndexView;
12+
import org.jboss.jandex.MethodInfo;
13+
import org.jboss.jandex.MethodParameterInfo;
14+
import org.jboss.jandex.Type;
15+
import org.jboss.jandex.Type.Kind;
16+
17+
import io.quarkus.arc.deployment.TransformedAnnotationsBuildItem;
18+
import io.quarkus.arc.processor.Annotations;
19+
import io.quarkus.arc.processor.DotNames;
20+
import io.quarkus.gizmo.BytecodeCreator;
21+
import io.quarkus.gizmo.FieldDescriptor;
22+
import io.quarkus.gizmo.ResultHandle;
23+
import io.quarkus.websockets.next.WebSocketException;
24+
import io.quarkus.websockets.next.deployment.CallbackArgument.InvocationBytecodeContext;
25+
import io.quarkus.websockets.next.deployment.CallbackArgument.ParameterContext;
26+
import io.quarkus.websockets.next.runtime.WebSocketConnectionBase;
27+
import io.quarkus.websockets.next.runtime.WebSocketEndpoint.ExecutionModel;
28+
import io.quarkus.websockets.next.runtime.WebSocketEndpointBase;
29+
30+
/**
31+
* Represents either an endpoint callback or a global error handler.
32+
*/
33+
public class Callback {
34+
35+
public final Target target;
36+
public final String endpointPath;
37+
public final AnnotationInstance annotation;
38+
public final MethodInfo method;
39+
public final ExecutionModel executionModel;
40+
public final MessageType messageType;
41+
public final List<CallbackArgument> arguments;
42+
43+
public Callback(Target target, AnnotationInstance annotation, MethodInfo method, ExecutionModel executionModel,
44+
CallbackArgumentsBuildItem callbackArguments, TransformedAnnotationsBuildItem transformedAnnotations,
45+
String endpointPath, IndexView index) {
46+
this.target = target;
47+
this.method = method;
48+
this.annotation = annotation;
49+
this.executionModel = executionModel;
50+
if (WebSocketDotNames.ON_BINARY_MESSAGE.equals(annotation.name())) {
51+
this.messageType = MessageType.BINARY;
52+
} else if (WebSocketDotNames.ON_TEXT_MESSAGE.equals(annotation.name())) {
53+
this.messageType = MessageType.TEXT;
54+
} else if (WebSocketDotNames.ON_PONG_MESSAGE.equals(annotation.name())) {
55+
this.messageType = MessageType.PONG;
56+
} else {
57+
this.messageType = MessageType.NONE;
58+
}
59+
this.endpointPath = endpointPath;
60+
this.arguments = collectArguments(annotation, method, callbackArguments, transformedAnnotations, index);
61+
}
62+
63+
public boolean isGlobal() {
64+
return endpointPath == null;
65+
}
66+
67+
public boolean isClient() {
68+
return target == Target.CLIENT;
69+
}
70+
71+
public boolean isServer() {
72+
return target == Target.SERVER;
73+
}
74+
75+
public boolean isOnOpen() {
76+
return annotation.name().equals(WebSocketDotNames.ON_OPEN);
77+
}
78+
79+
public boolean isOnClose() {
80+
return annotation.name().equals(WebSocketDotNames.ON_CLOSE);
81+
}
82+
83+
public boolean isOnError() {
84+
return annotation.name().equals(WebSocketDotNames.ON_ERROR);
85+
}
86+
87+
public Type returnType() {
88+
return method.returnType();
89+
}
90+
91+
public Type messageParamType() {
92+
return acceptsMessage() ? method.parameterType(0) : null;
93+
}
94+
95+
public boolean isReturnTypeVoid() {
96+
return returnType().kind() == Kind.VOID;
97+
}
98+
99+
public boolean isReturnTypeUni() {
100+
return WebSocketDotNames.UNI.equals(returnType().name());
101+
}
102+
103+
public boolean isReturnTypeMulti() {
104+
return WebSocketDotNames.MULTI.equals(returnType().name());
105+
}
106+
107+
public boolean acceptsMessage() {
108+
return messageType != MessageType.NONE;
109+
}
110+
111+
public boolean acceptsBinaryMessage() {
112+
return messageType == MessageType.BINARY || messageType == MessageType.PONG;
113+
}
114+
115+
public boolean acceptsMulti() {
116+
return acceptsMessage() && method.parameterType(0).name().equals(WebSocketDotNames.MULTI);
117+
}
118+
119+
public Callback.MessageType messageType() {
120+
return messageType;
121+
}
122+
123+
public boolean broadcast() {
124+
AnnotationValue broadcastValue = annotation.value("broadcast");
125+
return broadcastValue != null && broadcastValue.asBoolean();
126+
}
127+
128+
public DotName getInputCodec() {
129+
return getCodec("codec");
130+
}
131+
132+
public DotName getOutputCodec() {
133+
DotName output = getCodec("outputCodec");
134+
return output != null ? output : getInputCodec();
135+
}
136+
137+
public String asString() {
138+
return method.declaringClass().name() + "#" + method.name() + "()";
139+
}
140+
141+
private DotName getCodec(String valueName) {
142+
AnnotationValue codecValue = annotation.value(valueName);
143+
if (codecValue != null) {
144+
return codecValue.asClass().name();
145+
}
146+
return null;
147+
}
148+
149+
public enum MessageType {
150+
NONE,
151+
PONG,
152+
TEXT,
153+
BINARY
154+
}
155+
156+
public enum Target {
157+
CLIENT,
158+
SERVER,
159+
UNDEFINED
160+
}
161+
162+
public ResultHandle[] generateArguments(ResultHandle endpointThis, BytecodeCreator bytecode,
163+
TransformedAnnotationsBuildItem transformedAnnotations, IndexView index) {
164+
if (arguments.isEmpty()) {
165+
return new ResultHandle[] {};
166+
}
167+
ResultHandle[] resultHandles = new ResultHandle[arguments.size()];
168+
int idx = 0;
169+
for (CallbackArgument argument : arguments) {
170+
resultHandles[idx] = argument.get(
171+
invocationBytecodeContext(annotation, method.parameters().get(idx), transformedAnnotations, index,
172+
endpointThis, bytecode));
173+
idx++;
174+
}
175+
return resultHandles;
176+
}
177+
178+
private List<CallbackArgument> collectArguments(AnnotationInstance annotation, MethodInfo method,
179+
CallbackArgumentsBuildItem callbackArguments, TransformedAnnotationsBuildItem transformedAnnotations,
180+
IndexView index) {
181+
List<MethodParameterInfo> parameters = method.parameters();
182+
if (parameters.isEmpty()) {
183+
return List.of();
184+
}
185+
List<CallbackArgument> arguments = new ArrayList<>(parameters.size());
186+
for (MethodParameterInfo parameter : parameters) {
187+
List<CallbackArgument> found = callbackArguments
188+
.findMatching(parameterContext(annotation, parameter, transformedAnnotations, index));
189+
if (found.isEmpty()) {
190+
String msg = String.format("Unable to inject @%s callback parameter '%s' declared on %s: no injector found",
191+
DotNames.simpleName(annotation.name()),
192+
parameter.name() != null ? parameter.name() : "#" + parameter.position(),
193+
asString());
194+
throw new WebSocketException(msg);
195+
} else if (found.size() > 1 && (found.get(0).priotity() == found.get(1).priotity())) {
196+
String msg = String.format(
197+
"Unable to inject @%s callback parameter '%s' declared on %s: ambiguous injectors found: %s",
198+
DotNames.simpleName(annotation.name()),
199+
parameter.name() != null ? parameter.name() : "#" + parameter.position(),
200+
asString(),
201+
found.stream().map(p -> p.getClass().getSimpleName() + ":" + p.priotity()));
202+
throw new WebSocketException(msg);
203+
}
204+
arguments.add(found.get(0));
205+
}
206+
return List.copyOf(arguments);
207+
}
208+
209+
Type argumentType(Predicate<CallbackArgument> filter) {
210+
for (int i = 0; i < arguments.size(); i++) {
211+
if (filter.test(arguments.get(i))) {
212+
return method.parameterType(i);
213+
}
214+
}
215+
return null;
216+
}
217+
218+
private ParameterContext parameterContext(AnnotationInstance callbackAnnotation, MethodParameterInfo parameter,
219+
TransformedAnnotationsBuildItem transformedAnnotations, IndexView index) {
220+
return new ParameterContext() {
221+
222+
@Override
223+
public Target callbackTarget() {
224+
return target;
225+
}
226+
227+
@Override
228+
public MethodParameterInfo parameter() {
229+
return parameter;
230+
}
231+
232+
@Override
233+
public Set<AnnotationInstance> parameterAnnotations() {
234+
return Annotations.getParameterAnnotations(
235+
transformedAnnotations::getAnnotations, parameter.method(), parameter.position());
236+
}
237+
238+
@Override
239+
public AnnotationInstance callbackAnnotation() {
240+
return callbackAnnotation;
241+
}
242+
243+
@Override
244+
public String endpointPath() {
245+
return endpointPath;
246+
}
247+
248+
@Override
249+
public IndexView index() {
250+
return index;
251+
}
252+
253+
};
254+
}
255+
256+
private InvocationBytecodeContext invocationBytecodeContext(AnnotationInstance callbackAnnotation,
257+
MethodParameterInfo parameter, TransformedAnnotationsBuildItem transformedAnnotations, IndexView index,
258+
ResultHandle endpointThis, BytecodeCreator bytecode) {
259+
return new InvocationBytecodeContext() {
260+
261+
@Override
262+
public Target callbackTarget() {
263+
return target;
264+
}
265+
266+
@Override
267+
public AnnotationInstance callbackAnnotation() {
268+
return callbackAnnotation;
269+
}
270+
271+
@Override
272+
public MethodParameterInfo parameter() {
273+
return parameter;
274+
}
275+
276+
@Override
277+
public Set<AnnotationInstance> parameterAnnotations() {
278+
return Annotations.getParameterAnnotations(
279+
transformedAnnotations::getAnnotations, parameter.method(), parameter.position());
280+
}
281+
282+
@Override
283+
public String endpointPath() {
284+
return endpointPath;
285+
}
286+
287+
@Override
288+
public IndexView index() {
289+
return index;
290+
}
291+
292+
@Override
293+
public BytecodeCreator bytecode() {
294+
return bytecode;
295+
}
296+
297+
@Override
298+
public ResultHandle getPayload() {
299+
return acceptsMessage() || callbackAnnotation.name().equals(WebSocketDotNames.ON_ERROR)
300+
? bytecode.getMethodParam(0)
301+
: null;
302+
}
303+
304+
@Override
305+
public ResultHandle getDecodedMessage(Type parameterType) {
306+
return acceptsMessage()
307+
? WebSocketProcessor.decodeMessage(endpointThis, bytecode, acceptsBinaryMessage(),
308+
parameterType,
309+
getPayload(), Callback.this)
310+
: null;
311+
}
312+
313+
@Override
314+
public ResultHandle getConnection() {
315+
return bytecode.readInstanceField(
316+
FieldDescriptor.of(WebSocketEndpointBase.class, "connection", WebSocketConnectionBase.class),
317+
endpointThis);
318+
}
319+
};
320+
}
321+
322+
}

extensions/websockets-next/server/deployment/src/main/java/io/quarkus/websockets/next/deployment/CallbackArgument.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
import io.quarkus.websockets.next.OnError;
1414
import io.quarkus.websockets.next.OnOpen;
1515
import io.quarkus.websockets.next.WebSocketConnection;
16-
import io.quarkus.websockets.next.WebSocketServerException;
16+
import io.quarkus.websockets.next.WebSocketException;
17+
import io.quarkus.websockets.next.deployment.Callback.Target;
1718

1819
/**
1920
* Provides arguments for method parameters of a callback method declared on a WebSocket endpoint.
@@ -24,7 +25,7 @@ interface CallbackArgument {
2425
*
2526
* @param context
2627
* @return {@code true} if this provider matches the given parameter context, {@code false} otherwise
27-
* @throws WebSocketServerException If an invalid parameter is detected
28+
* @throws WebSocketException If an invalid parameter is detected
2829
*/
2930
boolean matches(ParameterContext context);
3031

@@ -49,6 +50,12 @@ default int priotity() {
4950

5051
interface ParameterContext {
5152

53+
/**
54+
*
55+
* @return the callback target
56+
*/
57+
Target callbackTarget();
58+
5259
/**
5360
*
5461
* @return the endpoint path or {@code null} for global error handlers

extensions/websockets-next/server/deployment/src/main/java/io/quarkus/websockets/next/deployment/ConnectionCallbackArgument.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,30 @@
11
package io.quarkus.websockets.next.deployment;
22

3+
import org.jboss.jandex.DotName;
4+
35
import io.quarkus.gizmo.ResultHandle;
6+
import io.quarkus.websockets.next.WebSocketException;
7+
import io.quarkus.websockets.next.deployment.Callback.Target;
48

59
class ConnectionCallbackArgument implements CallbackArgument {
610

711
@Override
812
public boolean matches(ParameterContext context) {
9-
return context.parameter().type().name().equals(WebSocketDotNames.WEB_SOCKET_CONNECTION);
13+
DotName paramTypeName = context.parameter().type().name();
14+
if (context.callbackTarget() == Target.SERVER) {
15+
if (WebSocketDotNames.WEB_SOCKET_CONNECTION.equals(paramTypeName)) {
16+
return true;
17+
} else if (WebSocketDotNames.WEB_SOCKET_CLIENT_CONNECTION.equals(paramTypeName)) {
18+
throw new WebSocketException("@WebSocket callback method may not accept WebSocketClientConnection");
19+
}
20+
} else if (context.callbackTarget() == Target.CLIENT) {
21+
if (WebSocketDotNames.WEB_SOCKET_CLIENT_CONNECTION.equals(paramTypeName)) {
22+
return true;
23+
} else if (WebSocketDotNames.WEB_SOCKET_CONNECTION.equals(paramTypeName)) {
24+
throw new WebSocketException("@WebSocketClient callback method may not accept WebSocketConnection");
25+
}
26+
}
27+
return false;
1028
}
1129

1230
@Override

0 commit comments

Comments
 (0)