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