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