View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.myfaces.orchestra.conversation;
21  
22  import java.io.IOException;
23  import java.io.ObjectStreamException;
24  import java.io.Serializable;
25  import java.util.Collections;
26  import java.util.HashMap;
27  import java.util.Iterator;
28  import java.util.Map;
29  
30  import org.apache.commons.logging.Log;
31  import org.apache.commons.logging.LogFactory;
32  import org.apache.myfaces.orchestra.frameworkAdapter.FrameworkAdapter;
33  import org.apache.myfaces.orchestra.requestParameterProvider.RequestParameterProviderManager;
34  
35  /**
36   * Deals with the various conversation contexts in the current session.
37   * <p>
38   * A new conversation context will be created if the servlet request did
39   * not specify an existing conversation context id.
40   * <p>
41   * At the current time, this object does not serialize well. Any attempt to serialize
42   * this object (including any serialization of the user session) will just cause it
43   * to be discarded.
44   * <p>
45   * TODO: fix serialization issues.
46   */
47  public class ConversationManager implements Serializable
48  {
49      private static final long serialVersionUID = 1L;
50  
51      final static String CONVERSATION_CONTEXT_PARAM = "conversationContext";
52  
53      private final static String CONVERSATION_MANAGER_KEY = "org.apache.myfaces.ConversationManager";
54      private final static String CONVERSATION_CONTEXT_REQ = "org.apache.myfaces.ConversationManager.conversationContext";
55  
56      private static final Iterator EMPTY_ITERATOR = Collections.EMPTY_LIST.iterator();
57  
58      private final Log log = LogFactory.getLog(ConversationManager.class);
59  
60      /**
61       * Used to generate a unique id for each "window" that a user has open
62       * on the same webapp within the same HttpSession. Note that this is a
63       * property of an object stored in the session, so will correctly
64       * migrate from machine to machine along with a distributed HttpSession.
65       * 
66       */
67      private long nextConversationContextId = 1;
68  
69      // This member must always be accessed with a lock held on the parent ConverstationManager instance;
70      // a HashMap is not thread-safe and this class must be thread-safe.
71      private final Map conversationContexts = new HashMap();
72      
73      protected ConversationManager()
74      {
75      }
76  
77      /**
78       * Get the conversation manager. This creates a new one if none exists.
79       */
80      public static ConversationManager getInstance()
81      {
82          return getInstance(true);
83      }
84  
85      /**
86       * Get the conversation manager.
87       * <p>
88       * When create is true, an instance is always returned; one is
89       * created if none currently exists for the current user session.
90       * <p>
91       * When create is false, null is returned if no instance yet
92       * exists for the current user session.
93       */
94      public static ConversationManager getInstance(boolean create)
95      {
96          FrameworkAdapter frameworkAdapter = FrameworkAdapter.getCurrentInstance();
97          if (frameworkAdapter == null)
98          {
99              if (!create)
100             {
101                 // if we should not created one, it doesn't matter if there is no
102                 // FrameworkAdapter available.
103                 return null;
104             }
105             else
106             {
107                 throw new IllegalStateException("FrameworkAdapter not found");
108             }
109         }
110 
111         ConversationManager conversationManager = (ConversationManager) frameworkAdapter.getSessionAttribute(CONVERSATION_MANAGER_KEY);
112         if (conversationManager == null && create)
113         {
114             // TODO: do not call new directly here, as it makes it impossible to configure
115             // an alternative ConversationManager instance. This is IOC and test unfriendly.
116             conversationManager = new ConversationManager();
117 
118             // initialize environmental systems
119             RequestParameterProviderManager.getInstance().register(new ConversationRequestParameterProvider());
120 
121             // set mark
122             FrameworkAdapter.getCurrentInstance().setSessionAttribute(CONVERSATION_MANAGER_KEY, conversationManager);
123         }
124 
125         return conversationManager;
126     }
127 
128     /**
129      * Get the current, or create a new unique conversationContextId.
130      * <p>
131      * The current conversationContextId will be retrieved from the request
132      * parameters. If no such parameter is present then a new id will be
133      * allocated.
134      * <p>
135      * In either case the result will be stored within the request for
136      * faster lookup.
137      * <p>
138      * Note that there is no security flaw regarding injection of fake
139      * context ids; the id must match one already in the session and there
140      * is no security problem with two windows in the same session exchanging
141      * ids.
142      * <p>
143      * This method <i>never</i> returns null.
144      */
145     public Long getConversationContextId()
146     {
147         FrameworkAdapter fa = FrameworkAdapter.getCurrentInstance();
148 
149         // first, check whether we have previously found this value and cached it as a
150         // request-scoped value.
151         Long conversationContextId = (Long)fa.getRequestAttribute(CONVERSATION_CONTEXT_REQ);
152 
153         if (conversationContextId == null)
154         {
155             if (fa.containsRequestParameterAttribute(CONVERSATION_CONTEXT_PARAM))
156             {
157                 String urlConversationContextId = fa.getRequestParameterAttribute(CONVERSATION_CONTEXT_PARAM).toString();
158                 conversationContextId = new Long(Long.parseLong(urlConversationContextId, Character.MAX_RADIX));
159             }
160             else
161             {
162                 synchronized(this)
163                 {
164                     conversationContextId = new Long(nextConversationContextId);
165                     nextConversationContextId++;
166                 }
167             }
168 
169             fa.setRequestAttribute(CONVERSATION_CONTEXT_REQ, conversationContextId);
170         }
171 
172         return conversationContextId;
173     }
174 
175     /**
176      * Get the conversation context for the given id
177      */
178     protected ConversationContext getConversationContext(Long conversationContextId)
179     {
180         synchronized (this)
181         {
182             return (ConversationContext) conversationContexts.get(conversationContextId);
183         }
184     }
185 
186     /**
187      * Get the conversation context for the given id. <br />
188      * If there is no conversation context a new one will be created
189      */
190     protected ConversationContext getOrCreateConversationContext(Long conversationContextId)
191     {
192         synchronized (this)
193         {
194             ConversationContext conversationContext = (ConversationContext) conversationContexts.get(conversationContextId);
195             if (conversationContext == null)
196             {
197                 conversationContext = new ConversationContext(conversationContextId.longValue());
198                 conversationContexts.put(conversationContextId, conversationContext);
199             }
200 
201             return conversationContext;
202         }
203     }
204 
205     /**
206      * Ends all conversations within the current context; the context itself will remain active.
207      */
208     public void clearCurrentConversationContext()
209     {
210         Long conversationContextId = getConversationContextId();
211         ConversationContext conversationContext = getConversationContext(conversationContextId);
212         if (conversationContext != null)
213         {
214             conversationContext.clear();
215         }
216     }
217 
218     /**
219      * <p>Destroy the given conversation context</p>
220      * <p/>
221      * <p>Notice: its assumed that the context is already been destroyed</p>
222      */
223     protected void removeConversationContext(Long conversationContextId)
224     {
225         synchronized (this)
226         {
227             conversationContexts.remove(conversationContextId);
228         }
229     }
230 
231     /**
232      * Start a conversation.
233      *
234      * @see ConversationContext#startConversation(String, ConversationFactory)
235      */
236     public Conversation startConversation(String name, ConversationFactory factory)
237     {
238         Long conversationContextId = getConversationContextId();
239         ConversationContext conversationContext = getOrCreateConversationContext(conversationContextId);
240         return conversationContext.startConversation(name, factory);
241     }
242 
243     /**
244      * Remove a conversation
245      *
246      * <p>Notice: Its assumed that the conversation has already been invalidated</p>
247      *
248      * @see ConversationContext#removeConversation(String)
249      */
250     protected void removeConversation(String name)
251     {
252         Long conversationContextId = getConversationContextId();
253         ConversationContext conversationContext = getConversationContext(conversationContextId);
254         if (conversationContext != null)
255         {
256             conversationContext.removeConversation(name);
257         }
258     }
259 
260     /**
261      * Get the conversation with the given name
262      *
263      * @return null if no conversation context is active or if the conversation did not exist.
264      */
265     public Conversation getConversation(String name)
266     {
267         ConversationContext conversationContext = getCurrentConversationContext();
268         if (conversationContext == null)
269         {
270             return null;
271         }
272         return conversationContext.getConversation(name);
273     }
274 
275     /**
276      * check if the given conversation is active
277      */
278     public boolean hasConversation(String name)
279     {
280         ConversationContext conversationContext = getCurrentConversationContext();
281         if (conversationContext == null)
282         {
283             return false;
284         }
285         return conversationContext.hasConversation(name);
286     }
287 
288     /**
289      * Returns an iterator over all the Conversation objects in the current conversation
290      * context. Never returns null, even if no conversation context exists.
291      */
292     public Iterator iterateConversations()
293     {
294         ConversationContext conversationContext = getCurrentConversationContext();
295         if (conversationContext == null)
296         {
297             return EMPTY_ITERATOR;
298         }
299 
300         return conversationContext.iterateConversations();
301     }
302 
303     /**
304      * Get the current conversation context.
305      *
306      * @return null if there is no context active
307      */
308     public ConversationContext getCurrentConversationContext()
309     {
310         Long conversationContextId = getConversationContextId();
311         ConversationContext conversationContext = getConversationContext(conversationContextId);
312         return conversationContext;
313     }
314 
315     /**
316      * check if we have a conversation context
317      */
318     public boolean hasConversationContext()
319     {
320         return
321             (
322                 FrameworkAdapter.getCurrentInstance().containsRequestAttribute(CONVERSATION_CONTEXT_REQ) ||
323                     FrameworkAdapter.getCurrentInstance().containsRequestParameterAttribute(CONVERSATION_CONTEXT_REQ)) &&
324                 getCurrentConversationContext() != null;
325     }
326 
327     /**
328      * Get the Messager used to inform the user about anomalies.
329      * <p>
330      * What instance is returned is controlled by the FrameworkAdapter. See
331      * {@link org.apache.myfaces.orchestra.frameworkAdapter.FrameworkAdapter} for details.
332      */
333     public ConversationMessager getMessager()
334     {
335         return FrameworkAdapter.getCurrentInstance().getConversationMessager();
336     }
337 
338     /**
339      * Check the timeout for each conversation context, and all conversations
340      * within those contexts.
341      * <p>
342      * If any conversation has not been accessed within its timeout period
343      * then clear the context.
344      * <p>
345      * Invoke the checkTimeout method on each context so that any conversation
346      * that has not been accessed within its timeout is invalidated.
347      */
348     protected void checkTimeouts()
349     {
350         Map.Entry[] contexts;
351         synchronized (this)
352         {
353             contexts = new Map.Entry[conversationContexts.size()];
354             conversationContexts.entrySet().toArray(contexts);
355         }
356 
357         long checkTime = System.currentTimeMillis();
358 
359         for (int i = 0; i<contexts.length; i++)
360         {
361             Map.Entry context = contexts[i];
362 
363             Long conversationContextId = (Long) context.getKey();
364             ConversationContext conversationContext = (ConversationContext) context.getValue();
365             conversationContext.checkConversationTimeout();
366 
367             if (conversationContext.getTimeout() > -1 &&
368                 (conversationContext.getLastAccess() +
369                 conversationContext.getTimeout()) < checkTime)
370             {
371                 if (log.isDebugEnabled())
372                 {
373                     log.debug("end conversation context due to timeout: " + conversationContext.getId());
374                 }
375 
376                 conversationContext.clear();
377                 synchronized (this)
378                 {
379                     conversationContexts.remove(conversationContextId);
380                 }
381             }
382         }
383     }
384 
385     /*
386      * check if a bean with the given name will be managed by the conversationManager.
387     public boolean isManagedBean(String beanName)
388     {
389         ConfigurableApplicationContext applicationContext = FrameworkAdapter.getInstance().getApplicationContext();
390         ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
391 
392         // XXX: Hack to get the real definition in case of scoped targets
393         BeanDefinition beanDefinition = beanFactory.getBeanDefinition("scopedTarget." + beanName);
394         if (beanDefinition == null)
395         {
396             beanDefinition = beanFactory.getBeanDefinition(beanName);
397         }
398 
399         return beanDefinition.getScope() != null && managedScopes.contains(beanDefinition.getScope());
400     }
401      */
402 
403     private void writeObject(java.io.ObjectOutputStream out) throws IOException
404     {
405         // the conversation manager is not (yet) serializable, we just implement it
406         // to make it work with distributed sessions
407     }
408 
409     private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException
410     {
411         // nothing written, so nothing to read
412     }
413 
414     private Object readResolve() throws ObjectStreamException
415     {
416         // do not return a real object, that way on first request a new conversation manager will be created
417         return null;
418     }
419 }