View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
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   * Container for Client use of the javax.websocket API.
73   * <p>
74   * This should be specific to a JVM if run in a standalone mode. or specific to a WebAppContext if running on the Jetty server.
75   */
76  public class ClientContainer extends ContainerLifeCycle implements WebSocketContainer, WebSocketContainerScope, SessionListener
77  {
78      private static final Logger LOG = Log.getLogger(ClientContainer.class);
79      
80      /** The delegated Container Scope */
81      private final WebSocketContainerScope scopeDelegate;
82      /** Tracking all primitive decoders for the container */
83      private final DecoderFactory decoderFactory;
84      /** Tracking all primitive encoders for the container */
85      private final EncoderFactory encoderFactory;
86  
87      /** Tracking for all declared Client endpoints */
88      private final Map<Class<?>, EndpointMetadata> endpointClientMetadataCache;
89      /** Tracking for all open Sessions */
90      private Set<Session> openSessions = new CopyOnWriteArraySet<>();
91      /** The jetty websocket client in use for this container */
92      private WebSocketClient client;
93  
94      public ClientContainer()
95      {
96          // This constructor is used with Standalone JSR Client usage.
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             // Unwrap Actual Cause
155             Throwable cause = e.getCause();
156 
157             if (cause instanceof IOException)
158             {
159                 // Just rethrow
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         // Initialize the default decoder / encoder factories
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                 // Annotated takes precedence here
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                 // extends Endpoint
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      * Used in {@link Session#getOpenSessions()}
333      * @return the set of open sessions
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         // overall message limit (used in non-streaming)
420         client.getPolicy().setMaxBinaryMessageSize(max);
421         // incoming streaming buffer size
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         // overall message limit (used in non-streaming)
435         client.getPolicy().setMaxTextMessageSize(max);
436         // incoming streaming buffer size
437         client.setMaxTextMessageBufferSize(max);
438     }
439 }