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 */
022 package org.granite.gravity.tomcat;
023
024 import java.io.ByteArrayInputStream;
025 import java.io.ByteArrayOutputStream;
026 import java.io.IOException;
027 import java.io.ObjectInput;
028 import java.io.ObjectOutput;
029 import java.nio.ByteBuffer;
030 import java.nio.CharBuffer;
031 import java.util.Arrays;
032 import java.util.HashMap;
033 import java.util.LinkedList;
034
035 import javax.servlet.http.HttpSession;
036
037 import org.apache.catalina.websocket.MessageInbound;
038 import org.apache.catalina.websocket.StreamInbound;
039 import org.apache.catalina.websocket.WsOutbound;
040 import org.granite.context.GraniteContext;
041 import org.granite.context.SimpleGraniteContext;
042 import org.granite.gravity.AbstractChannel;
043 import org.granite.gravity.AsyncHttpContext;
044 import org.granite.gravity.Gravity;
045 import org.granite.gravity.GravityConfig;
046 import org.granite.logging.Logger;
047 import org.granite.messaging.jmf.JMFDeserializer;
048 import org.granite.messaging.jmf.JMFSerializer;
049 import org.granite.messaging.webapp.ServletGraniteContext;
050 import org.granite.util.ContentType;
051
052 import flex.messaging.messages.AsyncMessage;
053 import flex.messaging.messages.Message;
054
055
056 public 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 }