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