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    }