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  package org.apache.myfaces.shared.context.flash;
20  
21  import org.apache.myfaces.shared.util.SubKeyMap;
22  import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFWebConfigParam;
23  import org.apache.myfaces.shared.util.ExternalContextUtils;
24  
25  import javax.faces.application.FacesMessage;
26  import javax.faces.context.ExternalContext;
27  import javax.faces.context.FacesContext;
28  import javax.faces.context.Flash;
29  import javax.faces.event.PhaseId;
30  import javax.servlet.http.Cookie;
31  import javax.servlet.http.HttpServletResponse;
32  import java.io.Serializable;
33  import java.math.BigInteger;
34  import java.security.NoSuchAlgorithmException;
35  import java.security.SecureRandom;
36  import java.util.ArrayList;
37  import java.util.Collection;
38  import java.util.Iterator;
39  import java.util.List;
40  import java.util.Map;
41  import java.util.Set;
42  import java.util.concurrent.atomic.AtomicLong;
43  import java.util.logging.Logger;
44  import javax.faces.event.PostKeepFlashValueEvent;
45  import javax.faces.event.PostPutFlashValueEvent;
46  import javax.faces.event.PreClearFlashEvent;
47  import javax.faces.event.PreRemoveFlashValueEvent;
48  import javax.faces.lifecycle.ClientWindow;
49  
50  /**
51   * Implementation of Flash object
52   */
53  public class FlashImpl extends Flash
54  {
55      
56      // ~ static fields --------------------------------------------------------
57      
58      private static final Logger log = Logger.getLogger(FlashImpl.class.getName());
59      
60      /**
61       * Defines whether flash scope is disabled, preventing add the Flash cookie to the response. 
62       * 
63       * <p>This is useful for applications that does not require to use flash scope, and instead uses other scopes.</p>
64       */
65      @JSFWebConfigParam(defaultValue="false",since="2.0.5")
66      private static final String FLASH_SCOPE_DISABLED_PARAM = "org.apache.myfaces.FLASH_SCOPE_DISABLED";
67  
68      /**
69       * Use this prefix instead of the whole class name, because
70       * this makes the Cookies and the SubKeyMap operations (actually
71       * every String based operation where this is used as a key) faster.
72       */
73      private static final String FLASH_PREFIX = "oam.Flash";
74      
75      /**
76       * Key on application map to keep current instance
77       */
78      static final String FLASH_INSTANCE = FLASH_PREFIX + ".INSTANCE";
79  
80      /**
81       * Key to store if this setRedirect(true) was called on this request,
82       * and to store the redirect Cookie.
83       */
84      static final String FLASH_REDIRECT = FLASH_PREFIX + ".REDIRECT";
85      
86      /**
87       * Key to store the value of the redirect cookie
88       */
89      static final String FLASH_PREVIOUS_REQUEST_REDIRECT 
90              = FLASH_PREFIX + ".REDIRECT.PREVIOUSREQUEST";
91      
92      /**
93       * Key used to check if this request should keep messages
94       */
95      static final String FLASH_KEEP_MESSAGES = FLASH_PREFIX + ".KEEP_MESSAGES";
96  
97      /**
98       * Key used to store the messages list in the render FlashMap.
99       */
100     static final String FLASH_KEEP_MESSAGES_LIST = "KEEPMESSAGESLIST";
101 
102     /**
103      * Session map prefix to flash maps
104      */
105     static final String FLASH_SESSION_MAP_SUBKEY_PREFIX = FLASH_PREFIX + ".SCOPE";
106     
107     /**
108      * Key for the cached render FlashMap instance on the request map.
109      */
110     static final String FLASH_RENDER_MAP = FLASH_PREFIX + ".RENDERMAP";
111     
112     /**
113      * Key for the current render FlashMap token.
114      */
115     static final String FLASH_RENDER_MAP_TOKEN = FLASH_PREFIX + ".RENDERMAP.TOKEN";
116     
117     /**
118      * Key for the cached execute FlashMap instance on the request map.
119      */
120     static final String FLASH_EXECUTE_MAP = FLASH_PREFIX + ".EXECUTEMAP";
121 
122     /**
123      * Key for the current execute FlashMap token.
124      */
125     static final String FLASH_EXECUTE_MAP_TOKEN = FLASH_PREFIX + ".EXECUTEMAP.TOKEN";
126     
127     /**
128      * Token separator.
129      */
130     static final char SEPARATOR_CHAR = '.';
131     
132      // ~ static methods  -----------------------------------------------------
133     
134     /**
135      * Return a Flash instance from the application map
136      * 
137      * @param context
138      * @return
139      */
140     public static Flash getCurrentInstance(ExternalContext context)
141     {
142         return getCurrentInstance(context, true);
143     }
144     
145     public static Flash getCurrentInstance(ExternalContext context, boolean create)
146     {
147         Map<String, Object> applicationMap = context.getApplicationMap();
148         
149         Flash flash = (Flash) applicationMap.get(FLASH_INSTANCE);
150         if (flash == null && create)
151         {
152             // synchronize the ApplicationMap to ensure that only
153             // once instance of FlashImpl is created and stored in it.
154             synchronized (applicationMap)
155             {
156                 // check again, because first try was un-synchronized
157                 flash = (Flash) applicationMap.get(FLASH_INSTANCE);
158                 if (flash == null)
159                 {
160                     flash = new FlashImpl(context);
161                     applicationMap.put(FLASH_INSTANCE, flash);
162                 }
163             }
164         }
165 
166         return flash;
167     }
168 
169     /**
170      * Returns a cryptographically secure random number to use as the _count seed
171      */
172     private static long _getSeed()
173     {
174         SecureRandom rng;
175         try
176         {
177             // try SHA1 first
178             rng = SecureRandom.getInstance("SHA1PRNG");
179         }
180         catch (NoSuchAlgorithmException e)
181         {
182             // SHA1 not present, so try the default (which could potentially not be
183             // cryptographically secure)
184             rng = new SecureRandom();
185         }
186 
187         // use 48 bits for strength and fill them in
188         byte[] randomBytes = new byte[6];
189         rng.nextBytes(randomBytes);
190 
191         // convert to a long
192         return new BigInteger(randomBytes).longValue();
193     }
194     
195     // ~ private fields and constructor ---------------------------------------
196     
197     // the current token value
198     private final AtomicLong _count;
199     private boolean _flashScopeDisabled;
200     
201     public FlashImpl(ExternalContext externalContext)
202     {
203         _count = new AtomicLong(_getSeed());
204 
205         // Read whether flash scope is disabled.
206         _flashScopeDisabled = "true".equalsIgnoreCase(externalContext.getInitParameter(FLASH_SCOPE_DISABLED_PARAM));
207     }
208     
209     // ~ methods from javax.faces.context.Flash -------------------------------
210 
211     /**
212      * Used to restore the redirect value and the FacesMessages of the previous 
213      * request and to manage the flashMap tokens for this request before phase
214      * restore view starts.
215      */
216     @Override
217     public void doPrePhaseActions(FacesContext facesContext)
218     {
219         if (!_flashScopeDisabled)
220         {
221             final PhaseId currentPhaseId = facesContext.getCurrentPhaseId();
222         
223             if (PhaseId.RESTORE_VIEW.equals(currentPhaseId))
224             {
225                 // restore the redirect value
226                 // note that the result of this method is used in many places, 
227                 // thus it has to be the first thing to do
228                 _restoreRedirectValue(facesContext);
229             
230                 // restore the FlashMap token from the previous request
231                 // and create a new token for this request
232                 _manageFlashMapTokens(facesContext);
233             
234                 // try to restore any saved messages
235                 _restoreMessages(facesContext);
236             }
237         }
238     }
239     
240     /**
241      * Used to destroy the executeMap and to save all FacesMessages for the
242      * next request, but only if this is the last invocation of this method
243      * in the current lifecycle (if redirect phase 5, otherwise phase 6).
244      */
245     @Override
246     public void doPostPhaseActions(FacesContext facesContext)
247     {
248         if (!_flashScopeDisabled)
249         {
250             // do the actions only if this is the last time
251             // doPostPhaseActions() is called on this request
252             if (_isLastPhaseInRequest(facesContext))
253             {
254                 if (_isRedirectTrueOnThisRequest(facesContext))
255                 {
256                     // copy entries from executeMap to renderMap, if they do not exist
257                     Map<String, Object> renderMap = _getRenderFlashMap(facesContext);
258 
259                     for (Map.Entry<String, Object> entry 
260                         : _getExecuteFlashMap(facesContext).entrySet())
261                     {
262                         if (!renderMap.containsKey(entry.getKey()))
263                         {
264                             renderMap.put(entry.getKey(), entry.getValue());
265                         }
266                     }
267                 }
268             
269                 // remove execute Map entries from session (--> "destroy" executeMap)
270                 _clearExecuteFlashMap(facesContext);
271             
272                 // save the current FacesMessages in the renderMap, if wanted
273                 // Note that this also works on a redirect even though the redirect
274                 // was already performed and the response has already been committed,
275                 // because the renderMap is stored in the session.
276                 _saveMessages(facesContext);
277             }
278         }
279     }
280     
281     /**
282      * Return the value of this property for the flash for this session.
283      * 
284      * This must be false unless:
285      *   - setRedirect(boolean) was called for the current lifecycle traversal
286      *     with true as the argument.
287      *   - The current lifecycle traversal for this session is in the "execute"
288      *     phase and the previous traversal had setRedirect(boolean) called with
289      *     true as the argument.
290      */
291     @Override
292     public boolean isRedirect()
293     {
294         FacesContext facesContext = FacesContext.getCurrentInstance();
295         boolean thisRedirect = _isRedirectTrueOnThisRequest(facesContext);
296         boolean prevRedirect = _isRedirectTrueOnPreviousRequest(facesContext);
297         boolean executePhase = !PhaseId.RENDER_RESPONSE.equals(facesContext.getCurrentPhaseId());
298         
299         return thisRedirect || (executePhase && prevRedirect);
300     }
301     
302     @Override
303     public void setRedirect(boolean redirect)
304     {
305         // FIXME this method has a design flaw, because the only valid value
306         // is true and it should only be called by the NavigationHandler
307         // in a redirect case RIGHT BEFORE ExternalContext.redirect().
308         // Maybe a PreRedirectEvent issued by the ExternalContext would be a good
309         // choice for JSF 2.1.
310         
311         FacesContext facesContext = FacesContext.getCurrentInstance();
312         ExternalContext externalContext = facesContext.getExternalContext();
313         Map<String, Object> requestMap = externalContext.getRequestMap();
314         
315         // save the value on the requestMap for this request
316         Boolean alreadySet = (Boolean) requestMap.get(FLASH_REDIRECT);
317         alreadySet = (alreadySet == null ? Boolean.FALSE : Boolean.TRUE);
318         
319         // if true and not already set, store it for the following request
320         if (!alreadySet && redirect)
321         {
322             requestMap.put(FLASH_REDIRECT, Boolean.TRUE);
323             
324             // save redirect=true for the next request
325             _saveRedirectValue(facesContext);
326         }
327         else
328         {
329             if (alreadySet)
330             {
331                 log.warning("Multiple call to setRedirect() ignored.");
332             }
333             else // redirect = false
334             {
335                 log.warning("Ignored call to setRedirect(false), because this "
336                         + "should only be set to true by the NavigationHandler. "
337                         + "No one else should change it.");
338             }
339         }
340     }
341     
342     /**
343      * Take a value from the requestMap, or if it does not exist from the
344      * execute FlashMap, and put it on the render FlashMap, so it is visible on
345      * the next request.
346      */
347     @Override
348     public void keep(String key)
349     {
350         _checkFlashScopeDisabled();
351         FacesContext facesContext = FacesContext.getCurrentInstance();
352         Map<String, Object> requestMap = facesContext.getExternalContext().getRequestMap();
353         Object value = requestMap.get(key);
354         
355         // if the key does not exist in the requestMap,
356         // try to get it from the execute FlashMap
357         if (value == null)
358         {
359             Map<String, Object> executeMap = _getExecuteFlashMap(facesContext);
360             // Null-check, because in the GET request of a POST-REDIRECT-GET 
361             // pattern there is no execute map
362             if (executeMap != null)
363             {
364                 value = executeMap.get(key);
365                 // Store it on request map so we can get it later. For example, 
366                 // this is used by org.apache.myfaces.el.FlashELResolver to return
367                 // the value that has been promoted.
368                 requestMap.put(key, value);
369             }
370         }
371         
372         // put it in the render FlashMap
373         _getRenderFlashMap(facesContext).put(key, value);
374         
375         facesContext.getApplication().publishEvent(facesContext,
376                 PostKeepFlashValueEvent.class, key);
377     }
378 
379     /**
380      * This is just an alias for the request scope map.
381      */
382     @Override
383     public void putNow(String key, Object value)
384     {
385         _checkFlashScopeDisabled();
386         FacesContext.getCurrentInstance().getExternalContext()
387                 .getRequestMap().put(key, value);
388     }
389 
390     /**
391      * Returns the value of a previous call to setKeepMessages() from this
392      * request. If there was no call yet, false is returned.
393      */
394     @Override
395     public boolean isKeepMessages()
396     {
397         FacesContext facesContext = FacesContext.getCurrentInstance();
398         ExternalContext externalContext = facesContext.getExternalContext();
399         Map<String, Object> requestMap = externalContext.getRequestMap();
400         Boolean keepMessages = (Boolean) requestMap.get(FLASH_KEEP_MESSAGES);
401         
402         return (keepMessages == null ? Boolean.FALSE : keepMessages);
403     }
404     
405     /**
406      * If this property is true, the messages should be kept for the next 
407      * request, no matter if it is a normal postback case or a POST-
408      * REDIRECT-GET case.
409      * 
410      * Note that we don't have to store this value for the next request
411      * (like setRedirect()), because we will know if it was true on the 
412      * next request, if we can find any stored messages in the FlashMap.
413      * (also see _saveMessages() and _restoreMessages()).
414      */
415     @Override
416     public void setKeepMessages(boolean keepMessages)
417     {
418         FacesContext facesContext = FacesContext.getCurrentInstance();
419         ExternalContext externalContext = facesContext.getExternalContext();
420         Map<String, Object> requestMap = externalContext.getRequestMap();
421         requestMap.put(FLASH_KEEP_MESSAGES, keepMessages);
422     }
423     
424     // ~ Methods from Map interface -------------------------------------------
425     
426     // NOTE that all these methods do not necessarily delegate to the same Map,
427     // because we differentiate between reading and writing operations.
428     
429     public void clear()
430     {
431         _checkFlashScopeDisabled();
432         _getFlashMapForWriting().clear();
433     }
434 
435     public boolean containsKey(Object key)
436     {
437         _checkFlashScopeDisabled();
438         return _getFlashMapForReading().containsKey(key);
439     }
440 
441     public boolean containsValue(Object value)
442     {
443         _checkFlashScopeDisabled();
444         return _getFlashMapForReading().containsValue(value);
445     }
446 
447     public Set<java.util.Map.Entry<String, Object>> entrySet()
448     {
449         _checkFlashScopeDisabled();
450         return _getFlashMapForReading().entrySet();
451     }
452 
453     public Object get(Object key)
454     {
455         _checkFlashScopeDisabled();
456         if (key == null)
457         {
458             return null;
459         }
460         
461         if ("keepMessages".equals(key))
462         {
463             return isKeepMessages();
464         }
465         else if ("redirect".equals(key))
466         {
467             return isRedirect();
468         }
469         
470         return _getFlashMapForReading().get(key);
471     }
472     
473     public boolean isEmpty()
474     {
475         _checkFlashScopeDisabled();
476         return _getFlashMapForReading().isEmpty();
477     }
478 
479     public Set<String> keySet()
480     {
481         _checkFlashScopeDisabled();
482         return _getFlashMapForReading().keySet();
483     }
484 
485     public Object put(String key, Object value)
486     {
487         _checkFlashScopeDisabled();
488         if (key == null)
489         {
490             return null;
491         }
492         
493         if ("keepMessages".equals(key))
494         {
495             Boolean booleanValue = _convertToBoolean(value);
496             this.setKeepMessages(booleanValue);
497             return booleanValue;
498         }
499         else if ("redirect".equals(key))
500         {
501             Boolean booleanValue = _convertToBoolean(value);
502             this.setRedirect(booleanValue);
503             return booleanValue;
504         }
505         else
506         {
507             Object resp = _getFlashMapForWriting().put(key, value); 
508             
509             FacesContext facesContext = FacesContext.getCurrentInstance();
510             facesContext.getApplication().publishEvent(facesContext,
511                 PostPutFlashValueEvent.class, key);
512 
513             return resp;
514         }
515     }
516 
517     public void putAll(Map<? extends String, ? extends Object> m)
518     {
519         _checkFlashScopeDisabled();
520         _getFlashMapForWriting().putAll(m);
521     }
522 
523     public Object remove(Object key)
524     {
525         _checkFlashScopeDisabled();
526         
527         FacesContext facesContext = FacesContext.getCurrentInstance();
528         facesContext.getApplication().publishEvent(facesContext,
529             PreRemoveFlashValueEvent.class, key);
530         
531         return _getFlashMapForWriting().remove(key);
532     }
533 
534     public int size()
535     {
536         _checkFlashScopeDisabled();
537         return _getFlashMapForReading().size();
538     }
539 
540     public Collection<Object> values()
541     {
542         _checkFlashScopeDisabled();
543         return _getFlashMapForReading().values();
544     }
545     
546     // ~ Implementation methods ----------------------------------------------- 
547     
548     /**
549      * Returns true if the current phase is the last phase in the request
550      * and thus if doPostPhaseActions() is called for the last time.
551      * 
552      * This will be true if either we are in phase 6 (render response)
553      * or if setRedirect(true) was called on this request and we are
554      * in phase 5 (invoke application).
555      */
556     private boolean _isLastPhaseInRequest(FacesContext facesContext)
557     {
558         final PhaseId currentPhaseId = facesContext.getCurrentPhaseId();
559         
560         boolean lastPhaseNormalRequest = PhaseId.RENDER_RESPONSE.equals(currentPhaseId);
561         // According to the spec, if there is a redirect, responseComplete() 
562         // has been called, and Flash.setRedirect() has been called too,
563         // so we just need to check both are present.
564         boolean lastPhaseIfRedirect = facesContext.getResponseComplete()
565                 && _isRedirectTrueOnThisRequest(facesContext);
566         
567         return lastPhaseNormalRequest || lastPhaseIfRedirect;
568     }
569     
570     /**
571      * Return true if setRedirect(true) was called on this request.
572      * @param facesContext
573      * @return
574      */
575     private boolean _isRedirectTrueOnThisRequest(FacesContext facesContext)
576     {
577         ExternalContext externalContext = facesContext.getExternalContext();
578         Map<String, Object> requestMap = externalContext.getRequestMap();
579         Boolean redirect = (Boolean) requestMap.get(FLASH_REDIRECT);
580         
581         return Boolean.TRUE.equals(redirect);
582     }
583     
584     /**
585      * Return true if setRedirect(true) was called on the previous request.
586      * Precondition: doPrePhaseActions() must have been called on restore view phase.
587      * @param facesContext
588      * @return
589      */
590     private boolean _isRedirectTrueOnPreviousRequest(FacesContext facesContext)
591     {
592         ExternalContext externalContext = facesContext.getExternalContext();
593         Map<String, Object> requestMap = externalContext.getRequestMap();
594         Boolean redirect = (Boolean) requestMap.get(FLASH_PREVIOUS_REQUEST_REDIRECT);
595         
596         return Boolean.TRUE.equals(redirect);
597     }
598     
599     /**
600      * Saves the value of setRedirect() for the next request, if it was true
601      */
602     private void _saveRedirectValue(FacesContext facesContext)
603     {
604         ExternalContext externalContext = facesContext.getExternalContext();
605         
606         // This request contains a redirect. This condition is in general 
607         // triggered by a NavigationHandler. After a redirect all request scope
608         // values get lost, so in order to preserve this value we need to
609         // pass it between request. One strategy is use a cookie that is never sent
610         // to the client. Other alternative is use the session map.
611         // See _restoreRedirectValue() for restoring this value.
612         HttpServletResponse httpResponse = ExternalContextUtils
613                 .getHttpServletResponse(externalContext);
614         if (httpResponse != null)
615         {
616             Cookie cookie = _createFlashCookie(FLASH_REDIRECT, "true", externalContext);
617             httpResponse.addCookie(cookie);
618         }
619         else
620         {
621             externalContext.getSessionMap().put(FLASH_REDIRECT, true);
622         }
623     }
624 
625     /**
626      * Restores the redirect value of the previous request and saves
627      * it in the RequestMap under the key FLASH_PREVIOUS_REQUEST_REDIRECT.
628      * Must not be called more than once per request.
629      * After this method was invoked, the requestMap will contain Boolean.TRUE
630      * if setRedirect(true) was called on the previous request or Boolean.FALSE
631      * or null otherwise.
632      */
633     private void _restoreRedirectValue(FacesContext facesContext)
634     {
635         ExternalContext externalContext = facesContext.getExternalContext();
636         HttpServletResponse httpResponse = ExternalContextUtils
637                 .getHttpServletResponse(externalContext);
638         if (httpResponse != null)
639         {
640             // Request values are lost after a redirect. We can create a 
641             // temporal cookie to pass the params between redirect calls.
642             // It is better than use HttpSession object, because this cookie
643             // is never sent by the server.
644             Cookie cookie = (Cookie) externalContext
645                     .getRequestCookieMap().get(FLASH_REDIRECT);
646             if (cookie != null)
647             {
648                 // the cookie exists means there was a redirect, regardless of the value
649                 externalContext.getRequestMap().put(
650                         FLASH_PREVIOUS_REQUEST_REDIRECT, Boolean.TRUE);
651                 
652                 // A redirect happened, so it is safe to remove the cookie, setting
653                 // the maxAge to 0 seconds. The effect is we passed FLASH_REDIRECT param 
654                 // to this request object
655                 cookie.setMaxAge(0);
656                 cookie.setPath(_getCookiePath(externalContext));
657                 //MYFACES-3354 jetty 6.1.5 does not allow this,
658                 //call setMaxAge(0) is enough
659                 //cookie.setValue(null);
660                 httpResponse.addCookie(cookie);
661             }
662         }
663         else
664         {
665             // Note that on portlet world we can't create cookies,
666             // so we are forced to use the session map. Anyway, 
667             // according to the Bridge implementation(for example see 
668             // org.apache.myfaces.portlet.faces.bridge.BridgeImpl)
669             // session object is created at start faces request
670             Map<String, Object> sessionMap = externalContext.getSessionMap();
671             
672             // remove the value from the sessionMap
673             Boolean redirect = (Boolean) sessionMap.remove(FLASH_REDIRECT);
674             
675             // put the value into the requestMap
676             externalContext.getRequestMap().put(
677                     FLASH_PREVIOUS_REQUEST_REDIRECT, redirect);
678         }
679     }
680     
681     /**
682      * Saves the current FacesMessages as a List on the render FlashMap for the
683      * next request if isKeepMessages() is true. Otherwise it removes any
684      * existing FacesMessages-List from the renderFlashMap. 
685      * @param facesContext
686      */
687     private void _saveMessages(FacesContext facesContext)
688     {
689         if (isKeepMessages()) 
690         {
691             // get all messages from the FacesContext and store
692             // them on the renderMap
693             List<MessageEntry> messageList = null;
694                 
695             Iterator<String> iterClientIds = facesContext.getClientIdsWithMessages();
696             while (iterClientIds.hasNext())
697             {
698                 String clientId = (String) iterClientIds.next();
699                 Iterator<FacesMessage> iterMessages = facesContext.getMessages(clientId);
700                 
701                 while (iterMessages.hasNext())
702                 {
703                     FacesMessage message = iterMessages.next();
704     
705                     if (messageList == null)
706                     {
707                         messageList = new ArrayList<MessageEntry>();
708                     }
709                     messageList.add(new MessageEntry(clientId, message));
710                 }
711             }
712     
713             _getRenderFlashMap(facesContext).put(FLASH_KEEP_MESSAGES_LIST, messageList);
714         }
715         else
716         {
717             // do not keep messages --> remove messagesList from renderMap
718             _getRenderFlashMap(facesContext).remove(FLASH_KEEP_MESSAGES_LIST);
719         }
720     }
721 
722     /**
723      * Restore any saved FacesMessages from the previous request.
724      * Note that we don't need to save the keepMessages value for this request,
725      * because we just have to check if the value for FLASH_KEEP_MESSAGES_LIST exists.
726      * @param facesContext
727      */
728     @SuppressWarnings("unchecked")
729     private void _restoreMessages(FacesContext facesContext)
730     {
731         List<MessageEntry> messageList = (List<MessageEntry>) 
732                 _getExecuteFlashMap(facesContext).get(FLASH_KEEP_MESSAGES_LIST);
733 
734         if (messageList != null)
735         {
736             Iterator<MessageEntry> iterMessages = messageList.iterator();
737 
738             while (iterMessages.hasNext())
739             {
740                 MessageEntry entry = iterMessages.next();
741                 facesContext.addMessage(entry.clientId, entry.message);
742             }
743 
744             // we can now remove the messagesList from the flashMap
745             _getExecuteFlashMap(facesContext).remove(FLASH_KEEP_MESSAGES_LIST);
746         }
747     }
748     
749     /**
750      * Take the render map key and store it as a key for the next request.
751      * 
752      * On the next request we can get it with _getRenderFlashMapTokenFromPreviousRequest().
753      * @param externalContext
754      */
755     private void _saveRenderFlashMapTokenForNextRequest(ExternalContext externalContext)
756     {
757         String tokenValue = (String) externalContext.getRequestMap().get(FLASH_RENDER_MAP_TOKEN);
758         ClientWindow clientWindow = externalContext.getClientWindow();
759         if (clientWindow != null)
760         {
761             //Use HttpSession or PortletSession object
762             Map<String, Object> sessionMap = externalContext.getSessionMap();
763             sessionMap.put(FLASH_RENDER_MAP_TOKEN+SEPARATOR_CHAR+clientWindow.getId(), tokenValue);
764         }
765         else
766         {
767             HttpServletResponse httpResponse = ExternalContextUtils.getHttpServletResponse(externalContext);
768             if (httpResponse != null)
769             {
770                 Cookie cookie = _createFlashCookie(FLASH_RENDER_MAP_TOKEN, tokenValue, externalContext);
771                 httpResponse.addCookie(cookie);
772             }
773             else
774             {
775                 //Use HttpSession or PortletSession object
776                 Map<String, Object> sessionMap = externalContext.getSessionMap();
777                 sessionMap.put(FLASH_RENDER_MAP_TOKEN, tokenValue);
778             }
779         }
780     }
781     
782     /**
783      * Retrieve the map token of the render map from the previous request.
784      * 
785      * Returns the value of _saveRenderFlashMapTokenForNextRequest() from
786      * the previous request.
787      * @param externalContext
788      * @return
789      */
790     private String _getRenderFlashMapTokenFromPreviousRequest(ExternalContext externalContext)
791     {
792         String tokenValue = null;
793         ClientWindow clientWindow = externalContext.getClientWindow();
794         if (clientWindow != null)
795         {
796             Map<String, Object> sessionMap = externalContext.getSessionMap();
797             tokenValue = (String) sessionMap.get(FLASH_RENDER_MAP_TOKEN+
798                     SEPARATOR_CHAR+clientWindow.getId());
799         }
800         else
801         {
802             HttpServletResponse httpResponse = ExternalContextUtils.getHttpServletResponse(externalContext);
803             if (httpResponse != null)
804             {
805                 //Use a cookie
806                 Cookie cookie = (Cookie) externalContext.getRequestCookieMap().get(FLASH_RENDER_MAP_TOKEN);
807                 if (cookie != null)
808                 {
809                     tokenValue = cookie.getValue();
810                 }
811             }
812             else
813             {
814                 //Use HttpSession or PortletSession object
815                 Map<String, Object> sessionMap = externalContext.getSessionMap();
816                 tokenValue = (String) sessionMap.get(FLASH_RENDER_MAP_TOKEN);
817             }
818         }
819         return tokenValue;
820     }
821     
822     /**
823      * Restores the render FlashMap token from the previous request.
824      * This is the token of the executeMap for this request.
825      * Furthermore it also creates a new token for this request's renderMap
826      * (and thus implicitly a new renderMap).
827      * @param facesContext
828      */
829     private void _manageFlashMapTokens(FacesContext facesContext)
830     {
831         ExternalContext externalContext = facesContext.getExternalContext();
832         Map<String, Object> requestMap = externalContext.getRequestMap();
833 
834         final String previousRenderToken 
835                 = _getRenderFlashMapTokenFromPreviousRequest(externalContext);
836         if (previousRenderToken != null)
837         {
838             // "restore" the renderMap from the previous request
839             // and put it as the executeMap for this request
840             requestMap.put(FLASH_EXECUTE_MAP_TOKEN, previousRenderToken);
841         }
842         else
843         {
844             if (facesContext.isPostback())
845             {
846                 // on a postback, we should always have a previousToken
847                 log.warning("Identifier for execute FlashMap was lost on " +
848                         "the postback, thus FlashScope information is gone.");
849             }
850             
851             // create a new token (and thus a new Map) for this request's 
852             // executeMap so that we have an executeMap in any possible case.
853             final String newExecuteToken = _getNextToken();
854             requestMap.put(FLASH_EXECUTE_MAP_TOKEN, newExecuteToken);
855         }
856         
857         // create a new token (and thus a new Map) for this request's renderMap
858         final String newRenderToken = _getNextToken();
859         requestMap.put(FLASH_RENDER_MAP_TOKEN, newRenderToken);
860         
861         // we now have the final render token for this request, thus we can
862         // already save it for the next request, because it won't change
863         _saveRenderFlashMapTokenForNextRequest(externalContext);
864     }
865     
866     /**
867      * Get the next token to be assigned to this request
868      * 
869      * @return
870      */
871     private String _getNextToken()
872     {
873         // atomically increment the value
874         long nextToken = _count.incrementAndGet();
875 
876         // convert using base 36 because it is a fast efficient subset of base-64
877         return Long.toString(nextToken, 36);
878     }
879 
880     /**
881      * Create a new subkey-wrapper of the session map with the given prefix.
882      * This wrapper is used to implement the maps for the flash scope.
883      * For more information see the SubKeyMap doc.
884      */
885     private Map<String, Object> _createSubKeyMap(FacesContext context, String prefix)
886     {
887         ExternalContext external = context.getExternalContext();
888         Map<String, Object> sessionMap = external.getSessionMap();
889 
890         return new SubKeyMap<Object>(sessionMap, prefix);
891     }
892 
893     /**
894      * Return the flash map created on this traversal.
895      * 
896      * This FlashMap will be the execute FlashMap of the next traversal.
897      * 
898      * Note that it is supposed that FLASH_RENDER_MAP_TOKEN is initialized
899      * before restore view phase (see doPrePhaseActions() for details).
900      * 
901      * @param context
902      * @return
903      */
904     @SuppressWarnings("unchecked")
905     private Map<String, Object> _getRenderFlashMap(FacesContext context)
906     {
907         // Note that we don't have to synchronize here, because it is no problem
908         // if we create more SubKeyMaps with the same subkey, because they are
909         // totally equal and point to the same entries in the SessionMap.
910         
911         Map<String, Object> requestMap = context.getExternalContext().getRequestMap();
912         Map<String, Object> map = (Map<String, Object>) requestMap.get(FLASH_RENDER_MAP);
913         if (map == null)
914         {
915             String token = (String) requestMap.get(FLASH_RENDER_MAP_TOKEN);
916             String fullToken = FLASH_SESSION_MAP_SUBKEY_PREFIX + SEPARATOR_CHAR + token;
917             map =  _createSubKeyMap(context, fullToken);
918             requestMap.put(FLASH_RENDER_MAP, map);
919         }
920         return map;
921     }
922     
923     /**
924      * Return the execute Flash Map.
925      * 
926      * This FlashMap was the render FlashMap of the previous traversal. 
927      * 
928      * @param context
929      * @return
930      */
931     @SuppressWarnings("unchecked")
932     private Map<String, Object> _getExecuteFlashMap(FacesContext context)
933     {
934         // Note that we don't have to synchronize here, because it is no problem
935         // if we create more SubKeyMaps with the same subkey, because they are
936         // totally equal and point to the same entries in the SessionMap.
937         
938         Map<String, Object> requestMap = context.getExternalContext().getRequestMap();
939         Map<String, Object> map = (Map<String, Object>) requestMap.get(FLASH_EXECUTE_MAP);
940         if (map == null)
941         {
942             String token = (String) requestMap.get(FLASH_EXECUTE_MAP_TOKEN);
943             String fullToken = FLASH_SESSION_MAP_SUBKEY_PREFIX + SEPARATOR_CHAR + token;
944             map = _createSubKeyMap(context, fullToken);
945             requestMap.put(FLASH_EXECUTE_MAP, map);
946         }
947         return map;
948     }
949     
950     /**
951      * Get the proper map according to the current phase:
952      * 
953      * Normal case:
954      * 
955      * - First request, restore view phase (create a new one): render map n
956      * - First request, execute phase: Skipped
957      * - First request, render  phase: render map n
958      * 
959      *   Render map n saved and put as execute map n
960      * 
961      * - Second request, execute phase: execute map n
962      * - Second request, render  phase: render map n+1
963      * 
964      * Post Redirect Get case: Redirect is triggered by a call to setRedirect(true) from NavigationHandler
965      * or earlier using c:set tag.
966      * 
967      * - First request, restore view phase (create a new one): render map n
968      * - First request, execute phase: Skipped
969      * - First request, render  phase: render map n
970      * 
971      *   Render map n saved and put as execute map n
972      * 
973      * POST
974      * 
975      * - Second request, execute phase: execute map n
976      *   Note that render map n+1 is also created here to perform keep().
977      * 
978      * REDIRECT
979      * 
980      * - NavigationHandler do the redirect, requestMap data lost, called Flash.setRedirect(true)
981      * 
982      *   Render map n+1 saved and put as render map n+1 on GET request.
983      * 
984      * GET
985      * 
986      * - Third  request, restore view phase (create a new one): render map n+1 (restorred)
987      *   (isRedirect() should return true as javadoc says)
988      * - Third  request, execute phase: skipped
989      * - Third  request, render  phase: render map n+1
990      * 
991      * In this way proper behavior is preserved even in the case of redirect, since the GET part is handled as
992      * the "render" part of the current traversal, keeping the semantic of flash object.
993      * 
994      * @return
995      */
996     private Map<String, Object> _getActiveFlashMap()
997     {
998         FacesContext facesContext = FacesContext.getCurrentInstance();
999         if (PhaseId.RENDER_RESPONSE.equals(facesContext.getCurrentPhaseId()) 
1000                 || !facesContext.isPostback())
1001         {
1002             return _getRenderFlashMap(facesContext);
1003         }
1004         else
1005         {
1006             return _getExecuteFlashMap(facesContext);
1007         }
1008     }
1009     
1010     /**
1011      * Returns the FlashMap used in the reading methods of java.util.Map
1012      * like e.g. get() or values().
1013      * @return
1014      */
1015     private Map<String, Object> _getFlashMapForReading()
1016     {
1017         return _getExecuteFlashMap(FacesContext.getCurrentInstance());
1018     }
1019     
1020     /**
1021      * Returns the FlashMap used in the writing methods of java.util.Map
1022      * like e.g. put() or clear().
1023      * @return
1024      */
1025     private Map<String, Object> _getFlashMapForWriting()
1026     {
1027         return _getActiveFlashMap();
1028     }
1029 
1030     /**
1031      * Destroy the execute FlashMap, because it is not needed anymore.
1032      * @param facesContext
1033      */
1034     private void _clearExecuteFlashMap(FacesContext facesContext)
1035     {
1036         Map<String, Object> map = _getExecuteFlashMap(facesContext);
1037 
1038         //JSF 2.2 invoke PreClearFlashEvent
1039         facesContext.getApplication().publishEvent(facesContext, 
1040             PreClearFlashEvent.class, map);
1041         
1042         // Clear everything - note that because of naming conventions,
1043         // this will in fact automatically recurse through all children
1044         // grandchildren etc. - which is kind of a design flaw of SubKeyMap,
1045         // but one we're relying on
1046         
1047         // NOTE that we do not need a null check here, because there will
1048         // always be an execute Map, however sometimes an empty one!
1049         map.clear();
1050     }
1051 
1052     /**
1053      * Creates a Cookie with the given name and value.
1054      * In addition, it will be configured with maxAge=-1, the current request path and secure value.
1055      *
1056      * @param name
1057      * @param value
1058      * @param externalContext
1059      * @return
1060      */
1061     private Cookie _createFlashCookie(String name, String value, ExternalContext externalContext)
1062     {
1063         Cookie cookie = new Cookie(name, value);
1064 
1065         cookie.setMaxAge(-1);
1066         cookie.setPath(_getCookiePath(externalContext));
1067         cookie.setSecure(externalContext.isSecure());
1068 
1069         return cookie;
1070     }
1071 
1072     /**
1073      * Returns the path for the Flash-Cookies.
1074      * @param externalContext
1075      * @return
1076      */
1077     private String _getCookiePath(ExternalContext externalContext)
1078     {
1079         String contextPath = externalContext.getRequestContextPath();
1080 
1081         if (contextPath == null || "".equals(contextPath))
1082         {
1083             contextPath = "/";
1084         }
1085 
1086         return contextPath;
1087     }
1088     
1089     /**
1090      * Convert the Object to a Boolean.
1091      * @param value
1092      * @return
1093      */
1094     private Boolean _convertToBoolean(Object value)
1095     {
1096         Boolean booleanValue;
1097         if (value instanceof Boolean)
1098         {
1099             booleanValue = (Boolean) value;
1100         }
1101         else
1102         {
1103             booleanValue = Boolean.parseBoolean(value.toString());
1104         }
1105         return booleanValue;
1106     }
1107     
1108     /**
1109      * Checks whether flash scope is disabled.
1110      * @throws FlashScopeDisabledException if flash scope is disabled
1111      */
1112     private void _checkFlashScopeDisabled()
1113     {
1114         if (_flashScopeDisabled)
1115         {
1116             throw new FlashScopeDisabledException("Flash scope was disabled by context param " 
1117                 + FLASH_SCOPE_DISABLED_PARAM + " but erroneously accessed");
1118         }
1119     }
1120     
1121     // ~ Inner classes --------------------------------------------------------
1122     
1123     /**
1124      * Class used to store a FacesMessage with its clientId.
1125      */
1126     private static class MessageEntry implements Serializable
1127     {
1128         private static final long serialVersionUID = -690264660230199234L;
1129         private final String clientId;
1130         private final FacesMessage message;
1131 
1132         public MessageEntry(String clientId, FacesMessage message)
1133         {
1134             this.clientId = clientId;
1135             this.message = message;
1136         }
1137     }
1138     
1139 }