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 */ 022package org.granite.gravity; 023 024import java.io.IOException; 025import java.io.ObjectOutput; 026import java.io.OutputStream; 027import java.net.SocketException; 028import java.util.Collection; 029import java.util.LinkedList; 030import java.util.concurrent.ConcurrentHashMap; 031import java.util.concurrent.ConcurrentMap; 032import java.util.concurrent.locks.Lock; 033import java.util.concurrent.locks.ReentrantLock; 034 035import javax.servlet.http.HttpServletRequest; 036import javax.servlet.http.HttpServletResponse; 037 038import org.granite.context.AMFContextImpl; 039import org.granite.context.GraniteContext; 040import org.granite.gravity.udp.UdpReceiver; 041import org.granite.gravity.udp.UdpReceiverFactory; 042import org.granite.logging.Logger; 043import org.granite.messaging.webapp.HttpGraniteContext; 044import org.granite.util.ContentType; 045 046import flex.messaging.messages.AsyncMessage; 047import flex.messaging.messages.Message; 048 049/** 050 * @author Franck WOLFF 051 */ 052public 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 receiver; 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.receiver = 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 log.debug("Channel %s queue message %s for client %s", getId(), message.getMessageId(), message.getClientId()); 218 receivedQueue.add(message); 219 } 220 finally { 221 receivedQueueLock.unlock(); 222 } 223 224 if (hasAsyncHttpContext()) 225 receiver.queue(gravity); 226 } 227 228 public boolean hasReceivedMessage() { 229 receivedQueueLock.lock(); 230 try { 231 return !receivedQueue.isEmpty(); 232 } 233 finally { 234 receivedQueueLock.unlock(); 235 } 236 } 237 238 public boolean runReceive() { 239 return runReceived(null); 240 } 241 242 public ObjectOutput newSerializer(GraniteContext context, OutputStream os) { 243 return context.getGraniteConfig().newAMF3Serializer(os); 244 } 245 246 public String getSerializerContentType() { 247 return ContentType.AMF.mimeType(); 248 } 249 250 protected void createUdpReceiver(UdpReceiverFactory factory, AsyncHttpContext asyncHttpContext) { 251 OutputStream os = null; 252 try { 253 Message connectMessage = asyncHttpContext.getConnectMessage(); 254 255 if (udpReceiver == null || udpReceiver.isClosed()) 256 udpReceiver = factory.newReceiver(this, asyncHttpContext.getRequest(), connectMessage); 257 258 AsyncMessage reply = udpReceiver.acknowledge(connectMessage); 259 260 HttpServletRequest request = asyncHttpContext.getRequest(); 261 HttpServletResponse response = asyncHttpContext.getResponse(); 262 263 GraniteContext context = HttpGraniteContext.createThreadIntance( 264 gravity.getGraniteConfig(), gravity.getServicesConfig(), 265 null, request, response 266 ); 267 ((AMFContextImpl)context.getAMFContext()).setCurrentAmf3Message(asyncHttpContext.getConnectMessage()); 268 269 response.setStatus(HttpServletResponse.SC_OK); 270 response.setContentType(getSerializerContentType()); 271 response.setDateHeader("Expire", 0L); 272 response.setHeader("Cache-Control", "no-store"); 273 274 os = response.getOutputStream(); 275 276 ObjectOutput serializer = newSerializer(context, os); 277 278 serializer.writeObject(new AsyncMessage[] { reply }); 279 280 os.flush(); 281 response.flushBuffer(); 282 } 283 catch (IOException e) { 284 log.error(e, "Could not send UDP connect acknowledgement to channel: %s", this); 285 } 286 finally { 287 try { 288 GraniteContext.release(); 289 } 290 catch (Exception e) { 291 // should never happen... 292 } 293 294 // Close output stream. 295 try { 296 if (os != null) { 297 try { 298 os.close(); 299 } 300 catch (IOException e) { 301 log.warn(e, "Could not close output stream (ignored)"); 302 } 303 } 304 } 305 finally { 306 releaseAsyncHttpContext(asyncHttpContext); 307 } 308 } 309 } 310 311 public boolean runReceived(AsyncHttpContext asyncHttpContext) { 312 313 Gravity gravity = getGravity(); 314 315 if (asyncHttpContext != null && gravity.hasUdpReceiverFactory()) { 316 UdpReceiverFactory factory = gravity.getUdpReceiverFactory(); 317 318 if (factory.isUdpConnectRequest(asyncHttpContext.getConnectMessage())) { 319 createUdpReceiver(factory, asyncHttpContext); 320 return true; 321 } 322 323 if (udpReceiver != null) { 324 if (!udpReceiver.isClosed()) 325 udpReceiver.close(false); 326 udpReceiver = null; 327 } 328 } 329 330 boolean httpAsParam = (asyncHttpContext != null); 331 LinkedList<AsyncMessage> messages = null; 332 OutputStream os = null; 333 334 try { 335 receivedQueueLock.lock(); 336 try { 337 // Do we have any pending messages? 338 if (receivedQueue.isEmpty()) 339 return false; 340 341 // Do we have a valid http context? 342 if (asyncHttpContext == null) { 343 asyncHttpContext = acquireAsyncHttpContext(); 344 if (asyncHttpContext == null) 345 return false; 346 } 347 348 // Both conditions are ok, get all pending messages. 349 messages = receivedQueue; 350 receivedQueue = new LinkedList<AsyncMessage>(); 351 } 352 finally { 353 receivedQueueLock.unlock(); 354 } 355 356 HttpServletRequest request = asyncHttpContext.getRequest(); 357 HttpServletResponse response = asyncHttpContext.getResponse(); 358 359 // Set response messages correlation ids to connect request message id. 360 String correlationId = asyncHttpContext.getConnectMessage().getMessageId(); 361 AsyncMessage[] messagesArray = new AsyncMessage[messages.size()]; 362 int i = 0; 363 for (AsyncMessage message : messages) { 364 message.setCorrelationId(correlationId); 365 messagesArray[i++] = message; 366 } 367 368 // Setup serialization context (thread local) 369 GraniteContext context = HttpGraniteContext.createThreadIntance( 370 gravity.getGraniteConfig(), gravity.getServicesConfig(), 371 null, request, response 372 ); 373 ((AMFContextImpl)context.getAMFContext()).setCurrentAmf3Message(asyncHttpContext.getConnectMessage()); 374 375 // Write messages to response output stream. 376 377 response.setStatus(HttpServletResponse.SC_OK); 378 response.setContentType(getSerializerContentType()); 379 response.setDateHeader("Expire", 0L); 380 response.setHeader("Cache-Control", "no-store"); 381 382 os = response.getOutputStream(); 383 ObjectOutput serializer = newSerializer(context, os); 384 385 log.debug("<< [MESSAGES for channel=%s] %s", this, messagesArray); 386 387 serializer.writeObject(messagesArray); 388 389 os.flush(); 390 response.flushBuffer(); 391 392 return true; // Messages were delivered, http context isn't valid anymore. 393 } 394 catch (IOException e) { 395 log.warn(e, "Could not send messages to channel: %s (retrying later)", getId()); 396 397 GravityConfig gravityConfig = getGravity().getGravityConfig(); 398 if (gravityConfig.isRetryOnError()) { 399 receivedQueueLock.lock(); 400 try { 401 if (receivedQueue.size() + messages.size() > gravityConfig.getMaxMessagesQueuedPerChannel()) { 402 log.warn( 403 "Channel %s has reached its maximum queue capacity %s (throwing %s messages)", 404 getId(), 405 gravityConfig.getMaxMessagesQueuedPerChannel(), 406 messages.size() 407 ); 408 } 409 else 410 receivedQueue.addAll(0, messages); 411 } 412 finally { 413 receivedQueueLock.unlock(); 414 } 415 } 416 417 return true; // Messages weren't delivered, but http context isn't valid anymore. 418 } 419 finally { 420 // Cleanup serialization context (thread local) 421 try { 422 GraniteContext.release(); 423 } 424 catch (Exception e) { 425 // should never happen... 426 } 427 428 // Close output stream. 429 try { 430 if (os != null) { 431 try { 432 os.close(); 433 } 434 catch (IOException e) { 435 log.warn(e, "Could not close output stream (ignored)"); 436 } 437 } 438 } 439 finally { 440 // Cleanup http context (only if this method wasn't explicitly called with a non null 441 // AsyncHttpContext from the servlet). 442 if (!httpAsParam) 443 releaseAsyncHttpContext(asyncHttpContext); 444 } 445 } 446 } 447 448 public void destroy() { 449 destroy(false); 450 } 451 452 public void destroy(boolean timeout) { 453 try { 454 Gravity gravity = getGravity(); 455 gravity.cancel(publisher); 456 gravity.cancel(receiver); 457 458 subscriptions.clear(); 459 } 460 finally { 461 if (udpReceiver != null) { 462 if (!udpReceiver.isClosed()) 463 udpReceiver.close(timeout); 464 udpReceiver = null; 465 } 466 } 467 } 468 469 /////////////////////////////////////////////////////////////////////////// 470 // Protected utilities. 471 472 protected boolean queueReceiver() { 473 if (hasReceivedMessage()) { 474 receiver.queue(getGravity()); 475 return true; 476 } 477 return false; 478 } 479 480 /////////////////////////////////////////////////////////////////////////// 481 // Object overwritten methods. 482 483 @Override 484 public boolean equals(Object obj) { 485 return (obj instanceof Channel && id.equals(((Channel)obj).getId())); 486 } 487 488 @Override 489 public int hashCode() { 490 return id.hashCode(); 491 } 492 493 @Override 494 public String toString() { 495 return getClass().getName() + " {id=" + id + ", subscriptions=" + subscriptions.values() + "}"; 496 } 497}