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.glassfish;
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.granite.context.GraniteContext;
036import org.granite.context.SimpleGraniteContext;
037import org.granite.gravity.AbstractChannel;
038import org.granite.gravity.AsyncHttpContext;
039import org.granite.gravity.Gravity;
040import org.granite.gravity.GravityConfig;
041import org.granite.logging.Logger;
042import org.granite.messaging.jmf.JMFDeserializer;
043import org.granite.messaging.jmf.JMFSerializer;
044import org.granite.messaging.webapp.ServletGraniteContext;
045import org.granite.util.ContentType;
046
047import com.sun.grizzly.websockets.DataFrame;
048import com.sun.grizzly.websockets.WebSocket;
049import com.sun.grizzly.websockets.WebSocketListener;
050
051import flex.messaging.messages.AsyncMessage;
052import flex.messaging.messages.Message;
053
054
055public 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}