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.amf;
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.Map;
027    import java.util.TimerTask;
028    import java.util.concurrent.ConcurrentHashMap;
029    import java.util.concurrent.ConcurrentMap;
030    import java.util.concurrent.TimeUnit;
031    import java.util.concurrent.atomic.AtomicReference;
032    
033    import org.granite.client.messaging.Consumer;
034    import org.granite.client.messaging.ResponseListener;
035    import org.granite.client.messaging.channel.AsyncToken;
036    import org.granite.client.messaging.channel.Channel;
037    import org.granite.client.messaging.channel.MessagingChannel;
038    import org.granite.client.messaging.channel.ResponseMessageFuture;
039    import org.granite.client.messaging.codec.MessagingCodec;
040    import org.granite.client.messaging.messages.RequestMessage;
041    import org.granite.client.messaging.messages.ResponseMessage;
042    import org.granite.client.messaging.messages.requests.DisconnectMessage;
043    import org.granite.client.messaging.messages.responses.AbstractResponseMessage;
044    import org.granite.client.messaging.messages.responses.ResultMessage;
045    import org.granite.client.messaging.transport.DefaultTransportMessage;
046    import org.granite.client.messaging.transport.Transport;
047    import org.granite.client.messaging.transport.TransportMessage;
048    import org.granite.logging.Logger;
049    import org.granite.util.UUIDUtil;
050    
051    import flex.messaging.messages.AcknowledgeMessage;
052    import flex.messaging.messages.AsyncMessage;
053    import flex.messaging.messages.CommandMessage;
054    import flex.messaging.messages.Message;
055    
056    /**
057     * @author Franck WOLFF
058     */
059    public class AbstractAMFMessagingChannel extends AbstractAMFChannel implements MessagingChannel {
060            
061            private static final Logger log = Logger.getLogger(AbstractAMFMessagingChannel.class);
062            
063            protected final MessagingCodec<Message[]> codec;
064            
065            protected String sessionId = null;
066            protected final ConcurrentMap<String, Consumer> consumersMap = new ConcurrentHashMap<String, Consumer>();   
067            protected final AtomicReference<String> connectMessageId = new AtomicReference<String>(null);
068            protected final AtomicReference<ReconnectTimerTask> reconnectTimerTask = new AtomicReference<ReconnectTimerTask>();
069            
070            protected volatile long reconnectIntervalMillis = TimeUnit.SECONDS.toMillis(30L);
071            protected volatile long reconnectMaxAttempts = 60L;
072            protected volatile long reconnectAttempts = 0L;
073    
074            protected AbstractAMFMessagingChannel(MessagingCodec<Message[]> codec, Transport transport, String id, URI uri) {
075                    super(transport, id, uri, 1);
076                    
077                    this.codec = codec;
078            }
079            
080            public void setSessionId(String sessionId) {
081                    if ((sessionId == null && this.sessionId != null) || (sessionId != null && !sessionId.equals(this.sessionId))) {
082                            this.sessionId = sessionId;
083                            log.info("Messaging channel sessionId %s", sessionId);
084                    }                               
085            }
086    
087            protected boolean connect() {
088                    
089                    // Connecting: make sure we don't have an active reconnect timer task.
090                    cancelReconnectTimerTask();
091                    
092                    // No subscriptions...
093                    if (consumersMap.isEmpty())
094                            return false;
095                    
096                    // We are already waiting for a connection/answer.
097                    final String id = UUIDUtil.randomUUID();
098                    if (!connectMessageId.compareAndSet(null, id))
099                            return false;
100                    
101                    log.debug("Connecting channel with clientId %s", clientId);
102                    
103                    // Create and try to send the connect message.
104                    CommandMessage connectMessage = new CommandMessage();
105                    connectMessage.setOperation(CommandMessage.CONNECT_OPERATION);
106                    connectMessage.setMessageId(id);
107                    connectMessage.setTimestamp(System.currentTimeMillis());
108                    connectMessage.setClientId(clientId);
109    
110                    try {
111                            transport.send(this, new DefaultTransportMessage<Message[]>(id, true, clientId, sessionId, new Message[]{connectMessage}, codec));
112                            
113                            return true;
114                    }
115                    catch (Exception e) {
116                            // Connect immediately failed, release the message id and schedule a reconnect.
117                            connectMessageId.set(null);
118                            scheduleReconnectTimerTask();
119                            
120                            return false;
121                    }
122            }
123            
124            @Override
125            public void addConsumer(Consumer consumer) {
126                    consumersMap.putIfAbsent(consumer.getSubscriptionId(), consumer);
127                    
128                    connect();
129            }
130    
131            @Override
132            public boolean removeConsumer(Consumer consumer) {
133                    return (consumersMap.remove(consumer.getSubscriptionId()) != null);
134            }
135            
136            public synchronized ResponseMessageFuture disconnect(ResponseListener...listeners) {
137                    cancelReconnectTimerTask();
138                    
139                    connectMessageId.set(null);
140                    reconnectAttempts = 0L;
141                    
142                    for (Consumer consumer : consumersMap.values())
143                            consumer.onDisconnect();
144                    
145                    consumersMap.clear();   
146                    
147                    return send(new DisconnectMessage(clientId), listeners);
148            }
149    
150            @Override
151            protected TransportMessage createTransportMessage(AsyncToken token) throws UnsupportedEncodingException {
152                    Message[] messages = convertToAmf(token.getRequest());
153                    return new DefaultTransportMessage<Message[]>(token.getId(), false, clientId, sessionId, messages, codec);
154            }
155    
156            @Override
157            protected ResponseMessage decodeResponse(InputStream is) throws IOException {
158                    boolean reconnect = true;
159                    
160                    try {
161                            if (is.available() > 0) {
162                                    final Message[] messages = codec.decode(is);
163                                    
164                                    if (messages.length > 0 && messages[0] instanceof AcknowledgeMessage) {
165                                            
166                                            reconnect = false;
167    
168                                            final AbstractResponseMessage response = convertFromAmf((AcknowledgeMessage)messages[0]);
169                    
170                                            if (response instanceof ResultMessage) {
171                                                    RequestMessage request = getRequest(response.getCorrelationId());
172                                                    if (request != null) {
173                                                            ResultMessage result = (ResultMessage)response;
174                                                            switch (request.getType()) {
175    
176                                                            case PING:
177                                                                    if (messages[0].getBody() instanceof Map) {
178                                                                            Map<?, ?> advices = (Map<?, ?>)messages[0].getBody();
179                                                                            Object reconnectIntervalMillis = advices.get(Channel.RECONNECT_INTERVAL_MS_KEY);
180                                                                            if (reconnectIntervalMillis instanceof Number)
181                                                                                    this.reconnectIntervalMillis = ((Number)reconnectIntervalMillis).longValue();
182                                                                            Object reconnectMaxAttempts = advices.get(Channel.RECONNECT_MAX_ATTEMPTS_KEY);
183                                                                            if (reconnectMaxAttempts instanceof Number)
184                                                                                    this.reconnectMaxAttempts = ((Number)reconnectMaxAttempts).longValue();
185                                                                    }
186                                                                    break;
187                                                            
188                                                            case SUBSCRIBE:
189                                                                    result.setResult(messages[0].getHeader(AsyncMessage.DESTINATION_CLIENT_ID_HEADER));
190                                                                    break;
191    
192                                                            default:
193                                                                    break;
194                                                            }
195                                                    }
196                                            }
197                                            
198                                            AbstractResponseMessage current = response;
199                                            for (int i = 1; i < messages.length; i++) {
200                                                    if (!(messages[i] instanceof AcknowledgeMessage))
201                                                            throw new RuntimeException("Message should be an AcknowledgeMessage: " + messages[i]);
202                                                    
203                                                    AbstractResponseMessage next = convertFromAmf((AcknowledgeMessage)messages[i]);
204                                                    current.setNext(next);
205                                                    current = next;
206                                            }
207                                            
208                                            return response;
209                                    }
210                                    
211                                    for (Message message : messages) {
212                                            if (!(message instanceof AsyncMessage))
213                                                    throw new RuntimeException("Message should be an AsyncMessage: " + message);
214                                            
215                                            String subscriptionId = (String)message.getHeader(AsyncMessage.DESTINATION_CLIENT_ID_HEADER);
216                                            Consumer consumer = consumersMap.get(subscriptionId);
217                                            if (consumer != null)
218                                                    consumer.onMessage(convertFromAmf((AsyncMessage)message));
219                                            else
220                                                    log.warn("No consumer for subscriptionId: %s", subscriptionId);
221                                    }
222                            }
223                    }
224                    finally {
225                            if (reconnect) {
226                                    connectMessageId.set(null);
227                                    connect();
228                            }
229                    }
230                    
231                    return null;
232            }
233    
234            @Override
235            public void onError(TransportMessage message, Exception e) {
236                    super.onError(message, e);
237                    
238                    if (message != null && connectMessageId.compareAndSet(message.getId(), null))
239                            scheduleReconnectTimerTask();
240            }
241    
242            protected void cancelReconnectTimerTask() {
243                    ReconnectTimerTask task = reconnectTimerTask.getAndSet(null);
244                    if (task != null && task.cancel())
245                            reconnectAttempts = 0L;
246            }
247            
248            protected void scheduleReconnectTimerTask() {
249                    ReconnectTimerTask task = new ReconnectTimerTask();
250                    
251                    ReconnectTimerTask previousTask = reconnectTimerTask.getAndSet(task);
252                    if (previousTask != null)
253                            previousTask.cancel();
254                    
255                    if (reconnectAttempts < reconnectMaxAttempts) {
256                            reconnectAttempts++;
257                            schedule(task, reconnectIntervalMillis);
258                    }
259            }
260            
261            class ReconnectTimerTask extends TimerTask {
262    
263                    @Override
264                    public void run() {
265                            connect();
266                    }
267            }
268    
269    }