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.glassfish;
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.util.Arrays;
030 import java.util.HashMap;
031 import java.util.LinkedList;
032
033 import javax.servlet.http.HttpSession;
034
035 import org.granite.context.GraniteContext;
036 import org.granite.context.SimpleGraniteContext;
037 import org.granite.gravity.AbstractChannel;
038 import org.granite.gravity.AsyncHttpContext;
039 import org.granite.gravity.Gravity;
040 import org.granite.gravity.GravityConfig;
041 import org.granite.logging.Logger;
042 import org.granite.messaging.jmf.JMFDeserializer;
043 import org.granite.messaging.jmf.JMFSerializer;
044 import org.granite.messaging.webapp.ServletGraniteContext;
045 import org.granite.util.ContentType;
046
047 import com.sun.grizzly.websockets.DataFrame;
048 import com.sun.grizzly.websockets.WebSocket;
049 import com.sun.grizzly.websockets.WebSocketListener;
050
051 import flex.messaging.messages.AsyncMessage;
052 import flex.messaging.messages.Message;
053
054
055 public 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 }