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}