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