001    /**
002     *   GRANITE DATA SERVICES
003     *   Copyright (C) 2006-2013 GRANITE DATA SERVICES S.A.S.
004     *
005     *   This file is part of Granite Data Services.
006     *
007     *   Granite Data Services is free software; you can redistribute it and/or modify
008     *   it under the terms of the GNU Library General Public License as published by
009     *   the Free Software Foundation; either version 2 of the License, or (at your
010     *   option) any later version.
011     *
012     *   Granite Data Services is distributed in the hope that it will be useful, but
013     *   WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
014     *   FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License
015     *   for more details.
016     *
017     *   You should have received a copy of the GNU Library General Public License
018     *   along with this library; if not, see <http://www.gnu.org/licenses/>.
019     */
020    package org.granite.client.messaging.transport.jetty;
021    
022    import java.io.ByteArrayInputStream;
023    import java.io.IOException;
024    import java.net.URI;
025    import java.util.LinkedList;
026    import java.util.concurrent.Future;
027    import java.util.concurrent.TimeoutException;
028    
029    import org.eclipse.jetty.websocket.WebSocket.Connection;
030    import org.eclipse.jetty.websocket.WebSocket.OnBinaryMessage;
031    import org.eclipse.jetty.websocket.WebSocketClient;
032    import org.eclipse.jetty.websocket.WebSocketClientFactory;
033    import org.granite.client.messaging.channel.Channel;
034    import org.granite.client.messaging.transport.AbstractTransport;
035    import org.granite.client.messaging.transport.TransportException;
036    import org.granite.client.messaging.transport.TransportFuture;
037    import org.granite.client.messaging.transport.TransportMessage;
038    import org.granite.client.messaging.transport.WebSocketTransport;
039    import org.granite.logging.Logger;
040    import org.granite.util.PublicByteArrayOutputStream;
041    
042    
043    /**
044     * @author William DRAI
045     */
046    public class JettyWebSocketTransport extends AbstractTransport<Object> implements WebSocketTransport {
047            
048            private static final Logger log = Logger.getLogger(JettyWebSocketTransport.class);
049    
050            private final static int CLOSE_NORMAL = 1000;
051            private final static int CLOSE_SHUTDOWN = 1001;
052    //      private final static int CLOSE_PROTOCOL = 1002;
053            
054            private WebSocketClientFactory webSocketClientFactory = null;
055    
056            private Future<Connection> connectionFuture = null;
057            private boolean connected = false;
058            
059            private int maxIdleTime = 3000000;
060            private int reconnectMaxAttempts = 5;
061            private int reconnectIntervalMillis = 60000;
062            
063            public void setMaxIdleTime(int maxIdleTime) {
064                    this.maxIdleTime = maxIdleTime;
065            }
066            
067            @Override
068            public synchronized boolean start() {
069                    if (isStarted())
070                            return true;
071    
072                    log.info("Starting Jetty WebSocketClient transport...");
073                    
074                    try {
075                            webSocketClientFactory = new WebSocketClientFactory();
076                            webSocketClientFactory.setBufferSize(4096);
077                            webSocketClientFactory.start();
078                            
079                            final long timeout = System.currentTimeMillis() + 10000L; // 10sec.
080                            while (!webSocketClientFactory.isStarted()) {
081                                    if (System.currentTimeMillis() > timeout)
082                                            throw new TimeoutException("Jetty WebSocketFactory start process too long");
083                                    Thread.sleep(100);
084                            }
085                            
086                            log.info("Jetty WebSocketClient transport started.");
087                            return true;
088                    }
089                    catch (Exception e) {
090                            webSocketClientFactory = null;
091                            getStatusHandler().handleException(new TransportException("Could not start Jetty WebSocketFactory", e));
092                            
093                            log.error(e, "Jetty WebSocketClient transport failed to start.");
094                            return false;
095                    }
096            }
097            
098            public synchronized boolean isStarted() {
099                    return webSocketClientFactory != null && webSocketClientFactory.isStarted();
100            }
101            
102            @Override
103            public TransportFuture send(final Channel channel, final TransportMessage message) {
104    
105                    synchronized (channel) {
106    
107                            TransportData transportData = channel.getTransportData();
108                            if (transportData == null) {
109                                    transportData = new TransportData();
110                                    channel.setTransportData(transportData);
111                            }
112                            
113                            if (message != null) {
114                                    if (message.isConnect())
115                                            connectMessage = message;
116                                    else
117                                            transportData.pendingMessages.addLast(message);
118                            }
119                            
120                            if (transportData.connection == null) {
121                                    connect(channel, message);
122                                    return null;
123                            }
124    
125                            while (!transportData.pendingMessages.isEmpty()) {
126                                    TransportMessage pendingMessage = transportData.pendingMessages.removeFirst();
127                                    try {
128                                            PublicByteArrayOutputStream os = new PublicByteArrayOutputStream(256);
129                                            pendingMessage.encode(os);
130                                            byte[] data = os.getBytes();
131                                            transportData.connection.sendMessage(data, 0, os.size());
132                                    }
133                                    catch (IOException e) {
134                                            transportData.pendingMessages.addFirst(pendingMessage);
135                                            // report error...
136                                            break;
137                                    }
138                            }
139                    }
140                    
141                    return null;
142            }
143            
144            @Override
145            public void poll(final Channel channel, final TransportMessage message) {
146                    send(channel, message);
147            }
148            
149            private int reconnectAttempts = 0;
150            private TransportMessage connectMessage = null;
151    
152            public Future<Connection> connect(final Channel channel, final TransportMessage transportMessage) {
153                    if (connectionFuture != null)
154                            return connectionFuture;
155                    
156                    connected = true;
157                    
158                    URI uri = channel.getUri();
159                    
160                    try {           
161                            WebSocketClient webSocketClient = webSocketClientFactory.newWebSocketClient();
162                            webSocketClient.setMaxIdleTime(maxIdleTime);
163                            webSocketClient.setMaxTextMessageSize(1024);
164                            webSocketClient.setProtocol("org.granite.gravity");
165                            
166                            if (transportMessage.getSessionId() != null)
167                                    webSocketClient.getCookies().put("JSESSIONID", transportMessage.getSessionId());
168                            
169                            String u = uri.toString();
170                            u += "?connectId=" + transportMessage.getId() + "&GDSClientType=" + transportMessage.getClientType();
171                            if (transportMessage.getClientId() != null)
172                                    u += "&GDSClientId=" + transportMessage.getClientId();
173                            else if (channel.getClientId() != null)
174                                    u += "&GDSClientId=" + channel.getClientId();
175                            
176                            connectionFuture = webSocketClient.open(new URI(u), new OnBinaryMessage() {
177                                    
178                                    @Override
179                                    public void onOpen(Connection connection) {
180                                            synchronized (channel) {
181                                                    connectionFuture = null;
182                                                    reconnectAttempts = 0;
183                                                    ((TransportData)channel.getTransportData()).connection = connection;
184                                                    send(channel, null);
185                                            }
186                                    }
187                                    
188                                    @Override
189                                    public void onMessage(byte[] data, int offset, int length) {
190                                            channel.onMessage(new ByteArrayInputStream(data, offset, length));
191                                    }
192            
193                                    @Override
194                                    public void onClose(int closeCode, String message) {
195                                            boolean waitBeforeReconnect = !(closeCode == CLOSE_NORMAL && message.startsWith("Idle"));
196                                            
197                                            synchronized (channel) {
198                                                    // Mark the connection as close, the channel should reopen a connection for the next message
199                                                    ((TransportData)channel.getTransportData()).connection = null;
200                                                    connectionFuture = null;
201                                                    
202                                                    if (!isStarted())
203                                                            connected = false;
204                                                    
205                                                    if (closeCode == CLOSE_SHUTDOWN) {
206                                                            connected = false;
207                                                            return;
208                                                    }
209                                                    
210                                                    if (channel.getClientId() == null) {
211                                                            getStatusHandler().handleException(new TransportException("Transport could not connect code: " + closeCode + " " + message));
212                                                            return;
213                                                    }
214                                                    
215                                                    if (connected) {
216                                                            if (reconnectAttempts >= reconnectMaxAttempts) {
217                                                                    connected = false;
218                                                                    if (isStarted())
219                                                                            stop();
220                                                                    
221                                                                    channel.onError(transportMessage, new RuntimeException(message + " (code=" + closeCode + ")"));
222                                                                    getStatusHandler().handleException(new TransportException("Transport disconnected"));
223                                                                    return;
224                                                            }
225                                                            
226                                                            if (waitBeforeReconnect) {
227                                                                    try {
228                                                                            waitBeforeReconnect = false;
229                                                                            Thread.sleep(reconnectIntervalMillis);
230                                                                    }
231                                                                    catch (InterruptedException e) {
232                                                                    }
233                                                            }
234                                                            
235                                                            reconnectAttempts++;
236                                                            
237                                                            // If the channel should be connected, try to reconnect
238                                                            log.info("Connection lost (code %d, msg %s), reconnect channel (retry #%d)", closeCode, message, reconnectAttempts);
239                                                            connect(channel, connectMessage);
240                                                    }
241                                            }
242                                    }
243                            });
244                            
245                            return connectionFuture;
246                    }
247                    catch (Exception e) {
248                            getStatusHandler().handleException(new TransportException("Could not connect to uri " + channel.getUri(), e));
249                            
250                            return null;
251                    }
252            }
253            
254            private static class TransportData {
255                    
256                    private final LinkedList<TransportMessage> pendingMessages = new LinkedList<TransportMessage>();
257                    private Connection connection = null;
258            }
259    
260            @Override
261            public synchronized void stop() {
262                    if (webSocketClientFactory == null)
263                            return;
264                    
265                    log.info("Stopping Jetty WebSocketClient transport...");
266                    
267                    super.stop();
268                    
269                    try {
270                            webSocketClientFactory.stop();
271                    }
272                    catch (Exception e) {
273                            getStatusHandler().handleException(new TransportException("Could not stop Jetty WebSocketFactory", e));
274    
275                            log.error(e, "Jetty WebSocketClient failed to stop properly.");
276                    }
277                    finally {
278                            webSocketClientFactory = null;
279                    }
280                    
281                    log.info("Jetty WebSocketClient transport stopped.");
282            }
283    }