View Javadoc

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