/** * 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.amf; 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.Map; import java.util.TimerTask; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import org.granite.client.messaging.Consumer; import org.granite.client.messaging.ResponseListener; import org.granite.client.messaging.channel.AsyncToken; import org.granite.client.messaging.channel.Channel; import org.granite.client.messaging.channel.ChannelException; import org.granite.client.messaging.channel.MessagingChannel; import org.granite.client.messaging.channel.ResponseMessageFuture; import org.granite.client.messaging.codec.MessagingCodec; import org.granite.client.messaging.messages.Message.Type; 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.responses.AbstractResponseMessage; import org.granite.client.messaging.messages.responses.FaultMessage; import org.granite.client.messaging.messages.responses.ResultMessage; import org.granite.client.messaging.transport.DefaultTransportMessage; import org.granite.client.messaging.transport.Transport; import org.granite.client.messaging.transport.TransportMessage; import org.granite.logging.Logger; import org.granite.util.UUIDUtil; import flex.messaging.messages.AcknowledgeMessage; import flex.messaging.messages.AsyncMessage; import flex.messaging.messages.CommandMessage; import flex.messaging.messages.Message; /** * @author Franck WOLFF */ public class BaseAMFMessagingChannel extends AbstractAMFChannel implements MessagingChannel { private static final Logger log = Logger.getLogger(BaseAMFMessagingChannel.class); protected final MessagingCodec<Message[]> codec; protected volatile String sessionId = null; protected final ConcurrentMap<String, Consumer> consumersMap = new ConcurrentHashMap<String, Consumer>(); protected final AtomicReference<String> connectMessageId = new AtomicReference<String>(null); protected final AtomicReference<String> loginMessageId = new AtomicReference<String>(null); protected final AtomicReference<ReconnectTimerTask> reconnectTimerTask = new AtomicReference<ReconnectTimerTask>(); protected final List<ChannelResponseListener> responseListeners = new ArrayList<ChannelResponseListener>(); protected volatile long reconnectIntervalMillis = TimeUnit.SECONDS.toMillis(30L); protected boolean reconnectMaxAttemptsSet = false; protected volatile long reconnectMaxAttempts = 60L; protected volatile long reconnectAttempts = 0L; public BaseAMFMessagingChannel(MessagingCodec<Message[]> codec, Transport transport, String id, URI uri) { super(transport, id, uri, 1); this.codec = codec; } public void setSessionId(String sessionId) { if ((sessionId == null && this.sessionId != null) || (sessionId != null && !sessionId.equals(this.sessionId))) { this.sessionId = sessionId; log.info("Messaging channel %s set sessionId %s", clientId, sessionId); } } public void setDefaultMaxReconnectAttempts(long reconnectMaxAttempts) { this.reconnectMaxAttempts = reconnectMaxAttempts; this.reconnectMaxAttemptsSet = true; } @Override public void preconnect() throws ChannelException { executeReauthenticateCallback(); } protected boolean connect() { // Connecting: make sure we don't have an active reconnect timer task. cancelReconnectTimerTask(); // No subscriptions... if (consumersMap.isEmpty()) return false; // We are already waiting for a connection/answer. final String id = UUIDUtil.randomUUID(); if (!connectMessageId.compareAndSet(null, id)) return false; log.debug("Connecting channel with clientId %s", clientId); // Create and try to send the connect message. try { preconnect(); transport.send(this, createConnectMessage(id, false)); return true; } catch (Exception e) { // Connect immediately failed, release the message id and schedule a reconnect. connectMessageId.set(null); loginMessageId.set(null); scheduleReconnectTimerTask(false); return false; } } @Override protected void postSetAuthenticated(boolean authenticated, ResponseMessage response) { // Force disconnection for streaming transports to ensure next calls are in a new session/authentication context if (!authenticated && response instanceof FaultMessage && transport.isDisconnectAfterAuthenticationFailure()) { log.debug("Channel clientId %s force disconnection after unauthentication (new sessionId %s)", clientId, sessionId); disconnect(); } } @Override public ResponseMessageFuture logout(boolean sendLogout, ResponseListener... listeners) { ResponseMessageFuture future = super.logout(sendLogout, listeners); // Force disconnection for streaming transports to ensure next calls are in a new session/authentication context if (sendLogout) disconnect(); return future; } @Override public void addConsumer(Consumer consumer) { consumersMap.putIfAbsent(consumer.getSubscriptionId(), consumer); connect(); } @Override public boolean removeConsumer(Consumer consumer) { String subscriptionId = consumer.getSubscriptionId(); if (subscriptionId == null) { for (String sid : consumersMap.keySet()) { if (consumersMap.get(sid) == consumer) { subscriptionId = sid; break; } } } if (subscriptionId == null) { log.warn("Channel %s trying to remove unexisting consumer for destination %s", id, consumer.getDestination()); return false; } return consumersMap.remove(subscriptionId) != null; } public void addListener(ChannelResponseListener listener) { responseListeners.add(listener); } public void removeListener(ChannelResponseListener listener) { responseListeners.remove(listener); } public synchronized ResponseMessageFuture disconnect(ResponseListener...listeners) { cancelReconnectTimerTask(); for (Consumer consumer : consumersMap.values()) consumer.onDisconnect(); consumersMap.clear(); connectMessageId.set(null); loginMessageId.set(null); reconnectAttempts = 0L; if (isStarted()) return send(new DisconnectMessage(clientId), listeners); return null; } @Override protected TransportMessage createTransportMessage(AsyncToken token) throws UnsupportedEncodingException { Message[] messages = convertToAmf(token.getRequest()); return new DefaultTransportMessage<Message[]>(token.getId(), false, token.isDisconnectRequest(), clientId, sessionId, messages, codec); } @Override protected ResponseMessage decodeResponse(InputStream is) throws IOException { boolean reconnect = false; AbstractResponseMessage responseChain = null; AbstractResponseMessage currentResponse = null; try { if (is.available() > 0) { final Message[] messages = codec.decode(is); log.debug("Channel %s: received %d messages", clientId, messages.length); for (Message message : messages) { if (message instanceof AcknowledgeMessage) { AbstractResponseMessage response = convertFromAmf((AcknowledgeMessage)message); if (response instanceof ResultMessage) { log.debug("Channel %s received result %s of type %s correlationId %s", clientId, response.getId(), response.getType().name(), response.getCorrelationId()); Type requestType = null; RequestMessage request = getRequest(response.getCorrelationId()); if (request != null) requestType = request.getType(); else if (response.getCorrelationId().equals(connectMessageId.get())) { // Reconnect requestType = Type.PING; response.setProcessed(); } else if (response.getCorrelationId().equals(loginMessageId.get())) requestType = Type.LOGIN; if (requestType != null) { ResultMessage result = (ResultMessage)response; switch (requestType) { case PING: if (messages[0].getBody() instanceof Map) { Map<?, ?> advices = (Map<?, ?>)messages[0].getBody(); Object reconnectIntervalMillis = advices.get(Channel.RECONNECT_INTERVAL_MS_KEY); if (reconnectIntervalMillis instanceof Number) this.reconnectIntervalMillis = ((Number)reconnectIntervalMillis).longValue(); Object reconnectMaxAttempts = advices.get(Channel.RECONNECT_MAX_ATTEMPTS_KEY); if (reconnectMaxAttempts instanceof Number && !reconnectMaxAttemptsSet) this.reconnectMaxAttempts = ((Number)reconnectMaxAttempts).longValue(); } // Successful ping, reinitialize reconnect counter reconnectAttempts = 0L; if (messages[0].getHeaders().containsKey("JSESSIONID")) setSessionId((String)messages[0].getHeader("JSESSIONID")); boolean resubscribe = false; if (clientId != null && !clientId.equals(result.getClientId())) { log.warn("Channel %s ping successful new clientId %s current %s requested %s", id, result.getClientId(), clientId, request != null ? result.getClientId() : "(no request)"); resubscribe = true; } else log.debug("Channel %s ping successful clientId %s current %s requested %s", id, result.getClientId(), clientId, request != null ? result.getClientId() : "(no request)"); clientId = result.getClientId(); setPinged(true); LoginMessage loginMessage = authenticate(null); if (loginMessage != null) loginMessageId.set(loginMessage.getId()); else if (resubscribe) { for (Consumer consumer : consumersMap.values()) consumer.resubscribe(); } break; case LOGIN: log.debug("Channel %s authentication successful clientId %s", id, clientId); setAuthenticated(true, response); for (Consumer consumer : consumersMap.values()) consumer.resubscribe(); break; case SUBSCRIBE: String subscriptionId = (String)messages[0].getHeader(AsyncMessage.DESTINATION_CLIENT_ID_HEADER); log.debug("Channel %s subscription successful clientId %s subscriptionId %s", id, clientId, subscriptionId); result.setResult(subscriptionId); break; default: break; } } } else if (response instanceof FaultMessage) { log.debug("Channel %s received fault %s of type %s correlationId %s", clientId, response.getId(), response.getType().name(), response.getCorrelationId()); Type requestType = null; RequestMessage request = getRequest(response.getCorrelationId()); if (request != null) requestType = request.getType(); else if (response.getCorrelationId().equals(connectMessageId.get())) { // Reconnect requestType = Type.PING; response.setProcessed(); } else if (response.getCorrelationId().equals(loginMessageId.get())) // Login after reconnect requestType = Type.LOGIN; if (requestType != null) { switch (requestType) { case PING: log.warn("Channel %s ping failed current clientId %s requested %s", id, clientId, request != null ? request.getClientId() : "(no request)"); clientId = null; setPinged(false); case LOGIN: log.warn("Channel %s authentication failed current clientId %s requested %s", id, clientId, request != null ? request.getClientId() : "(no request)"); setAuthenticated(false, response); if (transport.isDisconnectAfterAuthenticationFailure()) { log.debug("Channel clientId %s force disconnection after authentication failure", clientId); disconnect(); } break; default: break; } } dispatchFault((FaultMessage)response); } if (responseChain == null) responseChain = currentResponse = response; else { currentResponse.setNext(response); currentResponse = response; } } } if (responseChain != null) { for (ChannelResponseListener listener : responseListeners) listener.onResponse(responseChain); } for (Message message : messages) { if (!(message instanceof AcknowledgeMessage)) { reconnect = transport.isReconnectAfterReceive(); if (!(message instanceof AsyncMessage)) throw new RuntimeException("Message should be an AsyncMessage: " + message); String subscriptionId = (String)message.getHeader(AsyncMessage.DESTINATION_CLIENT_ID_HEADER); Consumer consumer = consumersMap.get(subscriptionId); if (consumer != null) consumer.onMessage(convertFromAmf((AsyncMessage)message)); else log.warn("Channel %s: no consumer for subscriptionId: %s", clientId, subscriptionId); } } } else reconnect = transport.isReconnectAfterReceive(); } finally { if (reconnect) { connectMessageId.set(null); loginMessageId.set(null); connect(); } } return responseChain; } @Override protected void internalStop() { super.internalStop(); cancelReconnectTimerTask(); } @Override public void onError(TransportMessage message, Exception e) { if (!isStarted()) return; super.onError(message, e); // Mark consumers as unsubscribed // Should maybe not do it once consumers auto resubscribe after disconnect // for (Consumer consumer : consumersMap.values()) // consumer.onDisconnect(); // Don't reconnect here, there should be a following onClose if (message != null && connectMessageId.compareAndSet(message.getId(), null)) { scheduleReconnectTimerTask(false); } } protected void cancelReconnectTimerTask() { ReconnectTimerTask task = reconnectTimerTask.getAndSet(null); if (task != null && task.cancel()) reconnectAttempts = 0L; } @Override public TransportMessage createConnectMessage(String id, boolean reconnect) { CommandMessage connectMessage = new CommandMessage(); connectMessage.setOperation(CommandMessage.CONNECT_OPERATION); connectMessage.setMessageId(id); connectMessage.setTimestamp(System.currentTimeMillis()); connectMessage.setClientId(clientId); return new DefaultTransportMessage<Message[]>(id, !reconnect, false, clientId, sessionId, new Message[] { connectMessage }, codec); } protected void scheduleReconnectTimerTask(boolean immediate) { setPinged(false); setAuthenticated(false, null); ReconnectTimerTask task = new ReconnectTimerTask(); ReconnectTimerTask previousTask = reconnectTimerTask.getAndSet(task); if (previousTask != null) previousTask.cancel(); if (reconnectMaxAttempts <= 0 || reconnectAttempts < reconnectMaxAttempts) { reconnectAttempts++; log.info("Channel %s schedule reconnect (retry #%d / %d)", getId(), reconnectAttempts, reconnectMaxAttempts); schedule(task, immediate && reconnectAttempts == 1 ? 0L : reconnectIntervalMillis); } else log.error("Channel %s max number of reconnects (%d) reached", getId(), reconnectMaxAttempts); } class ReconnectTimerTask extends TimerTask { @Override public void run() { log.info("Channel %s reconnecting (retry #%d / %d)", getId(), reconnectAttempts, reconnectMaxAttempts); connect(); } } }