1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.eclipse.jetty.websocket.jsr356;
20
21 import java.io.IOException;
22 import java.net.URI;
23 import java.util.Collections;
24 import java.util.HashSet;
25 import java.util.Map;
26 import java.util.Objects;
27 import java.util.Set;
28 import java.util.concurrent.ConcurrentHashMap;
29 import java.util.concurrent.CopyOnWriteArraySet;
30 import java.util.concurrent.ExecutionException;
31 import java.util.concurrent.Executor;
32 import java.util.concurrent.Future;
33
34 import javax.websocket.ClientEndpoint;
35 import javax.websocket.ClientEndpointConfig;
36 import javax.websocket.DeploymentException;
37 import javax.websocket.Endpoint;
38 import javax.websocket.EndpointConfig;
39 import javax.websocket.Extension;
40 import javax.websocket.Session;
41 import javax.websocket.WebSocketContainer;
42
43 import org.eclipse.jetty.io.ByteBufferPool;
44 import org.eclipse.jetty.util.DecoratedObjectFactory;
45 import org.eclipse.jetty.util.component.ContainerLifeCycle;
46 import org.eclipse.jetty.util.log.Log;
47 import org.eclipse.jetty.util.log.Logger;
48 import org.eclipse.jetty.util.ssl.SslContextFactory;
49 import org.eclipse.jetty.util.thread.ShutdownThread;
50 import org.eclipse.jetty.websocket.api.InvalidWebSocketException;
51 import org.eclipse.jetty.websocket.api.WebSocketPolicy;
52 import org.eclipse.jetty.websocket.api.extensions.ExtensionFactory;
53 import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
54 import org.eclipse.jetty.websocket.client.WebSocketClient;
55 import org.eclipse.jetty.websocket.client.io.UpgradeListener;
56 import org.eclipse.jetty.websocket.common.SessionFactory;
57 import org.eclipse.jetty.websocket.common.SessionListener;
58 import org.eclipse.jetty.websocket.common.WebSocketSession;
59 import org.eclipse.jetty.websocket.common.scopes.SimpleContainerScope;
60 import org.eclipse.jetty.websocket.common.scopes.WebSocketContainerScope;
61 import org.eclipse.jetty.websocket.jsr356.annotations.AnnotatedEndpointScanner;
62 import org.eclipse.jetty.websocket.jsr356.client.AnnotatedClientEndpointMetadata;
63 import org.eclipse.jetty.websocket.jsr356.client.EmptyClientEndpointConfig;
64 import org.eclipse.jetty.websocket.jsr356.client.SimpleEndpointMetadata;
65 import org.eclipse.jetty.websocket.jsr356.decoders.PrimitiveDecoderMetadataSet;
66 import org.eclipse.jetty.websocket.jsr356.encoders.PrimitiveEncoderMetadataSet;
67 import org.eclipse.jetty.websocket.jsr356.endpoints.EndpointInstance;
68 import org.eclipse.jetty.websocket.jsr356.endpoints.JsrEventDriverFactory;
69 import org.eclipse.jetty.websocket.jsr356.metadata.EndpointMetadata;
70
71
72
73
74
75
76 public class ClientContainer extends ContainerLifeCycle implements WebSocketContainer, WebSocketContainerScope, SessionListener
77 {
78 private static final Logger LOG = Log.getLogger(ClientContainer.class);
79
80
81 private final WebSocketContainerScope scopeDelegate;
82
83 private final DecoderFactory decoderFactory;
84
85 private final EncoderFactory encoderFactory;
86
87
88 private final Map<Class<?>, EndpointMetadata> endpointClientMetadataCache;
89
90 private Set<Session> openSessions = new CopyOnWriteArraySet<>();
91
92 private WebSocketClient client;
93
94 public ClientContainer()
95 {
96
97 this(new SimpleContainerScope(WebSocketPolicy.newClientPolicy()));
98 client.setDaemon(true);
99 }
100
101 public ClientContainer(WebSocketContainerScope scope)
102 {
103 boolean trustAll = Boolean.getBoolean("org.eclipse.jetty.websocket.jsr356.ssl-trust-all");
104
105 this.scopeDelegate = scope;
106 client = new WebSocketClient(scope, new SslContextFactory(trustAll));
107 client.setEventDriverFactory(new JsrEventDriverFactory(client.getPolicy()));
108 SessionFactory sessionFactory = new JsrSessionFactory(this,this,client);
109 client.setSessionFactory(sessionFactory);
110 addBean(client);
111
112 this.endpointClientMetadataCache = new ConcurrentHashMap<>();
113 this.decoderFactory = new DecoderFactory(this,PrimitiveDecoderMetadataSet.INSTANCE);
114 this.encoderFactory = new EncoderFactory(this,PrimitiveEncoderMetadataSet.INSTANCE);
115
116 ShutdownThread.register(this);
117 }
118
119 private Session connect(EndpointInstance instance, URI path) throws IOException
120 {
121 Objects.requireNonNull(instance,"EndpointInstance cannot be null");
122 Objects.requireNonNull(path,"Path cannot be null");
123
124 ClientEndpointConfig config = (ClientEndpointConfig)instance.getConfig();
125 ClientUpgradeRequest req = new ClientUpgradeRequest();
126 UpgradeListener upgradeListener = null;
127
128 for (Extension ext : config.getExtensions())
129 {
130 req.addExtensions(new JsrExtensionConfig(ext));
131 }
132
133 if (config.getPreferredSubprotocols().size() > 0)
134 {
135 req.setSubProtocols(config.getPreferredSubprotocols());
136 }
137
138 if (config.getConfigurator() != null)
139 {
140 upgradeListener = new JsrUpgradeListener(config.getConfigurator());
141 }
142
143 Future<org.eclipse.jetty.websocket.api.Session> futSess = client.connect(instance,path,req,upgradeListener);
144 try
145 {
146 return (JsrSession)futSess.get();
147 }
148 catch (InterruptedException e)
149 {
150 throw new IOException("Connect failure",e);
151 }
152 catch (ExecutionException e)
153 {
154
155 Throwable cause = e.getCause();
156
157 if (cause instanceof IOException)
158 {
159
160 throw (IOException)cause;
161 }
162 else
163 {
164 throw new IOException("Connect failure",cause);
165 }
166 }
167 }
168
169 @Override
170 public Session connectToServer(Class<? extends Endpoint> endpointClass, ClientEndpointConfig config, URI path) throws DeploymentException, IOException
171 {
172 EndpointInstance instance = newClientEndpointInstance(endpointClass,config);
173 return connect(instance,path);
174 }
175
176 @Override
177 public Session connectToServer(Class<?> annotatedEndpointClass, URI path) throws DeploymentException, IOException
178 {
179 EndpointInstance instance = newClientEndpointInstance(annotatedEndpointClass,null);
180 return connect(instance,path);
181 }
182
183 @Override
184 public Session connectToServer(Endpoint endpoint, ClientEndpointConfig config, URI path) throws DeploymentException, IOException
185 {
186 EndpointInstance instance = newClientEndpointInstance(endpoint,config);
187 return connect(instance,path);
188 }
189
190 @Override
191 public Session connectToServer(Object endpoint, URI path) throws DeploymentException, IOException
192 {
193 EndpointInstance instance = newClientEndpointInstance(endpoint,null);
194 return connect(instance,path);
195 }
196
197 @Override
198 protected void doStart() throws Exception
199 {
200 super.doStart();
201
202
203 EmptyClientEndpointConfig empty = new EmptyClientEndpointConfig();
204 this.decoderFactory.init(empty);
205 this.encoderFactory.init(empty);
206 }
207
208 @Override
209 protected void doStop() throws Exception
210 {
211 ShutdownThread.deregister(this);
212 endpointClientMetadataCache.clear();
213 super.doStop();
214 }
215
216 @Override
217 public ByteBufferPool getBufferPool()
218 {
219 return scopeDelegate.getBufferPool();
220 }
221
222 public WebSocketClient getClient()
223 {
224 return client;
225 }
226
227 public EndpointMetadata getClientEndpointMetadata(Class<?> endpoint, EndpointConfig config)
228 {
229 EndpointMetadata metadata = null;
230
231 synchronized (endpointClientMetadataCache)
232 {
233 metadata = endpointClientMetadataCache.get(endpoint);
234
235 if (metadata != null)
236 {
237 return metadata;
238 }
239
240 ClientEndpoint anno = endpoint.getAnnotation(ClientEndpoint.class);
241 if (anno != null)
242 {
243
244 AnnotatedClientEndpointMetadata annoMetadata = new AnnotatedClientEndpointMetadata(this,endpoint);
245 AnnotatedEndpointScanner<ClientEndpoint, ClientEndpointConfig> scanner = new AnnotatedEndpointScanner<>(annoMetadata);
246 scanner.scan();
247 metadata = annoMetadata;
248 }
249 else if (Endpoint.class.isAssignableFrom(endpoint))
250 {
251
252 @SuppressWarnings("unchecked")
253 Class<? extends Endpoint> eendpoint = (Class<? extends Endpoint>)endpoint;
254 metadata = new SimpleEndpointMetadata(eendpoint,config);
255 }
256 else
257 {
258 StringBuilder err = new StringBuilder();
259 err.append("Not a recognized websocket [");
260 err.append(endpoint.getName());
261 err.append("] does not extend @").append(ClientEndpoint.class.getName());
262 err.append(" or extend from ").append(Endpoint.class.getName());
263 throw new InvalidWebSocketException("Unable to identify as valid Endpoint: " + endpoint);
264 }
265
266 endpointClientMetadataCache.put(endpoint,metadata);
267 return metadata;
268 }
269 }
270
271 public DecoderFactory getDecoderFactory()
272 {
273 return decoderFactory;
274 }
275
276 @Override
277 public long getDefaultAsyncSendTimeout()
278 {
279 return client.getAsyncWriteTimeout();
280 }
281
282 @Override
283 public int getDefaultMaxBinaryMessageBufferSize()
284 {
285 return client.getMaxBinaryMessageBufferSize();
286 }
287
288 @Override
289 public long getDefaultMaxSessionIdleTimeout()
290 {
291 return client.getMaxIdleTimeout();
292 }
293
294 @Override
295 public int getDefaultMaxTextMessageBufferSize()
296 {
297 return client.getMaxTextMessageBufferSize();
298 }
299
300 public EncoderFactory getEncoderFactory()
301 {
302 return encoderFactory;
303 }
304
305 @Override
306 public Executor getExecutor()
307 {
308 return scopeDelegate.getExecutor();
309 }
310
311 @Override
312 public Set<Extension> getInstalledExtensions()
313 {
314 Set<Extension> ret = new HashSet<>();
315 ExtensionFactory extensions = client.getExtensionFactory();
316
317 for (String name : extensions.getExtensionNames())
318 {
319 ret.add(new JsrExtension(name));
320 }
321
322 return ret;
323 }
324
325 @Override
326 public DecoratedObjectFactory getObjectFactory()
327 {
328 return scopeDelegate.getObjectFactory();
329 }
330
331
332
333
334
335 public Set<Session> getOpenSessions()
336 {
337 return Collections.unmodifiableSet(this.openSessions);
338 }
339
340 @Override
341 public WebSocketPolicy getPolicy()
342 {
343 return scopeDelegate.getPolicy();
344 }
345
346 @Override
347 public SslContextFactory getSslContextFactory()
348 {
349 return scopeDelegate.getSslContextFactory();
350 }
351
352 private EndpointInstance newClientEndpointInstance(Class<?> endpointClass, ClientEndpointConfig config)
353 {
354 try
355 {
356 return newClientEndpointInstance(endpointClass.newInstance(),config);
357 }
358 catch (InstantiationException | IllegalAccessException e)
359 {
360 throw new InvalidWebSocketException("Unable to instantiate websocket: " + endpointClass.getClass());
361 }
362 }
363
364 public EndpointInstance newClientEndpointInstance(Object endpoint, ClientEndpointConfig config)
365 {
366 EndpointMetadata metadata = getClientEndpointMetadata(endpoint.getClass(),config);
367 ClientEndpointConfig cec = config;
368 if (config == null)
369 {
370 if (metadata instanceof AnnotatedClientEndpointMetadata)
371 {
372 cec = ((AnnotatedClientEndpointMetadata)metadata).getConfig();
373 }
374 else
375 {
376 cec = new EmptyClientEndpointConfig();
377 }
378 }
379 return new EndpointInstance(endpoint,cec,metadata);
380 }
381
382 @Override
383 public void onSessionClosed(WebSocketSession session)
384 {
385 if (session instanceof Session)
386 {
387 this.openSessions.remove((Session)session);
388 }
389 else
390 {
391 LOG.warn("JSR356 Implementation should not be mixed with native implementation: Expected {} to implement {}",session.getClass().getName(),
392 Session.class.getName());
393 }
394 }
395
396 @Override
397 public void onSessionOpened(WebSocketSession session)
398 {
399 if (session instanceof Session)
400 {
401 this.openSessions.add((Session)session);
402 }
403 else
404 {
405 LOG.warn("JSR356 Implementation should not be mixed with native implementation: Expected {} to implement {}",session.getClass().getName(),
406 Session.class.getName());
407 }
408 }
409
410 @Override
411 public void setAsyncSendTimeout(long ms)
412 {
413 client.setAsyncWriteTimeout(ms);
414 }
415
416 @Override
417 public void setDefaultMaxBinaryMessageBufferSize(int max)
418 {
419
420 client.getPolicy().setMaxBinaryMessageSize(max);
421
422 client.setMaxBinaryMessageBufferSize(max);
423 }
424
425 @Override
426 public void setDefaultMaxSessionIdleTimeout(long ms)
427 {
428 client.setMaxIdleTimeout(ms);
429 }
430
431 @Override
432 public void setDefaultMaxTextMessageBufferSize(int max)
433 {
434
435 client.getPolicy().setMaxTextMessageSize(max);
436
437 client.setMaxTextMessageBufferSize(max);
438 }
439 }