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