001/**
002 *   GRANITE DATA SERVICES
003 *   Copyright (C) 2006-2014 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.seam;
023
024import java.lang.reflect.Method;
025import java.util.Iterator;
026import java.util.Map;
027
028import javax.faces.FactoryFinder;
029import javax.faces.application.FacesMessage;
030import javax.faces.context.FacesContext;
031import javax.faces.context.FacesContextFactory;
032import javax.faces.lifecycle.Lifecycle;
033import javax.faces.lifecycle.LifecycleFactory;
034import javax.servlet.ServletException;
035import javax.servlet.http.HttpServletRequest;
036
037import org.granite.context.GraniteContext;
038import org.granite.logging.Logger;
039import org.granite.messaging.amf.process.AMF3MessageInterceptor;
040import org.granite.messaging.service.ServiceException;
041import org.granite.messaging.webapp.HttpGraniteContext;
042import org.granite.messaging.webapp.HttpServletRequestParamWrapper;
043import org.jboss.seam.contexts.Contexts;
044import org.jboss.seam.contexts.ServletLifecycle;
045import org.jboss.seam.core.Conversation;
046import org.jboss.seam.core.ConversationPropagation;
047import org.jboss.seam.core.Manager;
048import org.jboss.seam.faces.FacesMessages;
049import org.jboss.seam.servlet.ServletRequestSessionMap;
050import org.jboss.seam.util.Reflections;
051import org.jboss.seam.web.ServletContexts;
052
053import flex.messaging.messages.Message;
054
055
056public class SeamInterceptor implements AMF3MessageInterceptor {
057        
058        private static final Logger log = Logger.getLogger(SeamInterceptor.class);
059
060    private static final String CONVERSATION_ID = "conversationId";
061    private static final String PARENT_CONVERSATION_ID = "parentConversationId";
062    private static final String IS_LONG_RUNNING_CONVERSATION = "isLongRunningConversation";
063    private static final String WAS_LONG_RUNNING_CONVERSATION_CREATED = "wasLongRunningConversationCreated";
064    private static final String WAS_LONG_RUNNING_CONVERSATION_ENDED = "wasLongRunningConversationEnded";
065        private static final String MESSAGE_HEADER = "MESSAGE_HEADER";
066        private static final String MSG_SEP = ":;:";
067    
068        /* (non-Javadoc)
069         * @see org.granite.messaging.amf.process.AMF3MessageInterceptor#before(flex.messaging.messages.Message)
070         */
071        public void before(Message amfReqMessage) {
072                if (log.isTraceEnabled())
073                        log.trace("Pre processing of request message: %s", amfReqMessage);
074
075                try {
076                        GraniteContext context = GraniteContext.getCurrentInstance();
077                        
078                        if (context instanceof HttpGraniteContext) {
079                    log.debug("Creating custom HttpServletRequest wrapper");
080                    HttpServletRequestParamWrapper request = new HttpServletRequestParamWrapper(((HttpGraniteContext)context).getRequest());
081
082                    //Initialize the FacesContext for each body request
083                                initializeFacesContext(request, (HttpGraniteContext)context);
084                                
085                        //Now export the headers - copy the headers to request object
086                        exportHeaders(request, amfReqMessage);
087                                
088                        //Time to initialize Seam Context
089                        initializeSeamContext(request);
090                        }
091                }
092                catch(Exception e) {
093            log.error(e, "Exception while pre processing the request message.");
094            throw new ServiceException("Error while pre processing the request message - " + e.getMessage());
095                }
096        }
097
098        /* (non-Javadoc)
099         * @see org.granite.messaging.amf.process.AMF3MessageInterceptor#after(flex.messaging.messages.Message, flex.messaging.messages.Message)
100         */
101        public void after(Message amfReqMessage, Message amfRespMessage) {              
102                try {
103                        if (log.isTraceEnabled())
104                                log.trace("Post processing of response message: %s", amfReqMessage);
105
106                        if (GraniteContext.getCurrentInstance() instanceof HttpGraniteContext) {
107                                try {
108                                        //Now time to set back the headers, always has one body
109                                        importHeaders(amfRespMessage);
110                                }
111                                finally {
112                                        //Time to destroy the seam context
113                                        destroySeamContext();
114                                }
115                        }
116                }
117                catch (Exception e) {
118            log.error(e, "Exception while post processing the response message.");
119            throw new ServiceException("Error while post processing the response message - " + e.getMessage());
120                }
121                finally {
122            //Release the FacesContext - clears messages etc.,
123                        FacesContext context = FacesContext.getCurrentInstance();
124                        if (context != null)
125                                context.release();              
126                }
127        }
128
129        /**
130         * Reads the AMF request header and populate them in the request object
131         * @param request - HttpServletRequestParamWrapper
132         * @param amf3RequestMessage
133         */
134        private void exportHeaders(HttpServletRequestParamWrapper request, Message amf3RequestMessage) {
135                //Read the headers from first body
136                Map<String, Object> headerMap = amf3RequestMessage.getHeaders();
137                if (headerMap != null && headerMap.size() > 0) {
138                        Iterator<String> headerKeys = headerMap.keySet().iterator();
139                        while (headerKeys.hasNext()) {
140                                String key = headerKeys.next();
141                                String value = headerMap.get(key) == null ? null : headerMap.get(key).toString();
142                                if( value != null) {
143                                        request.setParameter(key, value);
144                                }
145                        }
146                }
147        }
148
149        /**
150     * Update the AMF response message with the conversationId and other parameters.
151     * @param amf3ResponseMessage
152     */
153        private void importHeaders(Message amf3ResponseMessage) {
154                if (amf3ResponseMessage != null) {
155                        Conversation conversation = Conversation.instance();
156            if (Contexts.getEventContext().isSet("org.granite.tide.conversation.wasLongRunning") && !conversation.isLongRunning())
157                amf3ResponseMessage.setHeader(WAS_LONG_RUNNING_CONVERSATION_ENDED, true);
158                        
159            if (Contexts.getEventContext().isSet("org.granite.tide.conversation.wasCreated") && conversation.isLongRunning())
160                amf3ResponseMessage.setHeader(WAS_LONG_RUNNING_CONVERSATION_CREATED, true);
161            
162                        log.debug("CONVERSATION_ID: %s", conversation.getId());
163                        amf3ResponseMessage.setHeader(CONVERSATION_ID, conversation.getId());
164                        
165                        log.debug("PARENT_CONVERSATION_ID: %s", conversation.getParentId());
166                        amf3ResponseMessage.setHeader(PARENT_CONVERSATION_ID, conversation.getParentId());
167                        
168                        log.debug("IS_LONG_RUNNING_CONVERSATION: %s", conversation.isLongRunning());
169                        amf3ResponseMessage.setHeader(IS_LONG_RUNNING_CONVERSATION, conversation.isLongRunning());
170                        
171                        log.debug("Processing the Faces messages.");
172                        processFacesMessages(amf3ResponseMessage);
173                }
174        }
175                
176        /**
177         * Process the faces messages and sets to the response header.
178         * @param amf3ResponseMessage
179         */
180        private void processFacesMessages(Message amf3ResponseMessage) {
181                if (amf3ResponseMessage != null) {
182                //Prepare for the messages. First step is convert the tasks to Seam FacesMessages
183                FacesMessages.afterPhase();
184        
185                //Second step is add the Seam FacesMessages to JSF FacesContext Messages
186                FacesMessages.instance().beforeRenderResponse();
187        
188                //TODO - Work on how effective we can pass the messages to Flex side
189                Iterator<FacesMessage> messageItr = FacesContext.getCurrentInstance().getMessages();
190                log.debug("Found Messages: %b", messageItr.hasNext());
191                StringBuilder messagesBuf = new StringBuilder();
192                while (messageItr.hasNext()) {
193                    FacesMessage msg = messageItr.next();
194                    log.debug("FacesMessages %s - %s", msg.getDetail(), msg.getSummary());
195                    messagesBuf.append(msg.getSummary());
196                    messagesBuf.append(MSG_SEP);
197                }
198        
199                String messageStr = messagesBuf.toString().trim();
200        
201                if (messageStr.length() > 0) {
202                    messageStr = messageStr.substring(0, messageStr.lastIndexOf(MSG_SEP));
203                    amf3ResponseMessage.setHeader(MESSAGE_HEADER, messageStr);
204                }
205                }
206        }
207
208        /**
209         * Initialize the Seam Context
210         * @param request - HttpServletRequest
211         */
212        private void initializeSeamContext(HttpServletRequest request) {
213                log.debug("beginning request");
214                
215                ServletLifecycle.beginRequest(request);
216                ServletContexts.instance().setRequest(request);
217        
218        Manager.instance().setConversationIdParameter(CONVERSATION_ID);
219                restoreConversationId();
220        String conversationId = ConversationPropagation.instance().getConversationId();
221        Manager.instance().restoreConversation();
222        ServletLifecycle.resumeConversation(request);
223        handleConversationPropagation();
224        if (conversationId != null && !conversationId.equals(Manager.instance().getCurrentConversationId()))
225            Manager.instance().updateCurrentConversationId(conversationId);
226        if (Manager.instance().isLongRunningConversation())
227                Contexts.getEventContext().set("org.granite.tide.conversation.wasLongRunning", true);
228                
229                // Force creation of the session
230                if (request.getSession(false) == null)
231                        request.getSession(true);
232                
233        if (Boolean.TRUE.toString().equals(request.getParameter("org.granite.tide.isFirstCall")))
234                Contexts.getSessionContext().set("org.granite.tide.isFirstCall", Boolean.TRUE);
235                
236        if (Boolean.TRUE.toString().equals(request.getParameter("org.granite.tide.isFirstConversationCall")) && Manager.instance().isLongRunningConversation())
237                Contexts.getConversationContext().set("org.granite.tide.isFirstConversationCall", Boolean.TRUE);
238        }
239        
240        /**
241         * Destroy the Seam Context
242         * @param request - HttpServletRequest
243         */
244        private void destroySeamContext() {
245        // Flush current conversation metadata if needed
246        if (Manager.instance().isLongRunningConversation()) {
247                Conversation conversation = Conversation.instance();
248                try {
249                        Method method = conversation.getClass().getDeclaredMethod("flush");
250                        method.setAccessible(true);
251                        Reflections.invoke(method, conversation);
252                }
253                catch (Exception e) {
254                        log.error("Could not flush current long-running conversation " + conversation.getId(), e);
255                }
256        }
257        
258                //Retrieve the stored request from Seam Servlet Context
259        Manager.instance().endRequest( new ServletRequestSessionMap(ServletContexts.getInstance().getRequest()) );
260        ServletLifecycle.endRequest(ServletContexts.getInstance().getRequest());
261        
262        log.debug("ended request");
263        }
264
265        /**
266         * Returns facesContext if already exists otherwise creates a new one.
267         * @param request - HttpServletRequest
268         * @param context - HttpGraniteContext
269         * @return FacesContext
270         * @throws ServletException
271         */
272    private FacesContext initializeFacesContext(HttpServletRequest request, HttpGraniteContext context) throws ServletException {
273        try {
274            FacesContext facesContext = FacesContext.getCurrentInstance();
275
276            if (facesContext != null)
277                return facesContext;
278            
279            //Use the FactoryFinder to find the Lifecycle object
280            FacesContextFactory contextFactory = (FacesContextFactory)
281            FactoryFinder.getFactory(FactoryFinder.FACES_CONTEXT_FACTORY);
282            LifecycleFactory lifecycleFactory = (LifecycleFactory)
283            FactoryFinder.getFactory(FactoryFinder.LIFECYCLE_FACTORY);
284            Lifecycle lifecycle = lifecycleFactory.getLifecycle(LifecycleFactory.DEFAULT_LIFECYCLE);
285
286            facesContext = contextFactory.getFacesContext(context.getServletContext(), request, context.getResponse(), lifecycle);
287            //Set as current FacesContext
288            InnerFacesContext.setFacesContextAsCurrentInstance(facesContext);
289
290            return facesContext;
291        }
292        catch (Exception e) {
293            throw new ServletException(e);
294        }
295    }
296    
297    /**
298     * 
299     */
300    protected void handleConversationPropagation() {
301       Manager.instance().handleConversationPropagation( ServletContexts.getInstance().getRequest().getParameterMap() );
302    }
303
304    /**
305     * 
306     */
307    protected void restoreConversationId() {
308       ConversationPropagation.instance().restoreConversationId( ServletContexts.getInstance().getRequest().getParameterMap() );
309    }
310    
311    /**
312     * Create a inner class to make a call to setCurrentInstance. 
313     */
314    private abstract static class InnerFacesContext extends FacesContext {
315        protected static void setFacesContextAsCurrentInstance(FacesContext facesContext) {
316            FacesContext.setCurrentInstance(facesContext);
317        }
318    }
319}