/* 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.amf; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URI; 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.MessagingChannel; import org.granite.client.messaging.channel.ResponseMessageFuture; import org.granite.client.messaging.codec.MessagingCodec; 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.responses.AbstractResponseMessage; 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 AbstractAMFMessagingChannel extends AbstractAMFChannel implements MessagingChannel { private static final Logger log = Logger.getLogger(AbstractAMFMessagingChannel.class); protected final MessagingCodec<Message[]> codec; protected 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<ReconnectTimerTask> reconnectTimerTask = new AtomicReference<ReconnectTimerTask>(); protected volatile long reconnectIntervalMillis = TimeUnit.SECONDS.toMillis(30L); protected volatile long reconnectMaxAttempts = 60L; protected volatile long reconnectAttempts = 0L; protected AbstractAMFMessagingChannel(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 sessionId %s", sessionId); } } 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. CommandMessage connectMessage = new CommandMessage(); connectMessage.setOperation(CommandMessage.CONNECT_OPERATION); connectMessage.setMessageId(id); connectMessage.setTimestamp(System.currentTimeMillis()); connectMessage.setClientId(clientId); try { transport.send(this, new DefaultTransportMessage<Message[]>(id, true, clientId, sessionId, new Message[]{connectMessage}, codec)); return true; } catch (Exception e) { // Connect immediately failed, release the message id and schedule a reconnect. connectMessageId.set(null); scheduleReconnectTimerTask(); return false; } } @Override public void addConsumer(Consumer consumer) { consumersMap.putIfAbsent(consumer.getSubscriptionId(), consumer); connect(); } @Override public boolean removeConsumer(Consumer consumer) { return (consumersMap.remove(consumer.getSubscriptionId()) != null); } public synchronized ResponseMessageFuture disconnect(ResponseListener...listeners) { cancelReconnectTimerTask(); connectMessageId.set(null); reconnectAttempts = 0L; for (Consumer consumer : consumersMap.values()) consumer.onDisconnect(); consumersMap.clear(); return send(new DisconnectMessage(clientId), listeners); } @Override protected TransportMessage createTransportMessage(AsyncToken token) throws UnsupportedEncodingException { Message[] messages = convertToAmf(token.getRequest()); return new DefaultTransportMessage<Message[]>(token.getId(), false, clientId, sessionId, messages, codec); } @Override protected ResponseMessage decodeResponse(InputStream is) throws IOException { boolean reconnect = true; try { if (is.available() > 0) { final Message[] messages = codec.decode(is); if (messages.length > 0 && messages[0] instanceof AcknowledgeMessage) { reconnect = false; final AbstractResponseMessage response = convertFromAmf((AcknowledgeMessage)messages[0]); if (response instanceof ResultMessage) { RequestMessage request = getRequest(response.getCorrelationId()); if (request != null) { ResultMessage result = (ResultMessage)response; switch (request.getType()) { 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) this.reconnectMaxAttempts = ((Number)reconnectMaxAttempts).longValue(); } break; case SUBSCRIBE: result.setResult(messages[0].getHeader(AsyncMessage.DESTINATION_CLIENT_ID_HEADER)); break; default: break; } } } AbstractResponseMessage current = response; for (int i = 1; i < messages.length; i++) { if (!(messages[i] instanceof AcknowledgeMessage)) throw new RuntimeException("Message should be an AcknowledgeMessage: " + messages[i]); AbstractResponseMessage next = convertFromAmf((AcknowledgeMessage)messages[i]); current.setNext(next); current = next; } return response; } for (Message message : messages) { 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("No consumer for subscriptionId: %s", subscriptionId); } } } finally { if (reconnect) { connectMessageId.set(null); connect(); } } return null; } @Override public void onError(TransportMessage message, Exception e) { super.onError(message, e); if (message != null && connectMessageId.compareAndSet(message.getId(), null)) scheduleReconnectTimerTask(); } protected void cancelReconnectTimerTask() { ReconnectTimerTask task = reconnectTimerTask.getAndSet(null); if (task != null && task.cancel()) reconnectAttempts = 0L; } protected void scheduleReconnectTimerTask() { ReconnectTimerTask task = new ReconnectTimerTask(); ReconnectTimerTask previousTask = reconnectTimerTask.getAndSet(task); if (previousTask != null) previousTask.cancel(); if (reconnectAttempts < reconnectMaxAttempts) { reconnectAttempts++; schedule(task, reconnectIntervalMillis); } } class ReconnectTimerTask extends TimerTask { @Override public void run() { connect(); } } }