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.tomcat; 023 024import java.io.ByteArrayInputStream; 025import java.io.ByteArrayOutputStream; 026import java.io.IOException; 027import java.io.ObjectInput; 028import java.io.ObjectOutput; 029import java.nio.ByteBuffer; 030import java.nio.CharBuffer; 031import java.util.Arrays; 032import java.util.HashMap; 033import java.util.LinkedList; 034 035import javax.servlet.http.HttpSession; 036 037import org.apache.catalina.websocket.MessageInbound; 038import org.apache.catalina.websocket.StreamInbound; 039import org.apache.catalina.websocket.WsOutbound; 040import org.granite.context.GraniteContext; 041import org.granite.context.SimpleGraniteContext; 042import org.granite.gravity.AbstractChannel; 043import org.granite.gravity.AsyncHttpContext; 044import org.granite.gravity.Gravity; 045import org.granite.gravity.GravityConfig; 046import org.granite.logging.Logger; 047import org.granite.messaging.jmf.JMFDeserializer; 048import org.granite.messaging.jmf.JMFSerializer; 049import org.granite.messaging.webapp.ServletGraniteContext; 050import org.granite.util.ContentType; 051 052import flex.messaging.messages.AsyncMessage; 053import flex.messaging.messages.Message; 054 055 056public class TomcatWebSocketChannel extends AbstractChannel { 057 058 private static final Logger log = Logger.getLogger(TomcatWebSocketChannel.class); 059 060 private StreamInbound streamInbound = new MessageInboundImpl(); 061 private ContentType contentType; 062 private HttpSession session; 063 064 private WsOutbound connection; 065 private byte[] connectAckMessage; 066 067 068 public TomcatWebSocketChannel(Gravity gravity, String id, TomcatWebSocketChannelFactory factory, String clientType) { 069 super(gravity, id, factory, clientType); 070 } 071 072 public void setSession(HttpSession session) { 073 this.session = session; 074 } 075 076 public void setConnectAckMessage(Message ackMessage) { 077 try { 078 // Return an acknowledge message with the server-generated clientId 079 connectAckMessage = serialize(getGravity(), new Message[] { ackMessage }); 080 } 081 catch (IOException e) { 082 throw new RuntimeException("Could not send connect acknowledge", e); 083 } 084 } 085 086 public ContentType getContentType() { 087 return contentType; 088 } 089 090 public void setContentType(ContentType contentType) { 091 this.contentType = contentType; 092 } 093 094 public StreamInbound getStreamInbound() { 095 return streamInbound; 096 } 097 098 public class MessageInboundImpl extends MessageInbound { 099 100 public MessageInboundImpl() { 101 } 102 103 @Override 104 protected void onOpen(WsOutbound outbound) { 105 connection = outbound; 106 107 log.debug("WebSocket connection onOpen"); 108 109 if (connectAckMessage == null) 110 return; 111 112 try { 113 ByteBuffer buf = ByteBuffer.wrap(connectAckMessage); 114 connection.writeBinaryMessage(buf); 115 } 116 catch (IOException e) { 117 throw new RuntimeException("Could not send connect acknowledge", e); 118 } 119 120 connectAckMessage = null; 121 } 122 123 @Override 124 public void onClose(int closeCode) { 125 log.debug("WebSocket connection onClose %d", closeCode); 126 127 connection = null; 128 } 129 130 @Override 131 public void onBinaryMessage(ByteBuffer buf) { 132 byte[] data = buf.array(); 133 134 log.debug("WebSocket connection onBinaryMessage %d", data.length); 135 136 try { 137 initializeRequest(); 138 139 Message[] messages = deserialize(getGravity(), data); 140 141 log.debug(">> [AMF3 REQUESTS] %s", (Object)messages); 142 143 Message[] responses = null; 144 145 boolean accessed = false; 146 int responseIndex = 0; 147 for (int i = 0; i < messages.length; i++) { 148 Message message = messages[i]; 149 150 // Ask gravity to create a specific response (will be null with a connect request from tunnel). 151 Message response = getGravity().handleMessage(getFactory(), message); 152 String channelId = (String)message.getClientId(); 153 154 // Mark current channel (if any) as accessed. 155 if (!accessed) 156 accessed = getGravity().access(channelId); 157 158 if (response != null) { 159 if (responses == null) 160 responses = new Message[1]; 161 else 162 responses = Arrays.copyOf(responses, responses.length+1); 163 responses[responseIndex++] = response; 164 } 165 } 166 167 if (responses != null && responses.length > 0) { 168 log.debug("<< [AMF3 RESPONSES] %s", (Object)responses); 169 170 byte[] resultData = serialize(getGravity(), responses); 171 172 connection.writeBinaryMessage(ByteBuffer.wrap(resultData)); 173 } 174 } 175 catch (ClassNotFoundException e) { 176 log.error(e, "Could not handle incoming message data"); 177 } 178 catch (IOException e) { 179 log.error(e, "Could not handle incoming message data"); 180 } 181 finally { 182 cleanupRequest(); 183 } 184 } 185 186 @Override 187 protected void onTextMessage(CharBuffer buf) throws IOException { 188 } 189 190 public int getAckLength() { 191 return connectAckMessage != null ? connectAckMessage.length : 0; 192 } 193 } 194 195 private Gravity initializeRequest() { 196 if (session != null) 197 ServletGraniteContext.createThreadInstance(gravity.getGraniteConfig(), gravity.getServicesConfig(), session.getServletContext(), session, clientType); 198 else 199 SimpleGraniteContext.createThreadInstance(gravity.getGraniteConfig(), gravity.getServicesConfig(), sessionId, new HashMap<String, Object>(), clientType); 200 return gravity; 201 } 202 203 private Message[] deserialize(Gravity gravity, byte[] data) throws ClassNotFoundException, IOException { 204 ByteArrayInputStream is = new ByteArrayInputStream(data); 205 206 try { 207 Message[] messages = null; 208 209 if (ContentType.JMF_AMF.equals(contentType)) { 210 @SuppressWarnings("all") // JDK7 warning (Resource leak: 'deserializer' is never closed)... 211 JMFDeserializer deserializer = new JMFDeserializer(is, gravity.getSharedContext()); 212 messages = (Message[])deserializer.readObject(); 213 } 214 else { 215 ObjectInput amf3Deserializer = gravity.getGraniteConfig().newAMF3Deserializer(is); 216 Object[] objects = (Object[])amf3Deserializer.readObject(); 217 messages = new Message[objects.length]; 218 System.arraycopy(objects, 0, messages, 0, objects.length); 219 } 220 221 return messages; 222 } 223 finally { 224 is.close(); 225 } 226 } 227 228 private byte[] serialize(Gravity gravity, Message[] messages) throws IOException { 229 ByteArrayOutputStream os = null; 230 try { 231 os = new ByteArrayOutputStream(200*messages.length); 232 233 if (ContentType.JMF_AMF.equals(contentType)) { 234 @SuppressWarnings("all") // JDK7 warning (Resource leak: 'serializer' is never closed)... 235 JMFSerializer serializer = new JMFSerializer(os, gravity.getSharedContext()); 236 serializer.writeObject(messages); 237 } 238 else { 239 ObjectOutput amf3Serializer = gravity.getGraniteConfig().newAMF3Serializer(os); 240 amf3Serializer.writeObject(messages); 241 os.flush(); 242 } 243 244 return os.toByteArray(); 245 } 246 finally { 247 if (os != null) 248 os.close(); 249 } 250 } 251 252 private static void cleanupRequest() { 253 GraniteContext.release(); 254 } 255 256 @Override 257 public boolean runReceived(AsyncHttpContext asyncHttpContext) { 258 259 LinkedList<AsyncMessage> messages = null; 260 ByteArrayOutputStream os = null; 261 262 try { 263 receivedQueueLock.lock(); 264 try { 265 // Do we have any pending messages? 266 if (receivedQueue.isEmpty()) 267 return false; 268 269 // Both conditions are ok, get all pending messages. 270 messages = receivedQueue; 271 receivedQueue = new LinkedList<AsyncMessage>(); 272 } 273 finally { 274 receivedQueueLock.unlock(); 275 } 276 277 if (connection == null) 278 return false; 279 280 AsyncMessage[] messagesArray = new AsyncMessage[messages.size()]; 281 int i = 0; 282 for (AsyncMessage message : messages) 283 messagesArray[i++] = message; 284 285 // Setup serialization context (thread local) 286 Gravity gravity = getGravity(); 287 initializeRequest(); 288 289 log.debug("<< [MESSAGES for channel=%s] %s", this, messagesArray); 290 291 byte[] msg = serialize(gravity, messagesArray); 292 if (msg.length > 16000) { 293 // Split in ~2000 bytes chunks 294 int count = msg.length / 2000; 295 int chunkSize = Math.max(1, messagesArray.length / count); 296 int index = 0; 297 while (index < messagesArray.length) { 298 AsyncMessage[] chunk = Arrays.copyOfRange(messagesArray, index, Math.min(messagesArray.length, index+chunkSize)); 299 msg = serialize(gravity, chunk); 300 log.debug("Send binary message: %d msgs (%d bytes)", chunk.length, msg.length); 301 connection.writeBinaryMessage(ByteBuffer.wrap(msg)); 302 index += chunkSize; 303 } 304 } 305 else { 306 connection.writeBinaryMessage(ByteBuffer.wrap(msg)); 307 log.debug("Send binary message: %d msgs (%d bytes)", messagesArray.length, msg.length); 308 } 309 310 311 return true; // Messages were delivered 312 } 313 catch (IOException e) { 314 log.warn(e, "Could not send messages to channel: %s (retrying later)", this); 315 316 GravityConfig gravityConfig = getGravity().getGravityConfig(); 317 if (gravityConfig.isRetryOnError()) { 318 receivedQueueLock.lock(); 319 try { 320 if (receivedQueue.size() + messages.size() > gravityConfig.getMaxMessagesQueuedPerChannel()) { 321 log.warn( 322 "Channel %s has reached its maximum queue capacity %s (throwing %s messages)", 323 this, 324 gravityConfig.getMaxMessagesQueuedPerChannel(), 325 messages.size() 326 ); 327 } 328 else 329 receivedQueue.addAll(0, messages); 330 } 331 finally { 332 receivedQueueLock.unlock(); 333 } 334 } 335 336 return true; // Messages weren't delivered, but http context isn't valid anymore. 337 } 338 finally { 339 if (os != null) { 340 try { 341 os.close(); 342 } 343 catch (Exception e) { 344 // Could not close bytearray ??? 345 } 346 } 347 348 // Cleanup serialization context (thread local) 349 try { 350 GraniteContext.release(); 351 } 352 catch (Exception e) { 353 // should never happen... 354 } 355 } 356 } 357 358 @Override 359 public void destroy() { 360 try { 361 super.destroy(); 362 } 363 finally { 364 close(); 365 } 366 } 367 368 public void close() { 369 if (connection != null) { 370 try { 371 connection.close(1000, ByteBuffer.wrap("Channel closed".getBytes())); 372 } 373 catch (IOException e) { 374 log.error("Could not close WebSocket connection", e); 375 } 376 connection = null; 377 } 378 } 379 380 @Override 381 protected boolean hasAsyncHttpContext() { 382 return true; 383 } 384 385 @Override 386 protected void releaseAsyncHttpContext(AsyncHttpContext context) { 387 } 388 389 @Override 390 protected AsyncHttpContext acquireAsyncHttpContext() { 391 return null; 392 } 393}