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 }