| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.myfaces.shared.context.flash; |
| |
| import org.apache.myfaces.shared.util.SubKeyMap; |
| import org.apache.myfaces.shared.util.ExternalContextUtils; |
| |
| import javax.faces.application.FacesMessage; |
| import javax.faces.context.ExternalContext; |
| import javax.faces.context.FacesContext; |
| import javax.faces.context.Flash; |
| import javax.faces.event.PhaseId; |
| import javax.servlet.http.Cookie; |
| import javax.servlet.http.HttpServletResponse; |
| import java.io.Serializable; |
| import java.math.BigInteger; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.SecureRandom; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.concurrent.atomic.AtomicLong; |
| import java.util.logging.Logger; |
| import javax.faces.event.PostKeepFlashValueEvent; |
| import javax.faces.event.PostPutFlashValueEvent; |
| import javax.faces.event.PreClearFlashEvent; |
| import javax.faces.event.PreRemoveFlashValueEvent; |
| import javax.faces.lifecycle.ClientWindow; |
| import org.apache.myfaces.shared.config.MyfacesConfig; |
| import org.apache.myfaces.shared.util.ServletSpecifications; |
| |
| /** |
| * Implementation of Flash object |
| */ |
| public class FlashImpl extends Flash implements ReleasableFlash |
| { |
| |
| // ~ static fields -------------------------------------------------------- |
| |
| private static final Logger log = Logger.getLogger(FlashImpl.class.getName()); |
| |
| /** |
| * Use this prefix instead of the whole class name, because |
| * this makes the Cookies and the SubKeyMap operations (actually |
| * every String based operation where this is used as a key) faster. |
| */ |
| private static final String FLASH_PREFIX = "oam.Flash"; |
| |
| /** |
| * Key on application map to keep current instance |
| */ |
| static final String FLASH_INSTANCE = FLASH_PREFIX + ".INSTANCE"; |
| |
| /** |
| * Key to store if this setRedirect(true) was called on this request, |
| * and to store the redirect Cookie. |
| */ |
| static final String FLASH_REDIRECT = FLASH_PREFIX + ".REDIRECT"; |
| |
| /** |
| * Key to store the value of the redirect cookie |
| */ |
| static final String FLASH_PREVIOUS_REQUEST_REDIRECT |
| = FLASH_PREFIX + ".REDIRECT.PREVIOUSREQUEST"; |
| |
| /** |
| * Key used to check if this request should keep messages |
| */ |
| static final String FLASH_KEEP_MESSAGES = FLASH_PREFIX + ".KEEP_MESSAGES"; |
| |
| /** |
| * Key used to store the messages list in the render FlashMap. |
| */ |
| static final String FLASH_KEEP_MESSAGES_LIST = "KEEPMESSAGESLIST"; |
| |
| /** |
| * Session map prefix to flash maps |
| */ |
| static final String FLASH_SESSION_MAP_SUBKEY_PREFIX = FLASH_PREFIX + ".SCOPE"; |
| |
| /** |
| * Key for the cached render FlashMap instance on the request map. |
| */ |
| static final String FLASH_RENDER_MAP = FLASH_PREFIX + ".RENDERMAP"; |
| |
| /** |
| * Key for the current render FlashMap token. |
| */ |
| static final String FLASH_RENDER_MAP_TOKEN = FLASH_PREFIX + ".RENDERMAP.TOKEN"; |
| |
| /** |
| * Key for the cached execute FlashMap instance on the request map. |
| */ |
| static final String FLASH_EXECUTE_MAP = FLASH_PREFIX + ".EXECUTEMAP"; |
| |
| /** |
| * Key for the current execute FlashMap token. |
| */ |
| static final String FLASH_EXECUTE_MAP_TOKEN = FLASH_PREFIX + ".EXECUTEMAP.TOKEN"; |
| |
| static final String FLASH_CW_LRU_MAP = FLASH_PREFIX + ".CW.LRUMAP"; |
| |
| /** |
| * Token separator. |
| */ |
| static final char SEPARATOR_CHAR = '.'; |
| |
| // ~ static methods ----------------------------------------------------- |
| |
| /** |
| * Return a Flash instance from the application map |
| * |
| * @param context |
| * @return |
| */ |
| public static Flash getCurrentInstance(ExternalContext context) |
| { |
| return getCurrentInstance(context, true); |
| } |
| |
| public static Flash getCurrentInstance(ExternalContext context, boolean create) |
| { |
| Map<String, Object> applicationMap = context.getApplicationMap(); |
| |
| Flash flash = (Flash) applicationMap.get(FLASH_INSTANCE); |
| if (flash == null && create) |
| { |
| // synchronize the ApplicationMap to ensure that only |
| // once instance of FlashImpl is created and stored in it. |
| synchronized (applicationMap) |
| { |
| // check again, because first try was un-synchronized |
| flash = (Flash) applicationMap.get(FLASH_INSTANCE); |
| if (flash == null) |
| { |
| flash = new FlashImpl(context); |
| applicationMap.put(FLASH_INSTANCE, flash); |
| } |
| } |
| } |
| |
| return flash; |
| } |
| |
| /** |
| * Returns a cryptographically secure random number to use as the _count seed |
| */ |
| private static long _getSeed() |
| { |
| SecureRandom rng; |
| try |
| { |
| // try SHA1 first |
| rng = SecureRandom.getInstance("SHA1PRNG"); |
| } |
| catch (NoSuchAlgorithmException e) |
| { |
| // SHA1 not present, so try the default (which could potentially not be |
| // cryptographically secure) |
| rng = new SecureRandom(); |
| } |
| |
| // use 48 bits for strength and fill them in |
| byte[] randomBytes = new byte[6]; |
| rng.nextBytes(randomBytes); |
| |
| // convert to a long |
| return new BigInteger(randomBytes).longValue(); |
| } |
| |
| // ~ private fields and constructor --------------------------------------- |
| |
| // the current token value |
| private final AtomicLong _count; |
| private boolean _flashScopeDisabled; |
| |
| public FlashImpl(ExternalContext externalContext) |
| { |
| _count = new AtomicLong(_getSeed()); |
| |
| // Read whether flash scope is disabled. |
| _flashScopeDisabled = MyfacesConfig.getCurrentInstance(externalContext).isFlashScopeDisabled(); |
| } |
| |
| // ~ methods from javax.faces.context.Flash ------------------------------- |
| |
| /** |
| * Used to restore the redirect value and the FacesMessages of the previous |
| * request and to manage the flashMap tokens for this request before phase |
| * restore view starts. |
| */ |
| @Override |
| public void doPrePhaseActions(FacesContext facesContext) |
| { |
| if (!_flashScopeDisabled) |
| { |
| final PhaseId currentPhaseId = facesContext.getCurrentPhaseId(); |
| |
| if (PhaseId.RESTORE_VIEW.equals(currentPhaseId)) |
| { |
| // restore the redirect value |
| // note that the result of this method is used in many places, |
| // thus it has to be the first thing to do |
| _restoreRedirectValue(facesContext); |
| |
| // restore the FlashMap token from the previous request |
| // and create a new token for this request |
| _manageFlashMapTokens(facesContext); |
| |
| // try to restore any saved messages |
| _restoreMessages(facesContext); |
| } |
| } |
| } |
| |
| /** |
| * Used to destroy the executeMap and to save all FacesMessages for the |
| * next request, but only if this is the last invocation of this method |
| * in the current lifecycle (if redirect phase 5, otherwise phase 6). |
| */ |
| @Override |
| public void doPostPhaseActions(FacesContext facesContext) |
| { |
| if (!_flashScopeDisabled) |
| { |
| // do the actions only if this is the last time |
| // doPostPhaseActions() is called on this request |
| if (_isLastPhaseInRequest(facesContext)) |
| { |
| if (_isRedirectTrueOnThisRequest(facesContext)) |
| { |
| // copy entries from executeMap to renderMap, if they do not exist |
| Map<String, Object> renderMap = _getRenderFlashMap(facesContext); |
| |
| for (Map.Entry<String, Object> entry |
| : _getExecuteFlashMap(facesContext).entrySet()) |
| { |
| if (!renderMap.containsKey(entry.getKey())) |
| { |
| renderMap.put(entry.getKey(), entry.getValue()); |
| } |
| } |
| } |
| |
| // remove execute Map entries from session (--> "destroy" executeMap) |
| _clearExecuteFlashMap(facesContext); |
| |
| // save the current FacesMessages in the renderMap, if wanted |
| // Note that this also works on a redirect even though the redirect |
| // was already performed and the response has already been committed, |
| // because the renderMap is stored in the session. |
| _saveMessages(facesContext); |
| |
| _clearRenderFlashTokenIfMapEmpty(facesContext); |
| } |
| } |
| } |
| |
| /** |
| * Return the value of this property for the flash for this session. |
| * |
| * This must be false unless: |
| * - setRedirect(boolean) was called for the current lifecycle traversal |
| * with true as the argument. |
| * - The current lifecycle traversal for this session is in the "execute" |
| * phase and the previous traversal had setRedirect(boolean) called with |
| * true as the argument. |
| */ |
| @Override |
| public boolean isRedirect() |
| { |
| FacesContext facesContext = FacesContext.getCurrentInstance(); |
| boolean thisRedirect = _isRedirectTrueOnThisRequest(facesContext); |
| boolean prevRedirect = _isRedirectTrueOnPreviousRequest(facesContext); |
| boolean executePhase = !PhaseId.RENDER_RESPONSE.equals(facesContext.getCurrentPhaseId()); |
| |
| return thisRedirect || (executePhase && prevRedirect); |
| } |
| |
| @Override |
| public void setRedirect(boolean redirect) |
| { |
| // FIXME this method has a design flaw, because the only valid value |
| // is true and it should only be called by the NavigationHandler |
| // in a redirect case RIGHT BEFORE ExternalContext.redirect(). |
| // Maybe a PreRedirectEvent issued by the ExternalContext would be a good |
| // choice for JSF 2.1. |
| |
| FacesContext facesContext = FacesContext.getCurrentInstance(); |
| ExternalContext externalContext = facesContext.getExternalContext(); |
| Map<String, Object> requestMap = externalContext.getRequestMap(); |
| |
| // save the value on the requestMap for this request |
| Boolean alreadySet = (Boolean) requestMap.get(FLASH_REDIRECT); |
| alreadySet = (alreadySet == null ? Boolean.FALSE : Boolean.TRUE); |
| |
| // if true and not already set, store it for the following request |
| if (!alreadySet && redirect) |
| { |
| requestMap.put(FLASH_REDIRECT, Boolean.TRUE); |
| |
| // save redirect=true for the next request |
| _saveRedirectValue(facesContext); |
| } |
| else |
| { |
| if (alreadySet) |
| { |
| log.warning("Multiple call to setRedirect() ignored."); |
| } |
| else // redirect = false |
| { |
| log.warning("Ignored call to setRedirect(false), because this " |
| + "should only be set to true by the NavigationHandler. " |
| + "No one else should change it."); |
| } |
| } |
| } |
| |
| /** |
| * Take a value from the requestMap, or if it does not exist from the |
| * execute FlashMap, and put it on the render FlashMap, so it is visible on |
| * the next request. |
| */ |
| @Override |
| public void keep(String key) |
| { |
| _checkFlashScopeDisabled(); |
| FacesContext facesContext = FacesContext.getCurrentInstance(); |
| Map<String, Object> requestMap = facesContext.getExternalContext().getRequestMap(); |
| Object value = requestMap.get(key); |
| |
| // if the key does not exist in the requestMap, |
| // try to get it from the execute FlashMap |
| if (value == null) |
| { |
| Map<String, Object> executeMap = _getExecuteFlashMap(facesContext); |
| // Null-check, because in the GET request of a POST-REDIRECT-GET |
| // pattern there is no execute map |
| if (executeMap != null) |
| { |
| value = executeMap.get(key); |
| // Store it on request map so we can get it later. For example, |
| // this is used by org.apache.myfaces.el.FlashELResolver to return |
| // the value that has been promoted. |
| requestMap.put(key, value); |
| } |
| } |
| |
| // put it in the render FlashMap |
| _getRenderFlashMap(facesContext).put(key, value); |
| |
| facesContext.getApplication().publishEvent(facesContext, |
| PostKeepFlashValueEvent.class, key); |
| } |
| |
| /** |
| * This is just an alias for the request scope map. |
| */ |
| @Override |
| public void putNow(String key, Object value) |
| { |
| _checkFlashScopeDisabled(); |
| FacesContext.getCurrentInstance().getExternalContext() |
| .getRequestMap().put(key, value); |
| } |
| |
| /** |
| * Returns the value of a previous call to setKeepMessages() from this |
| * request. If there was no call yet, false is returned. |
| */ |
| @Override |
| public boolean isKeepMessages() |
| { |
| FacesContext facesContext = FacesContext.getCurrentInstance(); |
| ExternalContext externalContext = facesContext.getExternalContext(); |
| Map<String, Object> requestMap = externalContext.getRequestMap(); |
| Boolean keepMessages = (Boolean) requestMap.get(FLASH_KEEP_MESSAGES); |
| |
| return (keepMessages == null ? Boolean.FALSE : keepMessages); |
| } |
| |
| /** |
| * If this property is true, the messages should be kept for the next |
| * request, no matter if it is a normal postback case or a POST- |
| * REDIRECT-GET case. |
| * |
| * Note that we don't have to store this value for the next request |
| * (like setRedirect()), because we will know if it was true on the |
| * next request, if we can find any stored messages in the FlashMap. |
| * (also see _saveMessages() and _restoreMessages()). |
| */ |
| @Override |
| public void setKeepMessages(boolean keepMessages) |
| { |
| FacesContext facesContext = FacesContext.getCurrentInstance(); |
| ExternalContext externalContext = facesContext.getExternalContext(); |
| Map<String, Object> requestMap = externalContext.getRequestMap(); |
| requestMap.put(FLASH_KEEP_MESSAGES, keepMessages); |
| } |
| |
| // ~ Methods from Map interface ------------------------------------------- |
| |
| // NOTE that all these methods do not necessarily delegate to the same Map, |
| // because we differentiate between reading and writing operations. |
| |
| public void clear() |
| { |
| _checkFlashScopeDisabled(); |
| _getFlashMapForWriting().clear(); |
| } |
| |
| public boolean containsKey(Object key) |
| { |
| _checkFlashScopeDisabled(); |
| return _getFlashMapForReading().containsKey(key); |
| } |
| |
| public boolean containsValue(Object value) |
| { |
| _checkFlashScopeDisabled(); |
| return _getFlashMapForReading().containsValue(value); |
| } |
| |
| public Set<java.util.Map.Entry<String, Object>> entrySet() |
| { |
| _checkFlashScopeDisabled(); |
| return _getFlashMapForReading().entrySet(); |
| } |
| |
| public Object get(Object key) |
| { |
| _checkFlashScopeDisabled(); |
| if (key == null) |
| { |
| return null; |
| } |
| |
| if ("keepMessages".equals(key)) |
| { |
| return isKeepMessages(); |
| } |
| else if ("redirect".equals(key)) |
| { |
| return isRedirect(); |
| } |
| |
| return _getFlashMapForReading().get(key); |
| } |
| |
| public boolean isEmpty() |
| { |
| _checkFlashScopeDisabled(); |
| return _getFlashMapForReading().isEmpty(); |
| } |
| |
| public Set<String> keySet() |
| { |
| _checkFlashScopeDisabled(); |
| return _getFlashMapForReading().keySet(); |
| } |
| |
| public Object put(String key, Object value) |
| { |
| _checkFlashScopeDisabled(); |
| if (key == null) |
| { |
| return null; |
| } |
| |
| if ("keepMessages".equals(key)) |
| { |
| Boolean booleanValue = _convertToBoolean(value); |
| this.setKeepMessages(booleanValue); |
| return booleanValue; |
| } |
| else if ("redirect".equals(key)) |
| { |
| Boolean booleanValue = _convertToBoolean(value); |
| this.setRedirect(booleanValue); |
| return booleanValue; |
| } |
| else |
| { |
| Object resp = _getFlashMapForWriting().put(key, value); |
| |
| FacesContext facesContext = FacesContext.getCurrentInstance(); |
| facesContext.getApplication().publishEvent(facesContext, |
| PostPutFlashValueEvent.class, key); |
| |
| return resp; |
| } |
| } |
| |
| public void putAll(Map<? extends String, ? extends Object> m) |
| { |
| _checkFlashScopeDisabled(); |
| _getFlashMapForWriting().putAll(m); |
| } |
| |
| public Object remove(Object key) |
| { |
| _checkFlashScopeDisabled(); |
| |
| FacesContext facesContext = FacesContext.getCurrentInstance(); |
| facesContext.getApplication().publishEvent(facesContext, |
| PreRemoveFlashValueEvent.class, key); |
| |
| return _getFlashMapForWriting().remove(key); |
| } |
| |
| public int size() |
| { |
| _checkFlashScopeDisabled(); |
| return _getFlashMapForReading().size(); |
| } |
| |
| public Collection<Object> values() |
| { |
| _checkFlashScopeDisabled(); |
| return _getFlashMapForReading().values(); |
| } |
| |
| // ~ Implementation methods ----------------------------------------------- |
| |
| /** |
| * Returns true if the current phase is the last phase in the request |
| * and thus if doPostPhaseActions() is called for the last time. |
| * |
| * This will be true if either we are in phase 6 (render response) |
| * or if setRedirect(true) was called on this request and we are |
| * in phase 5 (invoke application). |
| */ |
| private boolean _isLastPhaseInRequest(FacesContext facesContext) |
| { |
| final PhaseId currentPhaseId = facesContext.getCurrentPhaseId(); |
| |
| boolean lastPhaseNormalRequest = PhaseId.RENDER_RESPONSE.equals(currentPhaseId); |
| // According to the spec, if there is a redirect, responseComplete() |
| // has been called, and Flash.setRedirect() has been called too, |
| // so we just need to check both are present. |
| boolean lastPhaseIfRedirect = facesContext.getResponseComplete() |
| && _isRedirectTrueOnThisRequest(facesContext); |
| |
| return lastPhaseNormalRequest || lastPhaseIfRedirect; |
| } |
| |
| /** |
| * Return true if setRedirect(true) was called on this request. |
| * @param facesContext |
| * @return |
| */ |
| private boolean _isRedirectTrueOnThisRequest(FacesContext facesContext) |
| { |
| ExternalContext externalContext = facesContext.getExternalContext(); |
| Map<String, Object> requestMap = externalContext.getRequestMap(); |
| Boolean redirect = (Boolean) requestMap.get(FLASH_REDIRECT); |
| |
| return Boolean.TRUE.equals(redirect); |
| } |
| |
| /** |
| * Return true if setRedirect(true) was called on the previous request. |
| * Precondition: doPrePhaseActions() must have been called on restore view phase. |
| * @param facesContext |
| * @return |
| */ |
| private boolean _isRedirectTrueOnPreviousRequest(FacesContext facesContext) |
| { |
| ExternalContext externalContext = facesContext.getExternalContext(); |
| Map<String, Object> requestMap = externalContext.getRequestMap(); |
| Boolean redirect = (Boolean) requestMap.get(FLASH_PREVIOUS_REQUEST_REDIRECT); |
| |
| return Boolean.TRUE.equals(redirect); |
| } |
| |
| /** |
| * Saves the value of setRedirect() for the next request, if it was true |
| */ |
| private void _saveRedirectValue(FacesContext facesContext) |
| { |
| ExternalContext externalContext = facesContext.getExternalContext(); |
| |
| // This request contains a redirect. This condition is in general |
| // triggered by a NavigationHandler. After a redirect all request scope |
| // values get lost, so in order to preserve this value we need to |
| // pass it between request. One strategy is use a cookie that is never sent |
| // to the client. Other alternative is use the session map. |
| // See _restoreRedirectValue() for restoring this value. |
| HttpServletResponse httpResponse = ExternalContextUtils |
| .getHttpServletResponse(externalContext); |
| if (httpResponse != null) |
| { |
| Cookie cookie = _createFlashCookie(FLASH_REDIRECT, "true", externalContext); |
| httpResponse.addCookie(cookie); |
| } |
| else |
| { |
| externalContext.getSessionMap().put(FLASH_REDIRECT, true); |
| } |
| } |
| |
| /** |
| * Restores the redirect value of the previous request and saves |
| * it in the RequestMap under the key FLASH_PREVIOUS_REQUEST_REDIRECT. |
| * Must not be called more than once per request. |
| * After this method was invoked, the requestMap will contain Boolean.TRUE |
| * if setRedirect(true) was called on the previous request or Boolean.FALSE |
| * or null otherwise. |
| */ |
| private void _restoreRedirectValue(FacesContext facesContext) |
| { |
| ExternalContext externalContext = facesContext.getExternalContext(); |
| HttpServletResponse httpResponse = ExternalContextUtils |
| .getHttpServletResponse(externalContext); |
| if (httpResponse != null) |
| { |
| // Request values are lost after a redirect. We can create a |
| // temporal cookie to pass the params between redirect calls. |
| // It is better than use HttpSession object, because this cookie |
| // is never sent by the server. |
| Cookie cookie = (Cookie) externalContext |
| .getRequestCookieMap().get(FLASH_REDIRECT); |
| if (cookie != null) |
| { |
| // the cookie exists means there was a redirect, regardless of the value |
| externalContext.getRequestMap().put( |
| FLASH_PREVIOUS_REQUEST_REDIRECT, Boolean.TRUE); |
| |
| // A redirect happened, so it is safe to remove the cookie, setting |
| // the maxAge to 0 seconds. The effect is we passed FLASH_REDIRECT param |
| // to this request object |
| cookie.setMaxAge(0); |
| cookie.setPath(_getCookiePath(externalContext)); |
| //MYFACES-3354 jetty 6.1.5 does not allow this, |
| //call setMaxAge(0) is enough |
| //cookie.setValue(null); |
| httpResponse.addCookie(cookie); |
| } |
| } |
| else |
| { |
| // Note that on portlet world we can't create cookies, |
| // so we are forced to use the session map. Anyway, |
| // according to the Bridge implementation(for example see |
| // org.apache.myfaces.portlet.faces.bridge.BridgeImpl) |
| // session object is created at start faces request |
| Map<String, Object> sessionMap = externalContext.getSessionMap(); |
| |
| // remove the value from the sessionMap |
| Boolean redirect = (Boolean) sessionMap.remove(FLASH_REDIRECT); |
| |
| // put the value into the requestMap |
| externalContext.getRequestMap().put( |
| FLASH_PREVIOUS_REQUEST_REDIRECT, redirect); |
| } |
| } |
| |
| /** |
| * Saves the current FacesMessages as a List on the render FlashMap for the |
| * next request if isKeepMessages() is true. Otherwise it removes any |
| * existing FacesMessages-List from the renderFlashMap. |
| * @param facesContext |
| */ |
| private void _saveMessages(FacesContext facesContext) |
| { |
| if (isKeepMessages()) |
| { |
| // get all messages from the FacesContext and store |
| // them on the renderMap |
| List<MessageEntry> messageList = null; |
| |
| Iterator<String> iterClientIds = facesContext.getClientIdsWithMessages(); |
| while (iterClientIds.hasNext()) |
| { |
| String clientId = (String) iterClientIds.next(); |
| Iterator<FacesMessage> iterMessages = facesContext.getMessages(clientId); |
| |
| while (iterMessages.hasNext()) |
| { |
| FacesMessage message = iterMessages.next(); |
| |
| if (messageList == null) |
| { |
| messageList = new ArrayList<MessageEntry>(); |
| } |
| messageList.add(new MessageEntry(clientId, message)); |
| } |
| } |
| |
| _getRenderFlashMap(facesContext).put(FLASH_KEEP_MESSAGES_LIST, messageList); |
| } |
| else |
| { |
| // do not keep messages --> remove messagesList from renderMap |
| _getRenderFlashMap(facesContext).remove(FLASH_KEEP_MESSAGES_LIST); |
| } |
| } |
| |
| /** |
| * Restore any saved FacesMessages from the previous request. |
| * Note that we don't need to save the keepMessages value for this request, |
| * because we just have to check if the value for FLASH_KEEP_MESSAGES_LIST exists. |
| * @param facesContext |
| */ |
| @SuppressWarnings("unchecked") |
| private void _restoreMessages(FacesContext facesContext) |
| { |
| List<MessageEntry> messageList = (List<MessageEntry>) |
| _getExecuteFlashMap(facesContext).get(FLASH_KEEP_MESSAGES_LIST); |
| |
| if (messageList != null) |
| { |
| Iterator<MessageEntry> iterMessages = messageList.iterator(); |
| |
| while (iterMessages.hasNext()) |
| { |
| MessageEntry entry = iterMessages.next(); |
| facesContext.addMessage(entry.clientId, entry.message); |
| } |
| |
| // we can now remove the messagesList from the flashMap |
| _getExecuteFlashMap(facesContext).remove(FLASH_KEEP_MESSAGES_LIST); |
| } |
| } |
| |
| /** |
| * Take the render map key and store it as a key for the next request. |
| * |
| * On the next request we can get it with _getRenderFlashMapTokenFromPreviousRequest(). |
| * @param externalContext |
| */ |
| private void _saveRenderFlashMapTokenForNextRequest(FacesContext facesContext) |
| { |
| ExternalContext externalContext = facesContext.getExternalContext(); |
| String tokenValue = (String) externalContext.getRequestMap().get(FLASH_RENDER_MAP_TOKEN); |
| ClientWindow clientWindow = externalContext.getClientWindow(); |
| if (clientWindow != null) |
| { |
| if (facesContext.getApplication().getStateManager().isSavingStateInClient(facesContext)) |
| { |
| //Use HttpSession or PortletSession object |
| Map<String, Object> sessionMap = externalContext.getSessionMap(); |
| sessionMap.put(FLASH_RENDER_MAP_TOKEN+SEPARATOR_CHAR+clientWindow.getId(), tokenValue); |
| } |
| else |
| { |
| FlashClientWindowTokenCollection lruMap = getFlashClientWindowTokenCollection(externalContext, true); |
| lruMap.put(clientWindow.getId(), tokenValue); |
| } |
| } |
| else |
| { |
| HttpServletResponse httpResponse = ExternalContextUtils.getHttpServletResponse(externalContext); |
| if (httpResponse != null) |
| { |
| Cookie cookie = _createFlashCookie(FLASH_RENDER_MAP_TOKEN, tokenValue, externalContext); |
| httpResponse.addCookie(cookie); |
| } |
| else |
| { |
| //Use HttpSession or PortletSession object |
| Map<String, Object> sessionMap = externalContext.getSessionMap(); |
| sessionMap.put(FLASH_RENDER_MAP_TOKEN, tokenValue); |
| } |
| } |
| } |
| |
| /** |
| * Retrieve the map token of the render map from the previous request. |
| * |
| * Returns the value of _saveRenderFlashMapTokenForNextRequest() from |
| * the previous request. |
| * @param externalContext |
| * @return |
| */ |
| private String _getRenderFlashMapTokenFromPreviousRequest(FacesContext facesContext) |
| { |
| ExternalContext externalContext = facesContext.getExternalContext(); |
| String tokenValue = null; |
| ClientWindow clientWindow = externalContext.getClientWindow(); |
| if (clientWindow != null) |
| { |
| if (facesContext.getApplication().getStateManager().isSavingStateInClient(facesContext)) |
| { |
| Map<String, Object> sessionMap = externalContext.getSessionMap(); |
| tokenValue = (String) sessionMap.get(FLASH_RENDER_MAP_TOKEN+ |
| SEPARATOR_CHAR+clientWindow.getId()); |
| } |
| else |
| { |
| FlashClientWindowTokenCollection lruMap = getFlashClientWindowTokenCollection(externalContext, false); |
| if (lruMap != null) |
| { |
| tokenValue = (String) lruMap.get(clientWindow.getId()); |
| } |
| } |
| } |
| else |
| { |
| HttpServletResponse httpResponse = ExternalContextUtils.getHttpServletResponse(externalContext); |
| if (httpResponse != null) |
| { |
| //Use a cookie |
| Cookie cookie = (Cookie) externalContext.getRequestCookieMap().get(FLASH_RENDER_MAP_TOKEN); |
| if (cookie != null) |
| { |
| tokenValue = cookie.getValue(); |
| } |
| } |
| else |
| { |
| //Use HttpSession or PortletSession object |
| Map<String, Object> sessionMap = externalContext.getSessionMap(); |
| tokenValue = (String) sessionMap.get(FLASH_RENDER_MAP_TOKEN); |
| } |
| } |
| return tokenValue; |
| } |
| |
| /** |
| * Restores the render FlashMap token from the previous request. |
| * This is the token of the executeMap for this request. |
| * Furthermore it also creates a new token for this request's renderMap |
| * (and thus implicitly a new renderMap). |
| * @param facesContext |
| */ |
| private void _manageFlashMapTokens(FacesContext facesContext) |
| { |
| ExternalContext externalContext = facesContext.getExternalContext(); |
| Map<String, Object> requestMap = externalContext.getRequestMap(); |
| |
| final String previousRenderToken |
| = _getRenderFlashMapTokenFromPreviousRequest(facesContext); |
| if (previousRenderToken != null) |
| { |
| // "restore" the renderMap from the previous request |
| // and put it as the executeMap for this request |
| requestMap.put(FLASH_EXECUTE_MAP_TOKEN, previousRenderToken); |
| } |
| else |
| { |
| if (facesContext.isPostback()) |
| { |
| if (facesContext.getExternalContext().getClientWindow() == null) |
| { |
| // on a postback, we should always have a previousToken |
| log.warning("Identifier for execute FlashMap was lost on " + |
| "the postback, thus FlashScope information is gone."); |
| } |
| else |
| { |
| // Next token was not preserved in session, which means flash map |
| // is empty. Create a new token and store it as execute map, which |
| // will be empty. |
| requestMap.put(FLASH_EXECUTE_MAP_TOKEN, _getNextToken()); |
| } |
| } |
| |
| // create a new token (and thus a new Map) for this request's |
| // executeMap so that we have an executeMap in any possible case. |
| final String newExecuteToken = _getNextToken(); |
| requestMap.put(FLASH_EXECUTE_MAP_TOKEN, newExecuteToken); |
| } |
| |
| // create a new token (and thus a new Map) for this request's renderMap |
| final String newRenderToken = _getNextToken(); |
| requestMap.put(FLASH_RENDER_MAP_TOKEN, newRenderToken); |
| |
| // we now have the final render token for this request, thus we can |
| // already save it for the next request, because it won't change |
| _saveRenderFlashMapTokenForNextRequest(facesContext); |
| } |
| |
| /** |
| * Get the next token to be assigned to this request |
| * |
| * @return |
| */ |
| private String _getNextToken() |
| { |
| // atomically increment the value |
| long nextToken = _count.incrementAndGet(); |
| |
| // convert using base 36 because it is a fast efficient subset of base-64 |
| return Long.toString(nextToken, 36); |
| } |
| |
| /** |
| * Create a new subkey-wrapper of the session map with the given prefix. |
| * This wrapper is used to implement the maps for the flash scope. |
| * For more information see the SubKeyMap doc. |
| */ |
| private Map<String, Object> _createSubKeyMap(FacesContext context, String prefix) |
| { |
| ExternalContext external = context.getExternalContext(); |
| Map<String, Object> sessionMap = external.getSessionMap(); |
| |
| return new SubKeyMap<Object>(sessionMap, prefix); |
| } |
| |
| /** |
| * Return the flash map created on this traversal. |
| * |
| * This FlashMap will be the execute FlashMap of the next traversal. |
| * |
| * Note that it is supposed that FLASH_RENDER_MAP_TOKEN is initialized |
| * before restore view phase (see doPrePhaseActions() for details). |
| * |
| * @param context |
| * @return |
| */ |
| @SuppressWarnings("unchecked") |
| private Map<String, Object> _getRenderFlashMap(FacesContext context) |
| { |
| // Note that we don't have to synchronize here, because it is no problem |
| // if we create more SubKeyMaps with the same subkey, because they are |
| // totally equal and point to the same entries in the SessionMap. |
| |
| Map<String, Object> requestMap = context.getExternalContext().getRequestMap(); |
| Map<String, Object> map = (Map<String, Object>) requestMap.get(FLASH_RENDER_MAP); |
| if (map == null) |
| { |
| String token = (String) requestMap.get(FLASH_RENDER_MAP_TOKEN); |
| String fullToken = FLASH_SESSION_MAP_SUBKEY_PREFIX + SEPARATOR_CHAR + token + SEPARATOR_CHAR; |
| map = _createSubKeyMap(context, fullToken); |
| requestMap.put(FLASH_RENDER_MAP, map); |
| } |
| return map; |
| } |
| |
| /** |
| * Return the execute Flash Map. |
| * |
| * This FlashMap was the render FlashMap of the previous traversal. |
| * |
| * @param context |
| * @return |
| */ |
| @SuppressWarnings("unchecked") |
| private Map<String, Object> _getExecuteFlashMap(FacesContext context) |
| { |
| // Note that we don't have to synchronize here, because it is no problem |
| // if we create more SubKeyMaps with the same subkey, because they are |
| // totally equal and point to the same entries in the SessionMap. |
| |
| Map<String, Object> requestMap = context.getExternalContext().getRequestMap(); |
| Map<String, Object> map = (Map<String, Object>) requestMap.get(FLASH_EXECUTE_MAP); |
| if (map == null) |
| { |
| String token = (String) requestMap.get(FLASH_EXECUTE_MAP_TOKEN); |
| String fullToken = FLASH_SESSION_MAP_SUBKEY_PREFIX + SEPARATOR_CHAR + token + SEPARATOR_CHAR; |
| map = _createSubKeyMap(context, fullToken); |
| requestMap.put(FLASH_EXECUTE_MAP, map); |
| } |
| return map; |
| } |
| |
| /** |
| * Get the proper map according to the current phase: |
| * |
| * Normal case: |
| * |
| * - First request, restore view phase (create a new one): render map n |
| * - First request, execute phase: Skipped |
| * - First request, render phase: render map n |
| * |
| * Render map n saved and put as execute map n |
| * |
| * - Second request, execute phase: execute map n |
| * - Second request, render phase: render map n+1 |
| * |
| * Post Redirect Get case: Redirect is triggered by a call to setRedirect(true) from NavigationHandler |
| * or earlier using c:set tag. |
| * |
| * - First request, restore view phase (create a new one): render map n |
| * - First request, execute phase: Skipped |
| * - First request, render phase: render map n |
| * |
| * Render map n saved and put as execute map n |
| * |
| * POST |
| * |
| * - Second request, execute phase: execute map n |
| * Note that render map n+1 is also created here to perform keep(). |
| * |
| * REDIRECT |
| * |
| * - NavigationHandler do the redirect, requestMap data lost, called Flash.setRedirect(true) |
| * |
| * Render map n+1 saved and put as render map n+1 on GET request. |
| * |
| * GET |
| * |
| * - Third request, restore view phase (create a new one): render map n+1 (restorred) |
| * (isRedirect() should return true as javadoc says) |
| * - Third request, execute phase: skipped |
| * - Third request, render phase: render map n+1 |
| * |
| * In this way proper behavior is preserved even in the case of redirect, since the GET part is handled as |
| * the "render" part of the current traversal, keeping the semantic of flash object. |
| * |
| * @return |
| */ |
| private Map<String, Object> _getActiveFlashMap() |
| { |
| FacesContext facesContext = FacesContext.getCurrentInstance(); |
| if (PhaseId.RENDER_RESPONSE.equals(facesContext.getCurrentPhaseId()) |
| || !facesContext.isPostback()) |
| { |
| return _getRenderFlashMap(facesContext); |
| } |
| else |
| { |
| return _getExecuteFlashMap(facesContext); |
| } |
| } |
| |
| /** |
| * Returns the FlashMap used in the reading methods of java.util.Map |
| * like e.g. get() or values(). |
| * @return |
| */ |
| private Map<String, Object> _getFlashMapForReading() |
| { |
| return _getExecuteFlashMap(FacesContext.getCurrentInstance()); |
| } |
| |
| /** |
| * Returns the FlashMap used in the writing methods of java.util.Map |
| * like e.g. put() or clear(). |
| * @return |
| */ |
| private Map<String, Object> _getFlashMapForWriting() |
| { |
| return _getActiveFlashMap(); |
| } |
| |
| /** |
| * Destroy the execute FlashMap, because it is not needed anymore. |
| * @param facesContext |
| */ |
| private void _clearExecuteFlashMap(FacesContext facesContext) |
| { |
| Map<String, Object> map = _getExecuteFlashMap(facesContext); |
| |
| //JSF 2.2 invoke PreClearFlashEvent |
| facesContext.getApplication().publishEvent(facesContext, |
| PreClearFlashEvent.class, map); |
| |
| // Clear everything - note that because of naming conventions, |
| // this will in fact automatically recurse through all children |
| // grandchildren etc. - which is kind of a design flaw of SubKeyMap, |
| // but one we're relying on |
| |
| // NOTE that we do not need a null check here, because there will |
| // always be an execute Map, however sometimes an empty one! |
| map.clear(); |
| } |
| |
| private void _clearRenderFlashTokenIfMapEmpty(FacesContext facesContext) |
| { |
| // Keep in mind that we cannot remove a cookie once the response has been sent, |
| // but we can remove an attribute from session anytime, so the idea is check |
| // if the map is empty or not and if that so, do not save the token. The effect |
| // is we can reduce the session size a bit. |
| ExternalContext externalContext = facesContext.getExternalContext(); |
| Object session = facesContext.getExternalContext().getSession(false); |
| ClientWindow clientWindow = externalContext.getClientWindow(); |
| if (session != null && clientWindow != null) |
| { |
| Map<String, Object> map = _getRenderFlashMap(facesContext); |
| if (map.isEmpty()) |
| { |
| if (facesContext.getApplication().getStateManager().isSavingStateInClient(facesContext)) |
| { |
| Map<String, Object> sessionMap = externalContext.getSessionMap(); |
| sessionMap.remove(FLASH_RENDER_MAP_TOKEN+SEPARATOR_CHAR+clientWindow.getId()); |
| } |
| else |
| { |
| // Remove token, because it is not necessary |
| FlashClientWindowTokenCollection lruMap = getFlashClientWindowTokenCollection( |
| externalContext, false); |
| if (lruMap != null) |
| { |
| lruMap.remove(clientWindow.getId()); |
| Map<String, Object> sessionMap = externalContext.getSessionMap(); |
| if (lruMap.isEmpty()) |
| { |
| sessionMap.remove(FLASH_CW_LRU_MAP); |
| } |
| else |
| { |
| //refresh remove |
| sessionMap.put(FLASH_CW_LRU_MAP, lruMap); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| protected FlashClientWindowTokenCollection getFlashClientWindowTokenCollection( |
| ExternalContext externalContext, boolean create) |
| { |
| Object session = externalContext.getSession(false); |
| if (session == null && !create) |
| { |
| return null; |
| } |
| Map<String, Object> sessionMap = externalContext.getSessionMap(); |
| FlashClientWindowTokenCollection lruMap = (FlashClientWindowTokenCollection) |
| sessionMap.get(FLASH_CW_LRU_MAP); |
| if (lruMap == null) |
| { |
| Integer ft = MyfacesConfig.getCurrentInstance(externalContext).getNumberOfFlashTokensInSession(); |
| lruMap = new FlashClientWindowTokenCollection(new ClientWindowFlashTokenLRUMap(ft)); |
| } |
| |
| if (create) |
| { |
| sessionMap.put(FLASH_CW_LRU_MAP, lruMap); |
| } |
| return lruMap; |
| } |
| |
| public void clearFlashMap(FacesContext facesContext, String clientWindowId, String token) |
| { |
| if ((!_flashScopeDisabled) && |
| (!facesContext.getApplication().getStateManager().isSavingStateInClient(facesContext))) |
| { |
| ExternalContext externalContext = facesContext.getExternalContext(); |
| ClientWindow clientWindow = externalContext.getClientWindow(); |
| if (clientWindow != null) |
| { |
| if (token != null) |
| { |
| String fullToken = FLASH_SESSION_MAP_SUBKEY_PREFIX + SEPARATOR_CHAR + token + SEPARATOR_CHAR; |
| Map<String, Object> map = _createSubKeyMap(facesContext, fullToken); |
| map.clear(); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Creates a Cookie with the given name and value. |
| * In addition, it will be configured with maxAge=-1, the current request path and secure value. |
| * |
| * @param name |
| * @param value |
| * @param externalContext |
| * @return |
| */ |
| private Cookie _createFlashCookie(String name, String value, ExternalContext externalContext) |
| { |
| Cookie cookie = new Cookie(name, value); |
| |
| cookie.setMaxAge(-1); |
| cookie.setPath(_getCookiePath(externalContext)); |
| cookie.setSecure(externalContext.isSecure()); |
| //cookie.setHttpOnly(true); |
| if (ServletSpecifications.isServlet30Available()) |
| { |
| _Servlet30Utils.setCookieHttpOnly(cookie, true); |
| } |
| return cookie; |
| } |
| |
| /** |
| * Returns the path for the Flash-Cookies. |
| * @param externalContext |
| * @return |
| */ |
| private String _getCookiePath(ExternalContext externalContext) |
| { |
| String contextPath = externalContext.getRequestContextPath(); |
| |
| if (contextPath == null || "".equals(contextPath)) |
| { |
| contextPath = "/"; |
| } |
| |
| return contextPath; |
| } |
| |
| /** |
| * Convert the Object to a Boolean. |
| * @param value |
| * @return |
| */ |
| private Boolean _convertToBoolean(Object value) |
| { |
| Boolean booleanValue; |
| if (value instanceof Boolean) |
| { |
| booleanValue = (Boolean) value; |
| } |
| else |
| { |
| booleanValue = Boolean.parseBoolean(value.toString()); |
| } |
| return booleanValue; |
| } |
| |
| /** |
| * Checks whether flash scope is disabled. |
| * @throws FlashScopeDisabledException if flash scope is disabled |
| */ |
| private void _checkFlashScopeDisabled() |
| { |
| if (_flashScopeDisabled) |
| { |
| throw new FlashScopeDisabledException("Flash scope was disabled by context param " |
| + MyfacesConfig.INIT_PARAM_FLASH_SCOPE_DISABLED + " but erroneously accessed"); |
| } |
| } |
| |
| // ~ Inner classes -------------------------------------------------------- |
| |
| /** |
| * Class used to store a FacesMessage with its clientId. |
| */ |
| private static class MessageEntry implements Serializable |
| { |
| private static final long serialVersionUID = -690264660230199234L; |
| private final String clientId; |
| private final FacesMessage message; |
| |
| public MessageEntry(String clientId, FacesMessage message) |
| { |
| this.clientId = clientId; |
| this.message = message; |
| } |
| } |
| |
| } |