View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2013 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.common.io;
20  
21  import java.io.IOException;
22  import java.util.List;
23  import java.util.concurrent.CopyOnWriteArrayList;
24  
25  import org.eclipse.jetty.util.log.Log;
26  import org.eclipse.jetty.util.log.Logger;
27  import org.eclipse.jetty.websocket.api.StatusCode;
28  import org.eclipse.jetty.websocket.common.CloseInfo;
29  import org.eclipse.jetty.websocket.common.ConnectionState;
30  
31  /**
32   * Simple state tracker for Input / Output and {@link ConnectionState}.
33   * <p>
34   * Use the various known .on*() methods to trigger a state change.
35   * <ul>
36   * <li>{@link #onOpened()} - connection has been opened</li>
37   * </ul>
38   */
39  public class IOState
40  {
41      /**
42       * The source of a close handshake. (ie: who initiated it).
43       */
44      private static enum CloseHandshakeSource
45      {
46          /** No close handshake initiated (yet) */
47          NONE,
48          /** Local side initiated the close handshake */
49          LOCAL,
50          /** Remote side initiated the close handshake */
51          REMOTE,
52          /** An abnormal close situation (disconnect, timeout, etc...) */
53          ABNORMAL;
54      }
55  
56      public static interface ConnectionStateListener
57      {
58          public void onConnectionStateChange(ConnectionState state);
59      }
60  
61      private static final Logger LOG = Log.getLogger(IOState.class);
62      private ConnectionState state;
63      private final List<ConnectionStateListener> listeners = new CopyOnWriteArrayList<>();
64  
65      private boolean inputAvailable;
66      private boolean outputAvailable;
67      private CloseHandshakeSource closeHandshakeSource;
68      private CloseInfo closeInfo;
69      private boolean cleanClose;
70  
71      /**
72       * Create a new IOState, initialized to {@link ConnectionState#CONNECTING}
73       */
74      public IOState()
75      {
76          this.state = ConnectionState.CONNECTING;
77          this.inputAvailable = false;
78          this.outputAvailable = false;
79          this.closeHandshakeSource = CloseHandshakeSource.NONE;
80          this.closeInfo = null;
81          this.cleanClose = false;
82      }
83  
84      public void addListener(ConnectionStateListener listener)
85      {
86          listeners.add(listener);
87      }
88  
89      public void assertInputOpen() throws IOException
90      {
91          if (!isInputAvailable())
92          {
93              throw new IOException("Connection input is closed");
94          }
95      }
96  
97      public void assertOutputOpen() throws IOException
98      {
99          if (!isOutputAvailable())
100         {
101             throw new IOException("Connection output is closed");
102         }
103     }
104 
105     public CloseInfo getCloseInfo()
106     {
107         return closeInfo;
108     }
109 
110     public ConnectionState getConnectionState()
111     {
112         return state;
113     }
114 
115     public boolean isClosed()
116     {
117         synchronized (state)
118         {
119             return (state == ConnectionState.CLOSED);
120         }
121     }
122 
123     public boolean isInputAvailable()
124     {
125         return inputAvailable;
126     }
127 
128     public boolean isOpen()
129     {
130         return (getConnectionState() != ConnectionState.CLOSED);
131     }
132 
133     public boolean isOutputAvailable()
134     {
135         return outputAvailable;
136     }
137 
138     private void notifyStateListeners(ConnectionState state)
139     {
140         for (ConnectionStateListener listener : listeners)
141         {
142             listener.onConnectionStateChange(state);
143         }
144     }
145 
146     /**
147      * A websocket connection has been disconnected for abnormal close reasons.
148      * <p>
149      * This is the low level disconnect of the socket. It could be the result of a normal close operation, from an IO error, or even from a timeout.
150      */
151     public void onAbnormalClose(CloseInfo close)
152     {
153         LOG.debug("onAbnormalClose({})",close);
154         ConnectionState event = null;
155         synchronized (this)
156         {
157             if (this.state == ConnectionState.CLOSED)
158             {
159                 // already closed
160                 return;
161             }
162 
163             if (this.state == ConnectionState.OPEN)
164             {
165                 this.cleanClose = false;
166             }
167 
168             this.state = ConnectionState.CLOSED;
169             if (closeInfo == null)
170                 this.closeInfo = close;
171             this.inputAvailable = false;
172             this.outputAvailable = false;
173             this.closeHandshakeSource = CloseHandshakeSource.ABNORMAL;
174             event = this.state;
175         }
176         notifyStateListeners(event);
177     }
178 
179     /**
180      * A close handshake has been issued from the local endpoint
181      */
182     public void onCloseLocal(CloseInfo close)
183     {
184         ConnectionState event = null;
185         ConnectionState initialState = this.state;
186         LOG.debug("onCloseLocal({}) : {}",close,initialState);
187         if (initialState == ConnectionState.CLOSED)
188         {
189             // already closed
190             LOG.debug("already closed");
191             return;
192         }
193 
194         if (initialState == ConnectionState.CONNECTED)
195         {
196             // fast close. a local close request from end-user onConnected() method
197             LOG.debug("FastClose in CONNECTED detected");
198             // Force the state open (to allow read/write to endpoint)
199             onOpened();
200         }
201 
202         synchronized (this)
203         {
204             if (closeInfo == null)
205                 closeInfo = close;
206 
207             boolean in = inputAvailable;
208             boolean out = outputAvailable;
209             if (closeHandshakeSource == CloseHandshakeSource.NONE)
210             {
211                 closeHandshakeSource = CloseHandshakeSource.LOCAL;
212             }
213             out = false;
214             outputAvailable = false;
215 
216             LOG.debug("onCloseLocal(), input={}, output={}",in,out);
217 
218             if (!in && !out)
219             {
220                 LOG.debug("Close Handshake satisfied, disconnecting");
221                 cleanClose = true;
222                 this.state = ConnectionState.CLOSED;
223                 event = this.state;
224             }
225             else if (this.state == ConnectionState.OPEN)
226             {
227                 // We are now entering CLOSING (or half-closed)
228                 this.state = ConnectionState.CLOSING;
229                 event = this.state;
230             }
231         }
232 
233         // Only notify on state change events
234         if (event != null)
235         {
236             LOG.debug("notifying state listeners: {}",event);
237             notifyStateListeners(event);
238 
239             /*
240             // if abnormal, we don't expect an answer.
241             if (close.isAbnormal())
242             {
243                 LOG.debug("Abnormal close, disconnecting");
244                 synchronized (this)
245                 {
246                     state = ConnectionState.CLOSED;
247                     cleanClose = false;
248                     outputAvailable = false;
249                     inputAvailable = false;
250                     closeHandshakeSource = CloseHandshakeSource.ABNORMAL;
251                     event = this.state;
252                 }
253                 notifyStateListeners(event);
254                 return;
255             }
256             */
257         }
258     }
259 
260     /**
261      * A close handshake has been received from the remote endpoint
262      */
263     public void onCloseRemote(CloseInfo close)
264     {
265         LOG.debug("onCloseRemote({})",close);
266         ConnectionState event = null;
267         synchronized (this)
268         {
269             if (this.state == ConnectionState.CLOSED)
270             {
271                 // already closed
272                 return;
273             }
274 
275             if (closeInfo == null)
276                 closeInfo = close;
277 
278             boolean in = inputAvailable;
279             boolean out = outputAvailable;
280             if (closeHandshakeSource == CloseHandshakeSource.NONE)
281             {
282                 closeHandshakeSource = CloseHandshakeSource.REMOTE;
283             }
284             in = false;
285             inputAvailable = false;
286 
287             LOG.debug("onCloseRemote(), input={}, output={}",in,out);
288 
289             if (!in && !out)
290             {
291                 LOG.debug("Close Handshake satisfied, disconnecting");
292                 cleanClose = true;
293                 state = ConnectionState.CLOSED;
294                 event = this.state;
295             }
296             else if (this.state == ConnectionState.OPEN)
297             {
298                 // We are now entering CLOSING (or half-closed)
299                 this.state = ConnectionState.CLOSING;
300                 event = this.state;
301             }
302         }
303 
304         // Only notify on state change events
305         if (event != null)
306         {
307             notifyStateListeners(event);
308         }
309     }
310 
311     /**
312      * WebSocket has successfully upgraded, but the end-user onOpen call hasn't run yet.
313      * <p>
314      * This is an intermediate state between the RFC's {@link ConnectionState#CONNECTING} and {@link ConnectionState#OPEN}
315      */
316     public void onConnected()
317     {
318         if (this.state != ConnectionState.CONNECTING)
319         {
320             LOG.debug("Unable to set to connected, not in CONNECTING state: {}",this.state);
321             return;
322         }
323 
324         ConnectionState event = null;
325         synchronized (this)
326         {
327             this.state = ConnectionState.CONNECTED;
328             inputAvailable = false; // cannot read (yet)
329             outputAvailable = true; // write allowed
330             event = this.state;
331         }
332         notifyStateListeners(event);
333     }
334 
335     /**
336      * A websocket connection has failed its upgrade handshake, and is now closed.
337      */
338     public void onFailedUpgrade()
339     {
340         assert (this.state == ConnectionState.CONNECTING);
341         ConnectionState event = null;
342         synchronized (this)
343         {
344             this.state = ConnectionState.CLOSED;
345             cleanClose = false;
346             inputAvailable = false;
347             outputAvailable = false;
348             event = this.state;
349         }
350         notifyStateListeners(event);
351     }
352 
353     /**
354      * A websocket connection has finished its upgrade handshake, and is now open.
355      */
356     public void onOpened()
357     {
358         if (this.state != ConnectionState.CONNECTED)
359         {
360             LOG.debug("Unable to open, not in CONNECTED state: {}",this.state);
361             return;
362         }
363 
364         assert (this.state == ConnectionState.CONNECTED);
365 
366         ConnectionState event = null;
367         synchronized (this)
368         {
369             this.state = ConnectionState.OPEN;
370             this.inputAvailable = true;
371             this.outputAvailable = true;
372             event = this.state;
373         }
374         notifyStateListeners(event);
375     }
376 
377     /**
378      * The local endpoint has reached a read EOF.
379      * <p>
380      * This could be a normal result after a proper close handshake, or even a premature close due to a connection disconnect.
381      */
382     public void onReadEOF()
383     {
384         ConnectionState event = null;
385         synchronized (this)
386         {
387             if (this.state == ConnectionState.CLOSED)
388             {
389                 // already closed
390                 return;
391             }
392 
393             CloseInfo close = new CloseInfo(StatusCode.NO_CLOSE,"Read EOF");
394 
395             this.cleanClose = false;
396             this.state = ConnectionState.CLOSED;
397             if (closeInfo == null)
398                 this.closeInfo = close;
399             this.inputAvailable = false;
400             this.outputAvailable = false;
401             this.closeHandshakeSource = CloseHandshakeSource.ABNORMAL;
402             event = this.state;
403         }
404         notifyStateListeners(event);
405     }
406 
407     public boolean wasAbnormalClose()
408     {
409         return closeHandshakeSource == CloseHandshakeSource.ABNORMAL;
410     }
411 
412     public boolean wasCleanClose()
413     {
414         return cleanClose;
415     }
416 
417     public boolean wasLocalCloseInitiated()
418     {
419         return closeHandshakeSource == CloseHandshakeSource.LOCAL;
420     }
421 
422     public boolean wasRemoteCloseInitiated()
423     {
424         return closeHandshakeSource == CloseHandshakeSource.REMOTE;
425     }
426 }