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    }