/* GRANITE DATA SERVICES Copyright (C) 2012 GRANITE DATA SERVICES S.A.S. This file is part of Granite Data Services. Granite Data Services is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Granite Data Services is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; if not, see <http://www.gnu.org/licenses/>. */ package org.granite.client.messaging.channel; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URI; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.granite.client.messaging.AllInOneResponseListener; import org.granite.client.messaging.ResponseListener; import org.granite.client.messaging.ResponseListenerDispatcher; import org.granite.client.messaging.events.Event; import org.granite.client.messaging.events.Event.Type; import org.granite.client.messaging.messages.MessageChain; import org.granite.client.messaging.messages.RequestMessage; import org.granite.client.messaging.messages.ResponseMessage; import org.granite.client.messaging.messages.requests.LoginMessage; import org.granite.client.messaging.messages.requests.LogoutMessage; import org.granite.client.messaging.messages.requests.PingMessage; import org.granite.client.messaging.messages.responses.FaultMessage; import org.granite.client.messaging.messages.responses.ResultMessage; import org.granite.client.messaging.transport.Transport; import org.granite.client.messaging.transport.TransportFuture; import org.granite.client.messaging.transport.TransportMessage; import org.granite.client.messaging.transport.TransportStopListener; import org.granite.logging.Logger; /** * @author Franck WOLFF */ public abstract class AbstractHTTPChannel extends AbstractChannel<Transport> implements TransportStopListener, Runnable { private static final Logger log = Logger.getLogger(AbstractHTTPChannel.class); private final BlockingQueue<AsyncToken> tokensQueue = new LinkedBlockingQueue<AsyncToken>(); private final ConcurrentMap<String, AsyncToken> tokensMap = new ConcurrentHashMap<String, AsyncToken>(); private Thread senderThread = null; private Semaphore connections; private Timer timer = null; protected volatile boolean pinged = false; protected volatile boolean authenticated = false; protected volatile int maxConcurrentRequests; protected volatile long defaultTimeToLive = DEFAULT_TIME_TO_LIVE; // 1 mn. public AbstractHTTPChannel(Transport transport, String id, URI uri, int maxConcurrentRequests) { super(transport, id, uri); if (maxConcurrentRequests < 1) throw new IllegalArgumentException("maxConcurrentRequests must be greater or equal to 1"); this.maxConcurrentRequests = maxConcurrentRequests; } protected abstract TransportMessage createTransportMessage(AsyncToken token) throws UnsupportedEncodingException; protected abstract ResponseMessage decodeResponse(InputStream is) throws IOException; protected boolean schedule(TimerTask timerTask, long delay) { if (timer != null) { timer.schedule(timerTask, delay); return true; } return false; } public long getDefaultTimeToLive() { return defaultTimeToLive; } public void setDefaultTimeToLive(long defaultTimeToLive) { this.defaultTimeToLive = defaultTimeToLive; } public boolean isAuthenticated() { return authenticated; } public int getMaxConcurrentRequests() { return maxConcurrentRequests; } @Override public void onStop(Transport transport) { stop(); } @Override public synchronized boolean start() { if (senderThread == null) { log.info("Starting channel %s...", id); senderThread = new Thread(this); try { timer = new Timer(id + "_timer", true); connections = new Semaphore(maxConcurrentRequests); senderThread.start(); transport.addStopListener(this); log.info("Channel %s started.", id); } catch (Exception e) { if (timer != null) { timer.cancel(); timer = null; } connections = null; senderThread = null; log.error(e, "Channel %s failed to start.", id); return false; } } return true; } @Override public synchronized boolean isStarted() { return senderThread != null; } @Override public synchronized boolean stop() { if (senderThread != null) { log.info("Stopping channel %s...", id); if (timer != null) { try { timer.cancel(); } catch (Exception e) { log.error(e, "Channel %s timer failed to stop.", id); } finally { timer = null; } } connections = null; tokensMap.clear(); tokensQueue.clear(); Thread thread = this.senderThread; senderThread = null; thread.interrupt(); pinged = false; clientId = null; authenticated = false; return true; } return false; } @Override public void run() { while (!Thread.interrupted()) { try { AsyncToken token = tokensQueue.take(); if (token.isDone()) continue; if (!pinged) { ResultMessage result = sendBlockingToken(new PingMessage(clientId), token); if (result == null) continue; clientId = result.getClientId(); pinged = true; } if (!authenticated) { Credentials credentials = this.credentials; if (credentials != null) { ResultMessage result = sendBlockingToken(new LoginMessage(clientId, credentials), token); if (result == null) continue; authenticated = true; } } sendToken(token); } catch (InterruptedException e) { log.info("Channel %s stopped.", id); break; } catch (Exception e) { log.error(e, "Channel %s got an unexepected exception.", id); } } } private ResultMessage sendBlockingToken(RequestMessage request, AsyncToken dependentToken) { // Make this blocking request share the timeout/timeToLive values of the dependent token. request.setTimestamp(dependentToken.getRequest().getTimestamp()); request.setTimeToLive(dependentToken.getRequest().getTimeToLive()); // Create the blocking token and schedule it with the dependent token timeout. AsyncToken blockingToken = new AsyncToken(request); try { timer.schedule(blockingToken, blockingToken.getRequest().getRemainingTimeToLive()); } catch (IllegalArgumentException e) { dependentToken.dispatchTimeout(System.currentTimeMillis()); return null; } catch (Exception e) { dependentToken.dispatchFailure(e); return null; } // Try to send the blocking token (can block if the connections semaphore can't be acquired // immediately). try { if (!sendToken(blockingToken)) return null; } catch (Exception e) { dependentToken.dispatchFailure(e); return null; } // Block until we get a server response (result or fault), a cancellation (unlikely), a timeout // or any other execution exception. try { ResponseMessage response = blockingToken.get(); // Request was successful, return a non-null result. if (response instanceof ResultMessage) return (ResultMessage)response; if (response instanceof FaultMessage) { FaultMessage faultMessage = (FaultMessage)response.copy(dependentToken.getRequest().getId()); if (dependentToken.getRequest() instanceof MessageChain) { ResponseMessage nextResponse = faultMessage; for (MessageChain<?> nextRequest = ((MessageChain<?>)dependentToken.getRequest()).getNext(); nextRequest != null; nextRequest = nextRequest.getNext()) { nextResponse.setNext(response.copy(nextRequest.getId())); nextResponse = nextResponse.getNext(); } } dependentToken.dispatchFault(faultMessage); } else throw new RuntimeException("Unknow response message type: " + response); } catch (InterruptedException e) { dependentToken.dispatchFailure(e); } catch (TimeoutException e) { dependentToken.dispatchTimeout(System.currentTimeMillis()); } catch (ExecutionException e) { if (e.getCause() instanceof Exception) dependentToken.dispatchFailure((Exception)e.getCause()); else dependentToken.dispatchFailure(e); } catch (Exception e) { dependentToken.dispatchFailure(e); } return null; } private boolean sendToken(final AsyncToken token) { boolean releaseConnections = false; try { // Block until a connection is available. if (!connections.tryAcquire(token.getRequest().getRemainingTimeToLive(), TimeUnit.MILLISECONDS)) { token.dispatchTimeout(System.currentTimeMillis()); return false; } // Semaphore was successfully acquired, we must release it in the finally block unless we succeed in // sending the data (see below). releaseConnections = true; // Check if the token has already received an event (likely a timeout or a cancellation). if (token.isDone()) return false; // Make sure we have set a clientId (can be null for ping message). token.getRequest().setClientId(clientId); // Add the token to active tokens map. if (tokensMap.putIfAbsent(token.getId(), token) != null) throw new RuntimeException("MessageId isn't unique: " + token.getId()); // Actually send the message content. TransportFuture transportFuture = transport.send(this, createTransportMessage(token)); // Create and try to set a channel listener: if no event has been dispatched for this token (tokenEvent == null), // the listener will be called on the next event. Otherwise, we just call the listener immediately. ResponseListener channelListener = new ChannelResponseListener(token.getId(), tokensMap, transportFuture, connections); Event tokenEvent = token.setChannelListener(channelListener); if (tokenEvent != null) ResponseListenerDispatcher.dispatch(channelListener, tokenEvent); // Message was sent and we were able to handle everything ourself. releaseConnections = false; return true; } catch (Exception e) { tokensMap.remove(token.getId()); token.dispatchFailure(e); if (timer != null) timer.purge(); // Must purge to cleanup timer references to AsyncToken return false; } finally { if (releaseConnections) connections.release(); } } protected RequestMessage getRequest(String id) { AsyncToken token = tokensMap.get(id); return (token != null ? token.getRequest() : null); } @Override public ResponseMessageFuture send(RequestMessage request, ResponseListener... listeners) { if (request == null) throw new NullPointerException("request cannot be null"); if (!start()) throw new RuntimeException("Channel not started"); AsyncToken token = new AsyncToken(request, listeners); request.setTimestamp(System.currentTimeMillis()); if (request.getTimeToLive() <= 0L) request.setTimeToLive(defaultTimeToLive); try { timer.schedule(token, request.getRemainingTimeToLive()); tokensQueue.add(token); } catch (Exception e) { log.error(e, "Could not add token to queue: %s", token); token.dispatchFailure(e); return new ImmediateFailureResponseMessageFuture(e); } return token; } @Override public ResponseMessageFuture logout(ResponseListener... listeners) { credentials = null; authenticated = false; return send(new LogoutMessage(), listeners); } @Override public void onMessage(InputStream is) { try { ResponseMessage response = decodeResponse(is); if (response != null) { AsyncToken token = tokensMap.remove(response.getCorrelationId()); if (token == null) { log.warn("Unknown correlation id: %s", response.getCorrelationId()); return; } switch (response.getType()) { case RESULT: token.dispatchResult((ResultMessage)response); break; case FAULT: FaultMessage faultMessage = (FaultMessage)response; if (isAuthenticated() && faultMessage.getCode() == FaultMessage.Code.NOT_LOGGED_IN || faultMessage.getCode() == FaultMessage.Code.SESSION_EXPIRED) { authenticated = false; credentials = null; } token.dispatchFault((FaultMessage)response); break; default: token.dispatchFailure(new RuntimeException("Unknown message type: " + response)); break; } if (timer != null) timer.purge(); // Must purge to cleanup timer references to AsyncToken } } catch (Exception e) { log.error(e, "Could not deserialize or dispatch incoming messages"); } } @Override public void onError(TransportMessage message, Exception e) { if (message != null) { AsyncToken token = tokensMap.remove(message.getId()); if (token != null) { token.dispatchFailure(e); if (timer != null) timer.purge(); // Must purge to cleanup timer references to AsyncToken } } } @Override public void onCancelled(TransportMessage message) { AsyncToken token = tokensMap.remove(message.getId()); if (token != null) { token.dispatchCancelled(); if (timer != null) timer.purge(); // Must purge to cleanup timer references to AsyncToken } } private static class ChannelResponseListener extends AllInOneResponseListener { private final String tokenId; private final ConcurrentMap<String, AsyncToken> tokensMap; private final TransportFuture transportFuture; private final Semaphore connections; public ChannelResponseListener( String tokenId, ConcurrentMap<String, AsyncToken> tokensMap, TransportFuture transportFuture, Semaphore connections) { this.tokenId = tokenId; this.tokensMap = tokensMap; this.transportFuture = transportFuture; this.connections = connections; } @Override public void onEvent(Event event) { try { tokensMap.remove(tokenId); if (event.getType() == Type.TIMEOUT || event.getType() == Type.CANCELLED) { if (transportFuture != null) transportFuture.cancel(); } } finally { connections.release(); } } } }