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