001/* 002 GRANITE DATA SERVICES 003 Copyright (C) 2011 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 021package org.granite.gravity; 022 023import java.io.IOException; 024import java.io.ObjectOutput; 025import java.io.OutputStream; 026import java.util.Collection; 027import java.util.LinkedList; 028import java.util.concurrent.ConcurrentHashMap; 029import java.util.concurrent.ConcurrentMap; 030import java.util.concurrent.locks.Lock; 031import java.util.concurrent.locks.ReentrantLock; 032 033import javax.servlet.http.HttpServletRequest; 034import javax.servlet.http.HttpServletResponse; 035 036import org.granite.context.AMFContextImpl; 037import org.granite.context.GraniteContext; 038import org.granite.logging.Logger; 039import org.granite.messaging.webapp.HttpGraniteContext; 040import org.granite.util.ContentType; 041 042import flex.messaging.messages.AsyncMessage; 043 044/** 045 * @author Franck WOLFF 046 */ 047public abstract class AbstractChannel implements Channel { 048 049 /////////////////////////////////////////////////////////////////////////// 050 // Fields. 051 052 private static final Logger log = Logger.getLogger(AbstractChannel.class); 053 054 protected final String id; 055 protected final String sessionId; 056 protected final String clientType; 057 protected final Gravity gravity; 058 protected final ChannelFactory<? extends Channel> factory; 059 // protected final ServletConfig servletConfig; 060 061 protected final ConcurrentMap<String, Subscription> subscriptions = new ConcurrentHashMap<String, Subscription>(); 062 063 protected LinkedList<AsyncPublishedMessage> publishedQueue = new LinkedList<AsyncPublishedMessage>(); 064 protected final Lock publishedQueueLock = new ReentrantLock(); 065 066 protected LinkedList<AsyncMessage> receivedQueue = new LinkedList<AsyncMessage>(); 067 protected final Lock receivedQueueLock = new ReentrantLock(); 068 069 protected final AsyncPublisher publisher; 070 protected final AsyncReceiver receiver; 071 072 /////////////////////////////////////////////////////////////////////////// 073 // Constructor. 074 075 protected AbstractChannel(Gravity gravity, String id, ChannelFactory<? extends Channel> factory, String clientType) { 076 if (id == null) 077 throw new NullPointerException("id cannot be null"); 078 079 this.id = id; 080 GraniteContext graniteContext = GraniteContext.getCurrentInstance(); 081 this.clientType = clientType; 082 this.sessionId = graniteContext != null ? graniteContext.getSessionId() : null; 083 this.gravity = gravity; 084 this.factory = factory; 085 086 this.publisher = new AsyncPublisher(this); 087 this.receiver = new AsyncReceiver(this); 088 } 089 090 /////////////////////////////////////////////////////////////////////////// 091 // Abstract protected method. 092 093 protected abstract boolean hasAsyncHttpContext(); 094 protected abstract AsyncHttpContext acquireAsyncHttpContext(); 095 protected abstract void releaseAsyncHttpContext(AsyncHttpContext context); 096 097 /////////////////////////////////////////////////////////////////////////// 098 // Channel interface implementation. 099 100 public String getId() { 101 return id; 102 } 103 104 public String getClientType() { 105 return clientType; 106 } 107 108 public ChannelFactory<? extends Channel> getFactory() { 109 return factory; 110 } 111 112 public Gravity getGravity() { 113 return gravity; 114 } 115 116 public Subscription addSubscription(String destination, String subTopicId, String subscriptionId, boolean noLocal) { 117 Subscription subscription = new Subscription(this, destination, subTopicId, subscriptionId, noLocal); 118 Subscription present = subscriptions.putIfAbsent(subscriptionId, subscription); 119 return (present != null ? present : subscription); 120 } 121 122 public Collection<Subscription> getSubscriptions() { 123 return subscriptions.values(); 124 } 125 126 public Subscription removeSubscription(String subscriptionId) { 127 return subscriptions.remove(subscriptionId); 128 } 129 130 public void publish(AsyncPublishedMessage message) throws MessagePublishingException { 131 if (message == null) 132 throw new NullPointerException("message cannot be null"); 133 134 publishedQueueLock.lock(); 135 try { 136 publishedQueue.add(message); 137 } 138 finally { 139 publishedQueueLock.unlock(); 140 } 141 142 publisher.queue(getGravity()); 143 } 144 145 public boolean hasPublishedMessage() { 146 publishedQueueLock.lock(); 147 try { 148 return !publishedQueue.isEmpty(); 149 } 150 finally { 151 publishedQueueLock.unlock(); 152 } 153 } 154 155 public boolean runPublish() { 156 LinkedList<AsyncPublishedMessage> publishedCopy = null; 157 158 publishedQueueLock.lock(); 159 try { 160 if (publishedQueue.isEmpty()) 161 return false; 162 publishedCopy = publishedQueue; 163 publishedQueue = new LinkedList<AsyncPublishedMessage>(); 164 } 165 finally { 166 publishedQueueLock.unlock(); 167 } 168 169 for (AsyncPublishedMessage message : publishedCopy) { 170 try { 171 message.publish(this); 172 } 173 catch (Exception e) { 174 log.error(e, "Error while trying to publish message: %s", message); 175 } 176 } 177 178 return true; 179 } 180 181 public void receive(AsyncMessage message) throws MessageReceivingException { 182 if (message == null) 183 throw new NullPointerException("message cannot be null"); 184 185 Gravity gravity = getGravity(); 186 187 receivedQueueLock.lock(); 188 try { 189 if (receivedQueue.size() + 1 > gravity.getGravityConfig().getMaxMessagesQueuedPerChannel()) 190 throw new MessageReceivingException(message, "Could not queue message (channel's queue is full) for channel: " + this); 191 192 receivedQueue.add(message); 193 } 194 finally { 195 receivedQueueLock.unlock(); 196 } 197 198 if (hasAsyncHttpContext()) 199 receiver.queue(gravity); 200 } 201 202 public boolean hasReceivedMessage() { 203 receivedQueueLock.lock(); 204 try { 205 return !receivedQueue.isEmpty(); 206 } 207 finally { 208 receivedQueueLock.unlock(); 209 } 210 } 211 212 public boolean runReceive() { 213 return runReceived(null); 214 } 215 216 protected ObjectOutput newSerializer(GraniteContext context, OutputStream os) { 217 return context.getGraniteConfig().newAMF3Serializer(os); 218 } 219 220 protected String getSerializerContentType() { 221 return ContentType.AMF.mimeType(); 222 } 223 224 public boolean runReceived(AsyncHttpContext asyncHttpContext) { 225 226 boolean httpAsParam = (asyncHttpContext != null); 227 LinkedList<AsyncMessage> messages = null; 228 OutputStream os = null; 229 230 try { 231 receivedQueueLock.lock(); 232 try { 233 // Do we have any pending messages? 234 if (receivedQueue.isEmpty()) 235 return false; 236 237 // Do we have a valid http context? 238 if (asyncHttpContext == null) { 239 asyncHttpContext = acquireAsyncHttpContext(); 240 if (asyncHttpContext == null) 241 return false; 242 } 243 244 // Both conditions are ok, get all pending messages. 245 messages = receivedQueue; 246 receivedQueue = new LinkedList<AsyncMessage>(); 247 } 248 finally { 249 receivedQueueLock.unlock(); 250 } 251 252 HttpServletRequest request = asyncHttpContext.getRequest(); 253 HttpServletResponse response = asyncHttpContext.getResponse(); 254 255 // Set response messages correlation ids to connect request message id. 256 String correlationId = asyncHttpContext.getConnectMessage().getMessageId(); 257 AsyncMessage[] messagesArray = new AsyncMessage[messages.size()]; 258 int i = 0; 259 for (AsyncMessage message : messages) { 260 message.setCorrelationId(correlationId); 261 messagesArray[i++] = message; 262 } 263 264 // Setup serialization context (thread local) 265 Gravity gravity = getGravity(); 266 GraniteContext context = HttpGraniteContext.createThreadIntance( 267 gravity.getGraniteConfig(), gravity.getServicesConfig(), 268 null, request, response 269 ); 270 ((AMFContextImpl)context.getAMFContext()).setCurrentAmf3Message(asyncHttpContext.getConnectMessage()); 271 272 // Write messages to response output stream. 273 274 response.setStatus(HttpServletResponse.SC_OK); 275 response.setContentType(getSerializerContentType()); 276 response.setDateHeader("Expire", 0L); 277 response.setHeader("Cache-Control", "no-store"); 278 279 os = response.getOutputStream(); 280 ObjectOutput serializer = newSerializer(context, os); 281 282 log.debug("<< [MESSAGES for channel=%s] %s", this, messagesArray); 283 284 serializer.writeObject(messagesArray); 285 286 os.flush(); 287 response.flushBuffer(); 288 289 return true; // Messages were delivered, http context isn't valid anymore. 290 } 291 catch (IOException e) { 292 log.warn(e, "Could not send messages to channel: %s (retrying later)", this); 293 294 GravityConfig gravityConfig = getGravity().getGravityConfig(); 295 if (gravityConfig.isRetryOnError()) { 296 receivedQueueLock.lock(); 297 try { 298 if (receivedQueue.size() + messages.size() > gravityConfig.getMaxMessagesQueuedPerChannel()) { 299 log.warn( 300 "Channel %s has reached its maximum queue capacity %s (throwing %s messages)", 301 this, 302 gravityConfig.getMaxMessagesQueuedPerChannel(), 303 messages.size() 304 ); 305 } 306 else 307 receivedQueue.addAll(0, messages); 308 } 309 finally { 310 receivedQueueLock.unlock(); 311 } 312 } 313 314 return true; // Messages weren't delivered, but http context isn't valid anymore. 315 } 316 finally { 317 // Cleanup serialization context (thread local) 318 try { 319 GraniteContext.release(); 320 } 321 catch (Exception e) { 322 // should never happen... 323 } 324 325 // Close output stream. 326 try { 327 if (os != null) { 328 try { 329 os.close(); 330 } 331 catch (IOException e) { 332 log.warn(e, "Could not close output stream (ignored)"); 333 } 334 } 335 } 336 finally { 337 // Cleanup http context (only if this method wasn't explicitly called with a non null 338 // AsyncHttpContext from the servlet). 339 if (!httpAsParam) 340 releaseAsyncHttpContext(asyncHttpContext); 341 } 342 } 343 } 344 345 public void destroy() { 346 Gravity gravity = getGravity(); 347 gravity.cancel(publisher); 348 gravity.cancel(receiver); 349 350 subscriptions.clear(); 351 } 352 353 /////////////////////////////////////////////////////////////////////////// 354 // Protected utilities. 355 356 protected boolean queueReceiver() { 357 if (hasReceivedMessage()) { 358 receiver.queue(getGravity()); 359 return true; 360 } 361 return false; 362 } 363 364 /////////////////////////////////////////////////////////////////////////// 365 // Object overwritten methods. 366 367 @Override 368 public boolean equals(Object obj) { 369 return (obj instanceof Channel && id.equals(((Channel)obj).getId())); 370 } 371 372 @Override 373 public int hashCode() { 374 return id.hashCode(); 375 } 376 377 @Override 378 public String toString() { 379 return getClass().getName() + " {id=" + id + ", subscriptions=" + subscriptions.values() + "}"; 380 } 381}