View Javadoc

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