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