001package org.granite.gravity.jetty8;
002
003import java.io.ByteArrayInputStream;
004import java.io.ByteArrayOutputStream;
005import java.io.IOException;
006import java.io.ObjectInput;
007import java.io.ObjectOutput;
008import java.util.Arrays;
009import java.util.LinkedList;
010
011import javax.servlet.ServletContext;
012
013import org.eclipse.jetty.websocket.WebSocket;
014import org.eclipse.jetty.websocket.WebSocket.OnBinaryMessage;
015import org.granite.context.GraniteContext;
016import org.granite.gravity.AbstractChannel;
017import org.granite.gravity.AsyncHttpContext;
018import org.granite.gravity.Gravity;
019import org.granite.gravity.GravityConfig;
020import org.granite.logging.Logger;
021import org.granite.messaging.webapp.ServletGraniteContext;
022
023import flex.messaging.messages.AsyncMessage;
024import flex.messaging.messages.Message;
025
026
027public class JettyWebSocketChannel extends AbstractChannel implements WebSocket, OnBinaryMessage {
028        
029        private static final Logger log = Logger.getLogger(JettyWebSocketChannel.class);
030        
031        private ServletContext servletContext;
032        private Connection connection;
033        private Message connectAckMessage;
034
035        
036        public JettyWebSocketChannel(Gravity gravity, String id, JettyWebSocketChannelFactory factory, ServletContext servletContext, String clientType) {
037        super(gravity, id, factory, clientType);
038        this.servletContext = servletContext;
039    }
040        
041        public void setConnectAckMessage(Message ackMessage) {
042                this.connectAckMessage = ackMessage;
043        }
044
045        public void onOpen(Connection connection) {
046                this.connection = connection;
047                this.connection.setMaxIdleTime((int)getGravity().getGravityConfig().getChannelIdleTimeoutMillis());
048                
049                log.debug("WebSocket connection onOpen");
050                
051                if (connectAckMessage == null)
052                        return;
053                
054                try {
055                        initializeRequest();
056                        
057                        // Return an acknowledge message with the server-generated clientId
058                byte[] resultData = serialize(getGravity(), new Message[] { connectAckMessage });
059                
060                        connection.sendMessage(resultData, 0, resultData.length);
061                        
062                        connectAckMessage = null;
063                }
064                catch (IOException e) {
065                        log.error(e, "Could not send connect acknowledge");
066                }
067                finally {
068                        cleanupRequest();
069                }
070        }
071
072        public void onClose(int closeCode, String message) {
073                log.debug("WebSocket connection onClose %d, %s", closeCode, message);
074        }
075
076        public void onMessage(byte[] data, int offset, int length) {
077                log.debug("WebSocket connection onMessage %d", data.length);
078                
079                try {
080                        initializeRequest();
081                        
082                        Message[] messages = deserialize(getGravity(), data, offset, length);
083
084            log.debug(">> [AMF3 REQUESTS] %s", (Object)messages);
085
086            Message[] responses = null;
087            
088            boolean accessed = false;
089            int responseIndex = 0;
090            for (int i = 0; i < messages.length; i++) {
091                Message message = messages[i];
092                
093                // Ask gravity to create a specific response (will be null with a connect request from tunnel).
094                Message response = getGravity().handleMessage(getFactory(), message);
095                String channelId = (String)message.getClientId();
096                
097                // Mark current channel (if any) as accessed.
098                if (!accessed)
099                        accessed = getGravity().access(channelId);
100                
101                if (response != null) {
102                        if (responses == null)
103                                responses = new Message[1];
104                        else
105                                responses = Arrays.copyOf(responses, responses.length+1);
106                        responses[responseIndex++] = response;
107                }
108            }
109            
110            if (responses != null && responses.length > 0) {
111                    log.debug("<< [AMF3 RESPONSES] %s", (Object)responses);
112        
113                    byte[] resultData = serialize(getGravity(), responses);
114                    
115                    connection.sendMessage(resultData, 0, resultData.length);
116            }
117                }
118                catch (ClassNotFoundException e) {
119                        log.error(e, "Could not handle incoming message data");
120                }
121                catch (IOException e) {
122                        log.error(e, "Could not handle incoming message data");
123                }
124                finally {
125                        cleanupRequest();
126                }
127        }
128        
129        private Gravity initializeRequest() {
130                ServletGraniteContext.createThreadInstance(gravity.getGraniteConfig(), gravity.getServicesConfig(), servletContext, sessionId, clientType);
131                return gravity;
132        }
133
134        private static Message[] deserialize(Gravity gravity, byte[] data, int offset, int length) throws ClassNotFoundException, IOException {
135                ByteArrayInputStream is = new ByteArrayInputStream(data, offset, length);
136                try {
137                        ObjectInput amf3Deserializer = gravity.getGraniteConfig().newAMF3Deserializer(is);
138                Object[] objects = (Object[])amf3Deserializer.readObject();
139                Message[] messages = new Message[objects.length];
140                System.arraycopy(objects, 0, messages, 0, objects.length);
141                
142                return messages;
143                }
144                finally {
145                        is.close();
146                }
147        }
148        
149        private static byte[] serialize(Gravity gravity, Message[] messages) throws IOException {
150                ByteArrayOutputStream os = null;
151                try {
152                os = new ByteArrayOutputStream(200*messages.length);
153                ObjectOutput amf3Serializer = gravity.getGraniteConfig().newAMF3Serializer(os);
154                amf3Serializer.writeObject(messages);           
155                os.flush();
156                return os.toByteArray();
157                }
158                finally {
159                        if (os != null)
160                                os.close();
161                }               
162        }
163        
164        private static void cleanupRequest() {
165                GraniteContext.release();
166        }
167        
168        @Override
169        public boolean runReceived(AsyncHttpContext asyncHttpContext) {
170                
171                LinkedList<AsyncMessage> messages = null;
172                ByteArrayOutputStream os = null;
173
174                try {
175                        receivedQueueLock.lock();
176                        try {
177                                // Do we have any pending messages? 
178                                if (receivedQueue.isEmpty())
179                                        return false;
180                                
181                                // Both conditions are ok, get all pending messages.
182                                messages = receivedQueue;
183                                receivedQueue = new LinkedList<AsyncMessage>();
184                        }
185                        finally {
186                                receivedQueueLock.unlock();
187                        }
188                        
189                        if (connection == null || !connection.isOpen())
190                                return false;
191                        
192                        AsyncMessage[] messagesArray = new AsyncMessage[messages.size()];
193                        int i = 0;
194                        for (AsyncMessage message : messages)
195                                messagesArray[i++] = message;
196                        
197                        // Setup serialization context (thread local)
198                        Gravity gravity = getGravity();
199                GraniteContext context = ServletGraniteContext.createThreadInstance(gravity.getGraniteConfig(), gravity.getServicesConfig(), servletContext, sessionId, clientType);
200                
201                os = new ByteArrayOutputStream(500);
202                ObjectOutput amf3Serializer = context.getGraniteConfig().newAMF3Serializer(os);
203                
204                log.debug("<< [MESSAGES for channel=%s] %s", this, messagesArray);
205                
206                amf3Serializer.writeObject(messagesArray);
207                
208                connection.sendMessage(os.toByteArray(), 0, os.size());
209                
210                return true; // Messages were delivered
211                }
212                catch (IOException e) {
213                        log.warn(e, "Could not send messages to channel: %s (retrying later)", this);
214                        
215                        GravityConfig gravityConfig = getGravity().getGravityConfig();
216                        if (gravityConfig.isRetryOnError()) {
217                                receivedQueueLock.lock();
218                                try {
219                                        if (receivedQueue.size() + messages.size() > gravityConfig.getMaxMessagesQueuedPerChannel()) {
220                                                log.warn(
221                                                        "Channel %s has reached its maximum queue capacity %s (throwing %s messages)",
222                                                        this,
223                                                        gravityConfig.getMaxMessagesQueuedPerChannel(),
224                                                        messages.size()
225                                                );
226                                        }
227                                        else
228                                                receivedQueue.addAll(0, messages);
229                                }
230                                finally {
231                                        receivedQueueLock.unlock();
232                                }
233                        }
234                        
235                        return true; // Messages weren't delivered, but http context isn't valid anymore.
236                }
237                finally {
238                        if (os != null) {
239                                try {
240                                        os.close();
241                                }
242                                catch (Exception e) {
243                                        // Could not close bytearray ???
244                                }
245                        }
246                        
247                        // Cleanup serialization context (thread local)
248                        try {
249                                GraniteContext.release();
250                        }
251                        catch (Exception e) {
252                                // should never happen...
253                        }
254                }
255        }
256
257        @Override
258        public void destroy() {
259                try {
260                        super.destroy();
261                }
262                finally {
263                        close();
264                }
265        }
266        
267        public void close() {
268                if (connection != null) {
269                        connection.close(1000, "Channel closed");
270                        connection = null;
271                }
272        }
273        
274        @Override
275        protected boolean hasAsyncHttpContext() {
276                return true;
277        }
278
279        @Override
280        protected void releaseAsyncHttpContext(AsyncHttpContext context) {
281        }
282
283        @Override
284        protected AsyncHttpContext acquireAsyncHttpContext() {
285        return null;
286    }           
287}