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.jetty8;
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.eclipse.jetty.websocket.WebSocket;
036 import org.eclipse.jetty.websocket.WebSocket.OnBinaryMessage;
037 import org.granite.context.GraniteContext;
038 import org.granite.context.SimpleGraniteContext;
039 import org.granite.gravity.AbstractChannel;
040 import org.granite.gravity.AsyncHttpContext;
041 import org.granite.gravity.Gravity;
042 import org.granite.gravity.GravityConfig;
043 import org.granite.logging.Logger;
044 import org.granite.messaging.jmf.JMFDeserializer;
045 import org.granite.messaging.jmf.JMFSerializer;
046 import org.granite.messaging.webapp.ServletGraniteContext;
047 import org.granite.util.ContentType;
048
049 import flex.messaging.messages.AsyncMessage;
050 import flex.messaging.messages.Message;
051
052
053 public 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 }