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.glassfish;
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.granite.context.GraniteContext;
036    import org.granite.context.SimpleGraniteContext;
037    import org.granite.gravity.AbstractChannel;
038    import org.granite.gravity.AsyncHttpContext;
039    import org.granite.gravity.Gravity;
040    import org.granite.gravity.GravityConfig;
041    import org.granite.logging.Logger;
042    import org.granite.messaging.jmf.JMFDeserializer;
043    import org.granite.messaging.jmf.JMFSerializer;
044    import org.granite.messaging.webapp.ServletGraniteContext;
045    import org.granite.util.ContentType;
046    
047    import com.sun.grizzly.websockets.DataFrame;
048    import com.sun.grizzly.websockets.WebSocket;
049    import com.sun.grizzly.websockets.WebSocketListener;
050    
051    import flex.messaging.messages.AsyncMessage;
052    import flex.messaging.messages.Message;
053    
054    
055    public class GlassFishWebSocketChannel extends AbstractChannel implements WebSocketListener {
056            
057            private static final Logger log = Logger.getLogger(GlassFishWebSocketChannel.class);
058            
059            private WebSocket websocket;
060        private HttpSession session;
061            private Message connectAckMessage;
062            private ContentType contentType;
063            
064            public GlassFishWebSocketChannel(Gravity gravity, String id, GlassFishWebSocketChannelFactory factory, String clientType) {
065            super(gravity, id, factory, clientType);
066        }
067    
068        public void setSession(HttpSession session) {
069            this.session = session;
070        }
071    
072            public void setConnectAckMessage(Message ackMessage) {
073                    this.connectAckMessage = ackMessage;
074            }
075            
076            public ContentType getContentType() {
077                    return contentType;
078            }
079    
080            public void setContentType(ContentType contentType) {
081                    this.contentType = contentType;
082            }
083            
084            public void setWebSocket(WebSocket websocket) {
085                    this.websocket = websocket;
086                    this.websocket.add(this);
087                    
088                    if (connectAckMessage == null)
089                            return;
090                    
091                    try {
092                            // Return an acknowledge message with the server-generated clientId
093                    byte[] resultData = serialize(getGravity(), new Message[] { connectAckMessage });
094                            websocket.send(resultData);
095                    }
096                    catch (IOException e) {
097                            throw new RuntimeException("Could not send connect acknowledge", e);
098                    }
099                    
100                    connectAckMessage = null;
101            }
102            
103            public void onConnect(WebSocket websocket) {
104            }
105    
106            public void onClose(WebSocket websocket, DataFrame frame) {
107            }
108            
109            public void onMessage(WebSocket websocket, byte[] data) {
110                    try {
111                            initializeRequest();
112                            
113                            Message[] messages = deserialize(getGravity(), data);
114    
115                log.debug(">> [AMF3 REQUESTS] %s", (Object)messages);
116    
117                Message[] responses = null;
118                
119                boolean accessed = false;
120                for (int i = 0; i < messages.length; i++) {
121                    Message message = messages[i];
122                    
123                    // Ask gravity to create a specific response (will be null with a connect request from tunnel).
124                    Message response = getGravity().handleMessage(getFactory(), message);
125                    String channelId = (String)message.getClientId();
126                    
127                    // Mark current channel (if any) as accessed.
128                    if (!accessed)
129                            accessed = getGravity().access(channelId);                
130    
131                    if (responses == null)
132                            responses = new Message[messages.length];
133                    responses[i] = response;
134                }
135    
136                log.debug("<< [AMF3 RESPONSES] %s", (Object)responses);
137    
138                byte[] resultData = serialize(getGravity(), responses);
139                
140                websocket.send(resultData);
141                    }
142                    catch (ClassNotFoundException e) {
143                            log.error(e, "Could not handle incoming message data");
144                    }
145                    catch (IOException e) {
146                            log.error(e, "Could not handle incoming message data");
147                    }
148                    finally {
149                            cleanupRequest();
150                    }
151            }
152            
153            private Gravity initializeRequest() {
154            if (session != null)
155                ServletGraniteContext.createThreadInstance(gravity.getGraniteConfig(), gravity.getServicesConfig(), session.getServletContext(), session, clientType);
156            else
157                SimpleGraniteContext.createThreadInstance(gravity.getGraniteConfig(), gravity.getServicesConfig(), sessionId, new HashMap<String, Object>(), clientType);
158            return gravity;
159            }
160    
161            private Message[] deserialize(Gravity gravity, byte[] data) throws ClassNotFoundException, IOException {
162                    ByteArrayInputStream is = new ByteArrayInputStream(data);
163                    
164                    try {
165                            Message[] messages = null;
166                            
167                            if (ContentType.JMF_AMF.equals(contentType)) {
168                            @SuppressWarnings("all") // JDK7 warning (Resource leak: 'deserializer' is never closed)...
169                                    JMFDeserializer deserializer = new JMFDeserializer(is, gravity.getSharedContext());
170                                    messages = (Message[])deserializer.readObject();
171                            }
172                            else {
173                                    ObjectInput amf3Deserializer = gravity.getGraniteConfig().newAMF3Deserializer(is);
174                            Object[] objects = (Object[])amf3Deserializer.readObject();
175                            messages = new Message[objects.length];
176                            System.arraycopy(objects, 0, messages, 0, objects.length);
177                            }
178                    
179                    return messages;
180                    }
181                    finally {
182                            is.close();
183                    }
184            }
185            
186            private byte[] serialize(Gravity gravity, Message[] messages) throws IOException {
187                    ByteArrayOutputStream os = null;
188                    try {
189                    os = new ByteArrayOutputStream(200*messages.length);
190                    
191                    if (ContentType.JMF_AMF.equals(contentType)) {
192                            @SuppressWarnings("all") // JDK7 warning (Resource leak: 'serializer' is never closed)...
193                        JMFSerializer serializer = new JMFSerializer(os, gravity.getSharedContext());
194                        serializer.writeObject(messages);
195                    }
196                    else {
197                            ObjectOutput amf3Serializer = gravity.getGraniteConfig().newAMF3Serializer(os);
198                            amf3Serializer.writeObject(messages);           
199                            os.flush();
200                    }
201    
202                    return os.toByteArray();
203                    }
204                    finally {
205                            if (os != null)
206                                    os.close();
207                    }               
208            }
209            
210            private static void cleanupRequest() {
211                    GraniteContext.release();
212            }
213            
214            @Override
215            public boolean runReceived(AsyncHttpContext asyncHttpContext) {
216                    
217                    LinkedList<AsyncMessage> messages = null;
218                    ByteArrayOutputStream os = null;
219    
220                    try {
221                            receivedQueueLock.lock();
222                            try {
223                                    // Do we have any pending messages? 
224                                    if (receivedQueue.isEmpty())
225                                            return false;
226                                    
227                                    // Both conditions are ok, get all pending messages.
228                                    messages = receivedQueue;
229                                    receivedQueue = new LinkedList<AsyncMessage>();
230                            }
231                            finally {
232                                    receivedQueueLock.unlock();
233                            }
234                            
235                    if (websocket == null || !websocket.isConnected())
236                            return false;
237                    
238                            AsyncMessage[] messagesArray = new AsyncMessage[messages.size()];
239                            int i = 0;
240                            for (AsyncMessage message : messages)
241                                    messagesArray[i++] = message;
242                            
243                            // Setup serialization context (thread local)
244                            Gravity gravity = getGravity();
245                    SimpleGraniteContext.createThreadInstance(
246                        gravity.getGraniteConfig(), gravity.getServicesConfig(), sessionId, new HashMap<String, Object>(), clientType
247                    );
248                    
249                log.debug("<< [MESSAGES for channel=%s] %s", this, messagesArray);
250    
251                byte[] msg = serialize(gravity, messagesArray);
252                if (msg.length > 16000) {
253                    // Split in ~2000 bytes chunks
254                    int count = msg.length / 2000;
255                    int chunkSize = Math.max(1, messagesArray.length / count);
256                    int index = 0;
257                    while (index < messagesArray.length) {
258                        AsyncMessage[] chunk = Arrays.copyOfRange(messagesArray, index, Math.min(messagesArray.length, index + chunkSize));
259                        msg = serialize(gravity, chunk);
260                        log.debug("Send binary message: %d msgs (%d bytes)", chunk.length, msg.length);
261                        websocket.send(msg);
262                        index += chunkSize;
263                    }
264                }
265                else {
266                    websocket.send(msg);
267                    log.debug("Send binary message: %d msgs (%d bytes)", messagesArray.length, msg.length);
268                }
269    
270                    return true; // Messages were delivered, http context isn't valid anymore.
271                    }
272                    catch (IOException e) {
273                            log.warn(e, "Could not send messages to channel: %s (retrying later)", this);
274                            
275                            GravityConfig gravityConfig = getGravity().getGravityConfig();
276                            if (gravityConfig.isRetryOnError()) {
277                                    receivedQueueLock.lock();
278                                    try {
279                                            if (receivedQueue.size() + messages.size() > gravityConfig.getMaxMessagesQueuedPerChannel()) {
280                                                    log.warn(
281                                                            "Channel %s has reached its maximum queue capacity %s (throwing %s messages)",
282                                                            this,
283                                                            gravityConfig.getMaxMessagesQueuedPerChannel(),
284                                                            messages.size()
285                                                    );
286                                            }
287                                            else
288                                                    receivedQueue.addAll(0, messages);
289                                    }
290                                    finally {
291                                            receivedQueueLock.unlock();
292                                    }
293                            }
294                            
295                            return true; // Messages weren't delivered, but http context isn't valid anymore.
296                    }
297                    finally {
298                            if (os != null) {
299                                    try {
300                                            os.close();
301                                    }
302                                    catch (Exception e) {
303                                            // Could not close bytearray ???
304                                    }
305                            }
306                            
307                            // Cleanup serialization context (thread local)
308                            try {
309                                    GraniteContext.release();
310                            }
311                            catch (Exception e) {
312                                    // should never happen...
313                            }
314                    }
315            }
316    
317            @Override
318            public void destroy() {
319                    try {
320                            super.destroy();
321                    }
322                    finally {
323                            close();
324                    }
325            }
326            
327            public void close() {
328                    if (websocket != null) {
329                            websocket.close(1000, "Channel closed");
330                            websocket = null;
331                    }
332            }
333            
334            @Override
335            protected boolean hasAsyncHttpContext() {
336                    return true;
337            }
338    
339            @Override
340            protected void releaseAsyncHttpContext(AsyncHttpContext context) {
341            }
342    
343            @Override
344            protected AsyncHttpContext acquireAsyncHttpContext() {
345            return null;
346        }
347    
348            public void onFragment(WebSocket arg0, String arg1, boolean arg2) {
349            }
350    
351            public void onFragment(WebSocket arg0, byte[] arg1, boolean arg2) {
352            }
353    
354            public void onMessage(WebSocket arg0, String arg1) {
355            }
356    
357            public void onPing(WebSocket arg0, byte[] arg1) {
358            }
359    
360            public void onPong(WebSocket arg0, byte[] arg1) {
361            }
362    
363    }