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    }