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     */
022    package org.granite.seam;
023    
024    import java.lang.reflect.Method;
025    import java.util.Iterator;
026    import java.util.Map;
027    
028    import javax.faces.FactoryFinder;
029    import javax.faces.application.FacesMessage;
030    import javax.faces.context.FacesContext;
031    import javax.faces.context.FacesContextFactory;
032    import javax.faces.lifecycle.Lifecycle;
033    import javax.faces.lifecycle.LifecycleFactory;
034    import javax.servlet.ServletException;
035    import javax.servlet.http.HttpServletRequest;
036    
037    import org.granite.context.GraniteContext;
038    import org.granite.logging.Logger;
039    import org.granite.messaging.amf.process.AMF3MessageInterceptor;
040    import org.granite.messaging.service.ServiceException;
041    import org.granite.messaging.webapp.HttpGraniteContext;
042    import org.granite.messaging.webapp.HttpServletRequestParamWrapper;
043    import org.jboss.seam.contexts.Contexts;
044    import org.jboss.seam.contexts.ServletLifecycle;
045    import org.jboss.seam.core.Conversation;
046    import org.jboss.seam.core.ConversationPropagation;
047    import org.jboss.seam.core.Manager;
048    import org.jboss.seam.faces.FacesMessages;
049    import org.jboss.seam.servlet.ServletRequestSessionMap;
050    import org.jboss.seam.util.Reflections;
051    import org.jboss.seam.web.ServletContexts;
052    
053    import flex.messaging.messages.Message;
054    
055    
056    public 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    }