001 /**
002 * GRANITE DATA SERVICES
003 * Copyright (C) 2006-2013 GRANITE DATA SERVICES S.A.S.
004 *
005 * This file is part of the Granite Data Services Platform.
006 *
007 * Granite Data Services is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * Granite Data Services is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
015 * General Public License for more details.
016 *
017 * You should have received a copy of the GNU Lesser General Public
018 * License along with this library; if not, write to the Free Software
019 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
020 * USA, or see <http://www.gnu.org/licenses/>.
021 */
022 package org.granite.gravity;
023
024 import java.io.IOException;
025 import java.io.ObjectOutput;
026 import java.io.OutputStream;
027 import java.net.SocketException;
028 import java.util.Collection;
029 import java.util.LinkedList;
030 import java.util.concurrent.ConcurrentHashMap;
031 import java.util.concurrent.ConcurrentMap;
032 import java.util.concurrent.locks.Lock;
033 import java.util.concurrent.locks.ReentrantLock;
034
035 import javax.servlet.http.HttpServletRequest;
036 import javax.servlet.http.HttpServletResponse;
037
038 import org.granite.context.AMFContextImpl;
039 import org.granite.context.GraniteContext;
040 import org.granite.gravity.udp.UdpReceiver;
041 import org.granite.gravity.udp.UdpReceiverFactory;
042 import org.granite.logging.Logger;
043 import org.granite.messaging.webapp.HttpGraniteContext;
044 import org.granite.util.ContentType;
045
046 import flex.messaging.messages.AsyncMessage;
047 import flex.messaging.messages.Message;
048
049 /**
050 * @author Franck WOLFF
051 */
052 public abstract class AbstractChannel implements Channel {
053
054 ///////////////////////////////////////////////////////////////////////////
055 // Fields.
056
057 private static final Logger log = Logger.getLogger(AbstractChannel.class);
058
059 protected final String id;
060 protected final String sessionId;
061 protected final String clientType;
062 protected final Gravity gravity;
063 protected final ChannelFactory<? extends Channel> factory;
064 // protected final ServletConfig servletConfig;
065
066 protected final ConcurrentMap<String, Subscription> subscriptions = new ConcurrentHashMap<String, Subscription>();
067
068 protected LinkedList<AsyncPublishedMessage> publishedQueue = new LinkedList<AsyncPublishedMessage>();
069 protected final Lock publishedQueueLock = new ReentrantLock();
070
071 protected LinkedList<AsyncMessage> receivedQueue = new LinkedList<AsyncMessage>();
072 protected final Lock receivedQueueLock = new ReentrantLock();
073
074 protected final AsyncPublisher publisher;
075 protected final AsyncReceiver httpReceiver;
076
077 protected UdpReceiver udpReceiver = null;
078
079 ///////////////////////////////////////////////////////////////////////////
080 // Constructor.
081
082 protected AbstractChannel(Gravity gravity, String id, ChannelFactory<? extends Channel> factory, String clientType) {
083 if (id == null)
084 throw new NullPointerException("id cannot be null");
085
086 this.id = id;
087 GraniteContext graniteContext = GraniteContext.getCurrentInstance();
088 this.clientType = clientType;
089 this.sessionId = graniteContext != null ? graniteContext.getSessionId() : null;
090 this.gravity = gravity;
091 this.factory = factory;
092
093 this.publisher = new AsyncPublisher(this);
094 this.httpReceiver = new AsyncReceiver(this);
095 }
096
097 ///////////////////////////////////////////////////////////////////////////
098 // Abstract protected method.
099
100 protected abstract boolean hasAsyncHttpContext();
101 protected abstract AsyncHttpContext acquireAsyncHttpContext();
102 protected abstract void releaseAsyncHttpContext(AsyncHttpContext context);
103
104 ///////////////////////////////////////////////////////////////////////////
105 // Channel interface implementation.
106
107 public String getId() {
108 return id;
109 }
110
111 public String getClientType() {
112 return clientType;
113 }
114
115 public ChannelFactory<? extends Channel> getFactory() {
116 return factory;
117 }
118
119 public Gravity getGravity() {
120 return gravity;
121 }
122
123 public Subscription addSubscription(String destination, String subTopicId, String subscriptionId, boolean noLocal) {
124 Subscription subscription = new Subscription(this, destination, subTopicId, subscriptionId, noLocal);
125 Subscription present = subscriptions.putIfAbsent(subscriptionId, subscription);
126 return (present != null ? present : subscription);
127 }
128
129 public Collection<Subscription> getSubscriptions() {
130 return subscriptions.values();
131 }
132
133 public Subscription removeSubscription(String subscriptionId) {
134 return subscriptions.remove(subscriptionId);
135 }
136
137 public void publish(AsyncPublishedMessage message) throws MessagePublishingException {
138 if (message == null)
139 throw new NullPointerException("message cannot be null");
140
141 publishedQueueLock.lock();
142 try {
143 publishedQueue.add(message);
144 }
145 finally {
146 publishedQueueLock.unlock();
147 }
148
149 publisher.queue(getGravity());
150 }
151
152 public boolean hasPublishedMessage() {
153 publishedQueueLock.lock();
154 try {
155 return !publishedQueue.isEmpty();
156 }
157 finally {
158 publishedQueueLock.unlock();
159 }
160 }
161
162 public boolean runPublish() {
163 LinkedList<AsyncPublishedMessage> publishedCopy = null;
164
165 publishedQueueLock.lock();
166 try {
167 if (publishedQueue.isEmpty())
168 return false;
169 publishedCopy = publishedQueue;
170 publishedQueue = new LinkedList<AsyncPublishedMessage>();
171 }
172 finally {
173 publishedQueueLock.unlock();
174 }
175
176 for (AsyncPublishedMessage message : publishedCopy) {
177 try {
178 message.publish(this);
179 }
180 catch (Exception e) {
181 log.error(e, "Error while trying to publish message: %s", message);
182 }
183 }
184
185 return true;
186 }
187
188 public void receive(AsyncMessage message) throws MessageReceivingException {
189 if (message == null)
190 throw new NullPointerException("message cannot be null");
191
192 Gravity gravity = getGravity();
193
194 if (udpReceiver != null) {
195 if (udpReceiver.isClosed())
196 return;
197
198 try {
199 udpReceiver.receive(message);
200 }
201 catch (MessageReceivingException e) {
202 if (e.getCause() instanceof SocketException) {
203 log.debug(e, "Closing unreachable UDP channel %s", getId());
204 udpReceiver.close(false);
205 }
206 else
207 log.error(e, "Cannot access UDP channel %s", getId());
208 }
209 return;
210 }
211
212 receivedQueueLock.lock();
213 try {
214 if (receivedQueue.size() + 1 > gravity.getGravityConfig().getMaxMessagesQueuedPerChannel())
215 throw new MessageReceivingException(message, "Could not queue message (channel's queue is full) for channel: " + this);
216
217 receivedQueue.add(message);
218 }
219 finally {
220 receivedQueueLock.unlock();
221 }
222
223 if (hasAsyncHttpContext())
224 httpReceiver.queue(gravity);
225 }
226
227 public boolean hasReceivedMessage() {
228 receivedQueueLock.lock();
229 try {
230 return !receivedQueue.isEmpty();
231 }
232 finally {
233 receivedQueueLock.unlock();
234 }
235 }
236
237 public boolean runReceive() {
238 return runReceived(null);
239 }
240
241 public ObjectOutput newSerializer(GraniteContext context, OutputStream os) {
242 return context.getGraniteConfig().newAMF3Serializer(os);
243 }
244
245 public String getSerializerContentType() {
246 return ContentType.AMF.mimeType();
247 }
248
249 protected void createUdpReceiver(UdpReceiverFactory factory, AsyncHttpContext asyncHttpContext) {
250 OutputStream os = null;
251 try {
252 Message connectMessage = asyncHttpContext.getConnectMessage();
253
254 if (udpReceiver == null || udpReceiver.isClosed())
255 udpReceiver = factory.newReceiver(this, asyncHttpContext.getRequest(), connectMessage);
256
257 AsyncMessage reply = udpReceiver.acknowledge(connectMessage);
258
259 HttpServletRequest request = asyncHttpContext.getRequest();
260 HttpServletResponse response = asyncHttpContext.getResponse();
261
262 GraniteContext context = HttpGraniteContext.createThreadIntance(
263 gravity.getGraniteConfig(), gravity.getServicesConfig(),
264 null, request, response
265 );
266 ((AMFContextImpl)context.getAMFContext()).setCurrentAmf3Message(asyncHttpContext.getConnectMessage());
267
268 response.setStatus(HttpServletResponse.SC_OK);
269 response.setContentType(getSerializerContentType());
270 response.setDateHeader("Expire", 0L);
271 response.setHeader("Cache-Control", "no-store");
272
273 os = response.getOutputStream();
274
275 ObjectOutput serializer = newSerializer(context, os);
276
277 serializer.writeObject(new AsyncMessage[] { reply });
278
279 os.flush();
280 response.flushBuffer();
281 }
282 catch (IOException e) {
283 log.error(e, "Could not send UDP connect acknowledgement to channel: %s", this);
284 }
285 finally {
286 try {
287 GraniteContext.release();
288 }
289 catch (Exception e) {
290 // should never happen...
291 }
292
293 // Close output stream.
294 try {
295 if (os != null) {
296 try {
297 os.close();
298 }
299 catch (IOException e) {
300 log.warn(e, "Could not close output stream (ignored)");
301 }
302 }
303 }
304 finally {
305 releaseAsyncHttpContext(asyncHttpContext);
306 }
307 }
308 }
309
310 public boolean runReceived(AsyncHttpContext asyncHttpContext) {
311
312 Gravity gravity = getGravity();
313
314 if (asyncHttpContext != null && gravity.hasUdpReceiverFactory()) {
315 UdpReceiverFactory factory = gravity.getUdpReceiverFactory();
316
317 if (factory.isUdpConnectRequest(asyncHttpContext.getConnectMessage())) {
318 createUdpReceiver(factory, asyncHttpContext);
319 return true;
320 }
321
322 if (udpReceiver != null) {
323 if (!udpReceiver.isClosed())
324 udpReceiver.close(false);
325 udpReceiver = null;
326 }
327 }
328
329 boolean httpAsParam = (asyncHttpContext != null);
330 LinkedList<AsyncMessage> messages = null;
331 OutputStream os = null;
332
333 try {
334 receivedQueueLock.lock();
335 try {
336 // Do we have any pending messages?
337 if (receivedQueue.isEmpty())
338 return false;
339
340 // Do we have a valid http context?
341 if (asyncHttpContext == null) {
342 asyncHttpContext = acquireAsyncHttpContext();
343 if (asyncHttpContext == null)
344 return false;
345 }
346
347 // Both conditions are ok, get all pending messages.
348 messages = receivedQueue;
349 receivedQueue = new LinkedList<AsyncMessage>();
350 }
351 finally {
352 receivedQueueLock.unlock();
353 }
354
355 HttpServletRequest request = asyncHttpContext.getRequest();
356 HttpServletResponse response = asyncHttpContext.getResponse();
357
358 // Set response messages correlation ids to connect request message id.
359 String correlationId = asyncHttpContext.getConnectMessage().getMessageId();
360 AsyncMessage[] messagesArray = new AsyncMessage[messages.size()];
361 int i = 0;
362 for (AsyncMessage message : messages) {
363 message.setCorrelationId(correlationId);
364 messagesArray[i++] = message;
365 }
366
367 // Setup serialization context (thread local)
368 GraniteContext context = HttpGraniteContext.createThreadIntance(
369 gravity.getGraniteConfig(), gravity.getServicesConfig(),
370 null, request, response
371 );
372 ((AMFContextImpl)context.getAMFContext()).setCurrentAmf3Message(asyncHttpContext.getConnectMessage());
373
374 // Write messages to response output stream.
375
376 response.setStatus(HttpServletResponse.SC_OK);
377 response.setContentType(getSerializerContentType());
378 response.setDateHeader("Expire", 0L);
379 response.setHeader("Cache-Control", "no-store");
380
381 os = response.getOutputStream();
382 ObjectOutput serializer = newSerializer(context, os);
383
384 log.debug("<< [MESSAGES for channel=%s] %s", this, messagesArray);
385
386 serializer.writeObject(messagesArray);
387
388 os.flush();
389 response.flushBuffer();
390
391 return true; // Messages were delivered, http context isn't valid anymore.
392 }
393 catch (IOException e) {
394 log.warn(e, "Could not send messages to channel: %s (retrying later)", this);
395
396 GravityConfig gravityConfig = getGravity().getGravityConfig();
397 if (gravityConfig.isRetryOnError()) {
398 receivedQueueLock.lock();
399 try {
400 if (receivedQueue.size() + messages.size() > gravityConfig.getMaxMessagesQueuedPerChannel()) {
401 log.warn(
402 "Channel %s has reached its maximum queue capacity %s (throwing %s messages)",
403 this,
404 gravityConfig.getMaxMessagesQueuedPerChannel(),
405 messages.size()
406 );
407 }
408 else
409 receivedQueue.addAll(0, messages);
410 }
411 finally {
412 receivedQueueLock.unlock();
413 }
414 }
415
416 return true; // Messages weren't delivered, but http context isn't valid anymore.
417 }
418 finally {
419 // Cleanup serialization context (thread local)
420 try {
421 GraniteContext.release();
422 }
423 catch (Exception e) {
424 // should never happen...
425 }
426
427 // Close output stream.
428 try {
429 if (os != null) {
430 try {
431 os.close();
432 }
433 catch (IOException e) {
434 log.warn(e, "Could not close output stream (ignored)");
435 }
436 }
437 }
438 finally {
439 // Cleanup http context (only if this method wasn't explicitly called with a non null
440 // AsyncHttpContext from the servlet).
441 if (!httpAsParam)
442 releaseAsyncHttpContext(asyncHttpContext);
443 }
444 }
445 }
446
447 public void destroy() {
448 destroy(false);
449 }
450
451 public void destroy(boolean timeout) {
452 try {
453 Gravity gravity = getGravity();
454 gravity.cancel(publisher);
455 gravity.cancel(httpReceiver);
456
457 subscriptions.clear();
458 }
459 finally {
460 if (udpReceiver != null) {
461 if (!udpReceiver.isClosed())
462 udpReceiver.close(timeout);
463 udpReceiver = null;
464 }
465 }
466 }
467
468 ///////////////////////////////////////////////////////////////////////////
469 // Protected utilities.
470
471 protected boolean queueReceiver() {
472 if (hasReceivedMessage()) {
473 httpReceiver.queue(getGravity());
474 return true;
475 }
476 return false;
477 }
478
479 ///////////////////////////////////////////////////////////////////////////
480 // Object overwritten methods.
481
482 @Override
483 public boolean equals(Object obj) {
484 return (obj instanceof Channel && id.equals(((Channel)obj).getId()));
485 }
486
487 @Override
488 public int hashCode() {
489 return id.hashCode();
490 }
491
492 @Override
493 public String toString() {
494 return getClass().getName() + " {id=" + id + ", subscriptions=" + subscriptions.values() + "}";
495 }
496 }