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 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}