/** * GRANITE DATA SERVICES * Copyright (C) 2006-2015 GRANITE DATA SERVICES S.A.S. * * This file is part of the Granite Data Services Platform. * * Granite Data Services is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 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 Lesser * General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, * USA, or 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.ArrayList; import java.util.List; 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 java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReentrantLock; 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.DisconnectMessage; 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 final AsyncToken stopToken = new AsyncToken(new DisconnectMessage()); private final AtomicBoolean stopped = new AtomicBoolean(true); private AsyncToken disconnectToken = null; private Thread senderThread = null; private Semaphore connections; private Timer timer = null; private List<ChannelStatusListener> statusListeners = new ArrayList<ChannelStatusListener>(); private volatile boolean pinged = false; private volatile boolean authenticated = false; private ReentrantLock authenticationLock = new ReentrantLock(); private volatile boolean authenticating = false; private ReauthenticateCallback reauthenticateCallback = null; 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) { try { timer.schedule(timerTask, delay); } catch (IllegalStateException e) { log.error(e, "Timer has been cancelled, starting new timer"); timer = null; timer = new Timer(id + "_timer", true); 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 void setReauthenticateCallback(ReauthenticateCallback callback) { this.reauthenticateCallback = callback; } public int getMaxConcurrentRequests() { return maxConcurrentRequests; } @Override public void onStop(Transport transport) { stop(); } @Override public synchronized boolean start() { if (senderThread != null) return true; tokensQueue.clear(); tokensMap.clear(); disconnectToken = null; stopped.set(false); log.info("Starting channel %s...", id); senderThread = new Thread(this, id + "_sender"); 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) return false; 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; } } internalStop(); log.debug("Interrupting thread %s", id); connections = null; tokensMap.clear(); tokensQueue.clear(); disconnectToken = null; stopped.set(true); tokensQueue.add(stopToken); Thread thread = this.senderThread; senderThread = null; thread.interrupt(); pinged = false; clientId = null; setAuthenticated(false, null); return true; } protected void internalStop() { } public void reauthenticate() throws ChannelException { Credentials credentials = this.credentials; if (credentials == null) return; log.debug("Channel %s client %s reauthenticating", getId(), clientId); ChannelException channelException = null; try { authenticationLock.lock(); // Avoid multiple simultaneous blocking login calls if (authenticating || authenticated) return; try { authenticating = true; LoginMessage loginMessage = new LoginMessage(clientId, credentials); ResponseMessage response = sendSimpleBlockingToken(loginMessage); if (response instanceof ResultMessage) setAuthenticated(true, response); else if (response instanceof FaultMessage) { FaultMessage fault = (FaultMessage)response; channelException = new ChannelException(clientId, "Could not reauthenticate channel " + clientId + " , fault " + fault.getCode() + " " + fault.getDescription() + " " + fault.getDetails()); } else channelException = new ChannelException(clientId, "Could not reauthenticate channel " + clientId); } finally { authenticating = false; } } catch (Exception e) { channelException = new ChannelException(clientId, "Could not reauthenticate channel " + clientId, e); } finally { authenticationLock.unlock(); } if (channelException != null) throw channelException; } protected LoginMessage authenticate(AsyncToken dependentToken) { Credentials credentials = this.credentials; if (credentials == null) return null; LoginMessage loginMessage = new LoginMessage(clientId, credentials); if (dependentToken != null) { log.debug("Channel %s blocking authentication %s clientId %s", id, loginMessage.getId(), clientId); try { authenticationLock.lock(); // Avoid multiple simultaneous blocking login calls if (authenticating || authenticated) return null; try { authenticating = true; ResultMessage result = sendBlockingToken(loginMessage, dependentToken); if (result == null) return loginMessage; setAuthenticated(true, result); } finally { authenticating = false; } } finally { authenticationLock.unlock(); } } else { if (authenticating || authenticated) return null; log.debug("Channel %s non blocking authentication %s clientId %s", id, loginMessage.getId(), clientId); send(loginMessage); } return loginMessage; } protected void executeReauthenticateCallback() throws ChannelException { // Force reauthentication from the remoting channel before connecting if this channel is not able to authenticate itself (i.e. websockets) if (transport.isAuthenticationAfterReconnectWithRemoting() && reauthenticateCallback != null) { log.debug("Channel clientId %s force reauthentication with remoting channel", clientId); reauthenticateCallback.reauthenticate(); } } @Override public void run() { while (!Thread.interrupted()) { try { if (stopped.get()) break; AsyncToken token = tokensQueue.take(); if (token == stopToken) break; if (token.isDone()) continue; if (token.isDisconnectRequest()) { sendToken(token); continue; } executeReauthenticateCallback(); if (!pinged) { PingMessage pingMessage = new PingMessage(clientId); log.debug("Channel %s send ping %s with clientId %s", id, pingMessage.getId(), clientId); ResultMessage result = sendBlockingToken(pingMessage, token); if (result == null) continue; clientId = result.getClientId(); log.debug("Channel %s pinged clientId %s", id, clientId); pinged = true; } authenticate(token); if (!(token.getRequest() instanceof PingMessage)) sendToken(token); } catch (InterruptedException e) { log.info("Channel %s stopped.", id); break; } catch (Exception e) { log.error(e, "Channel %s got an unexpected 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 { 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("Unknown 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 ResponseMessage sendSimpleBlockingToken(RequestMessage request) throws Exception { request.setTimestamp(System.currentTimeMillis()); if (request.getTimeToLive() <= 0L) request.setTimeToLive(defaultTimeToLive); // Create the blocking token and schedule it with the dependent token timeout. AsyncToken blockingToken = new AsyncToken(request); try { schedule(blockingToken, blockingToken.getRequest().getRemainingTimeToLive()); } catch (IllegalArgumentException e) { return null; } catch (Exception 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) { throw e; } // Block until we get a server response (result or fault), a cancellation (unlikely), a timeout // or any other execution exception. try { return blockingToken.get(); } catch (Exception e) { throw e; } } 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); if (token.getRequest() instanceof DisconnectMessage) { // Store disconnect token disconnectToken = token; } else { // Add the token to active tokens map. if (tokensMap.putIfAbsent(token.getId(), token) != null) throw new RuntimeException("MessageId isn't unique: " + token.getId()); } log.debug("Channel %s send %s message id %s", clientId, token.getRequest().getType().name(), token.getRequest().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 != null) 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 { log.debug("Client %s schedule request %s", clientId, request.getId()); 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) { return logout(true, listeners); } @Override public ResponseMessageFuture logout(boolean sendLogout, ResponseListener... listeners) { log.info("Logging out channel %s", clientId); clearCredentials(); setAuthenticated(false, null); if (sendLogout) return send(new LogoutMessage(), listeners); return null; } @Override public void onMessage(TransportMessage message, InputStream is) { try { ResponseMessage response = decodeResponse(is); if (response == null) return; if (response.getCorrelationId() == null || response.isProcessed()) return; 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: token.dispatchFault((FaultMessage)response); break; default: token.dispatchFailure(new RuntimeException("Unknown message type: " + response)); break; } } catch (Exception e) { log.error(e, "Could not deserialize or dispatch incoming messages"); AsyncToken token = message != null ? tokensMap.remove(message.getId()) : null; if (token != null) token.dispatchFailure(e); } finally { if (timer != null) timer.purge(); // Must purge to cleanup timer references to AsyncToken } } @Override public void onDisconnect() { log.info("Disconnecting channel %s", clientId); tokensMap.clear(); tokensQueue.clear(); if (timer != null) timer.purge(); // Must purge to cleanup timer references to AsyncToken if (disconnectToken != null) { // Handle "hard" disconnect ResultMessage resultMessage = new ResultMessage(clientId, disconnectToken.getRequest().getId(), true); disconnectToken.dispatchResult(resultMessage); disconnectToken = null; } clientId = null; pinged = false; authenticating = false; authenticated = false; } @Override public void onError(TransportMessage message, Exception e) { if (message == null) return; AsyncToken token = tokensMap.remove(message.getId()); if (token == null) return; 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) return; 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) { try { transportFuture.cancel(); } catch (UnsupportedOperationException e) { // In case transport does not support cancel } } } } finally { if (connections != null) connections.release(); } } } public void addListener(ChannelStatusListener listener) { statusListeners.add(listener); } public void removeListener(ChannelStatusListener listener) { statusListeners.remove(listener); } protected void setPinged(boolean pinged) { if (this.pinged == pinged) return; this.pinged = pinged; for (ChannelStatusListener listener : statusListeners) listener.pingedChanged(this, pinged); } protected void setAuthenticated(boolean authenticated, ResponseMessage response) { if (!this.authenticating && this.authenticated == authenticated) return; log.debug("Channel %s authentication changed clientId %s (%s)", id, clientId, String.valueOf(authenticated)); this.authenticating = false; this.authenticated = authenticated; for (ChannelStatusListener listener : statusListeners) listener.authenticatedChanged(this, authenticated, response); postSetAuthenticated(authenticated, response); } protected void postSetAuthenticated(boolean authenticated, ResponseMessage response) { } public void clearCredentials() { this.credentials = null; for (ChannelStatusListener listener : statusListeners) listener.credentialsCleared(this); } protected void dispatchFault(FaultMessage faultMessage) { for (ChannelStatusListener listener : statusListeners) listener.fault(this, faultMessage); } public void bindStatus(ChannelStatusNotifier notifier) { notifier.addListener(statusListener); } public void unbindStatus(ChannelStatusNotifier notifier) { notifier.removeListener(statusListener); } private ChannelStatusListener statusListener = new ChannelStatusListener() { @Override public void fault(Channel channel, FaultMessage faultMessage) { } @Override public void pingedChanged(Channel channel, boolean pinged) { if (channel == AbstractHTTPChannel.this) return; log.debug("Channel %s pinged changed %s", channel.getClientId(), pinged); AbstractHTTPChannel.this.pinged = pinged; } @Override public void authenticatedChanged(Channel channel, boolean authenticated, ResponseMessage response) { if (channel == AbstractHTTPChannel.this) return; log.debug("Channel %s authenticated changed %s", channel.getClientId(), authenticated); boolean wasAuthenticated = AbstractHTTPChannel.this.authenticated; AbstractHTTPChannel.this.authenticating = false; AbstractHTTPChannel.this.authenticated = authenticated; if (authenticated != wasAuthenticated) AbstractHTTPChannel.this.postSetAuthenticated(authenticated, response); } @Override public void credentialsCleared(Channel channel) { if (channel == AbstractHTTPChannel.this) return; log.debug("Channel %s credentials cleared", channel.getClientId()); AbstractHTTPChannel.this.credentials = null; } }; }