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.servlet3;
023
024import java.io.IOException;
025import java.io.InputStream;
026import java.io.OutputStream;
027
028import javax.servlet.AsyncContext;
029import javax.servlet.ServletConfig;
030import javax.servlet.ServletException;
031import javax.servlet.http.HttpServletRequest;
032import javax.servlet.http.HttpServletResponse;
033
034import org.granite.config.GraniteConfigListener;
035import org.granite.gravity.AbstractGravityServlet;
036import org.granite.gravity.AsyncHttpContext;
037import org.granite.gravity.Gravity;
038import org.granite.gravity.GravityManager;
039import org.granite.logging.Logger;
040import org.granite.messaging.jmf.JMFDeserializer;
041import org.granite.messaging.jmf.JMFSerializer;
042import org.granite.util.ContentType;
043import org.granite.util.UUIDUtil;
044
045import flex.messaging.messages.Message;
046
047/**
048 * @author Franck WOLFF
049 */
050public class GravityAsyncServlet extends AbstractGravityServlet {
051
052        private static final long serialVersionUID = 1L;
053
054        private static final Logger log = Logger.getLogger(GravityAsyncServlet.class);
055
056        @Override
057        public void init(ServletConfig config) throws ServletException {
058                super.init(config);
059        }
060
061        @Override
062        protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
063                
064                if (!request.isAsyncSupported())
065                        throw new ServletException("Asynchronous requests are not supported with this servlet. Please check your web.xml");
066
067                if (request.isAsyncStarted())
068                        throw new ServletException("Gravity Servlet3 implementation doesn't support dispatch(...) mode");
069
070                Gravity gravity = GravityManager.getGravity(getServletContext());
071                AsyncChannelFactory channelFactory = newAsyncChannelFactory(gravity, request.getContentType());
072        
073                try {
074            initializeRequest(gravity, request, response);
075
076            Message[] amf3Requests = deserialize(gravity, request);
077
078            log.debug(">> [AMF3 REQUESTS] %s", (Object)amf3Requests);
079
080            Message[] amf3Responses = null;
081            
082            boolean accessed = false;
083            for (int i = 0; i < amf3Requests.length; i++) {
084                Message amf3Request = amf3Requests[i];
085
086                // Ask gravity to create a specific response (will be null for connect request from tunnel).
087                Message amf3Response = gravity.handleMessage(channelFactory, amf3Request);
088                String channelId = (String)amf3Request.getClientId();
089                
090                // Mark current channel (if any) as accessed.
091                if (!accessed)
092                        accessed = gravity.access(channelId);
093                
094                // (Re)Connect message from tunnel...
095                if (amf3Response == null) {
096                    if (amf3Requests.length > 1)
097                        throw new IllegalArgumentException("Only one connect request is allowed on tunnel.");
098
099                    AsyncChannel channel = gravity.getChannel(channelFactory, channelId);
100                    if (channel == null)
101                                throw new NullPointerException("No channel on tunnel connect");
102                    
103                    // Try to send pending messages if any (using current container thread).
104                    if (!channel.runReceived(new AsyncHttpContext(request, response, amf3Request))) {
105                            // No pending messages, wait for new ones or timeout.
106                            setConnectMessage(request, amf3Request);
107                                AsyncContext asyncContext = request.startAsync();
108                                asyncContext.setTimeout(getLongPollingTimeout());
109                                try {
110                                        asyncContext.addListener(new AsyncRequestListener(channel));
111                                        channel.setAsyncContext(asyncContext);
112                                }
113                                catch (Exception e) {
114                                        log.error(e, "Error while setting async context. Closing context...");
115                                        asyncContext.complete();
116                                }
117                    }
118                    return;
119                }
120
121                if (amf3Responses == null)
122                        amf3Responses = new Message[amf3Requests.length];
123                amf3Responses[i] = amf3Response;
124            }
125
126            log.debug("<< [AMF3 RESPONSES] %s", (Object)amf3Responses);
127
128            serialize(gravity, response, amf3Responses, request.getContentType());
129        }
130        catch (IOException e) {
131            log.error(e, "Gravity message error");
132            throw e;
133        }
134        catch (Exception e) {
135            log.error(e, "Gravity message error");
136            throw new ServletException(e);
137        }
138        finally {
139                cleanupRequest(request);
140        }
141        }
142
143
144        @Override
145        public void destroy() {
146                super.destroy();
147        }
148        
149        protected AsyncChannelFactory newAsyncChannelFactory(Gravity gravity, String contentType) throws ServletException {
150                if (ContentType.JMF_AMF.mimeType().equals(contentType)) {
151                return new JMFAsyncChannelFactory(gravity);
152                }
153                return new AsyncChannelFactory(gravity);
154        }
155
156        @Override
157        protected Message[] deserialize(Gravity gravity, HttpServletRequest request) throws ClassNotFoundException, IOException, ServletException {
158                if (ContentType.JMF_AMF.mimeType().equals(request.getContentType())) {
159                        InputStream is = request.getInputStream();
160                        try {
161                                return deserializeJMFAMF(gravity, request, is);
162                        }
163                        finally {
164                                is.close();
165                        }
166                }
167                return super.deserialize(gravity, request);
168        }
169
170        @Override
171        protected Message[] deserialize(Gravity gravity, HttpServletRequest request, InputStream is) throws ClassNotFoundException, IOException, ServletException {
172                if (ContentType.JMF_AMF.mimeType().equals(request.getContentType()))
173                        return deserializeJMFAMF(gravity, request, is);
174                return super.deserialize(gravity, request, is);
175        }
176        
177        protected Message[] deserializeJMFAMF(Gravity gravity, HttpServletRequest request, InputStream is) throws ClassNotFoundException, IOException, ServletException {
178        if (gravity.getSharedContext() == null)
179                throw GraniteConfigListener.newSharedContextNotInitializedException();
180        
181        @SuppressWarnings("all") // JDK7 warning (Resource leak: 'deserializer' is never closed)...
182                JMFDeserializer deserializer = new JMFDeserializer(is, gravity.getSharedContext());
183        return (Message[])deserializer.readObject();
184        }
185
186        protected void serialize(Gravity gravity, HttpServletResponse response, Message[] messages, String contentType) throws IOException, ServletException {
187                if (ContentType.JMF_AMF.mimeType().equals(contentType))
188                        serializeJMFAMF(gravity, response, messages);
189                else
190                        super.serialize(gravity, response, messages);
191        }
192
193        protected void serializeJMFAMF(Gravity gravity, HttpServletResponse response, Message[] messages) throws IOException, ServletException {
194        if (gravity.getSharedContext() == null)
195                throw GraniteConfigListener.newSharedContextNotInitializedException();
196        
197        OutputStream os = null;
198                try {
199            // For SDK 2.0.1_Hotfix2+ (LCDS 2.5+).
200                        String dsId = null;
201            for (Message message : messages) {
202                    if ("nil".equals(message.getHeader(Message.DS_ID_HEADER))) {
203                        if (dsId == null)
204                                dsId = UUIDUtil.randomUUID();
205                        message.getHeaders().put(Message.DS_ID_HEADER, dsId);
206                    }
207            }
208                        
209                response.setStatus(HttpServletResponse.SC_OK);
210                response.setContentType(ContentType.JMF_AMF.mimeType());
211                response.setDateHeader("Expire", 0L);
212                response.setHeader("Cache-Control", "no-store");
213                
214                os = response.getOutputStream();
215
216                @SuppressWarnings("all") // JDK7 warning (Resource leak: 'serializer' is never closed)...
217            JMFSerializer serializer = new JMFSerializer(os, gravity.getSharedContext());
218            serializer.writeObject(messages);
219                
220                os.flush();
221                response.flushBuffer();
222                }
223                finally {
224                        if (os != null)
225                                os.close();
226                }
227        }
228}