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