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 }