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.channel;
021
022 import java.io.IOException;
023 import java.io.InputStream;
024 import java.io.UnsupportedEncodingException;
025 import java.net.URI;
026 import java.util.Timer;
027 import java.util.TimerTask;
028 import java.util.concurrent.BlockingQueue;
029 import java.util.concurrent.ConcurrentHashMap;
030 import java.util.concurrent.ConcurrentMap;
031 import java.util.concurrent.ExecutionException;
032 import java.util.concurrent.LinkedBlockingQueue;
033 import java.util.concurrent.Semaphore;
034 import java.util.concurrent.TimeUnit;
035 import java.util.concurrent.TimeoutException;
036
037 import org.granite.client.messaging.AllInOneResponseListener;
038 import org.granite.client.messaging.ResponseListener;
039 import org.granite.client.messaging.ResponseListenerDispatcher;
040 import org.granite.client.messaging.events.Event;
041 import org.granite.client.messaging.events.Event.Type;
042 import org.granite.client.messaging.messages.MessageChain;
043 import org.granite.client.messaging.messages.RequestMessage;
044 import org.granite.client.messaging.messages.ResponseMessage;
045 import org.granite.client.messaging.messages.requests.LoginMessage;
046 import org.granite.client.messaging.messages.requests.LogoutMessage;
047 import org.granite.client.messaging.messages.requests.PingMessage;
048 import org.granite.client.messaging.messages.responses.FaultMessage;
049 import org.granite.client.messaging.messages.responses.ResultMessage;
050 import org.granite.client.messaging.transport.Transport;
051 import org.granite.client.messaging.transport.TransportFuture;
052 import org.granite.client.messaging.transport.TransportMessage;
053 import org.granite.client.messaging.transport.TransportStopListener;
054 import org.granite.logging.Logger;
055
056 /**
057 * @author Franck WOLFF
058 */
059 public abstract class AbstractHTTPChannel extends AbstractChannel<Transport> implements TransportStopListener, Runnable {
060
061 private static final Logger log = Logger.getLogger(AbstractHTTPChannel.class);
062
063 private final BlockingQueue<AsyncToken> tokensQueue = new LinkedBlockingQueue<AsyncToken>();
064 private final ConcurrentMap<String, AsyncToken> tokensMap = new ConcurrentHashMap<String, AsyncToken>();
065
066 private Thread senderThread = null;
067 private Semaphore connections;
068 private Timer timer = null;
069
070 protected volatile boolean pinged = false;
071 protected volatile boolean authenticated = false;
072 protected volatile int maxConcurrentRequests;
073 protected volatile long defaultTimeToLive = DEFAULT_TIME_TO_LIVE; // 1 mn.
074
075 public AbstractHTTPChannel(Transport transport, String id, URI uri, int maxConcurrentRequests) {
076 super(transport, id, uri);
077
078 if (maxConcurrentRequests < 1)
079 throw new IllegalArgumentException("maxConcurrentRequests must be greater or equal to 1");
080
081 this.maxConcurrentRequests = maxConcurrentRequests;
082 }
083
084 protected abstract TransportMessage createTransportMessage(AsyncToken token) throws UnsupportedEncodingException;
085
086 protected abstract ResponseMessage decodeResponse(InputStream is) throws IOException;
087
088 protected boolean schedule(TimerTask timerTask, long delay) {
089 if (timer != null) {
090 timer.schedule(timerTask, delay);
091 return true;
092 }
093 return false;
094 }
095
096 public long getDefaultTimeToLive() {
097 return defaultTimeToLive;
098 }
099
100 public void setDefaultTimeToLive(long defaultTimeToLive) {
101 this.defaultTimeToLive = defaultTimeToLive;
102 }
103
104 public boolean isAuthenticated() {
105 return authenticated;
106 }
107
108 public int getMaxConcurrentRequests() {
109 return maxConcurrentRequests;
110 }
111
112 @Override
113 public void onStop(Transport transport) {
114 stop();
115 }
116
117 @Override
118 public synchronized boolean start() {
119 if (senderThread == null) {
120 log.info("Starting channel %s...", id);
121 senderThread = new Thread(this);
122 try {
123 timer = new Timer(id + "_timer", true);
124 connections = new Semaphore(maxConcurrentRequests);
125 senderThread.start();
126
127 transport.addStopListener(this);
128
129 log.info("Channel %s started.", id);
130 }
131 catch (Exception e) {
132 if (timer != null) {
133 timer.cancel();
134 timer = null;
135 }
136 connections = null;
137 senderThread = null;
138 log.error(e, "Channel %s failed to start.", id);
139 return false;
140 }
141 }
142 return true;
143 }
144
145 @Override
146 public synchronized boolean isStarted() {
147 return senderThread != null;
148 }
149
150 @Override
151 public synchronized boolean stop() {
152 if (senderThread != null) {
153 log.info("Stopping channel %s...", id);
154
155 if (timer != null) {
156 try {
157 timer.cancel();
158 }
159 catch (Exception e) {
160 log.error(e, "Channel %s timer failed to stop.", id);
161 }
162 finally {
163 timer = null;
164 }
165 }
166
167 connections = null;
168
169 tokensMap.clear();
170 tokensQueue.clear();
171
172 Thread thread = this.senderThread;
173 senderThread = null;
174 thread.interrupt();
175
176 pinged = false;
177 clientId = null;
178 authenticated = false;
179
180 return true;
181 }
182 return false;
183 }
184
185 @Override
186 public void run() {
187
188 while (!Thread.interrupted()) {
189 try {
190 AsyncToken token = tokensQueue.take();
191
192 if (token.isDone())
193 continue;
194
195 if (!pinged) {
196 ResultMessage result = sendBlockingToken(new PingMessage(clientId), token);
197 if (result == null)
198 continue;
199 clientId = result.getClientId();
200 pinged = true;
201 }
202
203 if (!authenticated) {
204 Credentials credentials = this.credentials;
205 if (credentials != null) {
206 ResultMessage result = sendBlockingToken(new LoginMessage(clientId, credentials), token);
207 if (result == null)
208 continue;
209 authenticated = true;
210 }
211 }
212
213 sendToken(token);
214 }
215 catch (InterruptedException e) {
216 log.info("Channel %s stopped.", id);
217 break;
218 }
219 catch (Exception e) {
220 log.error(e, "Channel %s got an unexepected exception.", id);
221 }
222 }
223 }
224
225 private ResultMessage sendBlockingToken(RequestMessage request, AsyncToken dependentToken) {
226
227 // Make this blocking request share the timeout/timeToLive values of the dependent token.
228 request.setTimestamp(dependentToken.getRequest().getTimestamp());
229 request.setTimeToLive(dependentToken.getRequest().getTimeToLive());
230
231 // Create the blocking token and schedule it with the dependent token timeout.
232 AsyncToken blockingToken = new AsyncToken(request);
233 try {
234 timer.schedule(blockingToken, blockingToken.getRequest().getRemainingTimeToLive());
235 }
236 catch (IllegalArgumentException e) {
237 dependentToken.dispatchTimeout(System.currentTimeMillis());
238 return null;
239 }
240 catch (Exception e) {
241 dependentToken.dispatchFailure(e);
242 return null;
243 }
244
245 // Try to send the blocking token (can block if the connections semaphore can't be acquired
246 // immediately).
247 try {
248 if (!sendToken(blockingToken))
249 return null;
250 }
251 catch (Exception e) {
252 dependentToken.dispatchFailure(e);
253 return null;
254 }
255
256 // Block until we get a server response (result or fault), a cancellation (unlikely), a timeout
257 // or any other execution exception.
258 try {
259 ResponseMessage response = blockingToken.get();
260
261 // Request was successful, return a non-null result.
262 if (response instanceof ResultMessage)
263 return (ResultMessage)response;
264
265 if (response instanceof FaultMessage) {
266 FaultMessage faultMessage = (FaultMessage)response.copy(dependentToken.getRequest().getId());
267 if (dependentToken.getRequest() instanceof MessageChain) {
268 ResponseMessage nextResponse = faultMessage;
269 for (MessageChain<?> nextRequest = ((MessageChain<?>)dependentToken.getRequest()).getNext(); nextRequest != null; nextRequest = nextRequest.getNext()) {
270 nextResponse.setNext(response.copy(nextRequest.getId()));
271 nextResponse = nextResponse.getNext();
272 }
273 }
274 dependentToken.dispatchFault(faultMessage);
275 }
276 else
277 throw new RuntimeException("Unknow response message type: " + response);
278
279 }
280 catch (InterruptedException e) {
281 dependentToken.dispatchFailure(e);
282 }
283 catch (TimeoutException e) {
284 dependentToken.dispatchTimeout(System.currentTimeMillis());
285 }
286 catch (ExecutionException e) {
287 if (e.getCause() instanceof Exception)
288 dependentToken.dispatchFailure((Exception)e.getCause());
289 else
290 dependentToken.dispatchFailure(e);
291 }
292 catch (Exception e) {
293 dependentToken.dispatchFailure(e);
294 }
295
296 return null;
297 }
298
299 private boolean sendToken(final AsyncToken token) {
300
301 boolean releaseConnections = false;
302 try {
303 // Block until a connection is available.
304 if (!connections.tryAcquire(token.getRequest().getRemainingTimeToLive(), TimeUnit.MILLISECONDS)) {
305 token.dispatchTimeout(System.currentTimeMillis());
306 return false;
307 }
308
309 // Semaphore was successfully acquired, we must release it in the finally block unless we succeed in
310 // sending the data (see below).
311 releaseConnections = true;
312
313 // Check if the token has already received an event (likely a timeout or a cancellation).
314 if (token.isDone())
315 return false;
316
317 // Make sure we have set a clientId (can be null for ping message).
318 token.getRequest().setClientId(clientId);
319
320 // Add the token to active tokens map.
321 if (tokensMap.putIfAbsent(token.getId(), token) != null)
322 throw new RuntimeException("MessageId isn't unique: " + token.getId());
323
324 // Actually send the message content.
325 TransportFuture transportFuture = transport.send(this, createTransportMessage(token));
326
327 // Create and try to set a channel listener: if no event has been dispatched for this token (tokenEvent == null),
328 // the listener will be called on the next event. Otherwise, we just call the listener immediately.
329 ResponseListener channelListener = new ChannelResponseListener(token.getId(), tokensMap, transportFuture, connections);
330 Event tokenEvent = token.setChannelListener(channelListener);
331 if (tokenEvent != null)
332 ResponseListenerDispatcher.dispatch(channelListener, tokenEvent);
333
334 // Message was sent and we were able to handle everything ourself.
335 releaseConnections = false;
336
337 return true;
338 }
339 catch (Exception e) {
340 tokensMap.remove(token.getId());
341 token.dispatchFailure(e);
342 if (timer != null)
343 timer.purge(); // Must purge to cleanup timer references to AsyncToken
344 return false;
345 }
346 finally {
347 if (releaseConnections)
348 connections.release();
349 }
350 }
351
352 protected RequestMessage getRequest(String id) {
353 AsyncToken token = tokensMap.get(id);
354 return (token != null ? token.getRequest() : null);
355 }
356
357
358 @Override
359 public ResponseMessageFuture send(RequestMessage request, ResponseListener... listeners) {
360 if (request == null)
361 throw new NullPointerException("request cannot be null");
362
363 if (!start())
364 throw new RuntimeException("Channel not started");
365
366 AsyncToken token = new AsyncToken(request, listeners);
367
368 request.setTimestamp(System.currentTimeMillis());
369 if (request.getTimeToLive() <= 0L)
370 request.setTimeToLive(defaultTimeToLive);
371
372 try {
373 timer.schedule(token, request.getRemainingTimeToLive());
374 tokensQueue.add(token);
375 }
376 catch (Exception e) {
377 log.error(e, "Could not add token to queue: %s", token);
378 token.dispatchFailure(e);
379 return new ImmediateFailureResponseMessageFuture(e);
380 }
381
382 return token;
383 }
384
385 @Override
386 public ResponseMessageFuture logout(ResponseListener... listeners) {
387 credentials = null;
388 authenticated = false;
389 return send(new LogoutMessage(), listeners);
390 }
391
392 @Override
393 public void onMessage(InputStream is) {
394 try {
395 ResponseMessage response = decodeResponse(is);
396
397 if (response != null) {
398
399 AsyncToken token = tokensMap.remove(response.getCorrelationId());
400 if (token == null) {
401 log.warn("Unknown correlation id: %s", response.getCorrelationId());
402 return;
403 }
404
405 switch (response.getType()) {
406 case RESULT:
407 token.dispatchResult((ResultMessage)response);
408 break;
409 case FAULT:
410 FaultMessage faultMessage = (FaultMessage)response;
411 if (isAuthenticated() && faultMessage.getCode() == FaultMessage.Code.NOT_LOGGED_IN || faultMessage.getCode() == FaultMessage.Code.SESSION_EXPIRED) {
412 authenticated = false;
413 credentials = null;
414 }
415
416 token.dispatchFault((FaultMessage)response);
417 break;
418 default:
419 token.dispatchFailure(new RuntimeException("Unknown message type: " + response));
420 break;
421 }
422
423 if (timer != null)
424 timer.purge(); // Must purge to cleanup timer references to AsyncToken
425 }
426 }
427 catch (Exception e) {
428 log.error(e, "Could not deserialize or dispatch incoming messages");
429 }
430 }
431
432 @Override
433 public void onError(TransportMessage message, Exception e) {
434 if (message != null) {
435 AsyncToken token = tokensMap.remove(message.getId());
436 if (token != null) {
437 token.dispatchFailure(e);
438 if (timer != null)
439 timer.purge(); // Must purge to cleanup timer references to AsyncToken
440 }
441 }
442 }
443
444 @Override
445 public void onCancelled(TransportMessage message) {
446 AsyncToken token = tokensMap.remove(message.getId());
447 if (token != null) {
448 token.dispatchCancelled();
449 if (timer != null)
450 timer.purge(); // Must purge to cleanup timer references to AsyncToken
451 }
452 }
453
454 private static class ChannelResponseListener extends AllInOneResponseListener {
455
456 private final String tokenId;
457 private final ConcurrentMap<String, AsyncToken> tokensMap;
458 private final TransportFuture transportFuture;
459 private final Semaphore connections;
460
461 public ChannelResponseListener(
462 String tokenId,
463 ConcurrentMap<String, AsyncToken> tokensMap,
464 TransportFuture transportFuture,
465 Semaphore connections) {
466
467 this.tokenId = tokenId;
468 this.tokensMap = tokensMap;
469 this.transportFuture = transportFuture;
470 this.connections = connections;
471 }
472
473 @Override
474 public void onEvent(Event event) {
475 try {
476 tokensMap.remove(tokenId);
477 if (event.getType() == Type.TIMEOUT || event.getType() == Type.CANCELLED) {
478 if (transportFuture != null)
479 transportFuture.cancel();
480 }
481 }
482 finally {
483 connections.release();
484 }
485 }
486 }
487 }